123456789_123456789_123456789_123456789_123456789_

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

Note:

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.

Since:

  • 2.5.0

Constant Summary

  • BACKOFF_INITIAL = private

    Exponential backoff settings for with_transaction retries.

    Since:

    • 2.5.0

    # File 'lib/mongo/session.rb', line 1395
    0.005
  • BACKOFF_MAX = private

    Since:

    • 2.5.0

    # File 'lib/mongo/session.rb', line 1396
    0.5
  • MISMATCHED_CLUSTER_ERROR_MSG =

    Error message 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.

    Since:

    • 2.5.0

    # File 'lib/mongo/session.rb', line 293
    '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 =

    The state of a session in which the last operation was not related to any transaction or no operations have yet occurred.

    Since:

    • 2.6.0

    # File 'lib/mongo/session.rb', line 313
    :no_transaction
  • SESSIONS_NOT_SUPPORTED =
    Deprecated.

    Error message describing that sessions are not supported by the server version.

    Since:

    • 2.5.0

    # File 'lib/mongo/session.rb', line 306
    'Sessions are not supported by the connected servers.'
  • SESSION_ENDED_ERROR_MSG =

    Error message describing that the session cannot be used because it has already been ended.

    Since:

    • 2.5.0

    # File 'lib/mongo/session.rb', line 300
    'This session has ended and cannot be used. Please create a new one.'
  • STARTING_TRANSACTION_STATE =

    The state of a session in which a user has initiated a transaction but no operations within the transactions have occurred yet.

    Since:

    • 2.6.0

    # File 'lib/mongo/session.rb', line 319
    :starting_transaction
  • TRANSACTION_ABORTED_STATE =

    The state of a session in which the last operation executed was a transaction abort.

    Since:

    • 2.6.0

    # File 'lib/mongo/session.rb', line 336
    :transaction_aborted
  • TRANSACTION_COMMITTED_STATE =

    The state of a session in which the last operation executed was a transaction commit.

    Since:

    • 2.6.0

    # File 'lib/mongo/session.rb', line 331
    :transaction_committed
  • TRANSACTION_IN_PROGRESS_STATE =

    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.

    Since:

    • 2.6.0

    # File 'lib/mongo/session.rb', line 326
    :transaction_in_progress
  • UNLABELED_WRITE_CONCERN_CODES = Internal use only

    Since:

    • 2.5.0

    # File 'lib/mongo/session.rb', line 339
    [
      79,  # UnknownReplWriteConcern
      100, # CannotSatisfyWriteConcern,
    ].freeze

Loggable - Included

PREFIX

Class Method Summary

Instance Attribute Summary

ClusterTime::Consumer - Included

#cluster_time

The cluster time tracked by the object including this module.

Instance Method Summary

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)

This method is for internal use only.

Returns:

  • (true | false)

    Whether the session is currently aborting a transaction.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 899

def aborting_transaction?
  !!@aborting_transaction
end

#causal_consistency?Boolean (readonly, private)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1324

def causal_consistency?
  @causal_consistency ||= if @options.key?(:causal_consistency)
                            !!@options[:causal_consistency]
                          else
                            true
                          end
end

#clientClient (readonly)

Returns:

  • (Client)

    The client through which this session was created.

Since:

  • 2.5.1

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 117

attr_reader :client

#cluster (readonly)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 119

attr_reader :cluster

#committing_transaction?true | false (readonly)

This method is for internal use only.

Returns:

  • (true | false)

    Whether the session is currently committing a transaction.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 891

def committing_transaction?
  !!@committing_transaction
end

#dirty?true | false | nil (readonly)

This method is for internal use only.

Returns:

  • (true | false | nil)

    whether the underlying server session is dirty. If no server session exists for this session, returns nil.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 147

def dirty?
  @server_session&.dirty?
end

#ended?true, false (readonly)

Whether this session has ended.

Examples:

session.ended?

Returns:

  • (true, false)

    Whether the session has ended.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 244

def ended?
  !!@ended
end

#explicit?true, false (readonly)

Is this session an explicit one (i.e. user-created).

Examples:

Is the session explicit?

session.explicit?

Returns:

  • (true, false)

    Whether this session is explicit.

Since:

  • 2.5.2

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 179

def explicit?
  !implicit?
end

#implicit?true, false (readonly)

Is this session an implicit one (not user-created).

Examples:

Is the session implicit?

session.implicit?

Returns:

  • (true, false)

    Whether this session is implicit.

Since:

  • 2.5.1

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 167

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.

Examples:

Is the session in a transaction?

session.in_transaction?

Returns:

  • (true | false)

    Whether or not the session in a transaction.

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 883

def in_transaction?
  within_states?(STARTING_TRANSACTION_STATE, TRANSACTION_IN_PROGRESS_STATE)
end

#inside_with_transaction?Boolean (readonly)

This method is for internal use only.

Returns:

  • (Boolean)

    Whether we are currently inside a with_transaction block.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1278

def inside_with_transaction?
  @inside_with_transaction
end

#materialized?Boolean (readonly)

This method is for internal use only.

Raises:

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1234

def materialized?
  raise Error::SessionEnded if ended?

  !@server_session.nil?
end

#operation_timeBSON::Timestamp (readonly)

Returns:

  • (BSON::Timestamp)

    The latest seen operation time for this session.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 130

attr_reader :operation_time

#optionsHash (readonly)

Returns:

  • (Hash)

    The options for this session.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 112

attr_reader :options

#pinned_connection_global_idInteger | nil (readonly)

This method is for internal use only.

Returns:

  • (Integer | nil)

    The connection global id that this session is pinned to, if any.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 281

attr_reader :pinned_connection_global_id

#pinned_serverServer | nil (readonly)

This method is for internal use only.

Returns:

  • (Server | nil)

    The server (which should be a mongos) that this session is pinned to, if any.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 275

attr_reader :pinned_server

#recovery_tokenBSON::Document | nil (rw)

This method is for internal use only.

Returns:

  • (BSON::Document | nil)

    Recovery token for the sharded transaction being executed on this session, if any.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 287

attr_accessor :recovery_token

#retry_reads?Boolean (readonly)

This method is for internal use only.

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.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 199

def retry_reads?
  client.options[:retry_reads] != false
end

#retry_writes?true, false (readonly)

Note:

Retryable writes are only available with sharded clusters, replica sets, or load-balanced topologies.

Will writes executed with this session be retried.

Examples:

Will writes be retried.

session.retry_writes?

Returns:

  • (true, false)

    If writes will be retried.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 214

def retry_writes?
  !!client.options[:retry_writes] && (cluster.replica_set? || cluster.sharded? || cluster.load_balanced?)
end

#snapshot?true | false (readonly)

Returns:

  • (true | false)

    Whether the session is configured for snapshot reads.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 123

def snapshot?
  !!options[:snapshot]
end

#snapshot_timestamp (rw)

This method is for internal use only.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1270

attr_accessor :snapshot_timestamp

#starting_transaction?Boolean (readonly)

This method is for internal use only.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 871

def starting_transaction?
  within_states?(STARTING_TRANSACTION_STATE)
end

#with_transaction_deadlineInteger | nil (readonly)

This method is for internal use only.

Returns:

  • (Integer | nil)

    The deadline for the current transaction, if any.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1274

attr_reader :with_transaction_deadline

Instance Method Details

#abort_transaction(options = nil)

Abort the currently active transaction without making any changes to the database.

Examples:

Abort the transaction.

session.abort_transaction

Parameters:

  • options (Hash) (defaults to: nil)

    a customizable set of options

Options Hash (options):

  • :timeout_ms (Integer)

    The operation timeout in milliseconds. Must be a non-negative integer. An explicit value of 0 means infinite. The default value is unset which means the value is inherited from the client.

Raises:

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 803

def abort_transaction(options = 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

  options ||= {}

  begin
    unless starting_transaction?
      @aborting_transaction = true
      context = Operation::Context.new(
        client: @client,
        session: self,
        operation_timeouts: operation_timeouts(options)
      )
      write_with_retry(txn_options[: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

This method is for internal use only.

Add the autocommit field to a command document if applicable.

Examples:

session.add_autocommit!(cmd)

Returns:

  • (Hash, BSON::Document)

    The command document.

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 985

def add_autocommit!(command)
  command.tap do |c|
    c[:autocommit] = false if in_transaction?
  end
end

#add_start_transaction!(command) ⇒ Hash, BSON::Document

This method is for internal use only.

Add the startTransaction field to a command document if applicable.

Examples:

session.add_start_transaction!(cmd)

Returns:

  • (Hash, BSON::Document)

    The command document.

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1000

def add_start_transaction!(command)
  command.tap do |c|
    c[:startTransaction] = true if starting_transaction?
  end
end

#add_txn_num!(command) ⇒ Hash, BSON::Document

This method is for internal use only.

Add the transaction number to a command document if applicable.

Examples:

session.add_txn_num!(cmd)

Returns:

  • (Hash, BSON::Document)

    The command document.

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1015

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

This method is for internal use only.

Add the transactions options if applicable.

Examples:

session.add_txn_opts!(cmd)

Returns:

  • (Hash, BSON::Document)

    The command document.

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1030

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 = txn_options[: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.

Examples:

Advance the operation time.

session.advance_operation_time(timestamp)

Parameters:

  • new_operation_time (BSON::Timestamp)

    The new operation time.

Returns:

  • (BSON::Timestamp)

    The max operation time, considering the current and new times.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1209

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)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1399

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)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1404

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)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1369

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.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1318

def causal_consistency_doc
  return unless operation_time && causal_consistency?

  { afterClusterTime: operation_time }
end

#check_if_ended! (private)

Raises:

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1338

def check_if_ended!
  raise Mongo::Error::InvalidSession.new(SESSION_ENDED_ERROR_MSG) if ended?
end

#check_if_no_transaction! (private)

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1303

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)

Raises:

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1342

def check_matching_cluster!(client)
  return unless cluster != client.cluster

  raise Mongo::Error::InvalidSession.new(MISMATCHED_CLUSTER_ERROR_MSG)
end

#check_transactions_supported! (private)

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1348

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.

Examples:

Commits the transaction.

session.commit_transaction

Parameters:

  • options (Hash) (defaults to: nil)

    a customizable set of options

Options Hash (options):

  • :write_concern (nil | WriteConcern::Base)

    The write concern to use for this operation.

  • :timeout_ms (Integer)

    The operation timeout in milliseconds. Must be a non-negative integer. An explicit value of 0 means infinite. The default value is unset which means the value is inherited from the client.

Raises:

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 717

def commit_transaction(options = 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

  options ||= {}

  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 = options[:write_concern] || txn_options[: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(options)
      )
      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.options.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)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1386

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.

Parameters:

  • mark (true | false) (defaults to: true)

    whether to mark the server session as dirty, or not.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 139

def dirty!(mark = true)
  @server_session&.dirty!(mark)
end

#end_sessionnil

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.

Examples:

session.end_session

Returns:

  • (nil)

    Always nil.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 374

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

#inspectString

Get a formatted string for use in inspection.

Examples:

Inspect the session object.

session.inspect

Returns:

  • (String)

    The session inspection.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 352

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.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1413

def make_timeout_error_from(last_error, timeout_message)
  if @with_transaction_timeout_ms
    timeout_error = Mongo::Error::TimeoutError.new("#{timeout_message}: #{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_neededSession

This method is for internal use only.

If not already set, populate a session objects’s server_session by checking out a session from the session pool.

Returns:

  • (Session)

    Self.

Raises:

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1223

def materialize_if_needed
  raise Error::SessionEnded if ended?

  return unless implicit? && !@server_session

  @server_session = cluster.session_pool.checkout

  self
end

#next_txn_numInteger

This method is for internal use only.

Increment and return the next transaction number.

Examples:

Get the next transaction number.

session.next_txn_num

Returns:

  • (Integer)

    The next transaction number.

Raises:

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1249

def next_txn_num
  raise Error::SessionEnded if ended?

  @server_session.next_txn_num
end

#operation_timeouts(opts) (private)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1352

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 = options[:default_timeout_ms]
      result[:operation_timeout_ms] = default_timeout_ms
    end
  end
end

#pin_to_connection(connection_global_id, connection: nil)

This method is for internal use only.

Pins this session to the specified connection.

this session to.

Parameters:

  • connection_global_id (Integer)

    The global id of connection to pin

  • connection (Connection | nil)

    The connection object to pin to.

Raises:

  • (ArgumentError)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 925

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)

This method is for internal use only.

Pins this session to the specified server, which should be a mongos.

Parameters:

  • server (Server)

    The server to pin this session to.

Raises:

  • (ArgumentError)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 908

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

This method is for internal use only.

Process a response from the server that used this session.

Examples:

Process a response from the server.

session.process(result)

Parameters:

Returns:

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1183

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!

This method is for internal use only.

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.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1134

def revert_to_starting_transaction!
  return unless within_states?(TRANSACTION_IN_PROGRESS_STATE)

  @state = STARTING_TRANSACTION_STATE
end

#session_idBSON::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.

Returns:

  • (BSON::Document)

    The server session id.

Raises:

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 256

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)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1332

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.

Examples:

Start a new transaction

session.start_transaction(options)

Parameters:

  • options (Hash) (defaults to: nil)

    The options for the transaction being started.

Options Hash (options):

  • :max_commit_time_ms (Integer)

    The maximum amount of time to allow a single commitTransaction command to run, in milliseconds. This options is deprecated, use :timeout_ms instead.

  • :read_concern (Hash)

    The read concern options hash, with the following optional keys:

    • :level – the read preference level as a symbol; valid values

      are *:local*, *:majority*, and *:snapshot*
  • :write_concern (Hash)

    The write concern options. Can be :w => Integer|String, :fsync => Boolean, :j => Boolean.

  • :read (Hash)

    The read preference options. The hash may have the following items:

    • :mode – read preference specified as a symbol; the only valid value is :primary.

  • :timeout_ms (Integer)

    The operation timeout in milliseconds. Must be a non-negative integer. An explicit value of 0 means infinite. The default value is unset which means the value is inherited from the client.

Raises:

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 653

def start_transaction(options = nil)
  check_transactions_supported!

  if options
    Lint.validate_read_concern_option(options[: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[:default_transaction_options] || {}).merge(options || {})

  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

This method is for internal use only.

Remove the read concern and/or write concern from the command if not applicable.

Examples:

session.suppress_read_write_concern!(cmd)

Returns:

  • (Hash, BSON::Document)

    The command document.

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1098

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_numInteger

Get the current transaction number.

Examples:

Get the current transaction number.

session.txn_num

Returns:

  • (Integer)

    The current transaction number.

Raises:

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1263

def txn_num
  raise Error::SessionEnded if ended?

  @server_session.txn_num
end

#txn_optionsHash

on this session.

Returns:

  • (Hash)

    The options for the transaction currently being executed

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 155

def txn_options
  @txn_options or raise ArgumentError, 'There is no active transaction'
end

#txn_read_concernHash (private)

Get the read concern the session will use when starting a transaction.

This is a driver style hash with underscore keys.

Examples:

Get the session’s transaction read concern.

session.txn_read_concern

Returns:

  • (Hash)

    The read concern used for starting transactions.

Since:

  • 2.9.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1294

def txn_read_concern
  # Read concern is inherited from client but not db or collection.
  txn_options[:read_concern] || @client.read_concern
end

#txn_read_preferenceHash

Get the read preference the session will use in the currently active transaction.

This is a driver style hash with underscore keys.

Examples:

Get the transaction’s read preference

session.txn_read_preference

Returns:

  • (Hash)

    The read preference of the transaction.

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 229

def txn_read_preference
  rp = txn_options[:read] ||
       @client.read_preference
  Mongo::Lint.validate_underscore_read_preference(rp)
  rp
end

#txn_write_concern (private)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1311

def txn_write_concern
  txn_options[:write_concern] ||
    (@client.write_concern && @client.write_concern.options)
end

#unpin(connection = nil)

This method is for internal use only.

Unpins this session from the pinned server or connection, if the session was pinned.

Parameters:

  • connection (Connection | nil) (defaults to: nil)

    Connection to unpin from.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 938

def unpin(connection = 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)

This method is for 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.

The exception instance should already have all of the labels set on it (both client- and server-side generated ones).

Parameters:

  • error (Error)

    The exception instance to process.

  • connection (Connection | nil) (defaults to: nil)

    Connection to unpin from.

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 964

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!

This method is for internal use only.

Update the state of the session due to a (non-commit and non-abort) operation being run.

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1144

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

This method is for internal use only.

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.

Parameters:

  • client (Client)

    The client the session is to be used with.

Returns:

  • (Session)

    self, if the session is valid.

Raises:

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1166

def validate!(client)
  check_if_ended!
  check_matching_cluster!(client)
  self
end

#validate_read_preference!(command)

This method is for internal use only.

Ensure that the read preference of a command primary.

Examples:

session.validate_read_preference!(command)

Raises:

Since:

  • 2.6.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1117

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)

Note:

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.

Examples:

Execute a statement in a transaction

session.with_transaction(write_concern: {w: :majority}) do
  collection.update_one({ id: 3 }, { '$set' => { status: 'Inactive'} },
                        session: session)

end

Execute a statement in a transaction, limiting total time consumed

Timeout.timeout(5) do
  session.with_transaction(write_concern: {w: :majority}) do
    collection.update_one({ id: 3 }, { '$set' => { status: 'Inactive'} },
                          session: session)

  end
end

Parameters:

  • options (Hash) (defaults to: nil)

    The options for the transaction being started. These are the same options that start_transaction accepts.

Raises:

Since:

  • 2.7.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 447

def with_transaction(options = nil)
  @inside_with_transaction = true
  @with_transaction_timeout_ms = options&.dig(:timeout_ms) || @options[:default_timeout_ms] || @client.timeout_ms
  @with_transaction_deadline = calculate_with_transaction_deadline(options)
  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

    commit_options = {}
    commit_options[:write_concern] = options[:write_concern] if options
    start_transaction(options)
    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(commit_options)
        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

          wc_options = case v = commit_options[:write_concern]
                       when WriteConcern::Base
                         v.options
                       when nil
                         {}
                       else
                         v
                       end
          commit_options[:write_concern] = wc_options.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)

Since:

  • 2.5.0

[ GitHub ]

  
# File 'lib/mongo/session.rb', line 1299

def within_states?(*states)
  states.include?(@state)
end