Class: Mongo::Session
| Relationships & Source Files | |
| Namespace Children | |
|
Classes:
| |
| Super Chains via Extension / Inclusion / Inheritance | |
|
Class Chain:
self,
Forwardable
|
|
|
Instance Chain:
|
|
| Inherits: | Object |
| Defined in: | lib/mongo/session.rb, lib/mongo/session/server_session.rb, lib/mongo/session/session_pool.rb, lib/mongo/session/server_session/dirtyable.rb |
Overview
Session objects are not thread-safe. An application may use a session
from only one thread or process at a time.
A logical session representing a set of sequential operations executed by an application that are related in some way.
Constant Summary
-
BACKOFF_INITIAL =
private
# File 'lib/mongo/session.rb', line 1428
Exponential backoff settings for with_transaction retries.
0.005 -
BACKOFF_MAX =
private
# File 'lib/mongo/session.rb', line 1429
0.5 -
MISMATCHED_CLUSTER_ERROR_MSG =
# File 'lib/mongo/session.rb', line 304
Errormessage indicating that the session was retrieved from a client with a different cluster than that of the client through which it is currently being used.'The configuration of the client used to create this session does not match that ' + 'of the client owning this operation. Please only use this session for operations through its parent ' + 'client.'
-
NO_TRANSACTION_STATE =
# File 'lib/mongo/session.rb', line 324
The state of a session in which the last operation was not related to any transaction or no operations have yet occurred.
:no_transaction
-
SESSIONS_NOT_SUPPORTED =
# File 'lib/mongo/session.rb', line 317Deprecated.
Errormessage describing that sessions are not supported by the server version.'Sessions are not supported by the connected servers.' -
SESSION_ENDED_ERROR_MSG =
# File 'lib/mongo/session.rb', line 311
Errormessage describing that the session cannot be used because it has already been ended.'This session has ended and cannot be used. Please create a new one.' -
STARTING_TRANSACTION_STATE =
# File 'lib/mongo/session.rb', line 330
The state of a session in which a user has initiated a transaction but no operations within the transactions have occurred yet.
:starting_transaction
-
TRANSACTION_ABORTED_STATE =
# File 'lib/mongo/session.rb', line 347
The state of a session in which the last operation executed was a transaction abort.
:transaction_aborted
-
TRANSACTION_COMMITTED_STATE =
# File 'lib/mongo/session.rb', line 342
The state of a session in which the last operation executed was a transaction commit.
:transaction_committed
-
TRANSACTION_IN_PROGRESS_STATE =
# File 'lib/mongo/session.rb', line 337
The state of a session in which a transaction has been started and at least one operation has occurred, but the transaction has not yet been committed or aborted.
:transaction_in_progress
-
UNLABELED_WRITE_CONCERN_CODES =
Internal use only
# File 'lib/mongo/session.rb', line 350
[ 79, # UnknownReplWriteConcern 100, # CannotSatisfyWriteConcern, ].freeze
Loggable - Included
Class Method Summary
-
.new(server_session, client, options = {}) ⇒ Session
constructor
Internal use only
Internal use only
Initialize a
Session.
Instance Attribute Summary
- #aborting_transaction? ⇒ true | false readonly Internal use only Internal use only
- #client ⇒ Client readonly
- #cluster readonly
- #committing_transaction? ⇒ true | false readonly Internal use only Internal use only
- #dirty? ⇒ true | false | nil readonly Internal use only Internal use only
-
#ended? ⇒ true, false
readonly
Whether this session has ended.
-
#explicit? ⇒ true, false
readonly
Is this session an explicit one (i.e.
-
#implicit? ⇒ true, false
readonly
Is this session an implicit one (not user-created).
-
#in_transaction? ⇒ true | false
readonly
Whether or not the session is currently in a transaction.
- #inside_with_transaction? ⇒ Boolean readonly Internal use only Internal use only
- #materialized? ⇒ Boolean readonly Internal use only Internal use only
- #operation_time ⇒ BSON::Timestamp readonly
- #options ⇒ Hash readonly
- #pinned_connection_global_id ⇒ Integer | nil readonly Internal use only Internal use only
- #pinned_server ⇒ Server | nil readonly Internal use only Internal use only
- #recovery_token ⇒ BSON::Document | nil rw Internal use only Internal use only
-
#retry_reads? ⇒ Boolean
readonly
Internal use only
Internal use only
Whether reads executed with this session can be retried according to the modern retryable reads specification.
-
#retry_writes? ⇒ true, false
readonly
Will writes executed with this session be retried.
- #snapshot? ⇒ true | false readonly
- #snapshot_timestamp ⇒ BSON::Timestamp | nil rw
-
#snapshot_timestamp=(value)
rw
Internal use only
Internal use only
Sets the snapshot time for the session.
- #starting_transaction? ⇒ Boolean readonly Internal use only Internal use only
- #with_transaction_deadline ⇒ Integer | nil readonly Internal use only Internal use only
- #causal_consistency? ⇒ Boolean readonly private
ClusterTime::Consumer - Included
| #cluster_time | The cluster time tracked by the object including this module. |
Instance Method Summary
-
#abort_transaction(options = nil)
Abort the currently active transaction without making any changes to the database.
-
#add_autocommit!(command) ⇒ Hash, BSON::Document
Internal use only
Internal use only
Add the autocommit field to a command document if applicable.
-
#add_start_transaction!(command) ⇒ Hash, BSON::Document
Internal use only
Internal use only
Add the startTransaction field to a command document if applicable.
-
#add_txn_num!(command) ⇒ Hash, BSON::Document
Internal use only
Internal use only
Add the transaction number to a command document if applicable.
-
#add_txn_opts!(command, _read, context) ⇒ Hash, BSON::Document
Internal use only
Internal use only
Add the transactions options if applicable.
-
#advance_operation_time(new_operation_time) ⇒ BSON::Timestamp
Advance the cached operation time for this session.
-
#commit_transaction(options = nil)
Commit the currently active transaction on the session.
-
#dirty!(mark = true)
Sets the dirty state to the given value for the underlying server session.
-
#end_session ⇒ nil
End this session.
-
#inspect ⇒ String
Get a formatted string for use in inspection.
-
#materialize_if_needed ⇒ Session
Internal use only
Internal use only
If not already set, populate a session objects's server_session by checking out a session from the session pool.
-
#next_txn_num ⇒ Integer
Internal use only
Internal use only
Increment and return the next transaction number.
-
#pin_to_connection(connection_global_id, connection: nil)
Internal use only
Internal use only
Pins this session to the specified connection.
-
#pin_to_server(server)
Internal use only
Internal use only
Pins this session to the specified server, which should be a mongos.
-
#process(result) ⇒ Operation::Result
Internal use only
Internal use only
Process a response from the server that used this session.
-
#revert_to_starting_transaction!
Internal use only
Internal use only
Reverts the session state to STARTING_TRANSACTION_STATE.
-
#session_id ⇒ BSON::Document
Get the server session id of this session, if the session has not been ended.
-
#start_transaction(options = nil)
Places subsequent operations in this session into a new transaction.
-
#suppress_read_write_concern!(command) ⇒ Hash, BSON::Document
Internal use only
Internal use only
Remove the read concern and/or write concern from the command if not applicable.
-
#txn_num ⇒ Integer
Get the current transaction number.
-
#txn_options ⇒ Hash
on this session.
-
#txn_read_preference ⇒ Hash
Get the read preference the session will use in the currently active transaction.
-
#unpin(connection = nil)
Internal use only
Internal use only
Unpins this session from the pinned server or connection, if the session was pinned.
-
#unpin_maybe(error, connection = nil)
Internal use only
Internal use only
Unpins this session from the pinned server or connection, if the session was pinned and the specified exception instance and the session's transaction state require it to be unpinned.
-
#update_state!
Internal use only
Internal use only
Update the state of the session due to a (non-commit and non-abort) operation being run.
-
#validate!(client) ⇒ Session
Internal use only
Internal use only
Validate the session for use by the specified client.
-
#validate_read_preference!(command)
Internal use only
Internal use only
Ensure that the read preference of a command is primary.
-
#with_transaction(options = nil)
Executes the provided block in a transaction, retrying as necessary.
- #backoff_seconds_for_retry(transaction_attempt) private
- #backoff_would_exceed_deadline?(deadline, backoff_seconds) ⇒ Boolean private
- #calculate_with_transaction_deadline(opts) private
-
#causal_consistency_doc
private
Returns causal consistency document if the last operation time is known and causal consistency is enabled, otherwise returns nil.
- #check_if_ended! private
- #check_if_no_transaction! private
- #check_matching_cluster!(client) private
- #check_transactions_supported! private
- #deadline_expired?(deadline) ⇒ Boolean private
-
#make_timeout_error_from(last_error, timeout_message)
private
Implements makeTimeoutError(lastError) from the transactions-convenient-api spec.
- #operation_timeouts(opts) private
- #set_operation_time(result) private
-
#txn_read_concern ⇒ Hash
private
Get the read concern the session will use when starting a transaction.
- #txn_write_concern private
- #within_states?(*states) ⇒ Boolean private
ClusterTime::Consumer - Included
| #advance_cluster_time | Advance the tracked cluster time document for the object including this module. |
Loggable - Included
| #log_debug | Convenience method to log debug messages with the standard prefix. |
| #log_error | Convenience method to log error messages with the standard prefix. |
| #log_fatal | Convenience method to log fatal messages with the standard prefix. |
| #log_info | Convenience method to log info messages with the standard prefix. |
| #log_warn | Convenience method to log warn messages with the standard prefix. |
| #logger | Get the logger instance. |
| #_mongo_log_prefix, #format_message | |
Retryable - Included
| #read_worker | Returns the read worker for handling retryable reads. |
| #select_server | This is a separate method to make it possible for the test suite to assert that server selection is performed during retry attempts. |
| #with_overload_retry | Wraps an operation with overload retry logic. |
| #write_worker | Returns the write worker for handling retryable writes. |
| #deprioritize_server? | Whether the failed server should be deprioritized during server selection for a retry attempt. |
Instance Attribute Details
#aborting_transaction? ⇒ true | false (readonly)
# File 'lib/mongo/session.rb', line 910
def aborting_transaction? !!@aborting_transaction end
#causal_consistency? ⇒ Boolean (readonly, private)
# File 'lib/mongo/session.rb', line 1357
def causal_consistency? @causal_consistency ||= if @options.key?(:causal_consistency) !!@options[:causal_consistency] else true end end
#client ⇒ Client (readonly)
# File 'lib/mongo/session.rb', line 128
attr_reader :client
#cluster (readonly)
# File 'lib/mongo/session.rb', line 130
attr_reader :cluster
#committing_transaction? ⇒ true | false (readonly)
# File 'lib/mongo/session.rb', line 902
def committing_transaction? !!@committing_transaction end
#dirty? ⇒ true | false | nil (readonly)
# File 'lib/mongo/session.rb', line 158
def dirty? @server_session&.dirty? end
#ended? ⇒ true, false (readonly)
Whether this session has ended.
# File 'lib/mongo/session.rb', line 255
def ended? !!@ended end
#explicit? ⇒ true, false (readonly)
Is this session an explicit one (i.e. user-created).
# File 'lib/mongo/session.rb', line 190
def explicit? !implicit? end
#implicit? ⇒ true, false (readonly)
Is this session an implicit one (not user-created).
# File 'lib/mongo/session.rb', line 178
def implicit? @implicit ||= !!(@options.key?(:implicit) && @options[:implicit] == true) end
#in_transaction? ⇒ true | false (readonly)
Whether or not the session is currently in a transaction.
# File 'lib/mongo/session.rb', line 894
def in_transaction? within_states?(STARTING_TRANSACTION_STATE, TRANSACTION_IN_PROGRESS_STATE) end
#inside_with_transaction? ⇒ Boolean (readonly)
# File 'lib/mongo/session.rb', line 1311
def inside_with_transaction? @inside_with_transaction end
#materialized? ⇒ Boolean (readonly)
# File 'lib/mongo/session.rb', line 1252
def materialized? raise Error::SessionEnded if ended? !@server_session.nil? end
#operation_time ⇒ BSON::Timestamp (readonly)
# File 'lib/mongo/session.rb', line 141
attr_reader :operation_time
#options ⇒ Hash (readonly)
# File 'lib/mongo/session.rb', line 123
attr_reader :
#pinned_connection_global_id ⇒ Integer | nil (readonly)
# File 'lib/mongo/session.rb', line 292
attr_reader :pinned_connection_global_id
#pinned_server ⇒ Server | nil (readonly)
# File 'lib/mongo/session.rb', line 286
attr_reader :pinned_server
#recovery_token ⇒ BSON::Document | nil (rw)
# File 'lib/mongo/session.rb', line 298
attr_accessor :recovery_token
#retry_reads? ⇒ Boolean (readonly)
Whether reads executed with this session can be retried according to the modern retryable reads specification.
If this method returns true, the modern retryable reads have been requested by the application. If the server selected for a read operation supports modern retryable reads, they will be used for that particular operation. If the server selected for a read operation does not support modern retryable reads, the read will not be retried.
If this method returns false, legacy retryable reads have been requested
by the application. Legacy retryable read logic will be used regardless
of server version of the server(s) that the client is connected to.
The number of read retries is given by :max_read_retries client option,
which is 1 by default and can be set to 0 to disable legacy read retries.
# File 'lib/mongo/session.rb', line 210
def retry_reads? client.[:retry_reads] != false end
#retry_writes? ⇒ true, false (readonly)
Retryable writes are only available with sharded clusters, replica
sets, or load-balanced topologies.
Will writes executed with this session be retried.
#snapshot? ⇒ true | false (readonly)
# File 'lib/mongo/session.rb', line 134
def snapshot? !![:snapshot] end
#snapshot_timestamp ⇒ BSON::Timestamp | nil (rw)
# File 'lib/mongo/session.rb', line 1291
attr_reader :
#snapshot_timestamp=(value) (rw)
Sets the snapshot time for the session. Once set, subsequent
assignments are ignored: snapshotTime is established at most once per
session, either from the :snapshot_time option at construction or from
the atClusterTime returned by the first find/aggregate/distinct
response. This keeps the property effectively read-only for callers,
per the snapshot-sessions spec rationale.
# File 'lib/mongo/session.rb', line 1301
def (value) @snapshot_timestamp ||= value end
#starting_transaction? ⇒ Boolean (readonly)
# File 'lib/mongo/session.rb', line 882
def starting_transaction? within_states?(STARTING_TRANSACTION_STATE) end
#with_transaction_deadline ⇒ Integer | nil (readonly)
# File 'lib/mongo/session.rb', line 1307
attr_reader :with_transaction_deadline
Instance Method Details
#abort_transaction(options = nil)
Abort the currently active transaction without making any changes to the database.
# File 'lib/mongo/session.rb', line 814
def abort_transaction( = nil) QueryCache.clear check_if_ended! check_if_no_transaction! if within_states?(TRANSACTION_COMMITTED_STATE) raise Mongo::Error::InvalidTransactionOperation.new( Mongo::Error::InvalidTransactionOperation.cannot_call_after_msg( :commitTransaction, :abortTransaction ) ) end if within_states?(TRANSACTION_ABORTED_STATE) raise Mongo::Error::InvalidTransactionOperation.new( Mongo::Error::InvalidTransactionOperation.cannot_call_twice_msg(:abortTransaction) ) end ||= {} begin unless starting_transaction? @aborting_transaction = true context = Operation::Context.new( client: @client, session: self, operation_timeouts: operation_timeouts() ) write_with_retry([:write_concern], ending_transaction: true, context: context) do |connection, txn_num, context| operation = Operation::Command.new( selector: { abortTransaction: 1 }, db_name: 'admin', session: self, txn_num: txn_num ) tracer.trace_operation(operation, context, op_name: 'abortTransaction') do operation.execute_with_connection(connection, context: context) end ensure unpin end end # Finish the transaction span before changing state tracer.finish_transaction_span(self) @state = TRANSACTION_ABORTED_STATE rescue Mongo::Error::InvalidTransactionOperation raise rescue Mongo::Error tracer.finish_transaction_span(self) @state = TRANSACTION_ABORTED_STATE rescue Exception tracer.finish_transaction_span(self) @state = TRANSACTION_ABORTED_STATE raise ensure @aborting_transaction = false end # No official return value, but return true so that in interactive # use the method hints that it succeeded. true end
#add_autocommit!(command) ⇒ Hash, BSON::Document
Add the autocommit field to a command document if applicable.
# File 'lib/mongo/session.rb', line 1003
def add_autocommit!(command) command.tap do |c| c[:autocommit] = false if in_transaction? end end
#add_start_transaction!(command) ⇒ Hash, BSON::Document
Add the startTransaction field to a command document if applicable.
# File 'lib/mongo/session.rb', line 1018
def add_start_transaction!(command) command.tap do |c| c[:startTransaction] = true if starting_transaction? end end
#add_txn_num!(command) ⇒ Hash, BSON::Document
Add the transaction number to a command document if applicable.
# File 'lib/mongo/session.rb', line 1033
def add_txn_num!(command) command.tap do |c| c[:txnNumber] = BSON::Int64.new(@server_session.txn_num) if in_transaction? end end
#add_txn_opts!(command, _read, context) ⇒ Hash, BSON::Document
Add the transactions options if applicable.
# File 'lib/mongo/session.rb', line 1048
def add_txn_opts!(command, _read, context) command.tap do |c| # The read concern should be added to any command that starts a transaction. if starting_transaction? # https://jira.mongodb.org/browse/SPEC-1161: transaction's # read concern overrides collection/database/client read concerns, # even if transaction's read concern is not set. # Read concern here is the one sent to the server and may # include afterClusterTime. if rc = c[:readConcern] rc = rc.dup rc.delete(:level) end if txn_read_concern if rc rc.update(txn_read_concern) else rc = txn_read_concern.dup end end if rc.nil? || rc.empty? c.delete(:readConcern) else c[:readConcern] = Options::Mapper.transform_values_to_strings(rc) end end # We need to send the read concern level as a string rather than a symbol. c[:readConcern] = Options::Mapper.transform_values_to_strings(c[:readConcern]) if c[:readConcern] if c[:commitTransaction] && (max_time_ms = [:max_commit_time_ms]) c[:maxTimeMS] = max_time_ms end # The write concern should be added to any abortTransaction or commitTransaction command. if c[:abortTransaction] || c[:commitTransaction] if @already_committed wc = BSON::Document.new(c[:writeConcern] || txn_write_concern || {}) wc.merge!(w: :majority) wc[:wtimeout] ||= 10_000 c[:writeConcern] = wc elsif txn_write_concern c[:writeConcern] ||= txn_write_concern end end # A non-numeric write concern w value needs to be sent as a string rather than a symbol. if c[:writeConcern] && c[:writeConcern][:w] && c[:writeConcern][:w].is_a?(Symbol) c[:writeConcern][:w] = c[:writeConcern][:w].to_s end # Ignore wtimeout if csot c[:writeConcern]&.delete(:wtimeout) if context&.csot? # We must not send an empty (server default) write concern. c.delete(:writeConcern) if c[:writeConcern] && c[:writeConcern].empty? end end
#advance_operation_time(new_operation_time) ⇒ BSON::Timestamp
Advance the cached operation time for this session.
# File 'lib/mongo/session.rb', line 1227
def advance_operation_time(new_operation_time) @operation_time = if @operation_time [ @operation_time, new_operation_time ].max else new_operation_time end end
#backoff_seconds_for_retry(transaction_attempt) (private)
# File 'lib/mongo/session.rb', line 1432
def backoff_seconds_for_retry(transaction_attempt) exponential = BACKOFF_INITIAL * (1.5**(transaction_attempt - 1)) Random.rand * [ exponential, BACKOFF_MAX ].min end
#backoff_would_exceed_deadline?(deadline, backoff_seconds) ⇒ Boolean (private)
# File 'lib/mongo/session.rb', line 1437
def backoff_would_exceed_deadline?(deadline, backoff_seconds) return false if deadline.zero? Utils.monotonic_time + backoff_seconds >= deadline end
#calculate_with_transaction_deadline(opts) (private)
# File 'lib/mongo/session.rb', line 1402
def calculate_with_transaction_deadline(opts) calc = lambda { |timeout| if timeout == 0 0 else Utils.monotonic_time + (timeout / 1000.0) end } if timeout_ms = opts&.dig(:timeout_ms) calc.call(timeout_ms) elsif default_timeout_ms = @options[:default_timeout_ms] calc.call(default_timeout_ms) elsif @client.timeout_ms calc.call(@client.timeout_ms) end end
#causal_consistency_doc (private)
Returns causal consistency document if the last operation time is known and causal consistency is enabled, otherwise returns nil.
# File 'lib/mongo/session.rb', line 1351
def causal_consistency_doc return unless operation_time && causal_consistency? { afterClusterTime: operation_time } end
#check_if_ended! (private)
# File 'lib/mongo/session.rb', line 1371
def check_if_ended! raise Mongo::Error::InvalidSession.new(SESSION_ENDED_ERROR_MSG) if ended? end
#check_if_no_transaction! (private)
# File 'lib/mongo/session.rb', line 1336
def check_if_no_transaction! return unless within_states?(NO_TRANSACTION_STATE) raise Mongo::Error::InvalidTransactionOperation.new( Mongo::Error::InvalidTransactionOperation::NO_TRANSACTION_STARTED ) end
#check_matching_cluster!(client) (private)
# File 'lib/mongo/session.rb', line 1375
def check_matching_cluster!(client) return unless cluster != client.cluster raise Mongo::Error::InvalidSession.new(MISMATCHED_CLUSTER_ERROR_MSG) end
#check_transactions_supported! (private)
# File 'lib/mongo/session.rb', line 1381
def check_transactions_supported! raise Mongo::Error::TransactionsNotSupported, 'standalone topology' if cluster.single? end
#commit_transaction(options = nil)
Commit the currently active transaction on the session.
# File 'lib/mongo/session.rb', line 728
def commit_transaction( = nil) QueryCache.clear check_if_ended! check_if_no_transaction! if within_states?(TRANSACTION_ABORTED_STATE) raise Mongo::Error::InvalidTransactionOperation.new( Mongo::Error::InvalidTransactionOperation.cannot_call_after_msg( :abortTransaction, :commitTransaction ) ) end ||= {} begin # If commitTransaction is called twice, we need to run the same commit # operation again, so we revert the session to the previous state. if within_states?(TRANSACTION_COMMITTED_STATE) @state = @last_commit_skipped ? STARTING_TRANSACTION_STATE : TRANSACTION_IN_PROGRESS_STATE @already_committed = true end if starting_transaction? @last_commit_skipped = true else @last_commit_skipped = false @committing_transaction = true write_concern = [:write_concern] || [:write_concern] write_concern = WriteConcern.get(write_concern) if write_concern && !write_concern.is_a?(WriteConcern::Base) context = Operation::Context.new( client: @client, session: self, operation_timeouts: operation_timeouts() ) write_with_retry(write_concern, ending_transaction: true, context: context) do |connection, txn_num, context| if context.retry? && !context.overload_only_retry? if write_concern wco = write_concern..merge(w: :majority) wco[:wtimeout] ||= 10_000 write_concern = WriteConcern.get(wco) else write_concern = WriteConcern.get(w: :majority, wtimeout: 10_000) end end spec = { selector: { commitTransaction: 1 }, db_name: 'admin', session: self, txn_num: txn_num, write_concern: write_concern, } operation = Operation::Command.new(spec) tracer.trace_operation(operation, context, op_name: 'commitTransaction') do operation.execute_with_connection(connection, context: context) end end end # Finish the transaction span before changing state tracer.finish_transaction_span(self) ensure @state = TRANSACTION_COMMITTED_STATE @committing_transaction = false end # No official return value, but return true so that in interactive # use the method hints that it succeeded. true end
#deadline_expired?(deadline) ⇒ Boolean (private)
# File 'lib/mongo/session.rb', line 1419
def deadline_expired?(deadline) if deadline.zero? false else Utils.monotonic_time >= deadline end end
#dirty!(mark = true)
Sets the dirty state to the given value for the underlying server session. If there is no server session, this does nothing.
# File 'lib/mongo/session.rb', line 150
def dirty!(mark = true) @server_session&.dirty!(mark) end
#end_session ⇒ nil
End this session.
If there is an in-progress transaction on this session, the transaction is aborted. The server session associated with this session is returned to the server session pool. Finally, this session is marked ended and is no longer usable.
If this session is already ended, this method does nothing.
Note that this method does not directly issue an endSessions command to this server, contrary to what its name might suggest.
# File 'lib/mongo/session.rb', line 385
def end_session if !ended? && @client if within_states?(TRANSACTION_IN_PROGRESS_STATE) begin abort_transaction rescue Mongo::Error, Error::AuthError end end # Release any pinned connection (e.g. after a committed transaction # in load-balanced mode). unpin if pinned_connection_global_id cluster.session_pool.checkin(@server_session) if @server_session end ensure @server_session = nil @ended = true @client = nil end
#inspect ⇒ String
Get a formatted string for use in inspection.
# File 'lib/mongo/session.rb', line 363
def inspect "#<Mongo::Session:0x#{object_id} session_id=#{session_id} options=#{@options}>" end
#make_timeout_error_from(last_error, timeout_message) (private)
Implements makeTimeoutError(lastError) from the transactions-convenient-api spec. In CSOT mode raises TimeoutError with last_error's message and labels copied. In non-CSOT mode re-raises last_error directly.
# File 'lib/mongo/session.rb', line 1446
def make_timeout_error_from(last_error, ) if @with_transaction_timeout_ms timeout_error = Mongo::Error::TimeoutError.new("#{}: #{last_error}") if last_error.respond_to?(:labels) last_error.labels.each { |label| timeout_error.add_label(label) } end raise timeout_error end raise last_error end
#materialize_if_needed ⇒ Session
If not already set, populate a session objects's server_session by checking out a session from the session pool.
# File 'lib/mongo/session.rb', line 1241
def materialize_if_needed raise Error::SessionEnded if ended? return unless implicit? && !@server_session @server_session = cluster.session_pool.checkout self end
#next_txn_num ⇒ Integer
Increment and return the next transaction number.
# File 'lib/mongo/session.rb', line 1267
def next_txn_num raise Error::SessionEnded if ended? @server_session.next_txn_num end
#operation_timeouts(opts) (private)
# File 'lib/mongo/session.rb', line 1385
def operation_timeouts(opts) { inherited_timeout_ms: @with_transaction_timeout_ms || @client.timeout_ms }.tap do |result| if @inside_with_transaction if opts[:timeout_ms] raise Mongo::Error::InvalidTransactionOperation, 'timeoutMS cannot be overridden inside a withTransaction callback' end elsif timeout_ms = opts[:timeout_ms] result[:operation_timeout_ms] = timeout_ms elsif default_timeout_ms = [:default_timeout_ms] result[:operation_timeout_ms] = default_timeout_ms end end end
#pin_to_connection(connection_global_id, connection: nil)
Pins this session to the specified connection.
this session to.
# File 'lib/mongo/session.rb', line 936
def pin_to_connection(connection_global_id, connection: nil) raise ArgumentError, 'Cannot pin to a nil connection id' if connection_global_id.nil? @pinned_connection_global_id = connection_global_id @pinned_connection = connection end
#pin_to_server(server)
Pins this session to the specified server, which should be a mongos.
# File 'lib/mongo/session.rb', line 919
def pin_to_server(server) raise ArgumentError, 'Cannot pin to a nil server' if server.nil? if Lint.enabled? && !server.mongos? raise Error::LintError, "Attempted to pin the session to server #{server.summary} which is not a mongos" end @pinned_server = server end
#process(result) ⇒ Operation::Result
Process a response from the server that used this session.
# File 'lib/mongo/session.rb', line 1201
def process(result) unless implicit? set_operation_time(result) if cluster_time_doc = result.cluster_time advance_cluster_time(cluster_time_doc) end end @server_session.set_last_use! if (doc = result.reply && result.reply.documents.first) && doc[:recoveryToken] self.recovery_token = doc[:recoveryToken] end result end
#revert_to_starting_transaction!
Reverts the session state to STARTING_TRANSACTION_STATE. Called before retrying the first command in a transaction so that startTransaction: true is preserved on the retry.
# File 'lib/mongo/session.rb', line 1152
def revert_to_starting_transaction! return unless within_states?(TRANSACTION_IN_PROGRESS_STATE) @state = STARTING_TRANSACTION_STATE end
#session_id ⇒ BSON::Document
Get the server session id of this session, if the session has not been
ended. If the session had been ended, raises Error::SessionEnded.
# File 'lib/mongo/session.rb', line 267
def session_id raise Error::SessionEnded if ended? # An explicit session will always have a session_id, because during # construction a server session must be provided. An implicit session # will not have a session_id until materialized, thus calls to # session_id might fail. An application should not have an opportunity # to experience this failure because an implicit session shouldn't be # accessible to applications due to its lifetime being constrained to # operation execution, which is done entirely by the driver. raise Error::SessionNotMaterialized unless materialized? @server_session.session_id end
#set_operation_time(result) (private)
# File 'lib/mongo/session.rb', line 1365
def set_operation_time(result) return unless result && result.operation_time @operation_time = result.operation_time end
#start_transaction(options = nil)
Places subsequent operations in this session into a new transaction.
Note that the transaction will not be started on the server until an operation is performed after start_transaction is called.
# File 'lib/mongo/session.rb', line 664
def start_transaction( = nil) check_transactions_supported! if Lint.validate_read_concern_option([:read_concern]) # # It would be handy to detect invalid read preferences here, but # # some of the spec tests require later detection of invalid read prefs. # # Maybe we can do this when lint mode is on. # mode = options[:read] && options[:read][:mode].to_s # if mode && mode != 'primary' # raise Mongo::Error::InvalidTransactionOperation.new( # "read preference in a transaction must be primary (requested: #{mode})" # ) # end end raise Mongo::Error::SnapshotSessionTransactionProhibited if snapshot? check_if_ended! if within_states?(STARTING_TRANSACTION_STATE, TRANSACTION_IN_PROGRESS_STATE) raise Mongo::Error::InvalidTransactionOperation.new( Mongo::Error::InvalidTransactionOperation::TRANSACTION_ALREADY_IN_PROGRESS ) end unpin next_txn_num @txn_options = (@options[:] || {}).merge( || {}) if txn_write_concern && !WriteConcern.get(txn_write_concern).acknowledged? raise Mongo::Error::InvalidTransactionOperation.new( Mongo::Error::InvalidTransactionOperation::UNACKNOWLEDGED_WRITE_CONCERN ) end @state = STARTING_TRANSACTION_STATE @already_committed = false tracer.start_transaction_span(self) # This method has no explicit return value. # We could return nil here but true indicates to the user that the # operation succeeded. This is intended for interactive use. # Note that the return value is not documented. true end
#suppress_read_write_concern!(command) ⇒ Hash, BSON::Document
Remove the read concern and/or write concern from the command if not applicable.
# File 'lib/mongo/session.rb', line 1116
def suppress_read_write_concern!(command) command.tap do |c| next unless in_transaction? c.delete(:readConcern) unless starting_transaction? c.delete(:writeConcern) unless c[:commitTransaction] || c[:abortTransaction] end end
#txn_num ⇒ Integer
Get the current transaction number.
# File 'lib/mongo/session.rb', line 1281
def txn_num raise Error::SessionEnded if ended? @server_session.txn_num end
#txn_options ⇒ Hash
on this session.
# File 'lib/mongo/session.rb', line 166
def @txn_options or raise ArgumentError, 'There is no active transaction' end
#txn_read_concern ⇒ Hash (private)
Get the read concern the session will use when starting a transaction.
This is a driver style hash with underscore keys.
# File 'lib/mongo/session.rb', line 1327
def txn_read_concern # Read concern is inherited from client but not db or collection. [:read_concern] || @client.read_concern end
#txn_read_preference ⇒ Hash
Get the read preference the session will use in the currently active transaction.
This is a driver style hash with underscore keys.
# File 'lib/mongo/session.rb', line 240
def txn_read_preference rp = [:read] || @client.read_preference Mongo::Lint.validate_underscore_read_preference(rp) rp end
#txn_write_concern (private)
# File 'lib/mongo/session.rb', line 1344
def txn_write_concern [:write_concern] || (@client.write_concern && @client.write_concern.) end
#unpin(connection = nil)
Unpins this session from the pinned server or connection, if the session was pinned.
# File 'lib/mongo/session.rb', line 949
def unpin(connection = nil) # Idempotent: if there is no pinned state to clear, do nothing. Nested # unpin_maybe handlers (e.g. in BulkWrite#execute_operation wrapping an # OpMsg execution that already calls unpin_maybe in its own do_execute) # can call this method twice for the same error; checking the connection # back into the pool a second time would raise from the pool. return if @pinned_server.nil? && @pinned_connection.nil? && @pinned_connection_global_id.nil? @pinned_server = nil @pinned_connection_global_id = nil conn = connection || @pinned_connection if conn conn.unpin(:transaction) # Only check the connection back into the pool if nothing else # still holds a pin on it (e.g. an open cursor). unless conn.pinned? conn.connection_pool.check_in(conn) end end @pinned_connection = nil end
#unpin_maybe(error, connection = nil)
Unpins this session from the pinned server or connection, if the session was pinned and the specified exception instance and the session's transaction state require it to be unpinned.
The exception instance should already have all of the labels set on it (both client- and server-side generated ones).
# File 'lib/mongo/session.rb', line 982
def unpin_maybe(error, connection = nil) if !within_states?(Session::NO_TRANSACTION_STATE) && error.label?('TransientTransactionError') unpin(connection) end if committing_transaction? && error.label?('UnknownTransactionCommitResult') unpin(connection) end end
#update_state!
Update the state of the session due to a (non-commit and non-abort) operation being run.
# File 'lib/mongo/session.rb', line 1162
def update_state! case @state when STARTING_TRANSACTION_STATE @state = TRANSACTION_IN_PROGRESS_STATE when TRANSACTION_COMMITTED_STATE, TRANSACTION_ABORTED_STATE @state = NO_TRANSACTION_STATE end end
#validate!(client) ⇒ Session
Validate the session for use by the specified client.
The session must not be ended and must have been created by a client with the same cluster as the client that the session is to be used with.
# File 'lib/mongo/session.rb', line 1184
def validate!(client) check_if_ended! check_matching_cluster!(client) self end
#validate_read_preference!(command)
Ensure that the read preference of a command is primary.
# File 'lib/mongo/session.rb', line 1135
def validate_read_preference!(command) return unless in_transaction? return unless command['$readPreference'] mode = command['$readPreference']['mode'] || command['$readPreference'][:mode] return unless mode && mode != 'primary' raise Mongo::Error::InvalidTransactionOperation.new( "read preference in a transaction must be primary (requested: #{mode})" ) end
#with_transaction(options = nil)
with_transaction contains a loop, therefore the if with_transaction itself is placed in a loop, its block should not call next or break to control the outer loop because this will instead affect the loop in with_transaction. The driver will warn and abort the transaction if it detects this situation.
Executes the provided block in a transaction, retrying as necessary.
Returns the return value of the block.
Exact number of retries and when they are performed are implementation details of the driver; the provided block should be idempotent, and should be prepared to be called more than once. The driver may retry the commit command within an active transaction or it may repeat the transaction and invoke the block again, depending on the error encountered if any. Note also that the retries may be executed against different servers.
Transactions cannot be nested - InvalidTransactionOperation will be raised if this method is called when the session already has an active transaction.
Exceptions raised by the block which are not derived from Error
stop processing, abort the transaction and are propagated out of
with_transaction. Exceptions derived from Error may be
handled by with_transaction, resulting in retries of the process.
Currently, with_transaction will retry commits and block invocations until at least 120 seconds have passed since with_transaction started executing. This timeout is not configurable and may change in a future driver version.
# File 'lib/mongo/session.rb', line 458
def with_transaction( = nil) @inside_with_transaction = true @with_transaction_timeout_ms = &.dig(:timeout_ms) || @options[:default_timeout_ms] || @client.timeout_ms @with_transaction_deadline = calculate_with_transaction_deadline() deadline = if @with_transaction_deadline # CSOT enabled, so we have a customer defined deadline. @with_transaction_deadline else # CSOT not enabled, so we use the default deadline, 120 seconds. Utils.monotonic_time + 120 end transaction_in_progress = false transaction_attempt = 0 last_error = nil overload_error_count = 0 overload_encountered = false loop do if transaction_attempt > 0 if overload_encountered delay = @client.retry_policy.backoff_delay(overload_error_count) if backoff_would_exceed_deadline?(deadline, delay) make_timeout_error_from(last_error, 'CSOT timeout expired waiting to retry withTransaction') end raise(last_error) unless @client.retry_policy.should_retry_overload?(overload_error_count, delay) sleep(delay) else backoff = backoff_seconds_for_retry(transaction_attempt) if backoff_would_exceed_deadline?(deadline, backoff) make_timeout_error_from(last_error, 'CSOT timeout expired waiting to retry withTransaction') end sleep(backoff) end end = {} [:write_concern] = [:write_concern] if start_transaction() transaction_in_progress = true transaction_attempt += 1 begin rv = yield self rescue Exception => e if within_states?(STARTING_TRANSACTION_STATE, TRANSACTION_IN_PROGRESS_STATE) log_warn("Aborting transaction due to #{e.class}: #{e}") # CSOT: if the deadline is already expired, clear it so that # abort_transaction uses a fresh timeout (not the expired deadline). # If the deadline is not yet expired, keep it so abort uses remaining time. @with_transaction_deadline = nil if @with_transaction_deadline && deadline_expired?(deadline) abort_transaction transaction_in_progress = false end if deadline_expired?(deadline) transaction_in_progress = false make_timeout_error_from(e, 'CSOT timeout expired during withTransaction callback') end if e.is_a?(Mongo::Error) && e.label?('TransientTransactionError') last_error = e if e.label?('SystemOverloadedError') overload_encountered = true overload_error_count += 1 elsif overload_encountered overload_error_count += 1 end next end raise else if within_states?(TRANSACTION_ABORTED_STATE, NO_TRANSACTION_STATE, TRANSACTION_COMMITTED_STATE) transaction_in_progress = false return rv end # CSOT: if the timeout has expired before we can commit, abort the # transaction instead and raise a client-side timeout error. if @with_transaction_deadline && deadline_expired?(deadline) transaction_in_progress = false @with_transaction_deadline = nil abort_transaction raise Mongo::Error::TimeoutError, 'CSOT timeout expired before transaction could be committed' end begin commit_transaction() transaction_in_progress = false return rv rescue Mongo::Error => e if e.label?('UnknownTransactionCommitResult') if deadline_expired?(deadline) || (e.is_a?(Error::OperationFailure::Family) && e.max_time_ms_expired?) transaction_in_progress = false raise unless @with_transaction_timeout_ms && deadline_expired?(deadline) make_timeout_error_from(e, 'CSOT timeout expired during withTransaction commit') end if e.label?('SystemOverloadedError') overload_encountered = true overload_error_count += 1 elsif overload_encountered overload_error_count += 1 end if overload_encountered delay = @client.retry_policy.backoff_delay(overload_error_count) if backoff_would_exceed_deadline?(deadline, delay) transaction_in_progress = false make_timeout_error_from(e, 'CSOT timeout expired during withTransaction commit') end unless @client.retry_policy.should_retry_overload?(overload_error_count, delay) transaction_in_progress = false raise end sleep(delay) end = case v = [:write_concern] when WriteConcern::Base v. when nil {} else v end [:write_concern] = .merge(w: :majority) retry elsif e.label?('TransientTransactionError') if Utils.monotonic_time >= deadline transaction_in_progress = false make_timeout_error_from(e, 'CSOT timeout expired during withTransaction commit') end last_error = e if e.label?('SystemOverloadedError') overload_encountered = true overload_error_count += 1 elsif overload_encountered overload_error_count += 1 end @state = NO_TRANSACTION_STATE next else transaction_in_progress = false raise end rescue Error::AuthError transaction_in_progress = false raise end end end # No official return value, but return true so that in interactive # use the method hints that it succeeded. true ensure if transaction_in_progress log_warn('with_transaction callback broke out of with_transaction loop, aborting transaction') begin abort_transaction rescue Error::OperationFailure::Family, Error::InvalidTransactionOperation end end @with_transaction_deadline = nil @with_transaction_timeout_ms = nil @inside_with_transaction = false end
#within_states?(*states) ⇒ Boolean (private)
# File 'lib/mongo/session.rb', line 1332
def within_states?(*states) states.include?(@state) end