Skip to content

Commit 7d2e3ce

Browse files
committed
core: fix potential race condition for synthetic key
1 parent d839eef commit 7d2e3ce

2 files changed

Lines changed: 52 additions & 10 deletions

File tree

src/cachew/__init__.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -553,16 +553,6 @@ def cachew_wrapper[**P, ItemT](
553553

554554
logger.debug('hash mismatch: computing data and writing to db')
555555

556-
if synthetic_key is not None:
557-
missing_synthetic_values = _synthetic.missing_synthetic_key_values_for_hashes(
558-
old_hash=old_hash,
559-
new_hash_d=new_hash_d,
560-
)
561-
if missing_synthetic_values is not None:
562-
# can reuse cache
563-
kwargs[_synthetic.CACHEW_CACHED] = session.cached_items() # ty: ignore[invalid-assignment]
564-
kwargs[synthetic_key] = missing_synthetic_values # ty: ignore[invalid-assignment]
565-
566556
got_write = backend.get_exclusive_write()
567557
if not got_write:
568558
# NOTE: this is the bit we really have to watch out for and not put in a helper function
@@ -573,6 +563,16 @@ def cachew_wrapper[**P, ItemT](
573563
running_uncached = False
574564
return
575565

566+
if synthetic_key is not None:
567+
missing_synthetic_values = _synthetic.missing_synthetic_key_values_for_hashes(
568+
old_hash=old_hash,
569+
new_hash_d=new_hash_d,
570+
)
571+
if missing_synthetic_values is not None:
572+
# can reuse cache
573+
kwargs[_synthetic.CACHEW_CACHED] = session.cached_items() # ty: ignore[invalid-assignment]
574+
kwargs[synthetic_key] = missing_synthetic_values # ty: ignore[invalid-assignment]
575+
576576
# at this point we're guaranteed to have an exclusive write transaction
577577
try:
578578
yield from session.write_to_cache(func(*args, **kwargs))

src/cachew/tests/test_cachew.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,48 @@ def fun() -> Iterator[int]:
11601160
assert calls == 1
11611161

11621162

1163+
def test_synthetic_lock_lost_runs_uncached_with_original_args(
1164+
tmp_path: Path,
1165+
restore_settings,
1166+
) -> None:
1167+
"""
1168+
If synthetic cachew loses the write lock, it should run uncached with the original arguments.
1169+
"""
1170+
settings.THROW_ON_ERROR = False
1171+
1172+
cache_path = tmp_path / 'cache'
1173+
recomputed: list[str] = []
1174+
consumed_cached = False
1175+
1176+
@cachew(cache_path, force_file=True, synthetic_key='keys')
1177+
def fun(keys: Sequence[str], *, cachew_cached: Iterable[str] = ()) -> Iterator[str]:
1178+
nonlocal consumed_cached
1179+
1180+
for item in cachew_cached:
1181+
consumed_cached = True
1182+
yield item
1183+
1184+
for key in keys:
1185+
recomputed.append(key)
1186+
yield key
1187+
1188+
assert list(fun(keys=['a'])) == ['a']
1189+
assert recomputed == ['a']
1190+
1191+
recomputed.clear()
1192+
backend_cls = {
1193+
'file': FileBackend,
1194+
'sqlite': SqliteBackend,
1195+
}[settings.DEFAULT_BACKEND]
1196+
1197+
with backend_cls(cache_path=cache_path, logger=logger) as backend:
1198+
assert backend.get_exclusive_write()
1199+
assert list(fun(keys=['a', 'b'])) == ['a', 'b']
1200+
1201+
assert recomputed == ['a', 'b']
1202+
assert consumed_cached is False
1203+
1204+
11631205
@pytest.mark.parametrize('throw', [False, True])
11641206
def test_bad_annotation(*, tmp_path: Path, throw: bool) -> None:
11651207
"""

0 commit comments

Comments
 (0)