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:
@@ -97,6 +97,9 @@ class Config:
|
||||
if os.getenv("GEN_STARTUP_HISTORY_ARTIFACT")
|
||||
else None
|
||||
)
|
||||
launch_profile: str = field(
|
||||
default_factory=lambda: os.getenv("GEN_LAUNCH_PROFILE", "ci")
|
||||
)
|
||||
|
||||
def __post_init__(self):
|
||||
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"lambda_base={self.config.lambda_base_per_min}/min, "
|
||||
f"jitter={self.config.jitter_pct}%, "
|
||||
f"launch_profile={self.config.launch_profile}, "
|
||||
f"state_enabled={self.config.state_enabled}, "
|
||||
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_timezone": config.model_timezone,
|
||||
"run_mode": "backfill",
|
||||
"launch_profile": config.launch_profile,
|
||||
"generation_settings": generation_settings_from_config(config),
|
||||
"state_version": state.version,
|
||||
"state": {
|
||||
|
||||
Reference in New Issue
Block a user