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,6 +1,9 @@
package org.springframework.session.data.redis;
import static org.fest.assertions.Assertions.*;
import static org.fest.assertions.Assertions.assertThat;
import java.io.IOException;
import java.net.ServerSocket;
import org.junit.After;
import org.junit.Before;
@@ -9,24 +12,19 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.ExpiringSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import redis.embedded.RedisServer;
import java.io.IOException;
import java.net.ServerSocket;
import redis.embedded.RedisServer;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@@ -89,6 +87,7 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
}
@Configuration
@EnableRedisHttpSession
static class Config {
@Bean
public JedisConnectionFactory connectionFactory() throws Exception {
@@ -97,20 +96,6 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
factory.setUsePool(false);
return factory;
}
@Bean
public RedisTemplate<String,ExpiringSession> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(connectionFactory);
return template;
}
@Bean
public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> redisTemplate) {
return new RedisOperationsSessionRepository(redisTemplate);
}
}
private static Integer availablePort;

View File

@@ -15,8 +15,17 @@
*/
package org.springframework.session.data.redis;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.session.ExpiringSession;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
@@ -24,16 +33,13 @@ import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* <p>
* A {@link org.springframework.session.SessionRepository} that is implemented using Spring Data's
* {@link org.springframework.data.redis.core.RedisOperations}. In a web environment, this is typically used in
* combination with {@link SessionRepositoryFilter}.
* A {@link org.springframework.session.SessionRepository} that is implemented
* using Spring Data's
* {@link org.springframework.data.redis.core.RedisOperations}. In a web
* environment, this is typically used in combination with
* {@link SessionRepositoryFilter}.
* </p>
*
* <h2>Creating a new instance</h2>
@@ -41,54 +47,68 @@ import java.util.concurrent.TimeUnit;
* A typical example of how to create a new instance can be seen below:
*
* <pre>
* JedisConnectionFactory factory = new JedisConnectionFactory();
* JedisConnectionFactory factory = new JedisConnectionFactory();
*
* RedisTemplate<String, Session> template = new RedisTemplate<String, Session>();
* template.setKeySerializer(new StringRedisSerializer());
* template.setHashKeySerializer(new StringRedisSerializer());
* template.setConnectionFactory(factory);
*
* RedisOperationsSessionRepository redisSessionRepository = new RedisOperationsSessionRepository(template);
* RedisOperationsSessionRepository redisSessionRepository = new RedisOperationsSessionRepository(
* factory);
* </pre>
*
* <p>
* For additional information on how to create a RedisTemplate, refer to the
* <a href="http://docs.spring.io/spring-data/data-redis/docs/current/reference/html/">Spring Data Redis Reference</a>.
* For additional information on how to create a RedisTemplate, refer to the <a
* href =
* "http://docs.spring.io/spring-data/data-redis/docs/current/reference/html/"
* >Spring Data Redis Reference</a>.
* </p>
*
* <h2>Storage Details</h2>
*
* <p>
* Each session is stored in Redis as a <a href="http://redis.io/topics/data-types#hashes">Hash</a>. Each session is
* set and updated using the <a href="http://redis.io/commands/hmset">HMSET command</a>. An example of how each session
* is stored can be seen below.
* Each session is stored in Redis as a <a
* href="http://redis.io/topics/data-types#hashes">Hash</a>. Each session is set
* and updated using the <a href="http://redis.io/commands/hmset">HMSET
* command</a>. An example of how each session is stored can be seen below.
* </p>
*
* <pre>
* HMSET spring-security-sessions:<session-id> creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:<attrName> someAttrValue sessionAttr2:<attrName> someAttrValue2
* HMSET spring:session:sessions:<session-id> creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:<attrName> someAttrValue sessionAttr2:<attrName> someAttrValue2
* </pre>
*
* <p>
* An expiration is associated to each session using the <a href="http://redis.io/commands/expire">EXPIRE command</a> based upon the
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#getMaxInactiveInterval()}.
* For example:
* An expiration is associated to each session using the <a
* href="http://redis.io/commands/expire">EXPIRE command</a> based upon the
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#getMaxInactiveInterval()}
* . For example:
* </p>
*
* <pre>
* EXPIRE spring-security-sessions:<session-id> 1800
* EXPIRE spring:session:sessions:<session-id> 1800
* </pre>
*
* <p>
* The {@link RedisSession} keeps track of the properties that have changed and only updates those. This means if an attribute
* is written once and read many times we only need to write that attribute once. For example, assume the session attribute
* "sessionAttr2" from earlier was updated. The following would be executed upon saving:
* The {@link RedisSession} keeps track of the properties that have changed and
* only updates those. This means if an attribute is written once and read many
* times we only need to write that attribute once. For example, assume the
* session attribute "sessionAttr2" from earlier was updated. The following
* would be executed upon saving:
* </p>
*
* <pre>
* HMSET spring-security-sessions:<session-id> sessionAttr2:<attrName> newValue
* EXPIRE spring-security-sessions:<session-id> 1800
* HMSET spring:session:sessions:<session-id> sessionAttr2:<attrName> newValue
* EXPIRE spring:session:sessions:<session-id> 1800
* </pre>
*
* Each session expiration is also tracked to the nearest minute. This allows a
* background task to cleanup expired sessions in a deterministic fashion. For
* example:
*
* <pre>
* SADD spring:session:expirations:<expire-rounded-up-to-nearest-minute> <session-id>
* EXPIRE spring:session:expirations:<expire-rounded-up-to-nearest-minute> 1800
* </pre>
*
* The Redis expiration is still placed on each key to ensure that if the server
* is down when the session expires, it is still cleaned up.
*
* @since 1.0
*
* @author Rob Winch
@@ -121,21 +141,36 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
*/
static final String SESSION_ATTR_PREFIX = "sessionAttr:";
private final RedisOperations<String,ExpiringSession> redisOperations;
private final RedisOperations<String,ExpiringSession> sessionRedisOperations;
private final RedisSessionExpirationPolicy expirationPolicy;
/**
* If non-null, this value is used to override {@link RedisSession#setDefaultMaxInactiveInterval(int)}.
*/
private Integer defaultMaxInactiveInterval;
/**
* Allows creating an instance and uses a default {@link RedisOperations} for both managing the session and the expirations.
*
* @param redisConnectionFactory the {@link RedisConnectionFactory} to use.
*/
@SuppressWarnings("unchecked")
public RedisOperationsSessionRepository(RedisConnectionFactory redisConnectionFactory) {
this(createDefaultTemplate(redisConnectionFactory), createDefaultTemplate(redisConnectionFactory));
}
/**
* Creates a new instance. For an example, refer to the class level javadoc.
*
* @param redisOperations The {@link RedisOperations} to use. Cannot be null.
* @param sessionRedisOperations The {@link RedisOperations} to use for managing the sessions. Cannot be null.
* @param expirationRedisOperations The {@link RedisOperations} to use for managing expiration mappings. Cannot be null.
*/
public RedisOperationsSessionRepository(RedisOperations<String, ExpiringSession> redisOperations) {
Assert.notNull(redisOperations, "RedisOperations cannot be null");
this.redisOperations = redisOperations;
public RedisOperationsSessionRepository(RedisOperations<String, ExpiringSession> sessionRedisOperations, RedisOperations<String,String> expirationRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
Assert.notNull(expirationRedisOperations, "expirationRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, expirationRedisOperations);
}
/**
@@ -154,8 +189,26 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
session.saveDelta();
}
@Scheduled(cron="0 * * * * *")
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
@Override
public RedisSession getSession(String id) {
return getSession(id, false);
}
/**
*
* @param id the session id
* @param allowExpired
* if true, will also include expired sessions that have not been
* deleted. If false, will ensure expired sessions are not
* returned.
* @return
*/
private RedisSession getSession(String id, boolean allowExpired) {
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
if(entries.isEmpty()) {
return null;
@@ -174,15 +227,27 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()), entry.getValue());
}
}
if(!allowExpired && loaded.isExpired()) {
return null;
}
RedisSession result = new RedisSession(loaded);
result.originalLastAccessTime = loaded.getLastAccessedTime() + TimeUnit.SECONDS.toMillis(loaded.getMaxInactiveInterval());
result.setLastAccessedTime(System.currentTimeMillis());
return result;
}
@Override
public void delete(String sessionId) {
ExpiringSession session = getSession(sessionId, true);
if(session == null) {
return;
}
String key = getKey(sessionId);
this.redisOperations.delete(key);
expirationPolicy.onDelete(session);
// always delete they key since session may be null if just expired
this.sessionRedisOperations.delete(key);
}
@Override
@@ -221,7 +286,17 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
*/
private BoundHashOperations<String, Object, Object> getSessionBoundHashOperations(String sessionId) {
String key = getKey(sessionId);
return this.redisOperations.boundHashOps(key);
return this.sessionRedisOperations.boundHashOps(key);
}
@SuppressWarnings("rawtypes")
private static RedisTemplate createDefaultTemplate(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory,"connectionFactory cannot be null");
RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(connectionFactory);
return template;
}
/**
@@ -235,6 +310,7 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
*/
final class RedisSession implements ExpiringSession {
private final MapSession cached;
private Long originalLastAccessTime;
private Map<String, Object> delta = new HashMap<String,Object>();
/**
@@ -262,6 +338,11 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
}
@Override
public boolean isExpired() {
return cached.isExpired();
}
@Override
public long getCreationTime() {
return cached.getCreationTime();
@@ -314,9 +395,11 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
* Saves any attributes that have been changed and updates the expiration of this session.
*/
private void saveDelta() {
getSessionBoundHashOperations(getId()).putAll(delta);
getSessionBoundHashOperations(getId()).expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
String sessionId = getId();
getSessionBoundHashOperations(sessionId).putAll(delta);
delta = new HashMap<String,Object>(delta.size());
expirationPolicy.onExpirationUpdated(originalLastAccessTime, this);
}
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2002-2013 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
*
* http://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.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.ExpiringSession;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
/**
* A strategy for expiring {@link RedisSession} instances. This performs two
* operations:
*
* Redis has no guarantees of when an expired session event will be fired. In
* order to ensure expired session events are processed in a timely fashion the
* expiration (rounded to the nearest minute) is mapped to all the sessions that
* expire at that time. Whenever {@link #cleanExpiredSessions()} is invoked, the
* sessions for the previous minute are then deleted explicitly.
*
* In some instances the {@link #cleanExpiredSessions()} method may not be not
* invoked for a specific time. For example, this may happen when a server is
* restarted. To account for this, the expiration on the Redis session is also set.
*
* @author Rob Winch
* @since 1.0
*/
final class RedisSessionExpirationPolicy {
private static final Log logger = LogFactory.getLog(RedisOperationsSessionRepository.class);
/**
* The prefix for each key of the Redis Hash representing a single session. The suffix is the unique session id.
*/
static final String EXPIRATION_BOUNDED_HASH_KEY_PREFIX = "spring:session:expirations:";
private final RedisOperations<String,ExpiringSession> sessionRedisOperations;
private final RedisOperations<String,String> expirationRedisOperations;
public RedisSessionExpirationPolicy(
RedisOperations<String, ExpiringSession> sessionRedisOperations,
RedisOperations<String, String> expirationRedisOperations) {
super();
this.sessionRedisOperations = sessionRedisOperations;
this.expirationRedisOperations = expirationRedisOperations;
}
public void onDelete(ExpiringSession session) {
long lastAccessedTime = session.getLastAccessedTime();
int maxInactiveInterval = session.getMaxInactiveInterval();
long toExpire = roundUpToNextMinute(lastAccessedTime, maxInactiveInterval);
String expireKey = getExpirationKey(toExpire);
expirationRedisOperations.boundSetOps(expireKey).remove(session.getId());
}
public void onExpirationUpdated(Long originalExpirationTime, ExpiringSession session) {
if(originalExpirationTime != null) {
String expireKey = getExpirationKey(originalExpirationTime);
expirationRedisOperations.boundSetOps(expireKey).remove(session.getId());
}
long toExpire = roundUpToNextMinute(session.getLastAccessedTime(), session.getMaxInactiveInterval());
String expireKey = getExpirationKey(toExpire);
expirationRedisOperations.boundSetOps(expireKey).add(session.getId());
long redisExpirationInSeconds = session.getMaxInactiveInterval();
String sessionKey = getSessionKey(session.getId());
expirationRedisOperations.boundSetOps(expireKey).expire(redisExpirationInSeconds, TimeUnit.SECONDS);
sessionRedisOperations.boundHashOps(sessionKey).expire(redisExpirationInSeconds, TimeUnit.SECONDS);
}
private String getExpirationKey(long expires) {
return EXPIRATION_BOUNDED_HASH_KEY_PREFIX + expires;
}
private String getSessionKey(String sessionId) {
return RedisOperationsSessionRepository.BOUNDED_HASH_KEY_PREFIX + sessionId;
}
public void cleanExpiredSessions() {
long now = System.currentTimeMillis();
long prevMin = roundDownMinute(now);
if(logger.isDebugEnabled()) {
logger.debug("Cleaning up sessions expiring at "+ new Date(prevMin));
}
String expirationKey = getExpirationKey(prevMin);
Set<String> sessionsToExpire = expirationRedisOperations.boundSetOps(expirationKey).members();
Set<String> keysToDelete = new HashSet<String>(sessionsToExpire.size() + 1);
keysToDelete.add(expirationKey);
for(String session : sessionsToExpire) {
String sessionKey = getSessionKey(session);
keysToDelete.add(sessionKey);
}
sessionRedisOperations.delete(keysToDelete);
if(logger.isDebugEnabled()) {
logger.debug("The following expired Sessions were deleted " + keysToDelete);
}
}
private long roundUpToNextMinute(long timeInMs, int inactiveIntervalInSec) {
Calendar date = Calendar.getInstance();
date.setTimeInMillis(timeInMs + TimeUnit.SECONDS.toMillis(inactiveIntervalInSec));
date.add(Calendar.MINUTE, 1);
date.clear(Calendar.SECOND);
date.clear(Calendar.MILLISECOND);
return date.getTimeInMillis();
}
private long roundDownMinute(long timeInMs) {
Calendar date = Calendar.getInstance();
date.setTimeInMillis(timeInMs);
date.add(Calendar.MINUTE, -1);
date.clear(Calendar.SECOND);
date.clear(Calendar.MILLISECOND);
return date.getTimeInMillis();
}
}

View File

@@ -19,6 +19,7 @@ import java.util.Map;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
@@ -55,7 +56,7 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
private HttpSessionStrategy httpSessionStrategy;
@Bean
public RedisTemplate<String,ExpiringSession> redisTemplate(RedisConnectionFactory connectionFactory) {
public RedisTemplate<String,ExpiringSession> sessionRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
@@ -64,8 +65,17 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
}
@Bean
public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> redisTemplate) {
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
public RedisTemplate<String,String> expirationRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<String, String>();
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(connectionFactory);
return template;
}
@Bean
public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate, @Qualifier("expirationRedisTemplate") RedisTemplate<String,String> expirationRedisTemplate) {
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate,expirationRedisTemplate);
sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds);
return sessionRepository;
}

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