Files
airflow-greenplum/docs/reports/2026-09-22-pxf-airports.md
T

17 KiB
Raw Blame History

Проверка лабораторной PXF → STG, 22 сентября 2026

Задача #9 из серии #8. Материалы автора описывают границу самостоятельной работы и эталонное решение.

Среда и начальное состояние

Проверена учебная ветка на базе main (1dd5838) с добавленными lab_pxf_airports и независимой сверкой. Студенческие заглушки ODS/DDS/DM сохранены. Полная реализация из solution в этот стенд не переносилась. Версии: Airflow 2.9.2, Greenplum 6.27.1, Postgres 16.

make up
make bookings-init
make ddl-gp

После подготовки выполнена обычная цепочка STG → ODS → DDS → DM с Run ID lab9-baseline-stage, lab9-baseline-ods, lab9-baseline-dds, lab9-baseline-dm. Все четыре запуска завершились успешно до вычитания.

В локальном .env использованы свободные порты: Airflow 18090, метабаза 15433, Bookings 15434, Greenplum 15435. Airflow ограничен одним web-worker, PARALLELISM=2, MAX_ACTIVE_TASKS_PER_DAG=2, PARSING_PROCESSES=1. Эти настройки не внесены в репозиторий; SQL не изменен.

Первый прогон базового генератора прервался: Postgres сообщил о выходе server process с кодом 2 и переинициализации, после чего источник оказался пустым. OOMKilled=false, рестарта контейнера не было; доказанной причины нет. После ограничения ресурсов только своего Airflow источник восстановлен через make bookings-init, а базовая цепочка успешно повторена. Чужие контейнеры не изменялись. Дальнейшие снимки справочников получены через lab_pxf_airports, без генерации транзакционных дней.

DAG запускались через POST /api/v1/dags/{dag_id}/dagRuns с телом {"dag_run_id": "указанный в протоколе Run ID", "conf": {}}. Перед следующим шагом проверялось состояние success всего запуска.

Вычитание, A и повтор

A/B/C — обозначения авторского прогона. В ученической инструкции это первая загрузка, новый снимок после переименования и снимок после восстановления. Файлам source-A.json и source-B.json в задании соответствуют airports-before.json и airports-after.json.

mkdir -p data/lab9
uv run --no-project scripts/check_airports_snapshot.py export data/lab9/source-A.json

Прямой экспорт Postgres: 5501 аэропорт, все шесть бизнес-полей. Внешний DDL удален из sql/stg/airports_ddl.sql, загрузка заменена на SELECT 1;. В Greenplum:

DROP EXTERNAL TABLE IF EXISTS stg.airports_ext;
TRUNCATE TABLE stg.airports;
SELECT COUNT(*) FROM stg.airports;

Получено 0 строк. Заглушка успешно выполнилась, но сверка отказала:

uv run --no-project scripts/check_airports_snapshot.py check lab9-empty data/lab9/source-A.json

Код 1: Не получены ключи, всего 5501. Запрос к stg.airports_ext получил relation does not exist. Старые данные не маскируют вычитание. После возврата эталонных DDL и загрузки применен файл:

\set ON_ERROR_STOP on
\i /sql/stg/airports_ddl.sql
SELECT * FROM stg.airports_ext ORDER BY airport_code LIMIT 3;

PXF вернул AAA, AAC, AAE с исходными JSON, координатами и часовыми поясами. Успешно выполнен lab_pxf_airports с Run ID lab9-A-stage.

uv run --no-project scripts/check_airports_snapshot.py check lab9-A-stage data/lab9/source-A.json

Код 0: 5501 ключ, все бизнес-поля и метки соответствуют требованиям. Повторены только load_airports_to_stg, затем check_airports_dq в том же запуске через POST /api/v1/dags/lab_pxf_airports/clearTaskInstances:

{
  "dag_run_id": "lab9-A-stage",
  "task_ids": ["load_airports_to_stg"],
  "dry_run": true,
  "only_failed": false,
  "only_running": false,
  "include_upstream": false,
  "include_downstream": false,
  "include_subdags": false,
  "include_parentdag": false,
  "reset_dag_runs": true
}

Предварительный ответ содержал ровно одну выбранную задачу, затем отправлено то же тело с dry_run=false. Для DQ заменен task_ids. Повторная сверка A — код 0. При сравнении по ключу все строки, включая метки, совпали до и после повтора. Базовый генератор не повторялся.

В браузере проверены граф, Run ID и окно Clear task. Подтверждение повтора выполнялось через API. Первоначальный тестовый сценарий ошибочно сравнивал порядок строк MPP-ответа. После сравнения по ключу повтор подтвердился; ошибка не относилась к загрузке. Независимая сверка изначально сопоставляет строки по ключу, экспорт дополнительно сортирует их для файлового сравнения.

Изменение источника и неверная копия

В Postgres выполнено:

CREATE TABLE public.lab9_airport_original AS
SELECT airport_code, airport_name
FROM bookings.airports_data ORDER BY airport_code LIMIT 1;

UPDATE bookings.airports_data AS a
SET airport_name = jsonb_set(
    b.airport_name, '{ru}', to_jsonb((b.airport_name->>'ru') || ' [lab9]'), false
)
FROM public.lab9_airport_original AS b
WHERE a.airport_code = b.airport_code
RETURNING a.airport_code, b.airport_name AS original, a.airport_name AS changed;

Изменен один ключ AAA: Анаа → Анаа [lab9], английское Anaa и структура JSON сохранены. Внешняя таблица сразу вернула новое имя, первый снимок сохранил старое.

uv run --no-project scripts/check_airports_snapshot.py export data/lab9/source-B.json
uv run --no-project scripts/check_airports_snapshot.py check lab9-A-stage data/lab9/source-B.json

В источнике по-прежнему 5501 строка. Проверка — код 1, AAA: отличается airport_name. Затем в Greenplum создана старая копия с новым ID и заполненными метками:

INSERT INTO stg.airports
SELECT airport_code, airport_name, city, country, coordinates, timezone,
       now(), now(), 'lab9-bad-copy'
FROM stg.airports WHERE _load_id = 'lab9-A-stage';
uv run --no-project scripts/check_airports_snapshot.py check lab9-bad-copy data/lab9/source-B.json

Вставлено 5501 строк; проверка отвергла копию с кодом 1 и ошибкой имени. Контрольные строки удалены:

DELETE FROM stg.airports WHERE _load_id = 'lab9-bad-copy';

Снимок B и передача в ODS

После успешного lab_pxf_airports, Run ID lab9-B-stage:

uv run --no-project scripts/check_airports_snapshot.py check lab9-B-stage data/lab9/source-B.json
uv run --no-project scripts/check_airports_snapshot.py check lab9-A-stage data/lab9/source-A.json

Обе сверки прошли. Общий батч проверен запросом:

SELECT 'airports' AS object, _load_id, COUNT(*) FROM stg.airports WHERE _load_id = 'lab9-B-stage' GROUP BY _load_id
UNION ALL
SELECT 'airplanes', _load_id, COUNT(*) FROM stg.airplanes WHERE _load_id = 'lab9-B-stage' GROUP BY _load_id
UNION ALL
SELECT 'routes', _load_id, COUNT(*) FROM stg.routes WHERE _load_id = 'lab9-B-stage' GROUP BY _load_id
UNION ALL
SELECT 'seats', _load_id, COUNT(*) FROM stg.seats WHERE _load_id = 'lab9-B-stage' GROUP BY _load_id;
Таблица Строк с lab9-B-stage
airports 5501
airplanes 10
routes 860
seats 1741

До запуска ODS команда ниже отказала из-за прежнего батча и названия, после запуска — прошла:

uv run --no-project scripts/check_airports_snapshot.py check lab9-B-stage data/lab9/source-B.json --layer ods

Выполнен обычный bookings_to_gp_ods, Run ID lab9-B-ods, conf={}. XCom return_value задачи resolve_stg_batch_id равен lab9-B-stage. Все 5501 строк ODS совпали с ожидаемыми данными и меткой B; название AAA — Анаа [lab9]. Выбор через conf не подменял resolver.

Восстановление и продолжение

В Postgres дважды выполнен UPDATE восстановления:

UPDATE bookings.airports_data AS a
SET airport_name = b.airport_name
FROM public.lab9_airport_original AS b
WHERE a.airport_code = b.airport_code
RETURNING a.airport_code, a.airport_name;

Оба раза UPDATE 1, исходный JSON восстановлен. Файловая проверка:

uv run --no-project scripts/check_airports_snapshot.py export data/lab9/source-C.json
cmp data/lab9/source-A.json data/lab9/source-C.json

Файлы совпали побайтово. Затем успешно выполнены:

DAG Run ID
lab_pxf_airports lab9-C-stage
bookings_to_gp_ods lab9-C-ods
bookings_to_gp_dds lab9-C-dds
bookings_to_gp_dm lab9-C-dm

Resolver с пустым conf выбрал lab9-C-stage. Сверки A, B и C со своими источниками, а также всей ODS с C прошли.

SELECT _load_id, COUNT(*) FROM stg.airports GROUP BY _load_id ORDER BY _load_id;
SELECT airport_bk, airport_name FROM dds.dim_airports ORDER BY airport_bk LIMIT 1;
SELECT COUNT(*) FROM dds.fact_flight_sales;
SELECT COUNT(*) FROM dm.sales_report;

В STG остались A/B/C по 5501 строк каждый. В dds.dim_airports у AAA снова Анаа. Факт содержит 1 608 696 строк, dm.sales_report — 25 732. Остальные справочники сохраняют также батчи быстрого старта; транзакционные данные не менялись во время A/B/C. Новый общий снимок пригоден для заданий ods.airplanes и ods.seats. Полный bookings_validate не запускался: будущие задания остаются заглушками.

Проверки кода и документации

  • make lint — успешно.
  • make test — 14 passed, 15 skipped: десять тестов сверки и четыре SQL-контракта прошли. Четырнадцать smoke-тестов DAG пропущены из-за существующих заглушек Airflow в conftest.py; один тест ODS требует явного включения интеграционного прогона.
  • В настоящем Airflow отдельно проверен DagBag: ошибок импорта нет, восемь задач нового DAG, SQL-шаблоны раскрываются в исходные файлы, зависимости загрузок и DQ верны. REST API не сообщил ошибок импорта.
  • Проверены локальные ссылки и актуальность карты. В браузере пройден маршрут README → задания → PXF → ODS и ссылка карточки stg.airports.
  • git diff --check пройден в учебной и авторской рабочих копиях.

Учебный прогон выполнен до редакционной перестройки урока. Сценарии, SQL загрузки, DAG и сверка сохранены; изменились порядок объяснений, имена файлов в примерах и показ подсказок. После редактирования проверены 296 локальных ссылок учебной ветки и 9 ссылок авторских материалов, синтаксис bash-примеров, актуальность карты (39 объектов, 90 связей, 216 ссылок). В локальном HTML-предпросмотре Chromium раскрыты все восемь подсказок, проверено отображение девяти блоков кода и переход к ODS.

Упрощенное завершение урока использует исходный контроль вместо третьего файла. На сохраненном состоянии прогона отдельно выполнены обе команды:

uv run --no-project scripts/check_airports_snapshot.py check lab9-C-stage data/lab9/source-A.json
uv run --no-project scripts/check_airports_snapshot.py check lab9-C-stage data/lab9/source-A.json --layer ods

Обе прошли: 5501 ключ, все бизнес-поля и метки соответствуют требованиям. Полный прогон после редакционной правки не повторялся.

Проверка исправления после ревью

Независимое ревью Standards не обнаружило нарушений. Ревью Spec нашло ловушку при исправлении SQL: уже записанные дубли остаются в батче после добавления NOT EXISTS. Инструкция дополнена очисткой только ошибочного _load_id, восстановлением снимка и повтором на заполненном снимке. Исправление повторно просмотрено; замечание закрыто.

В отдельном батче lab9-review-retry дважды выполнен INSERT всех строк stg.airports_ext с текущими метками. Получено 11002 строки, сверка отказала из-за дублей. После выполнения эталонного airports_load.sql с этим же Run ID осталось 11002 строки; сверка снова отказала.

DELETE FROM stg.airports WHERE _load_id = 'lab9-review-retry';

После очистки эталонная загрузка дала 5501 строку и успешную сверку. Ее повтор также прошел. Временный батч затем удален тем же DELETE; состав и количества A/B/C сохранились. Это отдельный SQL-опыт восстановления, а не повтор всего учебного маршрута.

Перед публикацией make lint и make test снова прошли (14 passed, 15 skipped). Состояния семи запусков A/B/C и нижних слоев перечитаны через Airflow API: все success. Выдержка настоящего лога check_airports_dq первого запуска:

[2026-09-22T17:42:17.505+0300] {taskinstance.py:1206} INFO - Marking task as SUCCESS. dag_id=lab_pxf_airports, task_id=check_airports_dq, run_id=lab9-A-stage, execution_date=20260922T144212, start_date=20260922T144216, end_date=20260922T144217
[2026-09-22T17:42:17.586+0300] {local_task_job_runner.py:240} INFO - Task exited with return code 0