Add Sesison to SessionExpiredEvent
Fixes gh-260
This commit is contained in:
@@ -301,27 +301,94 @@ For additional information on how to create a `RedisConnectionFactory`, refer to
|
||||
[[api-redisoperationssessionrepository-storage]]
|
||||
==== Storage Details
|
||||
|
||||
The sections below outline how Redis is updated for each operation.
|
||||
An example of creating a new session can be found below.
|
||||
The subsequent sections describe the details.
|
||||
|
||||
----
|
||||
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \
|
||||
maxInactiveInterval 1800 \
|
||||
lastAccessedTime 1404360000000 \
|
||||
sessionAttr:attrName someAttrValue \
|
||||
sessionAttr2:attrName someAttrValue2
|
||||
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
|
||||
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
|
||||
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
|
||||
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
|
||||
EXPIRE spring:session:expirations1439245080000 2100
|
||||
----
|
||||
|
||||
===== Saving a Session
|
||||
|
||||
Each session is stored in Redis as a Hash.
|
||||
Each session is set and updated using the HMSET command.
|
||||
An example of how each session is stored can be seen below.
|
||||
|
||||
HMSET spring:session:sessions:<session-id> creationTime 1404360000000 \
|
||||
maxInactiveInterval 1800 lastAccessedTime 1404360000000 \
|
||||
sessionAttr:<attrName> someAttrValue sessionAttr:<attrName2> someAttrValue2
|
||||
|
||||
----
|
||||
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \
|
||||
maxInactiveInterval 1800 \
|
||||
lastAccessedTime 1404360000000 \
|
||||
sessionAttr:attrName someAttrValue \
|
||||
sessionAttr2:attrName someAttrValue2
|
||||
----
|
||||
|
||||
In this example, the session following statements are true about the session:
|
||||
|
||||
* The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe
|
||||
* The session was created at 1404360000000 in milliseconds since midnight of 1/1/1970 GMT.
|
||||
* The session expires in 1800 seconds (30 minutes).
|
||||
* The session was last accessed at 1404360000000 in milliseconds since midnight of 1/1/1970 GMT.
|
||||
* The session has two attributes.
|
||||
The first is "attrName" with the value of "someAttrValue".
|
||||
The second session attribute is named "attrName2" with the value of "someAttrValue2".
|
||||
|
||||
[[api-redisoperationssessionrepository-writes]]
|
||||
===== Optimized Writes
|
||||
|
||||
The `Session` instances managed by `RedisOperationsSessionRepository` 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:
|
||||
|
||||
----
|
||||
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
|
||||
----
|
||||
|
||||
[[api-redisoperationssessionrepository-expiration]]
|
||||
===== Session Expiration
|
||||
|
||||
An expiration is associated to each session using the EXPIRE command based upon the RedisOperationsSessionRepository.RedisSession.getMaxInactiveInterval().
|
||||
An expiration is associated to each session using the EXPIRE command based upon the `ExpiringSession.getMaxInactiveInterval()`.
|
||||
For example:
|
||||
|
||||
EXPIRE spring:session:sessions:<session-id> 1800
|
||||
----
|
||||
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
|
||||
----
|
||||
|
||||
You will note that the expiration that is set is 5 minutes after the session actually expires.
|
||||
This is necessary so that the value of the session can be accessed when the session expires.
|
||||
An expiration is set on the session itself five minutes after it actually expires to ensure it is cleaned up, but only after we perform any necessary processing.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The `SessionRepository.getSession(String)` method ensures that no expired sessions will be returned.
|
||||
This means there is no need to check the expiration before using a session.
|
||||
====
|
||||
|
||||
Spring Session relies on the expired and delete http://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <<api-redisoperationssessionrepository-sessiondestroyedevent,SessionDeletedEvent>> and <<api-redisoperationssessionrepository-sessiondestroyedevent,SessionExpiredEvent>> respectively.
|
||||
It is the `SessionDeletedEvent` or `SessionExpiredEvent` that ensures resources associated with the Session are cleaned up.
|
||||
For example, when using Spring Session's WebSocket support the Redis expired or delete event is what triggers any WebSocket connections associated with the session to be closed.
|
||||
|
||||
One problem with this approach is that Redis makes no guarantee of when the expired event will be fired if they key has not been accessed.
|
||||
Expiration is not tracked directly on the session key itself since this would mean the session data would no longer be available. Instead a special session expires key is used. In our example the expires key is:
|
||||
|
||||
----
|
||||
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
|
||||
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
|
||||
----
|
||||
|
||||
When a session expires key is deleted or expires, the keyspace notification triggers a lookup of the actual session and a SessionDestroyedEvent is fired.
|
||||
|
||||
One problem with relying on Redis expiration exclusively is that Redis makes no guarantee of when the expired event will be fired if they key has not been accessed.
|
||||
Specifically the background task that Redis uses to clean up expired keys is a low priority task and may not trigger the key expiration.
|
||||
For additional details see http://redis.io/topics/notifications[Timing of expired events] section in the Redis documentation.
|
||||
|
||||
@@ -332,26 +399,21 @@ For this reason, each session expiration is also tracked to the nearest minute.
|
||||
This allows a background task to access the potentially expired sessions to ensure that Redis expired events are fired in a more deterministic fashion.
|
||||
For example:
|
||||
|
||||
SADD spring:session:expirations:<expire-rounded-up-to-nearest-minute> <session-id>
|
||||
EXPIRE spring:session:expirations:<expire-rounded-up-to-nearest-minute> 1860
|
||||
----
|
||||
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
|
||||
EXPIRE spring:session:expirations1439245080000 2100
|
||||
----
|
||||
|
||||
The background task will then use these mappings to explicitly request each key.
|
||||
By accessing they key, rather than deleting it, we ensure that Redis deletes the key for us only if the TTL is expired.
|
||||
|
||||
NOTE: We do not explicitly delete the keys since in some instances there may be a race condition that incorrectly identifies a key as expired when it is not.
|
||||
[NOTE]
|
||||
====
|
||||
We do not explicitly delete the keys since in some instances there may be a race condition that incorrectly identifies a key as expired when it is not.
|
||||
Short of using distributed locks (which would kill our performance) there is no way to ensure the consistency of the expiration mapping.
|
||||
By simply accessing the key, we ensure that the key is only removed if the TTL on that key is expired.
|
||||
====
|
||||
|
||||
[[api-redisoperationssessionrepository-writes]]
|
||||
===== Optimized Writes
|
||||
|
||||
The `Session` instances managed by `RedisOperationsSessionRepository` 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:
|
||||
|
||||
HMSET spring:session:sessions:<session-id> sessionAttr:<attrName2> newValue
|
||||
EXPIRE spring:session:sessions:<session-id> 1800
|
||||
|
||||
[[api-redisoperationssessionrepository-sessiondestroyedevent]]
|
||||
==== SessionDeletedEvent and SessionExpiredEvent
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
import org.springframework.session.events.SessionDeletedEvent;
|
||||
import org.springframework.session.events.SessionDestroyedEvent;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
@@ -46,7 +46,7 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
private SessionRepository<S> repository;
|
||||
|
||||
@Autowired
|
||||
private SessionDeletedEventRegistry registry;
|
||||
private SessionDestroyedEventRegistry registry;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
@@ -58,7 +58,9 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
@Test
|
||||
public void saves() throws InterruptedException {
|
||||
S toSave = repository.createSession();
|
||||
toSave.setAttribute("a", "b");
|
||||
String expectedAttributeName = "a";
|
||||
String expectedAttributeValue = "b";
|
||||
toSave.setAttribute(expectedAttributeName, expectedAttributeValue);
|
||||
Authentication toSaveToken = new UsernamePasswordAuthenticationToken("user","password", AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
|
||||
toSaveContext.setAuthentication(toSaveToken);
|
||||
@@ -70,7 +72,7 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
|
||||
assertThat(session.getId()).isEqualTo(toSave.getId());
|
||||
assertThat(session.getAttributeNames()).isEqualTo(session.getAttributeNames());
|
||||
assertThat(session.getAttribute("a")).isEqualTo(toSave.getAttribute("a"));
|
||||
assertThat(session.getAttribute(expectedAttributeName)).isEqualTo(toSave.getAttribute(expectedAttributeName));
|
||||
|
||||
repository.delete(toSave.getId());
|
||||
|
||||
@@ -79,6 +81,9 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
lock.wait(3000);
|
||||
}
|
||||
assertThat(registry.receivedEvent()).isTrue();
|
||||
|
||||
|
||||
assertThat(registry.getEvent().getSession().getAttribute(expectedAttributeName)).isEqualTo(expectedAttributeValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -100,19 +105,23 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
assertThat(session.getAttribute("1")).isEqualTo("2");
|
||||
}
|
||||
|
||||
static class SessionDeletedEventRegistry implements ApplicationListener<SessionDeletedEvent> {
|
||||
private boolean receivedEvent;
|
||||
static class SessionDestroyedEventRegistry implements ApplicationListener<SessionDestroyedEvent> {
|
||||
private SessionDestroyedEvent event;
|
||||
private Object lock;
|
||||
|
||||
public void onApplicationEvent(SessionDeletedEvent event) {
|
||||
receivedEvent = true;
|
||||
public void onApplicationEvent(SessionDestroyedEvent event) {
|
||||
this.event = event;
|
||||
synchronized (lock) {
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean receivedEvent() {
|
||||
return receivedEvent;
|
||||
return this.event != null;
|
||||
}
|
||||
|
||||
public SessionDestroyedEvent getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
public void setLock(Object lock) {
|
||||
@@ -131,8 +140,8 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionDeletedEventRegistry sessionDestroyedEventRegistry() {
|
||||
return new SessionDeletedEventRegistry();
|
||||
public SessionDestroyedEventRegistry sessionDestroyedEventRegistry() {
|
||||
return new SessionDestroyedEventRegistry();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,12 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.BoundHashOperations;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
@@ -42,7 +48,7 @@ import org.springframework.util.Assert;
|
||||
* {@link org.springframework.data.redis.core.RedisOperations}. In a web
|
||||
* environment, this is typically used in combination with
|
||||
* {@link SessionRepositoryFilter}. This implementation supports
|
||||
* {@link SessionDeletedEvent} and {@link SessionExpiredEvent} through {@link SessionMessageListener}.
|
||||
* {@link SessionDeletedEvent} and {@link SessionExpiredEvent} by implementing {@link MessageListener}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Creating a new instance</h2>
|
||||
@@ -52,36 +58,61 @@ import org.springframework.util.Assert;
|
||||
* <pre>
|
||||
* JedisConnectionFactory factory = new JedisConnectionFactory();
|
||||
*
|
||||
* RedisOperationsSessionRepository redisSessionRepository = new RedisOperationsSessionRepository(
|
||||
* factory);
|
||||
* 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>
|
||||
*
|
||||
* The sections below outline how Redis is updated for each operation. An
|
||||
* example of creating a new session can be found below. The subsequent sections
|
||||
* describe the details.
|
||||
*
|
||||
* <pre>
|
||||
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName someAttrValue2
|
||||
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
|
||||
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
|
||||
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
|
||||
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
|
||||
* EXPIRE spring:session:expirations1439245080000 2100
|
||||
* </pre>
|
||||
*
|
||||
* <h3>Saving a Session</h3>
|
||||
*
|
||||
* <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
|
||||
* 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:session:sessions:<session-id> creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:<attrName> someAttrValue sessionAttr2:<attrName> someAttrValue2</pre>
|
||||
* <pre>
|
||||
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 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#getMaxInactiveIntervalInSeconds()}
|
||||
* . For example:
|
||||
* In this example, the session following statements are true about the session:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe</li>
|
||||
* <li>The session was created at 1404360000000 in milliseconds since midnight
|
||||
* of 1/1/1970 GMT.</li>
|
||||
* <li>The session expires in 1800 seconds (30 minutes).</li>
|
||||
* <li>The session was last accessed at 1404360000000 in milliseconds since
|
||||
* midnight of 1/1/1970 GMT.</li>
|
||||
* <li>The session has two attributes. The first is "attrName" with the value of
|
||||
* "someAttrValue". The second session attribute is named "attrName2" with the
|
||||
* value of "someAttrValue2".</li>
|
||||
* </ul>
|
||||
*
|
||||
* <pre>EXPIRE spring:session:sessions:<session-id> 1800</pre>
|
||||
*
|
||||
* <h3>Optimized Writes</h3>
|
||||
*
|
||||
* <p>
|
||||
* The {@link RedisSession} keeps track of the properties that have changed and
|
||||
@@ -92,54 +123,118 @@ import org.springframework.util.Assert;
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* HMSET spring:session:sessions:<session-id> sessionAttr2:<attrName> newValue
|
||||
* EXPIRE spring:session:sessions:<session-id> 1800
|
||||
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* Spring Session relies on the expired and delete <a href="http://redis.io/topics/notifications">keyspace notifications</a> from Redis to fire a <<SessionDestroyedEvent>>.
|
||||
* It is the `SessionDestroyedEvent` that ensures resources associated with the Session are cleaned up.
|
||||
* For example, when using Spring Session's WebSocket support the Redis expired or delete event is what triggers any
|
||||
* WebSocket connections associated with the session to be closed.
|
||||
* </p>
|
||||
* <h3>Expiration</h3>
|
||||
*
|
||||
* <p>
|
||||
* One problem with this approach is that Redis makes no guarantee of when the expired event will be fired if they key has not been accessed.
|
||||
* Specifically the background task that Redis uses to clean up expired keys is a low priority task and may not trigger the key expiration.
|
||||
* For additional details see <a href="http://redis.io/topics/notifications">Timing of expired events</a> section in the Redis documentation.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* To circumvent the fact that expired events are not guaranteed to happen we can ensure that each key is accessed when it is expected to expire.
|
||||
* This means that if the TTL is expired on the key, Redis will remove the key and fire the expired event when we try to access they key.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* For this reason, each session expiration is also tracked to the nearest minute.
|
||||
* This allows a background task to access the potentially expired sessions to ensure that Redis expired events are fired in a more deterministic fashion.
|
||||
* 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#getMaxInactiveIntervalInSeconds()}
|
||||
* . For example:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* SADD spring:session:expirations:<expire-rounded-up-to-nearest-minute> <session-id>
|
||||
* EXPIRE spring:session:expirations:<expire-rounded-up-to-nearest-minute> 1800
|
||||
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* The background task will then use these mappings to explicitly request each key.
|
||||
* By accessing they key, rather than deleting it, we ensure that Redis deletes the key for us only if the TTL is expired.
|
||||
* You will note that the expiration that is set is 5 minutes after the session
|
||||
* actually expires. This is necessary so that the value of the session can be
|
||||
* accessed when the session expires. An expiration is set on the session itself
|
||||
* five minutes after it actually expires to ensure it is cleaned up, but only
|
||||
* after we perform any necessary processing.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The {@link #getSession(String)} method ensures that no expired
|
||||
* sessions will be returned. This means there is no need to check the
|
||||
* expiration before using a session
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Spring Session relies on the expired and delete
|
||||
* <a href="http://redis.io/topics/notifications">keyspace notifications</a>
|
||||
* from Redis to fire a SessionDestroyedEvent. It is the
|
||||
* SessionDestroyedEvent that ensures resources associated with the Session
|
||||
* are cleaned up. For example, when using Spring Session's WebSocket support
|
||||
* the Redis expired or delete event is what triggers any WebSocket connections
|
||||
* associated with the session to be closed.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Expiration is not tracked directly on the session key itself since this would
|
||||
* mean the session data would no longer be available. Instead a special session
|
||||
* expires key is used. In our example the expires key is:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
|
||||
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* When a session expires key is deleted or expires, the keyspace notification
|
||||
* triggers a lookup of the actual session and a {@link SessionDestroyedEvent}
|
||||
* is fired.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* One problem with relying on Redis expiration exclusively is that Redis makes
|
||||
* no guarantee of when the expired event will be fired if they key has not been
|
||||
* accessed. Specifically the background task that Redis uses to clean up
|
||||
* expired keys is a low priority task and may not trigger the key expiration.
|
||||
* For additional details see
|
||||
* <a href="http://redis.io/topics/notifications">Timing of expired events</a>
|
||||
* section in the Redis documentation.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* To circumvent the fact that expired events are not guaranteed to happen we
|
||||
* can ensure that each key is accessed when it is expected to expire. This
|
||||
* means that if the TTL is expired on the key, Redis will remove the key and
|
||||
* fire the expired event when we try to access they key.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* For this reason, each session expiration is also tracked to the nearest
|
||||
* minute. This allows a background task to access the potentially expired
|
||||
* sessions to ensure that Redis expired events are fired in a more
|
||||
* deterministic fashion. For example:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
|
||||
* EXPIRE spring:session:expirations1439245080000 2100
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* The background task will then use these mappings to explicitly request each
|
||||
* session expires key. By accessing the key, rather than deleting it, we ensure
|
||||
* that Redis deletes the key for us only if the TTL is expired.
|
||||
* </p>
|
||||
* <p>
|
||||
* <b>NOTE</b>: We do not explicitly delete the keys since in some instances there may be a race condition that incorrectly identifies a key as expired when it is not.
|
||||
* Short of using distributed locks (which would kill our performance) there is no way to ensure the consistency of the expiration mapping.
|
||||
* By simply accessing the key, we ensure that the key is only removed if the TTL on that key is expired.
|
||||
* <b>NOTE</b>: We do not explicitly delete the keys since in some instances
|
||||
* there may be a race condition that incorrectly identifies a key as expired
|
||||
* when it is not. Short of using distributed locks (which would kill our
|
||||
* performance) there is no way to ensure the consistency of the expiration
|
||||
* mapping. By simply accessing the key, we ensure that the key is only removed
|
||||
* if the TTL on that key is expired.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class RedisOperationsSessionRepository implements SessionRepository<RedisOperationsSessionRepository.RedisSession> {
|
||||
public class RedisOperationsSessionRepository implements SessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
|
||||
private static final Log logger = LogFactory.getLog(SessionMessageListener.class);
|
||||
|
||||
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
|
||||
public void publishEvent(ApplicationEvent event) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The prefix for each key of the Redis Hash representing a single session. The suffix is the unique session id.
|
||||
*/
|
||||
@@ -196,6 +291,20 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
|
||||
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ApplicationEventPublisher} that is used to publish
|
||||
* {@link SessionDestroyedEvent}. The default is to not publish a
|
||||
* {@link SessionDestroyedEvent}.
|
||||
*
|
||||
* @param applicationEventPublisher
|
||||
* the {@link ApplicationEventPublisher} that is used to publish
|
||||
* {@link SessionDestroyedEvent}. Cannot be null.
|
||||
*/
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
|
||||
this.eventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum inactive interval in seconds between requests before newly created sessions will be
|
||||
* invalidated. A negative time indicates that the session will never timeout. The default is 1800 (30 minutes).
|
||||
@@ -258,16 +367,22 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
|
||||
}
|
||||
|
||||
public void delete(String sessionId) {
|
||||
ExpiringSession session = getSession(sessionId, true);
|
||||
RedisSession session = getSession(sessionId, true);
|
||||
if(session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String key = getKey(sessionId);
|
||||
expirationPolicy.onDelete(session);
|
||||
|
||||
// always delete they key since session may be null if just expired
|
||||
this.sessionRedisOperations.delete(key);
|
||||
String expireKey = getExpiredKey(session.getId());
|
||||
this.sessionRedisOperations.delete(expireKey);
|
||||
|
||||
session.setMaxInactiveIntervalInSeconds(0);
|
||||
save(session);
|
||||
}
|
||||
|
||||
private String getExpiredKey(String sessionId) {
|
||||
return getKey("expires:" + sessionId);
|
||||
}
|
||||
|
||||
public RedisSession createSession() {
|
||||
@@ -278,6 +393,68 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
|
||||
return redisSession;
|
||||
}
|
||||
|
||||
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
byte[] messageChannel = message.getChannel();
|
||||
byte[] messageBody = message.getBody();
|
||||
if(messageChannel == null || messageBody == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String channel = new String(messageChannel);
|
||||
|
||||
String body = new String(messageBody);
|
||||
if(!body.startsWith("spring:session:sessions:expires")) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isDeleted = channel.endsWith(":del");
|
||||
if(isDeleted || channel.endsWith(":expired")) {
|
||||
int beginIndex = body.lastIndexOf(":") + 1;
|
||||
int endIndex = body.length();
|
||||
String sessionId = body.substring(beginIndex, endIndex);
|
||||
|
||||
RedisSession session = getSession(sessionId, true);
|
||||
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
|
||||
}
|
||||
|
||||
if(isDeleted) {
|
||||
handleDeleted(sessionId, session);
|
||||
} else {
|
||||
handleExpired(sessionId, session);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDeleted(String sessionId, RedisSession session) {
|
||||
if(session == null) {
|
||||
publishEvent(new SessionDeletedEvent(this, sessionId));
|
||||
} else {
|
||||
publishEvent(new SessionDeletedEvent(this, session));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleExpired(String sessionId, RedisSession session) {
|
||||
if(session == null) {
|
||||
publishEvent(new SessionExpiredEvent(this, sessionId));
|
||||
} else {
|
||||
publishEvent(new SessionExpiredEvent(this, session));
|
||||
}
|
||||
}
|
||||
|
||||
private void publishEvent(ApplicationEvent event) {
|
||||
try {
|
||||
this.eventPublisher.publishEvent(event);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.error("Error publishing " + event + ".", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Hash key for this session by prefixing it appropriately.
|
||||
*
|
||||
@@ -342,6 +519,8 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
|
||||
delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new instance from the provided {@link MapSession}
|
||||
*
|
||||
|
||||
@@ -68,23 +68,27 @@ final class RedisSessionExpirationPolicy {
|
||||
}
|
||||
|
||||
public void onExpirationUpdated(Long originalExpirationTimeInMilli, ExpiringSession session) {
|
||||
String keyToExpire = "expires:" + session.getId();
|
||||
if(originalExpirationTimeInMilli != null) {
|
||||
long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);
|
||||
String expireKey = getExpirationKey(originalRoundedUp);
|
||||
redis.boundSetOps(expireKey).remove(expireKey);
|
||||
redis.boundSetOps(expireKey).remove(keyToExpire);
|
||||
}
|
||||
|
||||
long toExpire = roundUpToNextMinute(expiresInMillis(session));
|
||||
|
||||
String expireKey = getExpirationKey(toExpire);
|
||||
BoundSetOperations<Object, Object> expireOperations = redis.boundSetOps(expireKey);
|
||||
expireOperations.add(session.getId());
|
||||
expireOperations.add(keyToExpire);
|
||||
|
||||
long sessionExpireInSeconds = session.getMaxInactiveIntervalInSeconds();
|
||||
String sessionKey = getSessionKey(session.getId());
|
||||
long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5);
|
||||
String sessionKey = getSessionKey(keyToExpire);
|
||||
|
||||
expireOperations.expire(sessionExpireInSeconds + 60, TimeUnit.SECONDS);
|
||||
redis.boundHashOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);
|
||||
expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
|
||||
redis.boundValueOps(sessionKey).append("");
|
||||
redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);
|
||||
redis.boundHashOps(getSessionKey(session.getId())).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
String getExpirationKey(long expires) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import javax.servlet.ServletContext;
|
||||
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;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -32,6 +33,7 @@ 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.RedisOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.listener.PatternTopic;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
@@ -67,26 +69,18 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
|
||||
|
||||
private HttpSessionStrategy httpSessionStrategy;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
|
||||
|
||||
@Bean
|
||||
public RedisMessageListenerContainer redisMessageListenerContainer(
|
||||
RedisConnectionFactory connectionFactory) {
|
||||
RedisConnectionFactory connectionFactory, RedisOperationsSessionRepository redisSessionMessageListener) {
|
||||
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(connectionFactory);
|
||||
container.addMessageListener(redisSessionMessageListener(),
|
||||
container.addMessageListener(redisSessionMessageListener,
|
||||
Arrays.asList(new PatternTopic("__keyevent@*:del"),new PatternTopic("__keyevent@*:expired")));
|
||||
return container;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionMessageListener redisSessionMessageListener() {
|
||||
return new SessionMessageListener(eventPublisher);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<Object,Object> sessionRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
|
||||
@@ -97,8 +91,9 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisOperationsSessionRepository sessionRepository(@Qualifier("sessionRedisTemplate") RedisOperations<Object, Object> sessionRedisTemplate) {
|
||||
public RedisOperationsSessionRepository sessionRepository(@Qualifier("sessionRedisTemplate") RedisOperations<Object, Object> sessionRedisTemplate, ApplicationEventPublisher applicationEventPublisher) {
|
||||
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
|
||||
sessionRepository.setApplicationEventPublisher(applicationEventPublisher);
|
||||
sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds);
|
||||
return sessionRepository;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.springframework.session.SessionRepository;
|
||||
* fired when a {@link Session} is destroyed via deletion.
|
||||
*
|
||||
* @author Mark Anderson
|
||||
* @author Rob Winch
|
||||
* @since 1.1
|
||||
*
|
||||
*/
|
||||
@@ -33,4 +34,7 @@ public class SessionDeletedEvent extends SessionDestroyedEvent {
|
||||
super(source, sessionId);
|
||||
}
|
||||
|
||||
}
|
||||
public SessionDeletedEvent(Object source, Session session) {
|
||||
super(source, session);
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,30 @@ import org.springframework.session.Session;
|
||||
public class SessionDestroyedEvent extends ApplicationEvent {
|
||||
private final String sessionId;
|
||||
|
||||
private final Session session;
|
||||
|
||||
public SessionDestroyedEvent(Object source, String sessionId) {
|
||||
super(source);
|
||||
this.sessionId = sessionId;
|
||||
this.session = null;
|
||||
}
|
||||
|
||||
public SessionDestroyedEvent(Object source, Session session) {
|
||||
super(source);
|
||||
this.session = session;
|
||||
this.sessionId = session.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Session} that was destroyed. For some
|
||||
* {@link SessionRepository} implementations it may not be possible to get
|
||||
* the original session in which case this may be null.
|
||||
*
|
||||
* @return the expired {@link Session} or null if the data store does not support obtaining it
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S extends Session> S getSession() {
|
||||
return (S) session;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.springframework.session.SessionRepository;
|
||||
* fired when a {@link Session} is destroyed via expiration.
|
||||
*
|
||||
* @author Mark Anderson
|
||||
* @author Rob Winch
|
||||
* @since 1.1
|
||||
*
|
||||
*/
|
||||
@@ -32,4 +33,8 @@ public class SessionExpiredEvent extends SessionDestroyedEvent {
|
||||
public SessionExpiredEvent(Object source, String sessionId) {
|
||||
super(source, sessionId);
|
||||
}
|
||||
|
||||
public SessionExpiredEvent(Object source, Session session) {
|
||||
super(source, session);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,10 @@
|
||||
package org.springframework.session.data.redis;
|
||||
|
||||
import static org.fest.assertions.Assertions.assertThat;
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
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;
|
||||
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.MAX_INACTIVE_ATTR;
|
||||
@@ -42,6 +44,7 @@ import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.BoundHashOperations;
|
||||
import org.springframework.data.redis.core.BoundSetOperations;
|
||||
import org.springframework.data.redis.core.BoundValueOperations;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.session.ExpiringSession;
|
||||
import org.springframework.session.MapSession;
|
||||
@@ -56,19 +59,28 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
@Mock
|
||||
RedisConnection connection;
|
||||
@Mock
|
||||
RedisOperations redisOperations;
|
||||
RedisOperations<Object,Object> redisOperations;
|
||||
@Mock
|
||||
BoundHashOperations<String, Object, Object> boundHashOperations;
|
||||
BoundValueOperations<Object, Object> boundValueOperations;
|
||||
@Mock
|
||||
BoundSetOperations<String, String> boundSetOperations;
|
||||
BoundHashOperations<Object, Object, Object> boundHashOperations;
|
||||
@Mock
|
||||
BoundSetOperations<Object, Object> boundSetOperations;
|
||||
@Captor
|
||||
ArgumentCaptor<Map<String,Object>> delta;
|
||||
|
||||
private MapSession cached;
|
||||
|
||||
private RedisOperationsSessionRepository redisRepository;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.redisRepository = new RedisOperationsSessionRepository(redisOperations);
|
||||
|
||||
cached = new MapSession();
|
||||
cached.setId("session-id");
|
||||
cached.setCreationTime(1404360000000L);
|
||||
cached.setLastAccessedTime(1404360000000L);
|
||||
}
|
||||
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
@@ -76,6 +88,11 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
new RedisOperationsSessionRepository((RedisConnectionFactory)null);
|
||||
}
|
||||
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
public void setApplicationEventPublisherNull() {
|
||||
redisRepository.setApplicationEventPublisher(null);
|
||||
}
|
||||
|
||||
// gh-61
|
||||
@Test
|
||||
public void constructorConnectionFactory() {
|
||||
@@ -104,25 +121,66 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
@Test
|
||||
public void saveNewSession() {
|
||||
RedisSession session = redisRepository.createSession();
|
||||
when(redisOperations.boundHashOps(getKey(session.getId()))).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
Map<String,Object> delta = getDelta();
|
||||
assertThat(delta.size()).isEqualTo(3);
|
||||
Object creationTime = delta.get(CREATION_TIME_ATTR);
|
||||
assertThat(creationTime).isInstanceOf(Long.class);
|
||||
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(creationTime);
|
||||
assertThat(delta.get(LAST_ACCESSED_ATTR)).isEqualTo(session.getCreationTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveJavadocSummary() {
|
||||
RedisSession session = redisRepository.createSession();
|
||||
|
||||
String sessionKey = "spring:session:sessions:" + session.getId();
|
||||
String backgroundExpireKey = "spring:session:expirations:" + RedisSessionExpirationPolicy.roundUpToNextMinute(RedisSessionExpirationPolicy.expiresInMillis(session));
|
||||
String destroyedTriggerKey = "spring:session:sessions:expires:" + session.getId();
|
||||
|
||||
when(redisOperations.boundHashOps(sessionKey)).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundSetOps(backgroundExpireKey)).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundValueOps(destroyedTriggerKey)).thenReturn(boundValueOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
// the actual data in the session expires 5 minutes after expiration so the data can be accessed in expiration events
|
||||
// if the session is retrieved and expired it will not be returned since getSession checks if it is expired
|
||||
long fiveMinutesAfterExpires = session.getMaxInactiveIntervalInSeconds() + TimeUnit.MINUTES.toSeconds(5);
|
||||
verify(boundHashOperations).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
|
||||
verify(boundSetOperations).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
|
||||
verify(boundSetOperations).add("expires:" + session.getId());
|
||||
verify(boundValueOperations).expire(1800L, TimeUnit.SECONDS);
|
||||
verify(boundValueOperations).append("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveJavadoc() {
|
||||
RedisSession session = redisRepository.new RedisSession(cached);
|
||||
|
||||
when(redisOperations.boundHashOps("spring:session:sessions:session-id")).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundSetOps("spring:session:expirations:1404361860000")).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundValueOps("spring:session:sessions:expires:session-id")).thenReturn(boundValueOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
// the actual data in the session expires 5 minutes after expiration so the data can be accessed in expiration events
|
||||
// if the session is retrieved and expired it will not be returned since getSession checks if it is expired
|
||||
verify(boundHashOperations).expire(session.getMaxInactiveIntervalInSeconds() + TimeUnit.MINUTES.toSeconds(5), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveLastAccessChanged() {
|
||||
RedisSession session = redisRepository.new RedisSession(new MapSession());
|
||||
RedisSession session = redisRepository.new RedisSession(new MapSession(cached));
|
||||
session.setLastAccessedTime(12345678L);
|
||||
when(redisOperations.boundHashOps(getKey(session.getId()))).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
@@ -134,8 +192,9 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
String attrName = "attrName";
|
||||
RedisSession session = redisRepository.new RedisSession(new MapSession());
|
||||
session.setAttribute(attrName, "attrValue");
|
||||
when(redisOperations.boundHashOps(getKey(session.getId()))).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
@@ -147,8 +206,9 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
String attrName = "attrName";
|
||||
RedisSession session = redisRepository.new RedisSession(new MapSession());
|
||||
session.removeAttribute(attrName);
|
||||
when(redisOperations.boundHashOps(getKey(session.getId()))).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
|
||||
|
||||
redisRepository.save(session);
|
||||
|
||||
@@ -158,7 +218,7 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
@Test
|
||||
public void redisSessionGetAttributes() {
|
||||
String attrName = "attrName";
|
||||
RedisSession session = redisRepository.new RedisSession(new MapSession());
|
||||
RedisSession session = redisRepository.new RedisSession();
|
||||
assertThat(session.getAttributeNames()).isEmpty();
|
||||
session.setAttribute(attrName, "attrValue");
|
||||
assertThat(session.getAttributeNames()).containsOnly(attrName);
|
||||
@@ -172,7 +232,9 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
MapSession expected = new MapSession();
|
||||
expected.setLastAccessedTime(System.currentTimeMillis() - 60000);
|
||||
expected.setAttribute(attrName, "attrValue");
|
||||
when(redisOperations.boundHashOps(getKey(expected.getId()))).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
|
||||
Map map = map(
|
||||
getSessionAttrNameKey(attrName), expected.getAttribute(attrName),
|
||||
CREATION_TIME_ATTR, expected.getCreationTime(),
|
||||
@@ -183,13 +245,16 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
|
||||
String id = expected.getId();
|
||||
redisRepository.delete(id);
|
||||
verify(redisOperations).delete(getKey(id));
|
||||
|
||||
assertThat(getDelta().get(MAX_INACTIVE_ATTR)).isEqualTo(0);
|
||||
verify(redisOperations).delete(getKey("expires:"+id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteNullSession() {
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundHashOps(anyString())).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
when(redisOperations.boundValueOps(anyString())).thenReturn(boundValueOperations);
|
||||
|
||||
String id = "abc";
|
||||
redisRepository.delete(id);
|
||||
@@ -249,12 +314,12 @@ public class RedisOperationsSessionRepositoryTests {
|
||||
when(redisOperations.boundHashOps(getKey(expiredId))).thenReturn(boundHashOperations);
|
||||
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
|
||||
|
||||
Set<String> expiredIds = new HashSet<String>(Arrays.asList("expired-key1","expired-key2"));
|
||||
Set<Object> expiredIds = new HashSet<Object>(Arrays.asList("expired-key1","expired-key2"));
|
||||
when(boundSetOperations.members()).thenReturn(expiredIds);
|
||||
|
||||
redisRepository.cleanupExpiredSessions();
|
||||
|
||||
for(String id : expiredIds) {
|
||||
for(Object id : expiredIds) {
|
||||
String expiredKey = RedisOperationsSessionRepository.BOUNDED_HASH_KEY_PREFIX + id;
|
||||
// https://github.com/spring-projects/spring-session/issues/93
|
||||
verify(redisOperations).hasKey(expiredKey);
|
||||
|
||||
@@ -26,6 +26,7 @@ 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.BoundValueOperations;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.session.MapSession;
|
||||
|
||||
@@ -33,7 +34,6 @@ import org.springframework.session.MapSession;
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
@SuppressWarnings({"rawtypes","unchecked"})
|
||||
public class RedisSessionExpirationPolicyTests {
|
||||
// Wed Apr 15 10:28:32 CDT 2015
|
||||
final static Long NOW = 1429111712346L;
|
||||
@@ -42,11 +42,13 @@ public class RedisSessionExpirationPolicyTests {
|
||||
final static Long ONE_MINUTE_AGO = 1429111652346L;
|
||||
|
||||
@Mock
|
||||
RedisOperations sessionRedisOperations;
|
||||
RedisOperations<Object,Object> sessionRedisOperations;
|
||||
@Mock
|
||||
BoundSetOperations setOperations;
|
||||
BoundSetOperations<Object,Object> setOperations;
|
||||
@Mock
|
||||
BoundHashOperations hashOperations;
|
||||
BoundHashOperations<Object,Object, Object> hashOperations;
|
||||
@Mock
|
||||
BoundValueOperations<Object, Object> valueOperations;
|
||||
|
||||
RedisSessionExpirationPolicy policy;
|
||||
|
||||
@@ -61,6 +63,7 @@ public class RedisSessionExpirationPolicyTests {
|
||||
|
||||
when(sessionRedisOperations.boundSetOps(anyString())).thenReturn(setOperations);
|
||||
when(sessionRedisOperations.boundHashOps(anyString())).thenReturn(hashOperations);
|
||||
when(sessionRedisOperations.boundValueOps(anyString())).thenReturn(valueOperations);
|
||||
}
|
||||
|
||||
// gh-169
|
||||
@@ -74,7 +77,7 @@ public class RedisSessionExpirationPolicyTests {
|
||||
|
||||
// verify the original is removed
|
||||
verify(sessionRedisOperations).boundSetOps(originalExpireKey);
|
||||
verify(setOperations).remove(session.getId());
|
||||
verify(setOperations).remove("expires:"+ session.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -86,8 +89,8 @@ public class RedisSessionExpirationPolicyTests {
|
||||
policy.onExpirationUpdated(null, session);
|
||||
|
||||
verify(sessionRedisOperations).boundSetOps(expectedExpireKey);
|
||||
verify(setOperations).add(session.getId());
|
||||
verify(setOperations).expire(session.getMaxInactiveIntervalInSeconds() + 60, TimeUnit.SECONDS);
|
||||
verify(setOperations).add("expires:" + session.getId());
|
||||
verify(setOperations).expire(session.getMaxInactiveIntervalInSeconds() + TimeUnit.MINUTES.toSeconds(5), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,6 +100,6 @@ public class RedisSessionExpirationPolicyTests {
|
||||
policy.onExpirationUpdated(null, session);
|
||||
|
||||
verify(sessionRedisOperations).boundHashOps(sessionKey);
|
||||
verify(hashOperations).expire(session.getMaxInactiveIntervalInSeconds(), TimeUnit.SECONDS);
|
||||
verify(hashOperations).expire(session.getMaxInactiveIntervalInSeconds() + TimeUnit.MINUTES.toSeconds(5), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user