Skip to content

Commit 63265b0

Browse files
committed
Tracking status of Transaction as well as success / failure.
Fixes #496. NOTE: Some of these changes may belong on Batch, but the concept of "tombstone"-ing is unique to a Transaction (i.e. once started, can only be committed once and the transaction ID can never be used again).
1 parent 9518db7 commit 63265b0

File tree

2 files changed

+120
-11
lines changed

2 files changed

+120
-11
lines changed

gcloud/datastore/test_transaction.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def test_ctor(self):
4747
self.assertEqual(xact.dataset_id, _DATASET)
4848
self.assertEqual(xact.connection, connection)
4949
self.assertEqual(xact.id, None)
50+
self.assertEqual(xact._status, None)
51+
self.assertTrue(xact._commit_success is False)
5052
self.assertTrue(isinstance(xact.mutation, Mutation))
5153
self.assertEqual(len(xact._auto_id_entities), 0)
5254

@@ -64,6 +66,8 @@ def test_ctor_with_env(self):
6466
self.assertEqual(xact.id, None)
6567
self.assertEqual(xact.dataset_id, DATASET_ID)
6668
self.assertEqual(xact.connection, CONNECTION)
69+
self.assertEqual(xact._status, None)
70+
self.assertTrue(xact._commit_success is False)
6771

6872
def test_current(self):
6973
from gcloud.datastore.test_api import _NoCommitBatch
@@ -90,6 +94,47 @@ def test_current(self):
9094
self.assertTrue(xact1.current() is None)
9195
self.assertTrue(xact2.current() is None)
9296

97+
def test_succeeded_fresh_transaction(self):
98+
_DATASET = 'DATASET'
99+
connection = _Connection()
100+
xact = self._makeOne(dataset_id=_DATASET, connection=connection)
101+
self.assertEqual(xact._status, None)
102+
103+
success = marker = object()
104+
with self.assertRaises(ValueError):
105+
success = xact.succeeded
106+
self.assertTrue(success is marker)
107+
108+
def test_succeeded_in_progress(self):
109+
_DATASET = 'DATASET'
110+
connection = _Connection()
111+
xact = self._makeOne(dataset_id=_DATASET, connection=connection)
112+
xact.begin()
113+
self.assertEqual(xact._status, self._getTargetClass()._IN_PROGRESS)
114+
115+
success = marker = object()
116+
with self.assertRaises(ValueError):
117+
success = xact.succeeded
118+
self.assertTrue(success is marker)
119+
120+
def test_succeeded_on_success(self):
121+
_DATASET = 'DATASET'
122+
connection = _Connection()
123+
xact = self._makeOne(dataset_id=_DATASET, connection=connection)
124+
xact.begin()
125+
xact.commit()
126+
self.assertEqual(xact._status, self._getTargetClass()._FINISHED)
127+
self.assertTrue(xact.succeeded is True)
128+
129+
def test_succeeded_on_failure(self):
130+
_DATASET = 'DATASET'
131+
connection = _Connection()
132+
xact = self._makeOne(dataset_id=_DATASET, connection=connection)
133+
xact.begin()
134+
xact.rollback()
135+
self.assertEqual(xact._status, self._getTargetClass()._FINISHED)
136+
self.assertTrue(xact.succeeded is False)
137+
93138
def test_begin(self):
94139
_DATASET = 'DATASET'
95140
connection = _Connection(234)
@@ -98,6 +143,19 @@ def test_begin(self):
98143
self.assertEqual(xact.id, 234)
99144
self.assertEqual(connection._begun, _DATASET)
100145

146+
def test_begin_tombstoned(self):
147+
_DATASET = 'DATASET'
148+
connection = _Connection(234)
149+
xact = self._makeOne(dataset_id=_DATASET, connection=connection)
150+
xact.begin()
151+
self.assertEqual(xact.id, 234)
152+
self.assertEqual(connection._begun, _DATASET)
153+
154+
xact.rollback()
155+
self.assertEqual(xact.id, None)
156+
157+
self.assertRaises(ValueError, xact.begin)
158+
101159
def test_rollback(self):
102160
_DATASET = 'DATASET'
103161
connection = _Connection(234)

gcloud/datastore/transaction.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@ class Transaction(Batch):
3232
3333
>>> datastore.set_defaults()
3434
35-
>>> with Transaction() as xact:
36-
... datastore.put(entity1)
37-
... datastore.put(entity2)
35+
>>> with Transaction():
36+
... datastore.put([entity1, entity2])
3837
3938
Because it derives from :class:`Batch`, :class`Transaction` also provides
4039
:meth:`put` and :meth:`delete` methods::
@@ -46,7 +45,7 @@ class Transaction(Batch):
4645
By default, the transaction is rolled back if the transaction block
4746
exits with an error::
4847
49-
>>> with Transaction() as txn:
48+
>>> with Transaction():
5049
... do_some_work()
5150
... raise SomeException() # rolls back
5251
@@ -71,16 +70,34 @@ class Transaction(Batch):
7170
... entity = Entity(key=Key('Thing'))
7271
... datastore.put([entity])
7372
... assert entity.key.is_partial # There is no ID on this key.
73+
...
7474
>>> assert not entity.key.is_partial # There *is* an ID.
7575
76+
After completion, you can determine if a commit succeeded or failed.
77+
For example, trying to delete a key that doesn't exist::
78+
79+
>>> with Transaction() as xact:
80+
... xact.delete(key)
81+
...
82+
>>> xact.succeeded
83+
False
84+
85+
or successfully storing two entities:
86+
87+
>>> with Transaction() as xact:
88+
... datastore.put([entity1, entity2])
89+
...
90+
>>> xact.succeeded
91+
True
92+
7693
If you don't want to use the context manager you can initialize a
7794
transaction manually::
7895
7996
>>> transaction = Transaction()
8097
>>> transaction.begin()
8198
8299
>>> entity = Entity(key=Key('Thing'))
83-
>>> transaction.put([entity])
100+
>>> transaction.put(entity)
84101
85102
>>> if error:
86103
... transaction.rollback()
@@ -97,9 +114,17 @@ class Transaction(Batch):
97114
are not set.
98115
"""
99116

117+
_IN_PROGRESS = 1
118+
"""Enum value for _IN_PROGRESS status of transaction."""
119+
120+
_FINISHED = 2
121+
"""Enum value for _FINISHED status of transaction."""
122+
100123
def __init__(self, dataset_id=None, connection=None):
101124
super(Transaction, self).__init__(dataset_id, connection)
102125
self._id = None
126+
self._status = None
127+
self._commit_success = False
103128

104129
@property
105130
def id(self):
@@ -123,13 +148,32 @@ def current():
123148
if isinstance(top, Transaction):
124149
return top
125150

151+
@property
152+
def succeeded(self):
153+
"""Determines if transaction has succeeded or failed.
154+
155+
:rtype: boolean
156+
:returns: Boolean indicating successful commit.
157+
:raises: :class:`ValueError` if the transaction is still in progress.
158+
"""
159+
if self._status != self._FINISHED:
160+
raise ValueError('Transaction not yet finished. '
161+
'Success not known.')
162+
163+
return self._commit_success
164+
126165
def begin(self):
127166
"""Begins a transaction.
128167
129168
This method is called automatically when entering a with
130169
statement, however it can be called explicitly if you don't want
131170
to use a context manager.
171+
172+
:raises: :class:`ValueError` if the transaction has already begun.
132173
"""
174+
if self._status is not None:
175+
raise ValueError('Transaction already started previously.')
176+
self._status = self._IN_PROGRESS
133177
self._id = self.connection.begin_transaction(self._dataset_id)
134178

135179
def rollback(self):
@@ -140,8 +184,12 @@ def rollback(self):
140184
- Sets the current connection's transaction reference to None.
141185
- Sets the current transaction's ID to None.
142186
"""
143-
self.connection.rollback(self._dataset_id, self._id)
144-
self._id = None
187+
try:
188+
self.connection.rollback(self._dataset_id, self._id)
189+
finally:
190+
self._status = self._FINISHED
191+
# Clear our own ID in case this gets accidentally reused.
192+
self._id = None
145193

146194
def commit(self):
147195
"""Commits the transaction.
@@ -154,7 +202,10 @@ def commit(self):
154202
155203
- Sets the current transaction's ID to None.
156204
"""
157-
super(Transaction, self).commit()
158-
159-
# Clear our own ID in case this gets accidentally reused.
160-
self._id = None
205+
try:
206+
super(Transaction, self).commit()
207+
finally:
208+
self._commit_success = True
209+
self._status = self._FINISHED
210+
# Clear our own ID in case this gets accidentally reused.
211+
self._id = None

0 commit comments

Comments
 (0)