Skip to content

Commit ec5969c

Browse files
committed
InMemoryWebSession cleans up expired sessions
Issue: SPR-15963
1 parent 15cc44e commit ec5969c

File tree

2 files changed

+96
-14
lines changed

2 files changed

+96
-14
lines changed

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

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
import java.time.Duration;
2121
import java.time.Instant;
2222
import java.time.ZoneId;
23+
import java.util.Iterator;
2324
import java.util.Map;
2425
import java.util.concurrent.ConcurrentHashMap;
26+
import java.util.concurrent.ConcurrentMap;
2527
import java.util.concurrent.atomic.AtomicReference;
28+
import java.util.concurrent.locks.ReentrantLock;
2629

2730
import reactor.core.publisher.Mono;
2831

@@ -40,12 +43,20 @@
4043
*/
4144
public class InMemoryWebSessionStore implements WebSessionStore {
4245

46+
/** Minimum period between expiration checks */
47+
private static final Duration EXPIRATION_CHECK_PERIOD = Duration.ofSeconds(60);
48+
49+
4350
private static final IdGenerator idGenerator = new JdkIdGenerator();
4451

4552

4653
private Clock clock = Clock.system(ZoneId.of("GMT"));
4754

48-
private final Map<String, InMemoryWebSession> sessions = new ConcurrentHashMap<>();
55+
private final ConcurrentMap<String, InMemoryWebSession> sessions = new ConcurrentHashMap<>();
56+
57+
private volatile Instant nextExpirationCheckTime = Instant.now(this.clock).plus(EXPIRATION_CHECK_PERIOD);
58+
59+
private final ReentrantLock expirationCheckLock = new ReentrantLock();
4960

5061

5162
/**
@@ -60,6 +71,8 @@ public class InMemoryWebSessionStore implements WebSessionStore {
6071
public void setClock(Clock clock) {
6172
Assert.notNull(clock, "Clock is required");
6273
this.clock = clock;
74+
// Force a check when clock changes..
75+
this.nextExpirationCheckTime = Instant.now(this.clock);
6376
}
6477

6578
/**
@@ -77,20 +90,46 @@ public Mono<WebSession> createWebSession() {
7790

7891
@Override
7992
public Mono<WebSession> retrieveSession(String id) {
93+
94+
Instant currentTime = Instant.now(this.clock);
95+
96+
if (!this.sessions.isEmpty() && !currentTime.isBefore(this.nextExpirationCheckTime)) {
97+
checkExpiredSessions(currentTime);
98+
}
99+
80100
InMemoryWebSession session = this.sessions.get(id);
81101
if (session == null) {
82102
return Mono.empty();
83103
}
84-
else if (session.isExpired()) {
104+
else if (session.isExpired(currentTime)) {
85105
this.sessions.remove(id);
86106
return Mono.empty();
87107
}
88108
else {
89-
session.updateLastAccessTime();
109+
session.updateLastAccessTime(currentTime);
90110
return Mono.just(session);
91111
}
92112
}
93113

114+
private void checkExpiredSessions(Instant currentTime) {
115+
if (this.expirationCheckLock.tryLock()) {
116+
try {
117+
Iterator<InMemoryWebSession> iterator = this.sessions.values().iterator();
118+
while (iterator.hasNext()) {
119+
InMemoryWebSession session = iterator.next();
120+
if (session.isExpired(currentTime)) {
121+
iterator.remove();
122+
session.invalidate();
123+
}
124+
}
125+
}
126+
finally {
127+
this.nextExpirationCheckTime = currentTime.plus(EXPIRATION_CHECK_PERIOD);
128+
this.expirationCheckLock.unlock();
129+
}
130+
}
131+
}
132+
94133
@Override
95134
public Mono<Void> removeSession(String id) {
96135
this.sessions.remove(id);
@@ -101,7 +140,7 @@ public Mono<WebSession> updateLastAccessTime(WebSession webSession) {
101140
return Mono.fromSupplier(() -> {
102141
Assert.isInstanceOf(InMemoryWebSession.class, webSession);
103142
InMemoryWebSession session = (InMemoryWebSession) webSession;
104-
session.updateLastAccessTime();
143+
session.updateLastAccessTime(Instant.now(getClock()));
105144
return session;
106145
});
107146
}
@@ -122,7 +161,7 @@ private class InMemoryWebSession implements WebSession {
122161
private final AtomicReference<State> state = new AtomicReference<>(State.NEW);
123162

124163

125-
InMemoryWebSession() {
164+
public InMemoryWebSession() {
126165
this.creationTime = Instant.now(getClock());
127166
this.lastAccessTime = this.creationTime;
128167
}
@@ -201,25 +240,28 @@ public Mono<Void> save() {
201240

202241
@Override
203242
public boolean isExpired() {
243+
return isExpired(Instant.now(getClock()));
244+
}
245+
246+
private boolean isExpired(Instant currentTime) {
204247
if (this.state.get().equals(State.EXPIRED)) {
205248
return true;
206249
}
207-
if (checkExpired()) {
250+
if (checkExpired(currentTime)) {
208251
this.state.set(State.EXPIRED);
209252
return true;
210253
}
211254
return false;
212255
}
213256

214-
private boolean checkExpired() {
257+
private boolean checkExpired(Instant currentTime) {
215258
return isStarted() && !this.maxIdleTime.isNegative() &&
216-
Instant.now(getClock()).minus(this.maxIdleTime).isAfter(this.lastAccessTime);
259+
currentTime.minus(this.maxIdleTime).isAfter(this.lastAccessTime);
217260
}
218261

219-
private void updateLastAccessTime() {
220-
this.lastAccessTime = Instant.now(getClock());
262+
private void updateLastAccessTime(Instant currentTime) {
263+
this.lastAccessTime = currentTime;
221264
}
222-
223265
}
224266

225267
private enum State { NEW, STARTED, EXPIRED }

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

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.junit.Test;
2323

24+
import org.springframework.util.Assert;
2425
import org.springframework.web.server.WebSession;
2526

2627
import static junit.framework.TestCase.assertSame;
@@ -59,7 +60,7 @@ public void retrieveExpiredSession() throws Exception {
5960
WebSession session = this.store.createWebSession().block();
6061
assertNotNull(session);
6162
session.getAttributes().put("foo", "bar");
62-
session.save();
63+
session.save().block();
6364

6465
String id = session.getId();
6566
WebSession retrieved = this.store.retrieveSession(id).block();
@@ -78,7 +79,7 @@ public void lastAccessTimeIsUpdatedOnRetrieve() throws Exception {
7879
assertNotNull(session1);
7980
String id = session1.getId();
8081
Instant time1 = session1.getLastAccessTime();
81-
session1.save();
82+
session1.save().block();
8283

8384
// Fast-forward a few seconds
8485
this.store.setClock(Clock.offset(this.store.getClock(), Duration.ofSeconds(5)));
@@ -91,7 +92,46 @@ public void lastAccessTimeIsUpdatedOnRetrieve() throws Exception {
9192
}
9293

9394
@Test
94-
public void invalidate() throws Exception {
95+
public void expirationChecks() throws Exception {
96+
// Create 3 sessions
97+
WebSession session1 = this.store.createWebSession().block();
98+
assertNotNull(session1);
99+
session1.start();
100+
session1.save().block();
101+
102+
WebSession session2 = this.store.createWebSession().block();
103+
assertNotNull(session2);
104+
session2.start();
105+
session2.save().block();
106+
107+
WebSession session3 = this.store.createWebSession().block();
108+
assertNotNull(session3);
109+
session3.start();
110+
session3.save().block();
111+
112+
// Fast-forward 31 minutes
113+
this.store.setClock(Clock.offset(this.store.getClock(), Duration.ofMinutes(31)));
114+
115+
// Create 2 more sessions
116+
WebSession session4 = this.store.createWebSession().block();
117+
assertNotNull(session4);
118+
session4.start();
119+
session4.save().block();
120+
121+
WebSession session5 = this.store.createWebSession().block();
122+
assertNotNull(session5);
123+
session5.start();
124+
session5.save().block();
125+
126+
// Retrieve, forcing cleanup of all expired..
127+
assertNull(this.store.retrieveSession(session1.getId()).block());
128+
assertNull(this.store.retrieveSession(session2.getId()).block());
129+
assertNull(this.store.retrieveSession(session3.getId()).block());
95130

131+
assertNotNull(this.store.retrieveSession(session4.getId()).block());
132+
assertNotNull(this.store.retrieveSession(session5.getId()).block());
96133
}
134+
135+
136+
97137
}

0 commit comments

Comments
 (0)