Skip to content

Commit 6da3518

Browse files
committed
WebSessionStore updates lastAccessTime on retrieve
Now that WebSessionStore is in charge of expiration checks on retrieve it makes sense to also update the lastAccessTime on retrieve at the same time, saving the need to call it after a retrieve. Issue: SPR-15963
1 parent cb2decc commit 6da3518

File tree

4 files changed

+49
-64
lines changed

4 files changed

+49
-64
lines changed

spring-web/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,9 @@ public WebSessionStore getSessionStore() {
7878

7979
@Override
8080
public Mono<WebSession> getSession(ServerWebExchange exchange) {
81-
return Mono.defer(() ->
82-
retrieveSession(exchange)
83-
.flatMap(this.getSessionStore()::updateLastAccessTime)
84-
.switchIfEmpty(this.sessionStore.createWebSession())
85-
.doOnNext(session -> exchange.getResponse().beforeCommit(() -> save(exchange, session))));
81+
return Mono.defer(() -> retrieveSession(exchange)
82+
.switchIfEmpty(this.sessionStore.createWebSession())
83+
.doOnNext(session -> exchange.getResponse().beforeCommit(() -> save(exchange, session))));
8684
}
8785

8886
private Mono<WebSession> retrieveSession(ServerWebExchange exchange) {
@@ -93,11 +91,7 @@ private Mono<WebSession> retrieveSession(ServerWebExchange exchange) {
9391

9492
private Mono<Void> save(ServerWebExchange exchange, WebSession session) {
9593
if (session.isExpired()) {
96-
return Mono.error(new IllegalStateException(
97-
"Sessions are checked for expiration and have their " +
98-
"lastAccessTime updated when first accessed during request processing. " +
99-
"However this session is expired meaning that maxIdleTime elapsed " +
100-
"before the call to session.save()."));
94+
return Mono.error(new IllegalStateException("Session='" + session.getId() + "' expired."));
10195
}
10296

10397
if (!session.isStarted()) {

spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class InMemoryWebSessionStore implements WebSessionStore {
4545

4646
private Clock clock = Clock.system(ZoneId.of("GMT"));
4747

48-
private final Map<String, WebSession> sessions = new ConcurrentHashMap<>();
48+
private final Map<String, InMemoryWebSession> sessions = new ConcurrentHashMap<>();
4949

5050

5151
/**
@@ -77,7 +77,7 @@ public Mono<WebSession> createWebSession() {
7777

7878
@Override
7979
public Mono<WebSession> retrieveSession(String id) {
80-
WebSession session = this.sessions.get(id);
80+
InMemoryWebSession session = this.sessions.get(id);
8181
if (session == null) {
8282
return Mono.empty();
8383
}
@@ -86,6 +86,7 @@ else if (session.isExpired()) {
8686
return Mono.empty();
8787
}
8888
else {
89+
session.updateLastAccessTime();
8990
return Mono.just(session);
9091
}
9192
}
@@ -98,27 +99,14 @@ public Mono<Void> removeSession(String id) {
9899

99100
public Mono<WebSession> updateLastAccessTime(WebSession webSession) {
100101
return Mono.fromSupplier(() -> {
102+
Assert.isInstanceOf(InMemoryWebSession.class, webSession);
101103
InMemoryWebSession session = (InMemoryWebSession) webSession;
102-
Instant lastAccessTime = Instant.now(getClock());
103-
return new InMemoryWebSession(session, lastAccessTime);
104+
session.updateLastAccessTime();
105+
return session;
104106
});
105107
}
106108

107109

108-
// Private methods for InMemoryWebSession
109-
110-
private Mono<Void> changeSessionId(String oldId, WebSession session) {
111-
this.sessions.remove(oldId);
112-
this.sessions.put(session.getId(), session);
113-
return Mono.empty();
114-
}
115-
116-
private Mono<Void> storeSession(WebSession session) {
117-
this.sessions.put(session.getId(), session);
118-
return Mono.empty();
119-
}
120-
121-
122110
private class InMemoryWebSession implements WebSession {
123111

124112
private final AtomicReference<String> id;
@@ -127,12 +115,13 @@ private class InMemoryWebSession implements WebSession {
127115

128116
private final Instant creationTime;
129117

130-
private final Instant lastAccessTime;
118+
private volatile Instant lastAccessTime;
131119

132120
private volatile Duration maxIdleTime;
133121

134122
private volatile boolean started;
135123

124+
136125
InMemoryWebSession() {
137126
this.id = new AtomicReference<>(String.valueOf(idGenerator.generateId()));
138127
this.attributes = new ConcurrentHashMap<>();
@@ -141,14 +130,6 @@ private class InMemoryWebSession implements WebSession {
141130
this.maxIdleTime = Duration.ofMinutes(30);
142131
}
143132

144-
InMemoryWebSession(InMemoryWebSession existingSession, Instant lastAccessTime) {
145-
this.id = existingSession.id;
146-
this.attributes = existingSession.attributes;
147-
this.creationTime = existingSession.creationTime;
148-
this.lastAccessTime = lastAccessTime;
149-
this.maxIdleTime = existingSession.maxIdleTime;
150-
this.started = existingSession.isStarted(); // Use method (explicit or implicit start)
151-
}
152133

153134
@Override
154135
public String getId() {
@@ -192,22 +173,33 @@ public boolean isStarted() {
192173

193174
@Override
194175
public Mono<Void> changeSessionId() {
195-
String oldId = this.id.get();
176+
String currentId = this.id.get();
177+
if (InMemoryWebSessionStore.this.sessions.remove(currentId) == null) {
178+
return Mono.error(new IllegalStateException(
179+
"Failed to change session id: " + currentId +
180+
" because the Session is no longer present in the store."));
181+
}
196182
String newId = String.valueOf(idGenerator.generateId());
197183
this.id.set(newId);
198-
return InMemoryWebSessionStore.this.changeSessionId(oldId, this).doOnError(ex -> this.id.set(oldId));
184+
InMemoryWebSessionStore.this.sessions.put(this.getId(), this);
185+
return Mono.empty();
199186
}
200187

201188
@Override
202189
public Mono<Void> save() {
203-
return InMemoryWebSessionStore.this.storeSession(this);
190+
InMemoryWebSessionStore.this.sessions.put(this.getId(), this);
191+
return Mono.empty();
204192
}
205193

206194
@Override
207195
public boolean isExpired() {
208196
return (isStarted() && !this.maxIdleTime.isNegative() &&
209197
Instant.now(getClock()).minus(this.maxIdleTime).isAfter(this.lastAccessTime));
210198
}
199+
200+
private void updateLastAccessTime() {
201+
this.lastAccessTime = Instant.now(getClock());
202+
}
211203
}
212204

213205
}

spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ public interface WebSessionStore {
4141
/**
4242
* Return the WebSession for the given id.
4343
* <p><strong>Note:</strong> This method should perform an expiration check,
44-
* remove the session if it has expired and return empty.
44+
* and if it has expired remove the session and return empty. This method
45+
* should also update the lastAccessTime of retrieved sessions.
4546
* @param sessionId the session to load
4647
* @return the session, or an empty {@code Mono} .
4748
*/

spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.time.Clock;
1919
import java.time.Duration;
20+
import java.time.Instant;
2021

2122
import org.junit.Test;
2223

@@ -28,36 +29,14 @@
2829
import static org.junit.Assert.assertTrue;
2930

3031
/**
31-
* Unit tests.
32+
* Unit tests for {@link InMemoryWebSessionStore}.
3233
* @author Rob Winch
3334
*/
3435
public class InMemoryWebSessionStoreTests {
3536

3637
private InMemoryWebSessionStore store = new InMemoryWebSessionStore();
3738

3839

39-
@Test
40-
public void constructorWhenImplicitStartCopiedThenCopyIsStarted() {
41-
WebSession original = this.store.createWebSession().block();
42-
assertNotNull(original);
43-
original.getAttributes().put("foo", "bar");
44-
45-
WebSession copy = this.store.updateLastAccessTime(original).block();
46-
assertNotNull(copy);
47-
assertTrue(copy.isStarted());
48-
}
49-
50-
@Test
51-
public void constructorWhenExplicitStartCopiedThenCopyIsStarted() {
52-
WebSession original = this.store.createWebSession().block();
53-
assertNotNull(original);
54-
original.start();
55-
56-
WebSession copy = this.store.updateLastAccessTime(original).block();
57-
assertNotNull(copy);
58-
assertTrue(copy.isStarted());
59-
}
60-
6140
@Test
6241
public void startsSessionExplicitly() {
6342
WebSession session = this.store.createWebSession().block();
@@ -87,8 +66,27 @@ public void retrieveExpiredSession() throws Exception {
8766
assertNotNull(retrieved);
8867
assertSame(session, retrieved);
8968

69+
// Fast-forward 31 minutes
9070
this.store.setClock(Clock.offset(this.store.getClock(), Duration.ofMinutes(31)));
9171
WebSession retrievedAgain = this.store.retrieveSession(id).block();
9272
assertNull(retrievedAgain);
9373
}
74+
75+
@Test
76+
public void lastAccessTimeIsUpdatedOnRetrieve() throws Exception {
77+
WebSession session1 = this.store.createWebSession().block();
78+
assertNotNull(session1);
79+
String id = session1.getId();
80+
Instant time1 = session1.getLastAccessTime();
81+
session1.save();
82+
83+
// Fast-forward a few seconds
84+
this.store.setClock(Clock.offset(this.store.getClock(), Duration.ofSeconds(5)));
85+
86+
WebSession session2 = this.store.retrieveSession(id).block();
87+
assertNotNull(session2);
88+
assertSame(session1, session2);
89+
Instant time2 = session2.getLastAccessTime();
90+
assertTrue(time1.isBefore(time2));
91+
}
9492
}

0 commit comments

Comments
 (0)