Skip to content

Commit fe61584

Browse files
author
Chris Rossi
authored
fix: increase cache lock expiration time (#740)
Also converted assert to a warning in case a lock expires from the cache before it can be removed. Fixes #728
1 parent 54a0770 commit fe61584

2 files changed

Lines changed: 124 additions & 9 deletions

File tree

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
_LOCKED_FOR_READ = b"0-"
2929
_LOCKED_FOR_WRITE = b"00"
30-
_LOCK_TIME = 32
30+
_LOCK_TIME = 64
3131
_PREFIX = b"NDB30"
3232

3333
warnings.filterwarnings("always", module=__name__)
@@ -659,11 +659,27 @@ def global_unlock_for_write(key, lock):
659659
utils.logging_debug(log, "unlock for write: {}", lock)
660660

661661
def new_value(old_value):
662-
assert lock in old_value, "attempt to remove lock that isn't present"
663-
value = old_value.replace(lock, b"")
662+
value = old_value
663+
if value and lock in value:
664+
value = value.replace(lock, b"")
665+
666+
else:
667+
warnings.warn(
668+
"Attempt to remove a lock that doesn't exist. This is mostly likely "
669+
"caused by a long running operation and the lock timing out.",
670+
RuntimeWarning,
671+
)
672+
664673
if value == _LOCKED_FOR_WRITE:
665674
value = b""
666675

676+
if value and not value.startswith(_LOCKED_FOR_WRITE):
677+
# If this happens, it means the lock expired and something else got written
678+
# to the cache in the meantime. Whatever value that is, since there was a
679+
# write operation that is concluding now, we should consider it stale and
680+
# write a blank value.
681+
value = b""
682+
667683
return value
668684

669685
cache = _global_cache()
@@ -685,6 +701,10 @@ def _update_key(key, new_value):
685701
value = new_value(old_value)
686702
utils.logging_debug(log, "new value: {}", value) # pragma: SYNCPOINT update key
687703

704+
if old_value == value:
705+
utils.logging_debug(log, "nothing to do")
706+
return
707+
688708
if old_value is not None:
689709
utils.logging_debug(log, "compare and swap")
690710
yield _global_watch(key, old_value)

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

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ def test_first_time(_global_cache, _global_get, global_set_if_not_exists, uuid):
880880

881881
assert _cache.global_lock_for_write(b"key").result() == b".arandomuuid"
882882
_global_get.assert_called_once_with(b"key")
883-
global_set_if_not_exists.assert_called_once_with(b"key", lock_value, expires=32)
883+
global_set_if_not_exists.assert_called_once_with(b"key", lock_value, expires=64)
884884

885885
@staticmethod
886886
@mock.patch("google.cloud.ndb._cache.uuid")
@@ -922,8 +922,8 @@ def test_not_first_time_fail_once(
922922
)
923923
_global_compare_and_swap.assert_has_calls(
924924
[
925-
mock.call(b"key", new_lock_value, expires=32),
926-
mock.call(b"key", new_lock_value, expires=32),
925+
mock.call(b"key", new_lock_value, expires=64),
926+
mock.call(b"key", new_lock_value, expires=64),
927927
]
928928
)
929929

@@ -955,7 +955,102 @@ def test_last_time(
955955
assert _cache.global_unlock_for_write(b"key", lock).result() is None
956956
_global_get.assert_called_once_with(b"key")
957957
_global_watch.assert_called_once_with(b"key", lock_value)
958-
_global_compare_and_swap.assert_called_once_with(b"key", b"", expires=32)
958+
_global_compare_and_swap.assert_called_once_with(b"key", b"", expires=64)
959+
960+
@staticmethod
961+
@mock.patch("google.cloud.ndb._cache.uuid")
962+
@mock.patch("google.cloud.ndb._cache._global_compare_and_swap")
963+
@mock.patch("google.cloud.ndb._cache._global_watch")
964+
@mock.patch("google.cloud.ndb._cache._global_get")
965+
@mock.patch("google.cloud.ndb._cache._global_cache")
966+
def test_lock_missing(
967+
_global_cache, _global_get, _global_watch, _global_compare_and_swap, uuid
968+
):
969+
lock = b".arandomuuid"
970+
971+
_global_cache.return_value = mock.Mock(
972+
transient_errors=(),
973+
strict_write=False,
974+
spec=("transient_errors", "strict_write"),
975+
)
976+
977+
lock_value = _cache._LOCKED_FOR_WRITE + b".adifferentlock"
978+
_global_get.return_value = _future_result(lock_value)
979+
_global_watch.return_value = _future_result(None)
980+
_global_compare_and_swap.return_value = _future_result(True)
981+
982+
with warnings.catch_warnings(record=True) as logged:
983+
assert _cache.global_unlock_for_write(b"key", lock).result() is None
984+
logged = [
985+
warning for warning in logged if warning.category is RuntimeWarning
986+
]
987+
assert len(logged) == 1
988+
989+
_global_get.assert_called_once_with(b"key")
990+
_global_watch.assert_not_called()
991+
_global_compare_and_swap.assert_not_called()
992+
993+
@staticmethod
994+
@mock.patch("google.cloud.ndb._cache.uuid")
995+
@mock.patch("google.cloud.ndb._cache.global_set_if_not_exists")
996+
@mock.patch("google.cloud.ndb._cache._global_get")
997+
@mock.patch("google.cloud.ndb._cache._global_cache")
998+
def test_no_value_in_cache(
999+
_global_cache, _global_get, global_set_if_not_exists, uuid
1000+
):
1001+
lock = b".arandomuuid"
1002+
1003+
_global_cache.return_value = mock.Mock(
1004+
transient_errors=(),
1005+
strict_write=False,
1006+
spec=("transient_errors", "strict_write"),
1007+
)
1008+
1009+
_global_get.return_value = _future_result(None)
1010+
global_set_if_not_exists.return_value = _future_result(True)
1011+
1012+
with warnings.catch_warnings(record=True) as logged:
1013+
assert _cache.global_unlock_for_write(b"key", lock).result() is None
1014+
logged = [
1015+
warning for warning in logged if warning.category is RuntimeWarning
1016+
]
1017+
assert len(logged) == 1
1018+
1019+
_global_get.assert_called_once_with(b"key")
1020+
global_set_if_not_exists.assert_not_called()
1021+
1022+
@staticmethod
1023+
@mock.patch("google.cloud.ndb._cache.uuid")
1024+
@mock.patch("google.cloud.ndb._cache._global_compare_and_swap")
1025+
@mock.patch("google.cloud.ndb._cache._global_watch")
1026+
@mock.patch("google.cloud.ndb._cache._global_get")
1027+
@mock.patch("google.cloud.ndb._cache._global_cache")
1028+
def test_lock_overwritten(
1029+
_global_cache, _global_get, _global_watch, _global_compare_and_swap, uuid
1030+
):
1031+
lock = b".arandomuuid"
1032+
1033+
_global_cache.return_value = mock.Mock(
1034+
transient_errors=(),
1035+
strict_write=False,
1036+
spec=("transient_errors", "strict_write"),
1037+
)
1038+
1039+
lock_value = b"SOMERANDOMVALUE"
1040+
_global_get.return_value = _future_result(lock_value)
1041+
_global_watch.return_value = _future_result(None)
1042+
_global_compare_and_swap.return_value = _future_result(True)
1043+
1044+
with warnings.catch_warnings(record=True) as logged:
1045+
assert _cache.global_unlock_for_write(b"key", lock).result() is None
1046+
logged = [
1047+
warning for warning in logged if warning.category is RuntimeWarning
1048+
]
1049+
assert len(logged) == 1
1050+
1051+
_global_get.assert_called_once_with(b"key")
1052+
_global_watch.assert_called_once_with(b"key", lock_value)
1053+
_global_compare_and_swap.assert_called_once_with(b"key", b"", expires=64)
9591054

9601055
@staticmethod
9611056
@mock.patch("google.cloud.ndb._cache.uuid")
@@ -1023,8 +1118,8 @@ def test_not_last_time_fail_once(
10231118
)
10241119
_global_compare_and_swap.assert_has_calls(
10251120
[
1026-
mock.call(b"key", new_lock_value, expires=32),
1027-
mock.call(b"key", new_lock_value, expires=32),
1121+
mock.call(b"key", new_lock_value, expires=64),
1122+
mock.call(b"key", new_lock_value, expires=64),
10281123
]
10291124
)
10301125

0 commit comments

Comments
 (0)