4444# Types needed only for Type Hints
4545from google .cloud .firestore_v1 .base_document import DocumentSnapshot
4646from google .cloud .firestore_v1 .types import CommitResponse
47- from typing import Any , Callable , Generator , Optional
47+ from typing import Any , Callable , Generator
4848
4949
5050class Transaction (batch .WriteBatch , BaseTransaction ):
@@ -108,6 +108,7 @@ def _rollback(self) -> None:
108108
109109 Raises:
110110 ValueError: If no transaction is in progress.
111+ google.api_core.exceptions.GoogleAPICallError: If the rollback fails.
111112 """
112113 if not self .in_progress :
113114 raise ValueError (_CANT_ROLLBACK )
@@ -122,6 +123,7 @@ def _rollback(self) -> None:
122123 metadata = self ._client ._rpc_metadata ,
123124 )
124125 finally :
126+ # clean up, even if rollback fails
125127 self ._clean_up ()
126128
127129 def _commit (self ) -> list :
@@ -214,10 +216,6 @@ def __init__(self, to_wrap) -> None:
214216 def _pre_commit (self , transaction : Transaction , * args , ** kwargs ) -> Any :
215217 """Begin transaction and call the wrapped callable.
216218
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-
221219 Args:
222220 transaction
223221 (:class:`~google.cloud.firestore_v1.transaction.Transaction`):
@@ -241,41 +239,7 @@ def _pre_commit(self, transaction: Transaction, *args, **kwargs) -> Any:
241239 self .current_id = transaction ._id
242240 if self .retry_id is None :
243241 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 )
279243
280244 def __call__ (self , transaction : Transaction , * args , ** kwargs ):
281245 """Execute the wrapped callable within a transaction.
@@ -297,22 +261,34 @@ def __call__(self, transaction: Transaction, *args, **kwargs):
297261 ``max_attempts``.
298262 """
299263 self ._reset ()
264+ retryable_exceptions = (
265+ (exceptions .Aborted ) if not transaction ._read_only else ()
266+ )
267+ last_exc = None
300268
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
316292
317293
318294def transactional (to_wrap : Callable ) -> _Transactional :
0 commit comments