47 lines
8.5 KiB
Markdown
47 lines
8.5 KiB
Markdown
# 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-риск снижен revision-aware cache key и инвалидацией `flag:{key}` при обновлении default |
|
||
| 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 связи с экспериментом.
|
||
- После `FeatureFlag.default_value` update удаляется cache key `flag:{key}`; следующий `decide` читает актуальный flag из DB.
|
||
|
||
## Ключевые риски
|
||
|
||
| Риск | Проявление | Смягчение | Остаток |
|
||
|---|---|---|---|
|
||
| Stale ответ из кэша `decide` | Кратковременный возврат устаревшего результата в пределах TTL | Revision-aware cache key + инвалидация `flag:{key}` при update default + короткий 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` со статусами и ошибками | Средний |
|