Skip to content

Simplify expired session cleanup jobs #2163

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
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;
Expand All @@ -38,6 +40,10 @@
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;
Expand Down Expand Up @@ -249,12 +255,18 @@
* @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.
*/
Expand Down Expand Up @@ -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
Expand All @@ -322,6 +338,28 @@ public RedisIndexedSessionRepository(RedisOperations<String, Object> sessionRedi
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
Expand Down Expand Up @@ -382,6 +420,21 @@ public void setSaveMode(SaveMode saveMode) {
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
Expand Down Expand Up @@ -420,7 +473,7 @@ public void save(RedisSession session) {
}
}

public void cleanupExpiredSessions() {
public void cleanUpExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,6 @@
* 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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@
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;
Expand All @@ -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();

Expand Down Expand Up @@ -102,6 +97,7 @@ public RedisIndexedSessionRepository sessionRepository() {
}
sessionRepository.setFlushMode(getFlushMode());
sessionRepository.setSaveMode(getSaveMode());
sessionRepository.setCleanupCron(this.cleanupCron);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
getSessionRepositoryCustomizers()
Expand Down Expand Up @@ -247,25 +243,4 @@ public void afterPropertiesSet() {

}

/**
* 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);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
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;
Expand Down Expand Up @@ -451,7 +452,7 @@ void cleanupExpiredSessions() {
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;
Expand Down Expand Up @@ -744,6 +745,25 @@ void setFlushModeNull() {
.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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,8 @@ void setCustomFlushImmediately() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
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.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
Expand All @@ -48,6 +50,10 @@
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobCreator;
import org.springframework.jdbc.support.lob.LobHandler;
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;
Expand Down Expand Up @@ -130,14 +136,19 @@
* @author Craig Andrews
* @since 2.2.0
*/
public class JdbcIndexedSessionRepository
implements FindByIndexNameSessionRepository<JdbcIndexedSessionRepository.JdbcSession> {
public class JdbcIndexedSessionRepository implements
FindByIndexNameSessionRepository<JdbcIndexedSessionRepository.JdbcSession>, InitializingBean, DisposableBean {

/**
* The default name of database table used by Spring Session to store sessions.
*/
public static final String DEFAULT_TABLE_NAME = "SPRING_SESSION";

/**
* The default cron expression used for expired session cleanup job.
*/
public static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";

private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";

private static final String CREATE_SESSION_QUERY = """
Expand Down Expand Up @@ -241,6 +252,10 @@ public class JdbcIndexedSessionRepository

private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;

private String cleanupCron = DEFAULT_CLEANUP_CRON;

private ThreadPoolTaskScheduler taskScheduler;

/**
* Create a new {@link JdbcIndexedSessionRepository} instance which uses the provided
* {@link JdbcOperations} and {@link TransactionOperations} to manage sessions.
Expand All @@ -255,6 +270,28 @@ public JdbcIndexedSessionRepository(JdbcOperations jdbcOperations, TransactionOp
prepareQueries();
}

@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();
}
}

/**
* Set the name of database table used to store sessions.
* @param tableName the database table name
Expand Down Expand Up @@ -397,6 +434,21 @@ public void setSaveMode(SaveMode saveMode) {
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;
}

@Override
public JdbcSession createSession() {
MapSession delegate = new MapSession();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -98,7 +98,7 @@
* @return the session cleanup cron expression
* @since 2.0.0
*/
String cleanupCron() default JdbcHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
String cleanupCron() default JdbcIndexedSessionRepository.DEFAULT_CLEANUP_CRON;

/**
* Flush mode for the sessions. The default is {@code ON_SAVE} which only updates the
Expand Down
Loading