| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 0da5f78 commit 00d5f8e
14 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -104,6 +104,11 @@ def run_batch_dml(cursor: "Cursor", statements: List[Statement]): | |||
| 104 | 104 | connection._transaction = None | |
| 105 | 105 | raise Aborted(status.message) | |
| 106 | 106 | elif status.code != OK: | |
| 107 | + if not transaction._transaction_id: | ||
| 108 | + # This should normally not happen, | ||
| 109 | + # but we safeguard against it just to be sure. | ||
| 110 | + transaction._reset_and_begin() | ||
| 111 | + continue | ||
| 107 | 112 | raise OperationalError(status.message) | |
| 108 | 113 | ||
| 109 | 114 | cursor._batch_dml_rows_count = res | |
@@ -116,6 +121,11 @@ def run_batch_dml(cursor: "Cursor", statements: List[Statement]): | |||
| 116 | 121 | raise | |
| 117 | 122 | else: | |
| 118 | 123 | connection._transaction_helper.retry_transaction() | |
| 124 | + except Exception as ex: | ||
| 125 | + if not transaction._transaction_id: | ||
| 126 | + transaction._reset_and_begin() | ||
| 127 | + continue | ||
| 128 | + raise ex | ||
| 119 | 129 | ||
| 120 | 130 | ||
| 121 | 131 | def _do_batch_update_autocommit(transaction, statements): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -392,7 +392,14 @@ def transaction_checkout(self): | |||
| 392 | 392 | this connection yet. Return the started one otherwise. | |
| 393 | 393 | ||
| 394 | 394 | This method is a no-op if the connection is in autocommit mode and no | |
| 395 | - explicit transaction has been started | ||
| 395 | + explicit transaction has been started. | ||
| 396 | + | ||
| 397 | + The transaction is returned without calling ``begin()``. The | ||
| 398 | + underlying ``Transaction.execute_sql`` and ``execute_update`` | ||
| 399 | + methods detect ``_transaction_id is None`` and use *inline begin* | ||
| 400 | + — piggybacking a ``BeginTransaction`` on the first RPC via | ||
| 401 | + ``TransactionSelector(begin=...)``. This eliminates a separate | ||
| 402 | + ``BeginTransaction`` RPC round-trip per transaction. | ||
| 396 | 403 | ||
| 397 | 404 | :rtype: :class:`google.cloud.spanner_v1.transaction.Transaction` | |
| 398 | 405 | :returns: A Cloud Spanner transaction object, ready to use. | |
@@ -410,7 +417,6 @@ def transaction_checkout(self): | |||
| 410 | 417 | self.transaction_tag = None | |
| 411 | 418 | self._snapshot = None | |
| 412 | 419 | self._spanner_transaction_started = True | |
| 413 | - self._transaction.begin() | ||
| 414 | 420 | ||
| 415 | 421 | return self._transaction | |
| 416 | 422 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -366,6 +366,16 @@ def _execute_in_rw_transaction(self): | |||
| 366 | 366 | raise | |
| 367 | 367 | else: | |
| 368 | 368 | self.transaction_helper.retry_transaction() | |
| 369 | + except Exception as ex: | ||
| 370 | + # In case of inline-begin failure, the transaction isn't started. | ||
| 371 | + # We immediately retry with an explicit BeginTransaction. | ||
| 372 | + transaction = getattr(self.connection, "_transaction", None) | ||
| 373 | + if transaction and not transaction._transaction_id: | ||
| 374 | + transaction._reset_and_begin() | ||
| 375 | + | ||
| 376 | + # Let the existing retry loop handle the retry of the statement | ||
| 377 | + continue | ||
| 378 | + raise ex | ||
| 369 | 379 | else: | |
| 370 | 380 | self.connection.database.run_in_transaction( | |
| 371 | 381 | self._do_execute_update_in_autocommit, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -155,8 +155,6 @@ def _build_multiplexed_session(self) -> Session: | |||
| 155 | 155 | ) | |
| 156 | 156 | session.create() | |
| 157 | 157 | ||
| 158 | - self._database.logger.info("Created multiplexed session.") | ||
| 159 | - | ||
| 160 | 158 | return session | |
| 161 | 159 | ||
| 162 | 160 | def _build_maintenance_thread(self) -> Thread: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,9 +15,11 @@ | |||
| 15 | 15 | import inspect | |
| 16 | 16 | import grpc | |
| 17 | 17 | from concurrent import futures | |
| 18 | + from dataclasses import dataclass | ||
| 18 | 19 | ||
| 19 | - from google.protobuf import empty_pb2 | ||
| 20 | 20 | from grpc_status.rpc_status import _Status | |
| 21 | + from google.rpc.code_pb2 import OK | ||
| 22 | + from google.protobuf import empty_pb2 | ||
| 21 | 23 | ||
| 22 | 24 | from google.cloud.spanner_v1 import ( | |
| 23 | 25 | TransactionOptions, | |
@@ -53,10 +55,23 @@ def get_result(self, sql: str) -> result_set.ResultSet: | |||
| 53 | 55 | return result | |
| 54 | 56 | ||
| 55 | 57 | def add_error(self, method: str, error: _Status): | |
| 58 | + if not hasattr(self, "_errors_list"): | ||
| 59 | + self._errors_list = {} | ||
| 60 | + if method not in self._errors_list: | ||
| 61 | + self._errors_list[method] = [] | ||
| 62 | + self._errors_list[method].append(error) | ||
| 56 | 63 | self.errors[method] = error | |
| 57 | 64 | ||
| 58 | 65 | def pop_error(self, context): | |
| 59 | 66 | name = inspect.currentframe().f_back.f_code.co_name | |
| 67 | + if hasattr(self, "_errors_list") and name in self._errors_list: | ||
| 68 | + if self._errors_list[name]: | ||
| 69 | + error = self._errors_list[name].pop(0) | ||
| 70 | + context.abort_with_status(error) | ||
| 71 | + return | ||
| 72 | + return # Queue is empty, return normally (no error) | ||
| 73 | + | ||
| 74 | + # Fallback to single error | ||
| 60 | 75 | error: _Status | None = self.errors.pop(name, None) | |
| 61 | 76 | if error: | |
| 62 | 77 | context.abort_with_status(error) | |
@@ -94,6 +109,12 @@ def get_result_as_partial_result_sets( | |||
| 94 | 109 | return partials | |
| 95 | 110 | ||
| 96 | 111 | ||
| 112 | + @dataclass | ||
| 113 | + class BatchDmlResponseConfig: | ||
| 114 | + status: _Status | ||
| 115 | + include_transaction_id: bool = True | ||
| 116 | + | ||
| 117 | + | ||
| 97 | 118 | # An in-memory mock Spanner server that can be used for testing. | |
| 98 | 119 | class SpannerServicer(spanner_grpc.SpannerServicer): | |
| 99 | 120 | def __init__(self): | |
@@ -103,6 +124,7 @@ def __init__(self): | |||
| 103 | 124 | self.transaction_counter = 0 | |
| 104 | 125 | self.transactions = {} | |
| 105 | 126 | self._mock_spanner = MockSpanner() | |
| 127 | + self._batch_dml_response_configs = [] | ||
| 106 | 128 | ||
| 107 | 129 | @property | |
| 108 | 130 | def mock_spanner(self): | |
@@ -115,6 +137,15 @@ def requests(self): | |||
| 115 | 137 | def clear_requests(self): | |
| 116 | 138 | self._requests = [] | |
| 117 | 139 | ||
| 140 | + def add_batch_dml_response_status(self, status, include_transaction_id=True): | ||
| 141 | + if not hasattr(self, "_batch_dml_response_configs"): | ||
| 142 | + self._batch_dml_response_configs = [] | ||
| 143 | + self._batch_dml_response_configs.append( | ||
| 144 | + BatchDmlResponseConfig( | ||
| 145 | + status=status, include_transaction_id=include_transaction_id | ||
| 146 | + ) | ||
| 147 | + ) | ||
| 148 | + | ||
| 118 | 149 | def CreateSession(self, request, context): | |
| 119 | 150 | self._requests.append(request) | |
| 120 | 151 | return self.__create_session(request.database, request.session) | |
@@ -176,6 +207,14 @@ def ExecuteBatchDml(self, request, context): | |||
| 176 | 207 | self.mock_spanner.pop_error(context) | |
| 177 | 208 | response = spanner.ExecuteBatchDmlResponse() | |
| 178 | 209 | started_transaction = self.__maybe_create_transaction(request) | |
| 210 | + | ||
| 211 | + config = None | ||
| 212 | + if ( | ||
| 213 | + hasattr(self, "_batch_dml_response_configs") | ||
| 214 | + and self._batch_dml_response_configs | ||
| 215 | + ): | ||
| 216 | + config = self._batch_dml_response_configs.pop(0) | ||
| 217 | + | ||
| 179 | 218 | first = True | |
| 180 | 219 | for statement in request.statements: | |
| 181 | 220 | result = self.mock_spanner.get_result(statement.sql) | |
@@ -184,8 +223,16 @@ def ExecuteBatchDml(self, request, context): | |||
| 184 | 223 | self.mock_spanner.get_result(statement.sql) | |
| 185 | 224 | ) | |
| 186 | 225 | result.metadata = result_set.ResultSetMetadata(result.metadata) | |
| 187 | - result.metadata.transaction = started_transaction | ||
| 226 | + if config is None or config.include_transaction_id: | ||
| 227 | + result.metadata.transaction = started_transaction | ||
| 228 | + first = False | ||
| 188 | 229 | response.result_sets.append(result) | |
| 230 | + | ||
| 231 | + if config is not None: | ||
| 232 | + response.status.CopyFrom(config.status) | ||
| 233 | + else: | ||
| 234 | + response.status.code = OK | ||
| 235 | + | ||
| 189 | 236 | return response | |
| 190 | 237 | ||
| 191 | 238 | def Read(self, request, context): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -214,6 +214,12 @@ def wrapped_method(*args, **kwargs): | |||
| 214 | 214 | ||
| 215 | 215 | self.rolled_back = True | |
| 216 | 216 | ||
| 217 | + def _reset_and_begin(self): | ||
| 218 | + """This function can be used to reset the transaction and execute an explicit BeginTransaction RPC if the first statement in the transaction failed, and that statement included an inlined BeginTransaction option.""" | ||
| 219 | + self._read_request_count = 0 | ||
| 220 | + self._execute_sql_request_count = 0 | ||
| 221 | + self.begin() | ||
| 222 | + | ||
| 217 | 223 | def commit( | |
| 218 | 224 | self, return_commit_stats=False, request_options=None, max_commit_delay=None | |
| 219 | 225 | ): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,6 +12,7 @@ | |||
| 12 | 12 | # See the License for the specific language governing permissions and | |
| 13 | 13 | # limitations under the License. | |
| 14 | 14 | import logging | |
| 15 | + import os | ||
| 15 | 16 | import unittest | |
| 16 | 17 | ||
| 17 | 18 | import grpc | |
@@ -65,6 +66,19 @@ def aborted_status() -> _Status: | |||
| 65 | 66 | return status | |
| 66 | 67 | ||
| 67 | 68 | ||
| 69 | + def invalid_argument_status() -> _Status: | ||
| 70 | + error = status_pb2.Status( | ||
| 71 | + code=code_pb2.INVALID_ARGUMENT, | ||
| 72 | + message="Invalid argument.", | ||
| 73 | + ) | ||
| 74 | + status = _Status( | ||
| 75 | + code=code_to_grpc_status_code(error.code), | ||
| 76 | + details=error.message, | ||
| 77 | + trailing_metadata=(("grpc-status-details-bin", error.SerializeToString()),), | ||
| 78 | + ) | ||
| 79 | + return status | ||
| 80 | + | ||
| 81 | + | ||
| 68 | 82 | def _make_partial_result_sets( | |
| 69 | 83 | fields: list[tuple[str, TypeCode]], results: list[dict] | |
| 70 | 84 | ) -> list[result_set.PartialResultSet]: | |
@@ -174,6 +188,9 @@ class MockServerTestBase(unittest.TestCase): | |||
| 174 | 188 | ||
| 175 | 189 | def __init__(self, *args, **kwargs): | |
| 176 | 190 | super(MockServerTestBase, self).__init__(*args, **kwargs) | |
| 191 | + # Disable built-in metrics for tests to avoid Unauthenticated errors | ||
| 192 | + os.environ["SPANNER_DISABLE_BUILTIN_METRICS"] = "true" | ||
| 193 | + | ||
| 177 | 194 | self._client = None | |
| 178 | 195 | self._instance = None | |
| 179 | 196 | self._database = None | |
| Back | FazBrowse Home | New Git URL |
0 commit comments