Skip to content

Instantly share code, notes, and snippets.

@derylspielman
Last active January 29, 2024 07:31
Show Gist options
  • Save derylspielman/7cee44e216475d498ba3819702629a17 to your computer and use it in GitHub Desktop.
Save derylspielman/7cee44e216475d498ba3819702629a17 to your computer and use it in GitHub Desktop.
Retryable Jedis connection for Redis that uses Spring's RetryTemplate.
package com.example.app.session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.retry.RetryContext;
import org.springframework.retry.support.RetryTemplate;
/**
* A RedisTemplate that retries on exceptions
* @param <K> the key
* @param <V> the value
*/
public class RedisRetryTemplate<K, V> extends RedisTemplate<K, V> {
private static final Logger LOG = LoggerFactory.getLogger(RedisRetryTemplate.class);
private final RetryTemplate retryTemplate;
public RedisRetryTemplate(RetryTemplate retryTemplate) {
this.retryTemplate = retryTemplate;
}
@Override
@SuppressWarnings("unchecked")
public <T> T execute(final RedisCallback<T> action, final boolean exposeConnection, final boolean pipeline) {
if (retryTemplate != null) {
return retryTemplate.execute((RetryContext context) -> {
if (context.getRetryCount() > 0) {
LOG.warn("Retry of Redis Operation. Retry Count = {}", context.getRetryCount());
}
return super.execute(action, exposeConnection, pipeline);
});
}
return super.execute(action, exposeConnection, pipeline);
}
public RetryTemplate getRetryTemplate() {
return retryTemplate;
}
}
package com.example.app.session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.retry.RetryContext;
import org.springframework.retry.support.RetryTemplate;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;
/**
* Connection factory creating <a href="http://github.com/xetorthio/jedis">Jedis</a> based connections. Connections are retried
* based on the configured RetryTemplate.
* <p>
* {@link JedisConnectionFactory} should be configured using an environmental configuration and the
* {@link JedisClientConfiguration client configuration}. Jedis supports the following environmental configurations:
* <ul>
* <li>{@link RedisStandaloneConfiguration}</li>
* <li>{@link RedisSentinelConfiguration}</li>
* <li>{@link RedisClusterConfiguration}</li>
* </ul>
*
* @see JedisClientConfiguration
* @see Jedis
*/
public class RetryableJedisConnectionFactory extends JedisConnectionFactory {
private static final Logger LOG = LoggerFactory.getLogger(RetryableJedisConnectionFactory.class);
private RetryTemplate retryTemplate;
public RetryableJedisConnectionFactory() {
}
public RetryableJedisConnectionFactory(JedisPoolConfig poolConfig) {
super(poolConfig);
}
public RetryableJedisConnectionFactory(RedisSentinelConfiguration sentinelConfig) {
super(sentinelConfig);
}
public RetryableJedisConnectionFactory(RedisSentinelConfiguration sentinelConfig, JedisPoolConfig poolConfig) {
super(sentinelConfig, poolConfig);
}
public RetryableJedisConnectionFactory(RedisClusterConfiguration clusterConfig) {
super(clusterConfig);
}
public RetryableJedisConnectionFactory(RedisClusterConfiguration clusterConfig, JedisPoolConfig poolConfig) {
super(clusterConfig, poolConfig);
}
public RetryableJedisConnectionFactory(RedisStandaloneConfiguration standaloneConfig) {
super(standaloneConfig);
}
public RetryableJedisConnectionFactory(RedisStandaloneConfiguration standaloneConfig, JedisClientConfiguration clientConfig) {
super(standaloneConfig, clientConfig);
}
public RetryableJedisConnectionFactory(RedisSentinelConfiguration sentinelConfig, JedisClientConfiguration clientConfig) {
super(sentinelConfig, clientConfig);
}
public RetryableJedisConnectionFactory(RedisClusterConfiguration clusterConfig, JedisClientConfiguration clientConfig) {
super(clusterConfig, clientConfig);
}
/**
* The RetryTemplate that will be used to retry connections.
*
* @return the retryTemplate
*/
public RetryTemplate getRetryTemplate() {
return retryTemplate;
}
/**
* Sets the RetryTemplate used to retry connections.
*
* @param retryTemplate the retryTemplate; can be {@code null}
*/
public void setRetryTemplate(RetryTemplate retryTemplate) {
this.retryTemplate = retryTemplate;
}
/**
* Returns a Jedis instance to be used as a Redis connection. The instance can be newly created or retrieved from a pool.
* When there is an exception connecting it will be retried according to the {@code retryTemplate}.
*
* @return Jedis instance ready for wrapping into a {@link RedisConnection}.
*/
@Override
protected Jedis fetchJedisConnector() {
if (retryTemplate != null) {
return retryTemplate.execute((RetryContext context) -> {
if (context.getRetryCount() > 0) {
LOG.warn("Retrying Redis connection. Retry Count = {}", context.getRetryCount());
}
return super.fetchJedisConnector();
});
}
return super.fetchJedisConnector();
}
}
package com.example.app.config;
import com.example.app.session.RetryableJedisConnectionFactory;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* Configures Spring Session backed by Redis. Connections to Redis are retryable due to network instability.
*/
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class SessionConfig {
private static final Duration CONNECTION_RETRY_BACKOFF_INITIAL_INTERVAL = Duration.ofMillis(200);
private static final Duration CONNECTION_RETRY_BACKOFF_MAX_INTERVAL = Duration.ofSeconds(1);
private static final double CONNECTION_RETRY_BACKOFF_MULTIPLIER = 1.5;
private static final int CONNECTION_RETRY_MAX_ATTEMPTS = 10;
private static final Duration CONNECTION_READ_TIMEOUT = Duration.ofSeconds(10);
private static final Duration CONNECTION_TIMEOUT = Duration.ofSeconds(10);
@Value("${spring.redis.host}")
private String redisServerName;
@Value("${spring.redis.port}")
private Integer redisServerPort;
@Value("${spring.redis.database}")
private Integer redisServerDatabase;
@Value("${spring.redis.password}")
private String redisServerPassword;
/**
* Configures the connection used for Redis. A custom retryable connection factory is used since the Cloud Pipeline network
* seems to be unstable after working hours.
*
* @return the connection factory
*/
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
RetryableJedisConnectionFactory retryableJedisConnectionFactory = new RetryableJedisConnectionFactory(
redisConfiguration(), redisClientConfiguration());
retryableJedisConnectionFactory.setRetryTemplate(sessionRetryTemplate());
return retryableJedisConnectionFactory;
}
/*
* We need to register every HttpSessionListener as a bean to translate SessionDestroyedEvent and SessionCreatedEvent into
* HttpSessionEvent. Otherwise we will got a lot of warning messages about being Unable to publish Events for the session.
* See Spring Session Docs at:
* {@link https://docs.spring.io/spring-session/docs/current/reference/html5/#httpsession-httpsessionlistener}
*/
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
/**
* Builds the Redis client configuration. This extends the read timeout and connection timeout to prevent errors such as
* "Unexpected end of stream" if the Redis connection is lost or retried in the middle of a request.
*
* @return the client configuration
*/
private JedisClientConfiguration redisClientConfiguration() {
JedisClientConfiguration clientConfiguration = JedisClientConfiguration.builder()
.readTimeout(CONNECTION_READ_TIMEOUT)
.connectTimeout(CONNECTION_TIMEOUT)
.build();
return clientConfiguration;
}
private RedisStandaloneConfiguration redisConfiguration() {
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(redisServerName, redisServerPort);
redisConfig.setDatabase(redisServerDatabase);
redisConfig.setPassword(RedisPassword.of(redisServerPassword));
return redisConfig;
}
private RetryTemplate sessionRetryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(backOffPolicy());
retryTemplate.setRetryPolicy(retryPolicy());
return retryTemplate;
}
private BackOffPolicy backOffPolicy() {
ExponentialBackOffPolicy defaultBackOffPolicy = new ExponentialBackOffPolicy();
defaultBackOffPolicy.setInitialInterval(CONNECTION_RETRY_BACKOFF_INITIAL_INTERVAL.toMillis());
defaultBackOffPolicy.setMaxInterval(CONNECTION_RETRY_BACKOFF_MAX_INTERVAL.toMillis());
defaultBackOffPolicy.setMultiplier(CONNECTION_RETRY_BACKOFF_MULTIPLIER);
return defaultBackOffPolicy;
}
private RetryPolicy retryPolicy() {
SimpleRetryPolicy defaultRetryPolicy = new SimpleRetryPolicy(CONNECTION_RETRY_MAX_ATTEMPTS);
return defaultRetryPolicy;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment