Skip to content

Commit c4dd555

Browse files
kalayciburakjzheaux
authored andcommitted
Remove Old Session Id From Expiration Store
When ReactiveRedisIndexedSessionRepository renames a session after changeSessionId, remove the original id from the expiration sorted set so stale entries cannot accumulate. Mirrors RedisIndexedSessionRepository. Closes gh-3471 Signed-off-by: Burak Kalaycı <kalayciburak1996@gmail.com>
1 parent 01e06a4 commit c4dd555

2 files changed

Lines changed: 120 additions & 0 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,8 @@ private Mono<Void> saveChangeSessionId() {
758758
return renameKey(originalSessionKey, sessionKey)
759759
.then(Mono.defer(() -> renameKey(originalExpiredKey, expiredKey)))
760760
.then(Mono.defer(this::replaceSessionIdOnIndexes))
761+
.then(Mono.defer(() -> ReactiveRedisIndexedSessionRepository.this.expirationStore
762+
.remove(this.originalSessionId)))
761763
.then(Mono.defer(() -> replaceSessionId));
762764
}
763765

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2014-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.session.data.redis;
18+
19+
import java.lang.reflect.Constructor;
20+
import java.time.Duration;
21+
import java.time.Instant;
22+
import java.util.Map;
23+
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import org.mockito.Answers;
27+
import reactor.core.publisher.Flux;
28+
import reactor.core.publisher.Mono;
29+
import reactor.test.StepVerifier;
30+
31+
import org.springframework.data.redis.core.ReactiveRedisOperations;
32+
import org.springframework.data.redis.core.ReactiveRedisTemplate;
33+
import org.springframework.session.MapSession;
34+
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository.RedisSession;
35+
import org.springframework.test.util.ReflectionTestUtils;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
38+
import static org.mockito.ArgumentMatchers.any;
39+
import static org.mockito.ArgumentMatchers.anyString;
40+
import static org.mockito.ArgumentMatchers.eq;
41+
import static org.mockito.BDDMockito.given;
42+
import static org.mockito.Mockito.mock;
43+
import static org.mockito.Mockito.never;
44+
import static org.mockito.Mockito.verify;
45+
46+
/**
47+
* Tests for {@link ReactiveRedisIndexedSessionRepository}.
48+
*
49+
* @author Burak Kalaycı
50+
*/
51+
@SuppressWarnings("unchecked")
52+
class ReactiveRedisIndexedSessionRepositoryTests {
53+
54+
private final ReactiveRedisOperations<String, Object> sessionRedisOperations = mock(Answers.RETURNS_DEEP_STUBS);
55+
56+
private final ReactiveRedisTemplate<String, String> keyEventsOperations = mock(Answers.RETURNS_DEEP_STUBS);
57+
58+
private final SortedSetReactiveRedisSessionExpirationStore expirationStore = mock(
59+
SortedSetReactiveRedisSessionExpirationStore.class);
60+
61+
private ReactiveRedisIndexedSessionRepository repository;
62+
63+
@BeforeEach
64+
void setUp() {
65+
given(this.sessionRedisOperations.rename(anyString(), anyString())).willReturn(Mono.empty());
66+
given(this.sessionRedisOperations.delete(anyString())).willReturn(Mono.just(1L));
67+
given(this.sessionRedisOperations.opsForHash().putAll(anyString(), any(Map.class))).willReturn(Mono.just(true));
68+
given(this.sessionRedisOperations.opsForValue().append(anyString(), anyString())).willReturn(Mono.just(1L));
69+
given(this.sessionRedisOperations.expire(anyString(), any(Duration.class))).willReturn(Mono.just(true));
70+
given(this.sessionRedisOperations.opsForSet().add(anyString(), any())).willReturn(Mono.just(1L));
71+
given(this.sessionRedisOperations.opsForSet().remove(anyString(), any())).willReturn(Mono.just(1L));
72+
given(this.sessionRedisOperations.opsForSet().members(anyString())).willReturn(Flux.empty());
73+
given(this.expirationStore.add(anyString(), any())).willReturn(Mono.empty());
74+
given(this.expirationStore.remove(anyString())).willReturn(Mono.empty());
75+
76+
this.repository = new ReactiveRedisIndexedSessionRepository(this.sessionRedisOperations,
77+
this.keyEventsOperations);
78+
ReflectionTestUtils.setField(this.repository, "expirationStore", this.expirationStore);
79+
this.repository.setSessionIdGenerator(() -> "new-session-id");
80+
}
81+
82+
@Test
83+
void saveWhenSessionIdChangedThenRemovesOriginalIdFromExpirationStore() throws Exception {
84+
MapSession cached = new MapSession("original-session-id");
85+
cached.setCreationTime(Instant.ofEpochMilli(1_700_000_000_000L));
86+
cached.setLastAccessedTime(Instant.ofEpochMilli(1_700_000_000_000L));
87+
cached.setMaxInactiveInterval(Duration.ofMinutes(30));
88+
89+
RedisSession session = newRedisSession(cached, false);
90+
String originalId = session.getId();
91+
String changedId = session.changeSessionId();
92+
93+
assertThat(originalId).isEqualTo("original-session-id");
94+
assertThat(changedId).isEqualTo("new-session-id");
95+
96+
StepVerifier.create(this.repository.save(session)).verifyComplete();
97+
98+
verify(this.expirationStore).remove(eq(originalId));
99+
verify(this.expirationStore).add(eq(changedId), any());
100+
verify(this.expirationStore, never()).remove(eq(changedId));
101+
}
102+
103+
private RedisSession newRedisSession(MapSession cached, boolean isNew) throws Exception {
104+
Class<?> redisSessionClass = null;
105+
for (Class<?> nested : ReactiveRedisIndexedSessionRepository.class.getDeclaredClasses()) {
106+
if (nested.getSimpleName().equals("RedisSession")) {
107+
redisSessionClass = nested;
108+
break;
109+
}
110+
}
111+
assertThat(redisSessionClass).isNotNull();
112+
Constructor<?> constructor = redisSessionClass
113+
.getDeclaredConstructor(ReactiveRedisIndexedSessionRepository.class, MapSession.class, boolean.class);
114+
constructor.setAccessible(true);
115+
return (RedisSession) constructor.newInstance(this.repository, cached, isNew);
116+
}
117+
118+
}

0 commit comments

Comments
 (0)