//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.session;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
AbstractSessionDataStore
/**
* AbstractSessionDataStore
*/
@ManagedObject
public abstract class AbstractSessionDataStore extends ContainerLifeCycle implements SessionDataStore
{
private static final Logger LOG = LoggerFactory.getLogger(AbstractSessionDataStore.class);
protected SessionContext _context; //context associated with this session data store
protected int _gracePeriodSec = 60 * 60; //default of 1hr
protected long _lastExpiryCheckTime = 0; //last time in ms that getExpired was called
protected long _lastOrphanSweepTime = 0; //last time in ms that we deleted orphaned sessions
protected int _savePeriodSec = 0; //time in sec between saves
Small utility class to allow us to
return a result and an Exception
from invocation of Runnables.
Type parameters: - <V> – the type of the result.
/**
* Small utility class to allow us to
* return a result and an Exception
* from invocation of Runnables.
*
* @param <V> the type of the result.
*/
private class Result<V>
{
private V _result;
private Exception _exception;
public void setResult(V result)
{
_result = result;
}
public void setException(Exception exception)
{
_exception = exception;
}
private void throwIfException() throws Exception
{
if (_exception != null)
throw _exception;
}
public V getOrThrow() throws Exception
{
throwIfException();
return _result;
}
}
Check if a session for the given id exists.
Params: - id – the session id to check
Returns: true if the session exists in the persistent store, false otherwise
/**
* Check if a session for the given id exists.
*
* @param id the session id to check
* @return true if the session exists in the persistent store, false otherwise
*/
public abstract boolean doExists(String id) throws Exception;
Store the session data persistently.
Params: - id – identity of session to store
- data – info of the session
- lastSaveTime – time of previous save or 0 if never saved
Throws: - Exception – if unable to store data
/**
* Store the session data persistently.
*
* @param id identity of session to store
* @param data info of the session
* @param lastSaveTime time of previous save or 0 if never saved
* @throws Exception if unable to store data
*/
public abstract void doStore(String id, SessionData data, long lastSaveTime) throws Exception;
Load the session from persistent store.
Params: - id – the id of the session to load
Throws: - Exception – if unable to load the session
Returns: the re-inflated session
/**
* Load the session from persistent store.
*
* @param id the id of the session to load
* @return the re-inflated session
* @throws Exception if unable to load the session
*/
public abstract SessionData doLoad(String id) throws Exception;
Implemented by subclasses to resolve which sessions in this context
that are being managed by this node that should be expired.
Params: - candidates – the ids of sessions the SessionCache thinks has expired
- time – the time at which to check for expiry
Returns: the reconciled set of session ids that have been checked in the store
/**
* Implemented by subclasses to resolve which sessions in this context
* that are being managed by this node that should be expired.
*
* @param candidates the ids of sessions the SessionCache thinks has expired
* @param time the time at which to check for expiry
* @return the reconciled set of session ids that have been checked in the store
*/
public abstract Set<String> doCheckExpired(Set<String> candidates, long time);
Implemented by subclasses to find sessions for this context in the store
that expired at or before the time limit and thus not being actively
managed by any node. This method is only called periodically (the period
is configurable) to avoid putting too much load on the store.
Params: - before – the upper limit of expiry times to check. Sessions expired
at or before this timestamp will match.
Returns: the empty set if there are no sessions expired as at the time, or
otherwise a set of session ids.
/**
* Implemented by subclasses to find sessions for this context in the store
* that expired at or before the time limit and thus not being actively
* managed by any node. This method is only called periodically (the period
* is configurable) to avoid putting too much load on the store.
*
* @param before the upper limit of expiry times to check. Sessions expired
* at or before this timestamp will match.
*
* @return the empty set if there are no sessions expired as at the time, or
* otherwise a set of session ids.
*/
public abstract Set<String> doGetExpired(long before);
Implemented by subclasses to delete sessions for other contexts that
expired at or before the timeLimit. These are 'orphaned' sessions that
are no longer being actively managed by any node. These are explicitly
sessions that do NOT belong to this context (other mechanisms such as
doGetExpired take care of those). As they don't belong to this context,
they cannot be loaded by us.
This is called only periodically to avoid placing excessive load on the
store.
Params: - time – the upper limit of the expiry time to check in msec
/**
* Implemented by subclasses to delete sessions for other contexts that
* expired at or before the timeLimit. These are 'orphaned' sessions that
* are no longer being actively managed by any node. These are explicitly
* sessions that do NOT belong to this context (other mechanisms such as
* doGetExpired take care of those). As they don't belong to this context,
* they cannot be loaded by us.
*
* This is called only periodically to avoid placing excessive load on the
* store.
*
* @param time the upper limit of the expiry time to check in msec
*/
public abstract void doCleanOrphans(long time);
@Override
public void initialize(SessionContext context) throws Exception
{
if (isStarted())
throw new IllegalStateException("Context set after SessionDataStore started");
_context = context;
}
Remove all sessions for any context that expired at or before the given time.
Params: - timeLimit – the time before which the sessions must have expired.
/**
* Remove all sessions for any context that expired at or before the given time.
* @param timeLimit the time before which the sessions must have expired.
*/
public void cleanOrphans(long timeLimit)
{
if (!isStarted())
throw new IllegalStateException("Not started");
Runnable r = () ->
{
doCleanOrphans(timeLimit);
};
_context.run(r);
}
@Override
public SessionData load(String id) throws Exception
{
if (!isStarted())
throw new IllegalStateException("Not started");
final Result<SessionData> result = new Result<>();
Runnable r = () ->
{
try
{
result.setResult(doLoad(id));
}
catch (Exception e)
{
result.setException(e);
}
};
_context.run(r);
return result.getOrThrow();
}
@Override
public void store(String id, SessionData data) throws Exception
{
if (!isStarted())
throw new IllegalStateException("Not started");
if (data == null)
return;
long lastSave = data.getLastSaved();
long savePeriodMs = (_savePeriodSec <= 0 ? 0 : TimeUnit.SECONDS.toMillis(_savePeriodSec));
if (LOG.isDebugEnabled())
{
LOG.debug("Store: id={}, mdirty={}, dirty={}, lsave={}, period={}, elapsed={}", id, data.isMetaDataDirty(),
data.isDirty(), data.getLastSaved(), savePeriodMs, (System.currentTimeMillis() - lastSave));
}
//save session if attribute changed, never been saved or metadata changed (eg expiry time) and save interval exceeded
if (data.isDirty() || (lastSave <= 0) ||
(data.isMetaDataDirty() && ((System.currentTimeMillis() - lastSave) >= savePeriodMs)))
{
//set the last saved time to now
data.setLastSaved(System.currentTimeMillis());
final Result<Object> result = new Result<>();
Runnable r = () ->
{
try
{
//call the specific store method, passing in previous save time
doStore(id, data, lastSave);
data.clean(); //unset all dirty flags
}
catch (Exception e)
{
//reset last save time if save failed
data.setLastSaved(lastSave);
result.setException(e);
}
};
_context.run(r);
result.throwIfException();
}
}
@Override
public boolean exists(String id) throws Exception
{
Result<Boolean> result = new Result<>();
Runnable r = () ->
{
try
{
result.setResult(doExists(id));
}
catch (Exception e)
{
result.setException(e);
}
};
_context.run(r);
return result.getOrThrow();
}
@Override
public Set<String> getExpired(Set<String> candidates)
{
if (!isStarted())
throw new IllegalStateException("Not started");
long now = System.currentTimeMillis();
final Set<String> expired = new HashSet<>();
// 1. always verify the set of candidates we've been given
//by the sessioncache
Runnable r = () ->
{
Set<String> expiredCandidates = doCheckExpired(candidates, now);
if (expiredCandidates != null)
expired.addAll(expiredCandidates);
};
_context.run(r);
// 2. check the backing store to find other sessions
// in THIS context that expired long ago (ie cannot be actively managed
//by any node)
try
{
long t = 0;
// if we have never checked for old expired sessions, then only find
// those that are very old so we don't find sessions that other nodes
// that are also starting up find
if (_lastExpiryCheckTime <= 0)
t = now - TimeUnit.SECONDS.toMillis(_gracePeriodSec * 3);
else
{
// only do the check once every gracePeriod to avoid expensive searches,
// and find sessions that expired at least one gracePeriod ago
if (now > (_lastExpiryCheckTime + TimeUnit.SECONDS.toMillis(_gracePeriodSec)))
t = now - TimeUnit.SECONDS.toMillis(_gracePeriodSec);
}
if (t > 0)
{
if (LOG.isDebugEnabled())
LOG.debug("Searching for sessions expired before {} for context {}", t, _context.getCanonicalContextPath());
final long expiryTime = t;
r = () ->
{
Set<String> tmp = doGetExpired(expiryTime);
if (tmp != null)
expired.addAll(tmp);
};
_context.run(r);
}
}
finally
{
_lastExpiryCheckTime = now;
}
// 3. Periodically but infrequently comb the backing store to delete sessions for
// OTHER contexts that expired a very long time ago (ie not being actively
// managed by any node). As these sessions are not for our context, we
// can't load them, so they must just be forcibly deleted.
try
{
if (now > (_lastOrphanSweepTime + TimeUnit.SECONDS.toMillis(10 * _gracePeriodSec)))
{
if (LOG.isDebugEnabled())
LOG.debug("Cleaning orphans at {}, last sweep at {}", now, _lastOrphanSweepTime);
cleanOrphans(now - TimeUnit.SECONDS.toMillis(10 * _gracePeriodSec));
}
}
finally
{
_lastOrphanSweepTime = now;
}
return expired;
}
@Override
public SessionData newSessionData(String id, long created, long accessed, long lastAccessed, long maxInactiveMs)
{
return new SessionData(id, _context.getCanonicalContextPath(), _context.getVhost(), created, accessed, lastAccessed, maxInactiveMs);
}
protected void checkStarted() throws IllegalStateException
{
if (isStarted())
throw new IllegalStateException("Already started");
}
@Override
protected void doStart() throws Exception
{
if (_context == null)
throw new IllegalStateException("No SessionContext");
super.doStart();
}
@ManagedAttribute(value = "interval in secs to prevent too eager session scavenging", readonly = true)
public int getGracePeriodSec()
{
return _gracePeriodSec;
}
public void setGracePeriodSec(int sec)
{
_gracePeriodSec = sec;
}
Returns: the savePeriodSec
/**
* @return the savePeriodSec
*/
@ManagedAttribute(value = "min secs between saves", readonly = true)
public int getSavePeriodSec()
{
return _savePeriodSec;
}
The minimum time in seconds between save operations.
Saves normally occur every time the last request
exits as session. If nothing changes on the session
except for the access time and the persistence technology
is slow, this can cause delays.
By default the value is 0, which means we save
after the last request exists. A non zero value
means that we will skip doing the save if the
session isn't dirty if the elapsed time since
the session was last saved does not exceed this
value.
Params: - savePeriodSec – the savePeriodSec to set
/**
* The minimum time in seconds between save operations.
* Saves normally occur every time the last request
* exits as session. If nothing changes on the session
* except for the access time and the persistence technology
* is slow, this can cause delays.
* <p>
* By default the value is 0, which means we save
* after the last request exists. A non zero value
* means that we will skip doing the save if the
* session isn't dirty if the elapsed time since
* the session was last saved does not exceed this
* value.
*
* @param savePeriodSec the savePeriodSec to set
*/
public void setSavePeriodSec(int savePeriodSec)
{
_savePeriodSec = savePeriodSec;
}
@Override
public String toString()
{
return String.format("%s@%x[passivating=%b,graceSec=%d]", this.getClass().getName(), this.hashCode(), isPassivating(), getGracePeriodSec());
}
}