com.aspectran.core.component.session.AbstractSessionCache Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2019 The Aspectran Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.aspectran.core.component.session;
import com.aspectran.core.component.AbstractComponent;
import com.aspectran.core.util.StringUtils;
import com.aspectran.core.util.logging.Log;
import com.aspectran.core.util.logging.LogFactory;
import com.aspectran.core.util.thread.Locker.Lock;
import java.util.HashSet;
import java.util.Set;
/**
* A base implementation of the {@link SessionCache} interface for managing a set of
* Session objects pertaining to a context in memory.
*
* Created: 2017. 6. 24.
*/
public abstract class AbstractSessionCache extends AbstractComponent implements SessionCache {
private static final Log log = LogFactory.getLog(AbstractSessionCache.class);
/**
* The SessionHandler related to this SessionCache
*/
private final SessionHandler sessionHandler;
/**
* The authoritative source of session data
*/
private final SessionStore sessionStore;
/**
* Whether or not to support session clustering
*/
private final boolean clustered;
/**
* When, if ever, to evict sessions: never; only when the last request for
* them finishes; after inactivity time (expressed as secs)
*/
private int evictionIdleSecs = NEVER_EVICT;
/**
* If true, as soon as a new session is created, it will be persisted to
* the SessionStore
*/
private boolean saveOnCreate;
/**
* If true, a session that will be evicted from the cache because it has been
* inactive too long will be saved before being evicted.
*/
private boolean saveOnInactiveEviction;
/**
* If true, a Session whose data cannot be read will be
* deleted from the SessionStore.
*/
private boolean removeUnloadableSessions;
public AbstractSessionCache(SessionHandler sessionHandler, SessionStore sessionStore, boolean clustered) {
this.sessionHandler = sessionHandler;
this.sessionStore = sessionStore;
this.clustered = (clustered && sessionStore != null);
}
protected SessionHandler getSessionHandler() {
return sessionHandler;
}
protected SessionStore getSessionStore() {
return sessionStore;
}
@Override
public boolean isClustered() {
return clustered;
}
@Override
public int getEvictionIdleSecs() {
return evictionIdleSecs;
}
/**
* -1 means we never evict inactive sessions.
* 0 means we evict a session after the last request for it exits
* >0 is the number of seconds after which we evict inactive sessions from the cache
*/
@Override
public void setEvictionIdleSecs(int evictionTimeout) {
this.evictionIdleSecs = evictionTimeout;
}
@Override
public boolean isSaveOnCreate() {
return saveOnCreate;
}
@Override
public void setSaveOnCreate(boolean saveOnCreate) {
this.saveOnCreate = saveOnCreate;
}
/**
* Whether we should save a session that has been inactive before
* we boot it from the cache.
*
* @return true if an inactive session will be saved before being evicted
*/
@Override
public boolean isSaveOnInactiveEviction() {
return saveOnInactiveEviction;
}
@Override
public void setSaveOnInactiveEviction(boolean saveOnEvict) {
this.saveOnInactiveEviction = saveOnEvict;
}
/**
* @return true if sessions that can't be loaded are deleted from the store
*/
@Override
public boolean isRemoveUnloadableSessions() {
return removeUnloadableSessions;
}
/**
* If a session's data cannot be loaded from the store without error, remove
* it from the persistent store.
*
* @param removeUnloadableSessions whether to delete sessions that can not be loaded
*/
@Override
public void setRemoveUnloadableSessions(boolean removeUnloadableSessions) {
this.removeUnloadableSessions = removeUnloadableSessions;
}
@Override
public DefaultSession get(String id) throws Exception {
DefaultSession session;
Exception ex = null;
while (true) {
session = doGet(id);
if (sessionStore == null) {
break; // can't load any session data so just return null or the session object
}
if (session == null) {
if (log.isTraceEnabled()) {
log.trace("Session " + id + " not found locally, attempting to load");
}
// didn't get a session, try and create one and put in a placeholder for it
PlaceHolderSession phs = new PlaceHolderSession(id, sessionHandler);
Lock phsLock = phs.lock();
DefaultSession appeared = doPutIfAbsent(id, phs);
if (appeared == null) {
// My placeholder won, go ahead and load the full session data
try {
session = loadSession(id);
if (session == null) {
// session does not exist, remove the placeholder
doDelete(id);
phsLock.close();
break;
}
try (Lock ignored = session.lock()) {
// swap it in instead of the placeholder
boolean success = doReplace(id, phs, session);
if (success) {
// successfully swapped in the session
session.setResident(true);
} else {
// something has gone wrong, it should have been our placeholder
doDelete(id);
session = null;
log.warn("Replacement of placeholder for session " + id + " failed");
}
phsLock.close();
break;
}
} catch (Exception e) {
ex = e; // remember a problem happened loading the session
doDelete(id); // remove the placeholder
phsLock.close();
session = null;
break;
}
} else {
// my placeholder didn't win, check the session returned
phsLock.close();
try (Lock ignored = appeared.lock()) {
// is it a placeholder? or is a non-resident session?
// In both cases, chuck it away and start again
if (!appeared.isResident() || appeared instanceof PlaceHolderSession) {
continue;
}
// got the session
session = appeared;
break;
}
}
} else {
// check the session returned
try (Lock ignored = session.lock()) {
// is it a placeholder? or is it passivated? In both cases, chuck it away and start again
if (!session.isResident() || session instanceof PlaceHolderSession) {
continue;
}
if (isClustered() && session.getRequests() <= 0) {
DefaultSession stored = loadSession(id);
if (stored != null) {
// swap it in instead of the local session
boolean success = doReplace(id, session, stored);
if (success) {
// successfully swapped with the stored session
session = stored;
session.setResident(true);
} else {
// something has gone wrong, it must be removed from the cache
doDelete(id);
session.setResident(false);
session = null;
log.warn("Replacement with stored session " + id + " failed");
}
} else {
// is the session already destroyed? it must be removed from the cache
doDelete(id);
session.setResident(false);
session = null;
}
}
break;
}
}
}
if (ex != null) {
throw ex;
}
return session;
}
/**
* Load the info for the session from the session store.
*
* @param id the session id
* @return a Session object filled with data or null if the session doesn't exist
* @throws Exception if the session can not be loaded
*/
private DefaultSession loadSession(String id) throws Exception {
if (sessionStore == null) {
return null; // can't load it
}
try {
SessionData data = sessionStore.load(id);
if (data == null) { // session doesn't exist
return null;
}
return new DefaultSession(data, sessionHandler, false);
} catch (UnreadableSessionDataException e) {
// can't load the session, delete it
if (isRemoveUnloadableSessions()) {
sessionStore.delete(id);
}
throw e;
}
}
@Override
public DefaultSession add(String id, long time, long maxInactiveInterval) throws Exception {
if (id == null) {
throw new IllegalArgumentException("id must not be null");
}
if (log.isDebugEnabled()) {
log.debug("Creating new session id=" + id);
}
SessionData data = new SessionData(id, time, time, time, maxInactiveInterval);
DefaultSession session = new DefaultSession(data, sessionHandler, true);
if (doPutIfAbsent(id, session) == null) {
session.setResident(true); // its in the cache
if (sessionStore != null && (isSaveOnCreate() || isClustered())) {
sessionStore.save(id, data);
}
return session;
} else {
throw new IllegalStateException("Session " + id + " already in cache");
}
}
@Override
public void release(String id, DefaultSession session) throws Exception {
if (id == null || session == null) {
throw new IllegalArgumentException("Put key=" + id + " session=" +
(session == null ? "null" : session.getId()));
}
try (Lock ignored = session.lock()) {
if (!session.isValid()) {
return;
}
if (sessionStore == null) {
if (log.isTraceEnabled()) {
log.trace("Putting into SessionCache only id=" + id);
}
session.setResident(true);
doPutIfAbsent(id, session); // ensure it is in our map
return;
}
// don't do anything with the session until the last request for it has finished
if (session.getRequests() <= 0) {
// save the session
sessionStore.save(id, session.getSessionData());
// if we evict on session exit, boot it from the cache
if (getEvictionIdleSecs() == EVICT_ON_SESSION_EXIT) {
if (log.isDebugEnabled()) {
log.debug("Eviction on request exit id=" + id);
}
doDelete(session.getId());
session.setResident(false);
} else {
session.setResident(true);
doPutIfAbsent(id, session); // ensure it is in our map
}
} else {
if (log.isDebugEnabled()) {
log.debug("Session " + id + " request=" + session.getRequests());
}
session.setResident(true);
doPutIfAbsent(id, session); // ensure it is the map,
// but don't save it to the backing store until the last request exists
}
}
}
@Override
public boolean exists(String id) throws Exception {
if (isClustered()) {
DefaultSession ds = get(id);
if (ds != null) {
return ds.isValid();
} else {
return false;
}
} else {
// try the object store first
DefaultSession ds = doGet(id);
if (ds != null) {
return ds.isValid();
}
// not there, so find out if session data exists for it
return (sessionStore != null && sessionStore.exists(id));
}
}
@Override
public boolean contains(String id) throws Exception {
// just ask our object cache, not the store
return (doGet(id) != null);
}
@Override
public DefaultSession delete(String id) throws Exception {
// get the session, if its not in memory, this will load it
DefaultSession session = get(id);
// Always delete it from the backing data store
if (sessionStore != null) {
boolean deleted = sessionStore.delete(id);
if (log.isDebugEnabled()) {
log.debug("Session " + id + " deleted in session data store: " + deleted);
}
}
// delete it from the session object store
if (session != null) {
session.setResident(false);
}
return doDelete(id);
}
/**
* Get the session matching the key.
*
* @param id the session id
* @return the Session object matching the id
*/
public abstract DefaultSession doGet(String id);
/**
* Put the session into the map if it wasn't already there.
*
* @param id the identity of the session
* @param session the session object
* @return null if the session wasn't already in the map, or the existing entry otherwise
*/
public abstract DefaultSession doPutIfAbsent(String id, DefaultSession session);
/**
* Replace the mapping from id to oldValue with newValue.
*
* @param id the session id
* @param oldValue the old value
* @param newValue the new value
* @return true if replacement was done
*/
public abstract boolean doReplace(String id, DefaultSession oldValue, DefaultSession newValue);
/**
* Remove the session with this identity from the store.
*
* @param id the session id
* @return true if removed; false otherwise
*/
public abstract DefaultSession doDelete(String id);
@Override
public DefaultSession renewSessionId(String oldId, String newId) throws Exception {
if (!StringUtils.hasText(oldId)) {
throw new IllegalArgumentException("Old session id is null");
}
if (!StringUtils.hasText(oldId)) {
throw new IllegalArgumentException("New session id is null");
}
DefaultSession session = get(oldId);
renewSessionId(session, newId);
return session;
}
/**
* Swap the id on a session.
*
* @param session the session for which to do the swap
* @param newId the new id
* @throws Exception if there was a failure saving the change
*/
protected void renewSessionId(DefaultSession session, String newId) throws Exception {
if (session == null) {
return;
}
try (Lock ignored = session.lock()) {
String oldId = session.getId();
session.checkValidForWrite(); // can't change id on invalid session
session.getSessionData().setId(newId);
session.getSessionData().setLastSavedTime(0); // pretend that the session has never been saved before to get a full save
session.getSessionData().setDirty(true); // ensure we will try to write the session out
doPutIfAbsent(newId, session); // put the new id into our map
doDelete(oldId); // take old out of map
if (sessionStore != null) {
sessionStore.delete(oldId); //delete the session data with the old id
sessionStore.save(newId, session.getSessionData()); //save the session data with the new id
}
if (log.isDebugEnabled()) {
log.debug("Session id " + oldId + " swapped for new id " + newId);
}
}
}
@Override
public Set checkExpiration(Set candidates) {
if (!isInitialized()) {
return null;
}
if (sessionStore == null) {
return candidates;
}
if (log.isTraceEnabled()) {
log.trace("SessionStore checking expiration on " + candidates);
}
Set allCandidates = sessionStore.getExpired(candidates);
Set sessionsInUse = new HashSet<>();
if (allCandidates != null) {
for (String c : allCandidates) {
DefaultSession ds = doGet(c);
if (ds != null && ds.getRequests() > 0) {
// if the session is in my cache, check its not in use first
sessionsInUse.add(c);
}
}
try {
allCandidates.removeAll(sessionsInUse);
} catch (UnsupportedOperationException e) {
Set tmp = new HashSet<>(allCandidates);
tmp.removeAll(sessionsInUse);
allCandidates = tmp;
}
}
return allCandidates;
}
/**
* Check a session for being inactive and thus being able to be evicted,
* if eviction is enabled.
*
* @param session the session to check
*/
@Override
public void checkInactiveSession(DefaultSession session) {
if (session == null) {
return;
}
if (log.isTraceEnabled()) {
log.trace("Checking for idle " + session.getId());
}
try (Lock ignored = session.lock()) {
if (getEvictionIdleSecs() > 0 && session.isIdleLongerThan(getEvictionIdleSecs()) &&
session.isValid() && session.isResident() && session.getRequests() <= 0) {
// Be careful with saveOnInactiveEviction - you may be able to re-animate a session that was
// being managed on another node and has expired.
try {
if (log.isDebugEnabled()) {
log.debug("Evicting idle session " + session.getId());
}
// save before evicting
if (isSaveOnInactiveEviction() && sessionStore != null) {
sessionStore.save(session.getId(), session.getSessionData());
}
doDelete(session.getId()); // detach from this cache
session.setResident(false);
} catch (Exception e) {
log.warn("Passivation of idle session" + session.getId() + " failed", e);
}
}
}
}
/**
* PlaceHolder
*/
static class PlaceHolderSession extends DefaultSession {
PlaceHolderSession(String id, SessionHandler sessionHandler) {
super(new SessionData(id, 0, 0, 0, 0), sessionHandler, false);
}
}
}