Skip to content

Commit 84f4afc

Browse files
Introduce RedisSessionExpirationStore
With this commit it is now possible to customize the expiration policy in RedisIndexedHttpSession Closes gh-2906
1 parent af37d93 commit 84f4afc

File tree

10 files changed

+665
-15
lines changed

10 files changed

+665
-15
lines changed

spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java

+17
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.nio.charset.StandardCharsets;
2020
import java.util.Map;
2121
import java.util.UUID;
22+
import java.util.concurrent.TimeUnit;
2223

2324
import org.junit.jupiter.api.BeforeEach;
2425
import org.junit.jupiter.api.Test;
@@ -135,6 +136,22 @@ void saves() throws InterruptedException {
135136
.isEqualTo(expectedAttributeValue);
136137
}
137138

139+
@Test
140+
void saveThenSaveSessionKeyAndShadowKeyWith5MinutesDifference() {
141+
RedisSession toSave = this.repository.createSession();
142+
String expectedAttributeName = "a";
143+
String expectedAttributeValue = "b";
144+
toSave.setAttribute(expectedAttributeName, expectedAttributeValue);
145+
this.repository.save(toSave);
146+
147+
Long sessionKeyExpire = this.redis.getExpire("RedisIndexedSessionRepositoryITests:sessions:" + toSave.getId(),
148+
TimeUnit.SECONDS);
149+
Long shadowKeyExpire = this.redis
150+
.getExpire("RedisIndexedSessionRepositoryITests:sessions:expires:" + toSave.getId(), TimeUnit.SECONDS);
151+
long differenceInSeconds = sessionKeyExpire - shadowKeyExpire;
152+
assertThat(differenceInSeconds).isEqualTo(300);
153+
}
154+
138155
@Test
139156
void putAllOnSingleAttrDoesNotRemoveOld() {
140157
RedisSession toSave = this.repository.createSession();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2014-2024 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.time.Clock;
20+
import java.time.Duration;
21+
import java.time.Instant;
22+
import java.time.LocalDateTime;
23+
import java.time.ZoneOffset;
24+
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.extension.ExtendWith;
27+
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.context.annotation.Import;
32+
import org.springframework.data.redis.connection.RedisConnectionFactory;
33+
import org.springframework.data.redis.core.RedisTemplate;
34+
import org.springframework.data.redis.serializer.RedisSerializer;
35+
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
36+
import org.springframework.test.context.ContextConfiguration;
37+
import org.springframework.test.context.junit.jupiter.SpringExtension;
38+
import org.springframework.test.context.web.WebAppConfiguration;
39+
40+
import static org.assertj.core.api.Assertions.assertThat;
41+
import static org.mockito.BDDMockito.given;
42+
import static org.mockito.Mockito.mock;
43+
44+
/**
45+
* Tests for {@link SortedSetRedisSessionExpirationStore}
46+
*
47+
* @author Marcus da Coregio
48+
*/
49+
@ExtendWith(SpringExtension.class)
50+
@ContextConfiguration(classes = SortedSetRedisSessionExpirationStoreITests.Config.class)
51+
@WebAppConfiguration
52+
class SortedSetRedisSessionExpirationStoreITests {
53+
54+
@Autowired
55+
private SortedSetRedisSessionExpirationStore expirationStore;
56+
57+
@Autowired
58+
private RedisTemplate<String, Object> redisTemplate;
59+
60+
private static final Instant mockedTime = LocalDateTime.of(2024, 5, 8, 10, 30, 0)
61+
.atZone(ZoneOffset.UTC)
62+
.toInstant();
63+
64+
private static final Clock clock;
65+
66+
static {
67+
clock = Clock.fixed(mockedTime, ZoneOffset.UTC);
68+
}
69+
70+
@Test
71+
void saveThenStoreSessionWithItsExpiration() {
72+
Instant expireAt = mockedTime.plusSeconds(5);
73+
RedisSession session = createSession("123", expireAt);
74+
this.expirationStore.save(session);
75+
Double score = this.redisTemplate.opsForZSet().score("spring:session:sessions:expirations", "123");
76+
assertThat(score).isEqualTo(expireAt.toEpochMilli());
77+
}
78+
79+
@Test
80+
void removeWhenSessionIdExistsThenRemoved() {
81+
RedisSession session = createSession("toBeRemoved", mockedTime);
82+
this.expirationStore.save(session);
83+
Double score = this.redisTemplate.opsForZSet().score("spring:session:sessions:expirations", "toBeRemoved");
84+
assertThat(score).isEqualTo(mockedTime.toEpochMilli());
85+
this.expirationStore.remove("toBeRemoved");
86+
score = this.redisTemplate.opsForZSet().score("spring:session:sessions:expirations", "toBeRemoved");
87+
assertThat(score).isNull();
88+
}
89+
90+
private RedisSession createSession(String sessionId, Instant expireAt) {
91+
RedisSession session = mock();
92+
given(session.getId()).willReturn(sessionId);
93+
given(session.getLastAccessedTime()).willReturn(expireAt);
94+
given(session.getMaxInactiveInterval()).willReturn(Duration.ZERO);
95+
return session;
96+
}
97+
98+
@Configuration(proxyBeanMethods = false)
99+
@Import(AbstractRedisITests.BaseConfig.class)
100+
static class Config {
101+
102+
@Bean
103+
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
104+
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
105+
redisTemplate.setKeySerializer(RedisSerializer.string());
106+
redisTemplate.setHashKeySerializer(RedisSerializer.string());
107+
redisTemplate.setConnectionFactory(redisConnectionFactory);
108+
return redisTemplate;
109+
}
110+
111+
@Bean
112+
RedisSessionExpirationStore redisSessionExpirationStore(RedisTemplate<String, Object> redisTemplate) {
113+
SortedSetRedisSessionExpirationStore store = new SortedSetRedisSessionExpirationStore(redisTemplate,
114+
RedisIndexedSessionRepository.DEFAULT_NAMESPACE);
115+
store.setClock(SortedSetRedisSessionExpirationStoreITests.clock);
116+
return store;
117+
}
118+
119+
}
120+
121+
}

0 commit comments

Comments
 (0)