forked from mongodb/mongo-ruby-driver
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtransactions.txt
207 lines (151 loc) · 6.7 KB
/
transactions.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
************
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.