# События (Kafka outbox) Ordinis публикует все изменения справочников в Kafka через **outbox pattern** (at-least-once гарантия). Consumers подписываются на топики и инвалидируют свой кэш / обновляют projection. ## Топики | Топик | Scope | Назначение | |---|---|---| | `ordinis.cuod.events.public` | PUBLIC | Изменения публичных справочников. | | `ordinis.cuod.events.internal` | INTERNAL | INTERNAL словари — нужна role `ordinis-internal`+. | | `ordinis.cuod.events.restricted` | RESTRICTED | RESTRICTED — role `ordinis-restricted`. | Параметры топиков (для consumer config): - `cleanup.policy: delete` - `retention.ms: 604800000` (7 дней) - Multi-partition (точное число — спросить Ordinis team при registration, partition count может меняться по operational reasons). ## Подключение ### Bootstrap address + creds Запросить у Ordinis team: - Bootstrap servers (внутренний DNS на per-environment basis) - SASL credentials (SCRAM-SHA-512 username + password) - Topic ACL (Read + Describe на нужные топики) - Consumer group prefix (для group ACL) Auth: **SASL/SCRAM-SHA-512**. TLS опциональный (отдельный listener). Креденшелы выдаются per-consumer service. Не share между сервисами — ACL и audit за каждым. ## Schema события ### `dictionary.changed` Все события имеют общий envelope: ```json { "schemaVersion": "1.0.0", "eventType": "dictionary.changed", "occurredAt": "2026-05-07T15:23:45.123+03:00", "txTime": "2026-05-07T15:23:45.100+03:00", "changedBy": "admin@nstart.space", "changeReason": "Обновление after launch", "scope": "PUBLIC", "dictionary": "spacecraft", "businessKey": "RESURS-P-3", "action": "updated", "version": 3, "validFrom": "2026-05-07T00:00:00+03:00", "validTo": "9999-12-31T23:59:59+03:00", "data": { "name": { "ru-RU": "Ресурс-П № 3 (обновлён)" }, "status": "active", "satelliteTypeCode": "EARTH-OBSERVATION-OPTICAL" }, "previousData": { "name": { "ru-RU": "Ресурс-П № 3" }, "status": "in-orbit" } } ``` ### Поля envelope | Поле | Описание | |---|---| | `schemaVersion` | Semver схемы события. Bump major = breaking change. | | `eventType` | `dictionary.changed` для CRUD, `dictionary.bulk_imported` для bundle imports. | | `occurredAt` | Wall-clock когда событие создано. | | `txTime` | Transaction time изменения в БД (для ordering). | | `action` | `created` / `updated` / `closed`. | | `version` | Optimistic lock version. Monotonic per businessKey. | | `data` | Payload новой версии. | | `previousData` | Diff-like — поля, которые поменялись (только для `updated`). Полезен для consumer'ов которые хотят минимальный set re-fetches. | | `validFrom` / `validTo` | Bitemporal границы. | ### `action` semantics - **created** — новая запись с уникальным `businessKey`. `previousData = null`. - **updated** — существующий businessKey, новая версия. `previousData` = изменённые поля старой версии. - **closed** — `validTo` обновлён в `now()` (раньше `9999-...`). Запись больше не возвращается на `?at=now`. `previousData` = вся старая версия. ### Cascade events (v2 roadmap) При cascade close (см. design doc `dict-relationships-v2` — внутренний artifact в `~/.gstack/projects/claude/`) на каждую закрытую запись отдельное событие emit'ится в outbox с дополнительным полем `cascadeSource` = `{dictionary, businessKey}` источника. Consumer может trace cascade chain. ### Definition events Изменения **самой schema** dictionary (не records) emit'ятся отдельными event types: | `eventType` | Когда | |---|---| | `DefinitionCreated` | Новый dictionary зарегистрирован | | `DefinitionUpdated` | Inline PATCH schema (см. `/dictionaries/{name}/schema`) | Payload содержит `schemaJson`, `schemaVersion`, `previousSchema` (для updated). Consumer'ы которые кэшируют schemas инвалидируют по этим событиям. ### Schema workflow events (v2.2.0+) Workflow draft lifecycle emit'ит дополнительные events. Все содержат `draftId`, `dictionaryName`, `status`, `branchedFromVersion`, `makerId` плюс event-specific extras: | `eventType` | Trigger | Extras | |---|---|---| | `SchemaDraftCreated` | maker создаёт draft | `fromVersion` | | `SchemaDraftReviewRequested` | submit на review | `reviewers[]`, `note` | | `SchemaDraftDecided` | reviewer вынес verdict | `decision` (approve/request_changes/reject), `reviewerId`, `comment` | | `SchemaDraftPublished` | publisher выпустил draft → live | `newVersion`, `publishedAt`, `publishNote` | | `SchemaDraftWithdrawn` | maker отозвал draft | (без extras) | Webhook router маппит эти типы в `dict.draft.*` category (см. [webhooks.md](webhooks.md)). Полная state machine: [schema-workflow.md](schema-workflow.md). ## Partition key Сообщения партиционируются по `dictionary + businessKey` (одна запись — одна partition). Это даёт consumer'ам ordering guarantee per record. ``` key = "spacecraft:RESURS-P-3" ``` {% note info %} Если consumer обрабатывает события параллельно (multiple consumer threads / pods), Kafka гарантирует только partition-level ordering. Ordering между records в разных partitions не определён. Для consumer'ов это норма — each businessKey изолирован. {% endnote %} ## At-least-once семантика Ordinis использует **outbox pattern** — изменение БД и публикация в Kafka атомарны через transactional outbox. Гарантия: **at-least-once**. Что это значит для consumer'а: - Можешь получить **дубликат** события (rare, при partial failures на стороне Ordinis). Idempotency у consumer'а — must. - При недоступности Kafka события накапливаются в Ordinis side и доставляются после восстановления — **events не теряются**. - Order — гарантирован per partition key (`dictionary:businessKey`), не глобально. ## Idempotency Используй `(dictionary, businessKey, txTime, version)` как dedup key: ```python seen = redis.get(f"ordinis:event:{dict}:{key}:{tx_time}:{ver}") if not seen: process_event(event) redis.setex(f"ordinis:event:...", 86400, "1") ``` ## Persistent failures (Ordinis side) Если событие не публикуется > 1000 retry попыток (multi-day Kafka outage), оно складывается в DLQ внутри Ordinis. Не теряется, но требует ручного разбора Ordinis team. Consumer не видит таких событий до их re-publishing — но это очень редкий сценарий. ## Топик для discovery В roadmap планируется `nsi.dictionary.changed` aggregator topic — single firehose для consumers которые хотят видеть все scope'ы в одном потоке (c фильтрацией consumer-side). Сейчас **нет**, подписывайся на 3 scope топика отдельно. ## Дальше - [Webhooks](webhooks.md) — alternative для consumer'ов без Kafka access - [Bitemporal](bitemporal.md) — как интерпретировать `validFrom`/`validTo` - [Caching](caching.md) — invalidation pattern с Kafka events