Last active
January 29, 2024 07:31
-
-
Save derylspielman/7cee44e216475d498ba3819702629a17 to your computer and use it in GitHub Desktop.
Retryable Jedis connection for Redis that uses Spring's RetryTemplate.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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