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

7.3 KiB
Raw Blame History

События (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 = изменённые поля старой версии.
  • closedvalidTo обновлён в 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:

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