mongodb/mongo-ruby-driver

View on GitHub
docs/reference/transactions.txt

Summary

Maintainability
Test Coverage
************
Transactions
************

.. default-domain:: mongodb

.. contents:: On this page
   :local:
   :backlinks: none
   :depth: 1
   :class: singlecol


Version 4.0 of the MongoDB server introduces
`multi-document transactions <https://mongodb.com/docs/manual/core/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:

Using Transactions
==================

In order to start a transaction, the application must have a :ref:`session <sessions>`.

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

.. code-block:: ruby

  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:

.. code-block:: ruby

  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.

.. code-block:: ruby

  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:

.. code-block:: ruby

  session = client.start_session
  session.start_transaction

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

.. code-block:: ruby

  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 <https://mongodb.com/docs/manual/core/transactions/#transactions-and-sessions>`_.
A transaction may also be aborted explicitly.

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

.. code-block:: ruby

  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
<https://mongodb.com/docs/manual/reference/parameters/#param.transactionLifetimeLimitSeconds>`_
seconds (default 60) until the server expires and aborts the transaction:

.. code-block:: ruby

  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:

.. code-block:: ruby

  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
<https://mongodb.com/docs/manual/core/transactions/#retry-commit-operation>`_
if it fails. Here is the Ruby code to do so:

.. code-block:: ruby

  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.