Search code examples
pythondjangoredisdecoratorpython-decorators

Wrap Python decorator in try-except


I'm using Redis to cache some views in my Django Rest Ramework API. Let's say I have the following view:

from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from rest_framework.response import Response
from rest_framework.views import APIView

class MyView(APIView):
    @method_decorator(cache_page(60 * 15))
    def get(self, request):
        return Response({"timestamp": timezone.now()})

Everything works great when Redis is up and running. However, our Redis instance has been having intermittent outages lately (shoutout Heroku Redis), which is causing all endpoints that use the Redis cache and, therefore, any endpoints that use the cache_page decorator, to crash and return a 500 Internal Server Error.

I want to implement a failover mechanism that will simply ignore the cache and return a response successfully when a redis.ConnectionError is raised. Here is what I have so far:

def cache_page_with_failover(timeout, *, cache=None, key_prefix=None):
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(*args, **kwargs):
            try:
                return cache_page(timeout, cache=cache, key_prefix=key_prefix)(
                    view_func
                )(*args, **kwargs)
            except redis.ConnectionError:
                return view_func(*args, **kwargs)

        return wrapper

    return decorator

Usage:

class MyView(APIView):
    @method_decorator(cache_page_with_failover(60 * 15))
    def get(self, request):
        return Response({"timestamp": timezone.now()})

This works but it's awfully verbose and I feel like I'm copying the cache_page signature. Is there a cleaner and more elegant way to write this?


Solution

  • Directly accept the decorator returned by cache_page:

    def with_redis_failover(cache_decorator):
        def decorator(view_func):
            cache_decorated_view_func = cache_decorator(view_func)
    
            @wraps(view_func)
            def wrapper(*args, **kwargs):
                try:
                    return cache_decorated_view_func(*args, **kwargs)
                except redis.ConnectionError:
                    return view_func(*args, **kwargs)
    
            return wrapper
    
        return decorator
    

    Usage:

    # @method_decorator(cache_page_with_failover(60 * 15))
    @method_decorator(with_redis_failover(cache_page(60 * 15)))
    

    This could be generalised it to:

    def with_failover(decorator, exception_type):
        def _decorator(view_func):
            decorated_view_func = decorator(view_func)
    
            @wraps(view_func)
            def wrapper(*args, **kwargs):
                try:
                    return decorated_view_func(*args, **kwargs)
                except exception_type:
                    return view_func(*args, **kwargs)
    
            return wrapper
    
        return _decorator
    

    Usage:

    @method_decorator(with_failover(cache_page(60 * 15), redis.ConnectionError))
    
    with_redis_failover = partial(with_failover, exception_type=redis.ConnectionError)
    
    @method_decorator(with_redis_failover(cache_page(60 * 15)))