Перейти к содержимому

Доменные события

Доменные события — это механизм развязки, который позволяет модулям общаться без прямых зависимостей. Вместо того чтобы модуль A вызывал модуль B, модуль A публикует событие, а модуль B на него подписывается.

DomainEventBus — это внутрипроцессная широковещательная шина публикации и подписки. Издатели вызывают publish(event); подписчики получают типизированный поток on<T>(). Каждое событие реализует DomainEvent и несёт occurredAt.

Шина работает внутри cc_server. Там живёт каждый издатель и каждый живой подписчик. Ни один клиент эту шину не видит. Клиент видит лишь отобранное подмножество, которое сервер повторно отправляет в виде кадров notifications/* JSON-RPC.

Без событий любая реакция одного модуля на другой — это импорт. Путь уведомлений должен был бы импортировать модуль агентов, модуль pull request, тикеты и встречи только для того, чтобы знать, когда показать всплывающее уведомление, — и каждому из них, в свою очередь, пришлось бы знать, что уведомления существуют. Тикеты импортировали бы конвейеры, чтобы завершённый тикет мог продвинуть запуск. Граф зависимостей затягивается в узел, и общее ядро перестаёт быть общим.

Публикация события разрывает этот узел. Путь уведомлений подписывается на двадцать один класс событий и не импортирует ни одного из модулей, которые их порождают. Диспетчер триггеров конвейеров подписывается на всю шину и ничего не знает о pull request, тикетах или встречах. Наблюдаемость агрегирует результаты запусков, не заглядывая в диспетчеризацию.

Плата за это привычна: издатель не может узнать, слушает ли его кто-нибудь, а слушатель, который так и не был создан, молча бездействует. TicketRemoteSyncHandler — постоянный пример: он объявлен, покрыт тестами и никогда не подключается в продакшене.

Стоит точно обозначить одну вещь, которой шина не владеет.

Журнал аудита — не проекция потока событий. Каждая изменяющая операция RPC по умолчанию проходит аудит — RepoOp.audited по умолчанию имеет значение true, и операция отказывается от аудита, только объявив audited: false, что является осознанным действием, видимым в самой операции. Диспетчер добавляет строку «кто что сделал и откуда» сразу после успешного вызова. Никакого DomainEventAuditBridge нет. ActivityLogPersister записывает строки activity_log из событий ActivityLogged; это отдельный поток, не связанный с аудитом RPC.

Шина событий является транспортом для уведомлений и для автоматизации. Но она не система учёта, хранящая эталонные записи.

  • Уведомления. Сервер превращает двадцать один класс событий в кадры notifications/*, записывает их в долговечную ленту каждого рабочего пространства и транслирует подключённым сессиям. Кадр, в чьём workspace_id указано рабочее пространство, участником которого пользователь сессии не является, отбрасывается на сервере; затем клиент фильтрует оставшиеся кадры по тому рабочему пространству, которое просматривает.
  • Автоматизация по событиям. Триггеры конвейеров подписываются на шину и автоматически запускают подходящие конвейеры (см. ниже).
  • Реакции жизненного цикла. Создание рабочего пространства заводит агентов CEO и специалистов, встроенные шаблоны конвейеров и стартовые сьюиты eval. Завершённый запуск агента возобновляет приостановленный шаг конвейера, закрывает задачу, питает супервизор целей, может разбудить именованного проверяющего и может заново запустить лидера команды. Смерженный PR или удалённое пространство запускает сборку мусора рабочих деревьев.
  • Синхронизация между вендорами. Локальные изменения тикетов отправляются настроенным вендорам координатором, который слушает пять событий тикетов.

События охватывают всю поверхность продукта. В общих чертах:

  • Рабочие пространства, агенты и репозитории — создание рабочего пространства, завершение запуска, регистрация репозитория, обновления навыков
  • Pull requests — публикация, изменения статуса, слияния, запросы на проверку, упоминания, отслеживание созданных вами PR, обнаруженные извне PR
  • Сообщения — сообщения, пространства и ход подготовки пространств
  • Тикеты — создание, назначение, переназначение, детали, статус и три терминальных исхода
  • Задачи — упорядоченные кадры жизненного цикла одного запуска, чтобы клиенты могли их упорядочивать и устранять дубликаты
  • Конвейеры — только терминальные события запусков (завершён, отменён, не выполнен)
  • Наблюдаемость — записи журнала активности и пороги бюджета
  • Идентичность и членство — участник добавлен, удалён, роль изменена
  • Календарь и встречи — истечение авторизации, начало встреч и завершение записей
  • Rigs — перехват, сбор и неожиданное закрытие

Память, оркестрация, документы планов, артефакты, согласования, приём синхронизации тикетов, обновление календаря, слияния рабочих деревьев, создание пользователей, приглашения и отзыв устройств не являются доменными событиями; см. справку.

Полный каталог — все 53 конкретных класса событий, их полезные нагрузки и 24 подписчика, которые их потребляют, — см. в справочнике по доменным событиям. Та страница — инвентарь, эта — модель.

Контроль доступа в реальном времени опирается на два разных механизма

Заголовок раздела «Контроль доступа в реальном времени опирается на два разных механизма»

События членства и события устройств выглядят симметрично, но таковыми не являются, и это важно, если вы рассуждаете о том, как быстро отзыв вступает в силу.

Отзыв устройства действительно завершает его сессии. Сервер следит за таблицей привязанных устройств и в течение нескольких секунд отключает любую активную сессию, устройство которой покинуло активный набор.

Удаление участника не закрывает сокет. Событие WorkspaceMemberRemoved обрабатывается: LocalRpcServer и RemoteRelayHost снимают подписки этого пользователя на рабочее пространство, а RemoteEventForwarder инвалидирует свой кеш членства, чтобы дальнейшие кадры для этого рабочего пространства не отправлялись. Следующий вызов RPC отклоняет по-прежнему ролевой шлюз, который заново определяет членство при каждом вызове, — поэтому их следующий запрос отклоняется с сообщением «Not a member of this workspace». Эффект наступает немедленно для всего, что они попытаются сделать; просто он достигается не разрывом их соединения.

Триггеры конвейеров подписываются на доменные события и автоматически запускают подходящие конвейеры. PipelineTriggerDispatcher слушает всю шину, а затем:

  1. Завершается досрочно, если тип события не относится к тем, которые он умеет преобразовывать в полезную нагрузку
  2. Ищет включённые триггеры, соответствующие этому типу события, по всем рабочим пространствам
  3. Отбирает каждый по его собственному рабочему пространству и применяет его фильтр совпадения полезной нагрузки
  4. Стартует запуск для каждого триггера, который прошёл отбор

Важное ограничение — шаг 1. Шина несёт 53 типа событий; список выбора при добавлении триггера позволяет запускать конвейер только для этих четырнадцати (EventPayloadMapper, ветки полезной нагрузки): ExternalPrDetected, PullRequestPublished, PullRequestStatusChanged, PrMerged, MessageReceived, TicketAssigned, TicketCompleted, TicketFailed, TicketCancelled, BudgetThresholdCrossed, RepoAdded, MeetingRecordingStopped, SkillUpdated и SpaceDeleted. Больше ничего не может запустить конвейер.

RemoteEventForwarder преобразует события класса уведомлений в кадры и отправляет их по RPC подключённым сессиям, где преобразователь кадров превращает их в уведомления внутри приложения. Таким образом, тонкий клиент видит проекцию потока событий сервера, не выполняя ничего сам, — в этом и смысл разделения с тонким клиентом.

Однако не каждый отправленный кадр — уведомление. Поток жизненного цикла задач и переназначение тикетов передаются в реальном времени для управления интерфейсом, но они не записываются в ленту уведомлений и не показывают всплывающих уведомлений.