Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cachekit/cache_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,7 +1377,7 @@ def deserialize_l1(
if not self.keeps_msgpack_in_l1:
return self.deserialize_data(data, cache_key, args, kwargs), None # pyright: ignore[reportArgumentType]
if type(data) is VerifiedMsgpack: # only L1 holds this type: only such a handler swapped it in
return self._base_serializer._deserialize_verified(data), None # type: ignore[union-attr] # noqa: SLF001
return self._base_serializer._deserialize_verified(data), None # type: ignore[union-attr]
try:
payload, metadata, serializer_name = SerializationWrapper.unwrap_metadata(data)
except (AttributeError, KeyError, TypeError, ValueError):
Expand All @@ -1387,7 +1387,7 @@ def deserialize_l1(
# stay in L1, or a header that misdescribes its payload, is decoded by deserialize_data, not kept.
if serializer_name != self._serializer_string_name or metadata.encrypted or not metadata.compressed:
return self.deserialize_data(data, cache_key, args, kwargs), None
return self._base_serializer._deserialize_keeping_msgpack(payload, len(data)) # type: ignore[union-attr] # noqa: SLF001
return self._base_serializer._deserialize_keeping_msgpack(payload, len(data)) # type: ignore[union-attr]

def deserialize_data(
self,
Expand Down
2 changes: 1 addition & 1 deletion src/cachekit/decorators/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2249,7 +2249,7 @@ def _sync_call( # noqa: PLR0912
)
if l1_kept is not None: # its verified MessagePack: later hits skip retrieve()
_l1_cache.shrink_if_held(cache_key, l1_bytes, l1_kept)
else: # decoded as before; an entry another handler swapped in fails as a corrupt frame
else: # as before; a swapped entry fails: SuspiciousCacheEntryError if encrypted, else a corrupt frame
l1_value = operation_handler.serialization_handler.deserialize_data(
l1_bytes, # pyright: ignore[reportArgumentType]
cache_key,
Expand Down
37 changes: 23 additions & 14 deletions tests/unit/test_l1_verified_msgpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@
def _no_cachekit_env(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
"""Handlers here read key settings when built, so no CACHEKIT_* variable in the shell may reach them."""
clear_cachekit_env(monkeypatch)
reset_settings()
yield
reset_settings()


class _Backend:
Expand Down Expand Up @@ -229,27 +227,32 @@ def test_msgpack_exactly_as_long_as_the_limit_is_kept() -> None:
assert serializer._deserialize_keeping_msgpack(envelope, size - 1)[1] is None


_OTHER_HANDLERS: dict[str, Callable[[], CacheSerializationHandler]] = {
"auto": lambda: CacheSerializationHandler(serializer_name="auto"),
"integrity-off": lambda: CacheSerializationHandler(enable_integrity_checking=False),
"encrypted": lambda: CacheSerializationHandler(encryption=True, master_key=MASTER_KEY, single_tenant_mode=True),
_CORRUPT = (SerializationError, "Corrupt cache envelope")
_OTHER_HANDLERS: dict[str, tuple[Callable[[], CacheSerializationHandler], tuple[type[Exception], str]]] = {
"auto": (lambda: CacheSerializationHandler(serializer_name="auto"), _CORRUPT),
"integrity-off": (lambda: CacheSerializationHandler(enable_integrity_checking=False), _CORRUPT),
"encrypted": (
lambda: CacheSerializationHandler(encryption=True, master_key=MASTER_KEY, single_tenant_mode=True),
(SuspiciousCacheEntryError, "another handler's plaintext MessagePack"),
),
}


@pytest.mark.parametrize("make", list(_OTHER_HANDLERS.values()), ids=list(_OTHER_HANDLERS))
def test_other_handlers_keep_their_envelope_and_refuse_verified_msgpack(make: Callable[[], CacheSerializationHandler]) -> None:
@pytest.mark.parametrize(("make", "refusal"), list(_OTHER_HANDLERS.values()), ids=list(_OTHER_HANDLERS))
def test_other_handlers_keep_their_envelope_and_refuse_verified_msgpack(
make: Callable[[], CacheSerializationHandler], refusal: tuple[type[Exception], str]
) -> None:
"""The L1 hit sites send these handlers' entries to deserialize_data, which refuses a swapped entry another
handler left under the same key: under encryption as the plaintext envelope it replaced would be."""
handler = make()
envelope = handler.serialize_data({"x": 1}, cache_key="k")
assert not handler.keeps_msgpack_in_l1
assert handler.deserialize_l1(envelope, cache_key="k") == ({"x": 1}, None)
planted = VerifiedMsgpack(msgpack.packb({"x": 2}))
refusal = SuspiciousCacheEntryError if handler.encryption else SerializationError
error, message = refusal
for decode in (handler.deserialize_l1, handler.deserialize_data):
with pytest.raises(refusal) as raised:
with pytest.raises(error, match=message):
decode(planted, cache_key="k") # type: ignore[arg-type]
assert handler.encryption or "Corrupt cache envelope" in str(raised.value)


def _frames() -> dict[str, bytes]:
Expand Down Expand Up @@ -286,6 +289,11 @@ def _frames() -> dict[str, bytes]:
)


def test_frame_names_list_every_frame() -> None:
"""_FRAME_NAMES is spelled out because _frames() builds handlers, which must wait for the env fixture."""
assert set(_frames()) == set(_FRAME_NAMES)


@pytest.mark.parametrize("name", _FRAME_NAMES)
def test_deserialize_l1_decodes_or_refuses_every_entry_as_deserialize_data_does(
name: str, monkeypatch: pytest.MonkeyPatch
Expand Down Expand Up @@ -415,9 +423,10 @@ def compute(x: int) -> dict[str, int]:
# A replay over the real L1Cache. A read that misses puts the entry a put rule picks from the entry's
# envelope and MessagePack sizes; with promote, a read that hits an envelope swaps it for its MessagePack
# when that is no longer, as the L1 hit sites do (deserialize_l1, then L1Cache.shrink_if_held, which alone
# refuses a longer value here). The shipped design
# is the envelope rule with promote. The two rejected put rules are counterexamples against storing
# MessagePack at write time; they show the pinned traces bite.
# refuses a longer value here).
#
# The shipped design is the envelope rule with promote. The two rejected put rules are counterexamples
# against storing MessagePack at write time; they show the pinned traces bite.

Rule = Callable[[L1Cache, int, int], int]

Expand Down
Loading