All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.aspectran.core.component.session.AbstractSessionCache Maven / Gradle / Ivy

There is a newer version: 8.1.5
Show newest version
/*
 * Copyright (c) 2008-2024 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.utils.StringUtils;
import com.aspectran.utils.annotation.jsr305.Nullable;
import com.aspectran.utils.logging.Logger;
import com.aspectran.utils.logging.LoggerFactory;
import com.aspectran.utils.thread.AutoLock;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

/**
 * 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 Logger logger = LoggerFactory.getLogger(AbstractSessionCache.class); /** * The SessionHandler related to this SessionCache */ private final SessionHandler sessionHandler; /** * The authoritative source of session data */ private final SessionStore sessionStore; /** * Whether to support session clustering */ private final boolean clusterEnabled; /** * 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 clusterEnabled) { this.sessionHandler = sessionHandler; this.sessionStore = sessionStore; this.clusterEnabled = (clusterEnabled && sessionStore != null); } protected SessionHandler getSessionHandler() { return sessionHandler; } protected SessionStore getSessionStore() { return sessionStore; } protected SessionStatistics getStatistics() { return sessionHandler.getStatistics(); } @Override public boolean isClusterEnabled() { return clusterEnabled; } @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 { AtomicBoolean resident = new AtomicBoolean(true); AtomicReference thrown = new AtomicReference<>(); DefaultSession session; session = doComputeIfAbsent(id, k -> { if (logger.isTraceEnabled()) { logger.trace("Session " + id + " not found locally in " + this + ", attempting to load"); } try { DefaultSession stored = loadSession(id); if (stored != null) { try (AutoLock ignored = stored.lock()) { stored.setResident(true); // ensure freshly loaded session is resident } resident.set(false); } else { if (logger.isTraceEnabled()) { logger.trace("Session " + id + " not loaded by store"); } } return stored; } catch (Exception e) { thrown.set(e); return null; } }); if (thrown.get() != null) { throw thrown.get(); } if (session != null) { try (AutoLock ignored = session.lock()) { if (!session.isResident()) { // session isn't marked as resident in cache if (logger.isTraceEnabled()) { logger.debug("Non-resident session " + id + " in cache"); } return null; } if (isClusterEnabled() && resident.get() && 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 { // retry because it was updated by another thread return get(id); } } else { // is the session already destroyed? it must be removed from the cache doDelete(id); session.setResident(false); session = null; } } } } 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 */ @Nullable 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) { if (logger.isTraceEnabled()) { logger.trace("Session " + id + " loaded from session store " + sessionStore); } return new DefaultSession(data, sessionHandler, false); } else { // session doesn't exist return null; } } 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 (logger.isDebugEnabled()) { logger.debug("Create 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); // it's in the cache if (sessionStore != null && (isSaveOnCreate() || isClusterEnabled())) { 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 (AutoLock ignored = session.lock()) { if (!session.isValid()) { return; } if (sessionStore == null) { if (logger.isTraceEnabled()) { logger.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 (logger.isTraceEnabled()) { logger.trace("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 (logger.isDebugEnabled()) { logger.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 (isClusterEnabled()) { DefaultSession session = get(id); if (session != null) { return session.isValid(); } else { return false; } } else { // try to find it in the cache first DefaultSession session = doGet(id); if (session != null) { return session.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 it's 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 (logger.isTraceEnabled()) { logger.trace("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 */ protected 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 */ protected abstract DefaultSession doPutIfAbsent(String id, DefaultSession session); /** * Compute the mappingFunction to create a Session object if the session * with the given id isn't already in the map, otherwise return the existing Session. * This method is expected to have precisely the same behaviour as * {@link java.util.concurrent.ConcurrentHashMap#computeIfAbsent} * @param id the session id * @param mappingFunction the function to load the data for the session * @return an existing Session from the cache */ protected abstract DefaultSession doComputeIfAbsent(String id, Function mappingFunction); /** * 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 */ protected abstract boolean doReplace(String id, DefaultSession oldValue, DefaultSession newValue); /** * Remove the session with this identity from the store. * @param id the session id * @return the Session object if removed; null otherwise */ protected 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 (AutoLock ignored = session.lock()) { String oldId = session.getId(); session.checkValidForWrite(); // can't change id on invalid session session.getSessionData().setId(newId); session.getSessionData().setLastSaved(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 (logger.isDebugEnabled()) { logger.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 (logger.isTraceEnabled()) { logger.trace("SessionStore checking expiration on " + candidates); } Set allCandidates = sessionStore.getExpired(candidates); if (allCandidates != null) { Set sessionsInUse = new HashSet<>(); for (String id : allCandidates) { DefaultSession session = doGet(id); if (session != null && session.getRequests() > 0) { // if the session is in my cache, check it's not in use first sessionsInUse.add(id); } } if (!sessionsInUse.isEmpty()) { 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 * @return true if evicted session, false otherwise */ @Override public boolean checkInactiveSession(DefaultSession session) { if (session == null) { return false; } if (logger.isTraceEnabled()) { logger.trace("Checking for idle session id=" + session.getId()); } try (AutoLock 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. if (logger.isDebugEnabled()) { logger.debug("Evict idle session id=" + session.getId()); } // save before evicting if (sessionStore != null && (isClusterEnabled() || isSaveOnInactiveEviction())) { sessionStore.save(session.getId(), session.getSessionData()); } doDelete(session.getId()); // detach from this cache session.setResident(false); return true; } } catch (Exception e) { logger.warn("Passivation of idle session " + session.getId() + " failed", e); } return false; } @Override public void cleanOrphans(long time) { if (sessionStore != null) { sessionStore.cleanOrphans(time); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy