# 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 + 10–30% 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 (ориентир 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 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:@redis:6379/0 REDIS_REALTIME_URL=redis://api_backend:@redis:6379/1 MESSAGE_SAFETY_REDIS_URL=redis://message_safety:@redis:6379/0 ``` Первые два URL существуют только на ВМ1. На ВМ2 `MESSAGE_SAFETY_REDIS_URL=redis://message_safety:@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.