Files
mdm-ordinis/docs/integration/api/events.md
T
Zimin A.N. d5268b9395 docs: Diplodoc setup + полное руководство интеграции по API
Новая документация-as-code на платформе Diplodoc (Yandex open-source) с
YFM (Yandex Flavored Markdown). Build → static HTML, deployable куда угодно.

Что добавлено:
- package.json + .yfm + toc.yaml — Diplodoc setup, навигация для sidebar
- index.md — главная страница с tabs для разных audiences
- README.md — инструкция для contributors

Раздел integration/api/ — 11 страниц про интеграцию consumers:
- index — обзор + глоссарий
- auth — Keycloak service accounts, scope mapping
- read-endpoints — GET /dictionaries, list/by-key, spatial filters
- events — Kafka outbox топики, partition keys, idempotency, DLQ
- webhooks — HMAC, SSRF guard, retry policy, test ping endpoint
- bitemporal — validFrom/validTo + history, future-dated entries, bulk close/export
- x-references — FK syntax, validation errors, scope hiding, orphan scanner, v2 cascade preview
- caching — TTL / push / snapshot strategies, hybrid pattern
- errors — full error code reference (validation/auth/conflict/server), retry strategy
- best-practices — 33 numbered rules для consumers
- bundle-cuod — все 40 справочников с FK графом и сценариями

Обновлены:
- ops/README.md — link на новую integration/api/, убран inline <br>
- ops/slo.md — broken cross-repo links заменены на text references

Build:
  cd docs && pnpm install && pnpm build
  → ./_dist (20 HTML files, 7.1M)
  pnpm serve  # → http://localhost:8088

CI deploy job для GitLab Pages — в roadmap (см. docs/README.md).

Существующий tech/api-integration.md оставлен в toc как legacy
(содержимое мигрировано + расширено в integration/api/).
2026-05-07 23:19:02 +03:00

190 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# События (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`. |
Topology: 1 broker (single-node на cluster.142), 3+ partitions per topic
(historical setup имел 12 partitions, при cluster recreate 2026-05-07
spec задал 3; existing topics annotated `strimzi.io/managed=false`,
broker partitions сохранены), `cleanup.policy: delete`,
`retention.ms: 604800000` (7 дней).
## Подключение
### Адреса bootstrap
| Среда | Bootstrap servers | Auth |
|---|---|---|
| prod | `ordinis-kafka-kafka-bootstrap.ordinis-prod:9092` (TLS+SASL: `:9093`) | SCRAM-SHA-512 |
| staging | `ordinis-kafka-kafka-bootstrap.ordinis-staging:9092` | SCRAM-SHA-512 |
### Получение SASL креденшелов
Запросить у Ordinis team создание Kafka user (Strimzi `KafkaUser` CR):
```yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaUser
metadata:
name: altum-consumer
namespace: ordinis-prod
labels:
strimzi.io/cluster: ordinis-kafka
spec:
authentication:
type: scram-sha-512
authorization:
type: simple
acls:
- resource:
type: topic
name: ordinis.cuod.events.public
patternType: literal
operations: [Read, Describe]
- resource:
type: group
name: altum-consumer-
patternType: prefix
operations: [Read]
```
Strimzi user-operator создаст Secret `altum-consumer` с полями `username`,
`password`. Используй их в Kafka client config.
## 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 семантика
Outbox pattern гарантирует **at-least-once** delivery:
1. Writer пишет запись в БД + outbox table в одной транзакции.
2. OutboxPoller (scheduled @1s) читает unpublished rows, отправляет в Kafka.
3. После Kafka ACK — отмечает row как published (`published_at` set).
Что это значит для consumer'а:
- Можешь получить **дубликат** события если poller crashed после ACK но до
`published_at` write. Idempotency у consumer'а — must.
- При network partition между Kafka и poller — сообщения накапливаются в
outbox (alert `OrdinisOutboxLagHigh` срабатывает > 100 events).
## 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")
```
## DLQ (Dead-letter queue)
Если событие не публикуется > 1000 retry попыток (network issues,
Kafka unavailable) — переходит в DLQ table в Ordinis. Не теряется, но
требует ручного разбора.
Симптом: alert `OrdinisOutboxDLQNonEmpty` → check `/api/v1/admin/audit?action=outbox.dlq`.
## Топик для 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