This session manager is an implementation that stores sessions in Redis for easy distribution of requests across a cluster of Tomcat servers. Sessions are implemented as non-sticky -- that is, each request is able to go to any server in the cluster (unlike the Apache provided Tomcat clustering setup.)
Sessions are stored into Redis immediately as soon as they meet at least one of the following two conditions:
- They are being created right after a User logs into the dotCMS back-end, or an authenticated page in the front-end.
- The switch for storing absolutely all sessions in Redis is enabled -- more on this later on.
Sessions are loaded as requested directly from Redis (but subsequent requests for the session during the same request context will return a ThreadLocal cache rather than hitting Redis multiple times). In order to prevent collisions (and lost writes) as much as possible, session data is only updated in Redis if the session has been modified.
The manager relies on the native expiration capability of Redis to expire keys for automatic session expiration to avoid the overhead of constantly searching the entire list of sessions for expired sessions. However, for usual front-end sessions -- or one-time hits coming from bots or image requests -- the expiration process will fall back to how Tomcat works originally.
Additionally, it's very important to note that absolutely all data stored in the session must be Serializable and not null. Otherwise, a warning message will be printed in the logs stating so, and indicating what specific attribute was not added to the Session.
As of June 2024, this project currently supports the following Tomcat versions:
- 9.0.60
- 9.0.85
Greater or lower versions are yet to be tested.
This plugin provides a Gradle Wrapper that you can use to generate the expected .JAR file. Just open up a Terminal in the directory where this project is located, and run the following command:
./gradlew clean jar
The most important parts of this plugin are the following:
RedisSessionManager
: Provides the session creation, saving, and loading functionality.RedisSessionHandlerValve
: Hooks up the session manager with Tomcat, and ensures that sessions are saved after a request is finished processing.
Note: This architecture differs from the Apache PersistentManager
implementation which implements persistent sticky sessions. Because that implementation expects all requests from a specific session to be routed to the same server, the timing persistence of sessions is non-deterministic since it is primarily for failover capabilities.
The expected XML configuration must be added to the Tomcat context.xml
file (or the context block of the server.xml
, if applicable) in order to tell Tomcat that the Session Management will be customized. Different plugin and generic pool properties can be added as required. For instance:
<Valve className="com.dotcms.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.dotcms.tomcat.redissessions.RedisSessionManager"
host="localhost" <!-- optional: defaults to "localhost" -->
port="6379" <!-- optional: defaults to "6379" -->
database="0" <!-- optional: defaults to "0" -->
maxInactiveInterval="60" <!-- optional: defaults to "60" (in seconds) -->
sessionPersistPolicies="PERSIST_POLICY_1,PERSIST_POLICY_2,.." <!-- optional -->
sentinelMaster="SentinelMasterName" <!-- optional -->
sentinels="sentinel-host-1:port,sentinel-host-2:port,.." <!-- optional --> />
It's very important to note that the Valve
tag must be declared before the Manager
tag.
This plugin relies on the following JAR files, which must be present in the {TOMCAT_BASE}/lib/
directory:
commons-pool2-2.11.1.jar
jedis-4.4.6.jar
slf4j-api-1.7.36.jar
tomcat-redis-session-manager-VERSION.jar
(the JAR generated by this project, of course)
If changing the configuration parameters directly in the context.xml
file is not feasible or secure, they can be specified via Environment Variables, or Java Properties in the dotCMS startup script. Here's a list with the properties most commonly used by the plugin:
TOMCAT_REDIS_SESSION_CONFIG
-- In case the XML configuration as a whole needs to be updatedTOMCAT_REDIS_SESSION_HOST
TOMCAT_REDIS_SESSION_PORT
TOMCAT_REDIS_SESSION_USERNAME
-- In case the Redis Server requires username authenticationTOMCAT_REDIS_SESSION_PASSWORD
TOMCAT_REDIS_SESSION_DATABASE
TOMCAT_REDIS_SESSION_TIMEOUT
TOMCAT_REDIS_SESSION_PERSISTENT_POLICIES
TOMCAT_REDIS_MAX_CONNECTIONS
TOMCAT_REDIS_MAX_IDLE_CONNECTIONS
TOMCAT_REDIS_MIN_IDLE_CONNECTIONS
TOMCAT_REDIS_ENABLED_FOR_ANON_TRAFFIC
TOMCAT_REDIS_UNDEFINED_SESSION_TYPE_TIMEOUT
DOT_DOTCMS_CLUSTER_ID
Additionally, once dotCMS starts up, a section describing the initialization values for all these properties will be displayed in the catalina.out
or dotcms.log
file so that it can be easily monitored by System Administrators. Here's an example of what such an output could look like:
12-Jun-2023 10:16:30.510 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.startInternal ========================================================================
12-Jun-2023 10:16:30.510 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.startInternal
12-Jun-2023 10:16:30.510 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.startInternal Redis-managed Tomcat Session plugin
12-Jun-2023 10:16:30.510 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.startInternal
12-Jun-2023 10:16:30.511 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.startInternal ========================================================================
12-Jun-2023 10:16:30.511 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.startInternal -> Attaching 'com.dotcms.tomcat.redissessions.RedisSessionManager' to 'com.dotcms.tomcat.redissessions.RedisSessionHandlerValve'
12-Jun-2023 10:16:30.511 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeSerializer -> Attempting to use serializer: com.dotcms.tomcat.redissessions.JavaSerializer
12-Jun-2023 10:16:30.512 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams -> Loading configuration parameters:
12-Jun-2023 10:16:30.512 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_HOST: localhost
12-Jun-2023 10:16:30.512 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_PORT: 6379
12-Jun-2023 10:16:30.512 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_USERNAME: - Set -
12-Jun-2023 10:16:30.512 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_PASSWORD: - Set -
12-Jun-2023 10:16:30.512 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_SSL_ENABLED: false
12-Jun-2023 10:16:30.513 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_SENTINEL_MASTER: null
12-Jun-2023 10:16:30.513 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_SENTINELS: null
12-Jun-2023 10:16:30.513 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_DATABASE: 0
12-Jun-2023 10:16:30.513 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_TIMEOUT: 2000
12-Jun-2023 10:16:30.513 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_SESSION_PERSISTENT_POLICIES: DEFAULT
12-Jun-2023 10:16:30.513 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_MAX_CONNECTIONS: 128
12-Jun-2023 10:16:30.513 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_MAX_IDLE_CONNECTIONS: 100
12-Jun-2023 10:16:30.513 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_MAX_IDLE_CONNECTIONS: 32
12-Jun-2023 10:16:30.514 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_ENABLED_FOR_ANON_TRAFFIC: false
12-Jun-2023 10:16:30.514 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] TOMCAT_REDIS_UNDEFINED_SESSION_TYPE_TIMEOUT: 15
12-Jun-2023 10:16:30.514 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeConfigParams [✓] DOT_DOTCMS_CLUSTER_ID (Redis Key Prefix): dotcms-redis-cluster
12-Jun-2023 10:16:30.514 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeRedisConnection - Initializing Redis connection...
SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.
12-Jun-2023 10:16:30.568 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeRedisConnection -
12-Jun-2023 10:16:30.568 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeRedisConnection - Successful! Redis-managed Tomcat Sessions will expire after 1800 seconds.
12-Jun-2023 10:16:30.568 INFO [main] com.dotcms.tomcat.redissessions.RedisSessionManager.initializeRedisConnection -
For security reasons, the values of following two properties:
TOMCAT_REDIS_SESSION_USERNAME
TOMCAT_REDIS_SESSION_PASSWORD
Will NOT be displayed in the log. Instead, the String - Set -
will be displayed if a value has been set for them, and - Not Set -
if it hasn't.
The success message at the bottom is the key indicator that the plugin has successfully connected to Redis and is ready to receive data. By default, only sessions created by dotCMS for either the back-end or the front-end will be persisted to Redis. If you want to persist sessions created by anonymous traffic, you can set the TOMCAT_REDIS_ENABLED_FOR_ANON_TRAFFIC
property to true
.
Allowing multiple clusters to share the same session Redis store can be a very smart strategy. In order to accomplish this, you can specify the ID of the cluster via the DOT_DOTCMS_CLUSTER_ID
property which is used to prefix all keys persisted to Redis.
In your docker-compose.yml
file, go to the environment
section of your dotCMS node setup and add the configuration properties you deem necessary -- please refer to the Configuration Parameters
section. For instance:
dotcms-node:
image: dotcms/dotcms:master
environment:
TOMCAT_REDIS_SESSION_ENABLED: 'true'
TOMCAT_REDIS_SESSION_HOST: 'redis'
TOMCAT_REDIS_SESSION_PORT: '6379'
TOMCAT_REDIS_SESSION_PASSWORD: 'MY_SECRET_P4SS'
TOMCAT_REDIS_SESSION_SSL_ENABLED: 'false'
TOMCAT_REDIS_SESSION_PERSISTENT_POLICIES: 'DEFAULT'
DOT_DOTCMS_CLUSTER_ID: 'dotcms-redis-cluster'
...
..
.
Notice that there's a property called TOMCAT_REDIS_SESSION_ENABLED
in the example configuration. If you remove it or set its value to 'false'
and restart your dotCMS container, the plugin will NOT be activated during startup and the application will let Tomcat handle all Sessions in memory as usual.
In your local environment, you need to go to the Tomcat context.xml
file, scroll down to the bottom, and add the following code:
<Valve className="com.dotcms.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.dotcms.tomcat.redissessions.RedisSessionManager"
password="YOUR_P4SS_HERE"/>
For the plugin to be activated when dotCMS starts up. This configuration is what actually enables this plugin, so once you comment it back, dotCMS will go back to letting Tomcat handle its Sessions, as usual.
A local Redis Server must be up and running before the Redis Session Manager is enabled -- i.e., added to the context.xml
file -- and dotCMS is started. Here's an example of a docker-compose
file that sets up a simple password-protected Redis Server:
networks:
redis_net:
volumes:
redisdata:
services:
redis:
image: "redis:latest"
command: redis-server --requirepass YOUR_P4SS_HERE
ports:
- "6379:6379"
volumes:
- redisdata:/data
networks:
- redis_net
IMPORTANT: If you need to set up a Redis Server that requires both username and password, please refer to the sample docker-compose
file here: docker-compose-examples/redis-with-usr-pwd/redis/docker-compose.yml
.
Please refer to the Configuration Parameters
section in case you need to enable/disable additional configuration properties for Redis via Environment Variables or Java Properties. For instance, when using the docker-compose
file above, you'll need to specify both the username
and password
attributes.
All the configuration options from both org.apache.commons.pool2.impl.GenericObjectPoolConfig
and org.apache.commons.pool2.impl.BaseObjectPoolConfig
are also configurable for the Redis connection pool used by the session manager. To configure any of these attributes (e.g., maxIdle
and testOnBorrow
) just use the config attribute name prefixed with connectionPool
(e.g., connectionPoolMaxIdle
and connectionPoolTestOnBorrow
) and set the desired value in the <Manager>
declaration in your Tomcat's context.xml
file.
By default, and for security reasons, minimal initialization information is logged when dotCMS starts up. This is basically meant to let you know that the plugin is actually present, and is enabled. If more detailed information is required, you just need to follow these steps:
- Go to the
{TOMCAT_HOME}/conf/logging.properties
file. - Scroll down to the bottom, and add an entry for every class in this plugin for which you want to increase the logging level. For instance, if you need the
RedisSessionManager
class to log more information, add this:
com.dotcms.tomcat.redissessions.RedisSessionManager.level = FINE
- Restart dotCMS and check the
catalina.out
file.
It's important to notice that this operation should only be carried out under specific circumstances. Given the number of threads spawned by Tomcat, the additional logging can be overwhelming, hard to read, and fill the logs in a very short time. For more information on the different logging levels, please refer to the Apache Tomcat Logging documentation.
As noted in the "Overview" section above, in order to prevent colliding writes, the Redis Session Manager only serializes the session object into Redis if the session object has changed (it always updates the expiration separately, however.) This dirty tracking marks the session as needing serialization according to the following rules:
- Calling
session.removeAttribute(key)
always marks the session as dirty (needing serialization.) - Calling
session.setAttribute(key, newAttributeValue)
marks the session as dirty if any of the following are true:previousAttributeValue == null && newAttributeValue != null
previousAttributeValue != null && newAttributeValue == null
!newAttributeValue.getClass().isInstance(previousAttributeValue)
!newAttributeValue.equals(previousAttributeValue)
This feature can have the unintended consequence of hiding writes if you implicitly change a key in the session or if the object's equality does not change even though the key is updated. For example, assuming the session already contains the key "myArray"
with an Array instance as its corresponding value, and has been previously serialized, the following code would not cause the session to be serialized again:
List myArray = session.getAttribute("myArray");
myArray.add(additionalArrayValue);
If your code makes this kind of change, then the RedisSession provides a mechanism by which you can mark the session as dirty in order to guarantee serialization at the end of the request. For example:
List myArray = session.getAttribute("myArray");
myArray.add(additionalArrayValue);
session.setAttribute("__changed__");
In order to not cause issues with an application that may already use the key "__changed__"
, this feature is disabled by default. To enable this feature, simple call the following code in your application's initialization:
RedisSession.setManualDirtyTrackingSupportEnabled(true);
This feature also allows the attribute key used to mark the session as dirty to be changed. For example, if you executed the following:
RedisSession.setManualDirtyTrackingAttributeKey("customDirtyFlag");
Then the example above would look like this:
List myArray = session.getAttribute("myArray");
myArray.add(additionalArrayValue);
session.setAttribute("customDirtyFlag");
With a persistent session storage there is going to be the distinct possibility of race conditions when requests for the same session overlap/occur concurrently. Additionally, because the session manager works by serializing the entire session object into Redis, concurrent updating of the session will exhibit last-write-wins behavior for the entire session (not just specific session attributes).
Since each situation is different, the manager gives you several options which control the details of when/how sessions are persisted. Each of the following options may be selected by setting the sessionPersistPolicies="PERSIST_POLICY_1,PERSIST_POLICY_2,.."
attributes in your manager declaration in Tomcat's context.xml. Unless noted otherwise, the various options are all combinable.
SAVE_ON_CHANGE
: Every time either thesession.setAttribute()
orsession.removeAttribute()
methods are called, the session will be saved. Note: This feature cannot detect changes made to objects already stored in a specific session attribute. Tradeoffs: This option will degrade performance slightly as any change to the session will save it synchronously to Redis.ALWAYS_SAVE_AFTER_REQUEST
: Force saving after every request, whether the manager has detected changes to the session or not. This option is particularly useful if you make changes to objects already stored in a specific session attribute. Tradeoff: This option make actually increase the likelihood of race conditions if not all of your requests change the session.
The architecture of this project was based on the following project: https://github.com/jcoleman/tomcat-redis-session-manager