Skip to content

Commit e5e4518

Browse files
committed
Push batch / transaction onto a thread-local stack inside 'with'.
Prep. for #514.
1 parent 77cfe10 commit e5e4518

File tree

3 files changed

+129
-28
lines changed

3 files changed

+129
-28
lines changed

gcloud/datastore/batch.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,55 @@
1313
# limitations under the License.
1414

1515
"""Create / interact with a batch of updates / deletes."""
16+
try:
17+
from threading import local as Local
18+
except ImportError: # pragma: NO COVER (who doesn't have it?)
19+
class Local(object):
20+
"""Placeholder for non-threaded applications."""
1621

1722
from gcloud.datastore import _implicit_environ
1823
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
1924

2025

26+
class _Batches(Local):
27+
"""Manage a thread-local LIFO stack of active batches / transactions.
28+
29+
Intended for use only in :class:`gcloud.datastore.batch.Batch.__enter__`
30+
"""
31+
def __init__(self):
32+
super(_Batches, self).__init__()
33+
self._stack = []
34+
35+
@property
36+
def stack(self):
37+
"""Return a copy of our stack."""
38+
return self._stack[:]
39+
40+
def _push_batch(self, batch):
41+
"""Push a batch onto our stack.
42+
43+
Intended for use only in :meth:`gcloud.datastore.batch.Batch.__enter__`
44+
45+
:type batch: :class:`gcloud.datastore.batch.Batch` or
46+
:class:`gcloud.datastore.transaction.Transaction`
47+
"""
48+
self._stack.append(batch)
49+
50+
def _pop_batch(self):
51+
"""Pop a batch onto our stack.
52+
53+
Intended for use only in :meth:`gcloud.datastore.batch.Batch.__enter__`
54+
55+
:rtype: :class:`gcloud.datastore.batch.Batch` or
56+
:class:`gcloud.datastore.transaction.Transaction`
57+
:raises: IndexError if the stack is empty.
58+
"""
59+
return self._stack.pop()
60+
61+
62+
_BATCHES = _Batches()
63+
64+
2165
class Batch(object):
2266
"""An abstraction representing a collected group of updates / deletes.
2367
@@ -180,6 +224,13 @@ def delete(self, key):
180224
self.connection.delete_entities(
181225
self.dataset_id, [key_pb], mutation=self.mutation)
182226

227+
def begin(self):
228+
"""No-op
229+
230+
Overridden by :class:`gcloud.datastore.transaction.Transaction`.
231+
"""
232+
pass
233+
183234
def commit(self):
184235
"""Commits the batch.
185236
@@ -197,9 +248,23 @@ def commit(self):
197248
new_id = new_key_pb.path_element[-1].id
198249
entity.key = entity.key.completed_key(new_id)
199250

251+
def rollback(self):
252+
"""No-op
253+
254+
Overridden by :class:`gcloud.datastore.transaction.Transaction`.
255+
"""
256+
pass
257+
200258
def __enter__(self):
259+
_BATCHES._push_batch(self)
260+
self.begin()
201261
return self
202262

203263
def __exit__(self, exc_type, exc_val, exc_tb):
204-
if exc_type is None:
205-
self.commit()
264+
try:
265+
if exc_type is None:
266+
self.commit()
267+
else:
268+
self.rollback()
269+
finally:
270+
_BATCHES._pop_batch()

gcloud/datastore/test_batch.py

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def test_put_entity_w_partial_key(self):
106106

107107
self.assertEqual(
108108
connection._saved,
109-
(_DATASET, key._key, _PROPERTIES, (), batch.mutation))
109+
[(_DATASET, key._key, _PROPERTIES, (), batch.mutation)])
110110
self.assertEqual(batch._auto_id_entities, [entity])
111111

112112
def test_put_entity_w_completed_key(self):
@@ -121,7 +121,7 @@ def test_put_entity_w_completed_key(self):
121121

122122
self.assertEqual(
123123
connection._saved,
124-
(_DATASET, key._key, _PROPERTIES, (), batch.mutation))
124+
[(_DATASET, key._key, _PROPERTIES, (), batch.mutation)])
125125

126126
def test_delete_w_partial_key(self):
127127
_DATASET = 'DATASET'
@@ -142,7 +142,7 @@ def test_delete_w_completed_key(self):
142142

143143
self.assertEqual(
144144
connection._deleted,
145-
(_DATASET, [key._key], batch.mutation))
145+
[(_DATASET, [key._key], batch.mutation)])
146146

147147
def test_commit(self):
148148
_DATASET = 'DATASET'
@@ -151,7 +151,7 @@ def test_commit(self):
151151

152152
batch.commit()
153153

154-
self.assertEqual(connection._committed, (_DATASET, batch.mutation))
154+
self.assertEqual(connection._committed, [(_DATASET, batch.mutation)])
155155

156156
def test_commit_w_auto_id_entities(self):
157157
_DATASET = 'DATASET'
@@ -165,45 +165,91 @@ def test_commit_w_auto_id_entities(self):
165165

166166
batch.commit()
167167

168-
self.assertEqual(connection._committed, (_DATASET, batch.mutation))
168+
self.assertEqual(connection._committed, [(_DATASET, batch.mutation)])
169169
self.assertFalse(key._partial)
170170
self.assertEqual(key._id, _NEW_ID)
171171

172172
def test_as_context_mgr_wo_error(self):
173+
from gcloud.datastore.batch import _BATCHES
173174
_DATASET = 'DATASET'
174175
_PROPERTIES = {'foo': 'bar'}
175176
connection = _Connection()
176177
entity = _Entity(_PROPERTIES)
177178
key = entity.key = _Key(_DATASET)
178179

180+
self.assertEqual(_BATCHES.stack, [])
181+
179182
with self._makeOne(dataset_id=_DATASET,
180183
connection=connection) as batch:
184+
self.assertEqual(_BATCHES.stack, [batch])
181185
batch.put(entity)
182186

187+
self.assertEqual(_BATCHES.stack, [])
188+
183189
self.assertEqual(
184190
connection._saved,
185-
(_DATASET, key._key, _PROPERTIES, (), batch.mutation))
186-
self.assertEqual(connection._committed, (_DATASET, batch.mutation))
191+
[(_DATASET, key._key, _PROPERTIES, (), batch.mutation)])
192+
self.assertEqual(connection._committed, [(_DATASET, batch.mutation)])
193+
194+
def test_as_context_mgr_nested(self):
195+
from gcloud.datastore.batch import _BATCHES
196+
_DATASET = 'DATASET'
197+
_PROPERTIES = {'foo': 'bar'}
198+
connection = _Connection()
199+
entity1 = _Entity(_PROPERTIES)
200+
key = entity1.key = _Key(_DATASET)
201+
entity2 = _Entity(_PROPERTIES)
202+
key = entity2.key = _Key(_DATASET)
203+
204+
self.assertEqual(_BATCHES.stack, [])
205+
206+
with self._makeOne(dataset_id=_DATASET,
207+
connection=connection) as batch1:
208+
self.assertEqual(_BATCHES.stack, [batch1])
209+
batch1.put(entity1)
210+
with self._makeOne(dataset_id=_DATASET,
211+
connection=connection) as batch2:
212+
self.assertEqual(_BATCHES.stack, [batch1, batch2])
213+
batch2.put(entity2)
214+
215+
self.assertEqual(_BATCHES.stack, [batch1])
216+
217+
self.assertEqual(_BATCHES.stack, [])
218+
219+
self.assertEqual(
220+
connection._saved,
221+
[(_DATASET, key._key, _PROPERTIES, (), batch1.mutation),
222+
(_DATASET, key._key, _PROPERTIES, (), batch2.mutation)]
223+
)
224+
self.assertEqual(connection._committed,
225+
[(_DATASET, batch2.mutation),
226+
(_DATASET, batch1.mutation)])
187227

188228
def test_as_context_mgr_w_error(self):
229+
from gcloud.datastore.batch import _BATCHES
189230
_DATASET = 'DATASET'
190231
_PROPERTIES = {'foo': 'bar'}
191232
connection = _Connection()
192233
entity = _Entity(_PROPERTIES)
193234
key = entity.key = _Key(_DATASET)
194235

236+
self.assertEqual(_BATCHES.stack, [])
237+
195238
try:
196239
with self._makeOne(dataset_id=_DATASET,
197240
connection=connection) as batch:
241+
self.assertEqual(_BATCHES.stack, [batch])
198242
batch.put(entity)
199243
raise ValueError("testing")
200244
except ValueError:
201245
pass
202246

247+
self.assertEqual(_BATCHES.stack, [])
248+
203249
self.assertEqual(
204250
connection._saved,
205-
(_DATASET, key._key, _PROPERTIES, (), batch.mutation))
206-
self.assertEqual(connection._committed, None)
251+
[(_DATASET, key._key, _PROPERTIES, (), batch.mutation)])
252+
self.assertEqual(connection._committed, [])
207253

208254

209255
class _CommitResult(object):
@@ -226,23 +272,25 @@ def __init__(self, id):
226272

227273
class _Connection(object):
228274
_marker = object()
229-
_committed = _saved = _deleted = None
230275
_save_result = (False, None)
231276

232277
def __init__(self, *new_keys):
233278
self._commit_result = _CommitResult(*new_keys)
279+
self._committed = []
280+
self._saved = []
281+
self._deleted = []
234282

235283
def save_entity(self, dataset_id, key_pb, properties,
236284
exclude_from_indexes=(), mutation=None):
237-
self._saved = (dataset_id, key_pb, properties,
238-
tuple(exclude_from_indexes), mutation)
285+
self._saved.append((dataset_id, key_pb, properties,
286+
tuple(exclude_from_indexes), mutation))
239287
return self._save_result
240288

241289
def delete_entities(self, dataset_id, key_pbs, mutation=None):
242-
self._deleted = (dataset_id, key_pbs, mutation)
290+
self._deleted.append((dataset_id, key_pbs, mutation))
243291

244292
def commit(self, dataset_id, mutation):
245-
self._committed = (dataset_id, mutation)
293+
self._committed.append((dataset_id, mutation))
246294
return self._commit_result
247295

248296

gcloud/datastore/transaction.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,3 @@ def commit(self):
186186

187187
# Clear our own ID in case this gets accidentally reused.
188188
self._id = None
189-
190-
def __enter__(self):
191-
# Don't call super() -- we have different semantics.
192-
self.begin()
193-
return self
194-
195-
def __exit__(self, exc_type, exc_val, exc_tb):
196-
# Don't call super() -- we have different semantics.
197-
if exc_type is None:
198-
self.commit()
199-
else:
200-
self.rollback()

0 commit comments

Comments
 (0)