Simplify expired session cleanup jobs

At present, RedisIndexedHttpSessionConfiguration and JdbcHttpSessionConfiguration include [at]EnableScheduling annotated inner configuration classes that configure expired session cleanup jobs. This approach silently opts in users into general purpose task scheduling support provided by Spring Framework, which isn't something a library should do. Ideally, session cleanup jobs should only require a single thread dedicated to their execution and also one that doesn't compete for resources with general purpose task scheduling.

This commit updates RedisIndexedSessionRepository and JdbcIndexedSessionRepository to have them manage their own ThreadPoolTaskScheduler for purposes of running expired session cleanup jobs.

Closes gh-2136
This commit is contained in:
Vedran Pavic
2022-09-16 13:08:54 +02:00
committed by Rob Winch
parent fb66cf3150
commit 954a40f5d1
10 changed files with 162 additions and 71 deletions

View File

@@ -27,6 +27,8 @@ import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.NestedExceptionUtils;
@@ -38,6 +40,10 @@ import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
@@ -249,12 +255,18 @@ import org.springframework.util.Assert;
* @since 2.2.0
*/
public class RedisIndexedSessionRepository
implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener,
InitializingBean, DisposableBean {
private static final Log logger = LogFactory.getLog(RedisIndexedSessionRepository.class);
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
/**
* The default cron expression used for expired session cleanup job.
*/
public static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
/**
* The default Redis database used by Spring Session.
*/
@@ -309,6 +321,10 @@ public class RedisIndexedSessionRepository
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private ThreadPoolTaskScheduler taskScheduler;
/**
* Creates a new instance. For an example, refer to the class level javadoc.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
@@ -322,6 +338,28 @@ public class RedisIndexedSessionRepository
configureSessionChannels();
}
@Override
public void afterPropertiesSet() {
if (!Scheduled.CRON_DISABLED.equals(this.cleanupCron)) {
this.taskScheduler = createTaskScheduler();
this.taskScheduler.initialize();
this.taskScheduler.schedule(this::cleanUpExpiredSessions, new CronTrigger(this.cleanupCron));
}
}
private static ThreadPoolTaskScheduler createTaskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setThreadNamePrefix("spring-session-");
return taskScheduler;
}
@Override
public void destroy() {
if (this.taskScheduler != null) {
this.taskScheduler.destroy();
}
}
/**
* Sets the {@link ApplicationEventPublisher} that is used to publish
* {@link SessionDestroyedEvent}. The default is to not publish a
@@ -382,6 +420,21 @@ public class RedisIndexedSessionRepository
this.saveMode = saveMode;
}
/**
* Set the cleanup cron expression.
* @param cleanupCron the cleanup cron expression
* @since 3.0.0
* @see CronExpression
* @see Scheduled#CRON_DISABLED
*/
public void setCleanupCron(String cleanupCron) {
Assert.notNull(cleanupCron, "cleanupCron must not be null");
if (!Scheduled.CRON_DISABLED.equals(cleanupCron)) {
Assert.isTrue(CronExpression.isValidExpression(cleanupCron), "cleanupCron must be valid");
}
this.cleanupCron = cleanupCron;
}
/**
* Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}.
* @param database the database index to use
@@ -420,7 +473,7 @@ public class RedisIndexedSessionRepository
}
}
public void cleanupExpiredSessions() {
public void cleanUpExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}

View File

@@ -106,6 +106,6 @@ public @interface EnableRedisIndexedHttpSession {
* The cron expression for expired session cleanup job. By default runs every minute.
* @return the session cleanup cron expression
*/
String cleanupCron() default RedisIndexedHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
String cleanupCron() default RedisIndexedSessionRepository.DEFAULT_CLEANUP_CRON;
}

View File

@@ -41,9 +41,6 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.IndexResolver;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
@@ -68,9 +65,7 @@ public class RedisIndexedHttpSessionConfiguration
extends AbstractRedisHttpSessionConfiguration<RedisIndexedSessionRepository>
implements EmbeddedValueResolverAware, ImportAware {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private String cleanupCron = RedisIndexedSessionRepository.DEFAULT_CLEANUP_CRON;
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
@@ -102,6 +97,7 @@ public class RedisIndexedHttpSessionConfiguration
}
sessionRepository.setFlushMode(getFlushMode());
sessionRepository.setSaveMode(getSaveMode());
sessionRepository.setCleanupCron(this.cleanupCron);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
getSessionRepositoryCustomizers()
@@ -247,25 +243,4 @@ public class RedisIndexedHttpSessionConfiguration
}
/**
* Configuration of scheduled job for cleaning up expired sessions.
*/
@EnableScheduling
@Configuration(proxyBeanMethods = false)
class SessionCleanupConfiguration implements SchedulingConfigurer {
private final RedisIndexedSessionRepository sessionRepository;
SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions,
RedisIndexedHttpSessionConfiguration.this.cleanupCron);
}
}
}

View File

@@ -44,6 +44,7 @@ import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
@@ -451,7 +452,7 @@ class RedisIndexedSessionRepositoryTests {
Set<Object> expiredIds = new HashSet<>(Arrays.asList("expired-key1", "expired-key2"));
given(this.boundSetOperations.members()).willReturn(expiredIds);
this.redisRepository.cleanupExpiredSessions();
this.redisRepository.cleanUpExpiredSessions();
for (Object id : expiredIds) {
String expiredKey = "spring:session:sessions:" + id;
@@ -744,6 +745,25 @@ class RedisIndexedSessionRepositoryTests {
.withMessage("flushMode cannot be null");
}
@Test
void setCleanupCronNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.redisRepository.setCleanupCron(null))
.withMessage("cleanupCron must not be null");
}
@Test
void setCleanupCronInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> this.redisRepository.setCleanupCron("test"))
.withMessage("cleanupCron must be valid");
}
@Test
void setCleanupCronDisabled() {
this.redisRepository.setCleanupCron(Scheduled.CRON_DISABLED);
this.redisRepository.afterPropertiesSet();
assertThat(this.redisRepository).extracting("taskScheduler").isNull();
}
@Test
void changeRedisNamespace() {
String namespace = "foo:bar";

View File

@@ -117,10 +117,8 @@ class RedisIndexedHttpSessionConfigurationTests {
void customCleanupCronAnnotation() {
registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionAnnotationConfiguration.class);
RedisIndexedHttpSessionConfiguration configuration = this.context
.getBean(RedisIndexedHttpSessionConfiguration.class);
assertThat(configuration).isNotNull();
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
assertThat(sessionRepository).extracting("cleanupCron").isEqualTo(CLEANUP_CRON_EXPRESSION);
}
@Test