feat(generator): добавлены профили запуска

- Зачем:
  - запуск генератора должен быть понятным перед будущим DAG-пультом.
- Что:
  - добавлены глаголы запуска backfill, continue и reset.
  - добавлены профили ci и daily-wave с расчётом длительности истории.
  - обновлены runbook и документы запуска под профильный интерфейс.
- Проверка:
  - uv run --with-requirements generator/requirements.txt pytest generator/tests -q.
  - bash -n scripts/run_generator.sh scripts/export_startup_history_artifact.sh scripts/import_startup_history_artifact.sh scripts/run_generated_history_analytics.sh.
  - PROFILE=daily-wave COMPOSE_BIN=true bash scripts/run_generator.sh backfill.
This commit is contained in:
2026-07-04 18:51:10 +03:00
parent d2899e9ee3
commit 4f8f992363
19 changed files with 547 additions and 101 deletions
@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done
# Глаголы, длительность и профили запуска генератора # Глаголы, длительность и профили запуска генератора
+13
View File
@@ -4,6 +4,7 @@
reload-monitoring recover-monitoring \ reload-monitoring recover-monitoring \
superset-init superset-dashboard superset-ui superset-restart \ superset-init superset-dashboard superset-ui superset-restart \
generator-up generator-down generator-logs generator-restart \ generator-up generator-down generator-logs generator-restart \
generator-backfill generator-continue generator-reset \
generator-test generator-test-build generator-test-cov generator-test generator-test-build generator-test-cov
COMPOSE ?= docker compose COMPOSE ?= docker compose
@@ -130,6 +131,18 @@ generator-logs:
generator-restart: generator-restart:
$(COMPOSE) up -d --build --force-recreate generator $(COMPOSE) up -d --build --force-recreate generator
# Промотать стартовую историю через профиль запуска
generator-backfill:
COMPOSE_BIN="$(COMPOSE)" bash ./scripts/run_generator.sh backfill
# Продолжить live-поток из существующего state
generator-continue:
COMPOSE_BIN="$(COMPOSE)" bash ./scripts/run_generator.sh continue
# Запустить live-поток как новый мир
generator-reset:
COMPOSE_BIN="$(COMPOSE)" bash ./scripts/run_generator.sh reset
# Собрать тестовый образ генератора # Собрать тестовый образ генератора
generator-test-build: generator-test-build:
cd generator && docker build -t generator:test . cd generator && docker build -t generator:test .
+9 -3
View File
@@ -37,11 +37,17 @@ make generated-history-analytics
docker compose ps docker compose ps
``` ```
По умолчанию это быстрый проверочный профиль на 6 часов модельного времени. По умолчанию это быстрый профиль `ci`: 6 часов модельного времени. Историю на
Суточную историю можно прогнать отдельно: 2 суток с суточной волной можно прогнать одной командой:
```bash ```bash
GEN_MODEL_T_END=2026-01-02T00:00:00+00:00 make generated-history-analytics PROFILE=daily-wave make generated-history-analytics
```
Разовую длительность можно задать без ручного расчёта правой границы:
```bash
GEN_HISTORY_DURATION=2d make generated-history-analytics
``` ```
Повторить только техническую проверку после уже выполненного прогона: Повторить только техническую проверку после уже выполненного прогона:
+1
View File
@@ -307,6 +307,7 @@ services:
GEN_MODEL_TIMEZONE: ${GEN_MODEL_TIMEZONE:-UTC} GEN_MODEL_TIMEZONE: ${GEN_MODEL_TIMEZONE:-UTC}
GEN_MODEL_TIME_SPEED: ${GEN_MODEL_TIME_SPEED:-1} GEN_MODEL_TIME_SPEED: ${GEN_MODEL_TIME_SPEED:-1}
GEN_RUN_MODE: ${GEN_RUN_MODE:-live} GEN_RUN_MODE: ${GEN_RUN_MODE:-live}
GEN_LAUNCH_PROFILE: ${GEN_LAUNCH_PROFILE:-ci}
GEN_DATA_DIR: /data GEN_DATA_DIR: /data
GEN_SEED: ${GEN_SEED:-} GEN_SEED: ${GEN_SEED:-}
GEN_ENABLED: ${GEN_ENABLED:-true} GEN_ENABLED: ${GEN_ENABLED:-true}
+24 -45
View File
@@ -118,6 +118,7 @@ make generator-logs
| `GEN_MODEL_TIMEZONE` | Часовой пояс модельных часов для дневного коэффициента | `UTC` | | `GEN_MODEL_TIMEZONE` | Часовой пояс модельных часов для дневного коэффициента | `UTC` |
| `GEN_MODEL_TIME_SPEED` | Сколько модельных секунд проходит за одну настенную секунду | `1` | | `GEN_MODEL_TIME_SPEED` | Сколько модельных секунд проходит за одну настенную секунду | `1` |
| `GEN_RUN_MODE` | Режим генератора | `live` | | `GEN_RUN_MODE` | Режим генератора | `live` |
| `GEN_LAUNCH_PROFILE` | Имя профиля запуска для логов | `ci` |
| `GEN_STARTUP_HISTORY_ARTIFACT` | JSON-файл для экспорта стартовой истории в режиме `backfill` | пусто | | `GEN_STARTUP_HISTORY_ARTIFACT` | JSON-файл для экспорта стартовой истории в режиме `backfill` | пусто |
| `GEN_STATE_ENABLED` | Сохранять state v2 между рестартами | `true` | | `GEN_STATE_ENABLED` | Сохранять state v2 между рестартами | `true` |
| `GEN_STATE_RESET` | Сбросить state при старте | `false` | | `GEN_STATE_RESET` | Сбросить state при старте | `false` |
@@ -141,20 +142,25 @@ make generated-history-analytics
``` ```
Она выполняет полный сброс volumes, поднимает ClickHouse и Kafka, применяет DDL, Она выполняет полный сброс volumes, поднимает ClickHouse и Kafka, применяет DDL,
запускает `GEN_RUN_MODE=backfill`, прогоняет batch STG -> ODS -> DDS -> DM, выполняет глагол `backfill`, прогоняет batch STG -> ODS -> DDS -> DM,
инициализирует Superset и запускает техническую проверку. Для координатора или CI инициализирует Superset и запускает техническую проверку. Для координатора или
короткая повторная проверка после уже готового стенда: CI короткая повторная проверка после уже готового стенда:
```bash ```bash
make generated-history-check make generated-history-check
``` ```
По умолчанию команда использует быстрый проверочный профиль: 6 часов модельного По умолчанию команда использует быстрый профиль `ci`: 6 часов модельного
времени (`GEN_MODEL_T_END=2026-01-01T06:00:00+00:00`). Суточную историю можно времени. Историю на 2 суток с суточной волной можно получить одной командой:
прогнать отдельно, явно задав правую границу:
```bash ```bash
GEN_MODEL_T_END=2026-01-02T00:00:00+00:00 make generated-history-analytics PROFILE=daily-wave make generated-history-analytics
```
Разовую длительность можно задать без ручного расчёта `GEN_MODEL_T_END`:
```bash
GEN_HISTORY_DURATION=2d make generated-history-analytics
``` ```
`GEN_RUN_MODE=backfill` быстро проматывает модельное прошлое от `GEN_MODEL_T0` `GEN_RUN_MODE=backfill` быстро проматывает модельное прошлое от `GEN_MODEL_T0`
@@ -171,30 +177,16 @@ GEN_MODEL_T_END=2026-01-02T00:00:00+00:00 make generated-history-analytics
Kafka-топики данных, state и manifest генератора. `make generated-history-analytics` Kafka-топики данных, state и manifest генератора. `make generated-history-analytics`
делает это по умолчанию (`CLEAN_START=1`). делает это по умолчанию (`CLEAN_START=1`).
Ручной backfill без всего аналитического контура:
```bash ```bash
make clean PROFILE=daily-wave make generator-backfill
docker compose up -d clickhouse kafka
make ddl
GEN_RUN_MODE=backfill \
GEN_STATE_RESET=true \
GEN_SEED=4242 \
GEN_MODEL_T0=2026-01-01T00:00:00+00:00 \
GEN_MODEL_T_END=2026-01-01T06:00:00+00:00 \
GEN_MODEL_TIMEZONE=UTC \
GEN_MODEL_TIME_SPEED=1 \
GEN_TICK_SECONDS=60 \
GEN_LAMBDA_BASE_PER_MIN=60 \
GEN_JITTER_PCT=0 \
docker compose run --rm --no-deps generator
# Materialized View слоя STG читает Kafka сама; даём ей коротко догнать.
sleep 10
bash scripts/run_batch.sh
``` ```
Ручной сценарий выше нужен для отладки. В обычной проверке используйте Старый способ через `GEN_RUN_MODE`, `GEN_STATE_RESET` и `GEN_MODEL_T_END`
`make generated-history-analytics`, чтобы не забыть Superset и итоговый check. остаётся низкоуровневым путём для отладки прямого `docker compose run`. В обычной
проверке используйте `make generated-history-analytics`, чтобы не забыть
Superset и итоговый check.
Manifest можно посмотреть так: Manifest можно посмотреть так:
@@ -207,27 +199,14 @@ docker compose exec -T kafka /opt/kafka/bin/kafka-console-consumer.sh \
--timeout-ms 5000 --timeout-ms 5000
``` ```
Live-продолжение стартует с этого state. Используйте те же `GEN_SEED`, `T0`, Live-продолжение стартует с этого state. Используйте тот же профиль или ту же
`T_end`, часовой пояс и настройки генерации. `GEN_STATE_RESET=false` важен: иначе длительность, что были у backfill. Команда сама выставит `GEN_STATE_RESET=false`.
слепок стартовой истории будет проигнорирован.
```bash ```bash
docker compose run -d --name startup-history-live --no-deps \ PROFILE=daily-wave make generator-continue
-e GEN_RUN_MODE=live \
-e GEN_STATE_RESET=false \
-e GEN_SEED=4242 \
-e GEN_MODEL_T0=2026-01-01T00:00:00+00:00 \
-e GEN_MODEL_T_END=2026-01-01T06:00:00+00:00 \
-e GEN_MODEL_TIMEZONE=UTC \
-e GEN_MODEL_TIME_SPEED=1 \
-e GEN_TICK_SECONDS=60 \
-e GEN_LAMBDA_BASE_PER_MIN=60 \
-e GEN_JITTER_PCT=0 \
generator
sleep 130 sleep 130
docker stop startup-history-live make generator-down
docker rm startup-history-live
sleep 10 sleep 10
bash scripts/run_batch.sh bash scripts/run_batch.sh
``` ```
+29 -13
View File
@@ -16,12 +16,24 @@
## Что требует нового артефакта ## Что требует нового артефакта
- Другая длительность истории (`GEN_MODEL_T_END`). - Другая длительность истории или другой профиль запуска.
- Другой `GEN_SEED`, `GEN_MODEL_T0`, часовой пояс, скорость или настройки - Другой `GEN_SEED`, `GEN_MODEL_T0`, часовой пояс, скорость или настройки
генерации. генерации.
- Осознанный новый мир после несовместимого state: сначала сбросьте state через - Осознанный новый мир после несовместимого state: сначала сбросьте state через
`GEN_STATE_RESET=true` или чистые volumes. `GEN_STATE_RESET=true` или чистые volumes.
## Профили запуска
Основной способ — глагол плюс профиль:
| Профиль | Для чего | Длительность |
|---------|----------|--------------|
| `ci` | Быстрая проверка и CI | `6h` |
| `daily-wave` | История с видимой суточной волной | `2d` |
Длительность можно переопределить через `GEN_HISTORY_DURATION`, например `2d`.
Команда сама считает `GEN_MODEL_T_END` от `GEN_MODEL_T0`.
## Экспорт ## Экспорт
По умолчанию создаётся быстрый 6-часовой артефакт: По умолчанию создаётся быстрый 6-часовой артефакт:
@@ -32,11 +44,11 @@ make startup-history-export
Файл по умолчанию: `/tmp/clickstream-startup-history.json`. Файл по умолчанию: `/tmp/clickstream-startup-history.json`.
Суточная история: История на 2 суток с суточной волной:
```bash ```bash
ARTIFACT=/tmp/clickstream-startup-history-1d.json \ ARTIFACT=/tmp/clickstream-startup-history-2d.json \
GEN_MODEL_T_END=2026-01-02T00:00:00+00:00 \ PROFILE=daily-wave \
make startup-history-export make startup-history-export
``` ```
@@ -62,6 +74,13 @@ make generated-history-check
compact-топики в Kafka. ClickHouse читает данные через свои Kafka-таблицы и compact-топики в Kafka. ClickHouse читает данные через свои Kafka-таблицы и
Materialized View, затем batch строит ODS, DDS и DM. Materialized View, затем batch строит ODS, DDS и DM.
Если артефакт создан не профилем `ci`, импорт запускайте с тем же профилем:
```bash
PROFILE=daily-wave ARTIFACT=/tmp/clickstream-startup-history-2d.json \
make startup-history-import
```
`make startup-history-check` сверяет контрольные числа DM-витрины с manifest `make startup-history-check` сверяет контрольные числа DM-витрины с manifest
артефакта: события, визиты, пользователей и диапазон `event_timestamp`. Если артефакта: события, визиты, пользователей и диапазон `event_timestamp`. Если
data-топики Kafka уже непустые, импорт остановится до публикации событий. data-топики Kafka уже непустые, импорт остановится до публикации событий.
@@ -74,18 +93,15 @@ ClickHouse уже успел прочитать частичные сообще
## Live-продолжение ## Live-продолжение
После импорта запускайте live с теми же настройками, что были в артефакте: После импорта запускайте live с тем же профилем, что был в артефакте:
```bash ```bash
GEN_STATE_RESET=false \ PROFILE=ci make generator-continue
GEN_SEED=4242 \
GEN_MODEL_T0=2026-01-01T00:00:00+00:00 \
GEN_MODEL_T_END=2026-01-01T06:00:00+00:00 \
GEN_MODEL_TIMEZONE=UTC \
GEN_MODEL_TIME_SPEED=1 \
docker compose up -d generator
``` ```
Если читаемый state есть, но настройки не совпадают, генератор падает с перечнем Если читаемый state есть, но настройки не совпадают, генератор падает с перечнем
полей. Это защита от смешения разных миров. Для намеренного нового мира полей. Это защита от смешения разных миров. Для намеренного нового мира
используйте `GEN_STATE_RESET=true` или `make clean`. используйте `make generator-reset` или `make clean`.
Старые переменные `GEN_RUN_MODE`, `GEN_STATE_RESET` и `GEN_MODEL_T_END` остаются
низкоуровневым способом для отладки и прямого `docker compose run`.
@@ -90,12 +90,25 @@ ADR-0005 решил отвязать время генератора от реа
одну настенную секунду. Формат: положительное число, по умолчанию `1`. одну настенную секунду. Формат: положительное число, по умолчанию `1`.
- `GEN_RUN_MODE` — режим запуска: `live` или `backfill`. Значение по умолчанию — - `GEN_RUN_MODE` — режим запуска: `live` или `backfill`. Значение по умолчанию —
`live`. `live`.
- `GEN_LAUNCH_PROFILE` — имя профиля запуска для логов. Значение по умолчанию —
`ci`; на поток не влияет.
- `GEN_STARTUP_HISTORY_ARTIFACT` — путь к JSON-файлу, куда `backfill` дополнительно - `GEN_STARTUP_HISTORY_ARTIFACT` — путь к JSON-файлу, куда `backfill` дополнительно
пишет портативный артефакт стартовой истории. В обычном live-запуске не нужен. пишет портативный артефакт стартовой истории. В обычном live-запуске не нужен.
- `GEN_SEED`, `GEN_TICK_SECONDS` и остальные настройки генерации остаются частью - `GEN_SEED`, `GEN_TICK_SECONDS` и остальные настройки генерации остаются частью
контракта повторяемости. Если они отличаются, артефакт стартовой истории контракта повторяемости. Если они отличаются, артефакт стартовой истории
считается другим. считается другим.
Поверх этих переменных есть пользовательский слой запуска:
- `backfill` — подставляет `GEN_RUN_MODE=backfill` и `GEN_STATE_RESET=true`;
- `continue` — подставляет `GEN_RUN_MODE=live` и `GEN_STATE_RESET=false`;
- `reset` — подставляет `GEN_RUN_MODE=live` и `GEN_STATE_RESET=true`.
Профиль `ci` даёт быстрый 6-часовой прогон. Профиль `daily-wave` даёт 2 суток,
чтобы была видна суточная волна. `GEN_HISTORY_DURATION` задаёт длительность
вида `6h` или `2d`; `GEN_MODEL_T_END` считается от `GEN_MODEL_T0` внутри слоя
запуска. Старые переменные остаются низкоуровневым механизмом.
#### Ход часов #### Ход часов
В живом режиме модельное время идёт фиксированным шагом. На чистом старте оно В живом режиме модельное время идёт фиксированным шагом. На чистом старте оно
+25 -6
View File
@@ -78,6 +78,7 @@ generator-service -> Kafka topics -> (потребители отдельно)
| `GEN_MODEL_TIMEZONE` | Часовой пояс модельных часов для дневного коэффициента | `UTC` | | `GEN_MODEL_TIMEZONE` | Часовой пояс модельных часов для дневного коэффициента | `UTC` |
| `GEN_MODEL_TIME_SPEED` | Сколько модельных секунд проходит за одну настенную секунду | `1` | | `GEN_MODEL_TIME_SPEED` | Сколько модельных секунд проходит за одну настенную секунду | `1` |
| `GEN_RUN_MODE` | Режим генератора | `live` | | `GEN_RUN_MODE` | Режим генератора | `live` |
| `GEN_LAUNCH_PROFILE` | Имя профиля запуска для логов | `ci` |
| `GEN_STARTUP_HISTORY_ARTIFACT` | JSON-файл для экспорта стартовой истории в режиме `backfill` | — | | `GEN_STARTUP_HISTORY_ARTIFACT` | JSON-файл для экспорта стартовой истории в режиме `backfill` | — |
| `GEN_DATA_DIR` | Путь к JSONL файлам | `/data` | | `GEN_DATA_DIR` | Путь к JSONL файлам | `/data` |
| `GEN_SEED` | Сид для воспроизводимости | — | | `GEN_SEED` | Сид для воспроизводимости | — |
@@ -125,12 +126,17 @@ make generated-history-analytics
проверяет Superset metadata. Файлы `data/*.jsonl` при этом не грузятся в Kafka: проверяет Superset metadata. Файлы `data/*.jsonl` при этом не грузятся в Kafka:
они пока используются только как фактура для генератора. они пока используются только как фактура для генератора.
По умолчанию используется быстрый проверочный профиль на 6 часов модельного По умолчанию используется быстрый профиль `ci`: 6 часов модельного времени.
времени (`GEN_MODEL_T_END=2026-01-01T06:00:00+00:00`). Суточный прогон доступен Профиль `daily-wave` даёт 2 суток, чтобы была видна суточная волна:
явно:
```bash ```bash
GEN_MODEL_T_END=2026-01-02T00:00:00+00:00 make generated-history-analytics PROFILE=daily-wave make generated-history-analytics
```
Разовую длительность можно задать без ручного расчёта `GEN_MODEL_T_END`:
```bash
GEN_HISTORY_DURATION=2d make generated-history-analytics
``` ```
### Режим "раз в минуту" (для демо) ### Режим "раз в минуту" (для демо)
@@ -162,13 +168,26 @@ make generator-logs
# Перезапуск с пересборкой # Перезапуск с пересборкой
make generator-restart make generator-restart
# Запуск тестов # Промотать стартовую историю
make generator-test make generator-backfill
# Продолжить live-поток из state
make generator-continue
# Начать live-поток как новый мир
make generator-reset
# Чистый аналитический прогон всего стенда # Чистый аналитический прогон всего стенда
make generated-history-analytics make generated-history-analytics
# Запуск тестов
make generator-test
``` ```
Основной ручной путь запуска — глаголы `generator-backfill`, `generator-continue`
и `generator-reset`. Старые `GEN_RUN_MODE`, `GEN_STATE_RESET` и
`GEN_MODEL_T_END` остаются низкоуровневым способом для отладки.
## Метрики Prometheus ## Метрики Prometheus
Генератор экспортирует метрики на `:9109/metrics`: Генератор экспортирует метрики на `:9109/metrics`:
@@ -97,6 +97,9 @@ class Config:
if os.getenv("GEN_STARTUP_HISTORY_ARTIFACT") if os.getenv("GEN_STARTUP_HISTORY_ARTIFACT")
else None else None
) )
launch_profile: str = field(
default_factory=lambda: os.getenv("GEN_LAUNCH_PROFILE", "ci")
)
def __post_init__(self): def __post_init__(self):
if self.tick_seconds < 1: if self.tick_seconds < 1:
@@ -0,0 +1,175 @@
"""Глаголы и профили запуска генератора."""
from __future__ import annotations
import argparse
import os
import shlex
import sys
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
LAUNCH_ENV_KEYS = (
"GEN_SEED",
"GEN_MODEL_T0",
"GEN_MODEL_TIMEZONE",
"GEN_MODEL_TIME_SPEED",
"GEN_TICK_SECONDS",
"GEN_LAMBDA_BASE_PER_MIN",
"GEN_JITTER_PCT",
"GEN_MIN_EVENTS_PER_TICK",
"GEN_MAX_EVENTS_PER_TICK",
)
@dataclass(frozen=True)
class LaunchProfile:
"""Именованный набор настроек запуска."""
duration: str
env: dict[str, str]
PROFILES = {
"ci": LaunchProfile(
duration="6h",
env={
"GEN_SEED": "4242",
"GEN_MODEL_T0": "2026-01-01T00:00:00+00:00",
"GEN_MODEL_TIMEZONE": "UTC",
"GEN_MODEL_TIME_SPEED": "1",
"GEN_TICK_SECONDS": "60",
"GEN_LAMBDA_BASE_PER_MIN": "60",
"GEN_JITTER_PCT": "0",
"GEN_MIN_EVENTS_PER_TICK": "1",
"GEN_MAX_EVENTS_PER_TICK": "1000",
},
),
"daily-wave": LaunchProfile(
duration="2d",
env={
"GEN_SEED": "4242",
"GEN_MODEL_T0": "2026-01-01T00:00:00+00:00",
"GEN_MODEL_TIMEZONE": "UTC",
"GEN_MODEL_TIME_SPEED": "1",
"GEN_TICK_SECONDS": "60",
"GEN_LAMBDA_BASE_PER_MIN": "60",
"GEN_JITTER_PCT": "0",
"GEN_MIN_EVENTS_PER_TICK": "1",
"GEN_MAX_EVENTS_PER_TICK": "1000",
},
),
}
def parse_duration(value: str) -> timedelta:
"""Разбирает короткую длительность: 30m, 6h, 2d."""
if len(value) < 2:
raise ValueError("Duration must look like 6h or 2d")
amount_text = value[:-1]
unit = value[-1]
if not amount_text.isdigit():
raise ValueError("Duration amount must be a positive integer")
amount = int(amount_text)
if amount <= 0:
raise ValueError("Duration amount must be > 0")
if unit == "s":
return timedelta(seconds=amount)
if unit == "m":
return timedelta(minutes=amount)
if unit == "h":
return timedelta(hours=amount)
if unit == "d":
return timedelta(days=amount)
raise ValueError("Duration unit must be one of: s, m, h, d")
def build_launch_env(
verb: str,
*,
profile_name: str = "ci",
duration: str | None = None,
overrides: dict[str, str] | None = None,
) -> dict[str, str]:
"""Возвращает старые env-переменные для нового глагола запуска."""
if verb not in {"backfill", "continue", "reset"}:
raise ValueError("verb must be backfill, continue or reset")
if profile_name not in PROFILES:
known = ", ".join(sorted(PROFILES))
raise ValueError(f"Unknown launch profile {profile_name!r}; known: {known}")
profile = PROFILES[profile_name]
overrides = overrides or {}
env = dict(profile.env)
for key in LAUNCH_ENV_KEYS:
if key in overrides and overrides[key] != "":
env[key] = overrides[key]
selected_duration = (
duration
or overrides.get("GEN_HISTORY_DURATION")
or profile.duration
)
env["GEN_LAUNCH_VERB"] = verb
env["GEN_LAUNCH_PROFILE"] = profile_name
if verb == "backfill":
env["GEN_RUN_MODE"] = "backfill"
env["GEN_STATE_RESET"] = "true"
env["GEN_HISTORY_DURATION"] = selected_duration
env["GEN_MODEL_T_END"] = _model_t_end(env["GEN_MODEL_T0"], selected_duration)
elif verb == "continue":
env["GEN_RUN_MODE"] = "live"
env["GEN_STATE_RESET"] = "false"
env["GEN_HISTORY_DURATION"] = selected_duration
env["GEN_MODEL_T_END"] = _model_t_end(env["GEN_MODEL_T0"], selected_duration)
else:
env["GEN_RUN_MODE"] = "live"
env["GEN_STATE_RESET"] = "true"
return env
def shell_assignments(env: dict[str, str]) -> str:
"""Печатает env в виде, пригодном для eval в Bash."""
return "\n".join(
f"export {key}={shlex.quote(value)}"
for key, value in sorted(env.items())
)
def _model_t_end(model_t0: str, duration: str) -> str:
t0 = datetime.fromisoformat(model_t0.replace("Z", "+00:00"))
if t0.tzinfo is None:
raise ValueError("GEN_MODEL_T0 must include timezone")
return (t0.astimezone(timezone.utc) + parse_duration(duration)).isoformat()
def _parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Глаголы запуска генератора")
parser.add_argument("verb", choices=("backfill", "continue", "reset"))
parser.add_argument(
"--profile",
default=os.getenv("PROFILE") or os.getenv("GEN_LAUNCH_PROFILE") or "ci",
choices=sorted(PROFILES),
)
parser.add_argument("--duration", default=os.getenv("GEN_HISTORY_DURATION"))
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
"""CLI для Bash-скриптов запуска."""
args = _parse_args(sys.argv[1:] if argv is None else argv)
env = build_launch_env(
args.verb,
profile_name=args.profile,
duration=args.duration,
overrides=os.environ,
)
print(shell_assignments(env))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -70,6 +70,7 @@ class GeneratorService:
f"Configuration: tick={self.config.tick_seconds}s, " f"Configuration: tick={self.config.tick_seconds}s, "
f"lambda_base={self.config.lambda_base_per_min}/min, " f"lambda_base={self.config.lambda_base_per_min}/min, "
f"jitter={self.config.jitter_pct}%, " f"jitter={self.config.jitter_pct}%, "
f"launch_profile={self.config.launch_profile}, "
f"state_enabled={self.config.state_enabled}, " f"state_enabled={self.config.state_enabled}, "
f"state_reset={self.config.state_reset}" f"state_reset={self.config.state_reset}"
) )
@@ -299,6 +299,7 @@ def build_manifest(config: Config, counters: ManifestCounters, state: GeneratorS
"model_t_end": config.model_t_end.isoformat(), "model_t_end": config.model_t_end.isoformat(),
"model_timezone": config.model_timezone, "model_timezone": config.model_timezone,
"run_mode": "backfill", "run_mode": "backfill",
"launch_profile": config.launch_profile,
"generation_settings": generation_settings_from_config(config), "generation_settings": generation_settings_from_config(config),
"state_version": state.version, "state_version": state.version,
"state": { "state": {
+18
View File
@@ -118,3 +118,21 @@ class TestConfigDefaults:
finally: finally:
if orig_value is not None: if orig_value is not None:
os.environ["GEN_TICK_SECONDS"] = orig_value os.environ["GEN_TICK_SECONDS"] = orig_value
def test_launch_profile_defaults_to_ci(self, monkeypatch, data_dir):
"""По умолчанию выбран быстрый профиль запуска ci."""
monkeypatch.setenv("GEN_DATA_DIR", str(data_dir))
monkeypatch.delenv("GEN_LAUNCH_PROFILE", raising=False)
config = Config()
assert config.launch_profile == "ci"
def test_launch_profile_can_be_read_from_env(self, monkeypatch, data_dir):
"""Выбранный профиль запуска попадает в конфигурацию для логов."""
monkeypatch.setenv("GEN_DATA_DIR", str(data_dir))
monkeypatch.setenv("GEN_LAUNCH_PROFILE", "daily-wave")
config = Config()
assert config.launch_profile == "daily-wave"
+66
View File
@@ -0,0 +1,66 @@
"""
Тесты профилей и глаголов запуска генератора.
"""
from datetime import datetime, timezone
def test_duration_2d_sets_model_t_end_from_t0():
"""Длительность 2d считается от T0 без ручного GEN_MODEL_T_END."""
from clickstream_generator.launch import build_launch_env
env = build_launch_env("backfill", profile_name="ci", duration="2d")
assert env["GEN_MODEL_T0"] == "2026-01-01T00:00:00+00:00"
assert env["GEN_MODEL_T_END"] == "2026-01-03T00:00:00+00:00"
def test_daily_wave_profile_uses_two_days_by_default():
"""Профиль daily-wave даёт историю с суточной волной."""
from clickstream_generator.launch import build_launch_env
env = build_launch_env("backfill", profile_name="daily-wave")
assert env["GEN_LAUNCH_PROFILE"] == "daily-wave"
assert env["GEN_HISTORY_DURATION"] == "2d"
assert env["GEN_MODEL_T_END"] == "2026-01-03T00:00:00+00:00"
def test_backfill_verb_maps_to_existing_low_level_flags():
"""Глагол backfill оставляет прежнюю механику запуска под капотом."""
from clickstream_generator.launch import build_launch_env
env = build_launch_env("backfill", profile_name="ci")
assert env["GEN_RUN_MODE"] == "backfill"
assert env["GEN_STATE_RESET"] == "true"
def test_continue_and_reset_verbs_map_to_existing_low_level_flags():
"""Глаголы continue и reset задают только старый контракт state."""
from clickstream_generator.launch import build_launch_env
continue_env = build_launch_env("continue", profile_name="ci")
reset_env = build_launch_env("reset", profile_name="ci")
assert continue_env["GEN_RUN_MODE"] == "live"
assert continue_env["GEN_STATE_RESET"] == "false"
assert continue_env["GEN_MODEL_T_END"] == "2026-01-01T06:00:00+00:00"
assert reset_env["GEN_RUN_MODE"] == "live"
assert reset_env["GEN_STATE_RESET"] == "true"
assert "GEN_MODEL_T_END" not in reset_env
def test_ci_profile_keeps_previous_default_backfill_window():
"""CI-профиль сохраняет прежний 6-часовой проверочный запуск."""
from clickstream_generator.launch import build_launch_env
env = build_launch_env("backfill", profile_name="ci")
t0 = datetime.fromisoformat(env["GEN_MODEL_T0"])
t_end = datetime.fromisoformat(env["GEN_MODEL_T_END"])
assert t0 == datetime(2026, 1, 1, 0, 0, tzinfo=timezone.utc)
assert (t_end - t0).total_seconds() == 6 * 60 * 60
assert env["GEN_TICK_SECONDS"] == "60"
assert env["GEN_LAMBDA_BASE_PER_MIN"] == "60"
assert env["GEN_JITTER_PCT"] == "0"
@@ -130,6 +130,43 @@ def test_artifact_roundtrip_keeps_events_state_and_manifest(base_config):
path.unlink(missing_ok=True) path.unlink(missing_ok=True)
def test_manifest_and_artifact_show_launch_profile(base_config):
"""Manifest и файл артефакта показывают выбранный профиль запуска."""
from clickstream_generator.startup_history_artifact import (
StartupHistoryArtifactBuilder,
build_manifest,
load_startup_history_artifact,
write_startup_history_artifact,
)
state = _state()
builder = StartupHistoryArtifactBuilder()
builder.add_batch(_batch())
manifest = build_manifest(
config=replace(
base_config,
model_t0=state.model_t0,
model_t_end=state.model_timestamp,
model_time_speed=1,
model_timezone="UTC",
seed=42,
launch_profile="daily-wave",
),
counters=builder.counters,
state=state,
)
artifact = builder.to_artifact(manifest=manifest, state=state)
path = base_config.data_dir.parent / "tmp-startup-history-profile.json"
try:
write_startup_history_artifact(path, artifact)
loaded = load_startup_history_artifact(path)
assert manifest["launch_profile"] == "daily-wave"
assert loaded["manifest"]["launch_profile"] == "daily-wave"
finally:
path.unlink(missing_ok=True)
def test_artifact_validation_rejects_mismatched_manifest(base_config): def test_artifact_validation_rejects_mismatched_manifest(base_config):
"""Валидация отвергает артефакт, где manifest не совпадает с событиями.""" """Валидация отвергает артефакт, где manifest не совпадает с событиями."""
from clickstream_generator.startup_history_artifact import ( from clickstream_generator.startup_history_artifact import (
+17 -11
View File
@@ -10,26 +10,31 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
COMPOSE_BIN="${COMPOSE_BIN:-docker compose}" COMPOSE_BIN="${COMPOSE_BIN:-docker compose}"
CLEAN_START="${CLEAN_START:-1}" CLEAN_START="${CLEAN_START:-1}"
ARTIFACT="${ARTIFACT:-/tmp/clickstream-startup-history.json}" ARTIFACT="${ARTIFACT:-/tmp/clickstream-startup-history.json}"
PROFILE="${PROFILE:-${GEN_LAUNCH_PROFILE:-ci}}"
GEN_SEED="${GEN_SEED:-4242}" duration_args=()
GEN_MODEL_T0="${GEN_MODEL_T0:-2026-01-01T00:00:00+00:00}" if [[ -n "${GEN_HISTORY_DURATION:-}" ]]; then
GEN_MODEL_T_END="${GEN_MODEL_T_END:-2026-01-01T06:00:00+00:00}" duration_args=(--duration "${GEN_HISTORY_DURATION}")
GEN_MODEL_TIMEZONE="${GEN_MODEL_TIMEZONE:-UTC}" fi
GEN_MODEL_TIME_SPEED="${GEN_MODEL_TIME_SPEED:-1}"
GEN_TICK_SECONDS="${GEN_TICK_SECONDS:-60}"
GEN_LAMBDA_BASE_PER_MIN="${GEN_LAMBDA_BASE_PER_MIN:-60}"
GEN_JITTER_PCT="${GEN_JITTER_PCT:-0}"
GEN_MIN_EVENTS_PER_TICK="${GEN_MIN_EVENTS_PER_TICK:-1}"
GEN_MAX_EVENTS_PER_TICK="${GEN_MAX_EVENTS_PER_TICK:-1000}"
cd "${REPO_ROOT}" cd "${REPO_ROOT}"
launch_env="$(
PYTHONPATH="${REPO_ROOT}/generator/src" \
uv run python -m clickstream_generator.launch \
backfill \
--profile "${PROFILE}" \
"${duration_args[@]}"
)"
eval "${launch_env}"
ARTIFACT_DIR="$(dirname "${ARTIFACT}")" ARTIFACT_DIR="$(dirname "${ARTIFACT}")"
ARTIFACT_FILE="$(basename "${ARTIFACT}")" ARTIFACT_FILE="$(basename "${ARTIFACT}")"
mkdir -p "${ARTIFACT_DIR}" mkdir -p "${ARTIFACT_DIR}"
echo "=== Экспорт стартовой истории ===" echo "=== Экспорт стартовой истории ==="
echo "ARTIFACT=${ARTIFACT}" echo "ARTIFACT=${ARTIFACT}"
echo "GEN_LAUNCH_PROFILE=${GEN_LAUNCH_PROFILE}"
echo "GEN_HISTORY_DURATION=${GEN_HISTORY_DURATION}"
echo "GEN_SEED=${GEN_SEED}" echo "GEN_SEED=${GEN_SEED}"
echo "GEN_MODEL_T0=${GEN_MODEL_T0}" echo "GEN_MODEL_T0=${GEN_MODEL_T0}"
echo "GEN_MODEL_T_END=${GEN_MODEL_T_END}" echo "GEN_MODEL_T_END=${GEN_MODEL_T_END}"
@@ -53,6 +58,7 @@ ${COMPOSE_BIN} run --rm --no-deps \
-v "${ARTIFACT_DIR}:/startup-artifacts" \ -v "${ARTIFACT_DIR}:/startup-artifacts" \
-e GEN_RUN_MODE=backfill \ -e GEN_RUN_MODE=backfill \
-e GEN_STATE_RESET=true \ -e GEN_STATE_RESET=true \
-e GEN_LAUNCH_PROFILE="${GEN_LAUNCH_PROFILE}" \
-e GEN_STARTUP_HISTORY_ARTIFACT="/startup-artifacts/${ARTIFACT_FILE}" \ -e GEN_STARTUP_HISTORY_ARTIFACT="/startup-artifacts/${ARTIFACT_FILE}" \
-e GEN_SEED="${GEN_SEED}" \ -e GEN_SEED="${GEN_SEED}" \
-e GEN_MODEL_T0="${GEN_MODEL_T0}" \ -e GEN_MODEL_T0="${GEN_MODEL_T0}" \
+17 -11
View File
@@ -9,20 +9,23 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
COMPOSE_BIN="${COMPOSE_BIN:-docker compose}" COMPOSE_BIN="${COMPOSE_BIN:-docker compose}"
ARTIFACT="${ARTIFACT:-/tmp/clickstream-startup-history.json}" ARTIFACT="${ARTIFACT:-/tmp/clickstream-startup-history.json}"
PROFILE="${PROFILE:-${GEN_LAUNCH_PROFILE:-ci}}"
GEN_SEED="${GEN_SEED:-4242}" duration_args=()
GEN_MODEL_T0="${GEN_MODEL_T0:-2026-01-01T00:00:00+00:00}" if [[ -n "${GEN_HISTORY_DURATION:-}" ]]; then
GEN_MODEL_T_END="${GEN_MODEL_T_END:-2026-01-01T06:00:00+00:00}" duration_args=(--duration "${GEN_HISTORY_DURATION}")
GEN_MODEL_TIMEZONE="${GEN_MODEL_TIMEZONE:-UTC}" fi
GEN_MODEL_TIME_SPEED="${GEN_MODEL_TIME_SPEED:-1}"
GEN_TICK_SECONDS="${GEN_TICK_SECONDS:-60}"
GEN_LAMBDA_BASE_PER_MIN="${GEN_LAMBDA_BASE_PER_MIN:-60}"
GEN_JITTER_PCT="${GEN_JITTER_PCT:-0}"
GEN_MIN_EVENTS_PER_TICK="${GEN_MIN_EVENTS_PER_TICK:-1}"
GEN_MAX_EVENTS_PER_TICK="${GEN_MAX_EVENTS_PER_TICK:-1000}"
cd "${REPO_ROOT}" cd "${REPO_ROOT}"
launch_env="$(
PYTHONPATH="${REPO_ROOT}/generator/src" \
uv run python -m clickstream_generator.launch \
continue \
--profile "${PROFILE}" \
"${duration_args[@]}"
)"
eval "${launch_env}"
if [[ ! -s "${ARTIFACT}" ]]; then if [[ ! -s "${ARTIFACT}" ]]; then
echo "Ошибка: артефакт не найден или пуст: ${ARTIFACT}" >&2 echo "Ошибка: артефакт не найден или пуст: ${ARTIFACT}" >&2
exit 1 exit 1
@@ -33,6 +36,8 @@ ARTIFACT_FILE="$(basename "${ARTIFACT}")"
echo "=== Импорт стартовой истории ===" echo "=== Импорт стартовой истории ==="
echo "ARTIFACT=${ARTIFACT}" echo "ARTIFACT=${ARTIFACT}"
echo "GEN_LAUNCH_PROFILE=${GEN_LAUNCH_PROFILE}"
echo "GEN_HISTORY_DURATION=${GEN_HISTORY_DURATION}"
echo "GEN_SEED=${GEN_SEED}" echo "GEN_SEED=${GEN_SEED}"
echo "GEN_MODEL_T0=${GEN_MODEL_T0}" echo "GEN_MODEL_T0=${GEN_MODEL_T0}"
echo "GEN_MODEL_T_END=${GEN_MODEL_T_END}" echo "GEN_MODEL_T_END=${GEN_MODEL_T_END}"
@@ -47,6 +52,7 @@ ${COMPOSE_BIN} build generator
echo "Шаг 3: воспроизведение артефакта в Kafka" echo "Шаг 3: воспроизведение артефакта в Kafka"
${COMPOSE_BIN} run --rm --no-deps \ ${COMPOSE_BIN} run --rm --no-deps \
-v "${ARTIFACT_DIR}:/startup-artifacts:ro" \ -v "${ARTIFACT_DIR}:/startup-artifacts:ro" \
-e GEN_LAUNCH_PROFILE="${GEN_LAUNCH_PROFILE}" \
-e GEN_SEED="${GEN_SEED}" \ -e GEN_SEED="${GEN_SEED}" \
-e GEN_MODEL_T0="${GEN_MODEL_T0}" \ -e GEN_MODEL_T0="${GEN_MODEL_T0}" \
-e GEN_MODEL_T_END="${GEN_MODEL_T_END}" \ -e GEN_MODEL_T_END="${GEN_MODEL_T_END}" \
+17 -11
View File
@@ -15,20 +15,23 @@ COMPOSE_BIN="${COMPOSE_BIN:-docker compose}"
CLEAN_START="${CLEAN_START:-1}" CLEAN_START="${CLEAN_START:-1}"
WAIT_CLICKHOUSE_SECONDS="${WAIT_CLICKHOUSE_SECONDS:-60}" WAIT_CLICKHOUSE_SECONDS="${WAIT_CLICKHOUSE_SECONDS:-60}"
WAIT_STG_SECONDS="${WAIT_STG_SECONDS:-10}" WAIT_STG_SECONDS="${WAIT_STG_SECONDS:-10}"
PROFILE="${PROFILE:-${GEN_LAUNCH_PROFILE:-ci}}"
GEN_SEED="${GEN_SEED:-4242}" duration_args=()
GEN_MODEL_T0="${GEN_MODEL_T0:-2026-01-01T00:00:00+00:00}" if [[ -n "${GEN_HISTORY_DURATION:-}" ]]; then
GEN_MODEL_T_END="${GEN_MODEL_T_END:-2026-01-01T06:00:00+00:00}" duration_args=(--duration "${GEN_HISTORY_DURATION}")
GEN_MODEL_TIMEZONE="${GEN_MODEL_TIMEZONE:-UTC}" fi
GEN_MODEL_TIME_SPEED="${GEN_MODEL_TIME_SPEED:-1}"
GEN_TICK_SECONDS="${GEN_TICK_SECONDS:-60}"
GEN_LAMBDA_BASE_PER_MIN="${GEN_LAMBDA_BASE_PER_MIN:-60}"
GEN_JITTER_PCT="${GEN_JITTER_PCT:-0}"
GEN_MIN_EVENTS_PER_TICK="${GEN_MIN_EVENTS_PER_TICK:-1}"
GEN_MAX_EVENTS_PER_TICK="${GEN_MAX_EVENTS_PER_TICK:-1000}"
cd "${REPO_ROOT}" cd "${REPO_ROOT}"
launch_env="$(
PYTHONPATH="${REPO_ROOT}/generator/src" \
uv run python -m clickstream_generator.launch \
backfill \
--profile "${PROFILE}" \
"${duration_args[@]}"
)"
eval "${launch_env}"
wait_for_clickhouse() { wait_for_clickhouse() {
local deadline local deadline
deadline=$((SECONDS + WAIT_CLICKHOUSE_SECONDS)) deadline=$((SECONDS + WAIT_CLICKHOUSE_SECONDS))
@@ -46,6 +49,8 @@ wait_for_clickhouse() {
} }
echo "=== Чистый прогон стартовой истории как источника аналитики ===" echo "=== Чистый прогон стартовой истории как источника аналитики ==="
echo "GEN_LAUNCH_PROFILE=${GEN_LAUNCH_PROFILE}"
echo "GEN_HISTORY_DURATION=${GEN_HISTORY_DURATION}"
echo "GEN_SEED=${GEN_SEED}" echo "GEN_SEED=${GEN_SEED}"
echo "GEN_MODEL_T0=${GEN_MODEL_T0}" echo "GEN_MODEL_T0=${GEN_MODEL_T0}"
echo "GEN_MODEL_T_END=${GEN_MODEL_T_END}" echo "GEN_MODEL_T_END=${GEN_MODEL_T_END}"
@@ -73,6 +78,7 @@ echo "Шаг 4: backfill стартовой истории в Kafka"
${COMPOSE_BIN} run --rm --no-deps \ ${COMPOSE_BIN} run --rm --no-deps \
-e GEN_RUN_MODE=backfill \ -e GEN_RUN_MODE=backfill \
-e GEN_STATE_RESET=true \ -e GEN_STATE_RESET=true \
-e GEN_LAUNCH_PROFILE="${GEN_LAUNCH_PROFILE}" \
-e GEN_SEED="${GEN_SEED}" \ -e GEN_SEED="${GEN_SEED}" \
-e GEN_MODEL_T0="${GEN_MODEL_T0}" \ -e GEN_MODEL_T0="${GEN_MODEL_T0}" \
-e GEN_MODEL_T_END="${GEN_MODEL_T_END}" \ -e GEN_MODEL_T_END="${GEN_MODEL_T_END}" \
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
#
# Запускает генератор через понятные глаголы поверх старых env-переменных.
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "Использование: $0 backfill|continue|reset" >&2
exit 2
fi
VERB="$1"
case "${VERB}" in
backfill|continue|reset) ;;
*)
echo "Ошибка: неизвестный глагол запуска: ${VERB}" >&2
exit 2
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
COMPOSE_BIN="${COMPOSE_BIN:-docker compose}"
PROFILE="${PROFILE:-${GEN_LAUNCH_PROFILE:-ci}}"
duration_args=()
if [[ -n "${GEN_HISTORY_DURATION:-}" ]]; then
duration_args=(--duration "${GEN_HISTORY_DURATION}")
fi
cd "${REPO_ROOT}"
launch_env="$(
PYTHONPATH="${REPO_ROOT}/generator/src" \
uv run python -m clickstream_generator.launch \
"${VERB}" \
--profile "${PROFILE}" \
"${duration_args[@]}"
)"
eval "${launch_env}"
echo "=== Запуск генератора: ${GEN_LAUNCH_VERB} ==="
echo "GEN_LAUNCH_PROFILE=${GEN_LAUNCH_PROFILE}"
if [[ -n "${GEN_HISTORY_DURATION:-}" ]]; then
echo "GEN_HISTORY_DURATION=${GEN_HISTORY_DURATION}"
fi
echo "GEN_RUN_MODE=${GEN_RUN_MODE}"
echo "GEN_STATE_RESET=${GEN_STATE_RESET}"
if [[ -n "${GEN_MODEL_T_END:-}" ]]; then
echo "GEN_MODEL_T_END=${GEN_MODEL_T_END}"
fi
echo ""
if [[ "${VERB}" == "backfill" ]]; then
echo "Шаг 1: запуск Kafka"
${COMPOSE_BIN} up -d kafka
echo "Шаг 2: сборка образа генератора"
${COMPOSE_BIN} build generator
echo "Шаг 3: backfill стартовой истории"
${COMPOSE_BIN} run --rm --no-deps \
-e GEN_RUN_MODE="${GEN_RUN_MODE}" \
-e GEN_STATE_RESET="${GEN_STATE_RESET}" \
-e GEN_LAUNCH_PROFILE="${GEN_LAUNCH_PROFILE}" \
-e GEN_SEED="${GEN_SEED}" \
-e GEN_MODEL_T0="${GEN_MODEL_T0}" \
-e GEN_MODEL_T_END="${GEN_MODEL_T_END}" \
-e GEN_MODEL_TIMEZONE="${GEN_MODEL_TIMEZONE}" \
-e GEN_MODEL_TIME_SPEED="${GEN_MODEL_TIME_SPEED}" \
-e GEN_TICK_SECONDS="${GEN_TICK_SECONDS}" \
-e GEN_LAMBDA_BASE_PER_MIN="${GEN_LAMBDA_BASE_PER_MIN}" \
-e GEN_JITTER_PCT="${GEN_JITTER_PCT}" \
-e GEN_MIN_EVENTS_PER_TICK="${GEN_MIN_EVENTS_PER_TICK}" \
-e GEN_MAX_EVENTS_PER_TICK="${GEN_MAX_EVENTS_PER_TICK}" \
generator
else
echo "Шаг 1: запуск live-генератора"
${COMPOSE_BIN} up -d --build generator
fi