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'}, 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
Handling Errors Within the with_transaction
Block
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.
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
Handling Errors
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.
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?('UnknownTransactionCommitResult') 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.