Files
han-app/modules/module-04-redis.md
T

290 lines
20 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.
# module-04. Проектная спецификация Redis
> Статус: целевая спецификация Redis для двух Compose-контуров; legacy DB2 stub описан только до cutover.
> Источники: [`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 разделён по deployment/security boundary:
- Redis ВМ1: DB0 (`api-backend` idempotency/rate) и DB1 (realtime/coordination);
- Redis Safety ВМ2: отдельный instance для hot cache, rate limiting и optional worker wake-up;
- legacy DB2 ВМ1 существует только для test stub v1 до cutover и после него удаляется.
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. На каждой VM одна нужная primary instance без replica/Sentinel в MVP. Клиенты используют connection pool, bounded timeouts и не выполняют опасные команды.
Logical DB — изоляция имён, не security boundary. Safety уже вынесен в отдельный instance ВМ2; DB0/DB1 остаются на ВМ1.
## 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. Redis Safety ВМ2
| Key | Тип/value | TTL |
|---|---|---|
| `han:safety:rl:service:{caller}:{window}` | counter | window+jitter |
| `han:safety:text:{analysis_hash}:{rules_version}` | hot text-rules result, monitor rule ids без raw text | active config, seed ≤48h |
| `han:safety:verdict:{content_hash}:{config_version}:{detector_bundle}` | hot file verdict cache | active config, seed ≤30d |
| `han:safety:link:{url_hash}:{rules_version}:{config_version}` | stable local policy cache | active config, seed ≤48h |
| `han:safety:dns:{host_hash}:{rrtype}` | DNS answer; classification повторяется под текущей policy | actual TTL, active hard max seed 900s |
| `han:safety:wakeup` | Pub/Sub notification only | no storage |
PostgreSQL `message_safety.safety_tasks` — единственный queue/lease source (`FOR UPDATE SKIP LOCKED`, fencing generation). Redis не хранит authoritative task state, locks или leases. Cache loss/restart безопасно восстанавливается из PostgreSQL; Redis outage не выключает core Safety.
Legacy v1 stub может временно использовать DB2 ВМ1 для random task state. Этот namespace не используется production v2 и удаляется вместе со stub.
## 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 + 1030% deterministic jitter |
| realtime connection | 90s; set membership 120s |
| coordination lock | 30s |
| safety file hot cache | ≤30d; authoritative row/version в PostgreSQL |
| safety text-rules cache | 48h; invalidation by rules version |
| safety stable link policy cache | 48h |
| safety DNS cache | actual DNS TTL, hard max 900s |
Новый 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 (ориентир 7075%, оставляя 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
Redis Safety = hot verdict/link/DNS entries + rate windows + Pub/Sub buffers
each instance 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 и подключены только к local Docker `backend` своей VM. `protected-mode yes`, default user отключён. ACL users:
- `api_backend`: DB0/DB1 key prefixes, нужные command categories;
- `message_safety`: только Redis Safety 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/0
```
Первые два URL существуют только на ВМ1. На ВМ2 `MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/0`; credential доставляется secret file и не входит в общий `.env`.
## 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;
- production Safety продолжает task claim/poll через PostgreSQL; hot cache/rate/wakeup деградируют и прогреваются после восстановления Redis;
- legacy stub v1 может стать недоступным при потере своей DB2 до cutover;
- 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. production safety tasks продолжают обрабатываться из PostgreSQL; Redis Safety прогревается лениво.
Не копировать 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 hot-cache hit/miss, DNS TTL cap и wakeup subscribers.
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 cache loss/rebuild, DNS TTL cap и доказательство отсутствия task/lease state в Redis;
- `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.
**Допущения:** по одной Redis instance на ВМ1/ВМ2 и одна Safety API replica на старте; 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.