- Зачем: - при холодном старте GET уже видел DAG, пока POST запуска ещё считал его устаревшим. - Что: - точный стартовый ответ 404 повторяется до двадцати раз с паузой три секунды. - прочие ответы и сетевые ошибки завершают команду без риска двойного запуска. - граница повторов объяснена в коде и документации проверок. - Проверка: - выполнены `bash -n scripts/run-dag.sh`, `make config-test` и изолированные сценарии ответов API. - выполнены холодный `make up`, `make lint` и `make smoke` (21/21).
139 lines
6.9 KiB
Bash
Executable File
139 lines
6.9 KiB
Bash
Executable File
#!/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)"
|