From 1d7e54bd39ddfd26ffd95613efdd0d41d829a88d Mon Sep 17 00:00:00 2001 From: Rick Elrod Date: Tue, 18 Jul 2023 15:31:09 -0500 Subject: [PATCH] Wrap Django RedisCache to mute exceptions (#14243) We introduce a thin wrapper over Django's RedisCache so that the functionality of DJANGO_REDIS_IGNORE_EXCEPTIONS is retained while still being able to drop the django-redis dependency. Credit to django-redis's implementation for the idea of using a decorator for this and abstracting out the exception handling logic. Signed-off-by: Rick Elrod --- awx/main/cache.py | 87 ++++++++++++++++++++++++++++++++++++++++ awx/settings/defaults.py | 2 +- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 awx/main/cache.py diff --git a/awx/main/cache.py b/awx/main/cache.py new file mode 100644 index 0000000000..2dbbc2fc06 --- /dev/null +++ b/awx/main/cache.py @@ -0,0 +1,87 @@ +import functools + +from django.conf import settings +from django.core.cache.backends.base import DEFAULT_TIMEOUT +from django.core.cache.backends.redis import RedisCache + +from redis.exceptions import ConnectionError, ResponseError, TimeoutError +import socket + +# This list comes from what django-redis ignores and the behavior we are trying +# to retain while dropping the dependency on django-redis. +IGNORED_EXCEPTIONS = (TimeoutError, ResponseError, ConnectionError, socket.timeout) + +CONNECTION_INTERRUPTED_SENTINEL = object() + + +def optionally_ignore_exceptions(func=None, return_value=None): + if func is None: + return functools.partial(optionally_ignore_exceptions, return_value=return_value) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except IGNORED_EXCEPTIONS as e: + if settings.DJANGO_REDIS_IGNORE_EXCEPTIONS: + return return_value + raise e.__cause__ or e + + return wrapper + + +class AWXRedisCache(RedisCache): + """ + We just want to wrap the upstream RedisCache class so that we can ignore + the exceptions that it raises when the cache is unavailable. + """ + + @optionally_ignore_exceptions + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): + return super().add(key, value, timeout, version) + + @optionally_ignore_exceptions(return_value=CONNECTION_INTERRUPTED_SENTINEL) + def _get(self, key, default=None, version=None): + return super().get(key, default, version) + + def get(self, key, default=None, version=None): + value = self._get(key, default, version) + if value is CONNECTION_INTERRUPTED_SENTINEL: + return default + return value + + @optionally_ignore_exceptions + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): + return super().set(key, value, timeout, version) + + @optionally_ignore_exceptions + def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None): + return super().touch(key, timeout, version) + + @optionally_ignore_exceptions + def delete(self, key, version=None): + return super().delete(key, version) + + @optionally_ignore_exceptions + def get_many(self, keys, version=None): + return super().get_many(keys, version) + + @optionally_ignore_exceptions + def has_key(self, key, version=None): + return super().has_key(key, version) + + @optionally_ignore_exceptions + def incr(self, key, delta=1, version=None): + return super().incr(key, delta, version) + + @optionally_ignore_exceptions + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): + return super().set_many(data, timeout, version) + + @optionally_ignore_exceptions + def delete_many(self, keys, version=None): + return super().delete_many(keys, version) + + @optionally_ignore_exceptions + def clear(self): + return super().clear() diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 788b69558a..a7287fb515 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -477,7 +477,7 @@ CELERYBEAT_SCHEDULE = { # Django Caching Configuration DJANGO_REDIS_IGNORE_EXCEPTIONS = True -CACHES = {'default': {'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': 'unix:/var/run/redis/redis.sock?db=1'}} +CACHES = {'default': {'BACKEND': 'awx.main.cache.AWXRedisCache', 'LOCATION': 'unix:/var/run/redis/redis.sock?db=1'}} # Social Auth configuration. SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'