Skip to content

iamnalinor/prod26-onetwo

Repository files navigation

OneTwo - платформа для проведения A/B тестов

Решение кейса индивидуального тура для олимпиады 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

А. Пререквизиты

Ожидается, что у вас установлены:

  1. Python 3.14 + pip
  2. Docker + docker compose
  3. Make
  4. Git

Опционально можно установить uv, пакетный менеджер от Astral.

B. Настройка окружения

1. Создание виртуального окружения

С uv:

uv venv

Без uv:

python3 -m venv .venv

2. Активация виртуального окружения

source .venv/bin/activate

Для операционных систем, отличных от Linux, эта команда может отличаться. Точная команда выводится в консоль после выполнения шага 1.

3. Установка зависимостей

С uv:

uv sync

Без uv:

pip install -r requirements/prod.txt -r requirements/dev.txt

4. Настройка переменных окружения

cp .env.dist .env

Опционально можно заменить значения переменных AUTH_TOKEN_KEY, PROJECT_TOKEN_KEY и DECISION_TOKEN_KEY на свои (hex, случайные 64 символа). Текущие значения готовы к использованию.

C. Команды

1. Запуск

docker compose up -d

Приложение будет доступно по адресу http://localhost:80/api. Документация - по адресу http://localhost:80/docs.

Остальные сервисы:

Prometheus

Метрики приложения доступны по адресу http://localhost/metrics/.

Собирается:

  • количество запросов;
  • количество ошибок;
  • количество принятых событий - onetwo_runtime_events_accepted_total.

Firewall

В docker-compose определен контейнер firewall, который является однонаправленным прокси от хоста к контейнерам. Контейнеры, кроме firewall и nginx, не имеют сетевого доступа к хосту. Это гарантирует выполнение требования из ТЗ про отсуствие доступа к интернету во время эксплуатации решения.

Структурированные логи

Структурированные логи: пример (structlog)

structlog example

2. Запуск тестов

Для тестов настроен CI, coverage экспортируется в интерфейс GitLab. Пайплайн находится в .gitlab-ci.yml.

pytest --cov=onetwo

Эта команда выводит coverage всех тестов в конце. Coverage локально будет ниже, чем в CI - на уровне 80%. В CI тесты поднимают окружение, локально - нет.

Playbooks

Еще есть e2e тесты (make playbooks). Подробнее - в конце README.

3. Линтинг и форматирование кода

Для линтинга и форматирования кода используется ruff. Дополнительно как type checker используется ty. Конфигурация находится в pyproject.toml.

Запуск линтинга, форматирования кода и type checking:

make lint

Запуск только форматирования кода:

ruff format

Обратите внимание, что эти команды требуют активного виртуального окружения.

D. Описание

Проект

Проекты - контейнер для всех сущностей сервиса. У проектов есть уникальный slug.

В проектах есть ролевая система. После создания проекта пользователь получает роль ADMIN.

Доступные роли:

  • ADMIN: полное управление проектом, создание event types, feature flags, метрик, назначение пользователей
  • EXPERIMENTER: создание и управление экспериментами
  • REVIEWER: ревью (апрув/реджект) экспериментов
  • VIEWER: только read-only доступ к эксперименту.

Точное разделение прав по ролям находится в onetwo/experiments/domain/permissions.py. Можно назначать людей на одну или несколько ролей (один и тот же человек может и создавать, и ревьюить эксперименты, если у него есть роли EXPERIMENTER и REVIEWER). Считается, что у человека есть доступ к проекту, если у него есть хотя бы одна роль.

Feature Flag

Отношение "ключ" (Identifier) - "значение" (строка с любым значением). Создаются и управляются администратором.

В контексте экспериментов, значение флага в проекте - дефолтное (контрольное) значение флага в любом эксперименте с этим флагом.

Эксперимент

Объединение сущностей с единой целью - изменить значение feature flag для части пользователей и измерить изменения в метриках.

Конфигурация эксперимента делятся на две части:

  • настройки эксперимента;
  • options.

ExperimentOptions - это те параметры эксперимента, которые непосредственно влияют на пользователя:

  • флаг;
  • варианты и их веса (контрольные варианты отмечаются флагом isDefault, их значение всегда будет равно значению флага в проекте; сумма весов равна 1);
  • таргетинг;
  • приоритет.

Они выделены в отдельную сущность. Их можно изменять только в статусе DRAFT. Все изменения в Options логируются.

Остальные настройки эксперимента:

  • метрики;
  • параметры автопилота и guardrails;
  • параметры нотификаций;
  • доля трафика.

Их можно изменять, поскольку они:

  1. не влияют непосредственно на метрики и пользователей (от увеличения общей доли распределение по вариантам не изменится);
  2. есть достаточное количество сценариев, в которых эти параметры потребуется менять после запуска (ускорить/замедлить раскатку; добавить больше уведомлений; ...)

Эксперимент не привязывается к конкретному человеку. Над ним может работать кто угодно (хоть вся команда), если у них есть соответствующие права в проекте.

Статусная модель

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 --> [*]
Loading

Находясь в статусе 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 и обратно процесс ревью перезапускается, и все вердикты нужно будет выставить заново. Но все действия с ревью логируются + сущность "ревью" не удаляется, что дает возможности для аудита.

Decision

Я считаю, что все запросы к моему сервису будут приходить в закрытом контуре от других сервисов. Поэтому для корректной идентификации проекта (и связанных 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 по email
  • date == d'1970-01-01' - сравнение с датой (литерал d'YYYY-MM-DD')

Если запрос не задан, ограничения по атрибутам нет.

Алгоритм

onetwo/runtime/domain/decision.py

  1. Выбираются эксперименты:
  • статус RUNNING, PAUSED или FINISHED (да, finished тоже)
  • соответствие атрибутов пользователя - таргетингу;
  • murmurhash3 от user_id + experiment_id попадает в нужную долю traffic_share.

Такие эксперименты считаются выбранными (selected).

  1. Эксперименты сортируются по убыванию приоритета, при равенстве - по ID для детерминированности.

  2. Выбираются первые experiments_per_user экспериментов (из настроек проекта). Они считаются экспериментами, которые влияют (affected) на пользователя.

  3. Пропускаются эксперименты в статусах PAUSED или FINISHED. Оставшиеся считаются экспериментами, в которых пользователь участвует (participating).

  4. Для каждого эксперимента выбирается вариант в соответствии с их весами - аналогично шагу 1, с использованием murmurhash3.

  5. Выбранные эксперименты кодируются в 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)

Для метрик вида "сколько кликов произошло среди тех, кто получил экспозицию" используются логика subset.

В функцию агрегации (count / mean / sum / ...) нужно передать одну из двух функций:

  • count_in(subset, источник, окно) - количество событий указанного источника в окне относительно subset
  • count_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 по событиям view
  • sum(purchase.amount) - сумма поля amount по событиям purchase
  • p50(view.latency) - медиана latency
  • mean(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 объединены с автопилотом. Автопилот можно представить себе как компонент сервиса, задача которого сводится к управлению настройками проекта по заданным пользователям правилам. Под это определение так же попадают и 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:

E. Архитектура

Docker Swarm и масштабирование backend

Для горизонтального масштабирования 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).

C4 Context L1

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, "чтение событий")
Loading

C4 Context L3 критичного пути

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, "запросы")
Loading

E. Playbooks

Playbooks - end-2-end тесты, которые позволяют проверить функциональность приложения в легко воспринимаемом для человека виде. Они находятся в папке playbooks/.

Для запуска требуется поднять приложение.

python -m playbooks

Можно переопределить base url через --base-url, добавить задержку между запросами через --delay или выбрать фильтр по сценариям, указав их как позиционный аргмент.

F. Прочее

Для удобной навигации матрица соответствия критериям находится в файле CRITERIA.md

В папке media/adr/ находятся принятые архитектурные решения.

Я пользовался LLM при написании:

  • матрицы соответствия критериям;
  • ADR;
  • тесты (tests/ и playbooks/);
  • реализации адаптеров (репозитории и запросы в ClickHouse).

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages