Новая документация-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/).
7.3 KiB
События (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):
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:
{
"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:
- Writer пишет запись в БД + outbox table в одной транзакции.
- OutboxPoller (scheduled @1s) читает unpublished rows, отправляет в Kafka.
- После Kafka ACK — отмечает row как published (
published_atset).
Что это значит для consumer'а:
- Можешь получить дубликат события если poller crashed после ACK но до
published_atwrite. Idempotency у consumer'а — must. - При network partition между Kafka и poller — сообщения накапливаются в
outbox (alert
OrdinisOutboxLagHighсрабатывает > 100 events).
Idempotency
Используй (dictionary, businessKey, txTime, version) как dedup key:
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 — alternative для consumer'ов без Kafka access
- Bitemporal — как интерпретировать
validFrom/validTo - Caching — invalidation pattern с Kafka events