2020import java .time .Duration ;
2121import java .time .Instant ;
2222import java .time .ZoneId ;
23+ import java .time .temporal .ChronoUnit ;
24+ import java .util .Collections ;
2325import java .util .Iterator ;
2426import java .util .Map ;
2527import java .util .concurrent .ConcurrentHashMap ;
26- import java .util .concurrent .ConcurrentMap ;
2728import java .util .concurrent .atomic .AtomicReference ;
2829import java .util .concurrent .locks .ReentrantLock ;
2930
4344 */
4445public class InMemoryWebSessionStore implements WebSessionStore {
4546
46- /** Minimum period between expiration checks */
47- private static final Duration EXPIRATION_CHECK_PERIOD = Duration .ofSeconds (60 );
48-
4947 private static final IdGenerator idGenerator = new JdkIdGenerator ();
5048
5149
50+ private int maxSessions = 10000 ;
51+
5252 private Clock clock = Clock .system (ZoneId .of ("GMT" ));
5353
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 ();
5557
56- private volatile Instant nextExpirationCheckTime = Instant .now (this .clock ).plus (EXPIRATION_CHECK_PERIOD );
5758
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+ }
5970
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+ }
6078
6179 /**
6280 * Configure the {@link Clock} to use to set lastAccessTime on every created
@@ -70,8 +88,7 @@ public class InMemoryWebSessionStore implements WebSessionStore {
7088 public void setClock (Clock clock ) {
7189 Assert .notNull (clock , "Clock is required" );
7290 this .clock = clock ;
73- // Force a check when clock changes..
74- this .nextExpirationCheckTime = Instant .now (this .clock );
91+ removeExpiredSessions ();
7592 }
7693
7794 /**
@@ -81,67 +98,67 @@ public Clock getClock() {
8198 return this .clock ;
8299 }
83100
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+
84111
85112 @ Override
86113 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 ));
88117 }
89118
90119 @ Override
91120 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 );
97123 InMemoryWebSession session = this .sessions .get (id );
98124 if (session == null ) {
99125 return Mono .empty ();
100126 }
101- else if (session .isExpired (currentTime )) {
127+ else if (session .isExpired (now )) {
102128 this .sessions .remove (id );
103129 return Mono .empty ();
104130 }
105131 else {
106- session .updateLastAccessTime (currentTime );
132+ session .updateLastAccessTime (now );
107133 return Mono .just (session );
108134 }
109135 }
110136
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-
130137 @ Override
131138 public Mono <Void > removeSession (String id ) {
132139 this .sessions .remove (id );
133140 return Mono .empty ();
134141 }
135142
136- public Mono <WebSession > updateLastAccessTime (WebSession webSession ) {
143+ public Mono <WebSession > updateLastAccessTime (WebSession session ) {
137144 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 ());
141147 return session ;
142148 });
143149 }
144150
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+
145162
146163 private class InMemoryWebSession implements WebSession {
147164
@@ -157,8 +174,9 @@ private class InMemoryWebSession implements WebSession {
157174
158175 private final AtomicReference <State > state = new AtomicReference <>(State .NEW );
159176
160- public InMemoryWebSession () {
161- this .creationTime = Instant .now (getClock ());
177+
178+ public InMemoryWebSession (Instant creationTime ) {
179+ this .creationTime = creationTime ;
162180 this .lastAccessTime = this .creationTime ;
163181 }
164182
@@ -222,6 +240,12 @@ public Mono<Void> invalidate() {
222240
223241 @ Override
224242 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+ }
225249 if (!getAttributes ().isEmpty ()) {
226250 this .state .compareAndSet (State .NEW , State .STARTED );
227251 }
@@ -231,14 +255,14 @@ public Mono<Void> save() {
231255
232256 @ Override
233257 public boolean isExpired () {
234- return isExpired (Instant . now ( getClock () ));
258+ return isExpired (clock . instant ( ));
235259 }
236260
237- private boolean isExpired (Instant currentTime ) {
261+ private boolean isExpired (Instant now ) {
238262 if (this .state .get ().equals (State .EXPIRED )) {
239263 return true ;
240264 }
241- if (checkExpired (currentTime )) {
265+ if (checkExpired (now )) {
242266 this .state .set (State .EXPIRED );
243267 return true ;
244268 }
@@ -256,6 +280,47 @@ private void updateLastAccessTime(Instant currentTime) {
256280 }
257281
258282
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+
259324 private enum State { NEW , STARTED , EXPIRED }
260325
261326}
0 commit comments