Правки от GPT
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
# module-04. Проектная спецификация Redis
|
||||
|
||||
> Статус: целевая спецификация Redis в едином Docker Compose MVP.
|
||||
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md).
|
||||
|
||||
## 1. Назначение и инварианты
|
||||
|
||||
Один Redis-контейнер предоставляет быстрые ephemeral функции трём логическим DB:
|
||||
|
||||
- DB0 — `api-backend`: idempotency fast layer и API rate limits;
|
||||
- DB1 — realtime и coordination;
|
||||
- DB2 — `message-safety` stub tasks/cache.
|
||||
|
||||
Redis не является бизнес-очередью, source of truth сообщений, sync tasks, audit, профилей или delivery checkpoint. Надёжные состояния остаются в managed PostgreSQL/S3. Потеря Redis может ухудшить сервис, но не должна создавать потерю подтверждённых сообщений либо дубль side effect: durable idempotency/outbox/checkpoint api-backend описаны в module-01.
|
||||
|
||||
OTP counters api-backend в Redis не хранит; они принадлежат Keycloak/SPI.
|
||||
|
||||
## 2. Версия и topology
|
||||
|
||||
Redis 7.x, image закреплён по digest. Одна primary instance на VM без replica/Sentinel в MVP. Клиенты используют connection pool, bounded timeouts и не выполняют опасные команды.
|
||||
|
||||
Logical DB — изоляция имён, не security boundary и не независимый memory quota. При росте или разных eviction/SLA DB2 и DB0 выносятся в отдельные instances.
|
||||
|
||||
## 3. Общие правила ключей
|
||||
|
||||
Формат: `han:{domain}:{purpose}:{hashed-or-public-id}:{version}`. Только ASCII lowercase separators. Public UUID допустим; IP, phone, email, token, text и filename — только HMAC/SHA-256 с server-side pepper там, где нужна защита dictionary attack.
|
||||
|
||||
- key length желательно ≤ 200 bytes;
|
||||
- значения versioned (`v=1`);
|
||||
- timestamps — Unix ms/seconds или RFC3339, формат фиксирован для каждого key;
|
||||
- wildcard `KEYS` production запрещён; только `SCAN` для ops;
|
||||
- каждый non-channel key имеет TTL, кроме явно обоснованных bounded structures;
|
||||
- large payload/presigned URL/token/message text запрещены.
|
||||
|
||||
## 4. DB0: API rate limiting
|
||||
|
||||
Примеры:
|
||||
|
||||
| Key | Тип/value | TTL |
|
||||
|---|---|---|
|
||||
| `han:api:rl:user:{user_id}:{route_hash}:{window}` | ZSET timestamps либо counter | window + jitter |
|
||||
| `han:api:rl:ip:{ip_hmac}:{route_hash}:{window}` | ZSET/counter | window + jitter |
|
||||
| `han:api:rl:dialog:{dialog_id}:message:{window}` | ZSET/counter | window + jitter |
|
||||
| `han:api:rl:service:{service}:{route_hash}:{window}` | counter/token bucket | window + jitter |
|
||||
|
||||
Алгоритм — atomic Lua/function: удалить старые entries, посчитать, добавить текущий request, установить expiry, вернуть `allowed`, `remaining`, `retry_after_ms`, `reset_at`. Для fixed window `INCR` и первый `EXPIRE` выполняются в одном script, чтобы не оставить бессрочный key.
|
||||
|
||||
Clock используется Redis `TIME` внутри script, а не client wall clock. Script загружается при startup, SHA кэшируется; после `NOSCRIPT` выполняется контролируемый reload. Route labels — bounded allow-list/hash, исключающий cardinality attack.
|
||||
|
||||
## 5. DB0: idempotency
|
||||
|
||||
| Key | Значение | TTL |
|
||||
|---|---|---|
|
||||
| `han:api:idem:{scope}:{user_id}:{key_hmac}` | HASH/MessagePack: state, fingerprint, status, sanitized response, resource id, version | 24h |
|
||||
| `han:api:idemlock:{scope}:{user_id}:{key_hmac}` | random owner token | 30s + heartbeat |
|
||||
|
||||
State transitions `absent → in_progress → completed`; fingerprint mismatch возвращает conflict. Создание/сравнение/lock выполняется Lua. Unlock/extend разрешены только если owner token совпадает (`compare-and-delete/expire` script).
|
||||
|
||||
Response не содержит tokens, cookies, presigned URL или PII. Transient 503/504 не фиксируется как окончательный completed. PostgreSQL `idempotency_records` — durable fallback; Redis — ускоритель. При cache loss API читает durable row и прогревает key.
|
||||
|
||||
## 6. DB1: realtime
|
||||
|
||||
| Key/channel | Формат | TTL |
|
||||
|---|---|---|
|
||||
| `han:rt:conn:{connection_id}` | HASH: user_id, instance, last_seen, subscriptions_count | 90s |
|
||||
| `han:rt:user:{user_id}:connections` | ZSET connection_id → heartbeat | 120s |
|
||||
| `han:rt:dialog:{dialog_id}` | Pub/Sub channel | нет хранения |
|
||||
| `han:rt:user:{user_id}` | Pub/Sub channel | нет хранения |
|
||||
|
||||
Heartbeat атомарно обновляет connection и membership; cleanup удаляет stale ZSET entries bounded batches. Pub/Sub — at-most-once notification. Payload содержит только event id/type/entity UUID и DTO, допустимый realtime контрактом; DB остаётся source of truth. После reconnect frontend всегда делает REST reconciliation.
|
||||
|
||||
Redis Streams не используются как бизнес queue. Если позже понадобится durable realtime replay, сначала меняется архитектура и выбирается PostgreSQL outbox/event broker.
|
||||
|
||||
## 7. DB1: coordination locks
|
||||
|
||||
| Key | TTL |
|
||||
|---|---|
|
||||
| `han:coord:lock:safety-recovery:{task_id}` | 30s |
|
||||
| `han:coord:lock:delivery:{message_id}` | 30s |
|
||||
| `han:coord:lock:settings-refresh:{instance}` | 30s |
|
||||
|
||||
Acquire: `SET key owner NX PX ttl`; extend/release — Lua compare owner. Worker обязан опираться также на PostgreSQL row lease/`FOR UPDATE SKIP LOCKED`; Redis lock — оптимизация, не единственная защита. Fencing token рекомендуется для внешнего side effect, а уникальные DB constraints/idempotency остаются финальной защитой.
|
||||
|
||||
## 8. DB2: Message Safety stub
|
||||
|
||||
| Key | Тип/value | TTL |
|
||||
|---|---|---|
|
||||
| `han:safety:task:{task_id}` | HASH/JSON v1: created, polls, optional seed/context | `MESSAGE_SAFETY_TASK_TTL_SEC` |
|
||||
| `han:safety:tasklock:{task_id}` | owner token | 5–30s |
|
||||
| `han:safety:rl:service:{caller}:{window}` | counter | window+jitter |
|
||||
| `han:safety:verdict:{content_hash}:{rules_version}` | optional cache | bounded technical TTL |
|
||||
|
||||
Для требуемой заглушки task — ephemeral contract state. Истечение task возвращает безопасный `404 task_not_found/expired` по internal error semantics. В production safety authoritative audit/cache может находиться в PostgreSQL `message_safety`; Redis DB2 не заменяет его.
|
||||
|
||||
Random verdict каждого GET по заданию независим; Redis хранит существование/TTL и счётчик polls для observability, но не предопределяет финал. В deterministic tests seed/RNG injected на уровне сервиса.
|
||||
|
||||
## 9. Serialization и limits
|
||||
|
||||
- простые counters — integer;
|
||||
- locks — opaque random 128-bit token;
|
||||
- metadata — Redis HASH либо компактный JSON с `schema_version`;
|
||||
- max value target 32 KiB, hard application guard 128 KiB;
|
||||
- response cache хранит только allow-listed sanitized JSON;
|
||||
- decode error считается cache miss, key удаляется/карантинируется и поднимается metric.
|
||||
|
||||
## 10. TTL policy
|
||||
|
||||
| Категория | TTL |
|
||||
|---|---|
|
||||
| idempotency completed | 24h по arch-02 |
|
||||
| idempotency in-progress lock | 30s, heartbeat bounded |
|
||||
| rate limit | window + 10–30% deterministic jitter |
|
||||
| realtime connection | 90s; set membership 120s |
|
||||
| coordination lock | 30s |
|
||||
| safety task | default 15m, обязательно > API poll max 300s + recovery margin |
|
||||
| safety cache | default 5–60m по rules version |
|
||||
|
||||
Новый key без TTL запрещён contract test, кроме Pub/Sub channel (не key) и ops metadata с явным обоснованием.
|
||||
|
||||
## 11. Atomicity и Lua governance
|
||||
|
||||
Scripts/functions хранятся в репозитории рядом с клиентом, versioned и тестируются на real Redis. Запрещены unbounded loops/SCAN внутри Lua. Входные массивы ограничены. Script timeout отслеживается; `SCRIPT KILL` runbook применяется только если нет writes либо после оценки.
|
||||
|
||||
Обязательные scripts:
|
||||
|
||||
- rate-limit evaluate;
|
||||
- idempotency reserve/complete/conflict;
|
||||
- lock release/extend;
|
||||
- realtime heartbeat/cleanup membership;
|
||||
- safety task get+increment poll при необходимости.
|
||||
|
||||
Redis transaction не координирует PostgreSQL/S3/HTTP. Cross-system consistency обеспечивается DB checkpoint/outbox и idempotent finalize.
|
||||
|
||||
## 12. Persistence
|
||||
|
||||
Решение MVP: AOF `appendonly yes`, `appendfsync everysec` плюс RDB snapshots (`save 900 1`, `300 100`, `60 10000` либо tuned). Это ускоряет восстановление ephemeral state, но не превращает Redis в authoritative store.
|
||||
|
||||
`aof-use-rdb-preamble yes`, automatic rewrite с порогами; volume `redis-data`. При corruption используется `redis-check-aof`/restore clean instance, а сервисы восстанавливают authoritative state из PostgreSQL.
|
||||
|
||||
RPO Redis до ~1 секунды приемлем, потому что бизнес-RPO задаётся PostgreSQL/S3. Backup Redis не обязателен для бизнес-восстановления, но периодическая копия RDB/AOF полезна для ops forensic без secrets.
|
||||
|
||||
## 13. Memory и eviction
|
||||
|
||||
`maxmemory` задаётся относительно container limit (ориентир 70–75%, оставляя overhead/fork). Начальная оценка для одной VM — 512 MiB, уточняется load test.
|
||||
|
||||
Eviction MVP: `volatile-lru`/`volatile-ttl`, так как все application keys имеют TTL. `allkeys-lru` опасен для idempotency при memory pressure; `noeviction` может полностью закрыть writes. Окончательный выбор после нагрузки: предпочтительно `volatile-lru` + alerts, а при разделении instances DB0 idempotency получает отдельную noeviction policy.
|
||||
|
||||
Контролируются `used_memory`, RSS, fragmentation, evicted_keys, expired_keys, key count/avg TTL по DB. OOM/eviction idempotency не создаёт дубль благодаря PostgreSQL fallback.
|
||||
|
||||
## 14. Sizing
|
||||
|
||||
Расчёт до production:
|
||||
|
||||
```text
|
||||
DB0 rate = peak identities × routes × active windows × bytes/key
|
||||
DB0 idem = mutating requests/24h × avg sanitized record
|
||||
DB1 = peak connections × connection metadata + Pub/Sub buffers
|
||||
DB2 = safety tasks within TTL × avg task metadata
|
||||
total × 1.5 allocator/fragmentation × 1.3 growth reserve
|
||||
```
|
||||
|
||||
Pub/Sub output buffers и slow consumers имеют hard/soft limits. Load test фиксирует peak RPS, WS connections, idempotency response size и AOF rewrite headroom.
|
||||
|
||||
## 15. Auth, ACL и network boundary
|
||||
|
||||
Redis не публикует `6379` на host, подключён только к Docker `backend`. `protected-mode yes`, bind container interface, default user отключён. ACL users:
|
||||
|
||||
- `api_backend`: DB0/DB1 key prefixes, нужные command categories;
|
||||
- `message_safety`: только DB2 prefixes;
|
||||
- `ops_health`: `PING`, ограниченный `INFO`;
|
||||
|
||||
Важно: Redis ACL не ограничивает logical DB напрямую надёжно; key-prefix patterns и разные credentials обязательны. `SELECT` запрещается, клиент URL сразу задаёт DB, но ACL prefix остаётся основной защитой.
|
||||
|
||||
Dangerous/admin commands (`FLUSHALL`, `FLUSHDB`, `CONFIG`, `MODULE`, broad KEYS`, replication changes) запрещены application users; rename-command не считается основной защитой.
|
||||
|
||||
Пароли сильные, только env/secret mount, rotation current/new через rolling deploy. Внутри одной VM TLS Redis опционален при закрытой Docker network; при выносе за host/VPC TLS обязателен (`rediss://`) и plaintext отключается.
|
||||
|
||||
## 16. Docker/runtime
|
||||
|
||||
```text
|
||||
redis/
|
||||
docker-compose.yml
|
||||
redis.conf
|
||||
users.acl.template
|
||||
scripts/
|
||||
tests/
|
||||
```
|
||||
|
||||
Compose: pinned Redis image, `expose: 6379`, без `ports`, `backend` network, `redis-data:/data`, config/ACL read-only, non-root UID, no-new-privileges, dropped capabilities, resource/memory/ulimit settings.
|
||||
|
||||
Startup валидирует config и ACL, permissions volume, затем Redis. Healthcheck использует ACL health user и `redis-cli --no-auth-warning PING`, secret не печатается. Graceful stop timeout позволяет AOF flush.
|
||||
|
||||
URL:
|
||||
|
||||
```text
|
||||
REDIS_URL=redis://api_backend:<secret>@redis:6379/0
|
||||
REDIS_REALTIME_URL=redis://api_backend:<secret>@redis:6379/1
|
||||
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/2
|
||||
```
|
||||
|
||||
Добавление credential env требует обновления arch-04 `.env.example`; до этого имена credential variables — TBD, URL может содержать injected secret.
|
||||
|
||||
## 17. Health и degraded behavior
|
||||
|
||||
`PING` проверяет liveness Redis; readiness приложений проверяет auth, correct DB и выполнение малого read/write/expire script без оставления key.
|
||||
|
||||
При Redis недоступен:
|
||||
|
||||
- message send, attachment init и download URL api-backend fail-closed `503`, если нельзя безопасно применить лимит/idempotency;
|
||||
- completed idempotency восстанавливается из PostgreSQL;
|
||||
- profile/history GET могут работать под edge limits;
|
||||
- public GET использует bounded local conservative limiter/cache;
|
||||
- realtime cross-instance publish/coordination деградирует; REST/polling остаётся source of truth;
|
||||
- safety stub для digit task не может гарантировать GET task state — check возвращает `503`, а существующие task GET — `503`; синхронные text allow/deny могут работать только если policy явно разрешает Redis-independent path;
|
||||
- internal inbox не теряется из-за Redis, так как durable receipt в PostgreSQL.
|
||||
|
||||
При latency выше threshold clients используют short timeout/circuit, не создают бесконечные retry storms. Reconnect — exponential backoff+jitter.
|
||||
|
||||
## 18. Backup и restore
|
||||
|
||||
Redis backup не используется для бизнес restore. Runbook:
|
||||
|
||||
1. остановить/изолировать corrupted instance;
|
||||
2. при целостном AOF/RDB восстановить на отдельном instance и проверить;
|
||||
3. иначе поднять пустой Redis;
|
||||
4. api-backend прогревает idempotency по durable records, realtime восстанавливается reconnect/polling;
|
||||
5. незавершённые safety tasks обрабатываются по service semantics/expire; api-backend durable `safety_tasks` сообщает dependency error/recovery.
|
||||
|
||||
Не копировать Redis dump в небезопасное место: keys содержат UUID и hashed identifiers.
|
||||
|
||||
## 19. Metrics и alerts
|
||||
|
||||
- availability, commands/sec, latency percentiles;
|
||||
- connected/blocked clients, rejected connections;
|
||||
- memory/RSS/fragmentation, maxmemory ratio;
|
||||
- evictions/expirations/keyspace hits/misses;
|
||||
- AOF fsync latency/rewrite status/last save;
|
||||
- replication metrics зарезервированы;
|
||||
- key count/avg TTL по DB без key values;
|
||||
- script errors/NOSCRIPT/slowlog;
|
||||
- rate limit decisions, idempotency hit/conflict/fallback;
|
||||
- Pub/Sub subscribers/output buffer/slow disconnect;
|
||||
- safety task create/get/expire.
|
||||
|
||||
Alerts: unavailable, p99 latency, >80/90% memory, any sustained evictions, AOF error, no recent persistence, client buffer pressure, unexpected keys without TTL.
|
||||
|
||||
## 20. Тесты
|
||||
|
||||
- ACL: каждый service видит только свой prefix/commands;
|
||||
- порт 6379 недоступен с host/public network;
|
||||
- rate Lua concurrency и exact Retry-After;
|
||||
- idempotency same/different fingerprint, lock ownership, expiry, Redis loss + PostgreSQL fallback;
|
||||
- realtime heartbeat cleanup, duplicate disconnect, Pub/Sub loss + REST recovery;
|
||||
- locks expiry/late owner/fencing;
|
||||
- safety task TTL, concurrent polls и missing task;
|
||||
- `NOSCRIPT` reload;
|
||||
- all application keys имеют TTL;
|
||||
- max value/invalid serialization;
|
||||
- restart with AOF/RDB, corrupted AOF rehearsal, empty restore;
|
||||
- memory pressure/eviction и no duplicate business side effect;
|
||||
- network partition, latency, reconnect backoff;
|
||||
- logs/metrics не содержат secret/value/PII.
|
||||
|
||||
## 21. Definition of Done
|
||||
|
||||
- DB0/DB1/DB2 roles и prefixes реализованы;
|
||||
- Lua scripts atomic, bounded, versioned и покрыты real Redis tests;
|
||||
- idempotency 24h и durable fallback доказаны;
|
||||
- realtime loss восстанавливается REST;
|
||||
- Safety DB2 task TTL превышает poll/recovery budget;
|
||||
- AOF/RDB, volume, restart и clean-instance recovery проверены;
|
||||
- maxmemory/eviction/resource limits основаны на load test;
|
||||
- ACL users и network isolation работают, порт не published;
|
||||
- health/degraded policies реализованы в clients;
|
||||
- dashboards/alerts/runbook готовы;
|
||||
- Redis не используется как `sync_queue`, delivery queue, message/audit source of truth или OTP store.
|
||||
|
||||
## 22. Решения, допущения и TBD
|
||||
|
||||
**Решения:** один instance/три DB MVP; AOF everysec + RDB; Pub/Sub best effort; PostgreSQL durable fallback; prefix ACL; все application keys с TTL.
|
||||
|
||||
**Допущения:** одна VM и одна replica API на старте; Redis loss допустим без потери business truth.
|
||||
|
||||
**TBD:** R1 точный maxmemory после load profile; R2 eviction policy после измерений; R3 credential env names в arch-04; R4 Safety task TTL/recovery margin; R5 TLS при изменении network topology; R6 момент разделения DB на instances; R7 RPO/RTO ops target.
|
||||
Reference in New Issue
Block a user