GlobalContext и ServerContext
GlobalContext хранит общие фабрики соединений, repositories и сервисов. Он создаётся один раз на JVM и служит корнем DI-конфигурации, но не хранит пользователя, открытую транзакцию или состояние HTTP-запроса.
ServerContext создаётся для отдельной операции. В нём находятся текущий пользователь, транзакционный режим, лениво открываемые соединения, repositories и сервисы. Обычно один контекст соответствует одному HTTP-запросу, заданию или явно ограниченному блоку фоновой операции.
| Объект | Ответственность | Жизненный цикл |
|---|---|---|
|
Создаёт |
Один на JVM |
|
Пользователь, режим транзакции, соединения, кэш repositories и сервисов, callbacks после commit |
Одна прикладная операция |
|
Позволяет получить контекст через |
Привязка к текущему потоку |
Создание и привязка
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:
-
сохраняют ранее привязанный контекст;
-
создают и привязывают новый;
-
при успешном завершении подтверждают его, если
transactional = true; -
всегда закрывают новый контекст;
-
восстанавливают прежний контекст, если он был.
Они не вызывают 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, а не рассчитывайте на пересоздание сервиса внутри текущего контекста.
Завершение операции
| Метод | Поведение |
|---|---|
|
Подтверждает все открытые соединения, затем выполняет зарегистрированные callbacks |
|
Пытается откатить все открытые соединения; ошибки отдельных rollback журналируются |
|
Освобождает соединения только в нетранзакционном режиме |
|
Закрывает менеджер и все созданные соединения; повторно использовать контекст нельзя |
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;
-
использовать объект сервиса после закрытия создавшего его контекста.