1
18
19 package io.undertow.server.session;
20
21 import io.undertow.UndertowLogger;
22 import io.undertow.UndertowMessages;
23 import io.undertow.server.HttpServerExchange;
24 import io.undertow.util.AttachmentKey;
25 import io.undertow.util.ConcurrentDirectDeque;
26 import io.undertow.util.WorkerUtils;
27
28 import java.math.BigDecimal;
29 import java.math.BigInteger;
30 import java.math.MathContext;
31 import java.security.AccessController;
32 import java.security.PrivilegedAction;
33 import java.util.HashSet;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.concurrent.ConcurrentMap;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.atomic.AtomicInteger;
40 import java.util.concurrent.atomic.AtomicLong;
41 import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
42
43 import org.xnio.XnioExecutor;
44 import org.xnio.XnioIoThread;
45 import org.xnio.XnioWorker;
46
47
53 public class InMemorySessionManager implements SessionManager, SessionManagerStatistics {
54
55 private final AttachmentKey<SessionImpl> NEW_SESSION = AttachmentKey.create(SessionImpl.class);
56
57 private final SessionIdGenerator sessionIdGenerator;
58
59 private final ConcurrentMap<String, SessionImpl> sessions;
60
61 private final SessionListeners sessionListeners = new SessionListeners();
62
63
66 private volatile int defaultSessionTimeout = 30 * 60;
67
68 private final int maxSize;
69
70 private final ConcurrentDirectDeque<String> evictionQueue;
71
72 private final String deploymentName;
73
74 private final AtomicLong createdSessionCount = new AtomicLong();
75 private final AtomicLong rejectedSessionCount = new AtomicLong();
76 private volatile long longestSessionLifetime = 0;
77 private volatile long expiredSessionCount = 0;
78 private volatile BigInteger totalSessionLifetime = BigInteger.ZERO;
79 private final AtomicInteger highestSessionCount = new AtomicInteger();
80
81 private final boolean statisticsEnabled;
82
83 private volatile long startTime;
84
85 private final boolean expireOldestUnusedSessionOnMax;
86
87
88 public InMemorySessionManager(String deploymentName, int maxSessions, boolean expireOldestUnusedSessionOnMax) {
89 this(new SecureRandomSessionIdGenerator(), deploymentName, maxSessions, expireOldestUnusedSessionOnMax);
90 }
91
92 public InMemorySessionManager(SessionIdGenerator sessionIdGenerator, String deploymentName, int maxSessions, boolean expireOldestUnusedSessionOnMax) {
93 this(sessionIdGenerator, deploymentName, maxSessions, expireOldestUnusedSessionOnMax, true);
94 }
95
96 public InMemorySessionManager(SessionIdGenerator sessionIdGenerator, String deploymentName, int maxSessions, boolean expireOldestUnusedSessionOnMax, boolean statisticsEnabled) {
97 this.sessionIdGenerator = sessionIdGenerator;
98 this.deploymentName = deploymentName;
99 this.statisticsEnabled = statisticsEnabled;
100 this.expireOldestUnusedSessionOnMax = expireOldestUnusedSessionOnMax;
101 this.sessions = new ConcurrentHashMap<>();
102 this.maxSize = maxSessions;
103 ConcurrentDirectDeque<String> evictionQueue = null;
104 if (maxSessions > 0 && expireOldestUnusedSessionOnMax) {
105 evictionQueue = ConcurrentDirectDeque.newInstance();
106 }
107 this.evictionQueue = evictionQueue;
108 }
109
110 public InMemorySessionManager(String deploymentName, int maxSessions) {
111 this(deploymentName, maxSessions, false);
112 }
113
114 public InMemorySessionManager(String id) {
115 this(id, -1);
116 }
117
118 @Override
119 public String getDeploymentName() {
120 return this.deploymentName;
121 }
122
123 @Override
124 public void start() {
125 createdSessionCount.set(0);
126 expiredSessionCount = 0;
127 rejectedSessionCount.set(0);
128 totalSessionLifetime = BigInteger.ZERO;
129 startTime = System.currentTimeMillis();
130 }
131
132 @Override
133 public void stop() {
134 for (Map.Entry<String, SessionImpl> session : sessions.entrySet()) {
135 session.getValue().destroy();
136 sessionListeners.sessionDestroyed(session.getValue(), null, SessionListener.SessionDestroyedReason.UNDEPLOY);
137 }
138 sessions.clear();
139 }
140
141 @Override
142 public Session createSession(final HttpServerExchange serverExchange, final SessionConfig config) {
143 if (maxSize > 0) {
144 if(expireOldestUnusedSessionOnMax) {
145 while (sessions.size() >= maxSize && !evictionQueue.isEmpty()) {
146
147 String key = evictionQueue.poll();
148 UndertowLogger.REQUEST_LOGGER.debugf("Removing session %s as max size has been hit", key);
149 SessionImpl toRemove = sessions.get(key);
150 if (toRemove != null) {
151 toRemove.invalidate(null, SessionListener.SessionDestroyedReason.TIMEOUT);
152 }
153 }
154 } else if(sessions.size() >= maxSize) {
155 if(statisticsEnabled) {
156 rejectedSessionCount.incrementAndGet();
157 }
158 throw UndertowMessages.MESSAGES.tooManySessions(maxSize);
159 }
160 }
161 if (config == null) {
162 throw UndertowMessages.MESSAGES.couldNotFindSessionCookieConfig();
163 }
164 String sessionID = config.findSessionId(serverExchange);
165 if (sessionID == null) {
166 int count = 0;
167 while (sessionID == null) {
168 sessionID = sessionIdGenerator.createSessionId();
169 if (sessions.containsKey(sessionID)) {
170 sessionID = null;
171 }
172 if (count++ == 100) {
173
174
175 throw UndertowMessages.MESSAGES.couldNotGenerateUniqueSessionId();
176 }
177 }
178 } else {
179 if (sessions.containsKey(sessionID)) {
180 throw UndertowMessages.MESSAGES.sessionWithIdAlreadyExists(sessionID);
181 }
182 }
183 Object evictionToken;
184 if (evictionQueue != null) {
185 evictionToken = evictionQueue.offerLastAndReturnToken(sessionID);
186 } else {
187 evictionToken = null;
188 }
189 final SessionImpl session = new SessionImpl(this, sessionID, config, serverExchange.getIoThread(), serverExchange.getConnection().getWorker(), evictionToken, defaultSessionTimeout);
190
191 UndertowLogger.SESSION_LOGGER.debugf("Created session with id %s for exchange %s", sessionID, serverExchange);
192 sessions.put(sessionID, session);
193 config.setSessionId(serverExchange, session.getId());
194 session.bumpTimeout();
195 sessionListeners.sessionCreated(session, serverExchange);
196 serverExchange.putAttachment(NEW_SESSION, session);
197
198 if(statisticsEnabled) {
199 createdSessionCount.incrementAndGet();
200 int highest;
201 int sessionSize;
202 do {
203 highest = highestSessionCount.get();
204 sessionSize = sessions.size();
205 if(sessionSize <= highest) {
206 break;
207 }
208 } while (!highestSessionCount.compareAndSet(highest, sessionSize));
209 }
210 return session;
211 }
212
213 @Override
214 public Session getSession(final HttpServerExchange serverExchange, final SessionConfig config) {
215 if (serverExchange != null) {
216 SessionImpl newSession = serverExchange.getAttachment(NEW_SESSION);
217 if(newSession != null) {
218 return newSession;
219 }
220 }
221 String sessionId = config.findSessionId(serverExchange);
222 InMemorySessionManager.SessionImpl session = (SessionImpl) getSession(sessionId);
223 if(session != null && serverExchange != null) {
224 session.requestStarted(serverExchange);
225 }
226 return session;
227 }
228
229 @Override
230 public Session getSession(String sessionId) {
231 if (sessionId == null) {
232 return null;
233 }
234 final SessionImpl sess = sessions.get(sessionId);
235 if (sess == null) {
236 return null;
237 } else {
238 return sess;
239 }
240 }
241
242
243 @Override
244 public synchronized void registerSessionListener(final SessionListener listener) {
245 UndertowLogger.SESSION_LOGGER.debugf("Registered session listener %s", listener);
246 sessionListeners.addSessionListener(listener);
247 }
248
249 @Override
250 public synchronized void removeSessionListener(final SessionListener listener) {
251 UndertowLogger.SESSION_LOGGER.debugf("Removed session listener %s", listener);
252 sessionListeners.removeSessionListener(listener);
253 }
254
255 @Override
256 public void setDefaultSessionTimeout(final int timeout) {
257 UndertowLogger.SESSION_LOGGER.debugf("Setting default session timeout to %s", timeout);
258 defaultSessionTimeout = timeout;
259 }
260
261 @Override
262 public Set<String> getTransientSessions() {
263 return getAllSessions();
264 }
265
266 @Override
267 public Set<String> getActiveSessions() {
268 return getAllSessions();
269 }
270
271 @Override
272 public Set<String> getAllSessions() {
273 return new HashSet<>(sessions.keySet());
274 }
275
276 @Override
277 public boolean equals(Object object) {
278 if (!(object instanceof SessionManager)) return false;
279 SessionManager manager = (SessionManager) object;
280 return this.deploymentName.equals(manager.getDeploymentName());
281 }
282
283 @Override
284 public int hashCode() {
285 return this.deploymentName.hashCode();
286 }
287
288 @Override
289 public String toString() {
290 return this.deploymentName;
291 }
292
293 @Override
294 public SessionManagerStatistics getStatistics() {
295 return this;
296 }
297
298 public long getCreatedSessionCount() {
299 return createdSessionCount.get();
300 }
301
302 @Override
303 public long getMaxActiveSessions() {
304 return maxSize;
305 }
306
307 @Override
308 public long getHighestSessionCount() {
309 return highestSessionCount.get();
310 }
311
312 @Override
313 public long getActiveSessionCount() {
314 return sessions.size();
315 }
316
317 @Override
318 public long getExpiredSessionCount() {
319 return expiredSessionCount;
320 }
321
322 @Override
323 public long getRejectedSessions() {
324 return rejectedSessionCount.get();
325
326 }
327
328 @Override
329 public long getMaxSessionAliveTime() {
330 return longestSessionLifetime;
331 }
332
333 @Override
334 public synchronized long getAverageSessionAliveTime() {
335
336 if(expiredSessionCount == 0) {
337 return 0;
338 }
339 return new BigDecimal(totalSessionLifetime).divide(BigDecimal.valueOf(expiredSessionCount), MathContext.DECIMAL128).longValue();
340 }
341
342 @Override
343 public long getStartTime() {
344 return startTime;
345 }
346
347
348
351 private static class SessionImpl implements Session {
352
353
354 final AttachmentKey<Long> FIRST_REQUEST_ACCESS = AttachmentKey.create(Long.class);
355 final InMemorySessionManager sessionManager;
356 final ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();
357 volatile long lastAccessed;
358 final long creationTime;
359 volatile int maxInactiveInterval;
360
361 static volatile AtomicReferenceFieldUpdater<SessionImpl, Object> evictionTokenUpdater;
362 static {
363
364
365
366 evictionTokenUpdater = AccessController.doPrivileged(new PrivilegedAction<AtomicReferenceFieldUpdater<SessionImpl, Object>>() {
367 @Override
368 public AtomicReferenceFieldUpdater<SessionImpl, Object> run() {
369 return createTokenUpdater();
370 }
371 });
372 }
373
374 private static AtomicReferenceFieldUpdater<SessionImpl, Object> createTokenUpdater() {
375 return AtomicReferenceFieldUpdater.newUpdater(SessionImpl.class, Object.class, "evictionToken");
376 }
377
378
379 private String sessionId;
380 private volatile Object evictionToken;
381 private final SessionConfig sessionCookieConfig;
382 private volatile long expireTime = -1;
383 private volatile boolean invalid = false;
384 private volatile boolean invalidationStarted = false;
385
386 final XnioIoThread executor;
387 final XnioWorker worker;
388
389 XnioExecutor.Key timerCancelKey;
390
391 Runnable cancelTask = new Runnable() {
392 @Override
393 public void run() {
394 worker.execute(new Runnable() {
395 @Override
396 public void run() {
397 long currentTime = System.currentTimeMillis();
398 if(currentTime >= expireTime) {
399 invalidate(null, SessionListener.SessionDestroyedReason.TIMEOUT);
400 } else {
401 timerCancelKey = WorkerUtils.executeAfter(executor, cancelTask, expireTime - currentTime, TimeUnit.MILLISECONDS);
402 }
403 }
404 });
405 }
406 };
407
408 private SessionImpl(final InMemorySessionManager sessionManager, final String sessionId, final SessionConfig sessionCookieConfig, final XnioIoThread executor, final XnioWorker worker, final Object evictionToken, final int maxInactiveInterval) {
409 this.sessionManager = sessionManager;
410 this.sessionId = sessionId;
411 this.sessionCookieConfig = sessionCookieConfig;
412 this.executor = executor;
413 this.worker = worker;
414 this.evictionToken = evictionToken;
415 creationTime = lastAccessed = System.currentTimeMillis();
416 this.maxInactiveInterval = maxInactiveInterval;
417 }
418
419 synchronized void bumpTimeout() {
420 if(invalidationStarted) {
421 return;
422 }
423
424 final int maxInactiveInterval = getMaxInactiveInterval();
425 if (maxInactiveInterval > 0) {
426 long newExpireTime = System.currentTimeMillis() + (maxInactiveInterval * 1000L);
427 if(timerCancelKey != null && (newExpireTime < expireTime)) {
428
429 if (!timerCancelKey.remove()) {
430 return;
431 }
432 timerCancelKey = null;
433 }
434 expireTime = newExpireTime;
435 UndertowLogger.SESSION_LOGGER.tracef("Bumping timeout for session %s to %s", sessionId, expireTime);
436 if(timerCancelKey == null) {
437
438
439
440 timerCancelKey = executor.executeAfter(cancelTask, (maxInactiveInterval * 1000L) + 1L, TimeUnit.MILLISECONDS);
441 }
442 } else {
443 expireTime = -1;
444 if(timerCancelKey != null) {
445 timerCancelKey.remove();
446 timerCancelKey = null;
447 }
448 }
449 if (evictionToken != null) {
450 Object token = evictionToken;
451 if (evictionTokenUpdater.compareAndSet(this, token, null)) {
452 sessionManager.evictionQueue.removeToken(token);
453 this.evictionToken = sessionManager.evictionQueue.offerLastAndReturnToken(sessionId);
454 }
455 }
456 }
457
458
459 @Override
460 public String getId() {
461 return sessionId;
462 }
463
464 void requestStarted(HttpServerExchange serverExchange) {
465 Long existing = serverExchange.getAttachment(FIRST_REQUEST_ACCESS);
466 if(existing == null) {
467 if (!invalid) {
468 serverExchange.putAttachment(FIRST_REQUEST_ACCESS, System.currentTimeMillis());
469 }
470 }
471 }
472
473 @Override
474 public void requestDone(final HttpServerExchange serverExchange) {
475 Long existing = serverExchange.getAttachment(FIRST_REQUEST_ACCESS);
476 if(existing != null) {
477 lastAccessed = existing;
478 }
479 bumpTimeout();
480 }
481
482 @Override
483 public long getCreationTime() {
484 if (invalid) {
485 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
486 }
487 return creationTime;
488 }
489
490 @Override
491 public long getLastAccessedTime() {
492 if (invalid) {
493 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
494 }
495 return lastAccessed;
496 }
497
498 @Override
499 public void setMaxInactiveInterval(final int interval) {
500 if (invalid) {
501 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
502 }
503 UndertowLogger.SESSION_LOGGER.debugf("Setting max inactive interval for %s to %s", sessionId, interval);
504 maxInactiveInterval = interval;
505 }
506
507 @Override
508 public int getMaxInactiveInterval() {
509 if (invalid) {
510 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
511 }
512 return maxInactiveInterval;
513 }
514
515 @Override
516 public Object getAttribute(final String name) {
517 if (invalid) {
518 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
519 }
520 return attributes.get(name);
521 }
522
523 @Override
524 public Set<String> getAttributeNames() {
525 if (invalid) {
526 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
527 }
528 return attributes.keySet();
529 }
530
531 @Override
532 public Object setAttribute(final String name, final Object value) {
533 if (value == null) {
534 return removeAttribute(name);
535 }
536 if (invalid) {
537 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
538 }
539 final Object existing = attributes.put(name, value);
540 if (existing == null) {
541 sessionManager.sessionListeners.attributeAdded(this, name, value);
542 } else {
543 sessionManager.sessionListeners.attributeUpdated(this, name, value, existing);
544 }
545 UndertowLogger.SESSION_LOGGER.tracef("Setting session attribute %s to %s for session %s", name, value, sessionId);
546 return existing;
547 }
548
549 @Override
550 public Object removeAttribute(final String name) {
551 if (invalid) {
552 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
553 }
554 final Object existing = attributes.remove(name);
555 sessionManager.sessionListeners.attributeRemoved(this, name, existing);
556 UndertowLogger.SESSION_LOGGER.tracef("Removing session attribute %s for session %s", name, sessionId);
557 return existing;
558 }
559
560 @Override
561 public void invalidate(final HttpServerExchange exchange) {
562 invalidate(exchange, SessionListener.SessionDestroyedReason.INVALIDATED);
563 if(exchange != null) {
564 exchange.removeAttachment(sessionManager.NEW_SESSION);
565 }
566 Object evictionToken = this.evictionToken;
567 if(evictionToken != null) {
568 sessionManager.evictionQueue.removeToken(evictionToken);
569 }
570 }
571
572 void invalidate(final HttpServerExchange exchange, SessionListener.SessionDestroyedReason reason) {
573 synchronized(SessionImpl.this) {
574 if (timerCancelKey != null) {
575 timerCancelKey.remove();
576 }
577 SessionImpl sess = sessionManager.sessions.remove(sessionId);
578 if (sess == null) {
579 if (reason == SessionListener.SessionDestroyedReason.INVALIDATED) {
580 throw UndertowMessages.MESSAGES.sessionAlreadyInvalidated();
581 }
582 return;
583 }
584 invalidationStarted = true;
585 }
586 UndertowLogger.SESSION_LOGGER.debugf("Invalidating session %s for exchange %s", sessionId, exchange);
587
588 sessionManager.sessionListeners.sessionDestroyed(this, exchange, reason);
589 invalid = true;
590
591 if(sessionManager.statisticsEnabled) {
592 long life = System.currentTimeMillis() - creationTime;
593 synchronized (sessionManager) {
594 sessionManager.expiredSessionCount++;
595 sessionManager.totalSessionLifetime = sessionManager.totalSessionLifetime.add(BigInteger.valueOf(life));
596 if(sessionManager.longestSessionLifetime < life) {
597 sessionManager.longestSessionLifetime = life;
598 }
599 }
600 }
601 if (exchange != null) {
602 sessionCookieConfig.clearSession(exchange, this.getId());
603 }
604 }
605
606 @Override
607 public SessionManager getSessionManager() {
608 return sessionManager;
609 }
610
611 @Override
612 public String changeSessionId(final HttpServerExchange exchange, final SessionConfig config) {
613 final String oldId = sessionId;
614 String newId = sessionManager.sessionIdGenerator.createSessionId();
615 this.sessionId = newId;
616 if(!invalid) {
617 sessionManager.sessions.put(newId, this);
618 config.setSessionId(exchange, this.getId());
619 }
620 sessionManager.sessions.remove(oldId);
621 sessionManager.sessionListeners.sessionIdChanged(this, oldId);
622 UndertowLogger.SESSION_LOGGER.debugf("Changing session id %s to %s", oldId, newId);
623
624 return newId;
625 }
626
627 private synchronized void destroy() {
628 if (timerCancelKey != null) {
629 timerCancelKey.remove();
630 }
631 cancelTask = null;
632 }
633
634 }
635 }
636