From e3187d80896078fdd03377a374978f42e8ad28ec Mon Sep 17 00:00:00 2001 From: Simon Hewitt Date: Tue, 18 Apr 2023 18:42:59 -0400 Subject: handle CantDeserializeException raised from deserialize method Added new construct :class:`.api.CantDeserializeException` which can be raised by user-defined deserializer functions which would be passed to :paramref:`.CacheRegion.deserializer`, to indicate a cache value that can't be deserialized and therefore should be regenerated. This can allow an application that's been updated to gracefully re-cache old items that were persisted from a previous version of the application. Pull request courtesy Simon Hewitt. Closes: #236 Pull-request: https://github.com/sqlalchemy/dogpile.cache/pull/236 Pull-request-sha: f2ec26521acb8069d092c51749952f8540b5d75c Change-Id: Idec175b9c06274628d3d027024f9878abb1d188b --- docs/build/changelog.rst | 2 +- docs/build/unreleased/236.rst | 11 +++++++++++ dogpile/__init__.py | 2 +- dogpile/cache/api.py | 9 +++++++++ dogpile/cache/region.py | 24 ++++++++++++++++++++---- tests/cache/_fixtures.py | 18 ++++++++++++++++++ 6 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 docs/build/unreleased/236.rst diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index a4eedf3..c210ba3 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -3,7 +3,7 @@ Changelog ========= .. changelog:: - :version: 1.1.9 + :version: 1.2.0 :include_notes_from: unreleased .. changelog:: diff --git a/docs/build/unreleased/236.rst b/docs/build/unreleased/236.rst new file mode 100644 index 0000000..ebd06bd --- /dev/null +++ b/docs/build/unreleased/236.rst @@ -0,0 +1,11 @@ +.. change:: + :tags: feature, region + :tickets: 236 + + Added new construct :class:`.api.CantDeserializeException` which can be + raised by user-defined deserializer functions which would be passed to + :paramref:`.CacheRegion.deserializer`, to indicate a cache value that can't + be deserialized and therefore should be regenerated. This can allow an + application that's been updated to gracefully re-cache old items that were + persisted from a previous version of the application. Pull request courtesy + Simon Hewitt. diff --git a/dogpile/__init__.py b/dogpile/__init__.py index 175c208..52b2f14 100644 --- a/dogpile/__init__.py +++ b/dogpile/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.1.9" +__version__ = "1.2.0" from .lock import Lock # noqa from .lock import NeedRegenerationException # noqa 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 @@ Serializer = Callable[[ValuePayload], bytes] 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..79f7eaa 100644 --- a/dogpile/cache/region.py +++ b/dogpile/cache/region.py @@ -27,6 +27,7 @@ from .api import BackendFormatted 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,17 @@ class CacheRegion: 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 +1230,12 @@ class CacheRegion: 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 +1262,8 @@ class CacheRegion: 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 CacheRegion 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 @@ class _GenericBackendTest(_GenericBackendFixture, TestCase): ) +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() -- cgit v1.2.1