Repository: googleapis/google-cloud-python

Issue:
Cloud Spanner Client should prevent nested transactions
Cloud Spanner does not support [nested transactions](https://en.wikipedia.org/wiki/Nested_transaction). But the client library does allow users to call `database.run_in_transaction` inside of the callback that is provided to an outer `database.run_in_transaction`. This is misleading since users might believe that this will behave like a nested transaction but in reality these will be two independent transactions. This is confusing and a source of bugs. Specifically the inner transaction might succeed while the outer might ABORT, in which case the callback will be rerun thus rerunning the inner transaction. 
We should prevent this from happening by detecting and raising an error if someone calls `database.run_in_transaction` inside the callback provided to `database.run_in_transaction`.  Note that this needs to be done before Beta since this is a known breaking change.
Also note that if we come across a use case where we do want to allow calling a nested `database.run_in_transaction`, we should be able to enable that in future without making a breaking change. 

cc @jgeewax @bjwatson 


Discussion:
@tseaver This can wait until you have time to work on the Spanner integration tests.
Note that this also is beta blocking.
Thanks for the reminder, @vkedia. I'm reviewing the Python Spanner issues, and adding the `status: release blocking` to any that are Beta blocking. That way we can see a [hotlist](https://github.com/GoogleCloudPlatform/google-cloud-python/issues?utf8=%E2%9C%93&q=is%3Aopen%20label%3A%22status%3A%20release%20blocking%22%20label%3A%22api%3A%20spanner%22) for remaining issue.

Resolving patch:
diff --git a/spanner/google/cloud/spanner/session.py b/spanner/google/cloud/spanner/session.py
--- a/spanner/google/cloud/spanner/session.py
+++ b/spanner/google/cloud/spanner/session.py
@@ -249,6 +249,7 @@ def transaction(self):
 
         if self._transaction is not None:
             self._transaction._rolled_back = True
+            del self._transaction
 
         txn = self._transaction = Transaction(self)
         return txn
diff --git a/spanner/google/cloud/spanner/transaction.py b/spanner/google/cloud/spanner/transaction.py
--- a/spanner/google/cloud/spanner/transaction.py
+++ b/spanner/google/cloud/spanner/transaction.py
@@ -24,12 +24,24 @@
 
 
 class Transaction(_SnapshotBase, _BatchBase):
-    """Implement read-write transaction semantics for a session."""
+    """Implement read-write transaction semantics for a session.
+
+    :type session: :class:`~google.cloud.spanner.session.Session`
+    :param session: the session used to perform the commit
+
+    :raises ValueError: if session has an existing transaction
+    """
     committed = None
     """Timestamp at which the transaction was successfully committed."""
     _rolled_back = False
     _multi_use = True
 
+    def __init__(self, session):
+        if session._transaction is not None:
+            raise ValueError("Session has existing transaction.")
+
+        super(Transaction, self).__init__(session)
+
     def _check_state(self):
         """Helper for :meth:`commit` et al.
 


Test patch:
diff --git a/spanner/tests/unit/test_transaction.py b/spanner/tests/unit/test_transaction.py
--- a/spanner/tests/unit/test_transaction.py
+++ b/spanner/tests/unit/test_transaction.py
@@ -47,6 +47,12 @@ def _make_one(self, session, *args, **kwargs):
         session._transaction = transaction
         return transaction
 
+    def test_ctor_session_w_existing_txn(self):
+        session = _Session()
+        session._transaction = object()
+        with self.assertRaises(ValueError):
+            transaction = self._make_one(session)
+
     def test_ctor_defaults(self):
         session = _Session()
         transaction = self._make_one(session)