Files
clickstream-data-platform/scripts/run-dag.sh
T
ddadmin 20b0666d28 fix(airflow): добавлен повтор запуска DAG после индексации
- Зачем:
  - при холодном старте GET уже видел DAG, пока POST запуска ещё считал его устаревшим.
- Что:
  - точный стартовый ответ 404 повторяется до двадцати раз с паузой три секунды.
  - прочие ответы и сетевые ошибки завершают команду без риска двойного запуска.
  - граница повторов объяснена в коде и документации проверок.
- Проверка:
  - выполнены `bash -n scripts/run-dag.sh`, `make config-test` и изолированные сценарии ответов API.
  - выполнены холодный `make up`, `make lint` и `make smoke` (21/21).
2026-08-22 02:20:01 -04:00

139 lines
6.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Запускает публичный даг Airflow и возвращает его итог вызывающему make.
set -Eeuo pipefail
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly DAG_ID="${1:-}"
# Каждый цикл ограничен двадцатью минутами: успешная пересборка заняла 3 м 9 с.
readonly ATTEMPTS=400
readonly PAUSE_SECONDS=3
readonly REPORT_EVERY=5
readonly TRIGGER_ATTEMPTS=20
read -r -a COMPOSE_CMD <<<"${COMPOSE_BIN:-docker compose}"
fail() {
printf 'ОШИБКА: %s\n' "$1" >&2
exit 1
}
compose() {
"${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" "$@"
}
next_step() {
case "$DAG_ID" in
world_initialize)
printf 'посмотрите журнал; повторите make up, а для недействительного мира — make rebuild-storage'
;;
world_recreate) printf 'устраните причину и повторите make rebuild-storage' ;;
*) printf 'проверьте журнал дага и повторите команду' ;;
esac
}
[[ -n "$DAG_ID" ]] || fail 'укажите идентификатор дага'
[[ $# -eq 1 ]] || fail 'ожидался один аргумент: идентификатор дага'
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 2>/dev/null || true)"
port="${binding##*:}"
[[ -n "$user" && -n "$password" ]] || fail 'не найдены учётные данные Airflow в Compose'
[[ -n "$port" ]] || fail 'API Airflow не запущен; сначала поднимите инфраструктуру'
# В API v2 logical_date=null означает событийный ручной запуск. Даты мира уже
# записаны в данных, календарь Airflow здесь не должен придумывать свою.
if ! response="$(curl -sS --fail-with-body --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)"; then
fail 'Airflow не принял учётные данные администратора'
fi
token="$(jq -r '.access_token // empty' <<<"$response")"
[[ -n "$token" ]] || fail 'Airflow не вернул токен доступа'
# Здоровый обработчик DAG ещё не означает, что новый файл уже появился в API.
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} не появился за $((ATTEMPTS * PAUSE_SECONDS)) с; $(next_step)"
# GET читает DagModel по идентификатору, а POST запуска принимает только запись
# с `is_stale = false`. На холодном старте GET уже может видеть даг, пока POST
# ещё отвечает этим 404. Повторяем только его: после сетевой ошибки запрос мог
# успеть создать запуск, и повтор с автоматическим run_id создал бы второй.
expected_missing_detail="Dag with dag_id: '${DAG_ID}' not found"
triggered=0
for ((attempt = 1; attempt <= TRIGGER_ATTEMPTS; attempt++)); do
if ! response_with_status="$(curl -sS --max-time 10 -w $'\n%{http_code}' -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)"; then
fail "Airflow не ответил при запуске дага ${DAG_ID}; проверьте список запусков перед повтором команды"
fi
http_status="${response_with_status##*$'\n'}"
response="${response_with_status%$'\n'*}"
detail="$(jq -r '.detail // .message // empty' <<<"$response" 2>/dev/null || true)"
case "$http_status" in
2??)
triggered=1
break
;;
404)
if [[ "$detail" == "$expected_missing_detail" ]]; then
if ((attempt == 1 || attempt % REPORT_EVERY == 0)); then
printf 'Airflow: даг %s ещё не готов к запуску; ждём (%d с).\n' \
"$DAG_ID" "$((attempt * PAUSE_SECONDS))"
fi
((attempt < TRIGGER_ATTEMPTS)) && sleep "$PAUSE_SECONDS"
continue
fi
;;
esac
fail "Airflow не запустил даг ${DAG_ID}: HTTP ${http_status}${detail:+: ${detail}}; $(next_step)"
done
((triggered == 1)) || \
fail "даг ${DAG_ID} не стал доступен для запуска за $((TRIGGER_ATTEMPTS * PAUSE_SECONDS)) с; $(next_step)"
run_id="$(jq -r '.dag_run_id // empty' <<<"$response")"
state="$(jq -r '.state // "queued"' <<<"$response")"
[[ -n "$run_id" ]] || fail "Airflow не вернул идентификатор запуска ${DAG_ID}"
encoded_run_id="$(jq -rn --arg value "$run_id" '$value | @uri')"
printf 'Airflow: даг %s, запуск %s, состояние %s.\n' "$DAG_ID" "$run_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" 2>/dev/null || true)"
case "$state" in
success)
printf 'ЗЕЛЁНО: даг %s, запуск %s, состояние success.\n' "$DAG_ID" "$run_id"
exit 0
;;
failed)
fail "даг ${DAG_ID}, запуск ${run_id}, состояние ${state}; $(next_step)"
;;
esac
if ((attempt % REPORT_EVERY == 0)); then
printf 'Airflow: даг %s, запуск %s, состояние %s; ждём (%d с).\n' \
"$DAG_ID" "$run_id" "${state:-неизвестно}" "$((attempt * PAUSE_SECONDS))"
fi
sleep "$PAUSE_SECONDS"
done
fail "даг ${DAG_ID}, запуск ${run_id}, состояние ${state:-неизвестно} не завершился за $((ATTEMPTS * PAUSE_SECONDS)) с; $(next_step)"