Files
clickstream-data-platform/tests/stand-smoke-guards.sh
T
ddadminandClaude Opus 5 dc12953cac refactor(airflow): пробник ClickHouse разбит на четыре задачи
Зачем: пробник был одной задачей — в интерфейсе Airflow один красный
квадрат, а место отказа приходилось искать по журналу. Ручная машинерия
проброса и сведения ошибок занимала больше места, чем сама проверка, и
читатель продирался через неё раньше, чем понимал, что пробник проверяет.
Пробники — единственный образец DAG в стенде, по ним будут писать
остальные.

Что: test_clickhouse разбит на prepare_tables, write_marker,
read_from_node_2 и cleanup_tables; маркер и имя принявшей запись ноды едут
между задачами через XCom строками. Снято сведение ошибок: except
BaseException, ExceptionGroup, add_note и накопление ошибок в список;
клиент каждая задача заводит общим помощником и закрывает в finally.
Ноды описаны константой NODES парами «имя для человека — источник для
запроса», булев переключатель и параллельные списки подписей ушли.
Уборка идёт обычным правилом запуска, а не all_done: состояние запуска
Airflow считает по концам графа, и уборка, отработавшая после отказа,
покрасила бы в зелёный запуск с упавшей проверкой — решение записано
в ADR 0003. Комментарии остались в четырёх местах: чтение ноды 2 через
remote(), импорт клиента внутри функции, правило запуска уборки и
автосоздание топика в test_kafka. Малые проверки: заглушка task принимает
обе формы декоратора, проверка сведения ошибок заменена проверками
уборки. Красный путь ищет образец по журналам всех задач последнего
запуска, а не в одном самом свежем.

Проверка: make config-test, make smoke (25 проверок) и make smoke-guards
зелены. Разбитый пробник укладывается в 5 секунд из 120, отведённых
run_airflow_probe, — предел не трогаем.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 18:36:52 +03:00

194 lines
7.1 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
set -uo pipefail
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
read -r -a COMPOSE_CMD <<<"${COMPOSE_BIN:-docker compose}"
readonly LOG_FILE="$(mktemp)"
readonly CLICKHOUSE_LOCAL_TABLE="airflow_probe_local"
readonly CLICKHOUSE_DISTRIBUTED_TABLE="airflow_probe_distributed"
restored=0
passed=0
red_smoke_pid=''
compose() {
"${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" "$@"
}
published_port() {
local binding
local service="$1"
local container_port="$2"
binding="$(compose port "$service" "$container_port" 2>/dev/null || true)"
printf '%s\n' "${binding##*:}"
}
query_node_2() {
local sql="$1"
local port
port="$(published_port clickhouse-02 8123)"
curl -sf --max-time 2 --data-binary "$sql" "http://127.0.0.1:${port}/"
}
break_clickhouse_probe() {
local deadline
local table_state
deadline=$((EPOCHSECONDS + 60))
printf 'Ожидание служебных таблиц на ноде 2, предел 60 секунд...\n'
while [[ "$EPOCHSECONDS" -lt "$deadline" ]]; do
table_state="$(query_node_2 "
SELECT
countIf(name = '${CLICKHOUSE_LOCAL_TABLE}'),
countIf(name = '${CLICKHOUSE_DISTRIBUTED_TABLE}')
FROM system.tables
WHERE database = 'default'
AND name IN (
'${CLICKHOUSE_LOCAL_TABLE}',
'${CLICKHOUSE_DISTRIBUTED_TABLE}'
)
FORMAT TSV
" 2>/dev/null || true)"
if [[ "$table_state" == $'1\t1' ]]; then
query_node_2 \
"DROP TABLE IF EXISTS default.${CLICKHOUSE_LOCAL_TABLE} SYNC" \
>/dev/null
table_state="$(query_node_2 "
SELECT
countIf(name = '${CLICKHOUSE_LOCAL_TABLE}'),
countIf(name = '${CLICKHOUSE_DISTRIBUTED_TABLE}')
FROM system.tables
WHERE database = 'default'
AND name IN (
'${CLICKHOUSE_LOCAL_TABLE}',
'${CLICKHOUSE_DISTRIBUTED_TABLE}'
)
FORMAT TSV
" 2>/dev/null || true)"
if [[ "$table_state" == $'0\t1' ]]; then
printf 'Служебная локальная таблица удалена только на ноде 2.\n'
return
fi
return 1
fi
sleep 0.05
done
return 1
}
# Пробник разбит на четыре задачи, и упасть может любая из них: поломка на ноде
# 2 видна и проверке набора таблиц, и чтению маркера. Поэтому берём последний
# каталог запуска целиком и ищем образец по журналам всех его задач.
clickhouse_break_is_reported() {
compose exec -T airflow-scheduler bash -ceu '
latest="$(
find /opt/airflow/logs/dag_id=test_clickhouse \
-mindepth 1 -maxdepth 1 -type d -name "run_id=*" -printf "%T@ %p\n" |
sort -nr |
head -n 1
)"
latest="${latest#* }"
test -n "$latest"
grep -Eqr --include="attempt=1.log" \
"неверный набор таблиц на ноде 2|Unknown table expression identifier '\''default.airflow_probe_local'\''" \
"$latest"
'
}
probe_failure_is_reported() {
local dag_id="$1"
grep -Eq \
"ОШИБКА: (пробник ${dag_id}|Airflow не показал пробник ${dag_id})" \
"$LOG_FILE"
}
restore_stand() {
make --no-print-directory -C "$ROOT_DIR" COMPOSE="${COMPOSE_BIN:-docker compose}" up >/dev/null
}
on_exit() {
local status=$?
trap - EXIT INT TERM
if [[ -n "$red_smoke_pid" ]]; then
kill -TERM -- "-$red_smoke_pid" >/dev/null 2>&1 || true
wait "$red_smoke_pid" >/dev/null 2>&1 || true
fi
rm -f "$LOG_FILE"
if [[ "$restored" -eq 0 ]] && ! restore_stand; then
printf 'ОШИБКА: не удалось восстановить стенд после проверки.\n' >&2
status=1
fi
exit "$status"
}
trap on_exit EXIT
trap 'exit 130' INT TERM
if make --no-print-directory -C "$ROOT_DIR" COMPOSE="${COMPOSE_BIN:-docker compose}" smoke >"$LOG_FILE" 2>&1; then
passed=$((passed + 1))
printf 'ЗЕЛЁНО: перед проверкой отказа make smoke проходит полностью.\n'
else
printf 'ОШИБКА: исходный стенд не проходит make smoke; проверка отказа недостоверна.\n' >&2
exit 1
fi
compose stop prometheus >/dev/null
setsid make --no-print-directory -C "$ROOT_DIR" \
COMPOSE="${COMPOSE_BIN:-docker compose}" smoke >"$LOG_FILE" 2>&1 &
red_smoke_pid=$!
if ! break_clickhouse_probe; then
printf 'ОШИБКА: не удалось удалить служебную таблицу только на ноде 2.\n' >&2
exit 1
fi
compose stop kafka >/dev/null
wait "$red_smoke_pid"
status=$?
red_smoke_pid=''
red_path_ok=1
if [[ "$status" -eq 0 ]]; then
printf 'ОШИБКА: make smoke остался зелёным после внесённых поломок.\n' >&2
red_path_ok=0
fi
if ! grep -q 'ОШИБКА: сервис prometheus' "$LOG_FILE"; then
printf 'ОШИБКА: make smoke не назвал остановленный prometheus.\n' >&2
red_path_ok=0
fi
if ! probe_failure_is_reported test_clickhouse; then
printf 'ОШИБКА: make smoke не назвал пробник test_clickhouse.\n' >&2
red_path_ok=0
fi
if ! probe_failure_is_reported test_kafka; then
printf 'ОШИБКА: make smoke не назвал пробник test_kafka.\n' >&2
red_path_ok=0
fi
if ! clickhouse_break_is_reported; then
printf 'ОШИБКА: журнал test_clickhouse не связал отказ с удалённой таблицей на ноде 2.\n' >&2
red_path_ok=0
fi
if [[ "$red_path_ok" -eq 1 ]]; then
passed=$((passed + 1))
printf 'ЗЕЛЁНО: удаление таблицы на ноде 2 и остановка prometheus с kafka делают make smoke красным; оба пробника названы в отчёте.\n'
else
printf 'ОШИБКА: проверка красного пути завершилась с кодом make smoke %s.\n' "$status" >&2
exit 1
fi
if ! restore_stand; then
printf 'ОШИБКА: не удалось восстановить стенд после проверки.\n' >&2
exit 1
fi
restored=1
if make --no-print-directory -C "$ROOT_DIR" COMPOSE="${COMPOSE_BIN:-docker compose}" smoke >"$LOG_FILE" 2>&1; then
passed=$((passed + 1))
printf 'ЗЕЛЁНО: после восстановления make smoke снова проходит полностью.\n'
else
printf 'ОШИБКА: после восстановления стенд не проходит make smoke.\n' >&2
exit 1
fi
rm -f "$LOG_FILE"
trap - EXIT INT TERM
printf 'ИТОГ: пройдено %d, ошибок 0\n' "$passed"