GlobalContext и ServerContext

GlobalContext хранит общие фабрики соединений, repositories и сервисов. Он создаётся один раз на JVM и служит корнем DI-конфигурации, но не хранит пользователя, открытую транзакцию или состояние HTTP-запроса.

ServerContext создаётся для отдельной операции. В нём находятся текущий пользователь, транзакционный режим, лениво открываемые соединения, repositories и сервисы. Обычно один контекст соответствует одному HTTP-запросу, заданию или явно ограниченному блоку фоновой операции.

Объект Ответственность Жизненный цикл

GlobalContext

Создаёт ConnectionFactory, RepositoryFactory, ServiceFactory; предоставляет DiContainer; собирает новый ServerContext

Один на JVM

ServerContext

Пользователь, режим транзакции, соединения, кэш repositories и сервисов, callbacks после commit

Одна прикладная операция

ThreadLocal<ServerContext>

Позволяет получить контекст через ServerContext.get() и сервис через ServerContext.lookup(…​)

Привязка к текущему потоку

Создание и привязка

GlobalContext.newServerContext(user, transactional) только создаёт контекст. Он не привязывает его к потоку и не управляет его завершением.

Для самостоятельной операции используйте обёртки из ru.bgcrm.context.core.ContextUtils:

withContext(user, transactional = true) {
    getService<ProcessService>().addDescription(processId, message)
}
runWithContext(user, true, context -> {
    ProcessService service = context.getService(ProcessService.class);
    service.addDescription(processId, message);
});

withContext и runWithContext:

  1. сохраняют ранее привязанный контекст;

  2. создают и привязывают новый;

  3. при успешном завершении подтверждают его, если transactional = true;

  4. всегда закрывают новый контекст;

  5. восстанавливают прежний контекст, если он был.

Они не вызывают rollback() явно при исключении: блок не подтверждается, после чего контекст закрывается. Если ошибка перехвачена внутри блока и работа продолжается в том же контексте, решение об откате должен принять вызывающий код.

Низкоуровневые setupContext/closeContext нужны инфраструктурному коду, который действительно управляет полным жизненным циклом. Каждый успешный setupContext должен иметь гарантированный closeContext в finally.

Получение текущего контекста

val context = ServerContext.get()          // исключение, если контекст не привязан
val optional = ServerContext.getOrNull()   // null, если контекста нет
val service = context.getService<ProcessService>()
ServerContext context = ServerContext.get();
ProcessService service = ServerContext.lookup(ProcessService.class);

Не передавайте ServerContext между обычными потоками и не используйте один экземпляр одновременно из нескольких потоков. Для coroutine-кода инфраструктура предоставляет ServerContext.getErpDispatcher(…​), который переносит ThreadLocal и MDC в контекст coroutine.

Повторное использование и смена пользователя

withUserContextExistingOrNewContext(fallbackUser, transactional) повторно использует текущий контекст, если он уже существует. В этом случае:

  • новый контекст и новая транзакция не создаются;

  • аргумент transactional не меняет режим существующего контекста;

  • commit и close остаются обязанностью владельца существующего контекста;

  • пользователь временно заменяется на fallbackUser, а затем восстанавливается.

withUser/runWithUser меняют только context.user. Уже созданный сервис остаётся в кэше ServiceProvider, а его реализация получила пользователя в момент создания. И наоборот: сервис, впервые полученный во время временной смены пользователя, останется в кэше и после восстановления пользователя контекста. Если авторизация сервиса зависит от внедрённого пользователя, выполняйте операцию в отдельном withContext, а не рассчитывайте на пересоздание сервиса внутри текущего контекста.

Завершение операции

Метод Поведение

commit()

Подтверждает все открытые соединения, затем выполняет зарегистрированные callbacks runAfterCommit

rollback()

Пытается откатить все открытые соединения; ошибки отдельных rollback журналируются

recycle()

Освобождает соединения только в нетранзакционном режиме

close()

Закрывает менеджер и все созданные соединения; повторно использовать контекст нельзя

Callback после commit регистрируется Kotlin-расширением ServerContext.Companion.runAfterCommit. Он запускается только после успешного commit соединений. Ошибка callback журналируется и не отменяет уже выполненный commit.

rollback() не очищает список callbacks после commit. После отката такой контекст лучше завершить, а не подтверждать повторно.

Подробнее о границах транзакций и master/slave: Соединения и транзакции. Устройство регистрации и создания зависимостей: DI: RepositoryModule и ServiceModule.

Типичные ошибки

  • хранить ServerContext, сервис или repository в singleton;

  • вручную создавать реализацию сервиса и обходить proxy;

  • вызывать GlobalContext.newServerContext без гарантированного close;

  • рассчитывать, что вложенный withUserContextExistingOrNewContext создаст новую транзакцию;

  • продолжать работу после ошибки SQL без явного решения о rollback;

  • использовать объект сервиса после закрытия создавшего его контекста.