How to disable Redis Caching at run time if redis connection failed
Asked Answered
C

9

19

We have rest api application. We use redis for API response caching and internal method caching. If redis connection then it is making our API down. We want to bypass the redis caching if that redis connection fails or any exception instead of making our API down. There is a interface CacheErrorHandler but it handles the redis get set operation failures not redis connection problems. We are using Spring 4.1.2.

Chemarin answered 12/3, 2015 at 7:9 Comment(1)
In the case of Spring Boot - try to follow this answerAvocation
W
17

Let's boil this down a bit. Your application uses caching (implemented with Redis). If the Redis connection is stale/closed or otherwise, then you want the application to bypass caching and (presumably) go directly to an underlying data store (e.g. RDBMS). The application Service logic might look similar to...

@Service
class CustomerService ... {

    @Autowired
    private CustomerRepository customerRepo;

    protected CustomerRepository getCustomerRepo() {
        Assert.notNull(customerRepo, "The CustomerRepository was not initialized!");
        return customerRepo;
    }

    @Cacheable(value = "Customers")
    public Customer getCustomer(Long customerId) {
        return getCustomerRepo().load(customerId);
    }
    ...
}

All that matters in Spring core's Caching Abstraction to ascertain a Cache "miss" is that the value returned is null. As such, Spring Caching Infrastructure will then proceed in calling the actual Service method (i.e. getCustomer). Keep in mind on the return of the getCustomerRepo().load(customerId) call, you also need to handle the case where Spring's Caching Infrastructure attempts to now cache the value.

In the spirit of keeping it simple, we will do without AOP, but you should be able to achieve this using AOP as well (your choice).

All you (should) need is a "custom" RedisCacheManager extending the SDR CacheManager implementation, something like...

package example;

import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
...

class MyCustomRedisCacheManager extends RedisCacheManager {

    public MyCustomerRedisCacheManager(RedisTemplate redisTemplate) {
        super(redisTemplate);
    }

    @Override
    public Cache getCache(String name) {
        return new RedisCacheWrapper(super.getCache(name));
    }


    protected static class RedisCacheWrapper implements Cache {

        private final Cache delegate;

        public RedisCacheWrapper(Cache redisCache) {
            Assert.notNull(redisCache, "'delegate' must not be null");
            this.delegate = redisCache;
        }

        @Override
        public Cache.ValueWrapper get(Object key) {
            try {
              delegate.get(key);
            }
            catch (Exception e) {
                return handleErrors(e);
            }
        }

        @Override
        public void put(Object key, Object value) {
            try {
                delegate.put(key, value);
            }
            catch (Exception e) {
                handleErrors(e);
            }
        }

        // implement clear(), evict(key), get(key, type), getName(), getNativeCache(), putIfAbsent(key, value) accordingly (delegating to the delegate).

        protected <T> T handleErrors(Exception e) throws Exception {
            if (e instanceof <some RedisConnection Exception type>) {
                // log the connection problem
                return null;
            }
            else if (<something different>) { // act appropriately }
            ...
            else {
                throw e;
            }
        }
    }
}

So, if Redis is unavailable, perhaps the best you can do is log the problem and proceed to let the Service invocation happen. Clearly, this will hamper performance but at least it will raise awareness that a problem exists. Clearly, this could be tied into a more robust notification system, but it is a crude example of the possibilities. The important thing is, your Service remains available while the other services (e.g. Redis) that the application service depends on, may have failed.

In this implementation (vs. my previous explanation) I chose to delegate to the underlying, actual RedisCache implementation to let the Exception occur, then knowing full well a problem with Redis exists, and so that you can deal with the Exception appropriately. However, if you are a certain that the Exception is related to a connection problem upon inspection, you can return "null" to let Spring Caching Infrastructure proceed as if it were a Cache "miss" (i.e. bad Redis Connection == Cache miss, in this case).

I know something like this should help your problem as I built a similar prototype of a "custom" CacheManager implementation for GemFire and one of Pivotal's customers. In that particular UC, the Cache "miss" had to be triggered by an "out-of-date version" of the application domain object where production had a mix of newer and older application clients connecting to GemFire through Spring's Caching Abstraction. The application domain object fields would change in newer versions of the app for instance.

Anyway, hope this helps or gives you more ideas.

Cheers!

Weighty answered 26/3, 2015 at 17:55 Comment(2)
This won't work in Spring Data Redis 2.x since RedisCacheManager's constructor doesn't accept RedisTemplate. @JohnBlum Any suggestions?Leucite
Having the same issue. I tried implementing a similar extension to RedisCacheManager but it's now not really possible due to the changes therein and the fact that the fields are all private. @JohnBlum do you know if there's a way to do this now?Platina
W
7

So, I was digging through the core Spring Framework Caching Abstraction source today addressing another question and it seems if a CacheErrorHandler is implemented properly, then perhaps a problematic Redis Connection could still result in the desired behavior, e.g. cache "miss" (triggered with the return of a null value).

See the AbstractCacheInvoker source for more details.

The cache.get(key) should result in an exception due to a faulty Redis Connection and thus Exception handler would be invoked...

catch (RuntimeException e) {
    getErrorHandler().handleCacheGetError(e, cache, key);
    return null; // If the exception is handled, return a cache miss
}

If the CacheErrorHandler properly handles the Cache "get" error (and does not re-throw the/an Exception), then a null value will be returned indicating a cache "miss".

Weighty answered 30/3, 2015 at 18:30 Comment(1)
This works well with RuntimeException but does not handle Connection Exception.Gaitskell
I
4

Thank you @John Blum. My solution in Spring Boot is as follows.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.util.Assert;

import java.util.concurrent.Callable;

class CustomRedisCacheManager extends RedisCacheManager {
    private static Logger logger = LoggerFactory.getLogger(CustomRedisCacheManager.class);

    public CustomRedisCacheManager(RedisOperations redisOperations) {
        super(redisOperations);
    }

    @Override
    public Cache getCache(String name) {
        return new RedisCacheWrapper(super.getCache(name));
    }


    protected static class RedisCacheWrapper implements Cache {

        private final Cache delegate;

        public RedisCacheWrapper(Cache redisCache) {
            Assert.notNull(redisCache, "delegate cache must not be null");
            this.delegate = redisCache;
        }

        @Override
        public String getName() {
            try {
                return delegate.getName();
            } catch (Exception e) {
                return handleException(e);
            }
        }

        @Override
        public Object getNativeCache() {
            try {
                return delegate.getNativeCache();
            } catch (Exception e) {
                return handleException(e);
            }
        }

        @Override
        public Cache.ValueWrapper get(Object key) {
            try {
                return delegate.get(key);
            } catch (Exception e) {
                return handleException(e);
            }
        }

        @Override
        public <T> T get(Object o, Class<T> aClass) {
            try {
                return delegate.get(o, aClass);
            } catch (Exception e) {
                return handleException(e);
            }
        }

        @Override
        public <T> T get(Object o, Callable<T> callable) {
            try {
                return delegate.get(o, callable);
            } catch (Exception e) {
                return handleException(e);
            }
        }

        @Override
        public void put(Object key, Object value) {
            try {
                delegate.put(key, value);
            } catch (Exception e) {
                handleException(e);
            }
        }

        @Override
        public ValueWrapper putIfAbsent(Object o, Object o1) {
            try {
                return delegate.putIfAbsent(o, o1);
            } catch (Exception e) {
                return handleException(e);
            }
        }

        @Override
        public void evict(Object o) {
            try {
                delegate.evict(o);
            } catch (Exception e) {
                handleException(e);
            }
        }

        @Override
        public void clear() {
            try {
                delegate.clear();
            } catch (Exception e) {
                handleException(e);
            }
        }

        private <T> T handleException(Exception e) {
            logger.error("handleException", e);
            return null;
        }
    }
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfig {
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
        CustomRedisCacheManager redisCacheManager = new CustomRedisCacheManager(redisTemplate);
        redisCacheManager.setUsePrefix(true);
        return redisCacheManager;
    }
}
Industrialism answered 10/7, 2017 at 14:12 Comment(1)
This won't work in Spring Data Redis 2.x since RedisCacheManager's constructor doesn't accept RedisTemplateLeucite
J
4

actually my response is directed to Mr. @Vivek Aditya - I faced the same problem: new spring-data-redis api and not constructing RedisCacheManager per RedisTemplate. The only option - based on @John Blum suggestions - was to use aspects. And below is my code.

@Aspect
@Component
public class FailoverRedisCacheAspect {

    private static class FailoverRedisCache extends RedisCache {

        protected FailoverRedisCache(RedisCache redisCache) {
            super(redisCache.getName(), redisCache.getNativeCache(), redisCache.getCacheConfiguration());
        }

        @Override
        public <T> T get(Object key, Callable<T> valueLoader) {
            try {
                return super.get(key, valueLoader);
            } catch (RuntimeException ex) {
                return valueFromLoader(key, valueLoader);
            }
        }

        private <T> T valueFromLoader(Object key, Callable<T> valueLoader) {
            try {
                return valueLoader.call();
            } catch (Exception e) {
                throw new ValueRetrievalException(key, valueLoader, e);
            }
        }
    }

    @Around("execution(* org.springframework.cache.support.AbstractCacheManager.getCache (..))")
    public Cache beforeSampleCreation(ProceedingJoinPoint proceedingJoinPoint) {
        try {
            Cache cache = (Cache) proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
            if (cache instanceof RedisCache) {
                return new FailoverRedisCache((RedisCache) cache);
            } else {
                return cache;
            }
        } catch (Throwable ex) {
            return null;
        }
    }
}

works fine for all reasonable scenarios:

  • app starts fine with redis down
  • app (still) works during (sudden) redis outage
  • when redis starts working again, app sees it

Edit: the code is more like a poc - only for "get", and I don't like reinstantiating FailoverRedisCache every single cache hit - there should be a map.

Janinajanine answered 6/1, 2019 at 22:1 Comment(2)
This worked for me for all scenarios too. Only thing was I had to reduce redis connection timeout to few seconds from default 60 seconds otherwise it would wait longer before failing over.Gaitskell
@zorro, which spring-data-redis version did you use, for 1.8 it isn't working, configuration redisCache.getCacheConfiguration() not part of this version.Armenian
L
4

None of the above worked for us when using Spring Boot 2.3.9.release with Redis. We ended up creating and registering our own customized CacheErrorHandler named CustomCacheErrorHandler to override the default SimpleCacheErrorHandler provided by Spring Framework. This will work perfectly.

  @Configuration
  public class CachingConfiguration extends CachingConfigurerSupport {  
    @Override
    public CacheErrorHandler errorHandler() {
        return new CustomCacheErrorHandler();
    }
  }

  class CustomCacheErrorHandler implements CacheErrorHandler {
    Logger log = Logger.get(CustomCacheErrorHandler.class);

    @Override
    public void handleCacheGetError(RuntimeException e, Cache cache, Object o) {
        log.error(e.getMessage(), e);
    }

    @Override
    public void handleCachePutError(RuntimeException e, Cache cache, Object o, Object o1) {
        log.error(e.getMessage(), e);
    }

    @Override
    public void handleCacheEvictError(RuntimeException e, Cache cache, Object o) {
        log.error(e.getMessage(), e);
    }

    @Override
    public void handleCacheClearError(RuntimeException e, Cache cache) {
        log.error(e.getMessage(), e);
    }
  }
Loreanloredana answered 20/5, 2022 at 20:55 Comment(0)
B
4

Spring provides built implementation for skipping cache in case of the cache provider server (eg: Redis) is not reachable or any runtime exception happens while accessing the cache by just logging the exception as a warning using LoggingCacheErrorHandler.

By default Spring auto configures CacheErrorHandler of type SimpleCacheErrorHandler which simply throws the exception out. So to avoid throwing exception out -> all we need to do is override cacheErrorHandler by using the following configuration:

@Configuration
public class CachingConfiguration extends CachingConfigurerSupport {
  @override
  public CacheErrorHandler errorHandler() { return new LoggingCacheErrorHandler(); }
}

This will ensure the cache failure exceptions wont propagate or fail your application or API anymore. At the same time you will have those exceptions logged as warnings and we can take actions based on those warnings.

Note: I have tested this using spring-boot 2.7.10

Butylene answered 30/3, 2023 at 2:37 Comment(1)
This works in Spring Boot 3.2, except you extend CachingConfigurer.Acrylonitrile
C
1

I had same problem, but, unfortunately, none of the above solutions work for me. I checked for the problem and found out that the executed command never timed out if there was no connection to Redis. So I start to study lettuce library for a solution. I solve the problem by rejecting the command when there is no connection:

@Bean
public LettuceConnectionFactory lettuceConnectionFactory()
{
    final SocketOptions socketOptions = SocketOptions.builder().connectTimeout(Duration.ofSeconds(10)).build();
    ClientOptions clientOptions = ClientOptions.builder()
            .socketOptions(socketOptions)
            .autoReconnect(true)
            .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
            .build();

    LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .commandTimeout(Duration.ofSeconds(10))
            .clientOptions(clientOptions).build();

    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(this.host, this.port);
    return new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig);

}
Crat answered 26/5, 2019 at 12:55 Comment(0)
W
0

All the core Spring Framework Cache abstraction annotations (e.g. @Cacheable) along with the JSR-107 JCache annotations supported by the core SF delegate to the underlying CacheManager under-the-hood, and for Redis, that is the RedisCacheManager.

You would configure the RedisCacheManager in Spring XML configuration meta-data similar to here.

One approach would be to write an AOP Proxy for the (Redis)CacheManager that uses the RedisConnection (indirectly from the RedisTemplate) to ascertain the state of the connection on each (Redis)CacheManger operation.

If the connection has failed, or is closed, for standard cache ops, the (Redis)CacheManager could return an instance of RedisCache for getCache(String name) that always returns null (indicating a Cache miss on an entry), thus passing through to the underlying data store.

There maybe better ways to handle this as I am not an expert on all things Redis (or SDR), but this should work and perhaps give you a few ides of your own.

Cheers.

Weighty answered 12/3, 2015 at 16:37 Comment(2)
I don't think this will work. As the exception hierarchy don't show the trace till getCache method.Chemarin
I tried overriding execute methods the RedisTemplate Class. But i can't return the null object if exception occurs. I have to return the object of generic type which is returned by execute Method as problem will occur if generic type is interface or abstract class.Chemarin
A
-1

You can use CacheErrorHandler. But you should make sure to make RedisCacheManager transactionAware to false in your Redis Cache Config(to make sure the transaction is committed early when executing the caching part and the error is caught by CacheErrorHandler and don't wait until the end of the execution which skips CacheErrorHandler part). The function to set transactionAware to false looks like this:

    @Bean
    public RedisCacheManager redisCacheManager(LettuceConnectionFactory lettuceConnectionFactory) {
        JdkSerializationRedisSerializer redisSerializer = new JdkSerializationRedisSerializer(getClass().getClassLoader());

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(redisDataTTL))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer));

        redisCacheConfiguration.usePrefix();

        RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(lettuceConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();

        redisCacheManager.setTransactionAware(false);
        return redisCacheManager;
    }
Avens answered 2/2, 2021 at 12:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.