Add RedisFlushMode

This commit allows Redis support to flush lazily (already
supported) or immediately (new).

Fixes gh-273
This commit is contained in:
Rob Winch
2016-02-10 08:41:49 -06:00
parent 5f23b3c272
commit e0bbbbcd45
8 changed files with 317 additions and 3 deletions

View File

@@ -34,6 +34,7 @@ Below are the highlights of what is new in Spring Session 1.1. You can find a co
* https://github.com/spring-projects/spring-session/issues/277[#277] - Added link:guides/hazelcast-spring.html[@EnableHazelcastHttpSession]
* https://github.com/spring-projects/spring-session/issues/271[#271] - Performance improvements
* https://github.com/spring-projects/spring-session/pull/218[#218] - Allow scoping the session in Redis using <<api-redisoperationssessionrepository-config,redisNamespace>>
* https://github.com/spring-projects/spring-session/issues/273[#273] - Allow writing to Redis immediately (instead of lazily) using <<api-redisoperationssessionrepository-config,redisFlushMode>>
* https://github.com/spring-projects/spring-session/issues/272[#272] - Add `ExpiringSession.setLastAccessedTime(long)`
[[samples]]
@@ -565,6 +566,8 @@ You can use the following attributes to customize the configuration:
* **maxInactiveIntervalInSeconds** - the amount of time before the session will expire in seconds
* **redisNamespace** - allows configuring an application specific namespace for the sessions. Redis keys and channel ids will start with the prefix of `spring:session:<redisNamespace>:`.
* **redisFlushMode** - allows specifying when data will be written to Redis. The default is only when `save` is invoked on `SessionRepository`.
A value of `RedisFlushMode.IMMEDIATE` will write to Redis as soon as possible.
===== Custom RedisSerializer

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2002-2016 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.flushimmediately;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* @author Rob Winch
*
*/
@Configuration
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
public class RedisHttpSessionConfig {
@Bean
public JedisConnectionFactory connectionFactory() throws Exception {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setUsePool(false);
return factory;
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2002-2015 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.flushimmediately;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=RedisHttpSessionConfig.class)
@WebAppConfiguration
public class RedisOperationsSessionRepositoryFlushImmediatelyITests<S extends ExpiringSession> {
@Autowired
private SessionRepository<S> sessionRepository;
@Test
public void savesOnCreate() throws InterruptedException {
S created = sessionRepository.createSession();
S getSession = sessionRepository.getSession(created.getId());
assertThat(getSession).isNotNull();
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2002-2016 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 org.springframework.session.SessionRepository;
/**
* Specifies when to write to the backing Redis instance.
*
* @author Rob Winch
* @since 1.1
*/
public enum RedisFlushMode {
/**
* Only writes to Redis when
* {@link SessionRepository#save(org.springframework.session.Session)} is
* invoked. In a web environment this is typically done as soon as the HTTP
* response is committed.
*/
ON_SAVE,
/**
* Writes to Redis as soon as possible. For example
* {@link SessionRepository#createSession()} will write the session to
* Redis. Another example is that setting an attribute on the session will
* also write to Redis immediately.
*/
IMMEDIATE
}

View File

@@ -304,6 +304,8 @@ public class RedisOperationsSessionRepository implements FindByIndexNameSessionR
private RedisSerializer<Object> defaultSerializer = new JdkSerializationRedisSerializer();
private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;
/**
* Allows creating an instance and uses a default {@link RedisOperations} for both managing the session and the expirations.
*
@@ -360,6 +362,14 @@ public class RedisOperationsSessionRepository implements FindByIndexNameSessionR
this.defaultSerializer = defaultSerializer;
}
/**
* @param redisFlushMode
*/
public void setRedisFlushMode(RedisFlushMode redisFlushMode) {
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
this.redisFlushMode = redisFlushMode;
}
public void save(RedisSession session) {
session.saveDelta();
if(session.isNew()) {
@@ -636,6 +646,7 @@ public class RedisOperationsSessionRepository implements FindByIndexNameSessionR
delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());
delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
this.isNew = true;
flushImmediateIfNecessary();
}
/**
@@ -656,6 +667,7 @@ public class RedisOperationsSessionRepository implements FindByIndexNameSessionR
public void setLastAccessedTime(long lastAccessedTime) {
cached.setLastAccessedTime(lastAccessedTime);
delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
flushImmediateIfNecessary();
}
public boolean isExpired() {
@@ -681,6 +693,7 @@ public class RedisOperationsSessionRepository implements FindByIndexNameSessionR
public void setMaxInactiveIntervalInSeconds(int interval) {
cached.setMaxInactiveIntervalInSeconds(interval);
delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());
flushImmediateIfNecessary();
}
public int getMaxInactiveIntervalInSeconds() {
@@ -699,11 +712,19 @@ public class RedisOperationsSessionRepository implements FindByIndexNameSessionR
public void setAttribute(String attributeName, Object attributeValue) {
cached.setAttribute(attributeName, attributeValue);
delta.put(getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
}
public void removeAttribute(String attributeName) {
cached.removeAttribute(attributeName);
delta.put(getSessionAttrNameKey(attributeName), null);
flushImmediateIfNecessary();
}
private void flushImmediateIfNecessary() {
if(redisFlushMode == RedisFlushMode.IMMEDIATE) {
saveDelta();
}
}
/**

View File

@@ -22,7 +22,9 @@ import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.RedisFlushMode;
/**
* Add this annotation to an {@code @Configuration} class to expose the
@@ -76,4 +78,22 @@ public @interface EnableRedisHttpSession {
* @return the unique namespace for keys
*/
String redisNamespace() default "";
/**
* <p>
* Sets the flush mode for the Redis sessions. The default is IMMEDIATE
* which only updates the backing Redis when
* {@link SessionRepository#save(org.springframework.session.Session)} is
* invoked. In a web environment this happens just before the HTTP resposne
* is committed.
* </p>
* <p>
* Setting the value to IMMEDIATE will ensure that the any updates to the
* Session are immediately written to the Redis instance.
* </p>
*
* @return the {@link RedisFlushMode} to use
* @since 1.1
*/
RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
}

View File

@@ -37,10 +37,12 @@ import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -63,6 +65,8 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
private String redisNamespace = "";
private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;
private RedisSerializer<Object> defaultRedisSerializer;
@Bean
@@ -102,6 +106,8 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
if(StringUtils.hasText(redisNamespace)) {
sessionRepository.setRedisKeyNamespace(redisNamespace);
}
sessionRepository.setRedisFlushMode(redisFlushMode);
return sessionRepository;
}
@@ -113,6 +119,11 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.redisNamespace = namespace;
}
public void setRedisFlushMode(RedisFlushMode redisFlushMode) {
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
this.redisFlushMode = redisFlushMode;
}
private String getRedisNamespace() {
if(StringUtils.hasText(this.redisNamespace)) {
return this.redisNamespace;
@@ -126,6 +137,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap);
maxInactiveIntervalInSeconds = enableAttrs.getNumber("maxInactiveIntervalInSeconds");
this.redisNamespace = enableAttrs.getString("redisNamespace");
this.redisFlushMode = enableAttrs.getEnum("redisFlushMode");
}
@Bean

View File

@@ -15,12 +15,16 @@
*/
package org.springframework.session.data.redis;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.CREATION_TIME_ATTR;
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.LAST_ACCESSED_ATTR;
@@ -466,6 +470,132 @@ public class RedisOperationsSessionRepositoryTests {
assertThat(resolver.resolvePrincipal(session)).isEqualTo(principal);
}
@Test
public void flushModeOnSaveCreate() {
redisRepository.createSession();
verifyZeroInteractions(boundHashOperations);
}
@Test
public void flushModeOnSaveSetAttribute() {
RedisSession session = redisRepository.createSession();
session.setAttribute("something", "here");
verifyZeroInteractions(boundHashOperations);
}
@Test
public void flushModeOnSaveRemoveAttribute() {
RedisSession session = redisRepository.createSession();
session.removeAttribute("remove");
verifyZeroInteractions(boundHashOperations);
}
@Test
public void flushModeOnSaveSetLastAccessedTime() {
RedisSession session = redisRepository.createSession();
session.setLastAccessedTime(1L);
verifyZeroInteractions(boundHashOperations);
}
@Test
public void flushModeOnSaveSetMaxInactiveIntervalInSeconds() {
RedisSession session = redisRepository.createSession();
session.setMaxInactiveIntervalInSeconds(1);
verifyZeroInteractions(boundHashOperations);
}
@Test
public void flushModeImmediateCreate() {
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
redisRepository.setRedisFlushMode(RedisFlushMode.IMMEDIATE);
RedisSession session = redisRepository.createSession();
Map<String, Object> delta = getDelta();
assertThat(delta.size()).isEqualTo(3);
Object creationTime = delta.get(CREATION_TIME_ATTR);
assertThat(creationTime).isEqualTo(session.getCreationTime());
assertThat(delta.get(MAX_INACTIVE_ATTR)).isEqualTo(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
assertThat(delta.get(LAST_ACCESSED_ATTR)).isEqualTo(session.getCreationTime());
}
@Test
public void flushModeImmediateSetAttribute() {
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
redisRepository.setRedisFlushMode(RedisFlushMode.IMMEDIATE);
RedisSession session = redisRepository.createSession();
String attrName = "someAttribute";
session.setAttribute(attrName, "someValue");
Map<String, Object> delta = getDelta(2);
assertThat(delta.size()).isEqualTo(1);
assertThat(delta).isEqualTo(map(getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
public void flushModeImmediateRemoveAttribute() {
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
redisRepository.setRedisFlushMode(RedisFlushMode.IMMEDIATE);
RedisSession session = redisRepository.createSession();
String attrName = "someAttribute";
session.removeAttribute(attrName);
Map<String, Object> delta = getDelta(2);
assertThat(delta.size()).isEqualTo(1);
assertThat(delta).isEqualTo(map(getSessionAttrNameKey(attrName), session.getAttribute(attrName)));
}
@Test
public void flushModeSetMaxInactiveIntervalInSeconds() {
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
redisRepository.setRedisFlushMode(RedisFlushMode.IMMEDIATE);
RedisSession session = redisRepository.createSession();
reset(boundHashOperations);
session.setMaxInactiveIntervalInSeconds(1);
verify(boundHashOperations).expire(anyLong(), any(TimeUnit.class));
}
@Test
public void flushModeSetLastAccessedTime() {
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
redisRepository.setRedisFlushMode(RedisFlushMode.IMMEDIATE);
RedisSession session = redisRepository.createSession();
long now = System.currentTimeMillis();
session.setLastAccessedTime(now);
Map<String, Object> delta = getDelta(2);
assertThat(delta.size()).isEqualTo(1);
assertThat(delta).isEqualTo(map(LAST_ACCESSED_ATTR, session.getLastAccessedTime()));
}
@Test(expected = IllegalArgumentException.class)
public void setRedisFlushModeNull() {
redisRepository.setRedisFlushMode(null);
}
private String getKey(String id) {
return "spring:session:sessions:" + id;
}
@@ -482,7 +612,11 @@ public class RedisOperationsSessionRepositoryTests {
}
private Map<String,Object> getDelta() {
verify(boundHashOperations).putAll(delta.capture());
return delta.getValue();
return getDelta(1);
}
private Map<String,Object> getDelta(int times) {
verify(boundHashOperations,times(times)).putAll(delta.capture());
return delta.getAllValues().get(times - 1);
}
}