Add Support for background task cleanup of RedisSession
Redis key expiration has no guarantees of when the expired key is actually removed. In some instances, it is necessary to clean up resources as soon as the sessione expires. For example, when an HTTP Session expires we must ensure that the associated Web Socket sessions are closed. Sessions are now mapped to their expiration times and a background task is used to clean up the sessions as they expire. Fixes gh-59
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
package org.springframework.session.data.redis;
|
||||
|
||||
import static org.fest.assertions.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.*;
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.CREATION_TIME_ATTR;
|
||||
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.LAST_ACCESSED_ATTR;
|
||||
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.MAX_INACTIVE_ATTR;
|
||||
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.getKey;
|
||||
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.getSessionAttrNameKey;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -16,6 +24,7 @@ import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.data.redis.core.BoundHashOperations;
|
||||
import org.springframework.data.redis.core.BoundSetOperations;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.session.ExpiringSession;
|
||||
import org.springframework.session.MapSession;
|
||||
@@ -27,15 +36,26 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
@Mock
|
||||
RedisOperations<String,ExpiringSession> redisOperations;
|
||||
@Mock
|
||||
RedisOperations<String,String> expirationRedisOperations;
|
||||
@Mock
|
||||
BoundHashOperations<String, Object, Object> boundHashOperations;
|
||||
@Mock
|
||||
BoundSetOperations<String, String> boundSetOperations;
|
||||
@Captor
|
||||
ArgumentCaptor<Map<String,Object>> delta;
|
||||
@Captor
|
||||
ArgumentCaptor<Set<String>> keys;
|
||||
|
||||
private RedisOperationsSessionRepository redisRepository;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.redisRepository = new RedisOperationsSessionRepository(redisOperations);
|
||||
this.redisRepository = new RedisOperationsSessionRepository(redisOperations,expirationRedisOperations);
|
||||
}
|
||||
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void constructorNullConnectionFactory() {
|
||||
new RedisOperationsSessionRepository(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -56,6 +76,7 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
public void saveNewSession() {
|
||||
RedisSession session = redisRepository.createSession();
|
||||
when(redisOperations.boundHashOps(getKey(session.getId()))).thenReturn(boundHashOperations);
|
||||
when(expirationRedisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
@@ -72,6 +93,7 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
RedisSession session = redisRepository.new RedisSession(new MapSession());
|
||||
session.setLastAccessedTime(12345678L);
|
||||
when(redisOperations.boundHashOps(getKey(session.getId()))).thenReturn(boundHashOperations);
|
||||
when(expirationRedisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
@@ -84,6 +106,7 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
RedisSession session = redisRepository.new RedisSession(new MapSession());
|
||||
session.setAttribute(attrName, "attrValue");
|
||||
when(redisOperations.boundHashOps(getKey(session.getId()))).thenReturn(boundHashOperations);
|
||||
when(expirationRedisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
@@ -96,6 +119,7 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
RedisSession session = redisRepository.new RedisSession(new MapSession());
|
||||
session.removeAttribute(attrName);
|
||||
when(redisOperations.boundHashOps(getKey(session.getId()))).thenReturn(boundHashOperations);
|
||||
when(expirationRedisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
@@ -115,11 +139,35 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
|
||||
@Test
|
||||
public void delete() {
|
||||
String id = "abc";
|
||||
String attrName = "attrName";
|
||||
MapSession expected = new MapSession();
|
||||
expected.setLastAccessedTime(System.currentTimeMillis() - 60000);
|
||||
expected.setAttribute(attrName, "attrValue");
|
||||
when(redisOperations.boundHashOps(getKey(expected.getId()))).thenReturn(boundHashOperations);
|
||||
Map map = map(
|
||||
getSessionAttrNameKey(attrName), expected.getAttribute(attrName),
|
||||
CREATION_TIME_ATTR, expected.getCreationTime(),
|
||||
MAX_INACTIVE_ATTR, expected.getMaxInactiveInterval(),
|
||||
LAST_ACCESSED_ATTR, expected.getLastAccessedTime());
|
||||
when(boundHashOperations.entries()).thenReturn(map);
|
||||
when(expirationRedisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
|
||||
String id = expected.getId();
|
||||
redisRepository.delete(id);
|
||||
verify(redisOperations).delete(getKey(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteNullSession() {
|
||||
when(expirationRedisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
|
||||
|
||||
String id = "abc";
|
||||
redisRepository.delete(id);
|
||||
verify(redisOperations,times(0)).delete(anyString());
|
||||
verify(expirationRedisOperations,times(0)).delete(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void getSessionNotFound() {
|
||||
@@ -156,6 +204,39 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void getSessionExpired() {
|
||||
String expiredId = "expired-id";
|
||||
when(redisOperations.boundHashOps(getKey(expiredId))).thenReturn(boundHashOperations);
|
||||
Map map = map(
|
||||
MAX_INACTIVE_ATTR, 1,
|
||||
LAST_ACCESSED_ATTR, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5));
|
||||
when(boundHashOperations.entries()).thenReturn(map);
|
||||
|
||||
assertThat(redisRepository.getSession(expiredId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupExpiredSessions() {
|
||||
String expiredId = "expired-id";
|
||||
when(redisOperations.boundHashOps(getKey(expiredId))).thenReturn(boundHashOperations);
|
||||
when(expirationRedisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
Set<String> expiredIds = new HashSet<String>(Arrays.asList("expired-key1","expired-key2"));
|
||||
when(boundSetOperations.members()).thenReturn(expiredIds);
|
||||
|
||||
redisRepository.cleanupExpiredSessions();
|
||||
|
||||
verify(redisOperations).delete(keys.capture());
|
||||
|
||||
for(String id : expiredIds) {
|
||||
String expiredKey = RedisOperationsSessionRepository.BOUNDED_HASH_KEY_PREFIX + id;
|
||||
assertThat(keys.getValue()).contains(expiredKey);
|
||||
}
|
||||
// the key that maps the expiration time to the expired ids
|
||||
assertThat(keys.getValue().size()).isEqualTo(expiredIds.size() + 1);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private Map map(Object...objects) {
|
||||
Map<String,Object> result = new HashMap<String,Object>();
|
||||
|
||||
Reference in New Issue
Block a user