/**
* De-serialize the attribute map of a session.
*
* When the session was serialized, we will have recorded which classloader should be used to
* recover the attribute value. The classloader could be the container classloader, or the
* webapp classloader.
*
* @param data the SessionData for which to deserialize the attribute map
* @param in the serialized stream
*/
public static void deserializeAttributes(SessionData data, java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
Object o = in.readObject();
if (o instanceof Integer)
{
//new serialization was used
if (!(ClassLoadingObjectInputStream.class.isAssignableFrom(in.getClass())))
throw new IOException("Not ClassLoadingObjectInputStream");
data._attributes = new ConcurrentHashMap<>();
int entries = ((Integer)o).intValue();
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
ClassLoader serverLoader = SessionData.class.getClassLoader();
for (int i = 0; i < entries; i++)
{
String name = in.readUTF(); //attribute name
boolean isServerClassLoader = in.readBoolean(); //use server or webapp classloader to load
if (LOG.isDebugEnabled())
LOG.debug("Deserialize {} isServerLoader={} serverLoader={} tccl={}", name, isServerClassLoader, serverLoader, contextLoader);
Object value = ((ClassLoadingObjectInputStream)in).readObject(isServerClassLoader ? serverLoader : contextLoader);
data._attributes.put(name, value);
}
}
else
{
LOG.info("Legacy serialization detected for {}", data.getId());
//legacy serialization was used, we have just deserialized the
//entire attribute map
data._attributes = new ConcurrentHashMap<>();
data.putAllAttributes((Map<String, Object>)o);
}
}
Below is the log output.
2020-01-15 10:26:44.599:DBUG:ccjsr.RedisSessionDataStore:Session-HouseKeeper-5f8edcc5-1: Checking expiry for candidates
2020-01-15 10:26:44.599:DBUG:ccjsr.RedisSessionDataStore:Session-HouseKeeper-5f8edcc5-1: Checking expiry for NULL candidates
2020-01-15 10:26:44.616:DBUG:ccjsr.RedisSessionDataStore:Session-HouseKeeper-5f8edcc5-1: Loading session node012c4httt216k31ewmcqw42bdql1 from Redis
java.io.IOException: Not ClassLoadingObjectInputStream
at org.eclipse.jetty.server.session.SessionData.deserializeAttributes(SessionData.java:138)
at org.eclipse.jetty.server.session.SessionData.readObject(SessionData.java:490)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1170)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2178)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.cloudbees.jetty.session.redis.RedisSessionDataStore.doLoad(RedisSessionDataStore.java:55)
at org.eclipse.jetty.server.session.AbstractSessionDataStore.lambda$load$0(AbstractSessionDataStore.java:93)
at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1382)
at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1401)
at org.eclipse.jetty.server.session.SessionContext.run(SessionContext.java:92)
at org.eclipse.jetty.server.session.AbstractSessionDataStore.load(AbstractSessionDataStore.java:101)
at com.cloudbees.jetty.session.redis.RedisSessionDataStore.doGetExpired(RedisSessionDataStore.java:116)
at org.eclipse.jetty.server.session.AbstractSessionDataStore.getExpired(AbstractSessionDataStore.java:168)
at org.eclipse.jetty.server.session.AbstractSessionCache.checkExpiration(AbstractSessionCache.java:677)
at org.eclipse.jetty.server.session.SessionHandler.scavenge(SessionHandler.java:1256)
at org.eclipse.jetty.server.session.HouseKeeper.scavenge(HouseKeeper.java:256)
at org.eclipse.jetty.server.session.HouseKeeper$Runner.run(HouseKeeper.java:61)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
2020-01-15 10:26:44.621:WARN:ccjsr.RedisSessionDataStore:Session-HouseKeeper-5f8edcc5-1: Error checking if candidate node012c4httt216k31ewmcqw42bdql1 is expired java.io.IOException: Not ClassLoadingObjectInputStream
2020-01-15 10:36:43.329:DBUG:oejs.session:Session-Scheduler-68267da0-1: Timer expired for session node012c4httt216k31ewmcqw42bdql1
2020-01-15 10:36:43.330:DBUG:oejs.session:Session-Scheduler-68267da0-1: Inspecting session node012c4httt216k31ewmcqw42bdql1, valid=true
2020-01-15 10:36:43.330:DBUG:oejs.session:Session-Scheduler-68267da0-1: Testing expiry on session node012c4httt216k31ewmcqw42bdql1: expires at 1579106203302 now 1579106203330 maxIdle 900000
2020-01-15 10:36:43.331:DBUG:oejs.session:Session-Scheduler-68267da0-1: Session node012c4httt216k31ewmcqw42bdql1 is candidate for expiry
2020-01-15 10:36:43.331:DBUG:oejs.session:Session-Scheduler-68267da0-1: Testing expiry on session node012c4httt216k31ewmcqw42bdql1: expires at 1579106203302 now 1579106203330 maxIdle 900000
2020-01-15 10:36:44.640:DBUG:oejs.session:Session-HouseKeeper-5f8edcc5-1: node0 scavenging sessions
2020-01-15 10:36:44.640:DBUG:oejs.session:Session-HouseKeeper-5f8edcc5-1: org.eclipse.jetty.server.session.SessionHandler1747352992==dftMaxIdleSec=1800 scavenging sessions
2020-01-15 10:36:44.640:DBUG:oejs.session:Session-HouseKeeper-5f8edcc5-1: org.eclipse.jetty.server.session.SessionHandler1747352992==dftMaxIdleSec=1800 scavenging session ids [node012c4httt216k31ewmcqw42bdql1]
2020-01-15 10:36:44.640:DBUG:oejs.session:Session-HouseKeeper-5f8edcc5-1: org.eclipse.jetty.server.session.DefaultSessionCache@369f73a2[evict=-1,removeUnloadable=false,saveOnCreate=false,saveOnInactiveEvict=false] checking expiration on [node012c4httt216k31ewmcqw42bdql1]
Googling seems to indicate a problem with class loaders. Do I need to implement custom serialization?
Below is my RedisSessionDataStore doLoad(...), doStore(...) and doGetExpired(...) methods.
@Override
public SessionData doLoad(String id) throws Exception {
LOGGER.debug("Loading session {} from Redis", id);
final byte[] session = jedis.get(getCacheKey(id).getBytes());
if (session != null) {
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(session);
ObjectInputStream objectInputStream = new ObjectInputStreamWithLoader(byteArrayInputStream, _context.getContext().getClassLoader())) {
SessionData sd = (SessionData) objectInputStream.readObject();
return sd;
}
}
return null;
}
@Override
public void doStore(String id, SessionData data, long lastSaveTime) throws Exception {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
objectOutputStream.writeObject(data);
jedis.set(getCacheKey(id).getBytes(Charset.defaultCharset()), byteArrayOutputStream.toByteArray());
LOGGER.debug("Session {} saved to Redis, expires {} ", id, data.getExpiry());
}
}
@Override
public Set<String> doGetExpired(Set<String> candidates) {
LOGGER.debug("Checking expiry for candidates");
long now = System.currentTimeMillis();
if (candidates == null || candidates.isEmpty()) {
LOGGER.debug("Checking expiry for NULL candidates");
String keyPrefix = sessionKeyPrefix();
Set<String> sessionKeys = jedis.keys(keyPrefix + "*");
Set<String> expired = new HashSet<>();
if (null != sessionKeys) {
Iterator<String> iterator = sessionKeys.iterator();
while (iterator.hasNext()) {
String k = getKey(iterator.next());
try {
SessionData sd = load(k);
if (null == sd) {
continue;
}
if ((sd.getExpiry() > 0) && sd.getExpiry() <= now) {
expired.add(k);
LOGGER.debug("Session {} managed by {} is expired", k, _context.getWorkerName());
}
}
catch (Exception e) {
e.printStackTrace();
LOGGER.warn("Error checking if candidate {} is expired", k, e);
}
}
}
return expired;
}
Set<String> expired = new HashSet<>();
for (String candidate : candidates) {
LOGGER.debug("Checking expiry for candidate {}", candidate);
try {
SessionData sd = load(candidate);
//if the session no longer exists
if (sd == null) {
expired.add(candidate);
LOGGER.debug("Session {} does not exist in Redis", candidate);
} else {
if (_context.getWorkerName().equals(sd.getLastNode())) {
//we are its manager, add it to the expired set if it is expired now
if ((sd.getExpiry() > 0) && sd.getExpiry() <= now) {
expired.add(candidate);
LOGGER.debug("Session {} managed by {} is expired", candidate, _context.getWorkerName());
}
} else {
//if we are not the session's manager, only expire it iff:
// this is our first expiryCheck and the session expired a long time ago
//or
//the session expired at least one graceperiod ago
if (_lastExpiryCheckTime <= 0) {
if ((sd.getExpiry() > 0) && sd.getExpiry() < (now - (1000L * (3 * _gracePeriodSec))))
expired.add(candidate);
} else {
if ((sd.getExpiry() > 0) && sd.getExpiry() < (now - (1000L * _gracePeriodSec)))
expired.add(candidate);
}
}
}
} catch (Exception e) {
e.printStackTrace();
LOGGER.warn("Error checking if candidate {} is expired", candidate, e);
}
}
return expired;
}