44# license that can be found in the LICENSE file or at
55# https://developers.google.com/open-source/licenses/bsd
66
7+ import time
8+
79import google .api_core .exceptions as grpc_exceptions
810
911from .exceptions import (
@@ -26,6 +28,7 @@ def __init__(self, db_handle):
2628 self .__connection = db_handle
2729 self .__last_op = None
2830 self .__closed = False
31+ self .__sql_in_same_txn = []
2932
3033 # arraysize is a readable and writable property mandated
3134 # by PEP-0249 https://www.python.org/dev/peps/pep-0249/#arraysize
@@ -64,7 +67,7 @@ def __discard_aborted_txn(self):
6467 def __get_txn (self ):
6568 return self .__connection .get_txn ()
6669
67- def execute (self , sql , args = None , current_retry = 0 ):
70+ def execute (self , sql , args = None , already_in_retry = False ):
6871 """
6972 Abstracts and implements execute SQL statements on Cloud Spanner.
7073 If it encounters grpc_exceptions.Aborted error, it optimistically retries
@@ -102,21 +105,79 @@ def execute(self, sql, args=None, current_retry=0):
102105 self .__handle_insert (self .__get_txn (), sql , args or None )
103106 else :
104107 self .__handle_update (self .__get_txn (), sql , args or None )
108+
109+ except grpc_exceptions .InvalidArgument as e : # We can't retry a syntax issue, fail fast.
110+ self .__discard_aborted_txn ()
111+ raise ProgrammingError (e .details if hasattr (e , 'details' ) else e )
112+
105113 except (grpc_exceptions .AlreadyExists , grpc_exceptions .FailedPrecondition ) as e :
114+ # We can't retry an integrity error within the same transaction regardless.
115+ self .__discard_aborted_txn ()
106116 raise IntegrityError (e .details if hasattr (e , 'details' ) else e )
117+
118+ except Exception as e :
119+ # Firstly discard the aborted transaction.
120+ self .__discard_aborted_txn ()
121+
122+ if already_in_retry : # It is already being retried, so return immediately.
123+ raise e
124+
125+ # Attempt to replay all the prior sql within the same transaction.
126+ sql_args_tuples = self .__sql_in_same_txn [:]
127+ sql_args_tuples .append ((sql , args ,))
128+
129+ return self .__replay_all_prior_statements_in_transaction (sql_args_tuples )
130+ else : # No error here
131+ self .__sql_in_same_txn .append ((sql , args ,))
132+
133+ def _clear_transaction_state (self ):
134+ """
135+ Invoked on every Connection.commit() or Connection.rollback()
136+ """
137+ if self .__sql_in_same_txn :
138+ self .__sql_in_same_txn .clear ()
139+
140+ def __replay_all_prior_statements_in_transaction (self , sql_args_tuples ):
141+ if not sql_args_tuples :
142+ return
143+
144+ lastException = None
145+
146+ for i in range (5 ):
147+ # Clean up before attempting the replay.
148+ self .__sql_in_same_txn .clear ()
149+
150+ print ("\033 [31mAttempting transaction replay #%d with elements:\n %s\033 [00m" % (i , sql_args_tuples ))
151+
152+ for sql , args in sql_args_tuples :
153+ try :
154+ self .execute (sql , args , already_in_retry = True )
155+ except grpc_exceptions .InvalidArgument as e : # We can't retry a syntax issue, fail fast.
156+ raise ProgrammingError (e .details if hasattr (e , 'details' ) else e )
157+ except (grpc_exceptions .AlreadyExists , grpc_exceptions .FailedPrecondition ) as e :
158+ raise IntegrityError (e .details if hasattr (e , 'details' ) else e )
159+ except Exception as e :
160+ lastException = e
161+ # TODO: Use exponential backoff with jitter, before retrying.
162+ time .sleep (0.57 )
163+ break
164+ else :
165+ # All the elements in sql_args_tuples were executed,
166+ # thus we can now break out of the retry loop.
167+ # But first, reset all the executed (sql, args) for future replay.
168+ self .__sql_in_same_txn = sql_args_tuples [:]
169+ break
170+
171+ try :
172+ if lastException :
173+ self .__discard_aborted_txn ()
174+ raise lastException
107175 except grpc_exceptions .InvalidArgument as e :
108176 raise ProgrammingError (e .details if hasattr (e , 'details' ) else e )
109177 except grpc_exceptions .InternalServerError as e :
110178 raise OperationalError (e .details if hasattr (e , 'details' ) else e )
111179 except grpc_exceptions .Aborted as e :
112- if current_retry > 2 : # Arbitrary limit that should probably be a setting.
113- raise InternalError (e .details if hasattr (e , 'details' ) else e )
114-
115- self .__discard_aborted_txn ()
116- # Otherwise retry it.
117- print ('\033 [31mRetrying execution #%d of:\n SQL: %s\n Args: %s\033 [00m' % (
118- current_retry , sql , args ))
119- return self .execute (sql , args , current_retry = current_retry + 1 )
180+ raise InternalError (e .details if hasattr (e , 'details' ) else e )
120181
121182 def __handle_update (self , txn , sql , params , param_types = None ):
122183 sql = ensure_where_clause (sql )
0 commit comments