20
20
import java .time .Duration ;
21
21
import java .time .Instant ;
22
22
import java .time .ZoneId ;
23
+ import java .time .temporal .ChronoUnit ;
24
+ import java .util .Collections ;
23
25
import java .util .Iterator ;
24
26
import java .util .Map ;
25
27
import java .util .concurrent .ConcurrentHashMap ;
26
- import java .util .concurrent .ConcurrentMap ;
27
28
import java .util .concurrent .atomic .AtomicReference ;
28
29
import java .util .concurrent .locks .ReentrantLock ;
29
30
43
44
*/
44
45
public class InMemoryWebSessionStore implements WebSessionStore {
45
46
46
- /** Minimum period between expiration checks */
47
- private static final Duration EXPIRATION_CHECK_PERIOD = Duration .ofSeconds (60 );
48
-
49
47
private static final IdGenerator idGenerator = new JdkIdGenerator ();
50
48
51
49
50
+ private int maxSessions = 10000 ;
51
+
52
52
private Clock clock = Clock .system (ZoneId .of ("GMT" ));
53
53
54
- private final ConcurrentMap <String , InMemoryWebSession > sessions = new ConcurrentHashMap <>();
54
+ private final Map <String , InMemoryWebSession > sessions = new ConcurrentHashMap <>();
55
+
56
+ private final ExpiredSessionChecker expiredSessionChecker = new ExpiredSessionChecker ();
55
57
56
- private volatile Instant nextExpirationCheckTime = Instant .now (this .clock ).plus (EXPIRATION_CHECK_PERIOD );
57
58
58
- private final ReentrantLock expirationCheckLock = new ReentrantLock ();
59
+ /**
60
+ * Set the maximum number of sessions that can be stored. Once the limit is
61
+ * reached, any attempt to store an additional session will result in an
62
+ * {@link IllegalStateException}.
63
+ * <p>By default set to 10000.
64
+ * @param maxSessions the maximum number of sessions
65
+ * @since 5.1
66
+ */
67
+ public void setMaxSessions (int maxSessions ) {
68
+ this .maxSessions = maxSessions ;
69
+ }
59
70
71
+ /**
72
+ * Return the maximum number of sessions that can be stored.
73
+ * @since 5.1
74
+ */
75
+ public int getMaxSessions () {
76
+ return this .maxSessions ;
77
+ }
60
78
61
79
/**
62
80
* Configure the {@link Clock} to use to set lastAccessTime on every created
@@ -70,8 +88,7 @@ public class InMemoryWebSessionStore implements WebSessionStore {
70
88
public void setClock (Clock clock ) {
71
89
Assert .notNull (clock , "Clock is required" );
72
90
this .clock = clock ;
73
- // Force a check when clock changes..
74
- this .nextExpirationCheckTime = Instant .now (this .clock );
91
+ removeExpiredSessions ();
75
92
}
76
93
77
94
/**
@@ -81,67 +98,67 @@ public Clock getClock() {
81
98
return this .clock ;
82
99
}
83
100
101
+ /**
102
+ * Return the map of sessions with an {@link Collections#unmodifiableMap
103
+ * unmodifiable} wrapper. This could be used for management purposes, to
104
+ * list active sessions, invalidate expired ones, etc.
105
+ * @since 5.1
106
+ */
107
+ public Map <String , InMemoryWebSession > getSessions () {
108
+ return Collections .unmodifiableMap (this .sessions );
109
+ }
110
+
84
111
85
112
@ Override
86
113
public Mono <WebSession > createWebSession () {
87
- return Mono .fromSupplier (InMemoryWebSession ::new );
114
+ Instant now = this .clock .instant ();
115
+ this .expiredSessionChecker .checkIfNecessary (now );
116
+ return Mono .fromSupplier (() -> new InMemoryWebSession (now ));
88
117
}
89
118
90
119
@ Override
91
120
public Mono <WebSession > retrieveSession (String id ) {
92
- Instant currentTime = Instant .now (this .clock );
93
- if (!this .sessions .isEmpty () && !currentTime .isBefore (this .nextExpirationCheckTime )) {
94
- checkExpiredSessions (currentTime );
95
- }
96
-
121
+ Instant now = this .clock .instant ();
122
+ this .expiredSessionChecker .checkIfNecessary (now );
97
123
InMemoryWebSession session = this .sessions .get (id );
98
124
if (session == null ) {
99
125
return Mono .empty ();
100
126
}
101
- else if (session .isExpired (currentTime )) {
127
+ else if (session .isExpired (now )) {
102
128
this .sessions .remove (id );
103
129
return Mono .empty ();
104
130
}
105
131
else {
106
- session .updateLastAccessTime (currentTime );
132
+ session .updateLastAccessTime (now );
107
133
return Mono .just (session );
108
134
}
109
135
}
110
136
111
- private void checkExpiredSessions (Instant currentTime ) {
112
- if (this .expirationCheckLock .tryLock ()) {
113
- try {
114
- Iterator <InMemoryWebSession > iterator = this .sessions .values ().iterator ();
115
- while (iterator .hasNext ()) {
116
- InMemoryWebSession session = iterator .next ();
117
- if (session .isExpired (currentTime )) {
118
- iterator .remove ();
119
- session .invalidate ();
120
- }
121
- }
122
- }
123
- finally {
124
- this .nextExpirationCheckTime = currentTime .plus (EXPIRATION_CHECK_PERIOD );
125
- this .expirationCheckLock .unlock ();
126
- }
127
- }
128
- }
129
-
130
137
@ Override
131
138
public Mono <Void > removeSession (String id ) {
132
139
this .sessions .remove (id );
133
140
return Mono .empty ();
134
141
}
135
142
136
- public Mono <WebSession > updateLastAccessTime (WebSession webSession ) {
143
+ public Mono <WebSession > updateLastAccessTime (WebSession session ) {
137
144
return Mono .fromSupplier (() -> {
138
- Assert .isInstanceOf (InMemoryWebSession .class , webSession );
139
- InMemoryWebSession session = (InMemoryWebSession ) webSession ;
140
- session .updateLastAccessTime (Instant .now (getClock ()));
145
+ Assert .isInstanceOf (InMemoryWebSession .class , session );
146
+ ((InMemoryWebSession ) session ).updateLastAccessTime (this .clock .instant ());
141
147
return session ;
142
148
});
143
149
}
144
150
151
+ /**
152
+ * Check for expired sessions and remove them. Typically such checks are
153
+ * kicked off lazily during calls to {@link #createWebSession() create} or
154
+ * {@link #retrieveSession retrieve}, no less than 60 seconds apart.
155
+ * This method can be called to force a check at a specific time.
156
+ * @since 5.1
157
+ */
158
+ public void removeExpiredSessions () {
159
+ this .expiredSessionChecker .removeExpiredSessions (this .clock .instant ());
160
+ }
161
+
145
162
146
163
private class InMemoryWebSession implements WebSession {
147
164
@@ -157,8 +174,9 @@ private class InMemoryWebSession implements WebSession {
157
174
158
175
private final AtomicReference <State > state = new AtomicReference <>(State .NEW );
159
176
160
- public InMemoryWebSession () {
161
- this .creationTime = Instant .now (getClock ());
177
+
178
+ public InMemoryWebSession (Instant creationTime ) {
179
+ this .creationTime = creationTime ;
162
180
this .lastAccessTime = this .creationTime ;
163
181
}
164
182
@@ -222,6 +240,12 @@ public Mono<Void> invalidate() {
222
240
223
241
@ Override
224
242
public Mono <Void > save () {
243
+ if (sessions .size () >= maxSessions ) {
244
+ expiredSessionChecker .removeExpiredSessions (clock .instant ());
245
+ if (sessions .size () >= maxSessions ) {
246
+ return Mono .error (new IllegalStateException ("Max sessions limit reached: " + sessions .size ()));
247
+ }
248
+ }
225
249
if (!getAttributes ().isEmpty ()) {
226
250
this .state .compareAndSet (State .NEW , State .STARTED );
227
251
}
@@ -231,14 +255,14 @@ public Mono<Void> save() {
231
255
232
256
@ Override
233
257
public boolean isExpired () {
234
- return isExpired (Instant . now ( getClock () ));
258
+ return isExpired (clock . instant ( ));
235
259
}
236
260
237
- private boolean isExpired (Instant currentTime ) {
261
+ private boolean isExpired (Instant now ) {
238
262
if (this .state .get ().equals (State .EXPIRED )) {
239
263
return true ;
240
264
}
241
- if (checkExpired (currentTime )) {
265
+ if (checkExpired (now )) {
242
266
this .state .set (State .EXPIRED );
243
267
return true ;
244
268
}
@@ -256,6 +280,47 @@ private void updateLastAccessTime(Instant currentTime) {
256
280
}
257
281
258
282
283
+ private class ExpiredSessionChecker {
284
+
285
+ /** Max time between expiration checks. */
286
+ private static final int CHECK_PERIOD = 60 * 1000 ;
287
+
288
+
289
+ private final ReentrantLock lock = new ReentrantLock ();
290
+
291
+ private Instant checkTime = clock .instant ().plus (CHECK_PERIOD , ChronoUnit .MILLIS );
292
+
293
+
294
+ public void checkIfNecessary (Instant now ) {
295
+ if (this .checkTime .isBefore (now )) {
296
+ removeExpiredSessions (now );
297
+ }
298
+ }
299
+
300
+ public void removeExpiredSessions (Instant now ) {
301
+ if (sessions .isEmpty ()) {
302
+ return ;
303
+ }
304
+ if (this .lock .tryLock ()) {
305
+ try {
306
+ Iterator <InMemoryWebSession > iterator = sessions .values ().iterator ();
307
+ while (iterator .hasNext ()) {
308
+ InMemoryWebSession session = iterator .next ();
309
+ if (session .isExpired (now )) {
310
+ iterator .remove ();
311
+ session .invalidate ();
312
+ }
313
+ }
314
+ }
315
+ finally {
316
+ this .checkTime = now .plus (CHECK_PERIOD , ChronoUnit .MILLIS );
317
+ this .lock .unlock ();
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+
259
324
private enum State { NEW , STARTED , EXPIRED }
260
325
261
326
}
0 commit comments