Ensure Redis Configured to Send Keyspace Notifications

Previously there was a possibility that Session to WebSocket mapping was
leaked if keyspace notifications were not enabled in Redis.

To resolve this the RedisHttpSessionConfiguration now ensures that Redis
is configured to enable Keyspace notifications.

Fixes gh-76 gh-81
This commit is contained in:
Rob Winch
2014-12-15 16:13:03 -06:00
parent b3130edd98
commit 7f9b5c0515
13 changed files with 617 additions and 47 deletions

View File

@@ -47,18 +47,22 @@ public class SessionMessageListener implements MessageListener {
}
public void onMessage(Message message, byte[] pattern) {
byte[] messageChannel = message.getChannel();
byte[] messageBody = message.getBody();
if(messageBody == null) {
if(messageChannel == null || messageBody == null) {
return;
}
String channel = new String(messageChannel);
if(!(channel.endsWith(":del") || channel.endsWith(":expired"))) {
return;
}
String body = new String(messageBody);
if(!("del".equals(body) || "expired".equals(body))) {
if(!body.startsWith("spring:session:sessions:")) {
return;
}
String channel = new String(message.getChannel());
int beginIndex = channel.lastIndexOf(":") + 1;
int endIndex = channel.length();
String sessionId = channel.substring(beginIndex, endIndex);
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
publishEvent(new SessionDestroyedEvent(this, sessionId));
}

View File

@@ -15,9 +15,12 @@
*/
package org.springframework.session.data.redis.config.annotation.web.http;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
@@ -27,11 +30,13 @@ import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
@@ -51,6 +56,7 @@ import org.springframework.util.ClassUtils;
* @see EnableRedisHttpSession
*/
@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoaderAware {
private ClassLoader beanClassLoader;
@@ -68,7 +74,7 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(redisSessionMessageListener(),
new PatternTopic("__keyspace@0__:spring:session:sessions:*"));
Arrays.asList(new PatternTopic("__keyevent@*:del"),new PatternTopic("__keyevent@*:expired")));
return container;
}
@@ -136,6 +142,55 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
this.httpSessionStrategy = httpSessionStrategy;
}
@Bean
public EnableRedisKeyspaceNotificationsInitializer enableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory) {
return new EnableRedisKeyspaceNotificationsInitializer(connectionFactory);
}
/**
* Ensures that Redis is configured to send keyspace notifications. This is important to ensure that expiration and
* deletion of sessions trigger SessionDestroyedEvents. Without the SessionDestroyedEvent resources may not get
* cleaned up properly. For example, the mapping of the Session to WebSocket connections may not get cleaned up.
*/
static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean {
static final String CONFIG_NOTIFY_KEYSPACE_EVENTS = "notify-keyspace-events";
private final RedisConnectionFactory connectionFactory;
EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
RedisConnection connection = connectionFactory.getConnection();
String notifyOptions = getNotifyOptions(connection);
String customizedNotifyOptions = notifyOptions;
if(!customizedNotifyOptions.contains("E")) {
customizedNotifyOptions += "E";
}
boolean A = customizedNotifyOptions.contains("A");
if(!(A || customizedNotifyOptions.contains("g"))) {
customizedNotifyOptions += "g";
}
if(!(A || customizedNotifyOptions.contains("x"))) {
customizedNotifyOptions += "x";
}
if(!notifyOptions.equals(customizedNotifyOptions)) {
connection.setConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS, customizedNotifyOptions);
}
}
private String getNotifyOptions(RedisConnection connection) {
List<String> config = connection.getConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS);
if(config.size() < 2) {
return "";
}
return config.get(1);
}
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)

View File

@@ -73,8 +73,8 @@ public final class WebSocketRegistryListener implements ApplicationListener<Appl
return;
}
String id = getHttpSessionId(wsSession);
registerWsSession(id, wsSession);
String httpSessionId = getHttpSessionId(wsSession);
registerWsSession(httpSessionId, wsSession);
}
private String getHttpSessionId(WebSocketSession wsSession) {
@@ -93,27 +93,33 @@ public final class WebSocketRegistryListener implements ApplicationListener<Appl
if(logger.isDebugEnabled()) {
logger.debug("Removal of " + wsSessionId + " was " + result);
}
if(sessions.isEmpty()) {
httpSessionIdToWsSessions.remove(httpSessionId);
if(logger.isDebugEnabled()) {
logger.debug("Removed the corresponding HTTP Session for " + wsSessionId + " since it contained no WebSocket mappings");
}
}
}
}
private void registerWsSession(String sessionId, WebSocketSession wsSession) {
Map<String,WebSocketSession> sessions = httpSessionIdToWsSessions.get(sessionId);
private void registerWsSession(String httpSessionId, WebSocketSession wsSession) {
Map<String,WebSocketSession> sessions = httpSessionIdToWsSessions.get(httpSessionId);
if(sessions == null) {
sessions =
new ConcurrentHashMap<String,WebSocketSession>();
httpSessionIdToWsSessions.putIfAbsent(sessionId, sessions);
sessions = httpSessionIdToWsSessions.get(sessionId);
httpSessionIdToWsSessions.putIfAbsent(httpSessionId, sessions);
sessions = httpSessionIdToWsSessions.get(httpSessionId);
}
sessions.put(wsSession.getId(), wsSession);
}
private void closeWsSessions(String sessionId) {
Map<String,WebSocketSession> sessionsToClose = httpSessionIdToWsSessions.remove(sessionId);
private void closeWsSessions(String httpSessionId) {
Map<String,WebSocketSession> sessionsToClose = httpSessionIdToWsSessions.remove(httpSessionId);
if(sessionsToClose == null) {
return;
}
if(logger.isDebugEnabled()) {
logger.debug("Closing WebSocket connections associated to expired HTTP Session " + sessionId);
logger.debug("Closing WebSocket connections associated to expired HTTP Session " + httpSessionId);
}
for(WebSocketSession toClose : sessionsToClose.values()) {
try {