44
44
# Types needed only for Type Hints
45
45
from google .cloud .firestore_v1 .base_document import DocumentSnapshot
46
46
from google .cloud .firestore_v1 .types import CommitResponse
47
- from typing import Any , Callable , Generator , Optional
47
+ from typing import Any , Callable , Generator
48
48
49
49
50
50
class Transaction (batch .WriteBatch , BaseTransaction ):
@@ -108,6 +108,7 @@ def _rollback(self) -> None:
108
108
109
109
Raises:
110
110
ValueError: If no transaction is in progress.
111
+ google.api_core.exceptions.GoogleAPICallError: If the rollback fails.
111
112
"""
112
113
if not self .in_progress :
113
114
raise ValueError (_CANT_ROLLBACK )
@@ -122,6 +123,7 @@ def _rollback(self) -> None:
122
123
metadata = self ._client ._rpc_metadata ,
123
124
)
124
125
finally :
126
+ # clean up, even if rollback fails
125
127
self ._clean_up ()
126
128
127
129
def _commit (self ) -> list :
@@ -214,10 +216,6 @@ def __init__(self, to_wrap) -> None:
214
216
def _pre_commit (self , transaction : Transaction , * args , ** kwargs ) -> Any :
215
217
"""Begin transaction and call the wrapped callable.
216
218
217
- If the callable raises an exception, the transaction will be rolled
218
- back. If not, the transaction will be "ready" for ``Commit`` (i.e.
219
- it will have staged writes).
220
-
221
219
Args:
222
220
transaction
223
221
(:class:`~google.cloud.firestore_v1.transaction.Transaction`):
@@ -241,41 +239,7 @@ def _pre_commit(self, transaction: Transaction, *args, **kwargs) -> Any:
241
239
self .current_id = transaction ._id
242
240
if self .retry_id is None :
243
241
self .retry_id = self .current_id
244
- try :
245
- return self .to_wrap (transaction , * args , ** kwargs )
246
- except : # noqa
247
- # NOTE: If ``rollback`` fails this will lose the information
248
- # from the original failure.
249
- transaction ._rollback ()
250
- raise
251
-
252
- def _maybe_commit (self , transaction : Transaction ) -> Optional [bool ]:
253
- """Try to commit the transaction.
254
-
255
- If the transaction is read-write and the ``Commit`` fails with the
256
- ``ABORTED`` status code, it will be retried. Any other failure will
257
- not be caught.
258
-
259
- Args:
260
- transaction
261
- (:class:`~google.cloud.firestore_v1.transaction.Transaction`):
262
- The transaction to be ``Commit``-ed.
263
-
264
- Returns:
265
- bool: Indicating if the commit succeeded.
266
- """
267
- try :
268
- transaction ._commit ()
269
- return True
270
- except exceptions .GoogleAPICallError as exc :
271
- if transaction ._read_only :
272
- raise
273
-
274
- if isinstance (exc , exceptions .Aborted ):
275
- # If a read-write transaction returns ABORTED, retry.
276
- return False
277
- else :
278
- raise
242
+ return self .to_wrap (transaction , * args , ** kwargs )
279
243
280
244
def __call__ (self , transaction : Transaction , * args , ** kwargs ):
281
245
"""Execute the wrapped callable within a transaction.
@@ -297,22 +261,34 @@ def __call__(self, transaction: Transaction, *args, **kwargs):
297
261
``max_attempts``.
298
262
"""
299
263
self ._reset ()
264
+ retryable_exceptions = (
265
+ (exceptions .Aborted ) if not transaction ._read_only else ()
266
+ )
267
+ last_exc = None
300
268
301
- for attempt in range (transaction ._max_attempts ):
302
- result = self ._pre_commit (transaction , * args , ** kwargs )
303
- succeeded = self ._maybe_commit (transaction )
304
- if succeeded :
305
- return result
306
-
307
- # Subsequent requests will use the failed transaction ID as part of
308
- # the ``BeginTransactionRequest`` when restarting this transaction
309
- # (via ``options.retry_transaction``). This preserves the "spot in
310
- # line" of the transaction, so exponential backoff is not required
311
- # in this case.
312
-
313
- transaction ._rollback ()
314
- msg = _EXCEED_ATTEMPTS_TEMPLATE .format (transaction ._max_attempts )
315
- raise ValueError (msg )
269
+ try :
270
+ for attempt in range (transaction ._max_attempts ):
271
+ result = self ._pre_commit (transaction , * args , ** kwargs )
272
+ try :
273
+ transaction ._commit ()
274
+ return result
275
+ except retryable_exceptions as exc :
276
+ last_exc = exc
277
+ # Retry attempts that result in retryable exceptions
278
+ # Subsequent requests will use the failed transaction ID as part of
279
+ # the ``BeginTransactionRequest`` when restarting this transaction
280
+ # (via ``options.retry_transaction``). This preserves the "spot in
281
+ # line" of the transaction, so exponential backoff is not required
282
+ # in this case.
283
+ # retries exhausted
284
+ # wrap the last exception in a ValueError before raising
285
+ msg = _EXCEED_ATTEMPTS_TEMPLATE .format (transaction ._max_attempts )
286
+ raise ValueError (msg ) from last_exc
287
+ except BaseException : # noqa: B901
288
+ # rollback the transaction on any error
289
+ # errors raised during _rollback will be chained to the original error through __context__
290
+ transaction ._rollback ()
291
+ raise
316
292
317
293
318
294
def transactional (to_wrap : Callable ) -> _Transactional :
0 commit comments