From e32dc81dbb34457669664bae1a382bc5c107a865 Mon Sep 17 00:00:00 2001 From: Padraic Shafer Date: Sat, 23 Apr 2022 17:04:51 -0700 Subject: [PATCH 1/4] Decorator creates cache with Cache() constructor * See #473: Deprecate using cache specific constructors * https://github.com/aio-libs/aiocache/issues/473 * `Cache` class was introduced as a proxy for instantiating the different backends. There should be only one way of doing things and `Cache` is the preferred way of doing that so will deprecate the others. * This is now applied to `cached._get_cache()` --- aiocache/decorators.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aiocache/decorators.py b/aiocache/decorators.py index 202a45cc..7e85543d 100644 --- a/aiocache/decorators.py +++ b/aiocache/decorators.py @@ -205,7 +205,12 @@ async def decorator(self, f, *args, **kwargs): def _get_cache(cache=Cache.MEMORY, serializer=None, plugins=None, **cache_kwargs): - return cache(serializer=serializer, plugins=plugins, **cache_kwargs) + return Cache( + cache, + serializer=serializer, + plugins=plugins, + **cache_kwargs, + ) def _get_args_dict(func, args, kwargs): From 3f2137ce00d4144933a85907f29079204a2ee165 Mon Sep 17 00:00:00 2001 From: Padraic Shafer Date: Sat, 23 Apr 2022 17:44:20 -0700 Subject: [PATCH 2/4] Use namespace for decorators get/set * Include build_key(key, namespace) in decorators.cached: * get_from_cache() * set_in_cache() --- aiocache/decorators.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiocache/decorators.py b/aiocache/decorators.py index 7e85543d..90a39d04 100644 --- a/aiocache/decorators.py +++ b/aiocache/decorators.py @@ -136,14 +136,16 @@ def _key_from_args(self, func, args, kwargs): async def get_from_cache(self, key): try: - value = await self.cache.get(key) + namespace = self._kwargs.get("namespace", None) + value = await self.cache.get(key, namespace=namespace) return value except Exception: logger.exception("Couldn't retrieve %s, unexpected error", key) async def set_in_cache(self, key, value): try: - await self.cache.set(key, value, ttl=self.ttl) + namespace = self._kwargs.get("namespace", None) + await self.cache.set(key, value, namespace=namespace, ttl=self.ttl) except Exception: logger.exception("Couldn't set %s in key %s, unexpected error", value, key) From c47f119f14a7286c6c3dce55a0077d384cdd37c5 Mon Sep 17 00:00:00 2001 From: Padraic Shafer Date: Sat, 23 Apr 2022 18:00:09 -0700 Subject: [PATCH 3/4] Use namespace for HitMissRatioPlugin key * Use namespace, if available, to build key that is passed to plugins.HitMissRatioPlugin.post_get() --- aiocache/plugins.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/aiocache/plugins.py b/aiocache/plugins.py index efbb4311..efa11092 100644 --- a/aiocache/plugins.py +++ b/aiocache/plugins.py @@ -60,12 +60,15 @@ async def do_save_time(self, client, *args, took=0, **kwargs): ) -class HitMissRatioPlugin(BasePlugin): +class _HitMissRatioPlugin(BasePlugin): """ - Calculates the ratio of hits the cache has. The data is saved in the cache class as a dict - attribute called ``hit_miss_ratio``. For example, to access the hit ratio of the cache, - you can do ``cache.hit_miss_ratio['hit_ratio']``. It also provides the "total" and "hits" - keys. + Calculates the ratio of hits the cache has. The data is saved in the + cache class as a dict attribute called ``hit_miss_ratio``. + For example, to access the hit ratio of the cache, + you can do ``cache.hit_miss_ratio['hit_ratio']``. + It also provides the "total" and "hits" keys. + + :param key: str should already include the namespace, if available """ async def post_get(self, client, key, took=0, ret=None, **kwargs): @@ -96,3 +99,22 @@ async def post_multi_get(self, client, keys, took=0, ret=None, **kwargs): client.hit_miss_ratio["hit_ratio"] = ( client.hit_miss_ratio["hits"] / client.hit_miss_ratio["total"] ) + + +class HitMissRatioPlugin(_HitMissRatioPlugin): + """ + Calculates the ratio of hits the cache has. The data is saved in the + cache class as a dict attribute called ``hit_miss_ratio``. + For example, to access the hit ratio of the cache, + you can do ``cache.hit_miss_ratio['hit_ratio']``. + It also provides the "total" and "hits" keys. + + :param key: str should not include the namespace + """ + + async def post_get(self, client, key, took=0, ret=None, **kwargs): + """Update cache stats; use namespace if available""" + namespace = kwargs.get("namespace", None) + ns_key = client.build_key(key, namespace=namespace) + await super().post_get(client, ns_key, took=took, ret=ret, **kwargs) + return From 4559b2333d75c0cd7a054707b9d0c9826277b652 Mon Sep 17 00:00:00 2001 From: Padraic Shafer Date: Sat, 23 Apr 2022 18:09:08 -0700 Subject: [PATCH 4/4] Introduce key_filter for confirming key is in namespace * `BaseCache.key_filter(key, namespace)` returns True if key is in the namespace * `backends.memory.SimpleMemoryBackend` uses this to seleectively clear its cache for the given namespace --- aiocache/backends/memory.py | 3 ++- aiocache/base.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/aiocache/backends/memory.py b/aiocache/backends/memory.py index e2ddf03a..fdfd567d 100644 --- a/aiocache/backends/memory.py +++ b/aiocache/backends/memory.py @@ -79,9 +79,10 @@ async def _delete(self, key, _conn=None): return self.__delete(key) async def _clear(self, namespace=None, _conn=None): + """Empty the cache for the namespace provided or the entire cache""" if namespace: for key in list(SimpleMemoryBackend._cache): - if key.startswith(namespace): + if self.key_filter(key, namespace): self.__delete(key) else: SimpleMemoryBackend._cache = {} diff --git a/aiocache/base.py b/aiocache/base.py index d8481092..d3370415 100644 --- a/aiocache/base.py +++ b/aiocache/base.py @@ -98,6 +98,13 @@ class BaseCache: the backend. Default is None :param key_builder: alternative callable to build the key. Receives the key and the namespace as params and should return something that can be used as key by the underlying backend. + :param key_filter: callable that returns `True` if the specified key is + in the specified namespace. Callable signature must be: + `key_filter(key, namespace)`. + This callable is complementary to the `key_builder` callable. + If `key_builder` is provided, `key_filter` should also be provided. + If `key_filter` is not provided, default behavior will be compatible + with the default behavior of `key_builder`. :param timeout: int or float in seconds specifying maximum timeout for the operations to last. By default its 5. Use 0 or None if you want to disable it. :param ttl: int the expiration time in seconds to use as a default in all operations of @@ -107,12 +114,13 @@ class BaseCache: NAME: str def __init__( - self, serializer=None, plugins=None, namespace=None, key_builder=None, timeout=5, ttl=None + self, serializer=None, plugins=None, namespace=None, key_builder=None, key_filter=None, timeout=5, ttl=None ): self.timeout = float(timeout) if timeout is not None else timeout self.namespace = namespace self.ttl = float(ttl) if ttl is not None else ttl self.build_key = key_builder or self._build_key + self.key_filter = key_filter or self._key_filter self._serializer = None self.serializer = serializer or serializers.StringSerializer() @@ -488,6 +496,10 @@ def _build_key(self, key, namespace=None): return "{}{}".format(self.namespace, key) return key + def _key_filter(self, key, namespace): + """Return True if key is in namespace""" + return key.startswith(namespace) + def _get_ttl(self, ttl): return ttl if ttl is not SENTINEL else self.ttl