Add RedisFlushMode
This commit allows Redis support to flush lazily (already supported) or immediately (new). Fixes gh-273
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user