Files
mdm-ordinis/docs/integration/api/caching.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

204 lines
6.6 KiB
Markdown
Raw 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 кэш (Caffeine + Spring)
```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 dependents
if event.get('cascadeSource'):
# Источник тоже нужно invalidate (мы видим только закрытие dependent)
src = event['cascadeSource']
cache.delete(f"ordinis:{src['dictionary']}:byKey:{src['businessKey']}")
```
См. design doc `dict-relationships-v2` (внутренний artifact).
## 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) — что именно нужно кэшировать