bcfb07f547
Чёткое разделение audiences:
- docs/ — consumer-facing only, Diplodoc-built. Public contract для
интеграторов (Альтум, BI, third-party).
- docs-internal/ — operator runbooks, status snapshots, legacy tech pages.
НЕ в Diplodoc build, НЕ публикуются.
Изменения:
- Move docs/{ops,status,tech} → docs-internal/{ops,status,tech}
- docs/toc.yaml: убраны секции Operations, Архитектура (legacy), Status
- docs/index.md: убран tab "оператор / on-call", focus на integrators
- docs/README.md: rewritten — scope только integrator guide, editorial
guidelines (no leak internal architecture, stable API contract)
- .yfm: убран явный ignore status/* (теперь не нужно — папка переехала)
- docs-internal/README.md: index для внутренней документации с rationale
разделения
Scrubbing implementation details из Diplodoc страниц:
- events.md: убраны Strimzi/cluster.142 references, OutboxPoller детали,
internal alert names (OrdinisOutboxLagHigh, OrdinisOutboxDLQNonEmpty),
metric names (nsi_outbox_*). Заменены на consumer-observable contract.
- webhooks.md: убраны metric names (nsi_webhook_ssrf_rejected_total),
alert table (OrdinisWebhookHighFailureRate и т.д.). Retry policy
переписана на user-side perspective.
- errors.md: убран alert name OrdinisReadApiErrorBudgetBurnFast, Tempo
endpoint URL, internal connection details.
- x-references.md: OrphanReferenceScanner → general "Ordinis периодически
детектирует". Убраны metric/alert names. Cascade roadmap без link на
internal design doc.
- auth.md: убран file path ScopeContext.java.
- bundle-cuod.md: убран Maven module path.
- caching.md: cascade design link заменён на text reference.
CI:
- .gitlab-ci.yml: новый job build-docs (stage: build) — Node 20 alpine,
pnpm install --frozen-lockfile + pnpm build, artifact docs/_dist на
неделю. Triggers: auto на docs/**, либо manual web. Готов к follow-up
pages-deploy job когда host решён.
- Новый pattern .docs-changes для rules.
Build verified clean: 13 HTML pages, 6.9M, no broken links/refs.
162 lines
6.7 KiB
Markdown
162 lines
6.7 KiB
Markdown
# События (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.
|
||
|
||
## 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
|