feat(stand): добавлены стартовые слепки заказов

- Зачем:
  - свежий стенд должен показывать отставание ночной выгрузки заказов от кликстрима.
- Что:
  - после событий генератор отправляет семь слепков дней 0…6 в общий топик orders.
  - make up запускает orders_ingest, ограниченно ждёт его завершения и оставляет день 7 первому ходу мира.
  - README и карта проверок описывают второй источник и измеренную цену подъёма.
- Проверка:
  - make clean && make up; make smoke; make check-clickhouse; make check-services.
  - cd generator && make lint && make typecheck && make test.
This commit is contained in:
2026-08-20 19:12:01 +03:00
parent 664abf0160
commit ba4743ed52
5 changed files with 150 additions and 21 deletions
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# Последний шаг `make up`: запускает единственный путь приёма заказов и ждёт
# его конца. Слепки к этому моменту уже лежат в Kafka, а события доехали до ODS.
set -Eeuo pipefail
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly DAG_ID='orders_ingest'
# Потолок каждого ожидания — 300 с: больше чем всемеро от замеренных 40 с
# между запуском сценария и концом дага (#96). Это предел, а не бюджет времени.
readonly ATTEMPTS=100
readonly PAUSE_SECONDS=3
readonly REPORT_EVERY=5
read -r -a COMPOSE_CMD <<<"${COMPOSE_BIN:-docker compose}"
fail() {
printf 'ОШИБКА: %s\n' "$1" >&2
exit 1
}
compose() {
"${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" "$@"
}
config="$(compose config --format json)"
user="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_USER // empty' <<<"$config")"
password="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_PASSWORD // empty' <<<"$config")"
binding="$(compose port airflow-apiserver 8080)"
port="${binding##*:}"
[[ -n "$port" ]] || fail 'не найден отображённый порт API Airflow'
response="$(curl -sf --max-time 10 -X POST \
-H 'Content-Type: application/json' \
-d "$(jq -cn --arg username "$user" --arg password "$password" \
'{username: $username, password: $password}')" \
"http://127.0.0.1:${port}/auth/token" 2>/dev/null || true)"
token="$(jq -r '.access_token // empty' <<<"$response")"
[[ -n "$token" ]] || fail 'Airflow не принял учётные данные администратора'
# После `up --wait` обработчик DAG здоров, но файл мог ещё не попасть в список.
# Ограниченный опрос закрывает эту гонку без паузы наугад.
dag=''
for ((attempt = 1; attempt <= ATTEMPTS; attempt++)); do
dag="$(curl -sf --max-time 10 \
-H "Authorization: Bearer ${token}" \
"http://127.0.0.1:${port}/api/v2/dags/${DAG_ID}" \
2>/dev/null || true)"
jq -e --arg dag_id "$DAG_ID" '.dag_id == $dag_id' \
>/dev/null 2>&1 <<<"$dag" && break
sleep "$PAUSE_SECONDS"
done
jq -e --arg dag_id "$DAG_ID" '.dag_id == $dag_id' \
>/dev/null 2>&1 <<<"$dag" || \
fail "даг ${DAG_ID} не появился в Airflow за $((ATTEMPTS * PAUSE_SECONDS)) с"
# В API v2 Airflow 3.3 logical_date=null означает событийный ручной запуск.
# Это уместно для дага без расписания: календарь Airflow не подменяет дату
# слепка, которая уже записана в сообщениях.
response="$(curl -sf --max-time 10 -X POST \
-H "Authorization: Bearer ${token}" \
-H 'Content-Type: application/json' \
-d '{"logical_date":null}' \
"http://127.0.0.1:${port}/api/v2/dags/${DAG_ID}/dagRuns" \
2>/dev/null || true)"
run_id="$(jq -r '.dag_run_id // empty' <<<"$response")"
[[ -n "$run_id" ]] || fail "Airflow не запустил даг ${DAG_ID}"
encoded_run_id="$(jq -rn --arg value "$run_id" '$value | @uri')"
printf 'Ждём приём стартовых слепков дагом %s...\n' "$DAG_ID"
state=''
for ((attempt = 1; attempt <= ATTEMPTS; attempt++)); do
response="$(curl -sf --max-time 10 \
-H "Authorization: Bearer ${token}" \
"http://127.0.0.1:${port}/api/v2/dags/${DAG_ID}/dagRuns/${encoded_run_id}" \
2>/dev/null || true)"
state="$(jq -r '.state // empty' <<<"$response")"
case "$state" in
success)
printf 'ЗЕЛЁНО: стартовые слепки приняты в ODS.\n'
exit 0
;;
failed)
fail "даг ${DAG_ID} завершился с ошибкой; запуск ${run_id}"
;;
esac
if ((attempt % REPORT_EVERY == 0)); then
printf 'Приём ещё не завершён: состояние %s, ждём дальше (%d с)...\n' \
"${state:-неизвестно}" "$((attempt * PAUSE_SECONDS))"
fi
sleep "$PAUSE_SECONDS"
done
fail "даг ${DAG_ID} не завершился за $((ATTEMPTS * PAUSE_SECONDS)) с:
состояние ${state:-неизвестно}, запуск ${run_id}"