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

20 KiB
Raw Blame History

module-04. Проектная спецификация Redis

Статус: целевая спецификация Redis для двух Compose-контуров; legacy DB2 stub описан только до cutover.
Источники: README.md, arch-00-glossary.md, arch-01-system-architecture.md, arch-02-api-contracts.md, arch-03-docker-compose-blueprint.md, arch-04-settings-and-content.md, arch-05-agent-development-process.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:

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

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:

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.