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/).
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
# События (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
|
||||
Reference in New Issue
Block a user