Система событий
Серверные события связывают изменение доменной сущности с дополнительной реакцией: проверкой, пересчётом, автоматизацией процесса, сбросом кеша или публикацией сообщения во внешнюю шину. Центральная точка выполнения — ru.bgcrm.event.EventProcessor.
Эта система не является очередью фоновых задач. По умолчанию обработчики выполняются синхронно внутри вызывающей операции и влияют на её результат и транзакцию.
|
Серверные события |
Основные типы
| Тип | Назначение |
|---|---|
|
Базовый |
|
Базовая реализация с временем создания и идентификатором ERP-инстанса. |
|
Событие пользовательского действия. Хранит |
|
Обработчик события типа |
|
Базовый класс обработчика из динамического Java-кода. Для декларативной диспетчеризации служит |
|
Хранит локальные подписки, вызывает системные и динамические обработчики, записывает телеметрию и передаёт поддерживаемые события в |
События обычно группируются по моменту выполнения. Например, ProcessChangingEvent и ParamChangingEvent создаются до изменения, а ProcessChangedEvent и ParamChangedEvent — после него. Это соглашение конкретного прикладного API, а не автоматическое поведение EventProcessor: место вызова и состав полей определяет код, который создаёт событие.
Порядок обработки
Обычный EventProcessor.processEvent(…) выполняет следующие шаги:
-
Использует текущий
ServerContext. Если контекста нет, создаёт транзакционный контекст пользователя изUserEventлибо системного пользователя. -
Находит подписчиков по точному runtime-классу события и последовательно вызывает их в порядке регистрации.
-
Если передано имя динамического класса, создаёт его экземпляр и вызывает после системных подписчиков.
-
Передаёт событие в
MessageBus, если не отключена обработка системных listeners. -
Возвращает управление вызывающему коду. Commit выполняет владелец
ServerContext, а неEventProcessor.
|
Подписка на базовый класс не получает события его наследников. Ключом реестра является точный результат |
Если listener выбросил BGMessageException, BGException или runtime-исключение, выполнение прекращается: следующие обработчики и MessageBus не вызываются. Исключение поднимается владельцу операции; в HTTP-запросе это обычно означает отсутствие commit и ответ с ошибкой.
Порядок регистрации сохраняется внутри списка одного класса события, но отдельного API приоритетов нет. Не стройте бизнес-логику на том, что два независимых listener всегда будут зарегистрированы в определённой последовательности. Если шаги обязаны выполняться в строгом порядке, объедините их в одном обработчике или выразите порядок в сервисе.
Варианты вызова
| Вызов | Поведение |
|---|---|
|
Использует master-соединение текущего |
|
Вызывает системных подписчиков и |
|
После системных подписчиков вызывает динамический класс |
|
Вызывает только указанный динамический класс. Системные подписчики и |
|
Передаёт listener |
Новый код внутри сервиса должен использовать соединения текущего контекста. Для совместимости со старым контрактом передавайте ServerContextConnectionSet либо используйте EventProcessorWrapper.
val context = ServerContext.get()
EventProcessor.processEvent(
ExampleChangedEvent(entityId),
ServerContextConnectionSet(context)
)
Регистрация системных обработчиков
Подписка хранит переданный экземпляр listener в памяти JVM:
class ExampleChangedListener : EventListener<ExampleChangedEvent> {
override fun notify(event: ExampleChangedEvent, conSet: ConnectionSet) {
ServerContext.getService<ExampleService>().recalculate(event.entityId)
}
}
EventProcessor.subscribe(ExampleChangedListener(), ExampleChangedEvent::class.java)
Ядро создаёт системные listeners в KernelSystemListeners. Плагины регистрируют свои обработчики при инициализации. Динамические классы из настройки createOnStart создаются при старте; административная операция полной перекомпиляции вызывает их регистрацию повторно.
subscribe не проверяет дубли. Повторное создание регистратора приводит к повторному вызову одного и того же поведения. Если класс может инициализироваться повторно, сначала удалите старые экземпляры:
EventProcessor.unsubscribe(ExampleChangedListener::class.java.name)
EventProcessor.subscribe(ExampleChangedListener(), ExampleChangedEvent::class.java)
unsubscribe удаляет из всех списков каждый listener с указанным полным именем класса.
|
Зарегистрированный экземпляр может одновременно обслуживать события из разных HTTP-потоков. Системный listener должен быть stateless или потокобезопасным. Не храните событие, |
Динамические обработчики
Имя динамического listener обычно приходит из настройки типа процесса, параметра или другой конфигурируемой сущности. Если имя передано в processEvent, для вызова создаётся новый экземпляр через DynamicClassManager. DynamicEventListener, зарегистрированный обычным subscribe, остаётся общим экземпляром и подчиняется требованиям потокобезопасности системного listener.
В обычном режиме DynamicEventListener выполняется во внутреннем executor, а вызывающий поток синхронно ждёт результат. Максимальное время ожидания определяется в следующем порядке:
-
@EventProcessingTimeout(timeout = …)на классе listener; -
настройка
event.processTimeout; -
устаревшая настройка
dynamicEventListenerTimeOut; -
1000 мс по умолчанию.
При подключённом debugger (jdwp) динамический listener выполняется прямо в вызывающем потоке без timeout, чтобы breakpoint не приводил к ошибке ожидания.
|
Timeout ограничивает только ожидание. Текущая реализация не вызывает |
AnnotatedEventListener упрощает один динамический обработчик с большим числом поддерживаемых событий: методы выбираются по аннотациям @OnEvent, @OnProcessCreated, @OnStatusChanged, @OnParamChanged и другим. Полный каталог и правила сигнатур приведены на отдельной странице.
Транзакция и побочные эффекты
Listener участвует в текущей операции:
-
видит неподтверждённые изменения через тот же master connection;
-
может добавить другие изменения в общую транзакцию;
-
может остановить операцию исключением;
-
не должен самостоятельно подтверждать или закрывать соединения контекста.
Событие с суффиксом Changed означает «основное изменение выполнено в текущей транзакции», а не «изменение уже подтверждено». Внешняя система, slave БД и другой ERP-инстанс могут ещё не видеть результат.
Долгий или внешний побочный эффект запускайте после commit:
val user = ServerContext.get().user
val entityId = event.entityId
AsyncWorker.submitAfterCommit("example_sync", user) {
syncEntity(entityId)
}
Так listener не удерживает HTTP-запрос и JDBC-соединение во время внешнего вызова, а задача не стартует при откате исходной транзакции. Ограничения фоновых задач и надёжные сценарии описаны в разделе про внешние сервисы.
MessageBus
После локальных обработчиков EventProcessor передаёт событие в MessageBus. Шина включается настройкой message_bus.enabled; message_bus.async выбирает синхронную или асинхронную публикацию.
Во внешнее сообщение преобразуются только события, для которых зарегистрирован MessageBuilder. Сейчас это:
-
ParamChangedEvent; -
ProcessChangedEvent; -
LinkAddedEvent; -
LinkRemovedEvent.
Остальные типы MessageBus игнорирует. При синхронном режиме ошибка Kafka поднимается вызывающему коду; при асинхронном — записывается в лог фонового dispatcher.
|
Вызов |
Ошибки, журнал и телеметрия
Каждый вызов listener публикует метрики:
| Метрика | Содержание |
|---|---|
|
Количество завершённых вызовов; теги |
|
Длительность одного listener с теми же тегами. |
Для успеха status равен Successful, для ошибки — failure. Полный каталог находится в справочнике метрик.
Подробный журнал в помесячных таблицах event_processor_log_YYYYMM отключён по умолчанию и управляется настройками:
| Настройка | Что записывать |
|---|---|
|
Успешные вызовы. |
|
Вызовы, завершившиеся ошибкой. |
|
Все вызовы независимо от результата. |
Запись содержит класс события, класс listener, connection id, длительность, результат и instance_host_name. Несмотря на имя колонки, текущая реализация записывает туда системное свойство JVM user.name. Журнал создаётся через отдельное соединение и подтверждается независимо от прикладной транзакции. Срок хранения задаёт event_processor_log_live для планировщика очистки логов.
Аннотация @AdditionalLogging включает отдельный logger класса, если глобально установлено enableAdditionalLogging=1.
Добавление нового события
-
Создайте небольшой неизменяемый класс события. Передавайте идентификаторы и данные, необходимые обработчикам; не помещайте в событие открытые JDBC-ресурсы или service/repository.
-
Выберите точку публикации и определите её семантику: до изменения, после изменения внутри транзакции или после commit.
-
Зарегистрируйте listener один раз при инициализации ядра или плагина.
-
Проверьте успешный сценарий, исключение listener, повторный вызов и конкурентную обработку.
-
Если реакция внешняя или долгая, запускайте её через
AsyncWorker.submitAfterCommitи обеспечьте идемпотентность.
class ExampleChangedEvent(
val entityId: Int,
val previousTitle: String,
val currentTitle: String
) : AbstractEvent()
Не создавайте новый тип, если существующее событие уже означает тот же завершённый факт и содержит достаточные данные. Не переиспользуйте существующее событие только из-за похожего набора полей, если момент или бизнес-смысл отличаются.
Типичные ошибки
-
ожидать, что подписка на
Eventили другой supertype получит все дочерние события; -
регистрировать один listener несколько раз при reload;
-
хранить состояние текущего события в singleton-listener;
-
считать
Changedгарантией commit; -
выполнять долгий внешний вызов внутри listener;
-
рассчитывать, что timeout остановил динамический скрипт;
-
отправлять в MessageBus событие, для которого нет
MessageBuilder; -
смешивать серверные и клиентские события;
-
закрывать или подтверждать переданный
ConnectionSetвнутри listener.