Docs Menu
Docs Home
/ / /
Ruby MongoDB Driver
/

Transactions

On this page

  • Using Transactions
  • Low Level API
  • Retrying Commits
  • Transaction Nesting

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.

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'}, session: session)
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'}, session: session)
end

If a command inside the with_transaction block fails, it may cause the transaction on the server to be aborted. This situation is normally handled transparently by the driver. However, if the application catches such an error and does not re-raise it, the driver will not be able to determine whether the transaction was aborted or not. The driver will then retry the block indefinitely.

To avoid this situation, the application must not silently handle errors within with_transaction block. If the application needs to handle errors within the block, it must re-raise the errors.

session.with_transaction do
collection.insert_one({hello: 'world'}, session: session)
rescue Mongo::Error::OperationFailure => e
# Do something in response to the error
raise e
end

If the applications needs to handle errors in a custom way, it should use the low level API instead.

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

If a command inside the transaction fails, the transaction may be aborted on the server. Errors that abort transactions do not have TransientTransactionError in their error labels. An attempt to commit such a transaction will be rejected with NoSuchTransaction error.

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?('UnknownTransactionCommitResult')
retry
else
raise
end
end

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.

Back

Sessions