@@ -106,6 +106,8 @@ def initialize(server_session, client, options = {})
106106 @cluster_time = nil
107107 @state = NO_TRANSACTION_STATE
108108 @with_transaction_deadline = nil
109+ @with_transaction_timeout_ms = nil
110+ @inside_with_transaction = false
109111 end
110112
111113 # @return [ Hash ] The options for this session.
@@ -452,6 +454,8 @@ def end_session
452454 #
453455 # @since 2.7.0
454456 def with_transaction ( options = nil )
457+ @inside_with_transaction = true
458+ @with_transaction_timeout_ms = options &.dig ( :timeout_ms ) || @options [ :default_timeout_ms ] || @client . timeout_ms
455459 @with_transaction_deadline = calculate_with_transaction_deadline ( options )
456460 deadline = if @with_transaction_deadline
457461 # CSOT enabled, so we have a customer defined deadline.
@@ -471,7 +475,7 @@ def with_transaction(options = nil)
471475 if overload_encountered
472476 delay = @client . retry_policy . backoff_delay ( overload_error_count )
473477 if backoff_would_exceed_deadline? ( deadline , delay )
474- raise ( last_error )
478+ raise Mongo :: Error :: TimeoutError , 'CSOT timeout expired waiting to retry withTransaction'
475479 end
476480 unless @client . retry_policy . should_retry_overload? ( overload_error_count , delay )
477481 raise ( last_error )
@@ -480,7 +484,7 @@ def with_transaction(options = nil)
480484 else
481485 backoff = backoff_seconds_for_retry ( transaction_attempt )
482486 if backoff_would_exceed_deadline? ( deadline , backoff )
483- raise ( last_error )
487+ raise Mongo :: Error :: TimeoutError , 'CSOT timeout expired waiting to retry withTransaction'
484488 end
485489 sleep ( backoff )
486490 end
@@ -499,7 +503,10 @@ def with_transaction(options = nil)
499503 rescue Exception => e
500504 if within_states? ( STARTING_TRANSACTION_STATE , TRANSACTION_IN_PROGRESS_STATE )
501505 log_warn ( "Aborting transaction due to #{ e . class } : #{ e } " )
502- @with_transaction_deadline = nil
506+ # CSOT: if the deadline is already expired, clear it so that
507+ # abort_transaction uses a fresh timeout (not the expired deadline).
508+ # If the deadline is not yet expired, keep it so abort uses remaining time.
509+ @with_transaction_deadline = nil if @with_transaction_deadline && deadline_expired? ( deadline )
503510 abort_transaction
504511 transaction_in_progress = false
505512 end
@@ -528,6 +535,15 @@ def with_transaction(options = nil)
528535 return rv
529536 end
530537
538+ # CSOT: if the timeout has expired before we can commit, abort the
539+ # transaction instead and raise a client-side timeout error.
540+ if @with_transaction_deadline && deadline_expired? ( deadline )
541+ transaction_in_progress = false
542+ @with_transaction_deadline = nil
543+ abort_transaction
544+ raise Mongo ::Error ::TimeoutError , 'CSOT timeout expired before transaction could be committed'
545+ end
546+
531547 begin
532548 commit_transaction ( commit_options )
533549 transaction_in_progress = false
@@ -610,6 +626,8 @@ def with_transaction(options = nil)
610626 end
611627 end
612628 @with_transaction_deadline = nil
629+ @with_transaction_timeout_ms = nil
630+ @inside_with_transaction = false
613631 end
614632
615633 # Places subsequent operations in this session into a new transaction.
@@ -1282,6 +1300,12 @@ def txn_num
12821300 # @api private
12831301 attr_reader :with_transaction_deadline
12841302
1303+ # @return [ Boolean ] Whether we are currently inside a with_transaction block.
1304+ # @api private
1305+ def inside_with_transaction?
1306+ @inside_with_transaction
1307+ end
1308+
12851309 private
12861310
12871311 # Get the read concern the session will use when starting a transaction.
@@ -1355,9 +1379,14 @@ def check_transactions_supported!
13551379
13561380 def operation_timeouts ( opts )
13571381 {
1358- inherited_timeout_ms : @client . timeout_ms
1382+ inherited_timeout_ms : @with_transaction_timeout_ms || @ client. timeout_ms
13591383 } . tap do |result |
1360- if @with_transaction_deadline . nil?
1384+ if @inside_with_transaction
1385+ if opts [ :timeout_ms ]
1386+ raise Mongo ::Error ::InvalidTransactionOperation ,
1387+ 'timeoutMS cannot be overridden inside a withTransaction callback'
1388+ end
1389+ else
13611390 if timeout_ms = opts [ :timeout_ms ]
13621391 result [ :operation_timeout_ms ] = timeout_ms
13631392 elsif default_timeout_ms = options [ :default_timeout_ms ]
0 commit comments