From f2ec26521acb8069d092c51749952f8540b5d75c Mon Sep 17 00:00:00 2001 From: Simon Hewitt Date: Tue, 11 Apr 2023 13:57:36 -0700 Subject: [PATCH] handle CantDeserializeException raised from deserialize method --- dogpile/cache/api.py | 9 +++++++++ dogpile/cache/region.py | 23 +++++++++++++++++++---- tests/cache/_fixtures.py | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/dogpile/cache/api.py b/dogpile/cache/api.py index 0717d43..c773463 100644 --- a/dogpile/cache/api.py +++ b/dogpile/cache/api.py @@ -51,6 +51,15 @@ def __bool__(self): # pragma NO COVERAGE Deserializer = Callable[[bytes], ValuePayload] +class CantDeserializeException(Exception): + """Exception indicating deserialization failed, and that caching + should proceed to re-generate a value + + .. versionadded:: 1.2.0 + + """ + + class CacheMutex(abc.ABC): """Describes a mutexing object with acquire and release methods. diff --git a/dogpile/cache/region.py b/dogpile/cache/region.py index ef0dbc4..2411ce8 100644 --- a/dogpile/cache/region.py +++ b/dogpile/cache/region.py @@ -27,6 +27,7 @@ from .api import CachedValue from .api import CacheMutex from .api import CacheReturnType +from .api import CantDeserializeException from .api import KeyType from .api import MetaDataType from .api import NO_VALUE @@ -328,7 +329,16 @@ def generate_keys(*args): deserializer recommended by the backend will be used. Typical deserializers include ``pickle.dumps`` and ``json.dumps``. - .. versionadded:: 1.1.0 + Deserializers can raise a :class:`.api.CantDeserializeException` if they + are unable to deserialize the value from the backend, indicating + deserialization failed and that caching should proceed to re-generate + a value. This allows an application that has been updated to gracefully + re-cache old items which were persisted by a previous version of the + application and can no longer be successfully deserialized. + + .. versionadded:: 1.1.0 added "deserializer" parameter + .. versionadded:: 1.2.0 added support for + :class:`.api.CantDeserializeException` :param async_creation_runner: A callable that, when specified, will be passed to and called by dogpile.lock when @@ -1219,8 +1229,12 @@ def _parse_serialized_from_backend( bytes_metadata, _, bytes_payload = byte_value.partition(b"|") metadata = json.loads(bytes_metadata) - payload = self.deserializer(bytes_payload) - return CachedValue(payload, metadata) + try: + payload = self.deserializer(bytes_payload) + except CantDeserializeException: + return NO_VALUE + else: + return CachedValue(payload, metadata) def _serialize_cached_value_elements( self, payload: ValuePayload, metadata: MetaDataType @@ -1247,7 +1261,8 @@ def _serialized_payload( return self._serialize_cached_value_elements(payload, metadata) def _serialized_cached_value(self, value: CachedValue) -> BackendFormatted: - """Return a backend formatted representation of a :class:`.CachedValue`. + """Return a backend formatted representation of a + :class:`.CachedValue`. If a serializer is in use then this will return a string representation with the value formatted by the serializer. diff --git a/tests/cache/_fixtures.py b/tests/cache/_fixtures.py index 9e71f8f..8b3aa77 100644 --- a/tests/cache/_fixtures.py +++ b/tests/cache/_fixtures.py @@ -14,6 +14,7 @@ from dogpile.cache import register_backend from dogpile.cache.api import CacheBackend from dogpile.cache.api import CacheMutex +from dogpile.cache.api import CantDeserializeException from dogpile.cache.api import NO_VALUE from dogpile.cache.region import _backend_loader from . import assert_raises_message @@ -380,6 +381,10 @@ def boom(): ) +def raise_cant_deserialize_exception(v): + raise CantDeserializeException() + + class _GenericSerializerTest(TestCase): # Inheriting from this class will make test cases # use these serialization arguments @@ -388,6 +393,19 @@ class _GenericSerializerTest(TestCase): "deserializer": json.loads, } + def test_serializer_cant_deserialize(self): + region = self._region( + region_args={ + "serializer": self.region_args["serializer"], + "deserializer": raise_cant_deserialize_exception, + } + ) + + value = {"foo": ["bar", 1, False, None]} + region.set("k", value) + asserted = region.get("k") + eq_(asserted, NO_VALUE) + def test_uses_serializer(self): region = self._region()