[Java][JVM][Memory leak][Profiling][Spring] Spring Session JDBC + Apereo CAS = Heap memory leak, and it is a documented feature
Before you read this article
Make sure you have read my previous article about a Spring + CAS memory leak.
How a CAS client removes a HTTP Session from its storage
A little reminder for you, the CAS client removes a HTTP Session using an implementation below:
public final class SingleSignOutHttpSessionListener implements HttpSessionListener {
...
@Override
public void sessionDestroyed(final HttpSessionEvent event) {
if (sessionMappingStorage == null) {
sessionMappingStorage = getSessionMappingStorage();
}
final HttpSession session = event.getSession();
sessionMappingStorage.removeBySessionById(session.getId());
}
...
}
It is also mentioned in the Javadoc of that class.
Mind that HttpSessionListener
class is a part of HTTP server, not a Spring Framework. If you register such a listener then during the session
invalidation Embedded Tomcat at Spring is going to emit an event of type javax.servlet.http.HttpSessionEvent
.
Such an event is processed by all the listeners of type HttpSessionListener
.
Spring documentation
As I wrote in the title of that article, all the behavior described below were documented. Let’s look at the Spring Session documentation:
Spring Session supports
HttpSessionListener
by translatingSessionDestroyedEvent
andSessionCreatedEvent
intoHttpSessionEvent
by declaringSessionEventHttpSessionListenerAdapter
. To use this support, you need to:
- Ensure your
SessionRepository
implementation supports and is configured to fireSessionDestroyedEvent
andSessionCreatedEvent
The SessionRepository
used in JDBC implementation of Spring Session is defined in JdbcIndexedSessionRepository
. Let’s look at the
Javadoc:
A
SessionRepository
implementation that uses Spring’sJdbcOperations
to store sessions in a relational database. This implementation does not support publishing of session events.
Why is there a leak?
Long story short:
- The CAS client needs
HttpSessionEvent
published to remove sessions from its storage - The Spring Session needs
SessionRepository
implementation to emit SpringSessionDestroyedEvent
event to publish a properHttpSessionEvent
- The JDBC implementation doesn’t emit
SessionDestroyedEvent
And yes, it is all documented, so RTFM :)
What can we do about it?
The most proper way of dealing with this memory leak is:
- Get rid of CAS - in most companies it is not an easy task
- Get rid of Spring Session - doable
- Change Spring Session implementation from JDBC to Hazelcast or Redis
- Hack the JDBC implementation
How can we hack it?
I don’t see any good and easy way of hacking the implementation. We need to be prepared for two actions:
- Manual logout - this is an easy part, we can register
LogoutFilter
that will manually invokeSingleSignOutHttpSessionListener
, ugly, but it works - Logout after a timeout - a hard part
The implementation of removing expired session is done by simple JDBC delete:
public void cleanUpExpiredSessions() {
Integer deletedCount = this.transactionOperations
.execute((status) -> JdbcIndexedSessionRepository.this.jdbcOperations.update(
JdbcIndexedSessionRepository.this.
deleteSessionsByExpiryTimeQuery, System.currentTimeMillis()));
if (logger.isDebugEnabled()) {
logger.debug("Cleaned up " + deletedCount + " expired sessions");
}
}
Possible hacks that I can think of:
- Create custom implementation of
SessionRepository
that extendsJdbcIndexedSessionRepository
and change the implementation of that cleaning - hard in multi-node deployment, but doable - Create a trigger on delete on DB level, that will store sessions that should be invalidated in a separate table. This also needs some
@Scheduled
job to scan that table in order to invalidate such sessions.
What will be done in the “original” application, where I’ve found that leak?
Nothing :) Yes, there is a memory leak, but this leak in that application is not dangerous. The memory leaks about 100MB/month with a 3GB heap and deployments are done at least once a month. The size of that leak depends on the number of unique sessions (created with login through CAS) in the application. If that number increases to the level in which that leak becomes dangerous, the profiled application is going to be switched to another implementation of Spring Session.
The most important part is that the team developing the application knows the reason behind the leak and can easily remove it when necessary. For now adding new technology (Hazelcast/Redis) is not cost-effective.