Skip to content

Commit e1158d3

Browse files
RUBY-3370 Add CSOT to server selection and connection checkout (#2833)
1 parent 44c607f commit e1158d3

File tree

16 files changed

+257
-43
lines changed

16 files changed

+257
-43
lines changed

lib/mongo/client.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class Client
111111
:ssl_verify_certificate,
112112
:ssl_verify_hostname,
113113
:ssl_verify_ocsp_endpoint,
114+
:timeout_ms,
114115
:truncate_logs,
115116
:user,
116117
:wait_queue_timeout,
@@ -413,6 +414,8 @@ def hash
413414
# @option options [ true, false ] :ssl_verify_hostname Whether to perform peer hostname
414415
# validation. This setting overrides :ssl_verify with respect to whether hostname validation
415416
# is performed.
417+
# @option options [ Integer ] :timeout_ms The per-operation timeout in milliseconds.
418+
# Must a positive integer. The default value is unset which means infinite.
416419
# @option options [ true, false ] :truncate_logs Whether to truncate the
417420
# logs at the default 250 characters.
418421
# @option options [ String ] :user The user name.
@@ -1185,6 +1188,11 @@ def encrypted_fields_map
11851188
@encrypted_fields_map ||= @options.fetch(:auto_encryption_options, {})[:encrypted_fields_map]
11861189
end
11871190

1191+
# @api private
1192+
def timeout_ms
1193+
@options[:timeout_ms]
1194+
end
1195+
11881196
private
11891197

11901198
# Create a new encrypter object using the client's auto encryption options

lib/mongo/cluster.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -783,8 +783,13 @@ def has_writable_server?
783783
# @return [ Mongo::Server ] A primary server.
784784
#
785785
# @since 2.0.0
786-
def next_primary(ping = nil, session = nil)
787-
ServerSelector.primary.select_server(self, nil, session)
786+
def next_primary(ping = nil, session = nil, remaining_timeout_ms: nil)
787+
ServerSelector.primary.select_server(
788+
self,
789+
nil,
790+
session,
791+
remaining_timeout_ms: remaining_timeout_ms
792+
)
788793
end
789794

790795
# Get the connection pool for the server.

lib/mongo/collection.rb

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,10 @@ def create(opts = {})
401401
self.write_concern
402402
end
403403

404-
context = Operation::Context.new(client: client, session: session)
404+
context = Operation::Context.new(
405+
client: client,
406+
session: session
407+
)
405408
maybe_create_qe_collections(opts[:encrypted_fields], client, session) do |encrypted_fields|
406409
Operation::Create.new(
407410
selector: operation,
@@ -413,7 +416,10 @@ def create(opts = {})
413416
collation: options[:collation] || options['collation'],
414417
encrypted_fields: encrypted_fields,
415418
validator: options[:validator],
416-
).execute(next_primary(nil, session), context: context)
419+
).execute(
420+
next_primary(nil, session),
421+
context: context
422+
)
417423
end
418424
end
419425
end
@@ -801,7 +807,11 @@ def insert_one(document, opts = {})
801807
raise ArgumentError, "Document to be inserted cannot be nil"
802808
end
803809

804-
context = Operation::Context.new(client: client, session: session)
810+
context = Operation::Context.new(
811+
client: client,
812+
session: session,
813+
timeout_ms: timeout_ms(opts)
814+
)
805815
write_with_retry(write_concern, context: context) do |connection, txn_num, context|
806816
Operation::Insert.new(
807817
:documents => [ document ],
@@ -1152,5 +1162,13 @@ def namespace
11521162
def system_collection?
11531163
name.start_with?('system.')
11541164
end
1165+
1166+
def timeout_ms(opts = {})
1167+
if opts.key?(:timeout_ms)
1168+
opts.delete(:timeout_ms)
1169+
else
1170+
options.fetch(:timeout_ms) { database.timeout_ms }
1171+
end
1172+
end
11551173
end
11561174
end

lib/mongo/collection/view.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,11 @@ class View
6363
:client,
6464
:cluster,
6565
:database,
66+
:nro_write_with_retry,
6667
:read_with_retry,
6768
:read_with_retry_cursor,
69+
:timeout_ms,
6870
:write_with_retry,
69-
:nro_write_with_retry,
7071
:write_concern_with_session
7172

7273
# Delegate to the cluster for the next primary.

lib/mongo/database.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,5 +497,9 @@ def self.create(client)
497497
database = Database.new(client, client.options[:database], client.options)
498498
client.instance_variable_set(:@database, database)
499499
end
500+
501+
def timeout_ms
502+
options.fetch(:timeout_ms) { client.timeout_ms }
503+
end
500504
end
501505
end

lib/mongo/operation/context.rb

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ module Operation
3535
#
3636
# @api private
3737
class Context
38-
def initialize(client: nil, session: nil, connection_global_id: nil, options: nil)
38+
def initialize(
39+
client: nil,
40+
session: nil,
41+
connection_global_id: nil,
42+
timeout_ms: nil,
43+
options: nil
44+
)
3945
if options
4046
if client
4147
raise ArgumentError, 'Client and options cannot both be specified'
@@ -50,14 +56,25 @@ def initialize(client: nil, session: nil, connection_global_id: nil, options: ni
5056
raise ArgumentError, 'Trying to pin context to a connection when the session is already pinned to a connection.'
5157
end
5258

59+
if timeout_ms && timeout_ms < 0
60+
raise ArgumentError, 'timeout_ms must be a positive integer'
61+
end
62+
5363
@client = client
5464
@session = session
5565
@connection_global_id = connection_global_id
66+
@timeout_ms = timeout_ms
67+
@deadline = if @timeout_ms && @timeout_ms > 0
68+
Utils.monotonic_time + (@timeout_ms / 1_000.0)
69+
else
70+
nil
71+
end
5672
@options = options
5773
end
5874

5975
attr_reader :client
6076
attr_reader :session
77+
attr_reader :deadline
6178
attr_reader :options
6279

6380
def connection_global_id
@@ -133,6 +150,24 @@ def encrypter
133150
raise Error::InternalDriverError, 'Encrypter should only be accessed when encryption is to be performed'
134151
end
135152
end
153+
154+
def remaining_timeout_sec
155+
return nil if @timeout_ms.nil? || @timeout_ms == 0
156+
157+
remaining_seconds = deadline - Utils.monotonic_time
158+
if remaining_seconds <= 0
159+
0
160+
else
161+
remaining_seconds
162+
end
163+
end
164+
165+
def remaining_timeout_ms
166+
seconds = remaining_timeout_sec
167+
return nil if seconds.nil?
168+
169+
(seconds * 1_000).to_i
170+
end
136171
end
137172
end
138173
end

lib/mongo/operation/shared/op_msg_executable.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ module OpMsgExecutable
3232
#
3333
# @return [ Mongo::Operation::Result ] The operation result.
3434
def execute(server, context:, options: {})
35-
server.with_connection(connection_global_id: context.connection_global_id) do |connection|
35+
server.with_connection(
36+
connection_global_id: context.connection_global_id,
37+
context: context
38+
) do |connection|
3639
execute_with_connection(connection, context: context, options: options)
3740
end
3841
end

lib/mongo/operation/shared/write.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ module Write
3535
#
3636
# @since 2.5.2
3737
def execute(server, context:)
38-
server.with_connection(connection_global_id: context.connection_global_id) do |connection|
38+
server.with_connection(
39+
connection_global_id: context.connection_global_id,
40+
context: context
41+
) do |connection|
3942
execute_with_connection(connection, context: context)
4043
end
4144
end

lib/mongo/retryable.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,14 @@ module Retryable
4646
# @api private
4747
#
4848
# @return [ Mongo::Server ] A server matching the server preference.
49-
def select_server(cluster, server_selector, session, failed_server = nil)
50-
server_selector.select_server(cluster, nil, session, deprioritized: [failed_server].compact)
49+
def select_server(cluster, server_selector, session, failed_server = nil, remaining_timeout_ms: nil)
50+
server_selector.select_server(
51+
cluster,
52+
nil,
53+
session,
54+
deprioritized: [failed_server].compact,
55+
remaining_timeout_ms: remaining_timeout_ms
56+
)
5157
end
5258

5359
# Returns the read worker for handling retryable reads.

lib/mongo/retryable/write_worker.rb

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ def write_with_retry(write_concern, ending_transaction: false, context:, &block)
7474
# If we are here, session is not nil. A session being nil would have
7575
# failed retry_write_allowed? check.
7676

77-
server = select_server(cluster, ServerSelector.primary, session)
77+
server = select_server(
78+
cluster, ServerSelector.primary,
79+
session,
80+
remaining_timeout_ms: context.remaining_timeout_ms
81+
)
7882

7983
unless ending_transaction || server.retry_writes?
8084
return legacy_write_with_retry(server, context: context, &block)
@@ -177,8 +181,16 @@ def legacy_write_with_retry(server = nil, context:)
177181
attempt = 0
178182
begin
179183
attempt += 1
180-
server ||= select_server(cluster, ServerSelector.primary, session)
181-
server.with_connection(connection_global_id: context.connection_global_id) do |connection|
184+
server ||= select_server(
185+
cluster,
186+
ServerSelector.primary,
187+
session,
188+
remaining_timeout_ms: context.remaining_timeout_ms
189+
)
190+
server.with_connection(
191+
connection_global_id: context.connection_global_id,
192+
context: context
193+
) do |connection|
182194
# Legacy retries do not use txn_num
183195
yield connection, nil, context.dup
184196
end
@@ -220,7 +232,10 @@ def modern_write_with_retry(session, server, context, &block)
220232
txn_num = nil
221233
connection_succeeded = false
222234

223-
server.with_connection(connection_global_id: context.connection_global_id) do |connection|
235+
server.with_connection(
236+
connection_global_id: context.connection_global_id,
237+
context: context
238+
) do |connection|
224239
connection_succeeded = true
225240

226241
session.materialize_if_needed
@@ -263,7 +278,13 @@ def retry_write(original_error, txn_num, context:, failed_server: nil, &block)
263278
# server description and/or topology as necessary (specifically,
264279
# a socket error or a not master error should have marked the respective
265280
# server unknown). Here we just need to wait for server selection.
266-
server = select_server(cluster, ServerSelector.primary, session, failed_server)
281+
server = select_server(
282+
cluster,
283+
ServerSelector.primary,
284+
session,
285+
failed_server,
286+
remaining_timeout_ms: context.remaining_timeout_ms
287+
)
267288

268289
unless server.retry_writes?
269290
# Do not need to add "modern retry" here, it should already be on

0 commit comments

Comments
 (0)