- Зачем:
- до сих пор генератор умел собирать день, но не умел его отдать: топик
hits наполнялся пробником, а не настоящими данными. Тикет #41 доводит
события до стенда и закрывает форму на проводе, на которую обопрётся
типизированный ODS (#43).
- сериализатор один по решению спеки: второе место, печатающее событие в
JSON, разошлось бы с первым молча.
- Что:
- serialize.py — канонический сериализатор на orjson: единственное место,
где событие целиком становится JSON; 47 ключей всегда, «пусто» это
пустое значение, даты ISO-8601, ecommerce строкой. Вложенный блок
ecommerce в commerce.py вторым сериализатором не считается — правило
про событие, а не про блок внутри него.
- sinks.py — приёмники: файл (одно событие — одна строка) и Kafka (одно
событие — одно сообщение). Ключа у сообщения нет: WatchID уникален,
ключом он был бы ключом лишь на вид.
- player.py, cli.py — проигрыватель и интерфейс запуска: режимы batch и
live (темп ×60), несколько дней одним запуском, ограниченная пачка,
раздельные тайминги генерации и доставки, лаг в логе.
- день на оси и имя топика умолчаний не имеют: параметр, описывающий
среду или позицию, приходит от зовущего, иначе отказ до генерации.
Умолчания зерна, числа дней и темпа остаются — они описывают мир.
- generator/Dockerfile — свой образ: зависимости из uv.lock, база
закреплена до патча, раскладка репозитория сохранена ради каталога
товаров. Образ Airflow не тронут.
- разовая служба compose под профилем, цели generate-batch и
generate-live, .dockerignore, tmp/ в .gitignore.
- решения внесены в спеку (разделы 4, 8, 9), быстрый старт — в README.
- Проверка:
- make test 406 passed, make lint, make typecheck, make config-test.
- побайтовый детерминизм: два прогона дня в независимых процессах дают
один sha256; день в контейнере совпадает с днём на машине.
- на стенде: пакетный день доехал до stg.hits_raw_dist, счёт по
Distributed сошёлся — отправлено 50626, в таблице 50626.
- топик прочитан обеими нодами: clickhouse-01 раздел 0 (26368),
clickhouse-02 раздел 1 (24258).
- живой день: модельное время 01:00 на 60-й секунде, 02:00 на 120-й —
темп ×60, лаг печатается.
- форма на проводе в колонке raw: даты читаются глазами, ecommerce лежит
строкой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
52 lines
2.8 KiB
Bash
Executable File
52 lines
2.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
read -r -a COMPOSE_CMD <<<"${COMPOSE_BIN:-docker compose}"
|
|
readonly CACHE_DIR="$(mktemp -d)"
|
|
|
|
cleanup() {
|
|
rm -rf "$CACHE_DIR"
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
"${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" config --quiet
|
|
config_json="$("${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" config --format json)"
|
|
# Контекст сборки образов — корень репозитория, и локальный `.env` с настоящими
|
|
# паролями уехал бы в слой образа молча. Это единственная здешняя ошибка, о
|
|
# которой никто не узнает, пока образ не окажется у чужого.
|
|
for private_path in '.env' '.env.*' '*.pem' '*.key' '*.crt' 'secrets/' 'credentials/'; do
|
|
grep -qxF "$private_path" "$ROOT_DIR/.dockerignore"
|
|
done
|
|
grep -qx 'ARG SUPERSET_BASE_IMAGE' "$ROOT_DIR/infra/superset/Dockerfile"
|
|
jq -e '
|
|
.services.superset.image == "clickstream-superset:local" and
|
|
(.services.superset.build.args.SUPERSET_BASE_IMAGE | length > 0)
|
|
' >/dev/null <<<"$config_json"
|
|
grep -qx 'ARG AIRFLOW_BASE_IMAGE' "$ROOT_DIR/infra/airflow/Dockerfile"
|
|
jq -e '
|
|
.services["airflow-init"].image == "clickstream-airflow:local" and
|
|
(.services["airflow-init"].build.args.AIRFLOW_BASE_IMAGE | length > 0) and
|
|
all(
|
|
.services[];
|
|
((.environment // {}) | has("_PIP_ADDITIONAL_REQUIREMENTS") | not)
|
|
)
|
|
' >/dev/null <<<"$config_json"
|
|
jq -e '
|
|
.services["airflow-init"] as $service |
|
|
($service.environment.AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_PASSWORDS_FILE ==
|
|
"/opt/airflow/auth/simple_auth_manager_passwords.json") and
|
|
any($service.volumes[]; .source == "airflow_auth" and .target == "/opt/airflow/auth") and
|
|
any($service.volumes[]; .source == "airflow_logs" and .target == "/opt/airflow/logs")
|
|
' >/dev/null <<<"$config_json"
|
|
# Файлы DAG на машине никто не запускает: их разбирает обработчик внутри
|
|
# контейнера, и синтаксическая ошибка там всплывает не сообщением, а тем, что
|
|
# DAG молча не появился в списке. Локальный разбор — единственная дешёвая
|
|
# обратная связь. Скрипты стенда проверять так незачем: их запускают с этой же
|
|
# машины, и ошибка вылезает при первом же запуске с номером строки.
|
|
PYTHONPYCACHEPREFIX="$CACHE_DIR" uv run --no-project python -m compileall -q "$ROOT_DIR/dags"
|
|
git -C "$ROOT_DIR" diff --check
|
|
|
|
printf 'ЗЕЛЁНО: Compose, Python и пробельные ошибки diff проверены.\n'
|