Skip to content

Latest commit

 

History

History
271 lines (207 loc) · 19.2 KB

README.markdown

File metadata and controls

271 lines (207 loc) · 19.2 KB

dotCMS Redis Session Manager for Apache Tomcat

Introduction

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:

  1. They are being created right after a User logs into the dotCMS back-end, or an authenticated page in the front-end.
  2. 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.

Supported Tomcat Versions

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.

Compiling the Plugin

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

How this Plugin Works

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)

Configuration Parameters

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 updated
  • TOMCAT_REDIS_SESSION_HOST
  • TOMCAT_REDIS_SESSION_PORT
  • TOMCAT_REDIS_SESSION_USERNAME -- In case the Redis Server requires username authentication
  • TOMCAT_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.

Docker Setup

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.

Local Environment Setup

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.

Connection Pool Configuration

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.

Plugin's Logging

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.

Session Change Tracking

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");

Persistence Policies

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 the session.setAttribute() or session.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.

Acknowledgements

The architecture of this project was based on the following project: https://github.com/jcoleman/tomcat-redis-session-manager