docs(): added RUNBOOK, compliance matrix, ADR, refactored C4 and

repository map

zavoz
This commit is contained in:
ITQ
2026-02-24 13:17:24 +03:00
parent 7bf3ccee5c
commit 740fd2d7bd
18 changed files with 542 additions and 10 deletions
+7
View File
@@ -0,0 +1,7 @@
# 1. Цели архитектуры
1. Обеспечить воспроизводимый критичный поток `decide -> event -> report/guardrail` без ручных допущений в runtime.
2. Гарантировать безопасный lifecycle эксперимента: ревью, статусные переходы, блокировка конфликтных запусков.
3. Сохранить корректность данных для аналитики: идемпотентный приём событий, атрибуция через `decision_id`, обработка out-of-order через `PendingEvent`.
4. Поддержать операционную проверяемость: `health/readiness`, метрики, структурированные логи, фоновые задачи.
5. Дать артефакты, по которым жюри может пройти критерии `B1..B10` и выбранные `FX` без домысливания.
+26
View File
@@ -0,0 +1,26 @@
# 2. Контекст и границы
## Внутри системы
- Один backend-контейнер (Django + Django Ninja API).
- Доменные модули в `apps/*`, HTTP-слой в `api/v1/*`.
- Хранение: реляционная БД (PostgreSQL в целевом окружении, SQLite по умолчанию локально).
- Кэш/брокер: Redis/Valkey в целевом окружении, LocMem cache локально/в тестах.
- Фоновые задачи: Celery worker/beat (guardrails, notifications, cleanup pending).
## Внешние акторы
- Product client: вызывает `POST /api/v1/decide`, отправляет `POST /api/v1/events`.
- Experimenter/Approver/Admin/Viewer: управляют экспериментами и смотрят результаты через API.
- Каналы уведомлений: Telegram API и SMTP.
- Система наблюдаемости: скрейпинг `/metrics`, чтение структурированных логов.
## Служебные контракты
- `GET /health`: liveness.
- `GET /ready`: readiness (cache, db, storage, celery ping).
- `GET /metrics`: prometheus endpoint.
## Явная граница фактов
Документация привязана к текущей реализации в `src/backend`.
+42
View File
@@ -0,0 +1,42 @@
# 3. Драйверы и NFR
## Корректность и воспроизводимость
- Детерминированная раздача варианта через SHA-256 hash по `subject_id` и `experiment_id`.
- Один активный эксперимент на флаг (`running/paused`) через DB constraint.
- Строгие переходы lifecycle и блокировка недопустимых переходов.
- Идемпотентность событий по `event_id`.
## Safety
- Guardrails с порогом, окном наблюдения и автоматическим действием (`pause` или `rollback`).
- Ограничение участия пользователя: max concurrent + cooldown.
- Conflict domains для детерминированного разрешения коллизий экспериментов.
## Целостность аналитики
- Атрибуция через `decision_id`.
- Для событий с `requires_exposure=True`: без exposure событие уходит в `PendingEvent` и промотируется позже.
- Отчёт строится по вариантам и выбранным метрикам эксперимента.
## Эксплуатация
- Health/readiness probes.
- Prometheus-инструментация HTTP и бизнес-счётчиков.
- Structured logging с correlation id.
- Регламентные фоновые задачи Celery.
## Производительность
Реализовано:
- кэш флагов и активных экспериментов;
- кэш результата `decide` (TTL через `DECISION_RESULT_CACHE_TTL_SECONDS`);
- режим записи `Decision`: `sync|async|disabled` (`DECISION_WRITE_MODE`) с принудительным sync для `experiment_assigned`;
- async persistence через `events.persist_decision`;
- `reports` считает `average` через `Avg`, `percentile` через DB aggregate (`PERCENTILE_CONT` для PostgreSQL);
- в `reports` убрана materialization `decision_ids`, используется `Subquery`.
Текущие ограничения:
- в режиме `async` устойчивость записи зависит от здоровья Celery worker/broker;
- SQL-ветка percentile зависит от СУБД (PostgreSQL/не-PostgreSQL fallback);
- отдельный нагрузочный benchmark-артефакт не приложен.
+45
View File
@@ -0,0 +1,45 @@
# 4. Ключевые архитектурные решения
## Базовые решения
| ID | Решение | Зачем | Последствия |
|---|---|---|---|
| ADR-01 | Модульный monolith на Django + Django Ninja | Быстрый delivery и единая транзакционная модель | Вертикальное масштабирование, не микросервисы |
| ADR-02 | Разделение на `apps/*` и `api/v1/*` | Чёткие границы между domain и transport | Нужна дисциплина зависимостей |
| ADR-03 | Hash-based deterministic assignment в `decide` | Выполнение B2-4 (стабильность результата) | Чувствительность к качеству `subject_id` |
| ADR-04 | Review policy: `ApproverGroup` + fallback `ReviewSettings` | Выполнение требований по ревью и fallback | Политика плоская, без орг-иерархий |
| ADR-05 | Lifecycle как state machine + validators + DB constraint | Невалидные переходы и двойной запуск на флаг блокируются | Правила статусов централизованы |
| ADR-06 | Event ingestion с дедупом и `PendingEvent` | Идемпотентность и out-of-order атрибуция | Дополнительная сложность обработки pending |
| ADR-07 | Guardrails как first-class модели + auto action | Авто-реакция на деградацию и аудит триггера | Риск ложных срабатываний при шумных данных |
| ADR-08 | Conflict domains (`mutual_exclusion`, `priority`) | Детерминированное разрешение пересечений экспериментов | Дополнительные проверки на старте и в decide |
| ADR-09 | Базовая observability (`health`, `ready`, `/metrics`, JSON logs) | Проверяемость B9 и эксплуатационная диагностика | SLA readiness подтверждается только live-demo |
| ADR-10 | Проверка качества через `just` и автотесты | Воспроизводимая верификация B1/B8/B10 | - |
| ADR-11 | Кэш результата `decide` в cache backend (Redis/Valkey в целевом окружении) | Снизить CPU/DB в hot-path | Ниже latency на повторяющихся запросах; риск stale-ответа в рамках TTL |
| ADR-12 | Режим записи `Decision`: `sync\|async\|disabled`; async через `events.persist_decision`; `experiment_assigned` всегда sync | Снять write-pressure с hot-path без потери атрибуции | В `async` режиме нужна стабильность broker/worker и мониторинг очереди |
| ADR-13 | RBAC через роли `admin/experimenter/approver/viewer` + JWT bearer + endpoint guards | Выполнение требований доступа и ревью-ответственности из раздела 0 ТЗ | Вся авторизация централизована в role guards; нужен контроль качества секретов JWT в окружении |
| ADR-14 | Типизированные feature flags (`string/boolean/integer`) и публичный контракт обновления только `default_value` | Исключить несогласованные значения флагов и обеспечить безопасные переключения без релиза | Смена `key/value_type` решается созданием нового флага и миграцией использования |
| ADR-15 | Каталог типов событий (`EventType`) с `required_fields`, `is_exposure`, `requires_exposure` | Явно отделить схему событий от runtime-обработки и обеспечить корректную атрибуцию | Требуется дисциплина ведения каталога; неактивные типы событий блокируют ingestion |
| ADR-16 | Каталог метрик (`MetricDefinition`) + явная привязка метрик к эксперименту (`ExperimentMetric`) | Выполнение требований 5.4 ТЗ и единый расчёт для reports/guardrails | Изменение `calculation_rule` влияет на интерпретацию отчётов и требует управляемых изменений |
| ADR-17 | Завершение эксперимента через отдельную сущность `ExperimentOutcome` с обязательным `rationale` и валидируемыми outcome-сценариями | Выполнение требований 2.6 и B6-4/B6-5 | Решение по rollout/rollback/no_effect становится аудитируемым и воспроизводимым |
| ADR-18 | Уведомления как rule-based pipeline: `NotificationRule` -> `NotificationLog` -> async flush, с dedup/rate-limit по `event_key` | Закрыть FX notifications без “шторма” сообщений | Ограничение по каналам (`telegram`, `smtp`), требуется мониторинг failed delivery |
| ADR-19 | Поиск по эвристике среди Learnings | Реализовать поиск по базе | Поиск эвристический (веса/Jaccard), не ML и требует аккуратной интерпретации |
## Неочевидные инварианты
- В один момент времени на один `FeatureFlag` разрешён только один активный эксперимент (`running|paused`) через partial `UniqueConstraint`.
- После старта эксперимента заморожены поля, влияющие на раздачу (`traffic_allocation`, `targeting_rules`, `flag`), и запрещены изменения вариантов вне `draft`.
- Перед отправкой на review проверяются инварианты состава вариантов: минимум 2, ровно 1 control, сумма весов строго равна `traffic_allocation`.
- Один approver может оставить только одно одобрение по эксперименту (`unique_approval_per_user`).
- Для `requires_exposure=True` событие без exposure не атрибутируется сразу: уходит в `PendingEvent`, промотируется после прихода exposure, затем очищается по TTL.
- Одна метрика не может быть прикреплена к эксперименту дважды (`unique_experiment_metric`).
- `Learning` хранится в one-to-one связи с экспериментом.
## Ключевые риски
| Риск | Проявление | Смягчение | Остаток |
|---|---|---|---|
| Stale ответ из кэша `decide` | Кратковременный возврат устаревшего результата в пределах TTL | Revision-aware cache key + короткий TTL | Низкий/средний |
| Потеря throughput при проблемах Celery в `async` | Очередь растёт, запись `Decision` отстаёт | Режимы `sync|async|disabled`, force-sync для `experiment_assigned`, fallback на sync при ошибке enqueue | Средний |
| Тяжёлые запросы отчётов на больших данных | Рост latency для percentile/агрегаций | DB aggregate + `Subquery`, фильтрация attributed событий, индексы | Средний |
| Потеря отложенных атрибуций | `PendingEvent` истекает до прихода exposure | TTL 7 дней + cleanup + промоция при exposure | Средний |
| Сбой каналов уведомлений | Отправка не доходит в Telegram/SMTP | `NotificationLog` со статусами и ошибками | Средний |
+33
View File
@@ -0,0 +1,33 @@
# 5. Критичный путь
## Decide (`apps/decision/services.py`)
1. `flag` из cache/DB.
2. `active_experiment` из cache/DB.
3. Формируется cache key результата (`flag`, `subject`, digest атрибутов, ревизии `flag/experiment`).
4. При cache hit возвращается тот же outcome/reason/value с новым `decision_id`.
5. При cache miss выполняются проверки: running experiment, targeting, participation limits, domain conflicts, traffic allocation.
6. Если назначен вариант, выбирается детерминированно по hash + weights.
7. Результат может быть закэширован (по whitelist reason).
8. Запись `Decision` зависит от `DECISION_WRITE_MODE`: `sync|async|disabled`; для `experiment_assigned` запись всегда sync.
Reason-коды: `flag_not_found`, `no_active_experiment`, `targeting_mismatch`, `participation_limit`, `domain_conflict`, `outside_traffic_allocation`, `no_variants`, `experiment_assigned`.
## Events (`apps/events/services.py`)
1. Валидация типа события и payload.
2. Дедуп по `event_id`.
3. Exposure: `Exposure` + `Event`, затем промоция `PendingEvent`.
4. Conversion: при `requires_exposure=True` без exposure -> `PendingEvent` (TTL 7 дней), иначе attributed `Event`.
## Reports (`apps/reports/services.py`)
- По каждому варианту: `exposures`, `unique_subjects`, значения выбранных метрик.
- Поддерживается период `start/end`.
- `average` считает DB `Avg`, `percentile` - DB aggregate (`PERCENTILE_CONT` в PostgreSQL).
- Связь событий с экспозициями строится через `Subquery`, без materialization списка `decision_ids` в Python.
## Guardrails (`apps/guardrails/services.py`)
- Celery проверяет running эксперименты.
- При breach: `GuardrailTrigger` + действие (`pause`/`rollback`) + `ExperimentLog` + enqueue уведомления.
+34
View File
@@ -0,0 +1,34 @@
# 6. Эксплуатация и наблюдаемость
## Endpoints
- `/health` -> liveness (`health_check.Memory`).
- `/ready` -> readiness (`Cache`, `Database`, `Storage`, `Celery Ping`).
- `/metrics` -> Prometheus metrics.
SLA `ready <= 180s` подтверждается только live-demo.
## Метрики и логи
- Инфраструктурные метрики через `django-prometheus` middleware.
- Бизнес-счётчики: `lotty_decide_requests_total`, `lotty_events_ingested_total`.
- Production logs: JSON + `Correlation-ID` (`django-guid`).
## Celery задачи
Периодические:
- `guardrails.check_all` - 60s.
- `notifications.flush_pending` - 30s.
- `events.cleanup_expired_pending` - 3600s.
По запросу:
- `events.persist_decision` - асинхронная запись `Decision` при `DECISION_WRITE_MODE=async`.
## Runtime knobs
- `DECISION_RESULT_CACHE_TTL_SECONDS` (по умолчанию `60`).
- `DECISION_WRITE_MODE` (`sync|async|disabled`, по умолчанию `sync`).
## Команды
`cd src/backend && just run|test|test-coverage|lint|format`
+8
View File
@@ -0,0 +1,8 @@
# 7. Явные упрощения
1. Один backend-сервис (модульный monolith), без выделения микросервисов по доменам.
2. Отдельный statistical significance engine не реализован; решения фиксируются на метриках и guardrails.
3. Отчёты считаются синхронно в запросе, без pre-aggregation pipeline и materialized views (потому что нету ClickHouse).
4. Режим `DECISION_WRITE_MODE=sync` используется по умолчанию; `async`/`disabled` - эксплуатационные режимы.
5. Кэш результата `decide` основан на TTL и revision-aware ключе, без отдельной ручной инвалидции.
6. Similar learnings считаются эвристикой (Jaccard/веса), без ML-ранжирования.
+21
View File
@@ -0,0 +1,21 @@
# 8. Проверяемость
## Базовый набор
1. `cd src/backend && just --list`
2. `cd src/backend && just test`
3. `cd src/backend && just test-coverage && just show-coverage`
## Точечные проверки по доменам
1. `cd src/backend && uv run python manage.py test apps.decision.tests.test_decide`
2. `cd src/backend && uv run python manage.py test apps.events.tests.test_services tests.integration.test_events`
3. `cd src/backend && uv run python manage.py test apps.reports.tests.test_reports apps.guardrails.tests.test_guardrails`
4. `cd src/backend && uv run python manage.py test apps.reviews.tests.test_reviews_policy apps.experiments.tests.test_services`
5. `cd src/backend && uv run python manage.py test apps.conflicts.tests.test_conflicts apps.notifications.tests.test_notifications apps.learnings.tests.test_learnings`
## Runtime
- `GET /health` -> `200`
- `GET /ready` -> `200` после готовности зависимостей
- `GET /metrics` -> есть инфраструктурные и бизнес метрики
+16
View File
@@ -0,0 +1,16 @@
# ADR - LOTTY Backend
## Состав
| Файл | Содержание |
|---|---|
| `01-goals.md` | Цели архитектуры |
| `02-context.md` | Границы системы и акторы |
| `03-drivers-nfr.md` | Драйверы и NFR |
| `04-decisions.md` | Ключевые решения, неочевидные инварианты и ключевые риски |
| `05-critical-path.md` | Критичный поток `decide -> event -> report/guardrail` |
| `06-operations.md` | Эксплуатация и наблюдаемость |
| `07-simplifications.md` | Явные упрощения |
| `08-verification.md` | Как проверять критерии |
Связанная матрица соответствия: [compliance-matrix.md](../compliance-matrix.md).