def begin(self): """Begin a transaction on the database. :rtype: bytes :returns: the ID for the newly-begun transaction. :raises ValueError: if the transaction is already begun, committed, or rolled back. """ if self._transaction_id is not None: raise ValueError("Transaction already begun") if self.committed is not None: raise ValueError("Transaction already committed") if self.rolled_back: raise ValueError("Transaction is already rolled back") database = self._session._database api = database.spanner_api metadata = _metadata_with_prefix(database.name) txn_options = TransactionOptions( read_write=TransactionOptions.ReadWrite()) with trace_call("CloudSpanner.BeginTransaction", self._session): response = api.begin_transaction(session=self._session.name, options=txn_options, metadata=metadata) self._transaction_id = response.id return self._transaction_id
def commit(self, return_commit_stats=False): """Commit mutations to the database. :type return_commit_stats: bool :param return_commit_stats: If true, the response will return commit stats which can be accessed though commit_stats. :rtype: datetime :returns: timestamp of the committed changes. """ self._check_state() database = self._session._database api = database.spanner_api metadata = _metadata_with_prefix(database.name) txn_options = TransactionOptions( read_write=TransactionOptions.ReadWrite()) trace_attributes = {"num_mutations": len(self._mutations)} request = CommitRequest( session=self._session.name, mutations=self._mutations, single_use_transaction=txn_options, return_commit_stats=return_commit_stats, ) with trace_call("CloudSpanner.Commit", self._session, trace_attributes): response = api.commit( request=request, metadata=metadata, ) self.committed = response.commit_timestamp self.commit_stats = response.commit_stats return self.committed
def test_run_in_transaction_callback_raises_non_gax_error(self): from google.cloud.spanner_v1 import ( Transaction as TransactionPB, TransactionOptions, ) from google.cloud.spanner_v1.transaction import Transaction TABLE_NAME = "citizens" COLUMNS = ["email", "first_name", "last_name", "age"] VALUES = [ ["*****@*****.**", "Phred", "Phlyntstone", 32], ["*****@*****.**", "Bharney", "Rhubble", 31], ] TRANSACTION_ID = b"FACEDACE" transaction_pb = TransactionPB(id=TRANSACTION_ID) gax_api = self._make_spanner_api() gax_api.begin_transaction.return_value = transaction_pb gax_api.rollback.return_value = None database = self._make_database() database.spanner_api = gax_api session = self._make_one(database) session._session_id = self.SESSION_ID called_with = [] class Testing(Exception): pass def unit_of_work(txn, *args, **kw): called_with.append((txn, args, kw)) txn.insert(TABLE_NAME, COLUMNS, VALUES) raise Testing() with self.assertRaises(Testing): session.run_in_transaction(unit_of_work) self.assertIsNone(session._transaction) self.assertEqual(len(called_with), 1) txn, args, kw = called_with[0] self.assertIsInstance(txn, Transaction) self.assertIsNone(txn.committed) self.assertTrue(txn.rolled_back) self.assertEqual(args, ()) self.assertEqual(kw, {}) expected_options = TransactionOptions( read_write=TransactionOptions.ReadWrite()) gax_api.begin_transaction.assert_called_once_with( session=self.SESSION_NAME, options=expected_options, metadata=[("google-cloud-resource-prefix", database.name)], ) gax_api.rollback.assert_called_once_with( session=self.SESSION_NAME, transaction_id=TRANSACTION_ID, metadata=[("google-cloud-resource-prefix", database.name)], )
def commit(self, return_commit_stats=False, request_options=None): """Commit mutations to the database. :type return_commit_stats: bool :param return_commit_stats: If true, the response will return commit stats which can be accessed though commit_stats. :type request_options: :class:`google.cloud.spanner_v1.types.RequestOptions` :param request_options: (Optional) Common options for this request. If a dict is provided, it must be of the same form as the protobuf message :class:`~google.cloud.spanner_v1.types.RequestOptions`. :rtype: datetime :returns: timestamp of the committed changes. """ self._check_state() database = self._session._database api = database.spanner_api metadata = _metadata_with_prefix(database.name) txn_options = TransactionOptions( read_write=TransactionOptions.ReadWrite()) trace_attributes = {"num_mutations": len(self._mutations)} if request_options is None: request_options = RequestOptions() elif type(request_options) == dict: request_options = RequestOptions(request_options) request_options.transaction_tag = self.transaction_tag # Request tags are not supported for commit requests. request_options.request_tag = None request = CommitRequest( session=self._session.name, mutations=self._mutations, single_use_transaction=txn_options, return_commit_stats=return_commit_stats, request_options=request_options, ) with trace_call("CloudSpanner.Commit", self._session, trace_attributes): response = api.commit( request=request, metadata=metadata, ) self.committed = response.commit_timestamp self.commit_stats = response.commit_stats return self.committed
def commit(self): """Commit mutations to the database. :rtype: datetime :returns: timestamp of the committed changes. """ self._check_state() database = self._session._database api = database.spanner_api metadata = _metadata_with_prefix(database.name) txn_options = TransactionOptions(read_write=TransactionOptions.ReadWrite()) trace_attributes = {"num_mutations": len(self._mutations)} with trace_call("CloudSpanner.Commit", self._session, trace_attributes): response = api.commit( session=self._session.name, mutations=self._mutations, single_use_transaction=txn_options, metadata=metadata, ) self.committed = response.commit_timestamp return self.committed
def test_run_in_transaction_w_abort_w_retry_metadata(self): import datetime from google.api_core.exceptions import Aborted from google.protobuf.duration_pb2 import Duration from google.rpc.error_details_pb2 import RetryInfo from google.cloud.spanner_v1 import CommitResponse from google.cloud.spanner_v1 import ( Transaction as TransactionPB, TransactionOptions, ) from google.cloud._helpers import UTC from google.cloud._helpers import _datetime_to_pb_timestamp from google.cloud.spanner_v1.transaction import Transaction TABLE_NAME = "citizens" COLUMNS = ["email", "first_name", "last_name", "age"] VALUES = [ ["*****@*****.**", "Phred", "Phlyntstone", 32], ["*****@*****.**", "Bharney", "Rhubble", 31], ] TRANSACTION_ID = b"FACEDACE" RETRY_SECONDS = 12 RETRY_NANOS = 3456 retry_info = RetryInfo( retry_delay=Duration(seconds=RETRY_SECONDS, nanos=RETRY_NANOS)) trailing_metadata = [("google.rpc.retryinfo-bin", retry_info.SerializeToString())] aborted = _make_rpc_error(Aborted, trailing_metadata=trailing_metadata) transaction_pb = TransactionPB(id=TRANSACTION_ID) now = datetime.datetime.utcnow().replace(tzinfo=UTC) now_pb = _datetime_to_pb_timestamp(now) response = CommitResponse(commit_timestamp=now_pb) gax_api = self._make_spanner_api() gax_api.begin_transaction.return_value = transaction_pb gax_api.commit.side_effect = [aborted, response] database = self._make_database() database.spanner_api = gax_api session = self._make_one(database) session._session_id = self.SESSION_ID called_with = [] def unit_of_work(txn, *args, **kw): called_with.append((txn, args, kw)) txn.insert(TABLE_NAME, COLUMNS, VALUES) with mock.patch("time.sleep") as sleep_mock: session.run_in_transaction(unit_of_work, "abc", some_arg="def") sleep_mock.assert_called_once_with(RETRY_SECONDS + RETRY_NANOS / 1.0e9) self.assertEqual(len(called_with), 2) for index, (txn, args, kw) in enumerate(called_with): self.assertIsInstance(txn, Transaction) if index == 1: self.assertEqual(txn.committed, now) else: self.assertIsNone(txn.committed) self.assertEqual(args, ("abc", )) self.assertEqual(kw, {"some_arg": "def"}) expected_options = TransactionOptions( read_write=TransactionOptions.ReadWrite()) self.assertEqual( gax_api.begin_transaction.call_args_list, [ mock.call( session=self.SESSION_NAME, options=expected_options, metadata=[("google-cloud-resource-prefix", database.name)], ) ] * 2, ) self.assertEqual( gax_api.commit.call_args_list, [ mock.call( session=self.SESSION_NAME, mutations=txn._mutations, transaction_id=TRANSACTION_ID, metadata=[("google-cloud-resource-prefix", database.name)], ) ] * 2, )
def test_run_in_transaction_w_abort_no_retry_metadata(self): import datetime from google.api_core.exceptions import Aborted from google.cloud.spanner_v1 import CommitResponse from google.cloud.spanner_v1 import ( Transaction as TransactionPB, TransactionOptions, ) from google.cloud._helpers import UTC from google.cloud._helpers import _datetime_to_pb_timestamp from google.cloud.spanner_v1.transaction import Transaction TABLE_NAME = "citizens" COLUMNS = ["email", "first_name", "last_name", "age"] VALUES = [ ["*****@*****.**", "Phred", "Phlyntstone", 32], ["*****@*****.**", "Bharney", "Rhubble", 31], ] TRANSACTION_ID = b"FACEDACE" transaction_pb = TransactionPB(id=TRANSACTION_ID) now = datetime.datetime.utcnow().replace(tzinfo=UTC) now_pb = _datetime_to_pb_timestamp(now) aborted = _make_rpc_error(Aborted, trailing_metadata=[]) response = CommitResponse(commit_timestamp=now_pb) gax_api = self._make_spanner_api() gax_api.begin_transaction.return_value = transaction_pb gax_api.commit.side_effect = [aborted, response] database = self._make_database() database.spanner_api = gax_api session = self._make_one(database) session._session_id = self.SESSION_ID called_with = [] def unit_of_work(txn, *args, **kw): called_with.append((txn, args, kw)) txn.insert(TABLE_NAME, COLUMNS, VALUES) return "answer" return_value = session.run_in_transaction(unit_of_work, "abc", some_arg="def") self.assertEqual(len(called_with), 2) for index, (txn, args, kw) in enumerate(called_with): self.assertIsInstance(txn, Transaction) self.assertEqual(return_value, "answer") self.assertEqual(args, ("abc", )) self.assertEqual(kw, {"some_arg": "def"}) expected_options = TransactionOptions( read_write=TransactionOptions.ReadWrite()) self.assertEqual( gax_api.begin_transaction.call_args_list, [ mock.call( session=self.SESSION_NAME, options=expected_options, metadata=[("google-cloud-resource-prefix", database.name)], ) ] * 2, ) self.assertEqual( gax_api.commit.call_args_list, [ mock.call( session=self.SESSION_NAME, mutations=txn._mutations, transaction_id=TRANSACTION_ID, metadata=[("google-cloud-resource-prefix", database.name)], ) ] * 2, )
def test_run_in_transaction_w_args_w_kwargs_wo_abort(self): import datetime from google.cloud.spanner_v1 import CommitResponse from google.cloud.spanner_v1 import ( Transaction as TransactionPB, TransactionOptions, ) from google.cloud._helpers import UTC from google.cloud._helpers import _datetime_to_pb_timestamp from google.cloud.spanner_v1.transaction import Transaction TABLE_NAME = "citizens" COLUMNS = ["email", "first_name", "last_name", "age"] VALUES = [ ["*****@*****.**", "Phred", "Phlyntstone", 32], ["*****@*****.**", "Bharney", "Rhubble", 31], ] TRANSACTION_ID = b"FACEDACE" transaction_pb = TransactionPB(id=TRANSACTION_ID) now = datetime.datetime.utcnow().replace(tzinfo=UTC) now_pb = _datetime_to_pb_timestamp(now) response = CommitResponse(commit_timestamp=now_pb) gax_api = self._make_spanner_api() gax_api.begin_transaction.return_value = transaction_pb gax_api.commit.return_value = response database = self._make_database() database.spanner_api = gax_api session = self._make_one(database) session._session_id = self.SESSION_ID called_with = [] def unit_of_work(txn, *args, **kw): called_with.append((txn, args, kw)) txn.insert(TABLE_NAME, COLUMNS, VALUES) return 42 return_value = session.run_in_transaction(unit_of_work, "abc", some_arg="def") self.assertIsNone(session._transaction) self.assertEqual(len(called_with), 1) txn, args, kw = called_with[0] self.assertIsInstance(txn, Transaction) self.assertEqual(return_value, 42) self.assertEqual(args, ("abc", )) self.assertEqual(kw, {"some_arg": "def"}) expected_options = TransactionOptions( read_write=TransactionOptions.ReadWrite()) gax_api.begin_transaction.assert_called_once_with( session=self.SESSION_NAME, options=expected_options, metadata=[("google-cloud-resource-prefix", database.name)], ) gax_api.commit.assert_called_once_with( session=self.SESSION_NAME, mutations=txn._mutations, transaction_id=TRANSACTION_ID, metadata=[("google-cloud-resource-prefix", database.name)], )
def test_run_in_transaction_w_timeout(self): from google.api_core.exceptions import Aborted from google.cloud.spanner_v1 import ( Transaction as TransactionPB, TransactionOptions, ) from google.cloud.spanner_v1.transaction import Transaction TABLE_NAME = "citizens" COLUMNS = ["email", "first_name", "last_name", "age"] VALUES = [ ["*****@*****.**", "Phred", "Phlyntstone", 32], ["*****@*****.**", "Bharney", "Rhubble", 31], ] TRANSACTION_ID = b"FACEDACE" transaction_pb = TransactionPB(id=TRANSACTION_ID) aborted = _make_rpc_error(Aborted, trailing_metadata=[]) gax_api = self._make_spanner_api() gax_api.begin_transaction.return_value = transaction_pb gax_api.commit.side_effect = aborted database = self._make_database() database.spanner_api = gax_api session = self._make_one(database) session._session_id = self.SESSION_ID called_with = [] def unit_of_work(txn, *args, **kw): called_with.append((txn, args, kw)) txn.insert(TABLE_NAME, COLUMNS, VALUES) # retry several times to check backoff def _time(_results=[1, 2, 4, 8]): return _results.pop(0) with mock.patch("time.time", _time): if HAS_OPENTELEMETRY_INSTALLED: with mock.patch("opentelemetry.util.time", _ConstantTime()): with mock.patch("time.sleep") as sleep_mock: with self.assertRaises(Aborted): session.run_in_transaction(unit_of_work, timeout_secs=8) else: with mock.patch("time.sleep") as sleep_mock: with self.assertRaises(Aborted): session.run_in_transaction(unit_of_work, timeout_secs=8) # unpacking call args into list call_args = [call_[0][0] for call_ in sleep_mock.call_args_list] call_args = list(map(int, call_args)) assert call_args == [2, 4] assert sleep_mock.call_count == 2 self.assertEqual(len(called_with), 3) for txn, args, kw in called_with: self.assertIsInstance(txn, Transaction) self.assertIsNone(txn.committed) self.assertEqual(args, ()) self.assertEqual(kw, {}) expected_options = TransactionOptions( read_write=TransactionOptions.ReadWrite()) self.assertEqual( gax_api.begin_transaction.call_args_list, [ mock.call( session=self.SESSION_NAME, options=expected_options, metadata=[("google-cloud-resource-prefix", database.name)], ) ] * 3, ) self.assertEqual( gax_api.commit.call_args_list, [ mock.call( session=self.SESSION_NAME, mutations=txn._mutations, transaction_id=TRANSACTION_ID, metadata=[("google-cloud-resource-prefix", database.name)], ) ] * 3, )
def test_run_in_transaction_w_abort_w_retry_metadata_deadline(self): import datetime from google.api_core.exceptions import Aborted from google.protobuf.duration_pb2 import Duration from google.rpc.error_details_pb2 import RetryInfo from google.cloud.spanner_v1 import CommitResponse from google.cloud.spanner_v1 import ( Transaction as TransactionPB, TransactionOptions, ) from google.cloud.spanner_v1.transaction import Transaction from google.cloud._helpers import UTC from google.cloud._helpers import _datetime_to_pb_timestamp TABLE_NAME = "citizens" COLUMNS = ["email", "first_name", "last_name", "age"] VALUES = [ ["*****@*****.**", "Phred", "Phlyntstone", 32], ["*****@*****.**", "Bharney", "Rhubble", 31], ] TRANSACTION_ID = b"FACEDACE" RETRY_SECONDS = 1 RETRY_NANOS = 3456 transaction_pb = TransactionPB(id=TRANSACTION_ID) now = datetime.datetime.utcnow().replace(tzinfo=UTC) now_pb = _datetime_to_pb_timestamp(now) response = CommitResponse(commit_timestamp=now_pb) retry_info = RetryInfo( retry_delay=Duration(seconds=RETRY_SECONDS, nanos=RETRY_NANOS)) trailing_metadata = [("google.rpc.retryinfo-bin", retry_info.SerializeToString())] aborted = _make_rpc_error(Aborted, trailing_metadata=trailing_metadata) gax_api = self._make_spanner_api() gax_api.begin_transaction.return_value = transaction_pb gax_api.commit.side_effect = [aborted, response] database = self._make_database() database.spanner_api = gax_api session = self._make_one(database) session._session_id = self.SESSION_ID called_with = [] def unit_of_work(txn, *args, **kw): called_with.append((txn, args, kw)) txn.insert(TABLE_NAME, COLUMNS, VALUES) # retry once w/ timeout_secs=1 def _time(_results=[1, 1.5]): return _results.pop(0) with mock.patch("time.time", _time): if HAS_OPENTELEMETRY_INSTALLED: with mock.patch("opentelemetry.util.time", _ConstantTime()): with mock.patch("time.sleep") as sleep_mock: with self.assertRaises(Aborted): session.run_in_transaction(unit_of_work, "abc", timeout_secs=1) else: with mock.patch("time.sleep") as sleep_mock: with self.assertRaises(Aborted): session.run_in_transaction(unit_of_work, "abc", timeout_secs=1) sleep_mock.assert_not_called() self.assertEqual(len(called_with), 1) txn, args, kw = called_with[0] self.assertIsInstance(txn, Transaction) self.assertIsNone(txn.committed) self.assertEqual(args, ("abc", )) self.assertEqual(kw, {}) expected_options = TransactionOptions( read_write=TransactionOptions.ReadWrite()) gax_api.begin_transaction.assert_called_once_with( session=self.SESSION_NAME, options=expected_options, metadata=[("google-cloud-resource-prefix", database.name)], ) gax_api.commit.assert_called_once_with( session=self.SESSION_NAME, mutations=txn._mutations, transaction_id=TRANSACTION_ID, metadata=[("google-cloud-resource-prefix", database.name)], )