Skip to content

Commit 26b68da

Browse files
RUBY-3683 Fix CSOT for with_transaction
1 parent 17f8c4a commit 26b68da

File tree

2 files changed

+97
-17
lines changed

2 files changed

+97
-17
lines changed

lib/mongo/session.rb

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -446,20 +446,14 @@ def end_session
446446
#
447447
# @since 2.7.0
448448
def with_transaction(options = nil)
449-
if timeout_ms = (options || {})[:timeout_ms]
450-
timeout_sec = timeout_ms / 1_000.0
451-
deadline = Utils.monotonic_time + timeout_sec
452-
@with_transaction_deadline = deadline
453-
elsif default_timeout_ms = @options[:default_timeout_ms]
454-
timeout_sec = default_timeout_ms / 1_000.0
455-
deadline = Utils.monotonic_time + timeout_sec
456-
@with_transaction_deadline = deadline
457-
elsif @client.timeout_sec
458-
deadline = Utils.monotonic_time + @client.timeout_sec
459-
@with_transaction_deadline = deadline
460-
else
461-
deadline = Utils.monotonic_time + 120
462-
end
449+
@with_transaction_deadline = calculate_with_transaction_deadline(options)
450+
deadline = if @with_transaction_deadline
451+
# CSOT enabled, so we have a customer defined deadline.
452+
@with_transaction_deadline
453+
else
454+
# CSOT not enabled, so we use the default deadline, 120 seconds.
455+
Utils.monotonic_time + 120
456+
end
463457
transaction_in_progress = false
464458
loop do
465459
commit_options = {}
@@ -478,7 +472,7 @@ def with_transaction(options = nil)
478472
transaction_in_progress = false
479473
end
480474

481-
if Utils.monotonic_time >= deadline
475+
if deadline_expired?(deadline)
482476
transaction_in_progress = false
483477
raise
484478
end
@@ -500,7 +494,7 @@ def with_transaction(options = nil)
500494
return rv
501495
rescue Mongo::Error => e
502496
if e.label?('UnknownTransactionCommitResult')
503-
if Utils.monotonic_time >= deadline ||
497+
if deadline_expired?(deadline) ||
504498
e.is_a?(Error::OperationFailure::Family) && e.max_time_ms_expired?
505499
then
506500
transaction_in_progress = false
@@ -1191,6 +1185,8 @@ def txn_num
11911185
# @api private
11921186
attr_accessor :snapshot_timestamp
11931187

1188+
# @return [ Integer | nil ] The deadline for the current transaction, if any.
1189+
# @api private
11941190
attr_reader :with_transaction_deadline
11951191

11961192
private
@@ -1286,5 +1282,30 @@ def operation_timeouts(opts)
12861282
end
12871283
end
12881284
end
1285+
1286+
def calculate_with_transaction_deadline(opts)
1287+
calc = -> (timeout) {
1288+
if timeout == 0
1289+
0
1290+
else
1291+
Utils.monotonic_time + (timeout / 1000.0)
1292+
end
1293+
}
1294+
if timeout_ms = opts&.dig(:timeout_ms)
1295+
calc.call(timeout_ms)
1296+
elsif default_timeout_ms = @options[:default_timeout_ms]
1297+
calc.call(default_timeout_ms)
1298+
elsif @client.timeout_ms
1299+
calc.call(@client.timeout_ms)
1300+
end
1301+
end
1302+
1303+
def deadline_expired?(deadline)
1304+
if deadline.zero?
1305+
false
1306+
else
1307+
Utils.monotonic_time >= deadline
1308+
end
1309+
end
12891310
end
12901311
end

spec/mongo/session_transaction_spec.rb

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,18 @@ class SessionTransactionSpecError < StandardError; end
1010
min_server_fcv '4.0'
1111
require_topology :replica_set, :sharded
1212

13+
let(:subscriber) do
14+
Mrss::EventSubscriber.new(name: 'SessionTransactionSpec')
15+
end
16+
17+
let(:client) do
18+
authorized_client.tap do |client|
19+
client.subscribe(Mongo::Monitoring::COMMAND, subscriber)
20+
end
21+
end
22+
1323
let(:session) do
14-
authorized_client.start_session(session_options)
24+
client.start_session(session_options)
1525
end
1626

1727
let(:session_options) do
@@ -218,5 +228,54 @@ class SessionTransactionSpecError < StandardError; end
218228
expect(collection.find(timeout_around_with_tx: 2).first).not_to be nil
219229
end
220230
end
231+
232+
context 'csot' do
233+
context 'when csot is enabled' do
234+
context 'when timeout_ms is set to zero' do
235+
it 'sets with_transaction_deadline to infinite' do
236+
session.with_transaction(timeout_ms: 0) do
237+
expect(session.with_transaction_deadline).to be_zero
238+
end
239+
end
240+
241+
it 'does not sent maxTimeMS' do
242+
session.with_transaction(timeout_ms: 0) do
243+
collection.insert_one({ a: 1 }, session: session)
244+
end
245+
event = subscriber.single_command_started_event('insert', database_name: collection.database.name)
246+
expect(event.command['maxTimeMS']).to be_nil
247+
end
248+
end
249+
250+
context 'when timeout_ms is set to a positive value' do
251+
before do
252+
allow(Mongo::Utils).to receive(:monotonic_time).and_return(0)
253+
end
254+
255+
it 'sets with_transaction_deadline to the specified value' do
256+
session.with_transaction(timeout_ms: 1000) do
257+
expect(session.with_transaction_deadline).to be_within(0.1).of(1000 / 1000.0)
258+
end
259+
end
260+
261+
it 'sends maxTimeMS with the operation' do
262+
session.with_transaction(timeout_ms: 1000) do
263+
collection.insert_one({ a: 1 }, session: session)
264+
end
265+
event = subscriber.single_command_started_event('insert', database_name: collection.database.name)
266+
expect(event.command['maxTimeMS']).not_to be_nil
267+
expect(event.command['maxTimeMS']).to be_within(100).of(1000)
268+
end
269+
end
270+
end
271+
272+
context 'when csot is disabled' do
273+
it 'does not set with_transaction_deadline' do
274+
session.with_transaction do
275+
expect(session.with_transaction_deadline).to be_nil
276+
end
277+
end
278+
end
279+
end
221280
end
222281
end

0 commit comments

Comments
 (0)