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:
Rob Winch
2014-11-11 20:47:34 -06:00
parent dbc7018a11
commit 6430e43f0e
5 changed files with 372 additions and 69 deletions

View File

@@ -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>();