Files
airflow-manual/educational-tasks.md

46 KiB
Raw Permalink Blame History

Практика по Apache Airflow

Здесь мы знакомимся с Airflow на готовых примерах: запускаем DAG, меняем задачи и смотрим, что произошло. Свой цельный процесс обработки данных вы соберете в курсовых работах дальше по программе.

Как проходить практику

В каждом блоке сначала выполните основную практику. Задания "По желанию" находятся в конце блока: их можно пропустить и перейти дальше. Сложность указана отдельно. Если дополнительному заданию нужны результаты другого упражнения, это написано в его начале.

Основные задания одного блока выполняйте последовательно, сохраняя изменения. Перед новым блоком прочитайте условия начала. Загрузка CSV и проверки DQ используют одну таблицу, поэтому идут вместе.

Блок Примеры Что наблюдаем Перед практикой
1. Первый DAG hello_world_dag.py Задачи, зависимости, операторы, расписание Главы 4 и 5; для 1.3 - расписания
2. SQL и подключения sql_basic_dag.py Connection, SQL, ошибка подключения Подключения
3. Работа с файлами file_operations_dag.py Результаты отдельных задач Главы 5 и 6
4. Загрузка и DQ csv_to_postgres.py, csv_to_postgres_dq.py XCom, данные в БД, ошибка проверки Глава 6, шаблоны и XCom
5. Разбор ETL data_processing_dag.py Параллельные шаги и путь данных Глава 5
6. Ветвление branching_dag.py Variables, выбранная ветка, skipped Глава 6 и Variables
7. Ошибки и повторы error_handling_dag.py Попытки, обработчики, итог запуска Глава 6; для 7.B - callback
8. Пулы и группировка resource_management_dag.py Слоты, XCom, TaskGroup Пулы, XCom и TaskGroup

Главы доступны в оглавлении учебника. Инструкция запуска и параметры учебной БД находятся в README стенда, файлы примеров - в airflow-docker/dags/.

Откройте http://localhost:8080, войдите как admin / admin. Для выбранного DAG включите переключатель и нажмите Trigger DAG. Дождитесь завершения запуска, затем откройте его граф и логи задач. После правки Python-файла дождитесь обновления вкладки Code; новый запуск должен использовать новый код. Если DAG исчез или перестал обновляться, проверьте Import Errors и логи scheduler.

Каталог /opt/airflow/data/ в контейнере соответствует airflow-docker/data/ на компьютере. Команды ниже выполняются из airflow-docker. Подойдет терминал Linux или PowerShell в Windows.

Для SQL можно открыть консоль учебной БД:

docker compose exec postgres-training psql -U student -d training

В ней выполняются SQL-запросы из заданий; выход - \q. Сохраняйте исходные DAG перед опытами вне каталога dags: две Python-копии с одинаковым dag_id мешают Airflow выбрать нужную.

1. Первый DAG

Начните с исходного hello_world_dag.py. В нем три задачи: start_task, date_task, end_task. Включение DAG с расписанием может создать автоматический запуск; для опытов выбирайте свой ручной запуск.

1.1. Добавить задачу

Основная практика. Сложность: начальная.

Запустите исходный DAG и найдите сообщения всех трех задач. Переведите приветствие на русский, затем добавьте функцию с фиктивной погодой и PythonOperator с task_id="weather_task". Включите ее между start_task и date_task, изменив существующую цепочку зависимостей.

Что должно получиться: в новом запуске четыре задачи со статусом success, а в логе weather_task есть выбранный вами текст. В графе видно, какая задача должна закончиться перед ней.

1.2. Создать файл через BashOperator

Основная практика. Сложность: начальная.

Добавьте между date_task и end_task задачу write_note. Импорт BashOperator уже есть. В bash_command запишите команду echo 'Airflow works' > /opt/airflow/data/output/note.txt.

После запуска проверьте файл:

docker compose exec airflow-webserver cat /opt/airflow/data/output/note.txt

Что должно получиться: пять успешных задач и файл с текстом Airflow works. При повторном запуске этот файл перезаписывается.

1.3. Посмотреть расписание

Основная практика. Сложность: начальная.

Замените schedule_interval на '0 0 * * 1'. Найдите новое расписание и следующий автоматический запуск в UI. Сопоставьте дату запуска с часовым поясом DAG. catchup=False оставьте как есть.

Что должно получиться: UI показывает запуск по понедельникам в 00:00 в часовом поясе DAG. Ручной запуск по кнопке по-прежнему выполняется сразу, когда доступны ресурсы. Для проверки расписания ждать понедельника не нужно. После опыта выключите переключатель DAG.

1.A. Ограничить одновременные запуски

По желанию. Сложность: средняя.

В DAG(...) задайте max_active_runs=1. В одну Python-функцию временно добавьте time.sleep(30) с импортом time. Включите DAG и запустите его вручную дважды с разными Run Id.

Что должно получиться: пока первый запуск выполняет задержку, второй не начинает свои задачи. После первого он продолжает работу. Удалите задержку и выключите DAG; ограничение можно оставить.

2. SQL и подключения

Используйте исходный sql_basic_dag.py. Он создает public.students_sample, очищает ее перед вставкой и добавляет трех студентов. После выполнения данные остаются для проверки. Повторный полный запуск снова оставит три строки. Остальные таблицы учебной БД этот DAG не меняет.

2.1. Подключиться к учебной БД

Основная практика. Сложность: начальная.

Запустите DAG с готовым соединением postgres_training. В Admin > Connections создайте student_postgres: Connection Type = Postgres, Host = postgres-training, Database/Schema = training, Login = student, Password = student, Port = 5432. Замените postgres_conn_id во всех SQL-задачах на свой Conn Id.

Проверьте таблицу:

SELECT name, age FROM public.students_sample ORDER BY name;

Что должно получиться: Alice - 22, Bob - 24, Charlie - 21. Новый запуск с вашим Connection также успешен.

Чтобы увидеть ошибку подключения, временно укажите несуществующий Conn Id и поставьте retries=0 в default_args. Запустите DAG и найдите причину в логе первой задачи. Исправьте Conn Id, верните исходное число повторов и создайте новый запуск. Ручная отметка задачи как успешной не проверяет подключение.

2.2. Добавить SQL-задачу

Основная практика. Сложность: средняя.

Добавьте PostgresOperator с task_id="update_student" между insert_data и query_data. Его SQL должен установить Alice возраст 25. Используйте то же соединение, что и остальные задачи.

Что должно получиться: после запуска у Alice возраст 25, остальные строки не изменились. Повторный полный запуск дает те же три строки: сначала DAG очищает таблицу, потом вставляет данные и выполняет UPDATE.

2.A. Добавить колонку

По желанию. Сложность: средняя. Начните после 2.2.

Добавьте email VARCHAR(100) в create_table_sql и заполните его в insert_data_sql. Для уже созданной таблицы сначала выполните:

ALTER TABLE public.students_sample ADD COLUMN email VARCHAR(100);

Команда нужна один раз. CREATE TABLE IF NOT EXISTS не меняет существующую схему, а TRUNCATE очищает только строки.

Что должно получиться: после запуска в таблице три строки с непустым email. Задание 2.B можно выполнять и без этой колонки. Удаление таблицы или всей базы для опыта не требуется.

2.B. Сохранить отчет

По желанию. Сложность: средняя. Требуется 2.2; задание 2.A можно пропустить.

После query_data добавьте Python-задачу. Через PostgresHook получите количество студентов, средний возраст и количество студентов каждого возраста. Запишите результат в /opt/airflow/data/output/students_report.txt. У хука есть методы get_first() и get_records(); они позволяют обойтись без ручной работы с курсором.

Что должно получиться: в файле 3 студента, средний возраст около 23.33 и по одному студенту возрастов 21, 24 и 25. Эти числа должны совпасть с SQL по students_sample.

3. Работа с файлами

Начните с исходного file_operations_dag.py. Он сам создает 100 строк в input/sample_data.csv; данные каждого полного запуска могут отличаться. Чужие входные файлы для этого блока не нужны.

3.1. Изменить преобразование

Основная практика. Сложность: начальная.

Запустите DAG, откройте data/output/processed_data.csv и summary.txt. Найдите в transform_data() правило вычисления salary_category. Измените порог категории High с 70000 на 80000, оставив границу Medium равной 50000. Запустите DAG еще раз.

Что должно получиться: в обработанном CSV 100 строк, у каждой строки с зарплатой от 80000 категория High. Сумма количеств High, Medium и Low в отчете равна 100. Сверяйте отчет с CSV того же запуска: случайные данные не позволяют сравнивать только числа из двух отчетов.

3.A. Добавить отдел и опыт

По желанию. Сложность: средняя.

В generate_sample_data() добавьте department, experience_years, education_level. Отдел выбирайте из sales, support, engineering; опыт должен быть от 0 до age - 18. Дополните read_and_validate_data() проверками новых полей. В transform_data() сохраните сотрудников с опытом больше трех лет в отдельный output/experienced.csv, сохранив полный processed_data.csv.

Что должно получиться: основной файл по-прежнему содержит 100 строк, опыт у всех допустимый, а в experienced.csv нет опыта 3 года и меньше.

3.B. Отчет по отделам

По желанию. Сложность: средняя. Требуется 3.A.

Добавьте задачу после write_summary. Она читает полный processed_data.csv и пишет output/departments.json: для каждого отдела количество сотрудников и среднюю зарплату. Для расчета подойдет pandas groupby, для записи - to_json.

Что должно получиться: сумма численности отделов равна 100, средние зарплаты совпадают с группировкой исходного CSV. График для этого задания не требуется.

3.C. Разделить обработку по отделам

По желанию. Сложность: высокая. Требуется 3.A, отчет 3.B необязателен.

После transform_data поставьте три параллельные задачи. Каждая читает полный processed_data.csv, отбирает свой отдел, проверяет уникальность id и сохраняет отдельный файл. После них добавьте сборку этих трех файлов и проверьте количество строк и уникальность id уже в общем результате. Не перезаписывайте исходный файл из параллельных задач.

Для опыта с повтором одна из задач должна намеренно падать только на первой попытке; используйте ti.try_number, retries=1 и retry_delay=timedelta(seconds=2). Этот прием разобран в блоке 7.

Что должно получиться: объединенный результат содержит те же 100 уникальных id. В графе три задачи имеют общего предшественника, но не зависят друг от друга. Повтор не добавляет дубликаты: файл отдела перезаписывается.

4. Загрузка CSV и проверка качества

Здесь работают два DAG и одна таблица public.orders. Начните с исходных csv_to_postgres.py и csv_to_postgres_dq.py. SQL-блок использовал другую таблицу. Сначала выполняйте основные задания, затем расширяйте схему по желанию.

4.1. Проследить загрузку и XCom

Основная практика. Сложность: начальная.

Запустите csv_to_postgres. В задаче generate_csv откройте вкладку XCom с ключом return_value: там путь к CSV. Найдите этот файл в data/output. Затем выберите load_csv_to_postgres, откройте Details и прокрутите до Rendered Templates. В opKwargs вместо шаблона параметра csv_path появился путь к файлу. Часть пути в UI может выглядеть как ***; сравните имя CSV с XCom.

Проверьте результат в БД:

SELECT COUNT(*) FROM public.orders;
SELECT order_id, amount FROM public.orders ORDER BY order_id LIMIT 5;

Что должно получиться: первый запуск на пустой таблице загружает 1000 строк. В XCom передается путь к файлу, сами строки хранятся в CSV и PostgreSQL. Каждый новый полный запуск генерирует новые заказы; после двух последовательных запусков на изначально пустой таблице будет 2000 строк. Это отличается от повторного выполнения только задачи загрузки с тем же CSV.

4.2. Добавить проверку и увидеть ошибку

Основная практика. Сложность: средняя.

После успешной загрузки отдельно запустите csv_to_postgres_dq. Автоматической связи между этими двумя DAG нет.

По образцу _check_has_rows() добавьте функцию и Python-задачу check_positive_amount перед data_quality_summary. Проверьте число строк, где amount <= 0, и бросьте ValueError, если такие строки есть. Новую задачу включите в существующую цепочку.

Что должно получиться на корректных данных: все задачи и DAG Run успешны, сводка перечисляет результаты проверок.

Для отрицательного сценария поменяйте знак суммы одного заказа:

UPDATE public.orders SET amount = -amount
WHERE order_id = (SELECT MIN(order_id) FROM public.orders);

Запустите только DQ-DAG. Новая проверка должна получить failed, сводка - upstream_failed, весь DAG Run - failed. В ее логе должно быть понятно, какое условие нарушено. У DQ-задач нет повторов: повторный запуск с теми же неверными данными не исправляет ошибку.

Восстановите сумму тем же UPDATE еще один раз и повторите DQ. Дождитесь успешного результата перед продолжением. Во время опыта не запускайте загрузчик и не удаляйте строки: восстановление относится к тому же минимальному order_id.

4.A. Провести новое поле через загрузку

По желанию. Сложность: высокая. Требуется завершенный основной блок 4.

Добавьте status со значениями NEW, PROCESSING, COMPLETED. Поле нужно провести через четыре участка: генератор CSV, DDL целевой таблицы, список колонок COPY и обе части итогового INSERT ... SELECT в _load_csv(). Во всех списках добавляйте его в конец. Порядок колонок CSV должен совпадать с COPY: CSV HEADER пропускает первую строку, но не сопоставляет поля по ее именам.

В DDL используйте status VARCHAR(20) NOT NULL DEFAULT 'NEW'. Существующую таблицу подготовьте один раз:

ALTER TABLE public.orders
ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'NEW';

В EXPECTED_ORDERS_SCHEMA DQ-DAG добавьте в конец ("status", "character varying"). После загрузки проверьте значения status в БД и выполните DQ.

Что должно получиться: новые заказы содержат статусы из CSV, старые получили NEW, проверка схемы проходит. Проверяйте значения, а не только успешность задачи: забытый status в INSERT может оставить всем строкам значение по умолчанию.

Завершив опыт, удалите status из генератора, DDL, COPY и обеих частей INSERT ... SELECT. Уберите его и из EXPECTED_ORDERS_SCHEMA в DQ. Затем выполните ALTER TABLE public.orders DROP COLUMN status; и запустите DQ. Проверку положительной суммы из 4.2 сохраните.

4.B. Заменить PythonOperator на PostgresOperator

По желанию. Сложность: средняя. Используйте базовую схему после 4.2 или после восстановления в 4.A.

Замените задачу create_orders_table на PostgresOperator с тем же task_id, SQL создания таблицы и соединением postgres_training. Образец оператора есть в SQL-блоке. _get_conn() оставьте: загрузчик продолжает использовать эту функцию.

Что должно получиться: после замены DAG загружает новую порцию, затем DQ проходит. Имя задачи и цепочка зависимостей сохраняются.

4.C. Выполнить все проверки после ошибки

По желанию. Сложность: высокая. Требуется 4.2.

Временно замените ожидаемый тип amount в EXPECTED_ORDERS_SCHEMA на integer. Сначала запустите исходную цепочку: check_orders_schema упадет, последующие задачи получат upstream_failed.

У задач после проверки схемы, включая сводку, задайте trigger_rule="all_done". Повторите запуск. Прочитайте _log_dq_summary(): она собирает состояния всех проверок и сама падает, если хотя бы одна неуспешна.

Что должно получиться: проверки строк, дубликатов и положительной суммы выполняются даже после ошибки схемы. Сводка и DAG Run остаются failed. Успешная завершающая задача с all_done могла бы скрыть отказ внутри DAG; именно поэтому сводка проверяет результат.

Верните тип numeric и исходные trigger_rule, затем выполните успешный DQ-запуск.

5. Разбор ETL-примера

Начните с исходного data_processing_dag.py. Он сам создает свои customers.csv и orders.csv; таблица public.orders из блока 4 ему не нужна. Шаг simulate_load показывает подготовку к загрузке и печатает пример DDL. Записи в БД в этом DAG нет.

5.1. Проследить путь данных

Основная практика. Сложность: начальная.

Запустите DAG. Найдите две задачи извлечения и их файлы output/extracted_customers.csv, output/extracted_orders.csv. Проверьте, что transform_data() читает именно их. В графе обе задачи извлечения могут выполняться параллельно, а преобразование ждет обе.

В extract_orders() временно оставьте только заказы с amount >= 100 перед сохранением файла. Запустите DAG снова и сравните отчет с результатом до изменения.

Что должно получиться: исходный отчет содержит 6 заказов с выручкой 2105; после фильтра - 4 заказа с выручкой 2000. Лог simulate_load явно сообщает об имитации. Верните фильтр к исходному состоянию и запустите DAG перед дополнительными заданиями.

5.A. Добавить справочник продуктов

По желанию. Сложность: средняя.

В create_sample_data() добавьте input/products.csv с полями product, category, weight_kg. Для каждого из шести продуктов в заказах должна быть одна строка справочника. Добавьте параллельную задачу extract_products, сохраняющую output/extracted_products.csv, и подключите ее к transform_data. Объедините извлеченные заказы со справочником по product.

Что должно получиться: в результате 6 заказов, у всех заполнена категория, общая выручка осталась 2105. Если JOIN размножил строки или потерял заказы, проверьте уникальность и полноту справочника.

5.B. Расширить отчет

По желанию. Сложность: средняя. Справочник из 5.A не нужен.

В generate_report() добавьте топ-3 заказа по сумме, выручку по месяцам и среднюю сумму заказа каждого клиента. Сохраните output/detailed_report.txt.

Что должно получиться: топ-3 суммы - 1200, 400 и 300; все заказы относятся к октябрю 2023 года; средняя сумма заказа Alice Johnson равна 640. У исходного набора всего шесть строк, поэтому результат можно сверить вручную.

5.C. Валидация и архив

По желанию. Сложность: высокая.

Между преобразованием и simulate_load добавьте проверку: результат непустой, customer_id и name заполнены, суммы положительны. Для этой задачи задайте retry_delay=timedelta(seconds=2), сохранив один повтор. В create_sample_data() временно укажите для одного заказа несуществующий customer_id. Настройте on_failure_callback, который пишет в лог идентификатор задачи и причину ошибки; образец есть в задании 7.B.

После generate_report добавьте копирование transformed_data.csv в архивный файл с датой и временем в имени. Для этого подойдет shutil.copy.

Что должно получиться: после исчерпания повторов проверка падает, последующие шаги получают upstream_failed, новый архив не появляется. После возврата клиента к исходному значению весь DAG успешен, а содержимое нового архива совпадает с результатом запуска. Сообщение callback этой проверки находится в логе ее последней попытки.

6. Ветвление

Используйте исходный branching_dag.py. Ветки пока имитируют обработку, входные файлы не нужны. Дождитесь завершения каждого запуска перед изменением Variable.

6.1. Выбрать ветку через Variable

Основная практика. Сложность: средняя.

В Admin > Variables создайте ключ branch_format со значением csv. Запустите DAG, затем смените значение на json и сделайте новый запуск. Посмотрите граф и лог merge_results.

Что должно получиться: при csv CSV-ветка получает success, JSON-ветка - skipped; при json наоборот. merge_results выполняется в обоих случаях и читает XCom только текущего запуска. Правило none_failed_min_one_success допускает пропущенную ветку, если другая успешна.

Для третьего запуска задайте xml. choose_format должен упасть с понятным сообщением, DAG Run - завершиться ошибкой. Верните branch_format=csv и проверьте успешный запуск.

6.A. Добавить ветку ручной проверки

По желанию. Сложность: высокая.

Добавьте задачу review_branch и значение review в выбор формата. Для неизвестного значения направляйте выполнение в эту же ветку. Возвращайте из нее строку review, включите ее в зависимости перед слиянием и в список task_ids при чтении XCom в merge_results.

Что должно получиться: при review или неизвестном формате выполняется только новая ветка; две остальные пропущены, слияние и весь запуск успешны. Проверьте, что csv и json продолжают работать. Верните значение csv.

6.B. Прочитать CSV и JSON

По желанию. Сложность: высокая. Третья ветка из 6.A не требуется.

Создайте два входных файла в data/input: branch_records.csv и branch_records.json. Оба должны содержать две записи с id 1 и 2 и суммами amount 10 и 20. В функциях веток прочитайте соответствующий файл, проверьте положительность сумм и верните словарь с количеством записей и общей суммой. Слияние уже читает XCom выбранной ветки.

Что должно получиться: отдельные запуски для CSV и JSON дают по 2 записи и сумму 30. Сравните их логи вручную. В каждом запуске работает одна ветка; файл результата другой ветки из старого запуска для сравнения не нужен.

7. Ошибки и повторы

Используйте исходный error_handling_dag.py. Режим задается Variable error_mode; по умолчанию это retry_once. Обе рабочие задачи имеют одну повторную попытку с задержкой 10 секунд. Каждый сценарий начинайте новым запуском и ждите его завершения перед сменой режима.

7.1. Увидеть повторную попытку

Основная практика. Сложность: начальная.

Установите error_mode=retry_once и запустите DAG. Откройте retry_task и логи ее попыток. Первая намеренно падает, вторая завершается успешно. Состояние up_for_retry видно во время ожидания; если оно уже сменилось, причину и номера попыток можно найти в логах.

Что должно получиться: unreliable_task, retry_task и success_handler успешны, failure_handler пропущен, весь DAG Run успешен. В логе первой попытки есть временная учебная ошибка, во второй - сообщение о выполнении. retries=1 означает две попытки всего.

7.2. Проверить окончательную ошибку

Основная практика. Сложность: средняя.

Добавьте retries=0 непосредственно в оператор retry_task, дождитесь обновления кода и создайте новый запуск в режиме retry_once.

Что должно получиться: retry_task получает failed, success_handler - upstream_failed, а failure_handler выполняется. Весь DAG Run остается failed, хотя обработчик ошибки выполнился успешно.

Уберите локальное переопределение retries. Теперь по очереди запустите режимы fail_first и fail_second. Обработчик должен заметить отказ любой из двух задач. В графе проверьте, что каждый обработчик зависит от обеих рабочих задач. Верните error_mode=retry_once и получите успешный запуск.

7.3. Добавить свое сообщение в лог

Основная практика. Сложность: начальная.

В handle_success() добавьте logging.info() с собственным сообщением о завершении. У logging есть уровни INFO, WARNING и ERROR; print тоже попадает в лог, но не позволяет явно выбрать уровень сообщения.

Что должно получиться: после успешного запуска сообщение есть в логе success_handler. При окончательном отказе эта функция не выполняется, поэтому сообщения о всеобщем успехе нет.

7.A. Сравнить задержки

По желанию. Сложность: высокая.

В режиме fail_second задайте для retry_task три повтора, retry_delay=timedelta(seconds=5) и retry_exponential_backoff=True. Сопоставьте время окончания неудачной попытки с началом следующей. Задержка увеличивается; планировщик может добавить время ожидания, поэтому точного удвоения по часам требовать не нужно.

Что должно получиться: четыре неуспешные попытки, после них failure_handler и итог failed. Верните параметры повторов к исходным и режим к retry_once.

7.B. Добавить callback

По желанию. Сложность: высокая.

Добавьте функцию notify_failure(context) и укажите ее в on_failure_callback рабочих задач. В ней используйте logging.error и выведите context['ti'].task_id и context['exception']. В разных опытах бросьте ConnectionError, ValueError и OSError с различимыми сообщениями. Настройка настоящей почты для этого задания не нужна.

Что должно получиться: после исчерпания повторов callback сообщает, какая задача и почему упала. При временной ошибке с последующим успехом он не вызывается. В этом примере callback назначен оператору: откройте Logs упавшей задачи и выберите последнюю попытку. После сообщения об отказе найдите свой текст из callback.

Если нужно наблюдать каждый повтор, для этого есть отдельный on_retry_callback. Верните исходные исключения и error_mode=retry_once перед переходом дальше.

8. Пулы, XCom и TaskGroup

Используйте исходный resource_management_dag.py. Он имитирует чтение двух источников по 20 секунд и передает небольшие числа через XCom. Ни таблицы, ни файлы из предыдущих заданий ему не нужны.

8.1. Ограничить доступные слоты

Основная практика. Сложность: средняя.

Перед запуском в Admin > Pools создайте training_pool с двумя слотами. В DAG задача read_customers занимает два слота, read_orders - один. Запустите DAG и наблюдайте за задачами и пулом.

Что должно получиться: эти две задачи выполняются последовательно, хотя между ними нет зависимости: вместе им нужны три слота. Во время выполнения одной другая ожидает. Если пул не создан, задачи тоже будут ждать, поэтому сначала проверьте его имя и размер.

Поменяйте pool_slots у read_customers на 1 и повторите запуск. Теперь обе задачи могут работать одновременно, если свободны ресурсы executor. Сравните интервалы выполнения, затем верните значение 2.

8.2. Добавить метрику в XCom

Основная практика. Сложность: средняя.

В UI откройте XCom задачи calculate_metrics: исходный словарь содержит customers=20 и orders=30. Добавьте в возвращаемый словарь total_rows, равный сумме этих двух значений. Запустите DAG и проверьте XCom и лог log_metrics.

Что должно получиться: в обоих местах total_rows=50. Возвращаемое Python-функцией значение сохранено под ключом return_value и доступно следующей задаче через xcom_pull.

8.3. Сгруппировать задачи

Основная практика. Сложность: средняя.

Импортируйте TaskGroup из airflow.utils.task_group. Внутри блока with DAG(...) поместите определения двух задач чтения в with TaskGroup("extract"):. Метрики и лог оставьте снаружи, зависимости между задачами сохраните.

После группировки полные идентификаторы станут extract.read_customers и extract.read_orders. Обновите обе ссылки в calculate_metrics().

Что должно получиться: в графе появилась сворачиваемая группа. Нажмите на ее название со стрелкой, чтобы увидеть две задачи внутри. Метрика по-прежнему равна 50; ограничения пула продолжают действовать.

8.A. Подставить XCom в шаблон

По желанию. Сложность: средняя. Требуется 8.2; группировку 8.3 можно сохранить.

Добавьте после calculate_metrics задачу с BashOperator. В bash_command подставьте {{ ti.xcom_pull(task_ids='calculate_metrics')['total_rows'] }} в команду echo. Назовите задачу print_total.

Что должно получиться: в Details задачи print_total, в разделе Rendered Templates, уже подставлено число 50. В логе команды тоже 50.

8.B. Отправить уведомление

По желанию. Сложность: высокая. Требуется 7.B: вернитесь к error_handling_dag.py с callback отказа. Нужен отдельно настроенный SMTP и адрес, на который вы можете отправить тестовое письмо. В стандартном стенде SMTP не настроен; отсутствие письма здесь не означает ошибку основного маршрута.

Используйте пример из раздела об алертинге. Добавьте отправку письма в callback отказа. Укажите DAG, задачу, идентификатор запуска и причину ошибки. Для успешного DAG Run задайте отдельный on_success_callback у DAG(...).

Что должно получиться: после постоянной ошибки приходит сообщение об отказе, после успешного запуска - об успехе. Уведомление не меняет результат обработки данных. Запуск с отказом сохраняет failed; ошибку доставки ищите в логах callback. Возвращение к основной практике не требует настройки SMTP: отключите почтовый callback и верните исходный режим ошибок.

Когда можно двигаться дальше

Основная практика закончена, когда вы можете найти свой запуск и его логи, добавить задачу по образцу, объяснить выбранную ветку и повторную попытку, проверить файл или строки в таблице и прочитать XCom. По последнему блоку достаточно показать ожидание слотов и работающую TaskGroup.

Задания по желанию расширяют эти опыты. Их выполнение не требуется для перехода к следующему блоку программы.