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