- Зачем:
- агент использовал CLI (`docker exec ... airflow dags unpause`) вместо REST API,
что приводило к зависанию DAG'ов в статусе `queued`.
- Что:
- e2e-etl-test-protocol: явный запрет на CLI-команды, REST API как основной метод,
шаблон unpause+trigger для каждого слоя, пример проверки статуса через API.
- agent-dag-testing: убрана оговорка «если он новый» — unpause обязателен всегда.
- TODO: пункт «стенд поднимается на чистой машине» отмечен выполненным.
- Проверка:
- прочитать docs/e2e-etl-test-protocol.md и docs/agent-dag-testing.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
10 KiB
Протокол сквозного (E2E) тестирования ETL
Этот документ описывает процедуру полной проверки цепочки ETL: STG -> ODS -> DDS -> DM.
Цель теста — убедиться в корректности инкрементальной загрузки, работы паттерна Temporary Table и механизмов HWM.
Управление DAG'ами — только через Airflow UI или REST API.
Не используйте CLI-команды внутри контейнера (docker exec ... airflow dags ...) — они могут молча не сработать (например, unpause не снимает паузу). REST API работает надёжно и приближает опыт к реальной боевой эксплуатации.
1. Подготовка окружения
Убедитесь, что все сервисы запущены и генератор инициализирован.
make up
make bookings-init
Доступ к API:
В docker-compose.yml включена базовая аутентификация (basic_auth).
Для curl-запросов используйте учетные данные из вашего .env файла (переменные AIRFLOW_USER и AIRFLOW_PASSWORD, по умолчанию admin:admin).
- UI:
http://localhost:8080 - API Endpoint:
http://localhost:8080/api/v1
2. Очистка данных (Reset)
Перед началом теста необходимо полностью очистить все слои DWH.
make dwh-truncate
(Если вы меняли DDL, лучше полностью пересоздать схемы: make gp-psql -c "DROP SCHEMA IF EXISTS ods CASCADE; DROP SCHEMA IF EXISTS dds CASCADE; DROP SCHEMA IF EXISTS dm CASCADE; CREATE SCHEMA ods; CREATE SCHEMA dds; CREATE SCHEMA dm;")
3. Этап 1: Создание схем и Загрузка за первый день (Initial Load)
Рекомендуется запускать слои последовательно, дожидаясь завершения предыдущего.
Создание DDL
Откройте Airflow UI (http://localhost:8080) и нажмите кнопку ▶ Play -> Trigger DAG для DDL-дагов:
bookings_stg_ddlbookings_ods_ddlbookings_dds_ddlbookings_dm_ddl
Запуск пайплайна (Day 1)
После успешного создания таблиц, запустите DAG загрузки bookings_to_gp_stage.
Через UI: откройте http://localhost:8080, снимите DAG с паузы (переключатель слева от названия) и нажмите ▶ Play -> Trigger DAG.
Через REST API (рекомендуется для автоматизации и агентов):
Важно: при старте стенда все DAG-и находятся на паузе. Каждый DAG перед первым запуском нужно «разморозить» (unpause), иначе запуск зависнет в статусе queued. Для каждого DAG требуется два шага: 1) unpause, 2) trigger.
# 1. Снятие с паузы (обязательно перед первым запуском!)
curl -s -X PATCH "http://localhost:8080/api/v1/dags/bookings_to_gp_stage" \
--user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \
-H "Content-Type: application/json" \
-d '{"is_paused": false}'
# 2. Запуск
curl -s -X POST "http://localhost:8080/api/v1/dags/bookings_to_gp_stage/dagRuns" \
--user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \
-H "Content-Type: application/json" \
-d '{}'
Проверка статуса
Дождитесь, пока DAG перейдет в статус success (следите в UI или опрашивайте через API):
curl -s "http://localhost:8080/api/v1/dags/bookings_to_gp_stage/dagRuns?order_by=-execution_date&limit=1" \
--user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" | python3 -c "import sys,json; print(json.load(sys.stdin)['dag_runs'][0]['state'])"
Запуск остальных слоёв
Поочерёдно запускайте каждый следующий слой по той же схеме: unpause + trigger (замените <dag_id>):
bookings_to_gp_odsbookings_to_gp_ddsbookings_to_gp_dm
# Повторите для каждого DAG из списка выше
DAG_ID=bookings_to_gp_ods
# 1. Unpause
curl -s -X PATCH "http://localhost:8080/api/v1/dags/${DAG_ID}" \
--user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \
-H "Content-Type: application/json" \
-d '{"is_paused": false}'
# 2. Trigger
curl -s -X POST "http://localhost:8080/api/v1/dags/${DAG_ID}/dagRuns" \
--user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \
-H "Content-Type: application/json" \
-d '{}'
4. Этап 2: Проверка инкремента
Эмулируйте появление данных за второй день и проверьте дозагрузку. DAG слоя STG автоматически сгенерирует новый день в базе-источнике перед загрузкой.
# Повторный запуск цепочки ETL (DAG STG сам сгенерирует новый день)
# Снова нажмите "Trigger DAG" в UI для каждого слоя (STG -> ODS -> DDS -> DM).
5. Цикл отладки: Логи и Перезапуск (Clear)
Если DAG упал, не нужно пересоздавать стенд с нуля. Airflow позволяет исправить код и перезапустить только упавшие задачи.
Для выполнения команд ниже вам понадобится Run ID упавшего запуска. Его можно скопировать из UI (вкладка Graph -> кликнуть на фон сетки -> вкладка Details -> Run ID) или получить последним API-запросом:
# Получить Run ID последнего запуска ODS
curl -s "http://localhost:8080/api/v1/dags/bookings_to_gp_ods/dagRuns?order_by=-execution_date&limit=1" \
--user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" | grep -o '"dag_run_id": "[^"]*"'
Чтение логов через API
Подставьте ваш <RUN_ID> (например, manual__2026-03-03T...) и имя упавшей таски:
curl -s "http://localhost:8080/api/v1/dags/bookings_to_gp_ods/dagRuns/<RUN_ID>/taskInstances/<TASK_ID>/logs/1" \
--user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}"
Перезапуск задачи (Clear)
- Прочитайте ошибку в логах.
- Исправьте SQL-файл локально на хосте.
- Очистите состояние упавших задач (
only_failed: true) в конкретном запуске, передав ваш<RUN_ID>:
curl -s -X POST "http://localhost:8080/api/v1/dags/bookings_to_gp_ods/clearTaskInstances" \
--user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \
-H "Content-Type: application/json" \
-d '{
"only_failed": true,
"reset_dag_runs": true,
"dag_run_id": "<RUN_ID>"
}'
После этого планировщик подхватит обновленный SQL-код и продолжит выполнение DAG с точки падения. Вы также можете сделать это в UI: клик по упавшей задаче -> кнопка Clear.
6. Финальная верификация (Критерии успеха)
Выполните SQL-запрос для сверки данных:
make gp-psql -c "
SELECT 'STG' as layer, COUNT(*) FROM stg.bookings
UNION ALL
SELECT 'ODS' as layer, COUNT(*) FROM ods.bookings
UNION ALL
SELECT 'DDS' as layer, COUNT(*) FROM dds.fact_flight_sales
UNION ALL
SELECT 'DM ' as layer, COUNT(*) FROM dm.sales_report;
"
Критерии корректности:
- Инкремент STG: Количество строк в
stg.bookingsпосле Этапа 2 больше, чем после Этапа 1. - Инкремент ODS: Количество строк в
ods.bookingsвыросло. В ODS нет дублей (SELECT book_ref FROM ods.bookings GROUP BY book_ref HAVING COUNT(*) > 1должен вернуть 0 строк). - ODS Справочники: Количество строк в
ods.airportsиods.routesне должно меняться между днями (работает паттерн TRUNCATE+INSERT полного снимка). - HWM в DM: Витрина
sales_reportсодержит данные за оба дня. ЗначениеCOUNT(*)после Этапа 2 выросло. - Lineage: Поля
_load_idи_load_tsво всех слоях содержат метки соответствующих запусков (manual__...).
7. Зафиксированный опыт (Типичные ошибки)
- Рассинхронизация DDL и Load скриптов: Частая причина падения ODS/DDS — несовпадение имен колонок (например,
amountvssegment_amount) или типов данных (например,INTEGER[]vsTEXT) между схемой таблицы и запросом загрузки. Внимательно читайте логи задачи. - Работа с массивами: При генерации
hashdiffв Greenplum/PostgreSQL нельзя использовать пустую строку''вCOALESCEдля массива. Массив нужно предварительно привести к тексту:COALESCE(days_of_week::TEXT, ''). - Кавычки в psql: При выполнении ручных проверок через
psql -c "..."помните, что строковые литералы должны оборачиваться в одинарные кавычки ('text'), а двойные кавычки ("text") интерпретируются как идентификаторы колонок.