Решение кейса индивидуального тура для олимпиады PROD (2026), 93/100 баллов, 3 место среди 100+ участников
Матрица соответствия критериям - в CRITERIA.md
Развернуть карту репозитория
Структура проекта и назначение основных каталогов и файлов.
onetwo/
├── onetwo/ # Основное приложение (Litestar + Dishka)
│ ├── app.py # Точка входа, сборка Litestar, контроллеры, провайдеры
│ ├── config.py # Конфигурация приложения
│ │
│ ├── analytics/ # Аналитика и метрики
│ │ ├── domain/ # Парсер выражений метрик → SQL, типы, протоколы
│ │ ├── ports/ # EventReader, запросы к ClickHouse
│ │ ├── services/ # MetricService
│ │ ├── provider.py # DI-модуль Analytics
│ │ └── public.py # Публичный API модуля
│ │
│ ├── autopilot/ # Автопилот и guardrails
│ │ ├── domain/ # Протоколы правил
│ │ ├── controllers/ # POST /autopilot/runIteration
│ │ ├── ports/ # Cooldown (интервал итераций)
│ │ ├── services/ # AutopilotRunner — запуск правил по экспериментам
│ │ └── provider.py # DI-модуль Autopilot
│ │
│ ├── experiments/ # Проекты, эксперименты, флаги, ревью, отчёты
│ │ ├── domain/ # Типы, переходы статусов, права, ревью, ошибки
│ │ ├── ports/ # Репозитории (project, experiment, event_type, metric, review, history, …), ProjectCache, tokenizer
│ │ ├── controllers/ # CRUD проектов, флагов, типов событий, метрик, экспериментов, ревью, отчёты, guards
│ │ ├── services/ # Event types, History
│ │ ├── provider.py # DI-модуль Experiments
│ │ └── public.py # Публичный API модуля
│ │
│ ├── iam/ # Аутентификация и профиль
│ │ ├── domain/ # Типы, протоколы, ошибки
│ │ ├── ports/ # UserRepo, PasswordHasher, модели
│ │ ├── controllers/ # Auth (register/login), Profile
│ │ ├── middlewares.py # AuthenticationMiddleware
│ │ └── provider.py # DI-модуль IAM
│ │
│ ├── notify/ # Уведомления (email, Mattermost)
│ │ ├── domain/ # Протоколы, типы
│ │ ├── ports/ # Email, Mattermost
│ │ ├── services/ # Notifier (проверка правил, отправка)
│ │ └── provider.py # DI-модуль Notify
│ │
│ ├── runtime/ # Решение по флагам и приём событий
│ │ ├── domain/ # Decision (алгоритм выбора экспериментов), события, типы, протоколы, ошибки
│ │ ├── ports/ # ProjectCache, Tokenizer (PASETO), EventWriter, EventMetaWriter, EventDeduplicator, CacheNotifier
│ │ ├── controllers/ # requestFlags (decision), events (приём батчей)
│ │ ├── middlewares.py # RuntimeAuthMiddleware (project token)
│ │ └── provider.py # DI-модуль Runtime
│ │
│ └── shared/ # Общие компоненты
│
├── playbooks/ # E2E-тесты (сценарии для человека)
│ ├── __main__.py # Запуск: python -m playbooks [фильтр] [--base-url] [--delay]
│ ├── build.py # Сборка фикстур (проект, пользователь, эксперимент, события, …)
│ ├── fixture.py # Реестр фикстур, типы запросов
│ ├── require.py # Assert-хелперы для HTTP
│ ├── types.py # Типы фикстур (ProjectFixture, ExperimentFixture, …)
│ └── scenarios/ # Playbook-сценарии
│
├── tests/ # Unit- и интеграционные тесты (pytest)
│ ├── conftest.py # Общие фикстуры, маркеры (with-environment)
│ ├── utils.py
│ ├── analytics/ # Парсер метрик, чтение метрик, отчёты
│ ├── autopilot/ # Runner, сценарии автопилота
│ ├── experiments/ # Проекты, флаги, типы событий, метрики, эксперименты, ревью, переходы
│ ├── iam/ # Auth, профиль
│ ├── notify/ # Email-уведомления
│ ├── runtime/ # Decision, events
│ └── shared/ # Healthcheck, validation error
│
├── scripts/ # Вспомогательные скрипты
│
├── media/ # Документация и артефакты
│ ├── logo.png
│ ├── CODESTYLE.md # Стиль кода
│ └── adr/ # Архитектурные решения (ADR)
│
├── prometheus/ # Конфиг сбора метрик
│ └── prometheus.yml
├── nginx/ # Конфиг nginx (прокси к backend и сервисам)
│ └── default.conf
Кратко по модулям приложения:
| Модуль | Назначение |
|---|---|
| analytics | Выражения метрик → SQL, запросы к ClickHouse, отчёты |
| autopilot | Периодический запуск правил по экспериментам (traffic stages, guardrails, пауза) |
| experiments | Проекты, роли, feature flags, типы событий, метрики, эксперименты, ревью, отчёты, guards |
| iam | Регистрация, логин, профиль, middleware аутентификации |
| notify | Правила уведомлений по истории эксперимента → Email / Mattermost |
| runtime | Выдача флагов (requestFlags), приём событий (events), PASETO decision token, кэш проектов |
| shared | Healthcheck, ошибки, ответы, DI (config, транзакции), кэш-нотификации, метрики Prometheus |
Ожидается, что у вас установлены:
Опционально можно установить uv, пакетный менеджер от Astral.
С uv:
uv venvБез uv:
python3 -m venv .venvsource .venv/bin/activateДля операционных систем, отличных от Linux, эта команда может отличаться. Точная команда выводится в консоль после выполнения шага 1.
С uv:
uv syncБез uv:
pip install -r requirements/prod.txt -r requirements/dev.txtcp .env.dist .env
Опционально можно заменить значения переменных AUTH_TOKEN_KEY, PROJECT_TOKEN_KEY и DECISION_TOKEN_KEY
на свои (hex, случайные 64 символа). Текущие значения готовы к использованию.
docker compose up -dПриложение будет доступно по адресу http://localhost:80/api. Документация - по адресу http://localhost:80/docs.
Остальные сервисы:
- Mailpit: http://localhost/mailpit
- Метрики Prometheus (сырой эндпоинт): http://localhost/metrics
- Prometheus (UI и хранение метрик): http://localhost/prometheus/
Метрики приложения доступны по адресу http://localhost/metrics/.
Собирается:
- количество запросов;
- количество ошибок;
- количество принятых событий -
onetwo_runtime_events_accepted_total.
В docker-compose определен контейнер firewall, который является однонаправленным прокси от хоста к контейнерам. Контейнеры, кроме firewall и nginx, не имеют сетевого доступа к хосту. Это гарантирует выполнение требования из ТЗ про отсуствие доступа к интернету во время эксплуатации решения.
Для тестов настроен CI, coverage экспортируется в интерфейс GitLab. Пайплайн находится в .gitlab-ci.yml.
pytest --cov=onetwoЭта команда выводит coverage всех тестов в конце. Coverage локально будет ниже, чем в CI - на уровне 80%. В CI тесты поднимают окружение, локально - нет.
Еще есть e2e тесты (make playbooks). Подробнее - в конце README.
Для линтинга и форматирования кода используется ruff. Дополнительно как type checker используется ty. Конфигурация находится в pyproject.toml.
Запуск линтинга, форматирования кода и type checking:
make lintЗапуск только форматирования кода:
ruff formatОбратите внимание, что эти команды требуют активного виртуального окружения.
Проекты - контейнер для всех сущностей сервиса. У проектов есть уникальный slug.
В проектах есть ролевая система. После создания проекта пользователь получает роль ADMIN.
Доступные роли:
- ADMIN: полное управление проектом, создание event types, feature flags, метрик, назначение пользователей
- EXPERIMENTER: создание и управление экспериментами
- REVIEWER: ревью (апрув/реджект) экспериментов
- VIEWER: только read-only доступ к эксперименту.
Точное разделение прав по ролям находится в onetwo/experiments/domain/permissions.py. Можно назначать людей на одну или несколько ролей (один и тот же человек может и создавать, и ревьюить эксперименты, если у него есть роли EXPERIMENTER и REVIEWER). Считается, что у человека есть доступ к проекту, если у него есть хотя бы одна роль.
Отношение "ключ" (Identifier) - "значение" (строка с любым значением). Создаются и управляются администратором.
В контексте экспериментов, значение флага в проекте - дефолтное (контрольное) значение флага в любом эксперименте с этим флагом.
Объединение сущностей с единой целью - изменить значение feature flag для части пользователей и измерить изменения в метриках.
Конфигурация эксперимента делятся на две части:
- настройки эксперимента;
- options.
ExperimentOptions - это те параметры эксперимента, которые непосредственно влияют на пользователя:
- флаг;
- варианты и их веса (контрольные варианты отмечаются флагом isDefault, их значение всегда будет равно значению флага в проекте; сумма весов равна 1);
- таргетинг;
- приоритет.
Они выделены в отдельную сущность. Их можно изменять только в статусе DRAFT. Все изменения в Options логируются.
Остальные настройки эксперимента:
- метрики;
- параметры автопилота и guardrails;
- параметры нотификаций;
- доля трафика.
Их можно изменять, поскольку они:
- не влияют непосредственно на метрики и пользователей (от увеличения общей доли распределение по вариантам не изменится);
- есть достаточное количество сценариев, в которых эти параметры потребуется менять после запуска (ускорить/замедлить раскатку; добавить больше уведомлений; ...)
Эксперимент не привязывается к конкретному человеку. Над ним может работать кто угодно (хоть вся команда), если у них есть соответствующие права в проекте.
stateDiagram
DRAFT: DRAFT
REVIEW: REVIEW
RUNNING: RUNNING
PAUSED: PAUSED
FINISHED: FINISHED
ARCHIVED: ARCHIVED
[*] --> DRAFT
DRAFT --> REVIEW: Отправить на ревью
REVIEW --> DRAFT: Вернуть на доработку
REVIEW --> RUNNING: Запустить
RUNNING --> PAUSED: Остановить
PAUSED --> RUNNING: Возобновить
PAUSED --> FINISHED: Завершить
RUNNING --> FINISHED: Завершить
FINISHED --> ARCHIVED: Архивировать
ARCHIVED --> [*]
Находясь в статусе RUNNING или PAUSED, эксперименту можно задать Resolution. Это краткий вывод по эксперименту - какой эффект наблюдаем (POSITIVE / NO_EFFECT / NEGATIVE) и комментарий. Без заполненного Resolution нельзя перевести эксперимент в статус FINISHED.
Эксперименты в статусе FINISHED автоматически переходят в ARCHIVED спустя 7 дней после завершения.
Изменять статус можно только с правом на управление экспериментами.
При переходе в статус REVIEW эксперименту назначается ревью - поле review_id становится непустым.
У ревью (не у эксперимента) есть 3 статуса:
- APPROVED - набрано нужное количество вердиктов APPROVE и нет ни одного REJECT
- REJECTED - хотя бы один ревьюер оставил вердикт REJECT
- PENDING - не выполнено ни одно из условий выше
Количество апрувов для ревью указывается в настройках проекта (review_approves_required) и регулируется
администратором.
Люди с правом на ревью (это роли REVIEWER или ADMIN) могут оставить один из трех вердиктов:
- APPROVE;
- COMMENT (требуется комментарий);
- REJECT (требуется комментарий).
Пользователи с правом на ревью могут ревьюить любые эксперименты - в том числе свои.
Процесс ревью не влияет на статус проекта (не двигает его с REVIEW в RUNNING или DRAFT). Я считаю, что это две разные плоскости, которыми управляют разные группы людей. Например, так работает ревью в GitHub или GitLab. К тому же, это дает больше гибкости: если проект набрал нужное количество апрувов, но все еще находится в статусе REVIEW, можно заблокировать его вердиктом REJECT - например, если высшее руководство приняло такое решение.
При переходе из REVIEW в DRAFT и обратно процесс ревью перезапускается, и все вердикты нужно будет выставить заново. Но все действия с ревью логируются + сущность "ревью" не удаляется, что дает возможности для аудита.
Я считаю, что все запросы к моему сервису будут приходить в закрытом контуре от других сервисов.
Поэтому для корректной идентификации проекта (и связанных event types, метрик и экспериментов)
запросы нужно сопровождать project token из POST /projects/{projectId}/issueToken
в Authorization Bearer заголовке.
POST /runtime/requestFlags принимает в запросе user_id, user_attributes, flag_names.
user_id: любая строка, идентифицирующая конечного пользователя - для детерминированности флаговuser_attributes: словарь "строка" к "строке"flag_names: список из запрошенных флагов
Пользователи проверяются на соответствие attributesQuery из Experiment Options.
attributesQuery - выражение на языке rule-engine (Python-like синтаксис). Контекст вычисления - словарь user_attributes (ключи атрибутов становятся "переменными" в выражении).
Операторы: сравнение ==, !=, >, <, >=, <=; сопоставление со строкой по regex - =~; логика and, or.
Примеры:
country == "RU"- только пользователи из РФfirst_name == "Luke" and email =~ ".*@rebels.org$"- комбинация равенства и regex по emaildate == d'1970-01-01'- сравнение с датой (литералd'YYYY-MM-DD')
Если запрос не задан, ограничения по атрибутам нет.
onetwo/runtime/domain/decision.py
- Выбираются эксперименты:
- статус RUNNING, PAUSED или FINISHED (да, finished тоже)
- соответствие атрибутов пользователя - таргетингу;
- murmurhash3 от user_id + experiment_id попадает в нужную долю traffic_share.
Такие эксперименты считаются выбранными (selected).
-
Эксперименты сортируются по убыванию приоритета, при равенстве - по ID для детерминированности.
-
Выбираются первые
experiments_per_userэкспериментов (из настроек проекта). Они считаются экспериментами, которые влияют (affected) на пользователя. -
Пропускаются эксперименты в статусах PAUSED или FINISHED. Оставшиеся считаются экспериментами, в которых пользователь участвует (participating).
-
Для каждого эксперимента выбирается вариант в соответствии с их весами - аналогично шагу 1, с использованием murmurhash3.
-
Выбранные эксперименты кодируются в decision_token и отправляются пользователю вместе с флагами. decision_token закодирован при помощи PASETO (JWT-like tokenizer), при помощи которого можно восстановить выбранные эксперименты из токена.
Такой подход обеспечивает:
-
детерминированность и stickiness - при одинаковых входных данных результат будет один и тот же, поскольку сам алгоритм является stateless
-
идемпотентность по определению
-
высокую скорость - нет обращений к базе данных, только к кэшу в Redis
Также реализовано требование на защиту пользователя от постоянного участия в экспериментах.
На пользователя может одновременно влиять не больше experiments_per_user экспериментов.
Влиящими так же считаются эксперименты, которые недавно завершились - в статусе FINISHED.
Таким образом, если эксперимент, в котором участвовал пользователь, завершился, то еще
некоторое время он будет на него влиять - пользователь не будет видеть другие эксперименты.
Этим поведением можно управлять через приоритеты: если появится новый эксперимент с приоритетом выше, то он будет влиять вместо старого. Так можно легко запускать "очень важные" эксперименты на всю аудиторию, не переживая из-за ее нехватки.
В проектах можно создавать event types. Они фиксируют определенный перечень атрибутов, которые можно в них передать при отправке событий. На каждый тип события создается отдельная таблица в ClickHouse для эффективной записи и чтения.
Ручка POST /runtime/events принимает в себя батчем события. В них входят eventId, eventType, sentAt и атрибуты события.
Батчем можно отправить события только по одному decision token.
Для эффективности чтения метрик в проекте используется ClickHouse. Каждое событие хранится в своей структурированной таблице.
ID пользователя извлекается из decision token. Он, как и извлеченные ID пользователя и выбранные эксперименты, хранится в таблице вместе с атрибутами.
Валидация событий происходит в момент приемки решений.
В целях эффективности данные в ClickHouse записываются батчем. В момент записи батча происходит дедубликация всех событий на окне 7 дней через Redis.
Обратите внимание, что дублицированные события будут приняты так же, как и все остальные
- но они не попадут в базу данных и не повлияют на метрики.
Над типами событий можно создавать метрики. Метрика включает в себя выражение, которое компилируется в SQL и выполняется в ClickHouse.
Выражение метрики - это Python-подобное выражение (парсится через ast.parse в режиме eval).
Верхний уровень обязательно должен быть вызовом одной из агрегирующих функций.
Источники данных:
- Источник событий - идентификатор типа события (event type), зарегистрированного в проекте. Пример:
click,signup,purchase. - Атрибут события - обращение к атрибуту типа события в виде
источник.атрибут. Пример:view.latency,purchase.amount. Атрибут должен быть объявлен в типе события.
Агрегирующие функции (верхний уровень):
| Функция | Аргумент | Описание |
|---|---|---|
count(x) |
источник событий | Количество событий. По атрибуту вызывать нельзя |
mean(x) |
атрибут (источник.атрибут) |
Среднее значение атрибута |
sum(x) |
атрибут | Сумма значений атрибута |
pNN(x) |
атрибут | Перцентиль (NN - число от 1 до 99). Примеры: p50, p95, p99 |
Для метрик вида "сколько кликов произошло среди тех, кто получил экспозицию" используются логика subset.
В функцию агрегации (count / mean / sum / ...) нужно передать одну из двух функций:
count_in(subset, источник, окно)- количество событий указанного источника в окне относительно subsetcount_unique_in(subset, источник, окно)- количество уникальных пользователей (по user_id), у которых было хотя бы одно такое событие в окне
В качестве subset может быть:
subset_first(источник)- первое событие определённого типа для пользователя (например, первоеsignup)subset_last(источник)- последнее событие определённого типа
Окно времени:
within('7d')- рассчитывает окно от точки первого/последнего до +7 днейbetween('0d', '1d')- рассчитывает окно с 0-го по 1-й день от точки якоря (например, последние сутки). Полезно для retention
count(click)- число кликовmean(view.latency)- среднее latency по событиямviewsum(purchase.amount)- сумма поляamountпо событиямpurchasep50(view.latency)- медиана latencymean(count_unique_in(subset_first(signup), click, within('7d')))- конверсия из регистрации в клик в течение 7 днейcount(count_in(subset_last(purchase), view, between('0d', '1d')))- количество просмотров в последние сутки от последней покупки (на пользователя)
При подсчете временных событий система ориентируется на поле sentAt из входных данных.
Значение метрики можно "снять" по запросу на POST /projects/{project_id}/metrics/{metric_name}/query.
Параметры:
- timeFrom, timeTo - временное окно (обязательно)
- experimentId - искать только среди тех, кто участвовал в эксперименте
- variantName - искать только среди тех, кто участвовал в таком-то варианте
В настройках эксперимента можно указать метрики, которые будут отображаться в отчетах. Указывается название метрики в проекте и окно в секундах, за которое будет считаться метрика.
Получить отчет можно на POST /experiments/{experiment_id}/report. Указывается timeFrom и timeTo,
за который будет строиться отчет.
Возвращается список метрик эксперимента, значение во всем эксперименте и значение в каждой из метрик.
В моем решении guardrails объединены с автопилотом. Автопилот можно представить себе как компонент сервиса, задача которого сводится к управлению настройками проекта по заданным пользователям правилам. Под это определение так же попадают и guardrails.
Автопилот состоит из набора правил. Каждое правило состоит из:
- запроса;
- периода ожидания (сколько секунд должно пройти с момента срабатывания любого другого правила, прежде чем сработает это);
- приоритета (если срабатывает несколько правил - выбирается только одно, остальные игнорируются на период ожидания);
- действия.
Доступные действия:
- NEXT_STAGE - перейти на следующую ступень traffic_share
- PREVIOUS_STAGE - перейти на предыдущую ступень traffic_share, или поставить эксперимент на паузу
- PAUSE - поставить эксперимент на паузу и откатить всех пользователей к контольному варианту
- LOG - ничего не делать, только сделать запись в лог
При срабатывании правила вне зависимости от действия в History эксперимента появляется запись о срабатывании правила.
История эксперимента доступна в GET /experiments/{experiment_id}/history
У автопилота есть 4 статуса:
- ACTIVE - разрешены все действия
- ONLY_NEGATIVE - разрешены только негативные действия (PREVIOUS_STAGE / PAUSE)
- ONLY_LOG - нельзя предпринимать никаких действий, только сделать запись в аудит
- PAUSED - правила не могут срабатывать
Ступени traffic_share так же задаются в настройках автопилота - в trafficStages.
Приоритет guardrails над правилами раскатки реализуется через priority: если сработает правило с приоритетом выше, остальные не сработают.
Синтаксис запроса эквивалентен синтаксису запросов к атрибутам (rule-engine, Python-подобный). В качестве контекста (доступных переменных) доступны значения метрик в каждом из вариантов:
- Имя метрики - значение метрики по всему эксперименту (все варианты). Пример:
exposure_count,conversion_rate - Имя варианта.имя метрики - значение метрики в конкретном варианте. Пример:
control.conversion_rate,treatment.revenue
Примеры правил:
exposure_count >= 5- перейти на следующую стадию трафика, когда набрано достаточно экспозиций (например, для раскатки)control.conversion_rate < 0.01- поставить эксперимент на паузу, если в контроле конверсия упала ниже порога (guardrail)treatment.revenue_per_user > control.revenue_per_user * 1.1- следующая стадия, если в варианте выручка на пользователя выше контроля более чем на 10%exposure_count >= 100 and treatment.ctr > control.ctr- срабатывание при достаточной выборке и лучшем CTR в вариантеcontrol.p95_latency > 500- пауза при превышении p95 latency в контроле (guardrail)
В целях тестирования доступен внутренний эндпоинт POST /autopilot/runIteration
- запускает итерацию автопилота без ожидания (интервал в production режиме - 5-15 секунд)
В настройках эксперимента можно задать правила уведомлений (notifyRules). При каждой записи в историю эксперимента (см. GET /experiments/{experiment_id}/history) проверяются все правила; при совпадении запроса с контекстом события отправляется уведомление в указанный канал.
Каждое правило уведомлений задаётся полями:
- query - выражение на языке rule-engine (тот же синтаксис, что для таргетинга и правил автопилота);
- channel - канал доставки:
emailилиmattermost; - target - адрес назначения: для
email— адрес почты получателя; дляmattermost— имя канала в команде (channel name), указанной в конфиге (mattermost_team). Личные сообщения в Mattermost не поддерживаются
Контекст для запроса формируется из типа записи истории и её содержимого: в него входят переменное type и все поля из value записи.
Типы записей истории (type), при которых можно слать уведомления:
| type | Описание |
|---|---|
options_updated |
Изменены options эксперимента (флаг, варианты, таргетинг и т.п.) |
traffic_share_updated |
Изменена доля трафика |
status_updated |
Изменён статус эксперимента (в value передаётся новый статус) |
review_updated |
Обновлено ревью (вердикт, комментарий) |
autopilot_triggered |
Сработало правило автопилота (в value - query, action, priority, comment и т.д.) |
В ответе истории у каждой записи есть поле notifyStatus: notified (хотя бы одно правило сработало и отправка прошла успешно), error (правило сработало, но отправка завершилась с ошибкой), not_matched (ни одно правило не подошло).
Примеры правил:
type == "status_updated"- уведомлять при любой смене статуса экспериментаtype == "autopilot_triggered"- уведомлять при срабатывании автопилотаtype == "autopilot_triggered" and action == "pause"- уведомлять только когда автопилот ставит эксперимент на паузуtype == "review_updated"- уведомлять при обновлении ревью (новый вердикт)
В целях тестирования, из коробки настроен инстанс Mailpit и Mattermost:
- http://localhost/mailpit;
- http://localhost/mattermost (логин root, пароль rootpass1)
Для горизонтального масштабирования backend можно развернуть стек в Docker Swarm. Используется конфиг docker-compose.swarm.yml, который задаёт число реплик backend и политики обновления/отката
docker swarm init
docker stack deploy -c docker-compose.yml -c docker-compose.swarm.yml onetwoПо умолчанию backend запускается в 3 реплики. Изменить число реплик backend:
docker service scale onetwo_backend=5Просмотр сервисов стека:
docker stack services onetwoЭто возможно, поскольку backend не имеет состояния - он только обращается к другим сервисам (PostgreSQL, Redis, ClickHouse).
C4Context
title System Context - OneTwo Platform
Person(apiClient, "API Client", "Запрос флагов, отправка событий, отчёты")
System_Boundary(platform, "OneTwo Platform") {
System(backend, "Backend API", "Единое Litestar-приложение")
}
System_Ext(postgres, "PostgreSQL", "Эксперименты, проекты, метаданные")
System_Ext(redis, "Redis", "Кэш проектов, дедупликация событий")
System_Ext(clickhouse, "ClickHouse", "Хранилище событий")
System_Ext(mattermost, "Mattermost", "Уведомления")
System_Ext(smtp, "SMTP (Mailpit)", "Уведомления")
Rel(apiClient, backend, "HTTPS")
Rel(backend, postgres, "SQL")
Rel(backend, redis, "Redis")
Rel(backend, clickhouse, "ClickHouse")
Rel(backend, mattermost, "API")
Rel(backend, smtp, "SMTP")
### C4 Context L2
```mermaid
C4Container
title Container - Backend
Person(apiClient, "API Client", "Клиент платформы")
Container_Boundary(backend, "Backend API") {
Container(runtime, "Runtime", "Litestar", "Решение по флагам (decide), приём и запись событий (event), токен решения")
Container(experiments, "Experiments", "Litestar", "Проекты, эксперименты, отчёты, права доступа (guard), кэш проектов")
Container(analytics, "Analytics", "Litestar", "Метрики, запросы к событиям")
Container(iam, "IAM", "Litestar", "Аутентификация и профиль")
Container(notify, "Notify", "Litestar", "Email, Mattermost")
Container(autopilot, "Autopilot", "Litestar", "Периодический запуск итераций экспериментов")
}
ContainerDb(postgres, "PostgreSQL", "PostgreSQL", "Данные экспериментов")
ContainerDb(redis, "Redis", "Redis", "Кэш, дедупликация")
ContainerDb(clickhouse, "ClickHouse", "ClickHouse", "События")
Rel(apiClient, runtime, "requestFlags, events")
Rel(apiClient, experiments, "report, CRUD")
Rel(apiClient, iam, "auth")
Rel(runtime, experiments, "ProjectCache")
Rel(experiments, analytics, "MetricService")
Rel(runtime, redis, "кэш, дедупликация")
Rel(runtime, clickhouse, "запись событий")
Rel(experiments, postgres, "репозитории")
Rel(experiments, redis, "кэш проектов")
Rel(analytics, clickhouse, "чтение событий")
C4Component
title Component - Критичный путь decide -> event -> report/guardrail
Container_Boundary(backend, "Backend API") {
Component(decisionCtrl, "DecisionController", "Litestar", "POST /runtime/requestFlags, make_decision, токен")
Component(eventsCtrl, "EventsController", "Litestar", "POST /runtime/events, валидация, запись событий")
Component(reportsCtrl, "ReportsController", "Litestar", "POST .../report, отчёт по метрикам")
Component(guard, "ProjectPermissionGuard", "Litestar", "Проверка прав доступа к проекту")
Component(projectCache, "ProjectCache", "Port", "Проект, флаги, эксперименты, типы событий")
Component(metricService, "MetricService", "Analytics", "get_value по метрикам эксперимента")
Component(eventReader, "EventReader", "Port", "Чтение событий из хранилища")
}
ContainerDb(clickhouse, "ClickHouse", "ClickHouse", "События")
Rel(decisionCtrl, projectCache, "get_project, list_feature_flags, list_experiments")
Rel(eventsCtrl, projectCache, "list_event_types")
Rel(eventsCtrl, clickhouse, "EventWriter, EventMetaWriter")
Rel(reportsCtrl, guard, "project_permission_guard")
Rel(reportsCtrl, metricService, "get_value")
Rel(metricService, eventReader, "read_metric_value")
Rel(eventReader, clickhouse, "запросы")
Playbooks - end-2-end тесты, которые позволяют проверить функциональность приложения в легко воспринимаемом для человека виде. Они находятся в папке playbooks/.
Для запуска требуется поднять приложение.
python -m playbooksМожно переопределить base url через --base-url,
добавить задержку между запросами через --delay
или выбрать фильтр по сценариям, указав их как позиционный аргмент.
Для удобной навигации матрица соответствия критериям находится в файле CRITERIA.md
В папке media/adr/ находятся принятые архитектурные решения.
Я пользовался LLM при написании:
- матрицы соответствия критериям;
- ADR;
- тесты (tests/ и playbooks/);
- реализации адаптеров (репозитории и запросы в ClickHouse).

