Navigation

Transactions

Version 4.0 of the MongoDB server introduces multi-document transactions. (Updates to multiple fields within a single document are atomic in all versions of MongoDB.) Ruby driver version 2.6.0 adds support for transactions.

Using Transactions

In order to start a transaction, the application must have a session.

The recommended way to use transactions is to utilize the with_transaction helper method:

session = client.start_session
session.with_transaction do
  collection.insert_one(hello: 'world')
end

The with_transaction helper does the following:

  • It starts a transaction prior to calling the supplied block, and commits the transaction when the block finishes.
  • If any of the operations in the block, or the commit operation, result in a transient transaction error, the block and/or the commit will be executed again.

The block should be idempotent, because it may be called multiple times.

The block may explicitly commit or abort the transaction, by calling commit_transaction or abort_transaction; in this case with_transaction will not attempt to commit or abort (but may still retry the block on transient transaction errors propagated out of the block).

The block will also be retried if the transaction’s commit result is unknown. This may happen, for example, if the cluster undergoes an election during the commit. In this case when the block is retried, the primary server of the topology would likely have changed.

Currently with_transaction will stop retrying the block and the commit once 120 seconds pass since the beginning of its execution. This time is not configurable and may change in a future driver version. Note that this does not guarantee the overall runtime of with_transactions will be 120 seconds or less - just that once 120 seconds of wall clock time have elapsed, further retry attempts will not be initiated.

A low level API is also available if more control over transactions is desired.

with_transaction takes the same options as start_transaction does, which are read concern, write concern and read preference:

session = client.start_session
session.with_transaction(
  read_concern: {level: :majority},
  write_concern: {w: 3},
  read: {mode: :primary}
) do
  collection.insert_one(hello: 'world')
end

Low Level API

A transaction can be started by calling the start_transaction method on a session:

session = client.start_session
session.start_transaction

It is also possible to specify read concern, write concern and read preference when starting a transaction:

session = client.start_session
session.start_transaction(
  read_concern: {level: :majority},
  write_concern: {w: 3},
  read: {mode: :primary})

To persist changes made in a transaction to the database, the transaction must be explicitly committed. If a session ends with an open transaction, the transaction is aborted. A transaction may also be aborted explicitly.

To commit or abort a transaction, call commit_transaction or abort_transaction on the session instance:

session.commit_transaction

session.abort_transaction

Note: an outstanding transaction can hold locks to various objects in the server, such as the database. For example, the drop call in the following snippet will hang for transactionLifetimeLimitSeconds seconds (default 60) until the server expires and aborts the transaction:

c1 = Mongo::Client.new(['127.0.0.1:27017']).use(:test_db)
session = c1.start_session
c1['foo'].insert_one(test: 1)
session.start_transaction
c1['foo'].insert_one({test: 2}, session: session)

c2 = Mongo::Client.new(['127.0.0.1:27017']).use(:test_db)
# hangs
c2.database.drop

Since transactions are associated with server-side sessions, closing the client does not abort a transaction that this client initiated - the application must either call abort_transaction or wait for the transaction to time out on the server side. In addition to committing or aborting the transaction, an application can also end the session which will abort a transaction on this session if one is in progress:

session.end_session

c2 = Mongo::Client.new(['127.0.0.1:27017']).use(:test_db)
# ok
c2.database.drop

Retrying Commits

The transaction commit can be retried if it fails. Here is the Ruby code to do so:

begin
  session.commit_transaction
rescue Mongo::Error => e
  if e.label?(Mongo::Error::UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)
    retry
  else
    raise
  end
end

Transaction Nesting

MongoDB does not support nesting transactions. Attempting to call start_transaction or with_transaction when a transaction is already in progress will result in an error.

←   Sessions Change Streams  →