docs: split integrator guide (Diplodoc) vs internal docs + add build-docs CI
Чёткое разделение 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.
This commit is contained in:
@@ -69,7 +69,7 @@ Ordinis использует трёхуровневый доступ к слов
|
||||
|
||||
Альтернативный синтаксис ролей (для compat с другими сервисами в realm):
|
||||
суффикс `-PUBLIC|INTERNAL|RESTRICTED` (например `altum-PUBLIC`,
|
||||
`geoportal-INTERNAL`). Маппинг определяется в `ordinis-auth/.../ScopeContext.java`.
|
||||
`geoportal-INTERNAL`). Маппинг встроен в auth layer Ordinis.
|
||||
|
||||
{% note info %}
|
||||
|
||||
|
||||
@@ -77,8 +77,9 @@ graph LR
|
||||
I --supportedLevelCodes--> PL
|
||||
```
|
||||
|
||||
(Это subset, для full графа см. отдельные schemas в коде:
|
||||
`ordinis-cuod-bundle/src/main/resources/cuod/schemas/`.)
|
||||
(Это subset. Полный граф можно получить через `GET /dictionaries` +
|
||||
inspection schemas через `GET /dictionaries/{name}` — каждый возвращает
|
||||
JSON Schema с x-references маркерами.)
|
||||
|
||||
## Типичные сценарии
|
||||
|
||||
|
||||
@@ -24,7 +24,10 @@ useQuery({
|
||||
})
|
||||
```
|
||||
|
||||
### Server-side кэш (Caffeine + Spring)
|
||||
### Server-side кэш (на consumer side)
|
||||
|
||||
Любая стандартная in-memory cache library с TTL — Caffeine, Guava,
|
||||
Redis с EXPIRE и т.д. Пример (Spring + Caffeine):
|
||||
|
||||
```java
|
||||
@Cacheable(value = "ordinis-records", key = "#dictionary + ':' + #lang")
|
||||
@@ -170,14 +173,13 @@ referencing records. Consumer должен слушать events и invalidate
|
||||
def on_event(event):
|
||||
cache.delete(f"ordinis:{event['dictionary']}:byKey:{event['businessKey']}")
|
||||
|
||||
# NEW v2: cascade source — заодно invalidate dependents
|
||||
# NEW v2: cascade source — заодно invalidate источник
|
||||
if event.get('cascadeSource'):
|
||||
# Источник тоже нужно invalidate (мы видим только закрытие dependent)
|
||||
src = event['cascadeSource']
|
||||
cache.delete(f"ordinis:{src['dictionary']}:byKey:{src['businessKey']}")
|
||||
```
|
||||
|
||||
См. design doc `dict-relationships-v2` (внутренний artifact).
|
||||
Spec появится перед deployment v2.
|
||||
|
||||
## Stale cache на 5xx / timeout
|
||||
|
||||
|
||||
@@ -97,9 +97,9 @@ Ordinis следует стандартам REST с расширениями:
|
||||
| Code | Описание |
|
||||
|---|---|
|
||||
| `internal_unexpected` | Server bug — escalate с traceId |
|
||||
| `db_unavailable` | Postgres недоступен |
|
||||
| `vault_unavailable` | Vault sealed либо unreachable |
|
||||
| `kafka_publish_failed` | Outbox не смог publish (но запись в БД OK — eventually consistent) |
|
||||
| `db_unavailable` | БД недоступна — retry с long backoff |
|
||||
| `dependent_unavailable` | Зависимый сервис (config, secrets) недоступен — retry |
|
||||
| `event_publish_failed` | Запись в БД ОК, но события не отправлены — eventually consistent (events доедут позже) |
|
||||
|
||||
## Rate limits
|
||||
|
||||
@@ -114,9 +114,8 @@ limits через resource constraints:
|
||||
| Concurrent JWT verifications | bounded heap (~5k/sec практический максимум) |
|
||||
|
||||
При превышении (типично DDoS-like patterns):
|
||||
- HTTP `429 Too Many Requests` после ingress rate limit (ingress nginx default).
|
||||
- HTTP `429 Too Many Requests` после ingress rate limit.
|
||||
- HTTP `503 Service Unavailable` при exhausting connection pool.
|
||||
- `5xx error rate spike` → alert `OrdinisReadApiErrorBudgetBurnFast`.
|
||||
|
||||
### Recommended client-side limits
|
||||
|
||||
@@ -155,13 +154,14 @@ def retry_with_backoff(
|
||||
## Tracing
|
||||
|
||||
Каждый response имеет `X-Trace-Id` header и `traceId` field в error body.
|
||||
Ordinis эмитит OpenTelemetry traces в Tempo (`tempo.monitoring:4317`).
|
||||
|
||||
При эскалации Ordinis team:
|
||||
1. Скопируй `traceId` из response либо логов.
|
||||
2. Создай issue с воспроизведением.
|
||||
3. Ordinis team query'ит Tempo: `traceId="abc123-..."`.
|
||||
4. Trace покажет full path: ingress → read-api → postgres → response.
|
||||
3. Ordinis team поднимет trace по этому ID — full path запроса виден.
|
||||
|
||||
Без `traceId` investigation занимает существенно больше времени —
|
||||
обязательно сохраняй его при ошибках.
|
||||
|
||||
## Дальше
|
||||
|
||||
|
||||
@@ -12,53 +12,27 @@ Ordinis публикует все изменения справочников в
|
||||
| `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 дней).
|
||||
Параметры топиков (для consumer config):
|
||||
|
||||
- `cleanup.policy: delete`
|
||||
- `retention.ms: 604800000` (7 дней)
|
||||
- Multi-partition (точное число — спросить Ordinis team при registration,
|
||||
partition count может меняться по operational reasons).
|
||||
|
||||
## Подключение
|
||||
|
||||
### Адреса bootstrap
|
||||
### Bootstrap address + creds
|
||||
|
||||
| Среда | 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 |
|
||||
Запросить у Ordinis team:
|
||||
- Bootstrap servers (внутренний DNS на per-environment basis)
|
||||
- SASL credentials (SCRAM-SHA-512 username + password)
|
||||
- Topic ACL (Read + Describe на нужные топики)
|
||||
- Consumer group prefix (для group ACL)
|
||||
|
||||
### Получение SASL креденшелов
|
||||
Auth: **SASL/SCRAM-SHA-512**. TLS опциональный (отдельный listener).
|
||||
|
||||
Запросить у 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.
|
||||
Креденшелы выдаются per-consumer service. Не share между сервисами —
|
||||
ACL и audit за каждым.
|
||||
|
||||
## Schema события
|
||||
|
||||
@@ -143,18 +117,17 @@ each businessKey изолирован.
|
||||
|
||||
## 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).
|
||||
Ordinis использует **outbox pattern** — изменение БД и публикация в Kafka
|
||||
атомарны через transactional outbox. Гарантия: **at-least-once**.
|
||||
|
||||
Что это значит для consumer'а:
|
||||
|
||||
- Можешь получить **дубликат** события если poller crashed после ACK но до
|
||||
`published_at` write. Idempotency у consumer'а — must.
|
||||
- При network partition между Kafka и poller — сообщения накапливаются в
|
||||
outbox (alert `OrdinisOutboxLagHigh` срабатывает > 100 events).
|
||||
- Можешь получить **дубликат** события (rare, при partial failures на
|
||||
стороне Ordinis). Idempotency у consumer'а — must.
|
||||
- При недоступности Kafka события накапливаются в Ordinis side и
|
||||
доставляются после восстановления — **events не теряются**.
|
||||
- Order — гарантирован per partition key (`dictionary:businessKey`),
|
||||
не глобально.
|
||||
|
||||
## Idempotency
|
||||
|
||||
@@ -167,13 +140,12 @@ if not seen:
|
||||
redis.setex(f"ordinis:event:...", 86400, "1")
|
||||
```
|
||||
|
||||
## DLQ (Dead-letter queue)
|
||||
## Persistent failures (Ordinis side)
|
||||
|
||||
Если событие не публикуется > 1000 retry попыток (network issues,
|
||||
Kafka unavailable) — переходит в DLQ table в Ordinis. Не теряется, но
|
||||
требует ручного разбора.
|
||||
|
||||
Симптом: alert `OrdinisOutboxDLQNonEmpty` → check `/api/v1/admin/audit?action=outbox.dlq`.
|
||||
Если событие не публикуется > 1000 retry попыток (multi-day Kafka outage),
|
||||
оно складывается в DLQ внутри Ordinis. Не теряется, но требует ручного
|
||||
разбора Ordinis team. Consumer не видит таких событий до их
|
||||
re-publishing — но это очень редкий сценарий.
|
||||
|
||||
## Топик для discovery
|
||||
|
||||
|
||||
@@ -50,8 +50,7 @@ Webhook URLs валидируются перед отправкой:
|
||||
- **Запрещено:** non-HTTPS scheme. Только `https://`.
|
||||
- **Запрещено:** URL > 2048 chars.
|
||||
|
||||
Метрика `nsi_webhook_ssrf_rejected_total` increments при попытках. См.
|
||||
prometheus rule `OrdinisWebhookSsrfRejected`.
|
||||
При нарушении registration возвращает 422 + список причин.
|
||||
|
||||
## Payload
|
||||
|
||||
@@ -155,10 +154,11 @@ Consumer должен ответить `2xx` в течение 10 секунд:
|
||||
| 4 | через 10 мин |
|
||||
| 5 | через 1ч |
|
||||
| 6-10 | каждые 6ч |
|
||||
| 1000+ | DLQ → ручной разбор |
|
||||
| После всех попыток | Ordinis team помечает delivery как failed для ручного разбора |
|
||||
|
||||
После 1000 неудач delivery попадает в DLQ (`/api/v1/admin/webhooks/deliveries/dlq`).
|
||||
Alert: `OrdinisWebhookDLQNonEmpty`.
|
||||
Если consumer недоступен > 1000 retry попыток — Ordinis team будет
|
||||
расследовать. Долгосрочное unavailability стоит coordinate (planned
|
||||
maintenance window) чтобы не накапливать orphan deliveries.
|
||||
|
||||
## Test ping
|
||||
|
||||
@@ -196,19 +196,8 @@ Response:
|
||||
сложить в очередь.
|
||||
3. **HMAC verify первым** — до парсинга JSON.
|
||||
4. **Metrics** — track received / processed / errors для своего side.
|
||||
Сравнивай с Ordinis metrics при расследовании.
|
||||
5. **Whitelist Ordinis IP** на firewall если строгие правила. IP cluster.142
|
||||
= `192.168.100.142`, но в production будет ingress IP. Спросить у Ordinis
|
||||
team при registration.
|
||||
|
||||
## Alerting (Ordinis side)
|
||||
|
||||
| Alert | Trigger |
|
||||
|---|---|
|
||||
| `OrdinisWebhookHighFailureRate` | failure rate > 50% за 5 мин |
|
||||
| `OrdinisWebhookDLQNonEmpty` | events в DLQ > 0 |
|
||||
| `OrdinisWebhookDispatcherIdle` | 0 deliveries при > 0 active subscriptions |
|
||||
| `OrdinisWebhookSsrfRejected` | SSRF guard сработал > 0 раз за 15 мин |
|
||||
5. **Whitelist Ordinis IP** на firewall если строгие правила. Точный
|
||||
адрес запросить у Ordinis team при registration.
|
||||
|
||||
## Дальше
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ PUBLIC scope, ReferenceValidator возвращает тот же error
|
||||
(`x_references_target_not_found`) что и на real not-found. Ordinis **не leak'ает**
|
||||
existence of out-of-scope dictionary через differential errors.
|
||||
|
||||
## Orphan reference scanner
|
||||
## Orphan references
|
||||
|
||||
Возможна ситуация когда referenced record **закрывается**, а referencing
|
||||
record остаётся:
|
||||
@@ -78,19 +78,20 @@ record остаётся:
|
||||
3. spacecraft.RESURS-P-3 теперь имеет dangling FK
|
||||
```
|
||||
|
||||
Periodic scanner (`OrphanReferenceScanner`, runs каждые 5 мин) находит
|
||||
такие orphan'ы и emit'ит metric:
|
||||
Ordinis периодически детектирует такие orphan'ы серверной стороной — данные
|
||||
**не корректируются автоматически** (может быть intentional state до
|
||||
update referencing записей), но Ordinis team получает уведомление и
|
||||
может coordinate fix с data stewards.
|
||||
|
||||
```
|
||||
nsi_orphan_references_total{source_dict="spacecraft",target_dict="satellite_type"} 1
|
||||
```
|
||||
|
||||
Alert `OrdinisOrphanReferences` срабатывает при > 0 за 10 мин.
|
||||
Consumer-side defensiveness:
|
||||
- При получении event `record.closed` для записи на которую ты ссылаешься —
|
||||
invalidate свой cache + либо fetch свежую версию referencing
|
||||
записи (если она тоже обновилась), либо handle dangling FK явно.
|
||||
|
||||
## Cascade rules — roadmap (v2)
|
||||
|
||||
Сегодня закрытие источника НЕ closes referencing automatically. Это
|
||||
дolжно явно делаться через bulk close либо individual updates.
|
||||
дoлжно явно делаться через bulk close либо individual updates.
|
||||
|
||||
В v2 планируется schema extension `x-references-on-close`:
|
||||
|
||||
@@ -106,21 +107,22 @@ Alert `OrdinisOrphanReferences` срабатывает при > 0 за 10 мин
|
||||
|
||||
Опции:
|
||||
- **block** — закрытие источника невозможно пока есть active referencing (default).
|
||||
- **warn** — закрытие проходит, в response warning + metric `nsi_cascade_warnings_total`.
|
||||
- **cascade** — все referencing закрываются в той же транзакции.
|
||||
- **warn** — закрытие проходит, в response warning.
|
||||
- **cascade** — все referencing закрываются в той же транзакции, отдельные
|
||||
events emit'ятся per record.
|
||||
|
||||
См. design doc `dict-relationships-v2` (внутренний artifact в
|
||||
`~/.gstack/projects/claude/`) для полного scope.
|
||||
Конкретный timeline и финальная семантика будут опубликованы перед
|
||||
deployment v2 — следи за анонсами от Ordinis team.
|
||||
|
||||
## Lineage discovery — roadmap (v2)
|
||||
|
||||
Сегодня нет endpoint для "что depends on X". Admin должен вручную проходить
|
||||
schemas чтобы найти ссылки. В v2 планируется:
|
||||
Сегодня нет endpoint для "что depends on X". В v2 планируется:
|
||||
|
||||
- `GET /api/v1/dictionaries/{name}/dependents` — словари которые ссылаются.
|
||||
- `GET /api/v1/records/{dict}/{key}/dependents` — конкретные записи.
|
||||
|
||||
См. design выше для подробностей.
|
||||
Это позволит consumer'ам и UI узнавать связи без чтения всех schemas
|
||||
руками. Spec будет опубликован перед deployment v2.
|
||||
|
||||
## Best practices
|
||||
|
||||
|
||||
Reference in New Issue
Block a user