Skip to content

Commit cdca5cb

Browse files
committed
Migrate Redis session repositories from DisposableBean to SmartLifecycle
Replace DisposableBean with SmartLifecycle in RedisIndexedSessionRepository and ReactiveRedisIndexedSessionRepository to improve compatibility with Spring Data Redis connection factories. Fixes #3435 Signed-off-by: seung-hun-h <[email protected]>
1 parent 8fb90fd commit cdca5cb

File tree

4 files changed

+452
-8
lines changed

4 files changed

+452
-8
lines changed

spring-session-data-redis/src/main/java/org/springframework/session/data/redis/ReactiveRedisIndexedSessionRepository.java

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.beans.factory.DisposableBean;
3939
import org.springframework.beans.factory.InitializingBean;
4040
import org.springframework.context.ApplicationEventPublisher;
41+
import org.springframework.context.SmartLifecycle;
4142
import org.springframework.core.NestedExceptionUtils;
4243
import org.springframework.data.redis.connection.ReactiveSubscription;
4344
import org.springframework.data.redis.core.ReactiveRedisOperations;
@@ -235,7 +236,7 @@
235236
public class ReactiveRedisIndexedSessionRepository
236237
implements ReactiveSessionRepository<ReactiveRedisIndexedSessionRepository.RedisSession>,
237238
ReactiveFindByIndexNameSessionRepository<ReactiveRedisIndexedSessionRepository.RedisSession>, DisposableBean,
238-
InitializingBean {
239+
InitializingBean, SmartLifecycle {
239240

240241
private static final Log logger = LogFactory.getLog(ReactiveRedisIndexedSessionRepository.class);
241242

@@ -249,6 +250,28 @@ public class ReactiveRedisIndexedSessionRepository
249250
*/
250251
public static final int DEFAULT_DATABASE = 0;
251252

253+
/**
254+
* The default SmartLifecycle phase.
255+
*
256+
* <p>
257+
* Set to {@code Integer.MAX_VALUE / 2} to position this repository between the Redis
258+
* {@link org.springframework.data.redis.connection.RedisConnectionFactory} (typically
259+
* small, e.g. {@code 0}) and web server / messaging listener containers (very large
260+
* values, e.g. {@code Integer.MAX_VALUE - 1024}, {@code Integer.MAX_VALUE - 100},
261+
* {@code Integer.MAX_VALUE}), preventing shutdown races.
262+
* </p>
263+
*
264+
* <p>
265+
* <b>NOTE</b>: if the ConnectionFactory’s phase is >= this value, raise it via
266+
* {@link #setPhase(int)} to keep “SessionRepository phase > ConnectionFactory phase”.
267+
* </p>
268+
*
269+
* @see org.springframework.context.SmartLifecycle
270+
* @see org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
271+
* @see org.springframework.data.redis.connection.jedis.JedisConnectionFactory
272+
*/
273+
public static final int DEFAULT_SMART_LIFECYCLE_PHASE = Integer.MAX_VALUE / 2;
274+
252275
private final ReactiveRedisOperations<String, Object> sessionRedisOperations;
253276

254277
private final ReactiveRedisTemplate<String, String> keyEventsOperations;
@@ -281,6 +304,8 @@ public class ReactiveRedisIndexedSessionRepository
281304

282305
private int database = DEFAULT_DATABASE;
283306

307+
private int phase = DEFAULT_SMART_LIFECYCLE_PHASE;
308+
284309
private ReactiveRedisSessionIndexer indexer;
285310

286311
private SortedSetReactiveRedisSessionExpirationStore expirationStore;
@@ -289,6 +314,8 @@ public class ReactiveRedisIndexedSessionRepository
289314

290315
private Clock clock = Clock.systemUTC();
291316

317+
private volatile boolean running = false;
318+
292319
/**
293320
* Creates a new instance with the provided {@link ReactiveRedisOperations}.
294321
* @param sessionRedisOperations the {@link ReactiveRedisOperations} to use for
@@ -308,9 +335,20 @@ public ReactiveRedisIndexedSessionRepository(ReactiveRedisOperations<String, Obj
308335
}
309336

310337
@Override
311-
public void afterPropertiesSet() throws Exception {
338+
public void start() {
339+
if (this.running) {
340+
return;
341+
}
342+
312343
subscribeToRedisEvents();
313344
setupCleanupTask();
345+
346+
this.running = true;
347+
}
348+
349+
@Override
350+
public void afterPropertiesSet() throws Exception {
351+
start();
314352
}
315353

316354
private void setupCleanupTask() {
@@ -325,19 +363,46 @@ private void setupCleanupTask() {
325363
}
326364

327365
private Flux<Void> cleanUpExpiredSessions() {
328-
return this.expirationStore.retrieveExpiredSessions(this.clock.instant()).flatMap(this::touch);
366+
return this.expirationStore.retrieveExpiredSessions(this.clock.instant())
367+
.filter((ignored) -> isRunning())
368+
.flatMap(this::touch);
329369
}
330370

331371
private Mono<Void> touch(String sessionId) {
332372
return this.sessionRedisOperations.hasKey(getExpiredKey(sessionId)).then();
333373
}
334374

335375
@Override
336-
public void destroy() {
376+
public void stop() {
377+
if (!this.running) {
378+
return;
379+
}
380+
337381
for (Disposable subscription : this.subscriptions) {
338382
subscription.dispose();
339383
}
340384
this.subscriptions.clear();
385+
386+
this.running = false;
387+
}
388+
389+
@Override
390+
public void destroy() {
391+
stop();
392+
}
393+
394+
@Override
395+
public boolean isRunning() {
396+
return this.running;
397+
}
398+
399+
@Override
400+
public int getPhase() {
401+
return this.phase;
402+
}
403+
404+
public void setPhase(int phase) {
405+
this.phase = phase;
341406
}
342407

343408
@Override

spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2023 the original author or authors.
2+
* Copyright 2014-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@
3232
import org.springframework.beans.factory.InitializingBean;
3333
import org.springframework.context.ApplicationEvent;
3434
import org.springframework.context.ApplicationEventPublisher;
35+
import org.springframework.context.SmartLifecycle;
3536
import org.springframework.core.NestedExceptionUtils;
3637
import org.springframework.dao.NonTransientDataAccessException;
3738
import org.springframework.data.redis.connection.Message;
@@ -260,7 +261,7 @@
260261
*/
261262
public class RedisIndexedSessionRepository
262263
implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener,
263-
InitializingBean, DisposableBean {
264+
InitializingBean, DisposableBean, SmartLifecycle {
264265

265266
private static final Log logger = LogFactory.getLog(RedisIndexedSessionRepository.class);
266267

@@ -281,8 +282,32 @@ public class RedisIndexedSessionRepository
281282
*/
282283
public static final String DEFAULT_NAMESPACE = "spring:session";
283284

285+
/**
286+
* The default SmartLifecycle phase.
287+
*
288+
* <p>
289+
* Set to {@code Integer.MAX_VALUE / 2} to position this repository between the Redis
290+
* {@link org.springframework.data.redis.connection.RedisConnectionFactory} (typically
291+
* small, e.g. {@code 0}) and web server / messaging listener containers (very large
292+
* values, e.g. {@code Integer.MAX_VALUE - 1024}, {@code Integer.MAX_VALUE - 100},
293+
* {@code Integer.MAX_VALUE}), preventing shutdown races.
294+
* </p>
295+
*
296+
* <p>
297+
* <b>NOTE</b>: if the ConnectionFactory’s phase is >= this value, raise it via
298+
* {@link #setPhase(int)} to keep “SessionRepository phase > ConnectionFactory phase”.
299+
* </p>
300+
*
301+
* @see org.springframework.context.SmartLifecycle
302+
* @see org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
303+
* @see org.springframework.data.redis.connection.jedis.JedisConnectionFactory
304+
*/
305+
public static final int DEFAULT_SMART_LIFECYCLE_PHASE = Integer.MAX_VALUE / 2;
306+
284307
private int database = DEFAULT_DATABASE;
285308

309+
private int phase = DEFAULT_SMART_LIFECYCLE_PHASE;
310+
286311
/**
287312
* The namespace for every key used by Spring Session in Redis.
288313
*/
@@ -329,6 +354,8 @@ public class RedisIndexedSessionRepository
329354

330355
private BiFunction<String, Map<String, Object>, MapSession> redisSessionMapper = new RedisSessionMapper();
331356

357+
private volatile boolean running = false;
358+
332359
/**
333360
* Creates a new instance. For an example, refer to the class level javadoc.
334361
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
@@ -343,12 +370,23 @@ public RedisIndexedSessionRepository(RedisOperations<String, Object> sessionRedi
343370
}
344371

345372
@Override
346-
public void afterPropertiesSet() {
373+
public void start() {
374+
if (this.running) {
375+
return;
376+
}
377+
347378
if (!Scheduled.CRON_DISABLED.equals(this.cleanupCron)) {
348379
this.taskScheduler = createTaskScheduler();
349380
this.taskScheduler.initialize();
350381
this.taskScheduler.schedule(this::cleanUpExpiredSessions, new CronTrigger(this.cleanupCron));
351382
}
383+
384+
this.running = true;
385+
}
386+
387+
@Override
388+
public void afterPropertiesSet() {
389+
start();
352390
}
353391

354392
private static ThreadPoolTaskScheduler createTaskScheduler() {
@@ -358,10 +396,35 @@ private static ThreadPoolTaskScheduler createTaskScheduler() {
358396
}
359397

360398
@Override
361-
public void destroy() {
399+
public void stop() {
400+
if (!this.running) {
401+
return;
402+
}
403+
362404
if (this.taskScheduler != null) {
363405
this.taskScheduler.destroy();
364406
}
407+
408+
this.running = false;
409+
}
410+
411+
@Override
412+
public void destroy() {
413+
stop();
414+
}
415+
416+
@Override
417+
public boolean isRunning() {
418+
return this.running;
419+
}
420+
421+
@Override
422+
public int getPhase() {
423+
return this.phase;
424+
}
425+
426+
public void setPhase(int phase) {
427+
this.phase = phase;
365428
}
366429

367430
/**
@@ -486,6 +549,10 @@ public void save(RedisSession session) {
486549
}
487550

488551
public void cleanUpExpiredSessions() {
552+
if (!isRunning()) {
553+
return;
554+
}
555+
489556
this.expirationPolicy.cleanExpiredSessions();
490557
}
491558

0 commit comments

Comments
 (0)