Skip to content

Commit 8b7de14

Browse files
author
Chris Rossi
authored
fix: fix delete in transaction (#333)
This fixes a bug that caused deleting an entity inside of a transaction to hang. Fixes #271
1 parent 21d88e0 commit 8b7de14

File tree

4 files changed

+56
-9
lines changed

4 files changed

+56
-9
lines changed

packages/google-cloud-ndb/google/cloud/ndb/_datastore_api.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,8 @@ def put(self, entity_pb):
647647
self.mutations.append(mutation)
648648

649649
# If we have an incomplete key, add the incomplete key to a batch for a
650-
# call to AllocateIds
650+
# call to AllocateIds, since the call to actually store the entity
651+
# won't happen until the end of the transaction.
651652
if not _complete(entity_pb.key):
652653
# If this is the first key in the batch, we also need to
653654
# schedule our idle handler to get called
@@ -657,12 +658,28 @@ def put(self, entity_pb):
657658
self.incomplete_mutations.append(mutation)
658659
self.incomplete_futures.append(future)
659660

660-
# Complete keys get passed back None
661+
# Can't wait for result, since batch won't be sent until transaction
662+
# has ended. Complete keys get passed back None.
661663
else:
662664
future.set_result(None)
663665

664666
return future
665667

668+
def delete(self, key):
669+
"""Add a key to batch to be deleted.
670+
671+
Args:
672+
entity_pb (datastore.Key): The entity's key to be deleted.
673+
674+
Returns:
675+
tasklets.Future: Result will be :data:`None`, always.
676+
"""
677+
# Can't wait for result, since batch won't be sent until transaction
678+
# has ended.
679+
future = super(_TransactionalCommitBatch, self).delete(key)
680+
future.set_result(None)
681+
return future
682+
666683
def idle_callback(self):
667684
"""Call AllocateIds on any incomplete keys in the batch."""
668685
if not self.incomplete_mutations:

packages/google-cloud-ndb/google/cloud/ndb/_transaction.py

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

1515
import functools
16+
import logging
1617

1718
from google.cloud.ndb import exceptions
1819
from google.cloud.ndb import _retry
1920
from google.cloud.ndb import tasklets
2021

22+
log = logging.getLogger(__name__)
23+
2124

2225
def in_transaction():
2326
"""Determine if there is a currently active transaction.
@@ -102,9 +105,11 @@ def _transaction_async(context, callback, read_only=False):
102105
from google.cloud.ndb import _datastore_api
103106

104107
# Start the transaction
108+
log.debug("Start transaction")
105109
transaction_id = yield _datastore_api.begin_transaction(
106110
read_only, retries=0
107111
)
112+
log.debug("Transaction Id: {}".format(transaction_id))
108113

109114
on_commit_callbacks = []
110115
tx_context = context.new(

packages/google-cloud-ndb/tests/system/test_crud.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,3 +1167,31 @@ class SomeKind(ndb.Model):
11671167

11681168
keys = SomeKind.allocate_ids(N)
11691169
assert len(keys) == N
1170+
1171+
1172+
@pytest.mark.usefixtures("client_context")
1173+
def test_delete_multi_with_transactional(dispose_of):
1174+
"""Regression test for issue #271
1175+
1176+
https://github.com/googleapis/python-ndb/issues/271
1177+
"""
1178+
N = 10
1179+
1180+
class SomeKind(ndb.Model):
1181+
foo = ndb.IntegerProperty()
1182+
1183+
@ndb.transactional()
1184+
def delete_them(entities):
1185+
ndb.delete_multi([entity.key for entity in entities])
1186+
1187+
foos = list(range(N))
1188+
entities = [SomeKind(foo=foo) for foo in foos]
1189+
keys = ndb.put_multi(entities)
1190+
dispose_of(*(key._key for key in keys))
1191+
1192+
entities = ndb.get_multi(keys)
1193+
assert [entity.foo for entity in entities] == foos
1194+
1195+
assert delete_them(entities) is None
1196+
entities = ndb.get_multi(keys)
1197+
assert entities == [None] * N

packages/google-cloud-ndb/tests/unit/test__datastore_api.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -777,18 +777,15 @@ def __init__(self, delete=None):
777777
def __eq__(self, other):
778778
return self.delete == other.delete
779779

780-
eventloop = mock.Mock(spec=("add_idle", "run"))
781-
with in_context.new(
782-
eventloop=eventloop, transaction=b"tx123"
783-
).use() as context:
780+
with in_context.new(transaction=b"tx123").use() as context:
784781
datastore_pb2.Mutation = Mutation
785782

786783
key1 = key_module.Key("SomeKind", 1)._key
787784
key2 = key_module.Key("SomeKind", 2)._key
788785
key3 = key_module.Key("SomeKind", 3)._key
789-
_api.delete(key1, _options.Options())
790-
_api.delete(key2, _options.Options())
791-
_api.delete(key3, _options.Options())
786+
assert _api.delete(key1, _options.Options()).result() is None
787+
assert _api.delete(key2, _options.Options()).result() is None
788+
assert _api.delete(key3, _options.Options()).result() is None
792789

793790
batch = context.commit_batches[b"tx123"]
794791
assert batch.mutations == [

0 commit comments

Comments
 (0)