Менеджер HTTP-сессий

Менеджер HTTP-сессий хранит контекст пользователя между запросами к Tomcat. Авторизация выполняется отдельно в AuthFilter; менеджер отвечает за жизненный цикл HttpSession, её атрибуты и срок действия.

ERP поддерживает два варианта:

  • стандартный org.apache.catalina.session.StandardManager хранит сессии в памяти одного инстанса;

  • ru.bgcrm.session.CustomSessionManager сохраняет сессии в Redis и позволяет нескольким инстансам работать с одним хранилищем.

По умолчанию используется StandardManager. Кастомный менеджер включается только при запуске приложения и требует доступного Redis.

Как работает кастомный менеджер

При включении SessionManagerInitializer читает redisson.conf из рабочего каталога, создаёт RedissonStore и устанавливает CustomSessionManager в контекст Tomcat. Если конфигурация Redisson некорректна или соединение создать не удалось, запуск ERP завершается ошибкой.

SessionStoreValve сохраняет уже существующую сессию после обработки каждого запроса. Сессия не создаётся специально ради сохранения. Запросы, URL которых содержит .js, .css или .vue, пропускаются.

В Redis каждая сессия хранится в map с ключом session:erp:<sessionId>. Вместе с пользовательскими атрибутами записываются время создания и maxInactiveInterval. TTL ключа равен maxInactiveInterval, поэтому просроченные сессии удаляет Redis.

Не сохраняются:

  • атрибут локали Struts org.apache.struts.action.LOCALE;

  • значения, реализующие маркерный интерфейс IgnoreOnPersistSessionAttribute.

Остальные атрибуты должны сериализоваться настроенным Jackson-кодеком. После добавления в сессию нового типа нужно проверить его запись и чтение через Redis, в том числе после перезапуска ERP.

Конфигурация ERP

Параметры server.customManager.* читаются из общей конфигурации ERP при запуске. Изменение значений в административном интерфейсе не заменяет уже созданный менеджер: для применения нужен перезапуск инстанса.

Параметр По умолчанию Назначение

server.customManager.enabled

false

Включает CustomSessionManager. При false Tomcat использует стандартный менеджер.

server.customManager.nodeId

server.instance.id, а если он не задан — erp

Имя текущего инстанса в сообщениях об изменении сессий.

server.customManager.readMode

MEMORY

MEMORY оставляет загруженные сессии в локальном кэше. STORAGE удаляет локальную копию после сохранения, поэтому следующий запрос снова читает сессию из Redis.

server.customManager.writeMode

ASYNC

ASYNC завершает запрос, не дожидаясь записи в Redis; ошибка пишется в лог. SYNCHRONIZED ждёт запись и возвращает её ошибку в обработку запроса.

server.customManager.publishMode

NODE

Канал уведомлений об обновлении и удалении сессии: NODE, GLOBAL или DISABLED.

server.customManager.listeners

пустой список

Идентификаторы инстансов, чьи каналы нужно слушать в режиме NODE. Значения перечисляются через запятую без пробелов.

Значения перечислений задаются прописными буквами. Неизвестное значение останавливает инициализацию менеджера.

Минимальная конфигурация одного инстанса:

server.customManager.enabled=true
server.customManager.nodeId=erp0
server.customManager.readMode=MEMORY
server.customManager.writeMode=ASYNC
server.customManager.publishMode=GLOBAL

Конфигурация Redisson

Файл redisson.conf находится в рабочем каталоге ERP. Это YAML-конфигурация Redisson; менеджер не читает адрес Redis из bgcrm.properties.

Минимальный пример для одного Redis-сервера:

singleServerConfig:
  address: "redis://redis.example.org:6379"
  database: 0
  connectTimeout: 10000
  timeout: 2000
  retryAttempts: 3
  retryInterval: 1000

Полный набор параметров соединения определяется Redisson. После изменения файла нужен перезапуск ERP.

Согласование сессий между инстансами

Redis Pub/Sub передаёт только уведомление с идентификатором сессии. Сами атрибуты остаются в session:erp:<sessionId>.

При обновлении сессии другой инстанс удаляет её из локального кэша. При следующем запросе актуальная версия загружается из Redis. При удалении сессии найденная локальная копия завершается через expire().

Режимы публикации:

  • GLOBAL — изменения публикуются в общий канал. Каждый кастомный менеджер подписывается на него автоматически; listeners не требуется;

  • NODE — изменения публикуются в канал nodeId отправителя. Каждый другой инстанс должен перечислить этот nodeId в listeners;

  • DISABLED — уведомления не публикуются. Режим подходит только там, где не требуется инвалидировать локальные копии на других инстансах.

Для трёх инстансов в режиме NODE конфигурация подписок симметрична:

# erp0
server.customManager.nodeId=erp0
server.customManager.listeners=erp1,erp2

# erp1
server.customManager.nodeId=erp1
server.customManager.listeners=erp0,erp2

# erp2
server.customManager.nodeId=erp2
server.customManager.listeners=erp0,erp1

Сообщения, отправленные самим инстансом, игнорируются. При NODE с пустым listeners межсерверной инвалидации локального кэша не будет.

Выбор режимов

Режим Преимущество Особенность

MEMORY

Повторные запросы обычно не читают Redis.

Для нескольких инстансов нужна корректная публикация инвалидаций.

STORAGE

Каждый запрос получает состояние из общего хранилища.

После запроса локальная копия удаляется; возрастает нагрузка и зависимость от Redis.

ASYNC

Запрос не ждёт сетевую запись.

При ошибке сохранения запрос уже завершён. В сочетании со STORAGE возможен короткий интервал, когда локальная копия уже удалена, а асинхронная запись ещё не завершилась.

SYNCHRONIZED

К моменту успешного завершения сохранения данные уже находятся в Redis.

Задержка и недоступность Redis влияют на пользовательский запрос.

Для кластера с локальным кэшем обычно используют MEMORY и либо GLOBAL, либо полностью заполненную схему NODE/listeners. Выбор ASYNC или SYNCHRONIZED зависит от допустимого риска потери последнего изменения сессии и требований к задержке.

Ограничения текущей реализации

  • Единственное внешнее хранилище, которое создаёт SessionManagerInitializer, — Redis. Реализаций для базы данных или файловой системы в модуле нет.

  • Комбинацию writeMode=ASYNC и publishMode=DISABLED использовать нельзя: AsyncWriteRedissonAdapter после успешной записи или удаления безусловно обращается к объекту канала публикации, который в режиме DISABLED не создаётся. Если публикация не нужна, используйте writeMode=SYNCHRONIZED.

  • Redis Pub/Sub не хранит сообщения. Инстанс, который был отключён от Redis в момент обновления, не получит пропущенную инвалидацию локальной копии. Это особенно важно для режима MEMORY.

  • Удаление ключа самим Redis по TTL не создаёт SessionDestroyedMessage. Истечение локальной сессии и прикладная очистка через SessionListener выполняются Tomcat независимо.

Таймауты

Стандартный таймаут HTTP-сессии задан в WEB-INF/web.xml и равен пяти минутам. AuthFilter может установить maxInactiveInterval из параметра sessionTimeout для запросов с токеном. Именно фактический maxInactiveInterval конкретной сессии становится TTL её ключа в Redis.

sessionTimeout также используется LoginStat для проверки прикладной активности пользователя. Это отдельный механизм: LoginStat хранится в памяти каждого инстанса и не становится распределённым при включении CustomSessionManager.

Диагностика

При проверке конфигурации:

  1. Убедитесь, что при старте есть сообщение Initializing custom Session manager (Redis). Сообщение Using default session manager означает, что кастомный менеджер выключен.

  2. Проверьте появление ключа session:erp:<sessionId> и его TTL после авторизованного запроса.

  3. В кластере измените сессию на одном инстансе и проверьте, что другой получил сообщение SessionUpdatedMessage и удалил локальную копию.

  4. Для завершения сессии проверьте SessionDestroyedMessage и удаление ключа.

Для ASYNC таймаут записи выводится как Redis timeout on session save или Redis timeout on session remove; остальные ошибки записываются как Unknown Redis exception. Ошибки подписки выводятся как Exception on listeners subscribe.

Счётчики BGCRM:type=App (SessionsCount и UsersCount) и экран Статус сервера читают локальный LoginStat, а не число ключей в Redis. Поэтому они показывают данные только текущего инстанса.

Основные классы

Класс Назначение

SessionManagerInitializer

Выбирает менеджер при запуске и загружает redisson.conf.

SessionManagerConfig

Разбирает параметры server.customManager.*.

CustomSessionManager

Управляет локальным кэшем, загрузкой из store и подписками на уведомления.

SessionStoreValve

Сохраняет сессию после запроса.

RedissonStore

Преобразует HttpSession в Redis map, назначает TTL и создаёт сообщения обновления.

SynchronizedRedissonAdapter, AsyncWriteRedissonAdapter

Реализуют синхронную и асинхронную запись.

RedissonAdapter

Задаёт операции над Redis, используемые хранилищем, и отделяет их от режима записи.

AttributeMessage, SessionUpdatedMessage, SessionDestroyedMessage

Описывают уведомления Redis Pub/Sub: источник, идентификатор сессии и тип изменения.

IgnoreOnPersistSessionAttribute

Маркер для значений, которые должны оставаться только в памяти инстанса.

Модуль покрыт тестами RedissonStoreTest: проверяются загрузка и сохранение атрибутов, исключение локальных значений, удаление сессии и отписка listeners. Отдельных тестов жизненного цикла CustomSessionManager, работы SessionStoreValve и обоих режимов адаптера сейчас нет; изменения в этих частях требуют интеграционной проверки с Redis.