Introduce RedisSessionExpirationStore

With this commit it is now possible to customize the expiration policy in RedisIndexedHttpSession

Closes gh-2906
This commit is contained in:
Marcus Hert Da Coregio
2024-08-05 14:32:19 -03:00
parent af37d934ca
commit 84f4afcaf1
10 changed files with 665 additions and 15 deletions

View File

@@ -504,11 +504,16 @@ class RedisIndexedSessionRepositoryTests {
String deletedId = "deleted-id";
given(this.redisOperations.<String, Object>boundHashOps(getKey(deletedId)))
.willReturn(this.boundHashOperations);
long lastAccessedTimeMillis = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5);
Map<String, Object> map = map(RedisSessionMapper.CREATION_TIME_KEY, Instant.EPOCH.toEpochMilli(),
RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, 0, RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5));
lastAccessedTimeMillis);
given(this.boundHashOperations.entries()).willReturn(map);
String backgroundExpireKey = "spring:session:expirations:"
+ RedisSessionExpirationPolicy.roundUpToNextMinute(lastAccessedTimeMillis);
given(this.redisOperations.boundSetOps(backgroundExpireKey)).willReturn(this.boundSetOperations);
String channel = "__keyevent@0__:del";
String body = "spring:session:sessions:expires:" + deletedId;
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
@@ -517,8 +522,8 @@ class RedisIndexedSessionRepositoryTests {
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
verify(this.redisOperations).boundHashOps(eq(getKey(deletedId)));
verify(this.boundHashOperations).entries();
verify(this.redisOperations, times(2)).boundHashOps(eq(getKey(deletedId)));
verify(this.boundHashOperations, times(2)).entries();
verify(this.publisher).publishEvent(this.event.capture());
assertThat(this.event.getValue().getSessionId()).isEqualTo(deletedId);
verifyNoMoreInteractions(this.defaultSerializer);
@@ -555,11 +560,16 @@ class RedisIndexedSessionRepositoryTests {
String expiredId = "expired-id";
given(this.redisOperations.<String, Object>boundHashOps(getKey(expiredId)))
.willReturn(this.boundHashOperations);
long lastAccessedTimeMillis = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5);
Map<String, Object> map = map(RedisSessionMapper.CREATION_TIME_KEY, Instant.EPOCH.toEpochMilli(),
RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, 1, RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5));
lastAccessedTimeMillis);
given(this.boundHashOperations.entries()).willReturn(map);
String backgroundExpireKey = "spring:session:expirations:"
+ RedisSessionExpirationPolicy.roundUpToNextMinute(lastAccessedTimeMillis + 1000);
given(this.redisOperations.boundSetOps(backgroundExpireKey)).willReturn(this.boundSetOperations);
String channel = "__keyevent@0__:expired";
String body = "spring:session:sessions:expires:" + expiredId;
DefaultMessage message = new DefaultMessage(channel.getBytes(StandardCharsets.UTF_8),
@@ -568,8 +578,8 @@ class RedisIndexedSessionRepositoryTests {
this.redisRepository.setApplicationEventPublisher(this.publisher);
this.redisRepository.onMessage(message, "".getBytes(StandardCharsets.UTF_8));
verify(this.redisOperations).boundHashOps(eq(getKey(expiredId)));
verify(this.boundHashOperations).entries();
verify(this.redisOperations, times(2)).boundHashOps(eq(getKey(expiredId)));
verify(this.boundHashOperations, times(2)).entries();
verify(this.publisher).publishEvent(this.event.capture());
assertThat(this.event.getValue().getSessionId()).isEqualTo(expiredId);
verifyNoMoreInteractions(this.defaultSerializer);

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2014-2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.redis;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.springframework.data.redis.core.RedisTemplate;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link SortedSetRedisSessionExpirationStore}
*
* @author Marcus da Coregio
*/
class SortedSetRedisSessionExpirationStoreTests {
private SortedSetRedisSessionExpirationStore expirationStore;
private final RedisTemplate<String, Object> redisTemplate = mock(Answers.RETURNS_DEEP_STUBS);
@BeforeEach
void setup() {
this.expirationStore = new SortedSetRedisSessionExpirationStore(this.redisTemplate,
RedisIndexedSessionRepository.DEFAULT_NAMESPACE);
}
@Test
void setNamespaceWhenNullOrEmptyThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.expirationStore.setNamespace(null))
.withMessage("namespace cannot be null or empty");
assertThatIllegalArgumentException().isThrownBy(() -> this.expirationStore.setNamespace(""))
.withMessage("namespace cannot be null or empty");
}
@Test
void setClockWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.expirationStore.setClock(null))
.withMessage("clock cannot be null");
}
@Test
void setCleanupCountWhenZeroOrNegativeThenException() {
assertThatIllegalStateException().isThrownBy(() -> this.expirationStore.setCleanupCount(0))
.withMessage("cleanupCount must be greater than 0");
assertThatIllegalStateException().isThrownBy(() -> this.expirationStore.setCleanupCount(-1))
.withMessage("cleanupCount must be greater than 0");
}
@Test
void cleanupExpiredSessionsThenTouchExpiredSessions() {
given(this.redisTemplate.opsForZSet()
.reverseRangeByScore(anyString(), anyDouble(), anyDouble(), anyLong(), anyLong()))
.willReturn(Set.of("1", "2", "3"));
this.expirationStore.cleanupExpiredSessions();
verify(this.redisTemplate).hasKey("spring:session:sessions:1");
verify(this.redisTemplate).hasKey("spring:session:sessions:2");
verify(this.redisTemplate).hasKey("spring:session:sessions:3");
}
}