Files
Zimin A.N. bcfb07f547 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.
2026-05-07 23:27:03 +03:00

206 lines
6.6 KiB
Markdown
Raw Permalink 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.
# Стратегии кэширования
Ordinis оптимизирован под редкие записи и частые чтения. Кэшируй у себя,
не делай round-trip на каждый GET.
Три рекомендуемых стратегии:
## A. Простой TTL (рекомендуется для v1)
Самый простой подход — TTL 5 мин на per-`(dictionary, lang)` ключ.
### TanStack Query (React)
```ts
useQuery({
queryKey: ['ordinis', 'spacecraft', lang],
queryFn: () =>
fetch(`/api/reference/spacecraft?lang=${lang}`, {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json()),
staleTime: 5 * 60_000,
gcTime: 30 * 60_000,
retry: 3,
})
```
### Server-side кэш (на consumer side)
Любая стандартная in-memory cache library с TTL — Caffeine, Guava,
Redis с EXPIRE и т.д. Пример (Spring + Caffeine):
```java
@Cacheable(value = "ordinis-records", key = "#dictionary + ':' + #lang")
public List<Record> fetchRecords(String dictionary, String lang) {
return ordinisClient.list(dictionary, lang);
}
// application.yml
spring:
cache:
caffeine:
spec: maximumSize=10000,expireAfterWrite=5m
```
**Pro:** просто, работает out of the box.
**Con:** до 5 мин stale данных. При закрытии записи consumer покажет
её ещё 5 мин.
## B. Webhook / Kafka push (рекомендуется для prod)
Realtime invalidation через push-уведомления.
### Kafka
Подпишись на `ordinis.cuod.events.public` (см. [События](events.md)),
для каждого пришедшего event — invalidate соответствующий cache key:
```python
def on_event(event):
dict_name = event['dictionary']
business_key = event['businessKey']
redis.delete(f"ordinis:{dict_name}:list") # invalidate list
redis.delete(f"ordinis:{dict_name}:byKey:{business_key}")
```
### Webhook
Тот же pattern но на HTTP push (см. [Webhooks](webhooks.md)):
```javascript
app.post('/webhooks/ordinis', (req, res) => {
if (!verifyHmac(req)) return res.status(403).send()
for (const event of req.body.events) {
cache.delete(`ordinis:${event.dictionary}:list`)
cache.delete(`ordinis:${event.dictionary}:byKey:${event.businessKey}`)
}
res.status(202).send()
})
```
**Pro:** < 1s invalidation latency.
**Con:** требует Kafka/HTTPS infrastructure + handle duplicates (at-least-once).
## C. Snapshot at order time
Для аудит-кейсов когда нужен «как было на момент заказа» (legal compliance).
### Pattern
```sql
CREATE TABLE imaging_order (
id UUID PRIMARY KEY,
spacecraft_business_key VARCHAR(256),
instrument_business_key VARCHAR(256),
reference_snapshot JSONB, -- копии записей, не FK
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
При создании заказа:
```python
spacecraft_data = ordinis.get(f"/spacecraft/records/{key}")
instrument_data = ordinis.get(f"/instrument/records/{key}")
db.execute(
"INSERT INTO imaging_order(id, spacecraft_business_key, instrument_business_key, reference_snapshot) "
"VALUES (%s, %s, %s, %s)",
(order_id, spacecraft_key, instrument_key, json.dumps({
'spacecraft': spacecraft_data,
'instrument': instrument_data,
}))
)
```
**Pro:** гарантированная история, не зависит от Ordinis availability /
retention. Если Ordinis retroactively изменит запись — твоя копия не
сдвинется.
**Con:** дубликат данных, увеличение твоей БД. Для Альтум typical scale
(~100k orders/year × ~1KB snapshot = 100MB/year) — не проблема.
### Альтернатива: bitemporal query
Можно не делать копию, а позже запрашивать `?at=<order.created_at>`:
```bash
GET /api/v1/spacecraft/records/RESURS-P-3?at=2025-06-15T10:00:00Z
```
См. [Bitemporal](bitemporal.md). Pro: меньше storage. Con: depends on
Ordinis availability + retention. Альтум выбрала **snapshot** для legal
compliance.
## Hybrid: TTL + push invalidation
Лучший из двух — TTL 1 час (long enough чтобы сократить request rate) +
event-driven invalidation (для realtime updates):
```python
# Получаем event → invalidate
def on_event(event):
cache.delete(f"ordinis:{event['dictionary']}:list")
# При cache miss — refetch с TTL 1h
def get_records(dictionary, lang):
cached = cache.get(f"ordinis:{dictionary}:list:{lang}")
if cached:
return cached
fresh = ordinis_client.list(dictionary, lang)
cache.setex(f"ordinis:{dictionary}:list:{lang}", 3600, fresh)
return fresh
```
## Cache key patterns
| Что кэшируем | Ключ |
|---|---|
| List of dictionaries | `ordinis:dictionaries:{scope}` |
| List of records per dict | `ordinis:{dict}:list:{lang}` |
| Single record | `ordinis:{dict}:byKey:{businessKey}:{lang}` |
| History of record | `ordinis:{dict}:history:{businessKey}` (rare, TTL=24h) |
Включай `lang` в key — иначе разные locales перетирают друг друга.
## Invalidation на cascade close (v2 roadmap)
В будущем при cascade close источника emit'ятся events для всех
referencing records. Consumer должен слушать events и invalidate их тоже:
```python
def on_event(event):
cache.delete(f"ordinis:{event['dictionary']}:byKey:{event['businessKey']}")
# NEW v2: cascade source — заодно invalidate источник
if event.get('cascadeSource'):
src = event['cascadeSource']
cache.delete(f"ordinis:{src['dictionary']}:byKey:{src['businessKey']}")
```
Spec появится перед deployment v2.
## Stale cache на 5xx / timeout
Если Ordinis недоступен — отдавай старые данные с warn'ом, не падай каскадно:
```python
def get_records_safe(dictionary, lang):
try:
return get_records(dictionary, lang)
except (TimeoutError, HTTPError) as e:
log.warning(f"Ordinis unavailable, using stale cache: {e}")
stale = cache.get(f"ordinis:{dictionary}:list:{lang}", ignore_ttl=True)
if stale:
return stale
raise
```
Метрика `<consumer>_ordinis_stale_serves_total` для мониторинга.
## Дальше
- [Best practices](best-practices.md) — общие рекомендации
- [Bundle cuod](bundle-cuod.md) — что именно нужно кэшировать