diff --git a/02 - Понятное введение в ключевые понятия Airflow.md b/02 - Понятное введение в ключевые понятия Airflow.md index ccac530..3050d96 100644 --- a/02 - Понятное введение в ключевые понятия Airflow.md +++ b/02 - Понятное введение в ключевые понятия Airflow.md @@ -107,12 +107,14 @@ dag = DAG('greeting_dag', start_date=datetime(2023, 1, 1), catchup=False) -start_task = DummyOperator(task_id='start_process', retries=2) -greeting_task = PythonOperator(task_id='greeting_task', python_callable=greet_user) +start_task = DummyOperator(task_id='start_process', retries=2, dag=dag) +greeting_task = PythonOperator(task_id='greeting_task', python_callable=greet_user, dag=dag) start_task >> greeting_task ``` +Параметр `dag=dag` связывает каждый оператор с созданным DAG. Если объявить задачи внутри `with DAG(...) as dag:`, привязка произойдет автоматически. Стрелка `>>` задает порядок выполнения задач. + В этом примере задача `greeting_task` использует `PythonOperator` для запуска функции `greet_user`, которая выводит приветственное сообщение в логи. Airflow предоставляет множество готовых операторов: @@ -157,4 +159,4 @@ flowchart LR Task1 -.->|Как делать?| Op1["Оператор\n(например, PythonOperator)"] Task2 -.->|Чего ждать?| Sen1["Сенсор\n(например, S3Sensor)"] Task3 -.->|Как делать?| Op2["Оператор\n(например, PostgresOperator)"] -``` \ No newline at end of file +``` diff --git a/07 - Шаблоны, переменные и подключения в Airflow.md b/07 - Шаблоны, переменные и подключения в Airflow.md index 618fc87..5eb5226 100644 --- a/07 - Шаблоны, переменные и подключения в Airflow.md +++ b/07 - Шаблоны, переменные и подключения в Airflow.md @@ -8,45 +8,53 @@ Синтаксис шаблонов интуитивно понятен — переменная заключается в двойные фигурные скобки с пробелами по краям. Для эффективного использования достаточно знать, какое значение возвращает конкретный шаблон, и разместить его в нужном месте кода. -Подробный справочник доступных шаблонов можно найти в [официальной документации Airflow](https://airflow.apache.org/docs/apache-airflow/stable/templates-ref.html#variables). +Подробный справочник доступных шаблонов можно найти в [документации Airflow 2.9.2](https://airflow.apache.org/docs/apache-airflow/2.9.2/templates-ref.html#variables). -Вот практические примеры использования: +Каждый пример ниже - отдельный DAG. Для запуска сохраните один блок кода в файл `.py` внутри `airflow-docker/dags/`. Затем найдите DAG в UI и запустите его вручную. **Передача даты выполнения в задачу:** -В этом примере создается DAG с идентификатором "template_example", который запускается каждые 15 минут. Задача "display_date" использует шаблон {{ ds }} для вывода текущей даты выполнения в формате YYYY-MM-DD. +В этом примере DAG `template_example` запускается каждые 15 минут. Задача `display_date` выводит логическую дату запуска в формате YYYY-MM-DD через шаблон `{{ ds }}`. ```python -... +from datetime import datetime + +from airflow import DAG +from airflow.operators.bash import BashOperator dag = DAG( dag_id="template_example", schedule_interval="*/15 * * * *", - default_args=default_args -) + start_date=datetime(2023, 1, 1), + catchup=False, +) -t1 = BashOperator(task_id="display_date", bash_command="echo {{ ds }}") - -t1 +t1 = BashOperator(task_id="display_date", bash_command="echo {{ ds }}", dag=dag) ``` **Использование шаблонов для лучшей отслеживаемости задач:** В этом примере создается DAG с идентификатором "template_tracking_example", который запускается каждые 20 минут. Вторая задача использует шаблон {{ ds }} в команде bash, чтобы явно указывать дату обработки в логах. ```python -... +from datetime import datetime + +from airflow import DAG +from airflow.operators.bash import BashOperator dag = DAG( dag_id="template_tracking_example", schedule_interval="*/20 * * * *", - default_args=default_args -) + start_date=datetime(2023, 1, 1), + catchup=False, +) -t1 = BashOperator(task_id="show_date", bash_command="echo {{ ds }}") -t2 = BashOperator(task_id="process_for_date", bash_command="echo Processing for {{ ds }}") +t1 = BashOperator(task_id="show_date", bash_command="echo {{ ds }}", dag=dag) +t2 = BashOperator(task_id="process_for_date", bash_command="echo Processing for {{ ds }}", dag=dag) t1 >> t2 ``` +В первом DAG одна задача, во втором - две связанные задачи. В Details выбранной задачи откройте Rendered Templates: вместо `{{ ds }}` будет дата. Та же дата появится в логе после запуска. Привязку операторов к DAG здесь задает `dag=dag`. + # Безопасные переменные (Variables) Переменные Airflow представляют собой пары "ключ-значение", хранящиеся в метадатабазе системы. Они идеально подходят для хранения конфигурационных параметров, таких как пути к скриптам, имена таблиц или другие настройки, которые должны быть доступны в разных DAG. @@ -134,69 +142,62 @@ task = BashOperator( - Системы уведомлений (Email, Telegram) - И многие другие через Airflow Providers -При использовании операторов, взаимодействующих с внешними системами, Airflow автоматически ищет соответствующее подключение с суффиксом `_default`. Однако можно явно указать альтернативное подключение: +Оператор получает настройки по идентификатору подключения. Например, `PostgresOperator` использует параметр `postgres_conn_id`. PostgreSQL provider уже установлен в учебном стенде. -В приведенном примере используется PostgresOperator для создания таблицы в базе данных. Вместо использования подключения по умолчанию, явно указывается подключение с идентификатором 'my_postgres_conn'. - -```python -from airflow.providers.postgres.operators.postgres import PostgresOperator - -create_table = PostgresOperator( - task_id='create_user_table', - sql=''' - CREATE TABLE users( - user_id integer NOT NULL, - created_at TIMESTAMP NOT NULL - );''', - postgres_conn_id='my_postgres_conn' -) -``` - -Управление подключениями доступно через интерфейс Airflow (Admin → Connections). Если требуемый тип подключения отсутствует, его можно добавить установкой соответствующего Airflow Provider из [официального репозитория](https://airflow.apache.org/docs/#providers-packages-docs-apache-airflow-providers-index-html). - -Управление подключениями доступно через интерфейс Airflow (раздел **Admin → Connections**). Подключения хранятся в метадатабазе Airflow, а пароли и другие чувствительные поля шифруются с помощью Fernet и маскируются в UI и логах. +Подключения хранятся в базе метаданных Airflow. Пароли и другие чувствительные поля шифруются с помощью Fernet и маскируются в UI и логах. ### Как создать подключение к PostgreSQL через UI -Интерфейс ниже соответствует Airflow 2.9.x и стандартному Docker-стенду из документации: +Используйте учебную БД из [README стенда](airflow-docker/README.md#запуск). В контейнере Airflow она доступна по имени сервиса `postgres-training`; адрес `localhost:5432` предназначен для подключения с вашего компьютера. -1. Откройте веб-интерфейс Airflow. -2. В верхнем меню выберите **Admin → Connections**. -3. В правом верхнем углу нажмите кнопку **+ Add a new record**. -4. В форме укажите параметры: +1. Откройте **Admin > Connections** и нажмите **+ Add a new record**. +2. Заполните форму: - - **Connection Id**: `my_postgres_conn` - Это имя мы будем использовать в коде DAG (параметр `postgres_conn_id`). + - **Connection Id**: `my_postgres_conn`. Это имя будет указано в коде DAG. - **Connection Type**: `Postgres` - - **Host**: `postgres` - (так называется контейнер PostgreSQL в типовом `docker-compose.yaml` из официальной инструкции). - - **Schema**: `airflow` - (имя базы данных; в учебном стенде можно использовать стандартную БД). - - **Login**: `airflow` - - **Password**: `airflow` + - **Host**: `postgres-training` + - **Database / Schema**: `training` + - **Login**: `student` + - **Password**: `student` - **Port**: `5432` -5. Нажмите кнопку **Test** (если доступна) — Airflow попробует подключиться к базе. -6. Если тест успешен, нажмите **Save**. +3. Нажмите **Save**. Кнопка **Test** может быть отключена в конфигурации Airflow; подключение можно проверить запуском DAG ниже. -Теперь подключение с идентификатором `my_postgres_conn` доступно во всех DAG’ах. В примере ниже PostgresOperator явно использует это подключение: +База `airflow` на сервисе `postgres-metadata` хранит служебные данные Airflow. Для учебных таблиц используйте `training`. + +Сохраните пример в `airflow-docker/dags/connection_example_dag.py` и запустите `connection_example` через UI: ```python +from datetime import datetime + +from airflow import DAG from airflow.providers.postgres.operators.postgres import PostgresOperator -create_table = PostgresOperator( - task_id="create_user_table", - sql=""" - CREATE TABLE users( - user_id INTEGER NOT NULL, - created_at TIMESTAMP NOT NULL - ); - """, - postgres_conn_id="my_postgres_conn", -) +with DAG( + dag_id="connection_example", + start_date=datetime(2023, 1, 1), + schedule=None, + catchup=False, +) as dag: + create_table = PostgresOperator( + task_id="create_user_table", + sql=""" + CREATE TABLE IF NOT EXISTS public.connection_demo_users ( + user_id INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL + ); + """, + postgres_conn_id="my_postgres_conn", + ) ``` -Для успешной работы с внешними системами сначала необходимо создать соответствующее подключение, а затем использовать его идентификатор в операторах вашего DAG. +Задача должна завершиться успешно. В учебной БД появится пустая таблица `public.connection_demo_users`; повторный запуск сохранит ее. Проверить наличие таблицы можно из SQL-консоли стенда: + +```sql +SELECT to_regclass('public.connection_demo_users'); +``` + +Результат - `connection_demo_users`. Если задача упала, откройте ее лог и сверьте Conn Id в коде с именем подключения, затем Host и остальные поля формы. Правка настроек действует при следующем выполнении задачи. Более подробную информацию о настройке подключений можно найти в [документации Airflow](https://airflow.apache.org/docs/apache-airflow/2.9.3/howto/connection.html). diff --git a/09 - Пулы, XCom, TaskGroup и алертинг в Airflow.md b/09 - Пулы, XCom, TaskGroup и алертинг в Airflow.md index 8e1443b..7f1f58b 100644 --- a/09 - Пулы, XCom, TaskGroup и алертинг в Airflow.md +++ b/09 - Пулы, XCom, TaskGroup и алертинг в Airflow.md @@ -2,15 +2,15 @@ В этом материале мы познакомимся с управлением ресурсами через пулы задач, обменом данными между задачами через XCom, группировкой задач с помощью TaskGroup и настройкой системы оповещений (алертинга) в Airflow. -Начнем с улучшения нашего базового пайплайна — проведем рефакторинг кода для лучшей читаемости и поддержки. +Опыты с этими возможностями собраны в [блоке 8 практики](educational-tasks.md#resources); callback и ошибки разобраны в [блоке 7](educational-tasks.md#errors). ## Управление ресурсами с помощью пулов задач В системах с высокой нагрузкой, где одновременно запускается множество задач и DAG-ов, может возникнуть чрезмерная нагрузка на исполнителей и серверную часть. Это может привести к ошибкам выполнения и даже к отказу системы, если не установить соответствующие ограничения. -В Airflow для решения этой проблемы существует механизм управления ресурсами — **пулы задач** (pools). По умолчанию в Airflow настроен один пул задач — `default_pool` с 128 слотами, что означает возможность параллельного выполнения 128 задач одновременно. Пул `default_pool` нельзя удалить, но можно изменить его размер — увеличить или уменьшить количество слотов. +В Airflow для решения этой проблемы существует механизм управления ресурсами — **пулы задач** (pools). По умолчанию в Airflow настроен один пул задач — `default_pool` с 128 слотами. При стандартном `pool_slots=1` это верхняя граница в 128 одновременно работающих задач; ограничения executor и DAG могут уменьшить ее. Пул `default_pool` нельзя удалить, но можно изменить его размер — увеличить или уменьшить количество слотов. -Когда планировщик обнаруживает, что наступило время выполнения DAG, он запускает задачу согласно заданной последовательности. При этом задача занимает один слот в пуле и освобождает его после завершения. +Когда планировщик обнаруживает, что наступило время выполнения DAG, он запускает задачу согласно заданной последовательности. По умолчанию задача занимает один слот в пуле и освобождает его после завершения. Создать новый пул задач и установить его размер можно через веб-интерфейс Airflow. Рассмотрим пример пула `data_processing_pool` для тяжёлых задач: @@ -29,7 +29,7 @@ - Организовать запуск процессов в системе - Предотвратить перегрузку системы при выполнении большого количества ресурсоемких задач -Вы можете задать "вес" задачи через параметр `pool_slots`, чтобы оптимизировать распределение нагрузки. Если общее количество задач превышает доступные слоты, планировщик поставит задачу в очередь и запустит ее, как только появятся свободные ресурсы. +Вы можете задать "вес" задачи через параметр `pool_slots`, чтобы оптимизировать распределение нагрузки. Планировщик учитывает сумму занятых слотов. Например, в пуле из двух слотов задачи с `pool_slots=2` и `pool_slots=1` не смогут работать одновременно: одна ждет завершения другой. Пример настройки веса задач: @@ -58,7 +58,7 @@ BashOperator( ) ``` -Более подробную информацию о механизме пулов можно найти в [официальной документации](https://airflow.apache.org/docs/apache-airflow/stable/concepts/pools.html#pools). +Более подробную информацию о механизме пулов можно найти в [официальной документации](https://airflow.apache.org/docs/apache-airflow/2.9.2/administration-and-deployment/pools.html). ## Обмен данными между задачами: XCom и контекст выполнения @@ -151,338 +151,136 @@ XCom упрощает взаимодействие между задачами ## Группировка задач с помощью TaskGroup -Для удобства визуализации задачи можно группировать в веб-интерфейсе Airflow (начиная с версии 2.0). Повторяющиеся или логически связанные задачи можно объединить в группы: +TaskGroup объединяет задачи в сворачиваемую группу на графе. Зависимости и состояния остаются у отдельных задач. Группировку задают в Python-коде; UI позволяет раскрыть ее и посмотреть содержимое. -В этом примере задачи сгруппированы в три логические группы: 'data_extraction', 'data_transformation' и 'data_loading'. Каждая группа содержит несколько задач, которые выполняются последовательно внутри группы. Затем группы связаны между собой, чтобы показать общий порядок выполнения этапов обработки данных. +В примере три группы. Внутри каждой задачи идут последовательно. Группы извлечения и преобразования независимы, а загрузка ждет завершения обеих. `EmptyOperator` ничего не обрабатывает: здесь мы смотрим только на устройство графа. ```python -with TaskGroup("data_extraction") as extraction_group: - extract_1 = DummyOperator(task_id="extract_source_1") - extract_2 = DummyOperator(task_id="extract_source_2") - extract_3 = DummyOperator(task_id="extract_source_3") - extract_1 >> extract_2 >> extract_3 +from datetime import datetime -with TaskGroup("data_transformation") as transformation_group: - transform_1 = DummyOperator(task_id="transform_step_1") - transform_2 = DummyOperator(task_id="transform_step_2") - transform_1 >> transform_2 +from airflow import DAG +from airflow.operators.empty import EmptyOperator +from airflow.utils.task_group import TaskGroup -with TaskGroup("data_loading") as loading_group: - load_1 = DummyOperator(task_id="load_to_target_1") - load_2 = DummyOperator(task_id="load_to_target_2") - load_1 >> load_2 +with DAG( + dag_id="task_group_example", + start_date=datetime(2023, 1, 1), + schedule=None, + catchup=False, +) as dag: + with TaskGroup("data_extraction") as extraction_group: + extract_1 = EmptyOperator(task_id="extract_source_1") + extract_2 = EmptyOperator(task_id="extract_source_2") + extract_3 = EmptyOperator(task_id="extract_source_3") + extract_1 >> extract_2 >> extract_3 -[extraction_group, transformation_group] >> loading_group + with TaskGroup("data_transformation") as transformation_group: + transform_1 = EmptyOperator(task_id="transform_step_1") + transform_2 = EmptyOperator(task_id="transform_step_2") + transform_1 >> transform_2 + + with TaskGroup("data_loading") as loading_group: + load_1 = EmptyOperator(task_id="load_to_target_1") + load_2 = EmptyOperator(task_id="load_to_target_2") + load_1 >> load_2 + + [extraction_group, transformation_group] >> loading_group ``` -Обратите внимание: при использовании TaskGroup последовательность задач указывается внутри группы после объявления всех задач, а в конце DAG описывается последовательность выполнения самих групп. +Здесь TaskGroup создается внутри `with DAG(...)`, поэтому группа и ее задачи получают один DAG. При отдельном объявлении `dag = DAG(...)` группу нужно привязать явно: `TaskGroup("data_extraction", dag=dag)`. -Ниже приведена упрощённая схема зависимостей между тремя группами задач: +Схема повторяет зависимости из кода: ```mermaid flowchart LR - - %% group1 - subgraph G1["group1"] - g1_t1["task1"] - g1_t2["task2"] - g1_t3["task3"] - - g1_t1 --> g1_t2 - g1_t1 --> g1_t3 + subgraph extraction["data_extraction"] + e1["extract_source_1"] --> e2["extract_source_2"] --> e3["extract_source_3"] end - - %% group2 - subgraph G2["group2"] - g2_t1["task1"] - g2_t2["task2"] - - g2_t1 --> g2_t2 + subgraph transformation["data_transformation"] + t1["transform_step_1"] --> t2["transform_step_2"] end - - %% group3 - subgraph G3["group3"] - g3_t1["task1"] - g3_t2["task2"] - - g3_t1 --> g3_t2 + subgraph loading["data_loading"] + l1["load_to_target_1"] --> l2["load_to_target_2"] end - - %% зависимости между группами - g1_t2 --> g3_t1 - g1_t3 --> g3_t1 - g2_t2 --> g3_t1 + e3 --> l1 + t2 --> l1 ``` -Визуально в интерфейсе Airflow группы задач отображаются как один узел с небольшим индикатором. Клик по нему разворачивает или сворачивает вложенные задачи, что значительно улучшает восприятие DAG с большим количеством задач и связей, особенно когда в них десятки и сотни задач. - -TaskGroup — это удобный способ логической группировки задач, который помогает упростить код и представить сложные пайплайны более компактно. +Для запуска сохраните пример в `airflow-docker/dags/task_group_example_dag.py`. После ручного запуска все семь задач должны завершиться успешно. Нажмите название группы со стрелкой, чтобы раскрыть ее. Полный идентификатор первой задачи - `data_extraction.extract_source_1`: имя группы становится префиксом `task_id`. Такой идентификатор нужен при обращении к задаче через XCom. ## Система оповещений (алертинг) -Алертинг — один из ключевых компонентов системы оркестрации, так как важно своевременно получать уведомления об ошибках для их оперативного анализа и решения. - -В Airflow есть встроенная поддержка отправки уведомлений на электронную почту (при условии, что в конфигурации настроен SMTP-сервер). При создании DAG указываются email-адреса, на которые будут отправляться сообщения. С помощью параметров можно настроить различные сценарии оповещений. - -Давайте модифицируем наш первый DAG так, чтобы получать уведомления на почту при возникновении ошибок. При этом настроим перезапуск задач в случае неудачи (например, 2 попытки), но без уведомлений о самих перезапусках: - -В приведенном примере создан DAG с идентификатором 'customer_analysis_pipeline', который настроен на отправку уведомлений по электронной почте только при ошибках (email_on_failure=True), но не при повторных попытках (email_on_retry=False). Также установлено 2 попытки повторного запуска задач при ошибках с задержкой 2 минуты между попытками. +Для первого опыта достаточно callback с записью в лог. Он получает контекст задачи и помогает связать сообщение с конкретным запуском: ```python -import os -import datetime as dt -import pandas as pd -from airflow.models import DAG -from airflow.operators.python import PythonOperator -from airflow.operators.bash import BashOperator -from sqlalchemy import create_engine +import logging -# основные параметры DAG -args = { - 'owner': 'data_engineering_team', - 'start_date': dt.datetime(2021, 6, 15), - 'retries': 2, - 'retry_delay': dt.timedelta(minutes=2), - 'email': ["data-team@example.com"], - 'email_on_failure': True, - 'email_on_retry': False, -} -dag = DAG( - dag_id='customer_analysis_pipeline', - schedule_interval=None, - default_args=args, -) +def notify_failure(context): + ti = context["ti"] + logging.error( + "Ошибка: dag=%s task=%s run=%s причина=%s", + ti.dag_id, ti.task_id, context["run_id"], context["exception"], + ) ``` -Теперь вы будете получать email-уведомления при ошибках выполнения. +Укажите `on_failure_callback=notify_failure` в рабочем операторе. Функция вызывается после окончательного отказа, когда повторы исчерпаны. Для сообщения о предстоящем повторе существует `on_retry_callback`. Например, при `retries=2` задача может выполниться три раза: исходная попытка и два повтора. -Пример функции для отправки уведомлений с использованием параметров из контекста: +Callback запускается при реальном выполнении задачи. Ручная смена статуса в UI его не проверяет. В этом стенде сообщение callback оператора видно в Logs последней попытки задачи. Callback, назначенный самому DAG, пишет в файл логов планировщика. Например, для `error_handling_dag.py`: + +```bash +docker compose exec airflow-scheduler cat /opt/airflow/logs/scheduler/latest/error_handling_dag.py.log +``` + +Этот файл может содержать несколько запусков: сверяйте `run_id` и время. Обычный `docker compose logs airflow-scheduler` не показывает все записи из него. Виды событий описаны в [документации Airflow 2.9.2 о callbacks](https://airflow.apache.org/docs/apache-airflow/2.9.2/administration-and-deployment/logging-monitoring/callbacks.html). + +### Отправка почты по желанию + +В учебном стенде SMTP не настроен. Для отправки нужен SMTP-сервер и адрес получателя, которому можно отправлять тестовые сообщения. Укажите настройки в общем окружении сервисов Airflow: `AIRFLOW__SMTP__SMTP_HOST`, `AIRFLOW__SMTP__SMTP_PORT`, `AIRFLOW__SMTP__SMTP_MAIL_FROM`, параметры TLS/SSL и учетные данные вашего сервера. Секреты храните локально, вне Git. После изменения окружения пересоздайте сервисы командой `docker compose up -d`. Полный список параметров находится в [конфигурации Airflow 2.9.2](https://airflow.apache.org/docs/apache-airflow/2.9.2/configurations-ref.html#smtp). + +После настройки замените запись в лог отправкой сообщения: ```python -from datetime import datetime, timedelta, timezone -import dateutil +from html import escape + from airflow.utils.email import send_email_smtp -from airflow.operators.python import get_current_context -MAIL_LIST = [ - "email_1@gmail.ru", - "email_2@gmail.ru" -] +MAIL_TO = ["student@example.com"] # Замените своим тестовым адресом. -def notify_email(calculation_dt: str, dagrun_begin_time: str): - # dag_run date is utc timezone, so add `timezone.utc` to calculation_end to combat 3 hour diff. - context = get_current_context() - calculation_start = dateutil.parser.isoparse(dagrun_begin_time.replace('Z', '+00:00')) - calculation_end = datetime.now(timezone.utc) - duration = str(timedelta(seconds=(calculation_end - calculation_start).seconds)) - - title = f"Ежедневные расчёты завершились успешно (dag: {context['task_instance'].dag_id})." - - body = f""" - Привет,
- Я закончил работу над расчётом за "{calculation_dt}". Это заняло {duration} часов/минут/секунд.
- Логи и запуски тоже можно посмотреть тут. -
- -
-
- Навеки твой,
- Airflow бот
- """ - - send_email_smtp(";".join(MAIL_LIST), title, body) -``` -Такую функцию можно разместить в отдельном файле (например, `utils.py`), импортировать как модуль в нужных DAG и вызывать отдельной задачей: - -```python -from airflow.operators.python import PythonOperator -from airflow.utils.trigger_rule import TriggerRule -from utils import notify_email - -LOCAL_CALCULATION_DT = '{{ dag.timezone.convert(execution_date).strftime("%Y-%m-%d") }}' -DAG_RUN_BEGIN_TIME = "{{ dag_run.start_date }}" - -email_notification_task = PythonOperator( - task_id="send_email_notification", - python_callable=notify_email, - dag=dag, - trigger_rule=TriggerRule.ALL_DONE, - op_args=[LOCAL_CALCULATION_DT, DAG_RUN_BEGIN_TIME], -) - -... >> email_notification_task -``` - -## Практическое применение: улучшенный DAG - -Теперь применим изученные концепции для усовершенствования нашего DAG. Добавим переменные и разобьем задачи на логические группы. - -Сначала создадим переменную `DATABASE_URL` со строкой подключения к базе данных через веб-интерфейс Airflow и импортируем ее в коде DAG: - -В этом примере используется переменная 'database_connection_string', предварительно созданная в интерфейсе Airflow, для хранения строки подключения к базе данных. Это позволяет избежать жесткого кодирования конфиденциальной информации в коде DAG и упрощает настройку подключения для разных окружений. - -```python -import os -import datetime as dt -import pandas as pd -from airflow.models import DAG -from airflow.operators.bash import BashOperator -from airflow.operators.python import PythonOperator -from airflow.operators.dummy import DummyOperator -from airflow.utils.task_group import TaskGroup -from airflow.models import Variable -from sqlalchemy import create_engine - -DATABASE_URL = Variable.get('database_connection_string') - -args = { - 'owner': 'analytics_team', - 'start_date': dt.datetime(2021, 6, 15), - 'retries': 2, - 'retry_delay': dt.timedelta(minutes=2), -} - -# функции для обработки данных -def get_file_path(file_name): - return os.path.join(os.path.expanduser('~/data'), file_name) - -def load_customer_data(): - file_path = get_file_path('customer_data.csv') - df = pd.read_csv(file_path) - engine = create_engine(DATABASE_URL) - df.to_sql('customers', engine, index=False, if_exists='replace', schema='staging') - -def aggregate_customer_data(): - engine = create_engine(DATABASE_URL) - customer_df = pd.read_sql('select * from staging.customers', con=engine) - - df = customer_df.groupby(['region', 'category']).agg( - total_orders=('orders', 'sum'), - avg_amount=('amount', 'mean') - ).reset_index() - - df.to_sql('customer_summary', engine, index=False, if_exists='replace', schema='analytics') - -dag = DAG( - dag_id='customer_pipeline_enhanced', - schedule_interval=None, - default_args=args, -) -``` - -Теперь разобьем задачи на логические группы и добавим Jinja-шаблоны для доступа к контексту выполнения: - -В этом примере создается начальная задача 'pipeline_start', которая использует Jinja-шаблоны для вывода информации о запуске DAG, включая идентификатор запуска (run_id) и информацию о DAG Run. Затем задачи группируются в логическую группу 'data_processing_stage', что улучшает структуру и читаемость DAG. - -```python -# Начальная задача с информацией о запуске -start_task = BashOperator( - task_id='pipeline_start', - bash_command='echo "Pipeline started! Run ID: {{ run_id }} | DAG Run: {{ dag_run }}"', - dag=dag, -) - -# Группа задач по предварительной обработке данных -with TaskGroup(group_id="data_processing_stage") as data_processing: - # Загрузка данных - load_customer_dataset = PythonOperator( - task_id='load_customer_data', - python_callable=load_customer_data, - dag=dag, +def notify_failure(context): + ti = context["ti"] + send_email_smtp( + to=MAIL_TO, + subject=f"Ошибка {ti.dag_id}: {ti.task_id}", + html_content=( + f"

Запуск: {escape(context['run_id'])}

" + f"

Причина: {escape(str(context['exception']))}

" + f'

Лог задачи

' + ), ) - # Агрегация и запись данных - aggregate_customer_dataset = PythonOperator( - task_id='aggregate_customer_data', - python_callable=aggregate_customer_data, - dag=dag, - ) - load_customer_dataset >> aggregate_customer_dataset -# Установка последовательности выполнения -start_task >> data_processing + +def notify_success(context): + send_email_smtp( + to=MAIL_TO, + subject=f"DAG {context['dag'].dag_id} завершен успешно", + html_content=f"

Запуск: {escape(context['run_id'])}

", + ) ``` -Поскольку последовательность задач внутри групп указывается при их создании, в конце необходимо определить порядок выполнения самих групп, чтобы планировщик понимал общую логику выполнения. +Для примера с ошибками задайте `on_failure_callback=notify_failure` у двух рабочих задач, а `on_success_callback=notify_success` у самого `DAG(...)`. Если упадут обе задачи, придут два сообщения об отказе. Сообщение об успехе отправится только после успешного DAG Run. Не включайте одновременно `email_on_failure=True`, если не хотите получать еще и встроенное письмо о той же ошибке. -Итоговый DAG будет выглядеть следующим образом: +Не добавляйте ради уведомления единственную завершающую задачу с `trigger_rule="all_done"`: если она успешна, Airflow может признать весь запуск успешным, несмотря на отказ внутри графа. Callback не меняет граф и итог обработки. Ошибка отправки означает проблему уведомления; она не превращает успешно обработанные данные в неуспешные и не скрывает исходный отказ. Подробности определения результата - в [описании DAG Run](https://airflow.apache.org/docs/apache-airflow/2.9.2/core-concepts/dag-run.html#dag-run-status). -```python -import os -import datetime as dt -import pandas as pd -from airflow.models import DAG -from airflow.operators.bash import BashOperator -from airflow.operators.python import PythonOperator -from airflow.operators.dummy import DummyOperator -from airflow.utils.task_group import TaskGroup -from airflow.models import Variable -from sqlalchemy import create_engine +Проверка: сначала воспроизведите окончательный отказ, затем успешный запуск. Сверьте текст каждого письма с состоянием соответствующего DAG Run. Упражнения находятся в [блоке ошибок](educational-tasks.md#errors) и [задании на почту](educational-tasks.md#resources). -DATABASE_URL = Variable.get('database_connection_string') +## Практика на учебном стенде -args = { - 'owner': 'analytics_team', - 'start_date': dt.datetime(2021, 6, 15), - 'retries': 2, - 'retry_delay': dt.timedelta(minutes=2), -} +Откройте [resource_management_dag.py](airflow-docker/dags/resource_management_dag.py). Он имитирует чтение двух источников и передает небольшие результаты через XCom. Для запуска нужен пул `training_pool` с двумя слотами; создайте его в Admin > Pools. -def get_file_path(file_name): - return os.path.join(os.path.expanduser('~/data'), file_name) +В исходном DAG задачи `read_customers` и `read_orders` не зависят друг от друга, но занимают два и один слот соответственно. Поэтому они выполняются последовательно. `calculate_metrics` получает их результаты через `xcom_pull`, а `log_metrics` пишет в лог словарь `customers=20`, `orders=30`. -def load_customer_data(): - file_path = get_file_path('customer_data.csv') - df = pd.read_csv(file_path) - engine = create_engine(DATABASE_URL) - df.to_sql('customers', engine, index=False, if_exists='replace', schema='staging') +В [заданиях 8.1-8.3](educational-tasks.md#resources) вы измените вес задачи в пуле, добавите метрику и объедините задачи чтения в TaskGroup. Там описаны изменения, ожидаемые результаты и места проверки. Файлы CSV и подключение к БД для этого примера не нужны: чтение источников имитируется. -def aggregate_customer_data(): - engine = create_engine(DATABASE_URL) - customer_df = pd.read_sql('select * from staging.customers', con=engine) - - df = customer_df.groupby(['region', 'category']).agg( - total_orders=('orders', 'sum'), - avg_amount=('amount', 'mean') - ).reset_index() - - df.to_sql('customer_summary', engine, index=False, if_exists='replace', schema='analytics') - -dag = DAG( - dag_id='customer_pipeline_enhanced', - schedule_interval=None, - default_args=args, -) - -# Начальная задача -start_task = BashOperator( - task_id='pipeline_start', - bash_command='echo "Pipeline started! Run ID: {{ run_id }} | DAG Run: {{ dag_run }}"', - dag=dag, -) - -# Группа предварительной обработки -with TaskGroup(group_id="data_processing_stage") as data_processing: - load_customer_dataset = PythonOperator( - task_id='load_customer_data', - python_callable=load_customer_data, - dag=dag, - ) - aggregate_customer_dataset = PythonOperator( - task_id='aggregate_customer_data', - python_callable=aggregate_customer_data, - dag=dag, - ) - load_customer_dataset >> aggregate_customer_dataset - -start_task >> data_processing -``` - -В этом материале мы рассмотрели расширенные возможности Airflow, которые помогут улучшить работу ваших пайплайнов: -- Управление ресурсами с помощью пулов задач -- Обмен данными между задачами через XCom -- Логическая группировка задач с TaskGroup -- Настройка системы оповещений - -Помните: не стоит использовать все доступные функции сразу. Выбирайте инструменты последовательно и находите оптимальный набор возможностей под конкретную задачу. +Для опытов с callback используйте [error_handling_dag.py](airflow-docker/dags/error_handling_dag.py) и [блок 7](educational-tasks.md#errors). Отправка настоящей почты остается заданием по желанию. diff --git a/AGENTS.md b/AGENTS.md index 994938c..5dea9cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,9 @@ This repository contains an educational manual for Apache Airflow with runnable - `data/`: sample input data for exercises. - Note: `airflow-docker/AGENTS.md` adds extra, folder-specific guidance and takes precedence there. +## Practice Improvement Work +При переработке практических заданий, учебного поведения DAG или маршрута курса сначала прочитайте [план обновления практики](docs/specs/2026-09-28-practice-improvement-plan.md): согласованные решения, текущий этап и критерии его завершения. + ## Build, Test, and Development Commands Prerequisite: Docker + Docker Compose. - Build images: `cd airflow-docker && docker-compose build` diff --git a/README.md b/README.md index 66660a9..df810e8 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,8 @@ # Учебник по Apache Airflow для начинающих -Добро пожаловать в учебник по Apache Airflow! Этот курс создан специально для тех, кто уже освоил Python и SQL и готов познакомиться со своим первым инструментом для ETL-процессов. К учебнику прилагается готовый практический стенд, позволяющий сразу применить полученные знания на практике. +Учебник знакомит с Apache Airflow 2.9: устройством DAG, запуском задач, интерфейсом и поиском ошибок. К нему прилагается Docker-стенд с готовыми примерами. Перед началом нужны Python, pandas, SQL и основы Docker. -## 📚 Введение - -Apache Airflow — это мощный оркестратор рабочих процессов с открытым исходным кодом, который стал стандартом де-факто в мире обработки данных. Если вы работаете с автоматизацией процессов обработки информации — будь то подготовка данных для машинного обучения, создание отчетов или интеграция данных из различных источников — Airflow поможет вам организовать эти процессы эффективно и надежно. - -Этот учебник разработан с учетом того, что вы: -- Имеете базовые знания Python и SQL -- Хотите освоить свой первый ETL-инструмент -- Нуждаетесь в практическом руководстве с понятными объяснениями - -Мы начнем с основ и постепенно перейдем к продвинутым возможностям, чтобы вы могли уверенно использовать Airflow в реальных проектах. Курс ориентирован на версию Airflow 2.9 и использует практический подход с множеством примеров и визуальных материалов. - -## 🎯 Что вы узнаете +## Что вы узнаете - **Основы Airflow**: что это такое, для каких задач используется и почему он стал таким популярным - **Ключевые концепции**: DAG, задачи, операторы и другие фундаментальные понятия @@ -23,7 +12,7 @@ Apache Airflow — это мощный оркестратор рабочих п - **Отладка и мониторинг**: как понимать статусы задач и быстро находить проблемы - **Продвинутые возможности**: шаблоны, параметры, управление временем, пулы, XCom, TaskGroup и алертинг -## 📖 Содержание курса +## Содержание курса ### [01. Введение в Airflow](01%20-%20Введение%20в%20Airflow.md) - Почему Apache Airflow стал незаменимым инструментом для работы с данными @@ -73,34 +62,25 @@ Apache Airflow — это мощный оркестратор рабочих п - Логическая группировка задач с TaskGroup - Настройка системы оповещений и уведомлений -## 🛠️ Практический стенд +## Практический стенд -К этому учебнику прилагается полноценный стенд Apache Airflow, который позволяет сразу приступить к практике и закрепить полученные знания. В папке [`airflow-docker`](airflow-docker/) вы найдете готовую среду для экспериментов с 6 готовыми DAG'ами разной сложности и практическими заданиями для каждого из них. +В [`airflow-docker`](airflow-docker/) находятся Airflow 2.9.2, учебная PostgreSQL и девять готовых DAG. [Инструкция запуска](airflow-docker/README.md) поможет поднять стенд, а [маршрут практики](educational-tasks.md) связывает восемь блоков заданий с главами учебника. -Стенд включает настроенную среду с PostgreSQL для тренировок, образцы данных и детальные инструкции для практических занятий. Подробную информацию о возможностях стенда и инструкции по запуску смотрите в [`README.md`](airflow-docker/README.md) папки `airflow-docker`. +Здесь вы запускаете готовый DAG, меняете одну часть и проверяете результат в графе, логах или данных. Самостоятельные процессы обработки данных относятся к курсовым работам дальше по [программе](https://de.dementev.space/). -## 🚀 Как использовать этот учебник +## Как использовать этот учебник -1. **Изучайте теорию**: начните с Введения в Airflow и переходите к следующим главам по порядку -2. **Практикуйтесь на стенде**: после каждой главы выполняйте соответствующие задания в практическом стенде -3. **Создавайте свои DAG**: применяйте полученные знания для решения реальных задач -4. **Используйте визуальные материалы**: в учебнике много изображений и диаграмм для лучшего понимания -5. **Экспериментируйте**: не бойтесь пробовать разные настройки и сценарии на учебном стенде +1. Прочитайте главы 1-5 и запустите стенд. Перед каждым блоком практики сверяйтесь с колонкой "Перед практикой": некоторые задания используют материал следующих глав. +2. Выполняйте основные задания блока по порядку и сохраняйте изменения. Каждый новый блок описывает, с какого состояния начать. +3. Проверяйте раздел "Что должно получиться". В опыте с ошибкой результатом может быть `failed`; важно найти причину и восстановить успешный запуск. +4. Задания "По желанию" можно пропустить. Они не нужны для продолжения основного маршрута, а высокая сложность отмечена отдельно. -## 📝 Технические требования +Практика завершена, когда пройдены основные задания всех восьми блоков и вы можете объяснить наблюдаемое поведение. Выполнение всех усложнений для этого не требуется. -- **Python**: базовое знание языка +## Технические требования + +- **Python и pandas**: чтение функций, работа с таблицами - **SQL**: понимание основных запросов и концепций - **Airflow**: версия 2.9 (на которой ориентирован курс) +- **Docker и Docker Compose**: запуск учебного стенда - **Среда разработки**: любой текстовый редактор или IDE с поддержкой Python - -## 💡 Советы для успешного обучения - -- **Не торопитесь**: лучше глубоко понять каждый концепт, чем быстро пройти весь курс -- **Задавайте вопросы**: если что-то непонятно, вернитесь к предыдущим материалам -- **Делайте заметки**: записывайте важные моменты и примеры кода -- **Применяйте на практике**: сразу пробуйте создавать свои DAG для реальных задач - ---- - -*Этот учебник создан для начинающих специалистов в области данных и инженерии. Все материалы ориентированы на практическое применение и пошаговое освоение Apache Airflow.* diff --git a/_attachments/practice-failed-run.png b/_attachments/practice-failed-run.png new file mode 100644 index 0000000..19359ba Binary files /dev/null and b/_attachments/practice-failed-run.png differ diff --git a/_attachments/practice-rendered-template.png b/_attachments/practice-rendered-template.png new file mode 100644 index 0000000..377edb1 Binary files /dev/null and b/_attachments/practice-rendered-template.png differ diff --git a/_attachments/practice-task-group.png b/_attachments/practice-task-group.png new file mode 100644 index 0000000..ed121fb Binary files /dev/null and b/_attachments/practice-task-group.png differ diff --git a/airflow-docker/README.md b/airflow-docker/README.md index a08140f..ef84db7 100644 --- a/airflow-docker/README.md +++ b/airflow-docker/README.md @@ -1,273 +1,105 @@ -# Educational Airflow Setup for Beginners +# Учебный стенд Apache Airflow -Простой учебный стенд Apache Airflow для начинающих, изучающих SQL и Python. +Стенд содержит Airflow 2.9.2, учебную PostgreSQL и девять DAG. Он нужен для знакомства с инструментом: запустить пример, изменить его и посмотреть результат. Для работы понадобятся Docker с Compose, браузер и знания Python, pandas и SQL. -## 🎯 Цель проекта +## Запуск -Создать максимально простую среду для изучения Apache Airflow. -Для удобства все переменные окружения захардкожены в docker скрипты. На проде так делать не надо :) - -## 📋 Предварительные требования - -- Docker и Docker Compose -- Базовые знания Python и SQL -- Веб-браузер для доступа к интерфейсу Airflow - -## 🚀 Быстрый старт - -### 1. Запуск стенда +Из корня репозитория: ```bash -# Перейдите в директорию проекта cd airflow-docker - -# Запустите все сервисы -docker-compose up -d +docker compose up -d --build ``` -### 2. Доступ к интерфейсам +Команды подходят для Linux и PowerShell в Windows. При первом запуске Docker собирает образ, поднимает две БД, выполняет `airflow-init`, затем запускает webserver и scheduler. Инициализация создает учетную запись и подключение `postgres_training`. -- **Airflow UI**: http://localhost:8080 - - Логин: `admin` - - Пароль: `admin` +Проверьте состояние: -- **PostgreSQL для тренировок**: `localhost:5432` - - База данных: `training` - - Пользователь: `student` - - Пароль: `student` - -- **PostgreSQL для метаданных Airflow**: `localhost:5434` - - База данных: `airflow` - - Пользователь: `airflow` - - Пароль: `airflow` - -## 🏗️ Архитектура стенда - -```mermaid -graph LR - - %% Локальная файловая система - subgraph local["Локальная файловая система"] - dags["Каталог
Educational DAGs"] - fs["Local Filesystem"] - dags --> fs - end - - %% Docker-контейнеры - subgraph docker["Контейнеры Docker"] - subgraph airflow["Airflow"] - ws["Airflow Webserver"] - sch["Airflow Scheduler"] - meta["PostgreSQL Metadata"] - sch --> meta - ws --> meta - end - - subgraph training["Учебная БД"] - train["PostgreSQL Training"] - end - end - - %% Клиенты - subgraph clients["Клиенты"] - user["Пользователь
(браузер)"] - sql["SQL клиент"] - end - - %% Монтирование DAG'ов в контейнеры Airflow - dags -. "том с DAG-файлами" .- ws - dags -. "том с DAG-файлами" .- sch - - %% Airflow-задачи работают с учебной БД - sch -- "задачи DAG" --> train - - %% Внешние подключения - user -- "порт 8080" --> ws - sql -- "порт 5432" --> train +```bash +docker compose ps -a +docker compose logs airflow-init ``` -## 📁 Структура проекта +Завершение `airflow-init` с кодом 0 нормально: это разовая подготовка. Остальные четыре сервиса должны работать. Откройте http://localhost:8080 и войдите как `admin` / `admin`. -``` +| Сервис | Адрес с компьютера | База | Пользователь / пароль | +|---|---|---|---| +| Airflow UI | `http://localhost:8080` | - | `admin` / `admin` | +| Учебная PostgreSQL | `localhost:5432` | `training` | `student` / `student` | +| Метаданные Airflow | `localhost:5434` | `airflow` | `airflow` / `airflow` | + +В задачах используется адрес `postgres-training:5432`: это имя сервиса внутри Docker. Базу метаданных для упражнений не используйте. Учебные пароли заданы в Compose; для публичного сервера такая конфигурация не подходит. + +## Прохождение практики + +Откройте [задания](../educational-tasks.md). Их восемь блоков проходят последовательно; перед каждым указаны нужные главы [учебника](../README.md), исходные файлы и настройки. + +Основные задания внутри блока сохраняют изменения предыдущих шагов. Дополнительные находятся в конце блока и помечены "По желанию"; их можно пропустить. Для завершения практики достаточно основных заданий и проверки их результатов. + +| Блок | DAG | +|---|---| +| [Первый DAG](../educational-tasks.md#first-dag) | `hello_world_dag` | +| [SQL и подключения](../educational-tasks.md#sql) | `sql_basic_dag` | +| [Работа с файлами](../educational-tasks.md#files) | `file_operations_dag` | +| [Загрузка CSV и DQ](../educational-tasks.md#load-dq) | `csv_to_postgres`, `csv_to_postgres_dq` | +| [Разбор ETL](../educational-tasks.md#etl) | `data_processing_dag` | +| [Ветвление](../educational-tasks.md#branching) | `branching_dag` | +| [Ошибки и повторы](../educational-tasks.md#errors) | `error_handling_dag` | +| [Пулы, XCom и TaskGroup](../educational-tasks.md#resources) | `resource_management_dag` | + +Описание поведения исходных примеров находится в [справочнике DAG](dag-specifications.md). Почта нужна только для отдельного задания по желанию; SMTP в стенде не настроен. + +## Файлы и данные + +```text airflow-docker/ -├── docker-compose.yml # Конфигурация Docker -├── .env # Файл не используется: переменные заданы в docker-compose.yml -├── dags/ # DAG файлы для обучения -│ ├── hello_world_dag.py # Базовый пример -│ ├── sql_basic_dag.py # Работа с SQL -│ ├── file_operations_dag.py # Обработка файлов -│ ├── csv_to_postgres.py # Загрузка CSV в Postgres (ETL) -│ ├── csv_to_postgres_dq.py # Проверки качества данных (DQ) -│ ├── data_processing_dag.py # Сложный ETL пайплайн -│ ├── branching_dag.py # Условная логика -│ └── error_handling_dag.py # Обработка ошибок -├── data/ # Данные и артефакты прогонов -│ ├── input/ # Входные данные -│ └── output/ # Сгенерированные CSV, отчеты и результаты обработки -├── logs/ # Логи Airflow -├── README.md # Эта инструкция -└── educational-tasks.md # Практические задания для студентов +├── docker-compose.yml # Сервисы, порты и учебные подключения +├── Dockerfile # Airflow 2.9.2 +├── requirements.txt # Дополнительные Python-пакеты +├── dags/ # Девять учебных DAG +├── data/input/ # Входные CSV и данные упражнений +├── data/output/ # Результаты задач +└── init-postgres.sql # Начальная подготовка учебной БД ``` -## 🎓 Учебные материалы +`dags/` и `data/` смонтированы в контейнеры как `/opt/airflow/dags/` и `/opt/airflow/data/`. Правки DAG подхватываются планировщиком; перед новым запуском дождитесь обновления вкладки Code. Резервные копии Python-файлов храните вне `dags/`, чтобы не создать два определения одного `dag_id`. -### Основы Airflow +Таблицы PostgreSQL сохраняются в именованных томах `pg_data` и `pgmeta`. Обычная остановка их не удаляет. Логи Airflow хранятся внутри контейнеров; важные логи опыта сохраните до удаления контейнеров. -**Цели:** -- Понимание структуры DAG -- Создание простых задач -- Настройка зависимостей между задачами - -**Примеры DAG:** -- `hello_world_dag.py` - базовые операторы Python -- `sql_basic_dag.py` - работа с базами данных - -### Интеграция с данными - -**Цели:** -- Подключение к PostgreSQL -- Выполнение SQL запросов -- Обработка файлов CSV - -**Примеры DAG:** -- `file_operations_dag.py` - работа с файлами -- `csv_to_postgres.py` - загрузка данных из CSV в PostgreSQL -- `csv_to_postgres_dq.py` - автоматизированные проверки качества (Data Quality) -- `data_processing_dag.py` - ETL процессы - -### Продвинутые возможности - -**Цели:** -- Условное выполнение задач -- Обработка ошибок -- Параметризация workflows - -**Примеры DAG:** -- `branching_dag.py` - условная логика -- `error_handling_dag.py` - обработка ошибок - -## 🎯 Практические задания - -Для закрепления знаний по каждому DAG мы подготовили практические задания разного уровня сложности: - -### 📋 Учебные задания - -В файле **[educational-tasks.md](educational-tasks.md)** вы найдете детальные задания для каждого DAG: - -- 🌱 **Начальный уровень**: Базовые задачи по созданию и настройке DAG'ов -- 📚 **Средний уровень**: Работа с данными, файлами и SQL операциями -- 🚀 **Продвинутый уровень**: ETL процессы, ветвление, обработка ошибок - -### 🎓 Рекомендуемый путь обучения - -1. **Начните с `hello_world_dag.py`** - освоите основы Airflow -2. **Перейдите к `sql_basic_dag.py`** - изучите работу с базами данных -3. **Попрактикуйтесь на `file_operations_dag.py`** - работа с файлами -4. **Освойте ETL и DQ на `csv_to_postgres.py` и `csv_to_postgres_dq.py`** - загрузка и валидация данных -5. **Разберите сложный ETL на `data_processing_dag.py`** - обработка данных -6. **Изучите продвинутые темы** - ветвление и обработка ошибок - -Каждое задание содержит: -- Цель и сложность выполнения -- Конкретные требования к модификации кода -- Ожидаемый результат -- Время на выполнение - -## 🔧 Технические детали - -### Переменные окружения - -В этом проекте мы не используем `.env`: все значения заданы напрямую в `docker-compose.yml`. - -Сервисы Airflow получают: -- `POSTGRES_TRAINING_HOST=postgres-training` -- `POSTGRES_TRAINING_PORT=5432` -- `POSTGRES_TRAINING_DB=training` -- `POSTGRES_TRAINING_USER=student` -- `POSTGRES_TRAINING_PASSWORD=student` -- `AIRFLOW_CONN_POSTGRES_TRAINING=postgresql://student:student@postgres-training:5432/training` - -`docker-compose run --rm airflow-init` запускает `airflow connections add postgres_training`, поэтому соединение доступно сразу после инициализации. Проверить наличие можно через: +Для SQL-запросов из заданий: ```bash -docker-compose exec airflow-webserver airflow connections get postgres_training +docker compose exec postgres-training psql -U student -d training ``` -`csv_to_postgres.py` по умолчанию складывает сгенерированные CSV в `/opt/airflow/data/output`, то есть в локальный каталог `airflow-docker/data/output/`. +Выход из psql - `\q`. Готовое подключение Airflow можно проверить командой: - -### Порты - -- `8080` - Airflow Webserver -- `5432` - PostgreSQL для тренировок -- `5434` - PostgreSQL для метаданных Airflow - -## 🛠️ Управление стендом - -### Запуск сервисов ```bash -docker-compose up -d +docker compose exec airflow-webserver airflow connections get postgres_training ``` -### Остановка сервисов +## Управление и поиск ошибок + +Все команды ниже выполняются из `airflow-docker`. + ```bash -docker-compose down +docker compose exec airflow-webserver airflow version +docker compose exec airflow-webserver airflow dags list +docker compose exec airflow-webserver airflow dags list-import-errors +docker compose logs airflow-scheduler +docker compose logs airflow-webserver ``` -### Просмотр логов +Если DAG не появился, проверьте путь файла и ошибки импорта. Если задача упала, сначала откройте ее лог в UI. При ошибке подключения проверьте `docker compose ps` и Conn Id задачи. Задачи из блока 8 ждут, пока вы создадите указанный в задании пул. + +Остановка с сохранением данных: + ```bash -# Логи Airflow -docker-compose logs airflow-webserver -docker-compose logs airflow-scheduler - -# Логи PostgreSQL -docker-compose logs postgres-training -docker-compose logs postgres-metadata +docker compose down ``` -### Перезапуск конкретного сервиса -```bash -docker-compose restart airflow-webserver -``` +Повторный запуск - `docker compose up -d`. Для полного сброса обеих БД используйте `docker compose down -v`: эта команда удалит учебные таблицы, историю запусков и настройки Airflow в томах. Файлы в `data/` останутся. -## 🐛 Решение проблем +## Для авторов курса -### DAG не появляется в интерфейсе -- Проверьте, что файл находится в папке `dags/` -- Убедитесь в правильности синтаксиса Python -- Проверьте логи планировщика: `docker-compose logs airflow-scheduler` - -### Ошибки подключения к базе данных -- Убедитесь, что PostgreSQL запущен: `docker-compose ps` -- Проверьте логи PostgreSQL: `docker-compose logs postgres-training` - -### Задачи завершаются с ошибкой -- Проверьте логи задачи в интерфейсе Airflow -- Убедитесь в наличии необходимых Python пакетов - -## 📚 Дополнительные ресурсы - -- [Официальная документация Airflow](https://airflow.apache.org/docs/) -- [Учебные материалы в родительской папке](../) -- [Практические задания для студентов](educational-tasks.md) - ---- - -## 🌟 Начало работы с заданиями - -После запуска стенда Airflow, откройте файл [educational-tasks.md](educational-tasks.md) и выберите задание, соответствующее вашему уровню подготовки. Каждое задание содержит подробные инструкции и ожидаемые результаты. - -**Быстрый старт с заданиями:** -```bash -# Запустите стенд -docker-compose up -d - -# Откройте задания в браузере или редакторе -# И следуйте инструкциям из educational-tasks.md -``` - -Удачи в изучении Apache Airflow! - ---- - -**Примечание**: Этот стенд предназначен исключительно для учебных целей. Для production использования требуется дополнительная настройка безопасности. +[План обновления практики](../docs/specs/2026-09-28-practice-improvement-plan.md) сохраняет согласованные решения, разбор исходных заданий и результаты проверок. diff --git a/airflow-docker/dag-specifications.md b/airflow-docker/dag-specifications.md index ab674da..127a732 100644 --- a/airflow-docker/dag-specifications.md +++ b/airflow-docker/dag-specifications.md @@ -1,327 +1,29 @@ -# Educational DAG Specifications for Airflow Learning +# Что делают исходные DAG -## Learning Progression Structure +Это справочник примеров для Airflow 2.9.2. Порядок прохождения, изменения кода и условия завершения находятся в [практических заданиях](../educational-tasks.md). -### Level 1: Basic Concepts (Week 1) +| DAG и файл в `dags/` | Подготовка | Поведение и результат | +|---|---|---| +| `hello_world_dag` (`hello_world_dag.py`) | Не требуется | Три Python-задачи печатают приветствие, дату и завершение. Расписание ежедневное, `catchup=False`. | +| `sql_basic_dag` (`sql_basic_dag.py`) | Connection `postgres_training`, создаваемый при запуске стенда | Создает `public.students_sample`, очищает перед вставкой, добавляет трех студентов и выполняет SELECT. После каждого полного запуска в таблице три строки. | +| `file_operations_dag` (`file_operations_dag.py`) | Каталоги `data/input`, `data/output` из репозитория | Генерирует 100 строк, проверяет данные, сохраняет `output/processed_data.csv` и `output/summary.txt`. Категория High начинается с 70000. | +| `csv_to_postgres` (`csv_to_postgres.py`) | Connection `postgres_training` | Создает `public.orders`, генерирует CSV на 1000 строк, передает путь через XCom и загружает файл. Уже существующие `order_id` пропускаются; новый полный запуск генерирует новые заказы. | +| `csv_to_postgres_dq` (`csv_to_postgres_dq.py`) | Успешная загрузка `csv_to_postgres`; запускать вручную после нее | Проверяет таблицу, точную схему из четырех колонок, наличие строк и отсутствие дубликатов. Сводка перечисляет состояния проверок. По умолчанию после ошибки последующие задачи не выполняются (`all_success`). | +| `data_processing_dag` (`data_processing_dag.py`) | Не требуется, свои входные CSV создаются заново | Извлекает 5 клиентов и 6 заказов в `output/extracted_*.csv`, читает их при преобразовании. `simulate_load` печатает число строк и DDL без записи в БД. Отчет: 6 заказов, выручка 2105. | +| `branching_dag` (`branching_dag.py`) | Variable `branch_format`: `csv` или `json`; по умолчанию `csv` | Выбранная ветка имитирует обработку, другая получает `skipped`. Слияние читает XCom текущего запуска с `none_failed_min_one_success`. Неизвестный формат приводит к ошибке. | +| `error_handling_dag` (`error_handling_dag.py`) | Variable `error_mode`, по умолчанию `retry_once` | Две рабочие задачи, по одному повтору с задержкой 10 секунд. Оба обработчика зависят от обеих задач. Окончательный отказ сохраняет `failed` у DAG Run, даже если обработчик ошибки успешен. | +| `resource_management_dag` (`resource_management_dag.py`) | Создать `training_pool` с 2 слотами | Две задачи по 20 секунд имитируют чтение и занимают 2 и 1 слот, поэтому работают последовательно. Через XCom передают 20 и 30; следующая задача возвращает словарь, последняя печатает его. | -#### 1.1 hello_world_dag.py -**Learning Objectives:** -- Understand basic DAG structure -- Learn about PythonOperator -- Understand task dependencies +Все DAG, кроме `hello_world_dag`, запускаются вручную. Связи между загрузчиком CSV и DQ-DAG нет. Файлы из `input/orders.csv` в ETL-примере и таблица `public.orders` в загрузчике относятся к разным блокам. -**DAG Structure:** -```python -from airflow import DAG -from airflow.operators.python import PythonOperator -from datetime import datetime +## Режимы примера с ошибками -def print_hello(): - print("Hello World from Airflow!") +| `error_mode` | Рабочие задачи | Обработчики | DAG Run | +|---|---|---|---| +| `success` | Обе успешны с первой попытки | Успех: `success`, ошибка: `skipped` | `success` | +| `retry_once` | `retry_task` падает один раз, затем успешна | Успех: `success`, ошибка: `skipped` | `success` | +| `fail_first` | `unreliable_task` окончательно падает | Успех: `upstream_failed`, ошибка: `success` | `failed` | +| `fail_second` | `retry_task` окончательно падает | Успех: `upstream_failed`, ошибка: `success` | `failed` | +| `fail_both` | Обе окончательно падают | Успех: `upstream_failed`, ошибка: `success` | `failed` | -def print_date(): - print(f"Current date: {datetime.now()}") - -def print_goodbye(): - print("Goodbye from Airflow!") - -# DAG definition with simple tasks -``` - -**Tasks:** -- `start_task`: Print welcome message -- `date_task`: Print current date/time -- `end_task`: Print goodbye message - -**Dependencies:** -start_task → date_task → end_task - -#### 1.2 sql_basic_dag.py -**Learning Objectives:** -- Connect to PostgreSQL database -- Execute SQL queries -- Use PostgresOperator - -**Connection Hint:** `docker-compose run --rm airflow-init` automatically provisions the `postgres_training` connection via `airflow connections add`, so no manual setup is required. You can verify it with `docker-compose exec airflow-webserver airflow connections get postgres_training`. - -**Tasks:** -- `create_table`: Create simple table (users, products) -- `insert_data`: Insert sample records -- `query_data`: Select and display data -- `drop_table`: Clean up (optional) - -**SQL Operations:** -```sql --- Create table -CREATE TABLE IF NOT EXISTS students ( - id SERIAL PRIMARY KEY, - name VARCHAR(100), - age INTEGER, - created_at TIMESTAMP DEFAULT NOW() -); - --- Insert data -INSERT INTO students (name, age) VALUES -('Alice', 22), -('Bob', 24), -('Charlie', 21); -``` - -### Level 2: Intermediate Concepts (Week 2) - -#### 2.1 file_operations_dag.py -**Learning Objectives:** -- File system operations -- CSV data processing -- Data transformation - -**Tasks:** -- `generate_sample_data`: Create CSV file with random data -- `read_csv_file`: Read and validate data -- `transform_data`: Simple data transformations -- `write_output`: Save processed data - -**Sample Data Structure:** -```csv -id,name,department,salary -1,Alice,Engineering,50000 -2,Bob,Marketing,45000 -3,Charlie,Sales,48000 -``` - -#### 2.2 csv_to_postgres.py -**Learning Objectives:** -- Load CSV data into PostgreSQL database -- Implement idempotent loading via temporary table -- Use XCom for passing file paths between tasks -- Work with PostgreSQL connections in Airflow - -**Scenario:** -Generate sample orders data as CSV, preview it, and load it into PostgreSQL. - -**Tasks:** -- `create_orders_table`: Create public.orders table in PostgreSQL -- `generate_csv`: Generate sample orders CSV file -- `preview_csv`: Display first few rows of CSV -- `load_csv_to_postgres`: Load CSV data into PostgreSQL using temporary table - -**Database Connection:** Uses `postgres_training` connection (auto-provisioned by init script). -**Generated Files:** CSV files are written to `/opt/airflow/data/output/`. - -**Sample Data Structure:** -```csv -order_id,order_ts,customer_id,amount -1,2023-10-01 10:30:00,101,1250.50 -2,2023-10-01 11:45:00,102,890.00 -3,2023-10-02 09:15:00,103,2100.75 -``` - -**Data Quality Checks:** Run `csv_to_postgres_dq.py` after loading to validate the target table. - -#### 2.3 csv_to_postgres_dq.py -**Learning Objectives:** -- Implement data quality validation in Airflow -- Use Python functions for data checks -- Handle data quality failures -- Separate validation from main ETL pipeline - -**Scenario:** -Run automated data quality checks on the public.orders table after CSV loading. - -**Tasks:** -- `check_table_exists`: Verify public.orders table exists -- `check_schema`: Validate table schema matches expected structure -- `check_row_count`: Ensure table has data -- `check_duplicates`: Verify no duplicate order_id values - -**Quality Checks:** -- Table existence in public schema -- Column names and data types (order_id, order_ts, customer_id, amount) -- Minimum row count (> 0) -- Unique order_id values (no duplicates) - -**Helper Functions:** All DQ check functions are defined directly in `csv_to_postgres_dq.py`. - -#### 2.4 data_processing_dag.py -**Learning Objectives:** -- ETL pipeline concepts -- Multiple data sources -- Error handling basics - -**Tasks:** -- `extract_customers`: Read customer data -- `extract_orders`: Read order data -- `transform_data`: Join and process data -- `load_to_database`: Save results -- `generate_report`: Create summary - -### Level 3: Advanced Concepts (Week 3) - -#### 3.1 branching_dag.py -**Learning Objectives:** -- Conditional task execution -- BranchPythonOperator -- Decision making in workflows - -**Scenario:** -Process data based on file type or data quality - -**Tasks:** -- `check_file_type`: Determine processing path -- `process_csv_branch`: For CSV files -- `process_json_branch`: For JSON files -- `merge_results`: Combine outputs - -#### 3.2 error_handling_dag.py -**Learning Objectives:** -- Task retries -- Error notifications -- Failure handling - -**Tasks:** -- `unreliable_task`: Simulate failures -- `retry_task`: Demonstrate retry mechanism -- `success_handler`: On success callback -- `failure_handler`: On failure callback - -### Level 4: Orchestration & Collaboration (Week 4) - -#### 4.1 advanced_features_dag.py -**Learning Objectives:** -- Control concurrency with pools and `pool_slots` -- Exchange data between tasks using XCom -- Group related tasks using `TaskGroup` -- Configure email-based alerting on failures - -**Scenario:** -Enhanced daily analytics pipeline that reads data from the training database, performs transformations, and writes summaries, while limiting heavy backup tasks via a dedicated pool and sending notifications about pipeline status. - -**Tasks:** -- `extract_group`: Use `TaskGroup` to wrap extract tasks (e.g., customers and orders) -- `transform_group`: Aggregate metrics and prepare summary tables -- `load_group`: Simulate loading results back into the training database or files -- `backup_task`: Heavy backup task running in a dedicated pool (e.g., `backup_pool`) with custom `pool_slots` -- `calculate_metrics`: Python task that returns aggregated metrics (pushed to XCom) -- `log_metrics`: Task that reads metrics via `xcom_pull` and logs them or uses them in a template -- `send_notification`: Final notification task (email or log) triggered with `ALL_DONE` semantics - -## Sample Data Files - -### customers.csv -```csv -customer_id,name,email,join_date -1,Alice Johnson,alice@example.com,2023-01-15 -2,Bob Smith,bob@example.com,2023-02-20 -3,Charlie Brown,charlie@example.com,2023-03-10 -``` - -### orders.csv -```csv -order_id,customer_id,product,amount,order_date -101,1,Laptop,1200,2023-10-01 -102,2,Monitor,300,2023-10-02 -103,1,Keyboard,80,2023-10-03 -104,3,Mouse,25,2023-10-04 -``` - -### products.csv -```csv -product_id,name,category,price -1,Laptop,Electronics,1200 -2,Monitor,Electronics,300 -3,Keyboard,Electronics,80 -4,Mouse,Electronics,25 -``` - -## Database Schema for Training - -### Students Table -```sql -CREATE TABLE students ( - student_id SERIAL PRIMARY KEY, - first_name VARCHAR(50), - last_name VARCHAR(50), - email VARCHAR(100), - enrollment_date DATE, - grade INTEGER -); -``` - -### Courses Table -```sql -CREATE TABLE courses ( - course_id SERIAL PRIMARY KEY, - course_name VARCHAR(100), - instructor VARCHAR(100), - credits INTEGER -); -``` - -### Enrollments Table -```sql -CREATE TABLE enrollments ( - enrollment_id SERIAL PRIMARY KEY, - student_id INTEGER REFERENCES students(student_id), - course_id INTEGER REFERENCES courses(course_id), - enrollment_date DATE, - grade CHAR(1) -); -``` - -## Learning Outcomes by Week - -### Week 1: Foundation -- ✅ Understand DAG structure and components -- ✅ Create basic Python tasks -- ✅ Set up task dependencies -- ✅ Run first successful workflow - -### Week 2: Integration -- ✅ Connect to databases -- ✅ Execute SQL operations -- ✅ Process file data -- ✅ Build simple ETL pipelines - -### Week 3: Advanced Features -- ✅ Implement conditional logic -- ✅ Handle errors and retries -- ✅ Use parameters and templates -- ✅ Monitor and debug workflows - -### Week 4: Orchestration & Operations -- ✅ Use pools to control resource usage -- ✅ Share data between tasks via XCom -- ✅ Group tasks using `TaskGroup` -- ✅ Configure alerting and notifications for failures - -## Common Pitfalls and Solutions - -### Problem: DAG not appearing in UI -**Solution:** Check DAG file location and syntax - -### Problem: Database connection errors -**Solution:** Verify connection strings and database availability - -### Problem: Task failures -**Solution:** Check logs, implement proper error handling - -### Problem: Scheduling issues -**Solution:** Understand cron expressions and execution dates - -## Assessment Criteria - -### Basic Competency -- Can create simple DAG with 3+ tasks -- Understands task dependencies -- Can run and monitor workflows - -### Intermediate Competency -- Can integrate with databases -- Can process file data -- Implements basic error handling - -### Advanced Competency -- Uses conditional branching -- Implements proper error handling -- Creates reusable components -- Optimizes workflow performance +Меняйте Variable после завершения предыдущего запуска. `retries=1` означает две попытки всего. `failure_handler` использует `one_failed`: достаточно одного окончательного отказа, ждать завершения другой рабочей задачи ему не обязательно. diff --git a/airflow-docker/dags/branching_dag.py b/airflow-docker/dags/branching_dag.py index 3b7b2e2..b30587e 100644 --- a/airflow-docker/dags/branching_dag.py +++ b/airflow-docker/dags/branching_dag.py @@ -1,103 +1,72 @@ +"""Выбор ветки через Variable branch_format: csv или json. + +Обработка файлов имитируется. В основной практике изучаются граф и статусы. """ -DAG для демонстрации условного выполнения задач в Airflow -Уровень: Продвинутый -""" -from datetime import datetime, timedelta +import logging +from datetime import datetime + from airflow import DAG -from airflow.operators.python import PythonOperator, BranchPythonOperator -from airflow.operators.dummy import DummyOperator -import random +from airflow.models import Variable +from airflow.operators.empty import EmptyOperator +from airflow.operators.python import BranchPythonOperator, PythonOperator -# Определение DAG -default_args = { - 'owner': 'student', - 'depends_on_past': False, - 'start_date': datetime(2023, 1, 1), - 'email_on_failure': False, - 'email_on_retry': False, - 'retries': 1, - 'retry_delay': timedelta(minutes=5) -} -dag = DAG( - 'branching_dag', - default_args=default_args, - description='DAG для изучения условного выполнения задач в Airflow', - schedule_interval=None, - catchup=False, - tags=['educational', 'branching', 'advanced'] -) +def choose_format(): + """Возвращает task_id выбранной ветки.""" + file_format = Variable.get("branch_format", default_var="csv") + branches = {"csv": "process_csv_branch", "json": "process_json_branch"} + if file_format not in branches: + raise ValueError( + f"branch_format должен быть csv или json, получено: {file_format}" + ) + logging.info("Выбран формат: %s", file_format) + return branches[file_format] -def check_data_quality(): - """Проверка качества данных - случайным образом определяет, какие данные использовать""" - # В реальном сценарии здесь будет проверка качества данных - # Для учебных целей просто случайное решение - quality_score = random.random() # случайное число от 0 до 1 - - if quality_score > 0.5: - print(f"Качество данных хорошее (оценка: {quality_score:.2f}), используем CSV") - return 'process_csv_branch' - else: - print(f"Качество данных требует внимания (оценка: {quality_score:.2f}), используем JSON") - return 'process_json_branch' def process_csv_data(): - """Обработка CSV данных""" - print("Обработка CSV файла...") - # Здесь будет логика обработки CSV файла - return "CSV данные обработаны" + """Имитирует обработку CSV.""" + logging.info("Учебная CSV-ветка выполнена; файлы не читались") + return "csv" + def process_json_data(): - """Обработка JSON данных""" - print("Обработка JSON файла...") - # Здесь будет логика обработки JSON файла - return "JSON данные обработаны" + """Имитирует обработку JSON.""" + logging.info("Учебная JSON-ветка выполнена; файлы не читались") + return "json" -def merge_results(): - """Объединение результатов из разных веток""" - print("Объединение результатов из разных веток...") - return "Результаты объединены" -# Определение задач -start_task = DummyOperator( - task_id='start_task', - dag=dag -) +def merge_results(ti): + """Читает результат выполненной ветки текущего запуска.""" + results = ti.xcom_pull(task_ids=["process_csv_branch", "process_json_branch"]) + selected = [value for value in results if value is not None] + logging.info("Результат выбранной ветки: %s", selected) + return selected -check_quality_task = BranchPythonOperator( - task_id='check_data_quality', - python_callable=check_data_quality, - dag=dag -) -process_csv_task = PythonOperator( - task_id='process_csv_branch', - python_callable=process_csv_data, - dag=dag -) +with DAG( + "branching_dag", + start_date=datetime(2023, 1, 1), + schedule=None, + catchup=False, + default_args={"owner": "student", "retries": 0}, + tags=["educational", "branching"], +) as dag: + start_task = EmptyOperator(task_id="start_task") + choose_task = BranchPythonOperator( + task_id="choose_format", python_callable=choose_format, + ) + process_csv_task = PythonOperator( + task_id="process_csv_branch", python_callable=process_csv_data, + ) + process_json_task = PythonOperator( + task_id="process_json_branch", python_callable=process_json_data, + ) + merge_task = PythonOperator( + task_id="merge_results", + python_callable=merge_results, + trigger_rule="none_failed_min_one_success", + ) + end_task = EmptyOperator(task_id="end_task") -process_json_task = PythonOperator( - task_id='process_json_branch', - python_callable=process_json_data, - dag=dag -) - -merge_task = PythonOperator( - task_id='merge_results', - python_callable=merge_results, - trigger_rule='none_failed_or_skipped', # Выполняется, когда одна из веток завершена - dag=dag -) - -end_task = DummyOperator( - task_id='end_task', - dag=dag -) - -# Установка зависимостей -# BranchPythonOperator автоматически пропускает (skips) задачи в невыбранной ветке -# Например, если check_data_quality возвращает 'process_csv_branch', -# то задача process_json_branch будет пропущена (статус skipped) -start_task >> check_quality_task -check_quality_task >> [process_csv_task, process_json_task] -[process_csv_task, process_json_task] >> merge_task >> end_task \ No newline at end of file + start_task >> choose_task >> [process_csv_task, process_json_task] + [process_csv_task, process_json_task] >> merge_task >> end_task diff --git a/airflow-docker/dags/csv_to_postgres.py b/airflow-docker/dags/csv_to_postgres.py index 9aa14c2..76e8fe0 100644 --- a/airflow-docker/dags/csv_to_postgres.py +++ b/airflow-docker/dags/csv_to_postgres.py @@ -121,6 +121,8 @@ def _load_csv(csv_path: str) -> None: cur.execute( "CREATE TEMP TABLE tmp_orders (LIKE public.orders INCLUDING DEFAULTS) ON COMMIT DROP;" ) + # Порядок полей CSV должен совпадать со списком COPY. + # HEADER пропускает заголовок, а не сопоставляет имена колонок. cur.copy_expert( "COPY tmp_orders (order_id, order_ts, customer_id, amount) FROM STDIN WITH CSV HEADER", f, diff --git a/airflow-docker/dags/csv_to_postgres_dq.py b/airflow-docker/dags/csv_to_postgres_dq.py index 83822ed..30ee234 100644 --- a/airflow-docker/dags/csv_to_postgres_dq.py +++ b/airflow-docker/dags/csv_to_postgres_dq.py @@ -2,7 +2,7 @@ DAG для проверки качества данных (Data Quality) после загрузки в PostgreSQL Уровень: Средний -Этот DAG запускается после csv_to_postgres и проверяет таблицу public.orders: +Запустите этот DAG вручную после csv_to_postgres. Он проверяет public.orders: 1. Существование таблицы 2. Соответствие схемы (колонки и типы данных) 3. Наличие данных (таблица не пуста) @@ -14,12 +14,11 @@ DAG для проверки качества данных (Data Quality) пос from __future__ import annotations import logging -from datetime import datetime, timedelta - -from airflow.operators.python import PythonOperator -from airflow.providers.postgres.hooks.postgres import PostgresHook +from datetime import datetime from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.providers.postgres.hooks.postgres import PostgresHook POSTGRES_CONN_ID = "postgres_training" @@ -106,13 +105,20 @@ def _check_no_duplicates(): conn.close() -def _log_dq_summary(): - """Логирует итоговую сводку по качеству данных.""" - logging.info("Все проверки качества данных пройдены успешно!") - logging.info("Качество данных в таблице orders соответствует требованиям.") +def _log_dq_summary(dag_run, ti): + """Проверяет итог, в том числе при учебном переходе на all_done.""" + states = { + task.task_id: task.state + for task in dag_run.get_task_instances() + if task.task_id != ti.task_id + } + logging.info("Результаты проверок: %s", states) + if any(state != "success" for state in states.values()): + raise ValueError("Не все проверки качества данных прошли успешно") + logging.info("Все проверки качества данных пройдены") -default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(seconds=30)} +default_args = {"owner": "student", "retries": 0} with DAG( dag_id="csv_to_postgres_dq", diff --git a/airflow-docker/dags/data_processing_dag.py b/airflow-docker/dags/data_processing_dag.py index 4086e84..a2dbec8 100644 --- a/airflow-docker/dags/data_processing_dag.py +++ b/airflow-docker/dags/data_processing_dag.py @@ -6,7 +6,7 @@ DAG для демонстрации ETL процессов в Airflow 1. Создание тестовых данных (клиенты + заказы) в CSV 2. Параллельное извлечение (Extract) клиентов и заказов 3. Трансформация (Transform) — объединение таблиц, вычисляемые поля -4. Загрузка (Load) — подготовка SQL для вставки в БД +4. Имитация загрузки: число подготовленных строк и пример DDL в логах 5. Генерация текстового отчёта со статистикой Результат: файлы в /opt/airflow/data/output/ (extracted_*, transformed_data.csv, report.txt). @@ -14,7 +14,6 @@ DAG для демонстрации ETL процессов в Airflow from datetime import datetime, timedelta from airflow import DAG from airflow.operators.python import PythonOperator -from airflow.providers.postgres.operators.postgres import PostgresOperator # Определение DAG default_args = { @@ -85,8 +84,8 @@ def extract_orders(): def transform_data(): """Преобразование данных - объединение клиентов и заказов""" import pandas as pd - customers_df = pd.read_csv('/opt/airflow/data/input/customers.csv') - orders_df = pd.read_csv('/opt/airflow/data/input/orders.csv') + customers_df = pd.read_csv('/opt/airflow/data/output/extracted_customers.csv') + orders_df = pd.read_csv('/opt/airflow/data/output/extracted_orders.csv') # Объединяем данные merged_df = pd.merge(orders_df, customers_df, on='customer_id', how='left') @@ -101,14 +100,14 @@ def transform_data(): return f"Преобразованы {len(merged_df)} записей" -def load_to_database(): +def simulate_load(): """Загрузка данных в базу данных (симуляция)""" import pandas as pd df = pd.read_csv('/opt/airflow/data/output/transformed_data.csv') # В реальном сценарии здесь был бы код для загрузки в базу данных # Для учебных целей просто логируем - print(f"Загружено в базу данных: {len(df)} записей") + print(f"Имитация загрузки: подготовлено {len(df)} записей. Записи в БД нет.") # Создаем SQL для создания таблицы (в реальном сценарии) create_table_sql = """ @@ -185,8 +184,8 @@ transform_task = PythonOperator( ) load_task = PythonOperator( - task_id='load_to_database', - python_callable=load_to_database, + task_id='simulate_load', + python_callable=simulate_load, dag=dag ) @@ -197,4 +196,4 @@ report_task = PythonOperator( ) # Установка зависимостей -create_data_task >> [extract_customers_task, extract_orders_task] >> transform_task >> load_task >> report_task \ No newline at end of file +create_data_task >> [extract_customers_task, extract_orders_task] >> transform_task >> load_task >> report_task diff --git a/airflow-docker/dags/error_handling_dag.py b/airflow-docker/dags/error_handling_dag.py index 012f978..3fb7b2c 100644 --- a/airflow-docker/dags/error_handling_dag.py +++ b/airflow-docker/dags/error_handling_dag.py @@ -1,108 +1,89 @@ +"""Повторы и обработчики результата. Режим задается Variable error_mode. + +retry_once: вторая задача падает при первой попытке, затем выполняется. +success: обе задачи выполняются сразу. +fail_first / fail_second / fail_both: выбранные задачи падают при каждой попытке. +Меняйте режим после завершения предыдущего запуска. """ -DAG для демонстрации обработки ошибок в Airflow -Уровень: Продвинутый -""" +import logging from datetime import datetime, timedelta + from airflow import DAG +from airflow.models import Variable +from airflow.operators.empty import EmptyOperator from airflow.operators.python import PythonOperator -from airflow.operators.dummy import DummyOperator -from airflow.models.baseoperator import chain -import random -# Определение DAG -default_args = { - 'owner': 'student', - 'depends_on_past': False, - 'start_date': datetime(2023, 1, 1), - 'email_on_failure': False, # Отключаем email уведомления для простоты - 'email_on_retry': False, - 'retries': 3, # Количество попыток при ошибке - 'retry_delay': timedelta(seconds=10) # Задержка между попытками -} -dag = DAG( - 'error_handling_dag', - default_args=default_args, - description='DAG для изучения обработки ошибок в Airflow', - schedule_interval=None, +def get_error_mode(): + """Читает режим во время выполнения задачи.""" + mode = Variable.get("error_mode", default_var="retry_once") + if mode not in {"success", "retry_once", "fail_first", "fail_second", "fail_both"}: + raise ValueError(f"Неизвестный error_mode: {mode}") + return mode + + +def run_first_task(): + """Имитирует первую рабочую задачу.""" + if get_error_mode() in {"fail_first", "fail_both"}: + raise ValueError("Учебная ошибка первой задачи") + logging.info("Первая задача выполнена") + + +def run_retry_task(ti): + """Имитирует временную или постоянную ошибку второй задачи.""" + mode = get_error_mode() + logging.info("Режим: %s; попытка: %s", mode, ti.try_number) + if mode in {"fail_second", "fail_both"}: + raise ValueError("Постоянная учебная ошибка второй задачи") + if mode == "retry_once" and ti.try_number == 1: + raise ValueError("Временная учебная ошибка: следующая попытка выполнится") + logging.info("Вторая задача выполнена") + + +def handle_success(): + """Сообщает об успехе обеих рабочих задач.""" + logging.info("Обе рабочие задачи выполнены") + + +def handle_failure(): + """Сообщает об окончательном отказе хотя бы одной задачи.""" + logging.error("Рабочая задача исчерпала повторы. Откройте ее лог") + + +with DAG( + "error_handling_dag", + start_date=datetime(2023, 1, 1), + schedule=None, catchup=False, - tags=['educational', 'error_handling', 'advanced'] -) - -def unreliable_task(): - """Задача, которая может завершиться с ошибкой""" - # В реальном сценарии это может быть задача, зависящая от внешних факторов - # Для учебных целей случайным образом генерируем ошибку - if random.random() < 0.3: # 30% вероятность ошибки - print("Ошибка: задача не выполнена успешно!") - raise Exception("Случайная ошибка в задаче") - - print("Задача выполнена успешно!") - return "Задача выполнена" - -def success_handler(): - """Обработчик успешного выполнения""" - print("Поздравляем! Все задачи выполнены успешно!") - return "Успешно завершено" - -def failure_handler(): - """Обработчик ошибок""" - print("Одна или несколько задач завершились с ошибкой!") - print("Проверьте логи для получения дополнительной информации") - return "Ошибка обработана" - -def retry_task(): - """Задача с механизмом повторных попыток""" - # Имитируем задачу, которая может завершиться с ошибкой, но со временем исправляется - import time - time.sleep(2) # Имитация работы - - # С вероятностью 50% задача завершится с ошибкой - if random.random() < 0.5: - print("Ошибка в retry_task!") - raise Exception("Ошибка в задаче с повторными попытками") - - print("retry_task выполнена успешно!") - return "retry_task завершена" - -# Определение задач -start_task = DummyOperator( - task_id='start_task', - dag=dag -) - -unreliable_task = PythonOperator( - task_id='unreliable_task', - python_callable=unreliable_task, - dag=dag -) - -retry_task = PythonOperator( - task_id='retry_task', - python_callable=retry_task, - dag=dag -) - -success_handler_task = PythonOperator( - task_id='success_handler', - python_callable=success_handler, - trigger_rule='all_success', # Выполняется только если все предыдущие задачи успешны - dag=dag -) - -failure_handler_task = PythonOperator( - task_id='failure_handler', - python_callable=failure_handler, - trigger_rule='one_failed', # Выполняется если хотя бы одна предыдущая задача завершилась с ошибкой - dag=dag -) - -end_task = DummyOperator( - task_id='end_task', - dag=dag -) - -# Установка зависимостей -# Используем chain, чтобы наглядно показать ученикам построение ветвящихся зависимостей без ручного перечисления операторов. -chain(start_task, [unreliable_task, retry_task], [success_handler_task, failure_handler_task], end_task) + default_args={ + "owner": "student", + "retries": 1, + "retry_delay": timedelta(seconds=10), + }, + tags=["educational", "errors"], +) as dag: + start_task = EmptyOperator(task_id="start_task") + unreliable_task = PythonOperator( + task_id="unreliable_task", python_callable=run_first_task, + ) + retry_task = PythonOperator( + task_id="retry_task", python_callable=run_retry_task, + ) + success_handler_task = PythonOperator( + task_id="success_handler", + python_callable=handle_success, + trigger_rule="all_success", + retries=0, + ) + failure_handler_task = PythonOperator( + task_id="failure_handler", + python_callable=handle_failure, + trigger_rule="one_failed", + retries=0, + ) + start_task >> [unreliable_task, retry_task] + [unreliable_task, retry_task] >> success_handler_task + [unreliable_task, retry_task] >> failure_handler_task + # Оба обработчика завершающие. При отказе success_handler получает + # upstream_failed, поэтому успешное уведомление не скрывает ошибку DAG Run. diff --git a/airflow-docker/dags/resource_management_dag.py b/airflow-docker/dags/resource_management_dag.py new file mode 100644 index 0000000..7ad4fd7 --- /dev/null +++ b/airflow-docker/dags/resource_management_dag.py @@ -0,0 +1,61 @@ +"""Пулы и XCom на двух задачах с наблюдаемой длительностью. + +До запуска создайте пул training_pool с двумя слотами. +Чтение источников имитируется; подключений и входных файлов не требуется. +""" +import logging +import time +from datetime import datetime + +from airflow import DAG +from airflow.operators.python import PythonOperator + + +def read_source(source, rows): + """Имитирует чтение источника в течение 20 секунд.""" + logging.info("Начало чтения %s", source) + time.sleep(20) + logging.info("Чтение %s завершено: %s строк (имитация)", source, rows) + return rows + + +def calculate_metrics(ti): + """Получает небольшие результаты двух задач через XCom.""" + customers = ti.xcom_pull(task_ids="read_customers") + orders = ti.xcom_pull(task_ids="read_orders") + return {"customers": customers, "orders": orders} + + +def log_metrics(ti): + """Печатает словарь, возвращенный предыдущей задачей.""" + logging.info("Метрики: %s", ti.xcom_pull(task_ids="calculate_metrics")) + + +with DAG( + "resource_management_dag", + start_date=datetime(2023, 1, 1), + schedule=None, + catchup=False, + default_args={"owner": "student", "retries": 0}, + tags=["educational", "pools", "xcom"], +) as dag: + customers_task = PythonOperator( + task_id="read_customers", + python_callable=read_source, + op_kwargs={"source": "customers", "rows": 20}, + pool="training_pool", + pool_slots=2, + ) + orders_task = PythonOperator( + task_id="read_orders", + python_callable=read_source, + op_kwargs={"source": "orders", "rows": 30}, + pool="training_pool", + pool_slots=1, + ) + metrics_task = PythonOperator( + task_id="calculate_metrics", python_callable=calculate_metrics, + ) + log_task = PythonOperator(task_id="log_metrics", python_callable=log_metrics) + + [customers_task, orders_task] >> metrics_task >> log_task diff --git a/airflow-docker/dags/sql_basic_dag.py b/airflow-docker/dags/sql_basic_dag.py index fd7b655..4c1ff54 100644 --- a/airflow-docker/dags/sql_basic_dag.py +++ b/airflow-docker/dags/sql_basic_dag.py @@ -4,7 +4,6 @@ DAG для демонстрации работы с SQL в Airflow """ from datetime import datetime, timedelta from airflow import DAG -from airflow.operators.python import PythonOperator from airflow.providers.postgres.operators.postgres import PostgresOperator # Определение DAG @@ -76,11 +75,12 @@ query_data_task = PostgresOperator( ) truncate_table_task = PostgresOperator( - task_id='drop_table', + task_id='truncate_table', postgres_conn_id='postgres_training', sql=truncate_table_sql, dag=dag ) # Установка зависимостей -create_table_task >> insert_data_task >> query_data_task >> truncate_table_task +# Очистка перед вставкой: после запуска в таблице остаются три строки. +create_table_task >> truncate_table_task >> insert_data_task >> query_data_task diff --git a/airflow-docker/educational-tasks.md b/airflow-docker/educational-tasks.md deleted file mode 100644 index a40dbad..0000000 --- a/airflow-docker/educational-tasks.md +++ /dev/null @@ -1,478 +0,0 @@ -# Учебные задания по Apache Airflow - -Этот документ содержит практические задания для закрепления знаний по Apache Airflow. Каждое задание соответствует одному из учебных DAG'ов и направлено на лучшее понимание конкретных концепций. - -## Общие инструкции - -Перед выполнением заданий: -1. Убедитесь, что стенд Airflow запущен: `docker-compose up -d` -2. Проверьте доступность интерфейса: http://localhost:8080 -3. Ознакомьтесь с соответствующим DAG'ом в интерфейсе Airflow -4. Откройте исходный код DAG'а и прочитайте описание в начале файла - ---- - -## Задания для hello_world_dag.py - -**Цель:** Освоить базовые концепции Airflow — создание задач, зависимости, операторы. - -### Задание 1: Модификация существующих задач -**Сложность:** Начальная - -**Задача:** -- Откройте файл `hello_world_dag.py` -- Измените текст в функциях `print_hello()`, `print_date()`, `print_goodbye()` на русский язык -- Добавьте новую задачу, которая выводит текущую погоду (можно использовать фиктивные данные) - -**Подсказка:** -- Новая задача создаётся по аналогии с `date_task` — нужна Python-функция и `PythonOperator` для неё -- Не забудьте встроить задачу в цепочку зависимостей (`>>`) -- После сохранения файла DAG подхватится автоматически — запустите его вручную через UI и проверьте логи - -**Цель задания:** Понять структуру DAG, научиться добавлять и модифицировать задачи. - -### Задание 2: Изменение расписания -**Сложность:** Начальная - -**Задача:** -- Измените `schedule_interval` с ежедневного на еженедельное выполнение -- Добавьте параметр `max_active_runs` для ограничения одновременных запусков -- Проверьте изменения в интерфейсе Airflow - -**Подсказка:** -- Расписание задаётся в параметрах `DAG(...)` — ищите `schedule_interval` -- Можно использовать `timedelta(weeks=1)` или cron-выражение (например, `'0 0 * * 1'`) -- `max_active_runs` — тоже параметр `DAG()` - -**Цель задания:** Освоить настройку расписания и параметров выполнения DAG. - -### Задание 3: Добавление BashOperator -**Сложность:** Средняя - -**Задача:** -- Добавьте задачу с `BashOperator`, которая создает текстовый файл в папке `/opt/airflow/data/output/` -- Настройте зависимости так, чтобы эта задача выполнялась между `date_task` и `end_task` -- Убедитесь, что файл создается при каждом запуске DAG - -**Подсказка:** -- Импорт `BashOperator` уже есть в файле — посмотрите шапку -- `BashOperator` принимает параметр `bash_command` со строкой shell-команды -- Для записи в файл подойдёт обычный `echo "текст" > /path/to/file.txt` -- Проверить результат можно: `docker-compose exec airflow-webserver cat /opt/airflow/data/output/<ваш_файл>` - -**Цель задания:** Научиться работать с разными типами операторов. - ---- - -## Задания для sql_basic_dag.py - -**Цель:** Освоить работу с базами данных через Airflow. - -### Задание 1: Расширение структуры таблицы -**Сложность:** Средняя - -**Задача:** -- Добавьте в таблицу `students_sample` новые поля: `email`, `phone`, `registration_date` -- Модифицируйте SQL-запросы для работы с новой структурой -- Добавьте задачу для обновления существующих записей - -**Подсказка:** -- Новые колонки добавляются в переменную `create_table_sql` — подберите подходящие типы (VARCHAR, DATE и т.д.) -- Не забудьте обновить `insert_data_sql` — количество значений в INSERT должно совпадать с количеством колонок -- Для задачи UPDATE создайте новую SQL-переменную и новый `PostgresOperator` по аналогии с существующими -- **Подводный камень:** `TRUNCATE TABLE` не изменяет структуру. Чтобы новые колонки появились, замените его на `DROP TABLE IF EXISTS` - -**Цель задания:** Научиться работать с миграциями схемы базы данных. - -### Задание 2: Создание отчетов -**Сложность:** Средняя - -**Задача:** -- Добавьте задачу, которая создает сводный отчет по данным студентов -- Отчет должен содержать: количество студентов, средний возраст, распределение по возрасту -- Сохраните отчет в файл в папке `/opt/airflow/data/output/` - -**Подсказка:** -- Здесь понадобится `PythonOperator`, а не `PostgresOperator` — потому что нужно получить результат SQL и записать в файл -- Для подключения к БД из Python-функции используйте `PostgresHook` (импорт: `from airflow.providers.postgres.hooks.postgres import PostgresHook`) -- У хука есть метод `get_conn()`, который возвращает обычное psycopg2-соединение — дальше работайте через `cursor.execute()` и `fetchone()` -- Задачу поставьте перед `drop_table` в цепочке зависимостей - -**Цель задания:** Освоить создание аналитических отчетов в DAG'ах. - -### Задание 3: Работа с соединениями -**Сложность:** Средняя - -**Задача:** -- Создайте новое соединение к базе данных через Airflow UI -- Модифицируйте DAG для использования нового соединения -- Добавьте обработку ошибок подключения к БД - -**Подсказка:** -- Соединения создаются в UI: Admin → Connections → "+" -- Параметры для нашей учебной БД: Host = `postgres-training`, Schema = `training`, Login = `student`, Password = `student`, Port = `5432` -- В DAG-файле соединение указывается через `postgres_conn_id` — замените значение на ваш новый Conn Id -- Для проверки ошибок попробуйте указать несуществующее соединение и посмотрите, что покажут логи - -**Цель задания:** Научиться управлять соединениями с внешними системами. - ---- - -## Задания для file_operations_dag.py - -**Цель:** Освоить работу с файлами и данными в Airflow. - -### Задание 1: Модификация генерации данных -**Сложность:** Средняя - -**Задача:** -- Добавьте новые поля в генерируемые данные: `department`, `experience_years`, `education_level` -- Модифицируйте валидацию для проверки новых полей -- Добавьте фильтрацию данных по определенным критериям (например, опыт > 3 года) - -**Подсказка:** -- Генерация данных происходит в функции `generate_sample_data()` — новые поля добавляются в словарь `data` -- Для `department` подойдёт `random.choice()` со списком отделов, для `experience_years` — привяжите к возрасту (не может быть больше `age - 18`) -- Валидация — в функции `read_and_validate_data()`: добавьте `assert` по аналогии с существующими -- Фильтрацию (pandas `df[df['experience_years'] > 3]`) можно добавить в `transform_data()` и сохранить отдельным файлом - -**Цель задания:** Научиться работать с различными типами данных и валидацией. - -### Задание 2: Создание дополнительных отчетов -**Сложность:** Средняя - -**Задача:** -- Создайте отчет в формате JSON с детальной статистикой по отделам -- Добавьте визуализацию данных с помощью библиотеки matplotlib (сохранение графика в файл) -- Создайте сводку по зарплатам в разных возрастных категориях - -**Подсказка:** -- Создайте новую функцию и новый `PythonOperator`, добавьте в цепочку после `summary_task` -- Для JSON-отчёта: загрузите processed_data.csv через pandas, сгруппируйте (`groupby`) и сохраните через `json.dump()` -- matplotlib может быть не установлен в контейнере — это нормально, сделайте эту часть необязательной (обработайте `ImportError`) -- Не забудьте `import pandas as pd` внутри функции (не в шапке файла!) - -**Цель задания:** Освоить создание комплексных отчетов и визуализацию. - -### Задание 3: Оптимизация обработки -**Сложность:** Продвинутая - -**Задача:** -- Разделите обработку данных на параллельные задачи для разных отделов -- Добавьте контроль качества данных (проверка на дубликаты, аномалии) -- Реализуйте механизм повторной обработки при ошибках - -**Подсказка:** -- Параллельные задачи задаются списком: `read_task >> [task_a, task_b, task_c] >> summary_task` -- Каждая задача фильтрует DataFrame по своему отделу и сохраняет результат в отдельный файл -- Проверку дубликатов можно сделать через `df['id'].is_unique` -- Параметры `retries` и `retry_delay` можно задать как у отдельных задач, так и в `default_args` - -**Цель задания:** Научиться оптимизировать и делать обработку данных отказоустойчивой. - ---- - -## Задания для csv_to_postgres.py - -**Цель:** Освоить паттерны загрузки данных (ETL) из файлов в базу данных и применение XCom. - -### Задание 1: Расширение структуры данных -**Сложность:** Начальная - -**Задача:** -- Добавьте в генератор CSV новую колонку `status` (например, со случайными значениями 'NEW', 'PROCESSING', 'COMPLETED'). -- Обновите функцию `_create_table`, чтобы учесть новую колонку. -- Запустите DAG и проверьте, что данные успешно загрузились с новой колонкой. -- Убедитесь, что сгенерированный файл появился в каталоге `/opt/airflow/data/output/`. - -**Подсказка:** -- Новая колонка добавляется в трёх местах: генерация (`_generate_csv`), DDL (`_create_table`), загрузка (`_load_csv` — список колонок в COPY) -- В `_generate_csv()` для случайных значений подойдёт `random.choices(['NEW', 'PROCESSING', 'COMPLETED'], k=rows)` -- **Подводный камень:** если таблица уже существует со старой схемой, новая колонка не появится. Удалите таблицу вручную или добавьте `DROP TABLE` перед `CREATE TABLE` - -**Цель задания:** Понять процесс изменения схемы данных на всех этапах пайплайна. - -### Задание 2: Использование PostgresOperator -**Сложность:** Средняя - -**Задача:** -- Перепишите задачу `create_orders_table`. Сейчас она использует `PythonOperator` и `PostgresHook` внутри Python-функции. -- Замените её на использование стандартного `PostgresOperator`, используя соединение `postgres_training`. -- Убедитесь, что пайплайн продолжает работать корректно. - -**Подсказка:** -- Посмотрите, как устроен `sql_basic_dag.py` — там `PostgresOperator` уже используется, можно взять за образец -- `PostgresOperator` принимает `postgres_conn_id` и `sql` — SQL можно передать прямо строкой -- После замены проверьте, не осталось ли ссылок на удалённую функцию. `_get_conn()` всё ещё нужна в `_load_csv()` - -**Цель задания:** Научиться использовать специализированные операторы для работы с БД вместо кастомного Python-кода. - ---- - -## Задания для csv_to_postgres_dq.py - -**Цель:** Освоить подходы к обеспечению качества данных (Data Quality) в Airflow. - -### Задание 1: Новая проверка качества -**Сложность:** Средняя - -**Задача:** -- Добавьте новую функцию проверки прямо в `csv_to_postgres_dq.py`, которая будет убеждаться, что все значения в колонке `amount` строго больше нуля. -- Добавьте вызов этой функции как новую задачу в DAG `csv_to_postgres_dq`. -- Встройте новую задачу в общую цепочку выполнения (например, перед `data_quality_summary`). - -**Подсказка:** -- Возьмите за образец любую из существующих функций проверки (например, `_check_has_rows`) — паттерн одинаковый: подключиться, выполнить SQL, проверить результат, бросить `ValueError` если не ок -- SQL для проверки: `SELECT COUNT(*) FROM public.orders WHERE amount <= 0` -- Не забудьте создать `PythonOperator` и добавить задачу в цепочку - -**Цель задания:** Научиться расширять набор проверок качества данных. - -### Задание 2: Управление статусом при ошибках (Trigger Rules) -**Сложность:** Продвинутая - -**Задача:** -- Смоделируйте ошибку (например, временно измените данные так, чтобы проверки не прошли). -- По умолчанию, если падает одна проверка, следующие не выполняются (поведение `all_success`). -- Измените параметры задач так (с помощью `trigger_rule`), чтобы выполнялись *все* проверки, даже если некоторые из них упали. -- Сделайте так, чтобы задача `data_quality_summary` могла анализировать статусы предыдущих задач и отражать общий итог. - -**Подсказка:** -- Для моделирования ошибки: измените `EXPECTED_ORDERS_SCHEMA` так, чтобы схема не совпала. Запустите — увидите, что все задачи после упавшей будут skipped -- Ключевой параметр — `trigger_rule` у `PythonOperator`. Значение `"all_done"` означает: «запустись в любом случае, когда все upstream завершились (успешно или нет)» -- Для анализа статусов в `dq_summary`: функция может принимать `**context` и через `context['ti']` получить информацию о статусах предыдущих задач -- После экспериментов не забудьте вернуть `EXPECTED_ORDERS_SCHEMA` к правильным значениям - -**Цель задания:** Освоить продвинутую маршрутизацию статусов задач с помощью `trigger_rule`. - ---- - -## Задания для data_processing_dag.py - -**Цель:** Освоить ETL процессы и работу с бизнес-логикой. - -### Задание 1: Расширение ETL пайплайна -**Сложность:** Средняя - -**Задача:** -- Добавьте новый источник данных — файл с информацией о продуктах -- Создайте задачу для объединения данных о заказах с информацией о продуктах -- Добавьте расчет общей выручки по продуктам - -**Подсказка:** -- В `create_sample_data()` добавьте генерацию ещё одного CSV (products.csv) с колонками `product`, `category`, `weight_kg` — значения product должны совпадать с теми, что уже есть в orders -- Создайте функцию `extract_products()` по аналогии с `extract_customers()` -- Для объединения в `transform_data()` используйте `pd.merge()` по ключу `product` -- Новую extract-задачу поставьте параллельно с существующими: `create_data_task >> [extract_customers_task, extract_orders_task, extract_products_task]` - -**Цель задания:** Научиться работать с множественными источниками данных. - -### Задание 2: Расширение текстового отчёта -**Сложность:** Средняя - -**Задача:** -- Расширьте функцию `generate_report()` так, чтобы отчёт содержал больше полезной статистики -- Добавьте в отчёт: топ-3 самых дорогих заказа, распределение заказов по месяцам, среднюю сумму заказа по каждому клиенту -- Сохраните расширенный отчёт в `/opt/airflow/data/output/detailed_report.txt` - -**Подсказка:** -- Всё делается в существующей функции `generate_report()` — дополните её -- Полезные методы pandas: `df.nlargest()`, `df.groupby(...).mean()`, `df.groupby(...).sum()` -- Можно записать результат в тот же файл или создать отдельный - -**Цель задания:** Освоить создание аналитических отчётов с помощью pandas. - -### Задание 3: Мониторинг качества данных -**Сложность:** Продвинутая - -**Задача:** -- Добавьте проверки качества данных на каждом этапе ETL -- Создайте механизм оповещения о проблемах с данными -- Реализуйте архивирование обработанных данных - -**Подсказка:** -- Создайте отдельную функцию валидации (проверка: строки > 0, нет NULL в ключевых полях, суммы положительные) и поставьте её между extract и transform -- Для оповещения используйте `on_failure_callback` в `default_args` — это функция, которая вызывается при падении любой задачи. Она получает `context` с информацией об ошибке -- Для архивирования: скопируйте результат в файл с датой в имени (например, `shutil.copy()` + `datetime.now().strftime(...)`) - -**Цель задания:** Научиться обеспечивать качество данных в ETL процессах. - ---- - -## Задания для branching_dag.py - -**Цель:** Освоить условную логику и ветвление в Airflow. - -### Задание 1: Модификация условий ветвления -**Сложность:** Средняя - -**Задача:** -- Измените условие в функции `check_data_quality()` на основе реальных критериев (например, размер файла) -- Добавьте третью ветку обработки для данных "требующих ручной проверки" -- Настройте разные триггерные правила для слияния веток - -**Подсказка:** -- Сейчас функция выбирает ветку случайно — замените `random.random()` на проверку чего-то реального (например, `os.path.getsize()` для размера файла) -- Функция `BranchPythonOperator` возвращает `task_id` ветки для выполнения. Для третьей ветки — верните третий `task_id` -- У `merge_task` проверьте `trigger_rule` — при ветвлении непройденные ветки получают статус `skipped` - -**Цель задания:** Научиться создавать сложные условия ветвления. - -### Задание 2: Реализация реального сценария -**Сложность:** Продвинутая - -**Задача:** -- Создайте реальные задачи обработки для CSV и JSON форматов -- Добавьте валидацию данных в каждой ветке -- Реализуйте механизм сравнения результатов из разных веток - -**Подсказка:** -- Вместо `print("Обработка CSV...")` загрузите реальный файл (например, `sample_data.csv`) и посчитайте статистику -- Результат каждой ветки сохраните в отдельный файл — в `merge_results()` загрузите оба (если существуют) и сравните -- Учтите, что при ветвлении выполняется только одна ветка — функция слияния должна обрабатывать случай, когда один из файлов отсутствует - -**Цель задания:** Применить ветвление в реальном сценарии обработки данных. - -### Задание 3: Динамическое ветвление -**Сложность:** Продвинутая - -**Задача:** -- Реализуйте ветвление на основе внешних параметров (например, переданных через Variables) -- Добавьте обработку случая, когда ни одна ветка не подходит -- Создайте механизм логирования выбранного пути выполнения - -**Подсказка:** -- Variables создаются в UI: Admin → Variables. Для чтения в коде: `Variable.get('key', default_var='значение')` -- Для случая «ни одна ветка не подходит» добавьте `DummyOperator` как ветку-заглушку -- `BranchPythonOperator` должен всегда возвращать существующий `task_id` — иначе DAG упадёт с ошибкой - -**Цель задания:** Освоить динамическое принятие решений в DAG'ах. - ---- - -## Задания для error_handling_dag.py - -**Цель:** Освоить обработку ошибок и создание отказоустойчивых пайплайнов. - -### Задание 1: Настройка стратегий повторения -**Сложность:** Средняя - -**Задача:** -- Измените параметры `retries` и `retry_delay` для разных задач -- Добавьте экспоненциальную задержку между повторными попытками -- Реализуйте кастомный обработчик ошибок для конкретных исключений - -**Подсказка:** -- Параметры повторения можно задать на уровне задачи (перекроют `default_args`): `retries`, `retry_delay`, `retry_exponential_backoff`, `max_retry_delay` -- Для кастомного обработчика: параметр `on_retry_callback` принимает функцию с аргументом `context`, из которого можно получить номер попытки через `context['ti'].try_number` -- Запустите DAG несколько раз и посмотрите в логах, как меняется задержка между попытками - -**Цель задания:** Научиться настраивать стратегии обработки ошибок. - -### Задание 2: Создание комплексной обработки ошибок -**Сложность:** Продвинутая - -**Задача:** -- Добавьте задачи для разных типов ошибок (сетевая ошибка, ошибка данных, системная ошибка) -- Создайте механизм эскалации ошибок (после N неудачных попыток) -- Реализуйте отправку уведомлений о критических ошибках - -**Подсказка:** -- Создайте функции по аналогии с `unreliable_task()`, но бросающие разные типы исключений: `ConnectionError`, `ValueError`, `OSError` -- Для эскалации используйте `on_failure_callback` — внутри проверьте `context['ti'].try_number` и при достижении порога выведите сообщение уровня CRITICAL -- Пример структуры callback: - ```python - def escalation_callback(context): - if context['ti'].try_number >= 3: - # сюда — логику эскалации - ``` - -**Цель задания:** Освоить создание комплексной системы обработки ошибок. - -### Задание 3: Добавление логирования через модуль logging -**Сложность:** Средняя - -**Задача:** -- Замените все `print()` в функциях DAG'а на вызовы стандартного модуля `logging` -- Добавьте в каждую функцию логирование начала и окончания выполнения -- Посмотрите, как логи отображаются в интерфейсе Airflow (вкладка Log у каждой задачи) - -**Подсказка:** -- Стандартный паттерн: `import logging` в начале файла, затем `log = logging.getLogger(__name__)` в функции -- Уровни: `log.info()` для штатных событий, `log.warning()` для предупреждений, `log.error()` для ошибок -- В UI Airflow откройте выполненную задачу → вкладка **Log** — сообщения `logging` отображаются с метками уровня и временем, в отличие от `print()` - -**Цель задания:** Научиться использовать стандартное логирование Python в задачах Airflow и читать логи через UI. - ---- - -## Дополнительные задания: пулы, XCom, TaskGroup и алертинг - -**Цель:** Освоить продвинутые возможности оркестрации — управление ресурсами, обмен данными между задачами, группировку и оповещения. - -### Задание 1: Пулы и управление ресурсами -**Сложность:** Средняя - -**Задача:** -- В интерфейсе Airflow создайте пул `backup_pool` с 2 слотами -- Создайте новый DAG `advanced_features_dag.py` **или** расширьте `data_processing_dag.py` задачами резервного копирования (например, `backup_to_csv`, `backup_to_db`) -- Назначьте этим задачам параметр `pool="backup_pool"` и настройте `pool_slots` так, чтобы одна из задач занимала 2 слота, а другая — 1 -- Наблюдайте в UI, что одновременно запускается не более 2 задач из этого пула - -**Подсказка:** -- Пул создаётся в UI: Admin → Pools → "+" -- У `PythonOperator` есть параметры `pool` (имя пула) и `pool_slots` (сколько слотов занимает задача) -- Если задача занимает 2 слота из 2 доступных — вторая задача будет ждать, даже если они не связаны зависимостями - -**Цель задания:** Научиться управлять параллелизмом задач через пулы и `pool_slots`. - -### Задание 2: Обмен данными через XCom -**Сложность:** Средняя - -**Задача:** -- В том же DAG добавьте задачу `calculate_metrics` (PythonOperator), которая возвращает словарь с агрегированными показателями, например: `{"total_orders": ..., "avg_amount": ...}` -- Добавьте задачу `log_metrics`, которая с помощью `xcom_pull` читает результат `calculate_metrics` и выводит значения в лог -- Для одной из задач продемонстрируйте использование XCom в Jinja-шаблоне (например, в `bash_command` или SQL-запросе) - -**Подсказка:** -- Любое значение, которое функция возвращает через `return`, автоматически сохраняется в XCom -- Для чтения XCom в другой задаче: функция принимает `**context`, затем `context['ti'].xcom_pull(task_ids='имя_задачи')` -- В Jinja-шаблонах (параметры `bash_command`, `sql` и др.) XCom доступен через `{{ ti.xcom_pull(task_ids='...') }}` -- Посмотреть сохранённые XCom-значения можно в UI: откройте задачу → вкладка **XCom** - -**Цель задания:** Освоить передачу результатов между задачами через XCom и их использование в шаблонах. - -### Задание 3: TaskGroup и алертинг -**Сложность:** Продвинутая - -**Задача:** -- Объедините логически связанные задачи (например, `extract` / `transform` / `load`) в `TaskGroup`'ы -- Добавьте завершающую задачу `send_notification` на основе примера из раздела про алертинг (PythonOperator с `send_email_smtp` или другим механизмом уведомлений) -- Настройте для задачи уведомления `trigger_rule=TriggerRule.ALL_DONE`, чтобы уведомление отправлялось даже при частичных ошибках -- При желании вынесите функцию отправки письма в отдельный модуль `utils.py` и импортируйте её в DAG - -**Подсказка:** -- TaskGroup — контекстный менеджер: `with TaskGroup("имя") as group:` — внутри определяете задачи как обычно -- Зависимости работают на уровне групп: `group_a >> group_b` -- `TriggerRule.ALL_DONE` (из `airflow.utils.trigger_rule`) означает: «запустись, когда все upstream завершились — неважно, с успехом или ошибкой» -- В Graph View группы отображаются как складные блоки — удобно для больших DAG'ов - -**Цель задания:** Научиться группировать задачи с помощью TaskGroup и строить схему оповещений о статусе пайплайна. - ---- - -## Рекомендации по выполнению - -1. **Начинайте с простых заданий** и постепенно переходите к сложным -2. **Тестируйте каждое изменение** через интерфейс Airflow -3. **Изучайте логи выполнения** для понимания поведения задач (вкладка Log у каждой задачи в UI) -4. **Экспериментируйте** с разными настройками и параметрами -5. **Не бойтесь ломать** — DAG'и можно откатить через `git checkout`, а таблицы пересоздать - -## Оценка прогресса - -- **Начальный уровень:** Выполнены задания для hello_world_dag и sql_basic_dag -- **Средний уровень:** Выполнены задания для file_operations_dag, csv_to_postgres.py, csv_to_postgres_dq.py и data_processing_dag -- **Продвинутый уровень:** Выполнены все задания, включая branching_dag, error_handling_dag и дополнительные задания по пулам, XCom, TaskGroup и алертингу - -Удачи в изучении Apache Airflow! diff --git a/docs/specs/2026-09-28-practice-improvement-plan.md b/docs/specs/2026-09-28-practice-improvement-plan.md new file mode 100644 index 0000000..9127f9a --- /dev/null +++ b/docs/specs/2026-09-28-practice-improvement-plan.md @@ -0,0 +1,153 @@ +# План обновления практики по Airflow + +Дата: 2026-09-28. Подход согласован с автором курса. Реализация завершена. Результаты и границы проверки сохранены в [протоколе](../validation/2026-09-28-practice.md). + +Этот документ сохраняет решения, порядок доработки и результаты проверки. Текущие задания находятся в [educational-tasks.md](../../educational-tasks.md), запуск стенда описан в [README](../../airflow-docker/README.md). + +## Цель и границы + +Стенд знакомит студентов с Apache Airflow и входит в [программу Data Engineering](https://de.dementev.space/). Перед ним студенты проходят Python, pandas, SQL и Docker. Самостоятельное построение цельных процессов обработки данных относится к последующим курсовым работам. + +Результат этого блока: студент умеет запустить готовый DAG, найти его граф и логи, внести небольшое изменение по образцу и объяснить наблюдаемое поведение. Практика остается набором небольших экспериментов с инструментом. + +Обновление сохраняет ознакомительный формат и версию Airflow 2.9.x. Основной маршрут выполняется на существующем стенде; внешние интеграции относятся к заданиям по желанию. Учебная имитация отдельных операций допустима, если описание и логи честно называют ее имитацией. + +## Порядок работы + +Перед продолжением сверить чек-лист с текущими файлами и историей Git. Прочитать согласованные решения ниже, затем выполнить первый незавершенный этап. Локальные решения в этих границах принимать и записывать в рабочей таблице; противоречия с согласованной целью выносить на обсуждение. + +- [x] Зафиксировать подход и находки в этом документе; добавить ссылки из README стенда и AGENTS.md. +- [x] **1. Разобрать все исходные задания.** Создать здесь рабочую таблицу с колонками: исходное задание (DAG/раздел и номер), изучаемая возможность, исходное состояние и зависимости, решение с обоснованием, место в будущем маршруте, наблюдаемый результат. Решения: оставить основным, разделить, перенести в дополнительные или переформулировать. Исходный набор - 25 заданий в `educational-tasks.md` на коммите `13e3102`; после переработки число упражнений может измениться. **Готово:** учтены 25 из 25 исходных заданий, у каждой части разделенного задания определена обязательность, а все зависимости основного маршрута обеспечены без выполнения дополнительных упражнений. Сопоставление с главами учебника объясняет выбор тем; новые темы добавляются только при выявленной необходимости для цели блока. +- [x] **2. Проверить и исправить несогласованности.** Сверить каждую строку раздела "Найденные несогласованности и препятствия" с актуальным кодом. Для каждой записать итог: исправлено, не подтвердилось с объяснением или оставлено как явно описанный учебный сценарий. **Готово:** все находки разобраны; подтвержденные ошибки устранены, измененное поведение проверено на версии стенда. Невыполненная проверка остается открытой. +- [x] **3. Переработать показательный блок обработки ошибок.** Подготовить основной управляемый эксперимент с повтором, наблюдаемые результаты и отдельное усложнение по желанию. **Готово:** исходное состояние и возврат к нему описаны; воспроизведены успех после повтора и окончательная ошибка; логи, состояния задач и итог DAG Run совпадают с описанием. Этот блок служит образцом объема и оформления остальных упражнений. +- [x] **4. Переработать остальные блоки.** Применить выбранное распределение и образец к остальным заданиям. **Готово:** каждая строка рабочей таблицы ссылается на итоговое упражнение или содержит причину объединения; у каждого упражнения есть обязательность и проверяемый результат; подготовка и восстановление общих данных описаны там, где нужны. +- [x] **5. Согласовать навигацию.** Обновить маршрут и оценку прогресса в `educational-tasks.md`, корневом README и README стенда. Сверить `dag-specifications.md` с итоговым содержанием. **Готово:** ссылки ведут к актуальным упражнениям; документы согласованно описывают порядок и условия завершения; устаревшие прототипы и необоснованные оценки времени удалены или исправлены. +- [x] **6. Пройти маршрут двумя способами.** Выполнить только основную практику, затем дополнительные упражнения с переходами обратно к основной. **Готово:** обе последовательности пройдены в изолированном окружении; указанные результаты подтверждены в данных и UI; сохранены условия запусков, результаты и ограничения по правилам раздела "Как проверять результат доработки". + +По мере выполнения обновлять рабочую таблицу и чек-лист. У закрытого этапа указывать ссылку на коммит или запись о проверке. Если часть проверок недоступна, записать причину и продолжать независимую работу; соответствующий этап остается незавершенным. + +## Текущее состояние реализации + +Исправления DAG сохранены в `cf1b770`, задания и навигация в `d8dfe83`, уточнения после прохождения в `1277bff`. В `f6b450c` задания перенесены в корень репозитория. + +Все 25 исходных заданий сопоставлены с 34 упражнениями: 16 основных и 18 по желанию. Основной маршрут пройден без дополнительных упражнений. Затем проверены дополнительные расширения и возвраты к базовой схеме, режимам и настройкам. Сохранены [условия и результаты проверки](../validation/2026-09-28-practice.md), [состояния 70 контрольных и диагностических запусков](../validation/2026-09-28-practice-runs.jsonl) и снимки UI. + +Открытых этапов реализации нет. При следующей правке сверять затронутые задания с кодом и протоколом. Проверка не измеряет трудность на студентах; SMTP проверен локальным приемником, без внешнего почтового провайдера. + +После независимого педагогического ревью исправлены примеры в главах 2, 7 и 9 и уточнены ссылки на теорию перед практикой. Значимость находок, принятые решения и исполнение примеров записаны в [перепроверке ревью](../validation/2026-09-28-theory-review.md). Предложение расширить основную практику опытом с Clear оставлено до проверки на студентах; состав упражнений сохранен. + +## Согласованные решения + +1. **Последовательность внутри блока.** Студент запускает исходный пример, меняет одну существенную настройку или часть графа и наблюдает результат. Изменения основных заданий внутри блока сохраняются. +2. **Ясное начало каждого блока.** Указаны исходный DAG, необходимые файлы, таблицы и настройки, а также изменения, которые остаются от предыдущей работы. Блок определяется учебным смыслом: загрузка CSV и последующая DQ-проверка могут объединять два DAG. +3. **Основная практика и задания по желанию.** Обязательность отделена от сложности. Дополнительные задания расположены в конце своего блока; их результаты не нужны для продолжения основной практики. Пропуск дополнительного задания не препятствует завершению блока. +4. **Разделение смешанных заданий.** Если одно упражнение объединяет знакомство с возможностью Airflow и значительное усложнение, оно разделяется. Например, TaskGroup и настройка настоящей отправки писем не должны быть одним обязательным заданием. +5. **Наблюдаемый результат.** У каждого задания есть раздел "Что должно получиться": конкретное поведение в UI, сообщение в логах или результат в файле/таблице. Преднамеренная ошибка может быть правильным результатом упражнения. +6. **Управляемые учебные сценарии.** Основные примеры ветвления и повторных попыток позволяют гарантированно увидеть нужный случай. Случайность допустима в данных, если она не мешает наблюдать изучаемое поведение и проверять результат. +7. **Понятное завершение.** Основная практика дает достаточное знакомство для перехода дальше по программе. Выполнение всех дополнительных упражнений не является скрытым условием прохождения курса. + +Дополнительное задание, меняющее общую таблицу или настройку, должно описывать возврат к состоянию, необходимому следующему блоку. Перенос задания в конец раздела сам по себе эту проблему не решает. Восстановление касается только объектов упражнения, без общего сброса стенда или чужих изменений. + +## Как оформлять задания + +Задания находятся в корне репозитория рядом с главами учебника: `educational-tasks.md`. В `airflow-docker/` остаются инструкция запуска, стенд и примеры DAG. Основные и дополнительные упражнения сохраняются в одном документе. + +В начале блока: связь с главами учебника, необходимые исходные условия и короткий запуск готового примера. У задания: + +- пометка "Основная практика" или "По желанию"; сложность указывается отдельно; +- конкретное изменение, позволяющее познакомиться с одной возможностью Airflow; +- подсказка по необходимости; +- "Что должно получиться" с указанием места проверки; +- для экспериментов с ошибками - способ вызвать ошибку и вернуть нормальное поведение; +- при необходимости один короткий вопрос о причине наблюдаемого результата. + +Для ветвления и обработки ошибок проверяются статусы отдельных задач и всего DAG Run. Формулировка "всё зеленое" не подходит для сценария с ожидаемыми пропусками или ошибками. Проверка результата не должна требовать ожидания недельного расписания или неограниченного числа случайных запусков. + +## Распределение содержания + +Распределение уточнено после разбора 25 исходных заданий. Номера итоговых упражнений указаны в рабочей таблице ниже; правила прохождения находятся в разделе "Согласованные решения". + +| Блок | Основная практика | Возможное усложнение по желанию | +|---|---|---| +| Первый DAG | Запуск, граф, логи, добавление задачи по образцу, зависимости, знакомство с расписанием | Дополнительные эксперименты с ограничениями запусков | +| SQL и Connections | Выполнение SQL, смена подключения, наблюдение результата и ошибки подключения | Дополнительные отчеты и расширение модели данных | +| Файлы и преобразования | Проследить путь файла между задачами, изменить простое преобразование или проверку | Новые поля, отчеты, обработка по отделам | +| CSV → PostgreSQL → DQ | Проследить загрузку, найти путь к файлу в XCom, добавить проверку, увидеть корректные и ошибочные данные | Расширение схемы с согласованным изменением загрузчика и DQ, общая сводка проверок | +| Пример ETL | Прочитать граф с параллельными задачами, найти промежуточные и итоговые результаты | Новый источник, расширенная аналитика, архивирование | +| Ветвление и Variables | Управляемо выбрать ветку, увидеть `skipped`, понять слияние веток | Третья ветка и полноценная обработка разных форматов | +| Ошибки и повторы | Вызвать ошибку, увидеть повтор и успешное завершение, найти причину в логах | Экспоненциальная задержка, разные исключения, эскалация | +| Пулы, XCom и TaskGroup | Ожидание слотов, новая метрика XCom, группировка существующих задач | XCom в Bash-шаблоне; настоящая отправка писем на примере с ошибками | + +Чтение и изменение XCom не должно зависеть от выполнения дополнительного задания по резервному копированию. Для знакомства с пулами достаточно готовых задач с наблюдаемой длительностью; написание механизма резервного копирования не является целью такого эксперимента. + +## Разбор 25 исходных заданий + +Номера слева относятся к версии `13e3102`, номера справа - к новой редакции `educational-tasks.md`. Состав основной практики покрывает темы глав 4-9; главы 1-3 дают контекст перед запуском. Обработка по интервалам и самостоятельный проект остаются следующей ступенью программы. + +| Исходное задание | Возможность | Вход и зависимости | Решение и причина | Новое место | Наблюдаемый результат | +|---|---|---|---|---|---| +| hello_world / 1 | PythonOperator, зависимости | Исходный DAG | Основное: небольшое изменение графа | [1.1](../../educational-tasks.md#first-dag) | Новая задача и ее сообщение видны в UI | +| hello_world / 2 | Расписание, ограничения запусков | После 1.1 | Разделить: расписание основное, конкуренция по желанию | [1.3, 1.A](../../educational-tasks.md#first-dag) | Видно расписание; второй запуск ожидает при лимите 1 | +| hello_world / 3 | BashOperator | После 1.1 | Основное: другой оператор и файл | [1.2](../../educational-tasks.md#first-dag) | Файл содержит ожидаемый текст | +| sql_basic / 1 | SQL в задаче, схема таблицы | Исходный SQL-DAG и training | Разделить: UPDATE основное, схема по желанию | [2.2, 2.A](../../educational-tasks.md#sql) | Возраст изменен; дополнительная колонка заполнена | +| sql_basic / 2 | PostgresHook и отчет | После 2.2, таблица с тремя строками | По желанию: дополнительная обработка | [2.B](../../educational-tasks.md#sql) | Файл: 3 студента, средний возраст 23.33 после UPDATE | +| sql_basic / 3 | Connections и диагностика | Учебная БД, собственный Conn Id | Основное; вместо неопределенного обработчика - найти и исправить ошибку подключения | [2.1](../../educational-tasks.md#sql) | Ошибка видна в логах, после исправления запуск успешен | +| file_operations / 1 | Генерация, валидация | Исходный файловый DAG | Разделить: простая трансформация основная, новые поля по желанию | [3.1, 3.A](../../educational-tasks.md#files) | 100 обработанных строк; поля и фильтр проверены отдельно | +| file_operations / 2 | Отчет по отделам | Требует 3.A | По желанию; график исключен, поскольку требует отдельной зависимости и не знакомит с новой возможностью Airflow | [3.B](../../educational-tasks.md#files) | Сумма численности отделов равна числу строк входа | +| file_operations / 3 | Параллельность, DQ, retries | Требует 3.A | По желанию; параллельность также видна в основном ETL-примере | [3.C](../../educational-tasks.md#files) | Раздельные файлы покрывают все отделы, сборка читает их | +| csv_to_postgres / 1 | Проведение поля через загрузку | После основной загрузки и DQ | По желанию: согласовать все четыре SQL/CSV-участка и проверку схемы | [4.A](../../educational-tasks.md#load-dq) | status заполнен; DQ проходит; исходная схема восстановлена | +| csv_to_postgres / 2 | Специализированный оператор | Базовая схема orders | По желанию: сравнение двух реализаций уже знакомого действия | [4.B](../../educational-tasks.md#load-dq) | После замены оператора загрузка и DQ успешны | +| csv_to_postgres_dq / 1 | Новая проверка, ошибка данных | Основная загрузка orders | Основное: положительный и отрицательный сценарии | [4.2](../../educational-tasks.md#load-dq) | Отрицательная сумма вызывает failed; восстановление возвращает success | +| csv_to_postgres_dq / 2 | Trigger Rules и итог DAG Run | После 4.2 | По желанию: запуск всех проверок и общая сводка | [4.C](../../educational-tasks.md#load-dq) | Все проверки завершаются, итог сохраняет ошибку | +| data_processing / 1 | Несколько источников | Исходный ETL, свои CSV | По желанию; основной разбор графа добавлен перед расширениями | [5.1, 5.A](../../educational-tasks.md#etl) | После JOIN остаются 6 заказов и выручка 2105 | +| data_processing / 2 | Отчет pandas | После основного запуска, products не нужен | По желанию | [5.B](../../educational-tasks.md#etl) | Топ заказов и средние сверяются с шестью исходными строками | +| data_processing / 3 | Валидация, callback, архив | После 5.1; поля продуктов необязательны | По желанию с конкретной ошибкой и восстановлением | [5.C](../../educational-tasks.md#etl) | При ошибке архив не создается; после исправления совпадает с результатом | +| branching / 1 | Выбор ветки и слияние | Исходный DAG, Variable branch_format | Разделить: две управляемые ветки основные, третья по желанию | [6.1, 6.A](../../educational-tasks.md#branching) | Выбранная ветка success, другая skipped, слияние success | +| branching / 2 | Работа выбранной ветки с данными | Два явно заданных входных файла | По желанию; использовать XCom текущего запуска вместо старых файлов другой ветки | [6.B](../../educational-tasks.md#branching) | Оба формата дают одинаковые агрегаты в отдельных запусках | +| branching / 3 | Variables, неизвестный параметр | После 6.1 | Variable включена в основное; fallback объединен с третьей веткой | [6.1, 6.A](../../educational-tasks.md#branching) | Неизвестное значение дает понятную ошибку либо review-ветку после расширения | +| error_handling / 1 | Повторы и задержка | Исходный DAG, Variable error_mode | Разделить: один управляемый повтор основной, backoff по желанию | [7.1, 7.2, 7.A](../../educational-tasks.md#errors) | Две попытки; с нулем повторов - failed | +| error_handling / 2 | Типы ошибок и оповещение | После 7.2 | По желанию; локальный callback без внешней почты | [7.B](../../educational-tasks.md#errors) | После исчерпания повторов есть сообщение с task_id и причиной | +| error_handling / 3 | logging | После 7.1 | Основное небольшое дополнение вместо механической замены всех print | [7.3](../../educational-tasks.md#errors) | Собственные сообщения видны в логах | +| Дополнительные / 1 | Пулы и слоты | Готовый resource_management_dag, пул из 2 слотов | Основной короткий эксперимент; резервное копирование заменено имитацией чтения | [8.1](../../educational-tasks.md#resources) | Задачи с весами 2 и 1 выполняются последовательно | +| Дополнительные / 2 | XCom и Jinja | После 8.1 | Основная метрика; шаблон Bash по желанию | [8.2, 8.A](../../educational-tasks.md#resources) | total_rows = 50, шаблон выводит то же значение | +| Дополнительные / 3 | TaskGroup и алертинг | После 8.2; для почты нужен SMTP | Разделить: группировка основная, внешняя доставка по желанию | [8.3, 8.B](../../educational-tasks.md#resources) | Группа раскрывается; XCom работает с полными task_id | + +Основной маршрут сохраняет изменения внутри блоков. Общие данные есть только в паре загрузка/DQ; изменение схемы вынесено после основных опытов, с обратным ALTER и возвратом списков колонок. Задания 3.B и 3.C явно зависят от дополнительного 3.A, но дальнейшая основная практика от этой ветки не зависит. Variables возвращаются к исходным значениям перед переходом дальше. + +## Найденные несогласованности и препятствия + +Первичные находки подтвердились при чтении кода и выполнении. Ниже их итоговое состояние; подробные результаты приведены в [протоколе](../validation/2026-09-28-practice.md). + +| Место | Исходная проблема | Итог | +|---|---|---| +| SQL | DROP после вставки не готовил существующую таблицу к новой колонке; результат очищался до проверки. | Исправлено: TRUNCATE перед вставкой, task_id `truncate_table`, три строки остаются в БД. В 2.A ALTER выполняется до запуска. Проверены UPDATE, email и отчет. | +| Загрузчик CSV | Подсказка про status пропускала INSERT ... SELECT. | Исправлено: перечислены четыре участка загрузчика и порядок полей CSV/COPY. Проверены значения статусов в БД, а не только наличие колонки. | +| Схема DQ | Дополнительная колонка конфликтовала с ожидаемыми четырьмя. | Исправлено: расширение идет после основного DQ; описаны обновление ожидания и возврат схемы. Оба состояния проверены. | +| Trigger Rules DQ | После отказа обещан skipped. | Исправлено на upstream_failed; подтверждены отказ проверки суммы и отказ схемы. | +| Обработчики ошибок | chain связывал списки попарно. | Исправлено: оба обработчика имеют обе рабочие задачи в непосредственных предшественниках. Проверены успех, повтор, отказ первой, второй и обеих задач, общий итог DAG Run. | +| Сводка DQ и алертинг | Единственная успешная завершающая задача с all_done могла скрыть ошибку; письмо безусловно сообщало об успехе. | Исправлено: сводка сверяет состояния и падает при неуспешной проверке. Уведомления назначены callbacks задачи и DAG. Проверены all_done, письма об отказе/успехе и сохранение результата при ошибке SMTP. | +| ETL | Transform обходил результаты Extract, имитация называлась загрузкой в БД. | Исправлен путь данных. Имитация сохранена и явно обозначена именем simulate_load и логом. Фильтрация Extract изменила отчет с 6/2105 на 4/2000. | +| Пулы | Формулировка "не более двух задач" не показывала ограничение по весам. | Добавлен готовый пример: в пуле из двух слотов веса 2+1 выполняются последовательно, 1+1 пересекаются по времени. Проверены оба варианта. | +| Ветвление, повторы и зависимости | Случайность мешала наблюдать нужный случай; использовались неопределенные входы и результаты старых запусков. | Исправлено: Variables управляют режимами; входы CSV/JSON заданы; слияние читает XCom текущего запуска; зависимости дополнительных упражнений указаны явно. | +| Логи callbacks | Первоначальная инструкция отправляла все сообщения в docker compose logs scheduler. | Уточнено после выполнения: callback оператора виден в последней попытке задачи, callback DAG в файле scheduler. Оба места проверены. | + +Технические источники: [chain](https://airflow.apache.org/docs/apache-airflow/2.9.2/_api/airflow/models/baseoperator/index.html#airflow.models.baseoperator.chain), [статусы задач](https://airflow.apache.org/docs/apache-airflow/2.9.2/core-concepts/tasks.html#task-instances), [итог DAG Run](https://airflow.apache.org/docs/apache-airflow/2.9.2/core-concepts/dag-run.html#dag-run-status), [пулы](https://airflow.apache.org/docs/apache-airflow/2.9.2/administration-and-deployment/pools.html), [COPY в PostgreSQL 16](https://www.postgresql.org/docs/16/sql-copy.html). Условия поиска логов уточнены по фактическому стенду 2.9.2. + +## Как проверять результат доработки + +Результаты проверок сохранять в этом документе рядом с закрываемым этапом: проверенная версия/коммит, сценарий и исходное состояние, ожидаемый и фактический результат, способ проверки. Для длинных логов или снимков экрана достаточно ссылки на доступный артефакт. Версию Airflow сверять с фактическим окружением, а не только с тегом образа в Dockerfile. + +Проверки разделяются по уровню доказательств: + +- **Документы и исходники:** все 25 исходных заданий учтены, ссылки работают, имена файлов/задач/таблиц совпадают, обязательность и зависимости согласованы. +- **Стенд:** подтверждены версия Airflow и загрузка DAG без ошибок импорта; выполнены затронутые сценарии и проверены файлы/таблицы. Проверка синтаксиса Python не заменяет выполнение задач. +- **UI:** студент действительно может найти указанные состояния, логи, XCom, граф и настройки; показанные результаты совпадают с текстом. +- **Прохождение:** отдельно проверены основной маршрут и переходы после дополнительных заданий. Не выполненные проверки так и отмечаются; предполагаемое время не выдается за измеренное на студентах. + +Эксперименты с изменением данных выполняются в изолированном учебном окружении. Проверка не должна затрагивать другие работающие стенды или несвязанные изменения пользователя. + +## Почему выбран этот формат + +Одни метки сложности не разделяют смешанные упражнения и не объясняют условия продолжения. Полностью независимые задания увеличивают повторную подготовку. Отдельный сборник усложнений разрывает связь с исходным примером. Демонстрации без изменения кода дают меньше практики, а самостоятельный большой пайплайн дублирует цель последующих курсовых. + +Выбранный формат сохраняет практику изменения кода и позволяет переходить к следующему блоку после знакомства с нужными возможностями Airflow. diff --git a/docs/validation/2026-09-28-practice-runs.jsonl b/docs/validation/2026-09-28-practice-runs.jsonl new file mode 100644 index 0000000..dc4cfbb --- /dev/null +++ b/docs/validation/2026-09-28-practice-runs.jsonl @@ -0,0 +1,71 @@ +{"type": "environment", "date": "2026-09-28", "airflow": "2.9.2", "python": "3.12.4", "executor": "LocalExecutor", "required_scenarios": 69, "recorded_runs": 70, "final_source_sha256": {"branching_dag.py": "873f00e26ede268582245aac0ec3b82f858a3654fb7a4e1e08eb6e68ec5d031a", "csv_to_postgres.py": "43452b5f639365be07174e417fa981116b478e90acaeffb184e530d9ae67cb3b", "csv_to_postgres_dq.py": "05e43bc4bdab64813d7a853fee9285c11cb0e82a077c33a641219ca47b4604af", "data_processing_dag.py": "df39a91f89ff2e572d0a101b52171b9af97fb4abc7a66fc90c136425acb64b89", "error_handling_dag.py": "914e00b5845afa82c08aa540501a9994825b9b48a375bcc65ec59ae87b2cc23f", "file_operations_dag.py": "c4b5969a296af94c61cd63a9511a5572f9b858cbd75be913e8dffd015441b48f", "hello_world_dag.py": "09036daee495267a9dc23fd9c04fbd99c50b76194be1f3004822cee87f7797e7", "resource_management_dag.py": "03774dbfe5cadc6d6f036814f4fb2f8db0991c511b7ea3e877f577dca644b13e", "sql_basic_dag.py": "d028ff3d281ad322e8443447f43000497db67053cfeb757dc23b21b78f8b536f"}} +{"label": "base_branching_dag", "dag": "branching_dag", "run_id": "base_branching_dag__1790598303491558368", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:25:04.466969+00:00", "end": "2026-09-28T12:25:04.466989+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:06.300063+00:00", "end": "2026-09-28T12:25:07.740076+00:00"}, "process_csv_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:09.192486+00:00", "end": "2026-09-28T12:25:09.736934+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:25:11.926795+00:00", "end": "2026-09-28T12:25:11.926806+00:00"}, "process_json_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:25:07.614511+00:00", "end": "2026-09-28T12:25:07.614511+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:10.695251+00:00", "end": "2026-09-28T12:25:11.211111+00:00"}}} +{"label": "base_csv_to_postgres", "dag": "csv_to_postgres", "run_id": "base_csv_to_postgres__1790598316856917563", "state": "success", "expected_state": "success", "tasks": {"create_orders_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:17.703222+00:00", "end": "2026-09-28T12:25:18.385307+00:00"}, "generate_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:19.297092+00:00", "end": "2026-09-28T12:25:20.954830+00:00"}, "preview_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:21.691404+00:00", "end": "2026-09-28T12:25:23.672198+00:00"}, "load_csv_to_postgres": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:24.349364+00:00", "end": "2026-09-28T12:25:25.132534+00:00"}}} +{"label": "base_data_processing_dag", "dag": "data_processing_dag", "run_id": "base_data_processing_dag__1790598297048939564", "state": "success", "expected_state": "success", "tasks": {"create_sample_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:58.018194+00:00", "end": "2026-09-28T12:24:59.516887+00:00"}, "extract_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:00.524479+00:00", "end": "2026-09-28T12:25:04.438975+00:00"}, "extract_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:00.319521+00:00", "end": "2026-09-28T12:25:04.581621+00:00"}, "transform_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:06.641381+00:00", "end": "2026-09-28T12:25:08.648080+00:00"}, "simulate_load": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:10.564728+00:00", "end": "2026-09-28T12:25:11.970738+00:00"}, "generate_report": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:13.716924+00:00", "end": "2026-09-28T12:25:15.365893+00:00"}}} +{"label": "base_dq", "dag": "csv_to_postgres_dq", "run_id": "base_dq__1790598363500865461", "state": "success", "expected_state": "success", "tasks": {"data_quality_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T12:26:09.667195+00:00", "end": "2026-09-28T12:26:10.149648+00:00"}, "check_orders_schema": {"state": "success", "attempts": 1, "start": "2026-09-28T12:26:05.724275+00:00", "end": "2026-09-28T12:26:06.206894+00:00"}, "check_orders_has_rows": {"state": "success", "attempts": 1, "start": "2026-09-28T12:26:06.888967+00:00", "end": "2026-09-28T12:26:07.337686+00:00"}, "check_order_duplicates": {"state": "success", "attempts": 1, "start": "2026-09-28T12:26:08.530504+00:00", "end": "2026-09-28T12:26:09.010861+00:00"}, "check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:26:04.557178+00:00", "end": "2026-09-28T12:26:05.087473+00:00"}}} +{"label": "base_error_handling_dag", "dag": "error_handling_dag", "run_id": "base_error_handling_dag__1790598307068800147", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:25:08.688153+00:00", "end": "2026-09-28T12:25:08.688162+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:09.327897+00:00", "end": "2026-09-28T12:25:09.842635+00:00"}, "success_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:21.859653+00:00", "end": "2026-09-28T12:25:22.425907+00:00"}, "retry_task": {"state": "success", "attempts": 2, "start": "2026-09-28T12:25:20.608453+00:00", "end": "2026-09-28T12:25:21.211328+00:00"}, "failure_handler": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:25:21.289459+00:00", "end": "2026-09-28T12:25:21.289459+00:00"}}} +{"label": "base_file_operations_dag", "dag": "file_operations_dag", "run_id": "base_file_operations_dag__1790598285829374474", "state": "success", "expected_state": "success", "tasks": {"generate_sample_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:47.788802+00:00", "end": "2026-09-28T12:24:52.901884+00:00"}, "read_and_validate_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:54.105194+00:00", "end": "2026-09-28T12:24:56.336898+00:00"}, "transform_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:57.980925+00:00", "end": "2026-09-28T12:24:59.453129+00:00"}, "write_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:00.578118+00:00", "end": "2026-09-28T12:25:05.022890+00:00"}}} +{"label": "base_hello_world_dag", "dag": "hello_world_dag", "run_id": "base_hello_world_dag__1790598285331279953", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:47.450226+00:00", "end": "2026-09-28T12:24:48.038258+00:00"}, "date_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:50.252850+00:00", "end": "2026-09-28T12:24:50.964908+00:00"}, "end_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:54.481818+00:00", "end": "2026-09-28T12:24:55.586281+00:00"}}} +{"label": "base_resource_management_dag", "dag": "resource_management_dag", "run_id": "base_resource_management_dag__1790598315287681546", "state": "success", "expected_state": "success", "tasks": {"read_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:16.416179+00:00", "end": "2026-09-28T12:25:36.854962+00:00"}, "read_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:38.182441+00:00", "end": "2026-09-28T12:25:58.742686+00:00"}, "calculate_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:25:59.666739+00:00", "end": "2026-09-28T12:26:00.315747+00:00"}, "log_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:26:00.826752+00:00", "end": "2026-09-28T12:26:01.327392+00:00"}}} +{"label": "base_sql_basic_dag", "dag": "sql_basic_dag", "run_id": "base_sql_basic_dag__1790598285733066584", "state": "success", "expected_state": "success", "tasks": {"create_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:47.558668+00:00", "end": "2026-09-28T12:24:48.171774+00:00"}, "truncate_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:50.270342+00:00", "end": "2026-09-28T12:24:50.925384+00:00"}, "insert_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:54.165693+00:00", "end": "2026-09-28T12:24:55.422059+00:00"}, "query_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:24:58.018381+00:00", "end": "2026-09-28T12:24:58.580976+00:00"}}} +{"label": "main_1_1", "dag": "hello_world_dag", "run_id": "main_1_1__1790598669894851732", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:31:11.829375+00:00", "end": "2026-09-28T12:31:12.443251+00:00"}, "weather_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:31:12.949206+00:00", "end": "2026-09-28T12:31:13.415511+00:00"}, "date_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:31:14.385576+00:00", "end": "2026-09-28T12:31:14.885931+00:00"}, "end_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:31:16.391743+00:00", "end": "2026-09-28T12:31:16.910520+00:00"}}} +{"label": "main_1_2_3", "dag": "hello_world_dag", "run_id": "main_1_2_3__1790598718302127877", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:31:59.745450+00:00", "end": "2026-09-28T12:32:00.316366+00:00"}, "write_note": {"state": "success", "attempts": 1, "start": "2026-09-28T12:32:05.072071+00:00", "end": "2026-09-28T12:32:06.021474+00:00"}, "weather_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:32:01.159334+00:00", "end": "2026-09-28T12:32:01.723974+00:00"}, "date_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:32:02.837180+00:00", "end": "2026-09-28T12:32:03.474962+00:00"}, "end_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:32:07.286250+00:00", "end": "2026-09-28T12:32:07.807426+00:00"}}} +{"label": "main_2_1_bad_connection", "dag": "sql_basic_dag", "run_id": "main_2_1_bad_connection__1790598823284321738", "state": "failed", "expected_state": "failed", "tasks": {"truncate_table": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:33:44.966461+00:00", "end": "2026-09-28T12:33:44.966461+00:00"}, "insert_data": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:33:45.092205+00:00", "end": "2026-09-28T12:33:45.092205+00:00"}, "query_data": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:33:46.184509+00:00", "end": "2026-09-28T12:33:46.184509+00:00"}, "create_table": {"state": "failed", "attempts": 1, "start": "2026-09-28T12:33:44.394182+00:00", "end": "2026-09-28T12:33:44.864384+00:00"}}} +{"label": "main_2_1_connection", "dag": "sql_basic_dag", "run_id": "main_2_1_connection__1790598770237278526", "state": "success", "expected_state": "success", "tasks": {"truncate_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:32:53.727892+00:00", "end": "2026-09-28T12:32:54.590903+00:00"}, "query_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:32:57.779131+00:00", "end": "2026-09-28T12:32:58.430291+00:00"}, "insert_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:32:55.515062+00:00", "end": "2026-09-28T12:32:56.385369+00:00"}, "create_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:32:51.379822+00:00", "end": "2026-09-28T12:32:52.074251+00:00"}}} +{"label": "main_2_2_repeat", "dag": "sql_basic_dag", "run_id": "main_2_2_repeat__1790598880752727153", "state": "success", "expected_state": "success", "tasks": {"create_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:42.273136+00:00", "end": "2026-09-28T12:34:42.874104+00:00"}, "truncate_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:43.459390+00:00", "end": "2026-09-28T12:34:44.136238+00:00"}, "insert_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:45.510497+00:00", "end": "2026-09-28T12:34:46.049469+00:00"}, "query_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:49.918420+00:00", "end": "2026-09-28T12:34:50.754474+00:00"}, "update_student": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:47.643123+00:00", "end": "2026-09-28T12:34:48.453621+00:00"}}} +{"label": "main_2_2_update", "dag": "sql_basic_dag", "run_id": "main_2_2_update__1790598869163909877", "state": "success", "expected_state": "success", "tasks": {"create_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:30.191396+00:00", "end": "2026-09-28T12:34:30.709128+00:00"}, "truncate_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:31.374968+00:00", "end": "2026-09-28T12:34:31.950460+00:00"}, "insert_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:33.266504+00:00", "end": "2026-09-28T12:34:33.826087+00:00"}, "update_student": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:34.392167+00:00", "end": "2026-09-28T12:34:34.997350+00:00"}, "query_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:34:35.712919+00:00", "end": "2026-09-28T12:34:36.267889+00:00"}}} +{"label": "main_3_1", "dag": "file_operations_dag", "run_id": "main_3_1__1790598934667802974", "state": "success", "expected_state": "success", "tasks": {"transform_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:35:41.591826+00:00", "end": "2026-09-28T12:35:43.072378+00:00"}, "write_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T12:35:43.657051+00:00", "end": "2026-09-28T12:35:45.282296+00:00"}, "read_and_validate_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:35:38.756892+00:00", "end": "2026-09-28T12:35:40.407604+00:00"}, "generate_sample_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:35:35.850235+00:00", "end": "2026-09-28T12:35:37.611113+00:00"}}} +{"label": "main_4_1", "dag": "csv_to_postgres", "run_id": "main_4_1__1790598948261221421", "state": "success", "expected_state": "success", "tasks": {"preview_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:35:54.292219+00:00", "end": "2026-09-28T12:35:56.183442+00:00"}, "load_csv_to_postgres": {"state": "success", "attempts": 1, "start": "2026-09-28T12:35:57.028083+00:00", "end": "2026-09-28T12:35:57.557853+00:00"}, "create_orders_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:35:49.970156+00:00", "end": "2026-09-28T12:35:50.510862+00:00"}, "generate_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:35:51.118676+00:00", "end": "2026-09-28T12:35:52.724944+00:00"}}} +{"label": "main_4_2_negative", "dag": "csv_to_postgres_dq", "run_id": "main_4_2_negative__1790599011734924894", "state": "failed", "expected_state": "failed", "tasks": {"check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:52.890470+00:00", "end": "2026-09-28T12:36:53.470813+00:00"}, "check_orders_schema": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:54.443034+00:00", "end": "2026-09-28T12:36:54.969375+00:00"}, "check_positive_amount": {"state": "failed", "attempts": 1, "start": "2026-09-28T12:36:59.434123+00:00", "end": "2026-09-28T12:37:00.000016+00:00"}, "check_orders_has_rows": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:56.309978+00:00", "end": "2026-09-28T12:36:56.841855+00:00"}, "data_quality_summary": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:37:00.114864+00:00", "end": "2026-09-28T12:37:00.114864+00:00"}, "check_order_duplicates": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:57.649020+00:00", "end": "2026-09-28T12:36:58.151617+00:00"}}} +{"label": "main_4_2_positive", "dag": "csv_to_postgres_dq", "run_id": "main_4_2_positive__1790599000150640710", "state": "success", "expected_state": "success", "tasks": {"check_positive_amount": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:46.773434+00:00", "end": "2026-09-28T12:36:47.237395+00:00"}, "data_quality_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:47.780290+00:00", "end": "2026-09-28T12:36:48.296235+00:00"}, "check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:41.328500+00:00", "end": "2026-09-28T12:36:41.826903+00:00"}, "check_orders_schema": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:42.632046+00:00", "end": "2026-09-28T12:36:43.225919+00:00"}, "check_orders_has_rows": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:44.252071+00:00", "end": "2026-09-28T12:36:44.799840+00:00"}, "check_order_duplicates": {"state": "success", "attempts": 1, "start": "2026-09-28T12:36:45.510631+00:00", "end": "2026-09-28T12:36:46.128483+00:00"}}} +{"label": "main_4_2_restore", "dag": "csv_to_postgres_dq", "run_id": "main_4_2_restore__1790599023379510146", "state": "success", "expected_state": "success", "tasks": {"check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:37:04.600783+00:00", "end": "2026-09-28T12:37:05.244958+00:00"}, "check_orders_schema": {"state": "success", "attempts": 1, "start": "2026-09-28T12:37:06.720511+00:00", "end": "2026-09-28T12:37:07.273312+00:00"}, "check_order_duplicates": {"state": "success", "attempts": 1, "start": "2026-09-28T12:37:09.578662+00:00", "end": "2026-09-28T12:37:10.260325+00:00"}, "check_orders_has_rows": {"state": "success", "attempts": 1, "start": "2026-09-28T12:37:07.872450+00:00", "end": "2026-09-28T12:37:08.452528+00:00"}, "data_quality_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T12:37:12.613421+00:00", "end": "2026-09-28T12:37:13.154513+00:00"}, "check_positive_amount": {"state": "success", "attempts": 1, "start": "2026-09-28T12:37:11.411627+00:00", "end": "2026-09-28T12:37:11.924165+00:00"}}} +{"label": "main_5_1_filter", "dag": "data_processing_dag", "run_id": "main_5_1_filter__1790599074663133799", "state": "success", "expected_state": "success", "tasks": {"create_sample_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:37:55.824116+00:00", "end": "2026-09-28T12:37:57.680612+00:00"}, "extract_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:37:59.047390+00:00", "end": "2026-09-28T12:38:00.783528+00:00"}, "extract_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:37:59.039283+00:00", "end": "2026-09-28T12:38:00.812189+00:00"}, "transform_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:38:01.501036+00:00", "end": "2026-09-28T12:38:03.122143+00:00"}, "simulate_load": {"state": "success", "attempts": 1, "start": "2026-09-28T12:38:04.173110+00:00", "end": "2026-09-28T12:38:05.600297+00:00"}, "generate_report": {"state": "success", "attempts": 1, "start": "2026-09-28T12:38:06.618522+00:00", "end": "2026-09-28T12:38:08.794823+00:00"}}} +{"label": "main_5_1_restore", "dag": "data_processing_dag", "run_id": "main_5_1_restore__1790599131072124593", "state": "success", "expected_state": "success", "tasks": {"create_sample_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:38:52.059583+00:00", "end": "2026-09-28T12:38:53.524667+00:00"}, "extract_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:38:55.185088+00:00", "end": "2026-09-28T12:38:56.827156+00:00"}, "transform_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:38:58.100743+00:00", "end": "2026-09-28T12:38:59.670338+00:00"}, "simulate_load": {"state": "success", "attempts": 1, "start": "2026-09-28T12:39:01.276000+00:00", "end": "2026-09-28T12:39:02.673380+00:00"}, "extract_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:38:55.342213+00:00", "end": "2026-09-28T12:38:56.915006+00:00"}, "generate_report": {"state": "success", "attempts": 1, "start": "2026-09-28T12:39:03.515006+00:00", "end": "2026-09-28T12:39:04.916556+00:00"}}} +{"label": "main_6_1_csv", "dag": "branching_dag", "run_id": "main_6_1_csv__1790599221866037738", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:40:23.121466+00:00", "end": "2026-09-28T12:40:23.121476+00:00"}, "process_csv_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:25.928269+00:00", "end": "2026-09-28T12:40:26.732622+00:00"}, "process_json_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:40:25.140559+00:00", "end": "2026-09-28T12:40:25.140559+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:24.652209+00:00", "end": "2026-09-28T12:40:25.245620+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:27.779111+00:00", "end": "2026-09-28T12:40:28.278924+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:40:28.547843+00:00", "end": "2026-09-28T12:40:28.547856+00:00"}}} +{"label": "main_6_1_csv_restore", "dag": "branching_dag", "run_id": "main_6_1_csv_restore__1790599246736022049", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:40:47.688841+00:00", "end": "2026-09-28T12:40:47.688856+00:00"}, "process_json_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:40:49.804302+00:00", "end": "2026-09-28T12:40:49.804302+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:49.301493+00:00", "end": "2026-09-28T12:40:49.954481+00:00"}, "process_csv_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:50.449791+00:00", "end": "2026-09-28T12:40:51.052888+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:51.831887+00:00", "end": "2026-09-28T12:40:52.503932+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:40:52.882378+00:00", "end": "2026-09-28T12:40:52.882395+00:00"}}} +{"label": "main_6_1_json", "dag": "branching_dag", "run_id": "main_6_1_json__1790599230837355984", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:40:31.315989+00:00", "end": "2026-09-28T12:40:31.315999+00:00"}, "process_csv_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:40:33.528600+00:00", "end": "2026-09-28T12:40:33.528600+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:33.004290+00:00", "end": "2026-09-28T12:40:33.646275+00:00"}, "process_json_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:34.836572+00:00", "end": "2026-09-28T12:40:35.404802+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:36.209056+00:00", "end": "2026-09-28T12:40:36.773333+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:40:37.541270+00:00", "end": "2026-09-28T12:40:37.541279+00:00"}}} +{"label": "main_6_1_xml", "dag": "branching_dag", "run_id": "main_6_1_xml__1790599239960170363", "state": "failed", "expected_state": "failed", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:40:40.887013+00:00", "end": "2026-09-28T12:40:40.887026+00:00"}, "choose_format": {"state": "failed", "attempts": 1, "start": "2026-09-28T12:40:42.076249+00:00", "end": "2026-09-28T12:40:42.682151+00:00"}, "process_csv_branch": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:40:42.871931+00:00", "end": "2026-09-28T12:40:42.871931+00:00"}, "process_json_branch": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:40:42.884065+00:00", "end": "2026-09-28T12:40:42.884065+00:00"}, "merge_results": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:40:43.830677+00:00", "end": "2026-09-28T12:40:43.830677+00:00"}, "end_task": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:40:44.428621+00:00", "end": "2026-09-28T12:40:44.428621+00:00"}}} +{"label": "main_7_1", "dag": "error_handling_dag", "run_id": "main_7_1__1790599256282469073", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:40:56.875385+00:00", "end": "2026-09-28T12:40:56.875397+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:40:58.566416+00:00", "end": "2026-09-28T12:40:59.270822+00:00"}, "retry_task": {"state": "success", "attempts": 2, "start": "2026-09-28T12:41:09.795211+00:00", "end": "2026-09-28T12:41:10.316195+00:00"}, "failure_handler": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:41:10.449363+00:00", "end": "2026-09-28T12:41:10.449363+00:00"}, "success_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:41:11.113518+00:00", "end": "2026-09-28T12:41:11.598224+00:00"}}} +{"label": "main_7_2_fail_both", "dag": "error_handling_dag", "run_id": "main_7_2_fail_both__1790599401713196879", "state": "failed", "expected_state": "failed", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:43:22.530430+00:00", "end": "2026-09-28T12:43:22.530442+00:00"}, "retry_task": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:43:36.132875+00:00", "end": "2026-09-28T12:43:36.780473+00:00"}, "unreliable_task": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:43:36.118123+00:00", "end": "2026-09-28T12:43:36.741330+00:00"}, "success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:43:36.841371+00:00", "end": "2026-09-28T12:43:36.841371+00:00"}, "failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:43:37.573043+00:00", "end": "2026-09-28T12:43:38.121760+00:00"}}} +{"label": "main_7_2_fail_first", "dag": "error_handling_dag", "run_id": "main_7_2_fail_first__1790599364116452900", "state": "failed", "expected_state": "failed", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:42:44.750400+00:00", "end": "2026-09-28T12:42:44.750408+00:00"}, "retry_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:42:45.926971+00:00", "end": "2026-09-28T12:42:46.477915+00:00"}, "failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:42:59.251667+00:00", "end": "2026-09-28T12:42:59.716135+00:00"}, "unreliable_task": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:42:57.061887+00:00", "end": "2026-09-28T12:42:57.672300+00:00"}, "success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:42:57.864139+00:00", "end": "2026-09-28T12:42:57.864139+00:00"}}} +{"label": "main_7_2_fail_second", "dag": "error_handling_dag", "run_id": "main_7_2_fail_second__1790599383044317774", "state": "failed", "expected_state": "failed", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:43:03.978190+00:00", "end": "2026-09-28T12:43:03.978208+00:00"}, "retry_task": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:43:17.475284+00:00", "end": "2026-09-28T12:43:17.984522+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:43:05.869070+00:00", "end": "2026-09-28T12:43:06.372928+00:00"}, "success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:43:18.102304+00:00", "end": "2026-09-28T12:43:18.102304+00:00"}, "failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:43:19.615230+00:00", "end": "2026-09-28T12:43:20.101638+00:00"}}} +{"label": "main_7_2_no_retry", "dag": "error_handling_dag", "run_id": "main_7_2_no_retry__1790599315199596055", "state": "failed", "expected_state": "failed", "tasks": {"failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:42:00.036017+00:00", "end": "2026-09-28T12:42:00.705269+00:00"}, "start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:41:56.214072+00:00", "end": "2026-09-28T12:41:56.214088+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:41:57.872256+00:00", "end": "2026-09-28T12:41:58.819669+00:00"}, "retry_task": {"state": "failed", "attempts": 1, "start": "2026-09-28T12:41:57.911390+00:00", "end": "2026-09-28T12:41:58.936653+00:00"}, "success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:41:59.040015+00:00", "end": "2026-09-28T12:41:59.040015+00:00"}}} +{"label": "main_7_2_success", "dag": "error_handling_dag", "run_id": "main_7_2_success__1790599420340411753", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:43:40.702654+00:00", "end": "2026-09-28T12:43:40.702666+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:43:42.414466+00:00", "end": "2026-09-28T12:43:43.206132+00:00"}, "retry_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:43:42.347891+00:00", "end": "2026-09-28T12:43:43.281951+00:00"}, "failure_handler": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:43:43.383261+00:00", "end": "2026-09-28T12:43:43.383261+00:00"}, "success_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:43:44.696936+00:00", "end": "2026-09-28T12:43:45.165836+00:00"}}} +{"label": "main_7_3", "dag": "error_handling_dag", "run_id": "main_7_3__1790599466582227262", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:44:27.093897+00:00", "end": "2026-09-28T12:44:27.093911+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:44:28.737493+00:00", "end": "2026-09-28T12:44:29.422467+00:00"}, "retry_task": {"state": "success", "attempts": 2, "start": "2026-09-28T12:44:40.106129+00:00", "end": "2026-09-28T12:44:40.590496+00:00"}, "failure_handler": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:44:40.639777+00:00", "end": "2026-09-28T12:44:40.639777+00:00"}, "success_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:44:41.109197+00:00", "end": "2026-09-28T12:44:41.576977+00:00"}}} +{"label": "main_8_1_parallel", "dag": "resource_management_dag", "run_id": "main_8_1_parallel__1790599574558882106", "state": "success", "expected_state": "success", "tasks": {"read_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:46:16.538315+00:00", "end": "2026-09-28T12:46:37.107764+00:00"}, "read_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:46:16.596349+00:00", "end": "2026-09-28T12:46:37.192829+00:00"}, "calculate_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:46:37.808171+00:00", "end": "2026-09-28T12:46:38.458356+00:00"}, "log_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:46:39.331072+00:00", "end": "2026-09-28T12:46:39.899368+00:00"}}} +{"label": "main_8_1_serial", "dag": "resource_management_dag", "run_id": "main_8_1_serial__1790599482620382943", "state": "success", "expected_state": "success", "tasks": {"log_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:45:29.173691+00:00", "end": "2026-09-28T12:45:30.128755+00:00"}, "read_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:44:43.856681+00:00", "end": "2026-09-28T12:45:04.694013+00:00"}, "read_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:45:05.830821+00:00", "end": "2026-09-28T12:45:26.382507+00:00"}, "calculate_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:45:27.408530+00:00", "end": "2026-09-28T12:45:28.420441+00:00"}}} +{"label": "main_8_2", "dag": "resource_management_dag", "run_id": "main_8_2__1790599640804934985", "state": "success", "expected_state": "success", "tasks": {"read_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:47:21.889563+00:00", "end": "2026-09-28T12:47:42.647916+00:00"}, "read_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:47:43.632588+00:00", "end": "2026-09-28T12:48:04.391177+00:00"}, "calculate_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:48:05.904759+00:00", "end": "2026-09-28T12:48:06.622084+00:00"}, "log_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:48:08.049355+00:00", "end": "2026-09-28T12:48:08.564654+00:00"}}} +{"label": "main_8_3", "dag": "resource_management_dag", "run_id": "main_8_3__1790599731782559761", "state": "success", "expected_state": "success", "tasks": {"extract.read_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:48:53.427367+00:00", "end": "2026-09-28T12:49:13.903909+00:00"}, "extract.read_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:49:15.274901+00:00", "end": "2026-09-28T12:49:35.809526+00:00"}, "calculate_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:49:36.965983+00:00", "end": "2026-09-28T12:49:37.918550+00:00"}, "log_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T12:49:39.405235+00:00", "end": "2026-09-28T12:49:39.974974+00:00"}}} +{"label": "optional_1_A_first", "dag": "hello_world_dag", "run_id": "optional_1_A_first__1790599828501658295", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:29.705066+00:00", "end": "2026-09-28T12:51:00.256126+00:00"}, "date_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:02.651448+00:00", "end": "2026-09-28T12:51:03.175435+00:00"}, "weather_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:01.427549+00:00", "end": "2026-09-28T12:51:02.022120+00:00"}, "write_note": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:03.805590+00:00", "end": "2026-09-28T12:51:04.377917+00:00"}, "end_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:04.973858+00:00", "end": "2026-09-28T12:51:05.452179+00:00"}}} +{"label": "optional_1_A_second", "dag": "hello_world_dag", "run_id": "optional_1_A_second__1790599831490826780", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:07.523497+00:00", "end": "2026-09-28T12:51:38.200910+00:00"}, "weather_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:40.092576+00:00", "end": "2026-09-28T12:51:40.720014+00:00"}, "date_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:42.001534+00:00", "end": "2026-09-28T12:51:42.531175+00:00"}, "end_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:45.023411+00:00", "end": "2026-09-28T12:51:45.554981+00:00"}, "write_note": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:43.392660+00:00", "end": "2026-09-28T12:51:44.179028+00:00"}}} +{"label": "optional_2_A_B", "dag": "sql_basic_dag", "run_id": "optional_2_A_B__1790599987276042498", "state": "success", "expected_state": "success", "tasks": {"create_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:09.211357+00:00", "end": "2026-09-28T12:53:09.951512+00:00"}, "truncate_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:11.047425+00:00", "end": "2026-09-28T12:53:11.740876+00:00"}, "insert_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:12.934788+00:00", "end": "2026-09-28T12:53:13.607534+00:00"}, "update_student": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:15.944973+00:00", "end": "2026-09-28T12:53:16.638095+00:00"}, "query_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:17.435005+00:00", "end": "2026-09-28T12:53:18.141358+00:00"}, "report": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:19.546507+00:00", "end": "2026-09-28T12:53:20.286011+00:00"}}} +{"label": "optional_3_A_B_C", "dag": "file_operations_dag", "run_id": "optional_3_A_B_C__1790600042332111688", "state": "success", "expected_state": "success", "tasks": {"transform_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:09.980335+00:00", "end": "2026-09-28T12:54:12.248114+00:00"}, "write_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:13.879091+00:00", "end": "2026-09-28T12:54:17.284622+00:00"}, "read_and_validate_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:07.264422+00:00", "end": "2026-09-28T12:54:08.788050+00:00"}, "generate_sample_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:03.667581+00:00", "end": "2026-09-28T12:54:06.067366+00:00"}, "department_engineering": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:14.349510+00:00", "end": "2026-09-28T12:54:17.713422+00:00"}, "department_support": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:14.885206+00:00", "end": "2026-09-28T12:54:17.977159+00:00"}, "department_report": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:20.187672+00:00", "end": "2026-09-28T12:54:21.852688+00:00"}, "department_sales": {"state": "success", "attempts": 2, "start": "2026-09-28T12:54:20.381886+00:00", "end": "2026-09-28T12:54:22.016177+00:00"}, "gather": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:22.800577+00:00", "end": "2026-09-28T12:54:24.457104+00:00"}}} +{"label": "optional_4_A_dq", "dag": "csv_to_postgres_dq", "run_id": "optional_4_A_dq__1790600084423979625", "state": "success", "expected_state": "success", "tasks": {"check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:45.792417+00:00", "end": "2026-09-28T12:54:46.301310+00:00"}, "check_orders_schema": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:47.351405+00:00", "end": "2026-09-28T12:54:47.874188+00:00"}, "check_orders_has_rows": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:48.529849+00:00", "end": "2026-09-28T12:54:48.966554+00:00"}, "check_order_duplicates": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:49.787486+00:00", "end": "2026-09-28T12:54:50.271862+00:00"}, "check_positive_amount": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:51.064045+00:00", "end": "2026-09-28T12:54:51.705791+00:00"}, "data_quality_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:52.304665+00:00", "end": "2026-09-28T12:54:52.846778+00:00"}}} +{"label": "optional_4_A_restore", "dag": "csv_to_postgres_dq", "run_id": "optional_4_A_restore__1790600176128130911", "state": "success", "expected_state": "success", "tasks": {"check_orders_schema": {"state": "success", "attempts": 1, "start": "2026-09-28T12:56:18.907596+00:00", "end": "2026-09-28T12:56:19.356101+00:00"}, "check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:56:17.659452+00:00", "end": "2026-09-28T12:56:18.088407+00:00"}, "check_positive_amount": {"state": "success", "attempts": 1, "start": "2026-09-28T12:56:22.339147+00:00", "end": "2026-09-28T12:56:22.756030+00:00"}, "check_orders_has_rows": {"state": "success", "attempts": 1, "start": "2026-09-28T12:56:19.993271+00:00", "end": "2026-09-28T12:56:20.456629+00:00"}, "check_order_duplicates": {"state": "success", "attempts": 1, "start": "2026-09-28T12:56:21.187257+00:00", "end": "2026-09-28T12:56:21.591291+00:00"}, "data_quality_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T12:56:23.525671+00:00", "end": "2026-09-28T12:56:23.996173+00:00"}}} +{"label": "optional_4_A_status", "dag": "csv_to_postgres", "run_id": "optional_4_A_status__1790600072998689452", "state": "success", "expected_state": "success", "tasks": {"create_orders_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:34.420941+00:00", "end": "2026-09-28T12:54:34.965465+00:00"}, "generate_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:35.846349+00:00", "end": "2026-09-28T12:54:37.632934+00:00"}, "load_csv_to_postgres": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:40.758903+00:00", "end": "2026-09-28T12:54:41.313249+00:00"}, "preview_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:38.368799+00:00", "end": "2026-09-28T12:54:39.934048+00:00"}}} +{"label": "optional_4_A_status_bad_order", "dag": "csv_to_postgres", "run_id": "optional_4_A_status__1790599866797382800", "state": "failed", "expected_state": "failed", "tasks": {"create_orders_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:07.993200+00:00", "end": "2026-09-28T12:51:08.577937+00:00"}, "load_csv_to_postgres": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:51:47.478501+00:00", "end": "2026-09-28T12:51:48.034511+00:00"}, "generate_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:09.311281+00:00", "end": "2026-09-28T12:51:10.967148+00:00"}, "preview_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:12.691761+00:00", "end": "2026-09-28T12:51:14.377415+00:00"}}} +{"label": "optional_4_B_dq", "dag": "csv_to_postgres_dq", "run_id": "optional_4_B_dq__1790600237996456220", "state": "success", "expected_state": "success", "tasks": {"check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:19.103267+00:00", "end": "2026-09-28T12:57:19.560193+00:00"}, "check_orders_schema": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:20.324388+00:00", "end": "2026-09-28T12:57:20.846060+00:00"}, "check_orders_has_rows": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:22.250197+00:00", "end": "2026-09-28T12:57:22.727576+00:00"}, "check_order_duplicates": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:23.415948+00:00", "end": "2026-09-28T12:57:23.951371+00:00"}, "check_positive_amount": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:25.207786+00:00", "end": "2026-09-28T12:57:25.689645+00:00"}, "data_quality_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:27.492209+00:00", "end": "2026-09-28T12:57:28.083003+00:00"}}} +{"label": "optional_4_B_operator", "dag": "csv_to_postgres", "run_id": "optional_4_B_operator__1790600227029157362", "state": "success", "expected_state": "success", "tasks": {"generate_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:10.252580+00:00", "end": "2026-09-28T12:57:11.814673+00:00"}, "create_orders_table": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:08.082255+00:00", "end": "2026-09-28T12:57:08.667507+00:00"}, "preview_csv": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:13.073106+00:00", "end": "2026-09-28T12:57:14.567664+00:00"}, "load_csv_to_postgres": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:15.203959+00:00", "end": "2026-09-28T12:57:15.691633+00:00"}}} +{"label": "optional_4_C_all_done", "dag": "csv_to_postgres_dq", "run_id": "optional_4_C_all_done__1790600337228090134", "state": "failed", "expected_state": "failed", "tasks": {"check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:58:58.725586+00:00", "end": "2026-09-28T12:58:59.201374+00:00"}, "check_orders_schema": {"state": "failed", "attempts": 1, "start": "2026-09-28T12:59:01.015357+00:00", "end": "2026-09-28T12:59:01.541150+00:00"}, "check_orders_has_rows": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:02.461634+00:00", "end": "2026-09-28T12:59:02.991782+00:00"}, "check_order_duplicates": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:03.664066+00:00", "end": "2026-09-28T12:59:04.142534+00:00"}, "check_positive_amount": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:04.892560+00:00", "end": "2026-09-28T12:59:05.430995+00:00"}, "data_quality_summary": {"state": "failed", "attempts": 1, "start": "2026-09-28T12:59:07.120230+00:00", "end": "2026-09-28T12:59:07.593930+00:00"}}} +{"label": "optional_4_C_default", "dag": "csv_to_postgres_dq", "run_id": "optional_4_C_default__1790600291366135652", "state": "failed", "expected_state": "failed", "tasks": {"check_order_duplicates": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:58:15.164590+00:00", "end": "2026-09-28T12:58:15.164590+00:00"}, "check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:58:13.042381+00:00", "end": "2026-09-28T12:58:13.526508+00:00"}, "check_positive_amount": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:58:16.262284+00:00", "end": "2026-09-28T12:58:16.262284+00:00"}, "data_quality_summary": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:58:16.585583+00:00", "end": "2026-09-28T12:58:16.585583+00:00"}, "check_orders_schema": {"state": "failed", "attempts": 1, "start": "2026-09-28T12:58:14.330420+00:00", "end": "2026-09-28T12:58:14.851567+00:00"}, "check_orders_has_rows": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:58:14.960806+00:00", "end": "2026-09-28T12:58:14.960806+00:00"}}} +{"label": "optional_4_C_restore", "dag": "csv_to_postgres_dq", "run_id": "optional_4_C_restore__1790600390711838382", "state": "success", "expected_state": "success", "tasks": {"check_orders_table_exists": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:51.882799+00:00", "end": "2026-09-28T12:59:52.351329+00:00"}, "check_orders_schema": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:53.403783+00:00", "end": "2026-09-28T12:59:53.992901+00:00"}, "check_orders_has_rows": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:54.968165+00:00", "end": "2026-09-28T12:59:55.731129+00:00"}, "check_order_duplicates": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:58.083402+00:00", "end": "2026-09-28T12:59:59.365498+00:00"}, "check_positive_amount": {"state": "success", "attempts": 1, "start": "2026-09-28T13:00:01.904918+00:00", "end": "2026-09-28T13:00:03.145987+00:00"}, "data_quality_summary": {"state": "success", "attempts": 1, "start": "2026-09-28T13:00:03.953750+00:00", "end": "2026-09-28T13:00:04.515677+00:00"}}} +{"label": "optional_5_A_B_C_success", "dag": "data_processing_dag", "run_id": "optional_5_A_B_C_success__1790599826280464970", "state": "success", "expected_state": "success", "tasks": {"create_sample_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:28.065679+00:00", "end": "2026-09-28T12:50:29.967444+00:00"}, "validate": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:38.601847+00:00", "end": "2026-09-28T12:50:40.510529+00:00"}, "transform_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:35.381953+00:00", "end": "2026-09-28T12:50:37.142909+00:00"}, "extract_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:30.926518+00:00", "end": "2026-09-28T12:50:32.669276+00:00"}, "extract_products": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:31.062084+00:00", "end": "2026-09-28T12:50:32.916827+00:00"}, "extract_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:30.983002+00:00", "end": "2026-09-28T12:50:33.137312+00:00"}, "simulate_load": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:41.199488+00:00", "end": "2026-09-28T12:50:42.824662+00:00"}, "generate_report": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:43.422545+00:00", "end": "2026-09-28T12:50:45.474906+00:00"}, "archive": {"state": "success", "attempts": 1, "start": "2026-09-28T12:50:46.874833+00:00", "end": "2026-09-28T12:50:47.500464+00:00"}}} +{"label": "optional_5_C_failure", "dag": "data_processing_dag", "run_id": "optional_5_C_failure__1790599890880680316", "state": "failed", "expected_state": "failed", "tasks": {"create_sample_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:31.769851+00:00", "end": "2026-09-28T12:51:33.331037+00:00"}, "extract_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:34.907501+00:00", "end": "2026-09-28T12:51:36.969211+00:00"}, "extract_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:34.860135+00:00", "end": "2026-09-28T12:51:37.189529+00:00"}, "transform_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:38.760107+00:00", "end": "2026-09-28T12:51:40.621671+00:00"}, "simulate_load": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:51:49.189667+00:00", "end": "2026-09-28T12:51:49.189667+00:00"}, "generate_report": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:51:49.662176+00:00", "end": "2026-09-28T12:51:49.662176+00:00"}, "archive": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:51:50.496425+00:00", "end": "2026-09-28T12:51:50.496425+00:00"}, "extract_products": {"state": "success", "attempts": 1, "start": "2026-09-28T12:51:35.050525+00:00", "end": "2026-09-28T12:51:37.258136+00:00"}, "validate": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:51:47.454480+00:00", "end": "2026-09-28T12:51:49.033534+00:00"}}} +{"label": "optional_5_C_restore", "dag": "data_processing_dag", "run_id": "optional_5_C_restore__1790599954560978695", "state": "success", "expected_state": "success", "tasks": {"extract_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:38.792872+00:00", "end": "2026-09-28T12:52:41.056815+00:00"}, "extract_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:38.712862+00:00", "end": "2026-09-28T12:52:40.983042+00:00"}, "transform_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:44.098377+00:00", "end": "2026-09-28T12:52:46.183169+00:00"}, "generate_report": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:54.485567+00:00", "end": "2026-09-28T12:52:56.742958+00:00"}, "archive": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:57.823277+00:00", "end": "2026-09-28T12:52:58.453906+00:00"}, "create_sample_data": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:35.864954+00:00", "end": "2026-09-28T12:52:37.451156+00:00"}, "extract_products": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:38.920025+00:00", "end": "2026-09-28T12:52:41.149145+00:00"}, "validate": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:47.821696+00:00", "end": "2026-09-28T12:52:49.319829+00:00"}, "simulate_load": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:50.127366+00:00", "end": "2026-09-28T12:52:52.548589+00:00"}}} +{"label": "optional_6_A_csv", "dag": "branching_dag", "run_id": "optional_6_A_csv__1790599969413452767", "state": "success", "expected_state": "success", "tasks": {"process_csv_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:54.817429+00:00", "end": "2026-09-28T12:52:55.475004+00:00"}, "start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:52:49.756524+00:00", "end": "2026-09-28T12:52:49.756537+00:00"}, "process_json_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:52:53.426676+00:00", "end": "2026-09-28T12:52:53.426676+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:51.852609+00:00", "end": "2026-09-28T12:52:53.700004+00:00"}, "review_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:52:53.426676+00:00", "end": "2026-09-28T12:52:53.426676+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:56.749010+00:00", "end": "2026-09-28T12:52:57.301418+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:52:57.406094+00:00", "end": "2026-09-28T12:52:57.406105+00:00"}}} +{"label": "optional_6_A_json", "dag": "branching_dag", "run_id": "optional_6_A_json__1790599981318891965", "state": "success", "expected_state": "success", "tasks": {"process_csv_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:53:04.678219+00:00", "end": "2026-09-28T12:53:04.678219+00:00"}, "review_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:53:04.678219+00:00", "end": "2026-09-28T12:53:04.678219+00:00"}, "start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:53:02.190235+00:00", "end": "2026-09-28T12:53:02.190247+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:03.854653+00:00", "end": "2026-09-28T12:53:04.883167+00:00"}, "process_json_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:05.617301+00:00", "end": "2026-09-28T12:53:06.163931+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:06.846567+00:00", "end": "2026-09-28T12:53:07.431430+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:53:07.527378+00:00", "end": "2026-09-28T12:53:07.527385+00:00"}}} +{"label": "optional_6_A_review", "dag": "branching_dag", "run_id": "optional_6_A_review__1790599950878181492", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:52:31.880500+00:00", "end": "2026-09-28T12:52:31.880513+00:00"}, "process_csv_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:52:34.520952+00:00", "end": "2026-09-28T12:52:34.520952+00:00"}, "process_json_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:52:34.520952+00:00", "end": "2026-09-28T12:52:34.520952+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:33.686169+00:00", "end": "2026-09-28T12:52:34.634325+00:00"}, "review_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:35.883342+00:00", "end": "2026-09-28T12:52:36.335243+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:37.145269+00:00", "end": "2026-09-28T12:52:37.638569+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:52:38.080346+00:00", "end": "2026-09-28T12:52:38.080354+00:00"}}} +{"label": "optional_6_A_xml", "dag": "branching_dag", "run_id": "optional_6_A_xml__1790599960064387143", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:52:40.576461+00:00", "end": "2026-09-28T12:52:40.576475+00:00"}, "process_csv_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:52:42.254102+00:00", "end": "2026-09-28T12:52:42.254102+00:00"}, "process_json_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:52:42.254102+00:00", "end": "2026-09-28T12:52:42.254102+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:41.714877+00:00", "end": "2026-09-28T12:52:42.372355+00:00"}, "review_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:44.072978+00:00", "end": "2026-09-28T12:52:44.611876+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:52:47.349562+00:00", "end": "2026-09-28T12:52:47.349570+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:52:45.532475+00:00", "end": "2026-09-28T12:52:46.387047+00:00"}}} +{"label": "optional_6_B_csv", "dag": "branching_dag", "run_id": "optional_6_B_csv__1790600030982481228", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:53:51.382625+00:00", "end": "2026-09-28T12:53:51.382631+00:00"}, "process_json_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:53:53.983930+00:00", "end": "2026-09-28T12:53:53.983930+00:00"}, "review_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:53:53.983930+00:00", "end": "2026-09-28T12:53:53.983930+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:52.593521+00:00", "end": "2026-09-28T12:53:54.169523+00:00"}, "process_csv_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:55.338055+00:00", "end": "2026-09-28T12:53:57.034412+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:53:58.733441+00:00", "end": "2026-09-28T12:53:58.733456+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:58.016770+00:00", "end": "2026-09-28T12:53:58.536435+00:00"}}} +{"label": "optional_6_B_json", "dag": "branching_dag", "run_id": "optional_6_B_json__1790600043123594630", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:54:04.312564+00:00", "end": "2026-09-28T12:54:04.312573+00:00"}, "process_csv_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:54:07.267935+00:00", "end": "2026-09-28T12:54:07.267935+00:00"}, "review_branch": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:54:07.267935+00:00", "end": "2026-09-28T12:54:07.267935+00:00"}, "choose_format": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:06.715738+00:00", "end": "2026-09-28T12:54:07.404010+00:00"}, "process_json_branch": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:08.623885+00:00", "end": "2026-09-28T12:54:10.327426+00:00"}, "merge_results": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:11.590619+00:00", "end": "2026-09-28T12:54:12.518040+00:00"}, "end_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:54:13.240380+00:00", "end": "2026-09-28T12:54:13.240393+00:00"}}} +{"label": "optional_7_A", "dag": "error_handling_dag", "run_id": "optional_7_A__1790600019449470468", "state": "failed", "expected_state": "failed", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:53:40.237917+00:00", "end": "2026-09-28T12:53:40.237924+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:53:41.595590+00:00", "end": "2026-09-28T12:53:42.183338+00:00"}, "success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:54:50.391456+00:00", "end": "2026-09-28T12:54:50.391456+00:00"}, "failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:54:51.055581+00:00", "end": "2026-09-28T12:54:51.678943+00:00"}, "retry_task": {"state": "failed", "attempts": 4, "start": "2026-09-28T12:54:49.753125+00:00", "end": "2026-09-28T12:54:50.277862+00:00"}}} +{"label": "optional_7_B_ConnectionError", "dag": "error_handling_dag", "run_id": "optional_7_B_ConnectionError__1790600133593914390", "state": "failed", "expected_state": "failed", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:55:34.970055+00:00", "end": "2026-09-28T12:55:34.970064+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:55:36.562237+00:00", "end": "2026-09-28T12:55:37.011257+00:00"}, "retry_task": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:55:47.492118+00:00", "end": "2026-09-28T12:55:47.940804+00:00"}, "success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:55:48.064846+00:00", "end": "2026-09-28T12:55:48.064846+00:00"}, "failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:55:48.712490+00:00", "end": "2026-09-28T12:55:49.197555+00:00"}}} +{"label": "optional_7_B_OSError", "dag": "error_handling_dag", "run_id": "optional_7_B_OSError__1790600252561937372", "state": "failed", "expected_state": "failed", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:57:33.632472+00:00", "end": "2026-09-28T12:57:33.632487+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:34.290109+00:00", "end": "2026-09-28T12:57:34.768072+00:00"}, "retry_task": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:57:45.543523+00:00", "end": "2026-09-28T12:57:46.128267+00:00"}, "success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:57:46.246909+00:00", "end": "2026-09-28T12:57:46.246909+00:00"}, "failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:57:47.071181+00:00", "end": "2026-09-28T12:57:47.715989+00:00"}}} +{"label": "optional_7_B_ValueError", "dag": "error_handling_dag", "run_id": "optional_7_B_ValueError__1790600191959984913", "state": "failed", "expected_state": "failed", "tasks": {"success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:56:45.784424+00:00", "end": "2026-09-28T12:56:45.784424+00:00"}, "failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:56:46.689244+00:00", "end": "2026-09-28T12:56:47.474193+00:00"}, "start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:56:32.295591+00:00", "end": "2026-09-28T12:56:32.295598+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:56:33.808507+00:00", "end": "2026-09-28T12:56:34.327861+00:00"}, "retry_task": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:56:44.934551+00:00", "end": "2026-09-28T12:56:45.531229+00:00"}}} +{"label": "optional_7_B_restore", "dag": "error_handling_dag", "run_id": "optional_7_B_restore__1790600308604083181", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:58:28.905883+00:00", "end": "2026-09-28T12:58:28.905893+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:58:30.479933+00:00", "end": "2026-09-28T12:58:30.955552+00:00"}, "success_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:58:44.841191+00:00", "end": "2026-09-28T12:58:45.414819+00:00"}, "retry_task": {"state": "success", "attempts": 2, "start": "2026-09-28T12:58:42.378837+00:00", "end": "2026-09-28T12:58:43.211009+00:00"}, "failure_handler": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:58:43.325326+00:00", "end": "2026-09-28T12:58:43.325326+00:00"}}} +{"label": "optional_8_A", "dag": "resource_management_dag", "run_id": "optional_8_A__1790600366934777325", "state": "success", "expected_state": "success", "tasks": {"extract.read_customers": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:28.045365+00:00", "end": "2026-09-28T12:59:48.565983+00:00"}, "extract.read_orders": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:49.448034+00:00", "end": "2026-09-28T13:00:10.063529+00:00"}, "calculate_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T13:00:11.157140+00:00", "end": "2026-09-28T13:00:11.708381+00:00"}, "print_total": {"state": "success", "attempts": 1, "start": "2026-09-28T13:00:12.427443+00:00", "end": "2026-09-28T13:00:13.032322+00:00"}, "log_metrics": {"state": "success", "attempts": 1, "start": "2026-09-28T13:00:12.452891+00:00", "end": "2026-09-28T13:00:12.951344+00:00"}}} +{"label": "optional_8_B_delivery_error_failed", "dag": "error_handling_dag", "run_id": "optional_8_B_delivery_error_failed__1790600442808275037", "state": "failed", "expected_state": "failed", "tasks": {"retry_task": {"state": "failed", "attempts": 2, "start": "2026-09-28T13:00:55.863913+00:00", "end": "2026-09-28T13:00:56.344315+00:00"}, "success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T13:00:56.517274+00:00", "end": "2026-09-28T13:00:56.517274+00:00"}, "start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T13:00:43.241323+00:00", "end": "2026-09-28T13:00:43.241338+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T13:00:44.542395+00:00", "end": "2026-09-28T13:00:45.057656+00:00"}, "failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T13:00:58.115826+00:00", "end": "2026-09-28T13:00:58.719339+00:00"}}} +{"label": "optional_8_B_delivery_error_success", "dag": "error_handling_dag", "run_id": "optional_8_B_delivery_error_success__1790600461566675460", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T13:01:02.553513+00:00", "end": "2026-09-28T13:01:02.553524+00:00"}, "success_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T13:01:04.593955+00:00", "end": "2026-09-28T13:01:05.213490+00:00"}, "retry_task": {"state": "success", "attempts": 1, "start": "2026-09-28T13:01:03.364187+00:00", "end": "2026-09-28T13:01:03.838482+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T13:01:03.355462+00:00", "end": "2026-09-28T13:01:03.840328+00:00"}, "failure_handler": {"state": "skipped", "attempts": 0, "start": "2026-09-28T13:01:03.939908+00:00", "end": "2026-09-28T13:01:03.939908+00:00"}}} +{"label": "optional_8_B_failure", "dag": "error_handling_dag", "run_id": "optional_8_B_failure__1790600374167341045", "state": "failed", "expected_state": "failed", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:59:34.633405+00:00", "end": "2026-09-28T12:59:34.633414+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:36.262871+00:00", "end": "2026-09-28T12:59:36.837096+00:00"}, "retry_task": {"state": "failed", "attempts": 2, "start": "2026-09-28T12:59:47.584630+00:00", "end": "2026-09-28T12:59:48.117663+00:00"}, "success_handler": {"state": "upstream_failed", "attempts": 0, "start": "2026-09-28T12:59:48.256654+00:00", "end": "2026-09-28T12:59:48.256654+00:00"}, "failure_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:49.504044+00:00", "end": "2026-09-28T12:59:50.105201+00:00"}}} +{"label": "optional_8_B_restore", "dag": "error_handling_dag", "run_id": "optional_8_B_restore__1790600507926543810", "state": "success", "expected_state": "success", "tasks": {"start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T13:01:49.154391+00:00", "end": "2026-09-28T13:01:49.154407+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T13:01:50.686480+00:00", "end": "2026-09-28T13:01:51.192154+00:00"}, "retry_task": {"state": "success", "attempts": 2, "start": "2026-09-28T13:02:01.992429+00:00", "end": "2026-09-28T13:02:02.590182+00:00"}, "success_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T13:02:03.292271+00:00", "end": "2026-09-28T13:02:03.992153+00:00"}, "failure_handler": {"state": "skipped", "attempts": 0, "start": "2026-09-28T13:02:02.684938+00:00", "end": "2026-09-28T13:02:02.684938+00:00"}}} +{"label": "optional_8_B_success", "dag": "error_handling_dag", "run_id": "optional_8_B_success__1790600393174553780", "state": "success", "expected_state": "success", "tasks": {"success_handler": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:58.041525+00:00", "end": "2026-09-28T12:59:59.107268+00:00"}, "start_task": {"state": "success", "attempts": 0, "start": "2026-09-28T12:59:54.153604+00:00", "end": "2026-09-28T12:59:54.153613+00:00"}, "unreliable_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:54.985172+00:00", "end": "2026-09-28T12:59:55.734497+00:00"}, "retry_task": {"state": "success", "attempts": 1, "start": "2026-09-28T12:59:55.307245+00:00", "end": "2026-09-28T12:59:55.926514+00:00"}, "failure_handler": {"state": "skipped", "attempts": 0, "start": "2026-09-28T12:59:56.121310+00:00", "end": "2026-09-28T12:59:56.121310+00:00"}}} diff --git a/docs/validation/2026-09-28-practice.md b/docs/validation/2026-09-28-practice.md new file mode 100644 index 0000000..1327230 --- /dev/null +++ b/docs/validation/2026-09-28-practice.md @@ -0,0 +1,69 @@ +# Проверка обновленной практики + +Дата: 2026-09-28. Код примеров: коммит `cf1b770`; задания и навигация: `d8dfe83`. Проверка выполнена авторским прогоном по [заданиям](../../educational-tasks.md). Это проверка воспроизводимости инструкций; время и трудность прохождения на студентах не измерялись. + +## Окружение + +Образ собран из Dockerfile стенда. Фактические версии внутри контейнера: Apache Airflow 2.9.2, Python 3.12.4, pandas 2.1.4, mimesis 15.1.0, PostgreSQL provider 5.11.1. Executor: LocalExecutor, PostgreSQL 16. + +Использован отдельный Compose-проект `airflow-manual-practice-check`: собственные новые тома БД и копии `dags/`, `data/`. Порты перенесены на 18089, 15439, 15440. Для автоматизации включен Basic Auth API, `parallelism=4`, интервалы обнаружения файлов сокращены до 5 секунд. Перед запуском измененного DAG проверялся новый разбор файла и выдерживался интервал обновления сериализованного графа. + +Изменения студента выполнялись только в копиях. Внутри блока они сохранялись. Вначале прошли исходные примеры, затем основной маршрут без дополнительных заданий. Дополнительные расширения проверены после него. Независимые блоки выполнялись одновременно; загрузчик и DQ шли последовательно. + +Повторный `docker compose run --rm airflow-init` на существующих томах также завершился с кодом 0: учетная запись сохранена, подключение пересоздано. + +## Исходные примеры + +Все девять DAG загрузились без Import Errors и завершились успешно. Для `resource_management_dag` заранее создан пул из двух слотов. У `error_handling_dag` режим по умолчанию дал ошибку первой попытки и успех второй. У ветвления выбранная ветка успешна, другая пропущена. + +SQL оставил трех студентов, загрузчик создал 1000 заказов, DQ прошел. Файловый пример записал 100 строк. ETL создал шесть заказов с общей суммой 2105, шаг `simulate_load` сообщил об имитации. + +## Основной маршрут + +| Упражнения | Действия и фактический результат | +|---|---| +| 1.1-1.3 | После добавления погоды четыре задачи, после BashOperator пять; `note.txt` содержит `Airflow works`. Расписание изменилось на `0 0 * * 1`, ручной запуск прошел. | +| 2.1-2.2 | Создано отдельное подключение. Неверный Conn Id при нуле повторов: `create_table=failed`, следующие задачи `upstream_failed`. После восстановления и UPDATE: Alice 25, Bob 24, Charlie 21. Повторный полный запуск оставил три строки. | +| 3.1 | Порог High изменен на 80000. Проверены 100 строк и соответствие категории зарплате в CSV того же запуска. | +| 4.1-4.2 | Второй последовательный запуск загрузчика увеличил число заказов до 2000. Добавлена проверка суммы: корректные данные проходят; один отрицательный заказ дает `check_positive_amount=failed`, сводка `upstream_failed`, DAG Run `failed`. Повторный UPDATE восстановил успешный DQ. | +| 5.1 | Фильтр в Extract дал 4 заказа и сумму 2000 в результате Transform. После удаления фильтра снова 6 и 2105. | +| 6.1 | Режимы `csv`, `json`, `xml`, затем `csv`: выбор нужной ветки, отказ при неизвестном формате и успешное восстановление. XCom слияния относится к текущему запуску. | +| 7.1-7.3 | `retry_once`: две попытки и успех; переопределение `retries=0`: окончательный отказ. `fail_first`, `fail_second`, `fail_both`: обработчик ошибки успешен, обработчик успеха `upstream_failed`, DAG Run `failed`. Режим `success` и возврат к `retry_once` успешны. Добавлена запись в лог обработчика успеха. | +| 8.1 | Интервалы выполнения задач с весами 2 и 1 не пересекаются. После замены веса 2 на 1 интервалы пересекаются. | +| 8.2-8.3 | Добавлена метрика `total_rows=50`. После TaskGroup обновлены оба полных task_id, все четыре задачи успешны; XCom сохранил 20, 30 и 50. | + +Основной маршрут завершен без дополнительных упражнений. + +## Дополнительные упражнения + +| Упражнения | Действия и фактический результат | +|---|---| +| 1.A | Два ручных запуска с задержкой 30 секунд и `max_active_runs=1`: задачи второго начались после завершения первого. Затем задержка удалена, DAG выключен. | +| 2.A-2.B | Колонка email добавлена через ALTER до запуска. Три адреса заполнены, отчет содержит 3 студента и средний возраст 23.33; UPDATE из основной практики сохранен. | +| 3.A-3.C | Новые поля проверены, файл опытных сотрудников отделен от полного. Численность отделов в JSON дает 100; сборка трех файлов содержит те же 100 уникальных id. Отдел sales выполнился со второй попытки без дублей. | +| 4.A | При неверном порядке CSV/COPY получено `invalid input syntax for type numeric: "NEW"`. После согласования порядка новые статусы попали в БД и прошел DQ. После удаления поля из кода и таблицы DQ снова успешен; положительная сумма продолжает проверяться. | +| 4.B-4.C | Создание таблицы заменено PostgresOperator, загрузка и DQ успешны. Неверный ожидаемый тип amount при all_success дал upstream_failed у следующих задач. С all_done они выполнились, но сводка и DAG Run остались failed. Возврат numeric и исходных trigger rules восстановил успешный DQ. | +| 5.A-5.C | Справочник продуктов не изменил 6 заказов и сумму 2105. Топ-3: 1200, 400, 300; средний заказ Alice 640. Неверный клиент вызвал отказ проверки и заблокировал архив. После восстановления появился один архив, побайтово равный результату. | +| 6.A-6.B | Третья ветка выбрана для review и неизвестного формата; csv/json продолжают работать. Чтение двух фиксированных файлов дало по 2 записи с суммой 30 в отдельных запусках. | +| 7.A | Три повтора дали четыре неуспешные попытки, после которых DAG Run остался failed. | +| 7.B | ConnectionError, ValueError и OSError вызвали callback после второй попытки; проверены task_id и текст причины. В обеих попытках восстановленного retry_once callback отказа отсутствует. | +| 8.A | После группировки добавлен BashOperator с чтением XCom; в Details видна команда echo 50, в Logs число 50 и успешный выход. | +| 8.B | Локальный SMTP принял одно письмо об отказе retry_task и одно об успехе DAG. При недоступном SMTP неуспешный запуск остался failed, успешный остался success. После отключения почтовых callbacks и возврата retry_once запуск успешен. | + +[Протокол запусков JSONL](2026-09-28-practice-runs.jsonl) содержит 69 контрольных запусков и один диагностический запуск с неверным порядком CSV/COPY. Для каждого сохранены ожидаемый и фактический итог, состояния и число попыток задач, время начала и окончания. В первой строке указаны версии и SHA-256 итоговых DAG. Все ожидаемые состояния сошлись. + +При выполнении уточнены инструкции: одинаковый порядок CSV/COPY; короткая задержка повтора в 3.C и 5.C; расположение Rendered Templates внутри Details; раскрытие TaskGroup нажатием на название со стрелкой. Сообщение callback оператора подтверждено в логе последней попытки. Команда `docker compose logs airflow-scheduler` его не показала. Отправка из callback DAG и ошибка его доставки найдены в `/opt/airflow/logs/scheduler/latest/error_handling_dag.py.log`. + +## Интерфейс и документы + +Chromium: вход под учебной учетной записью, список девяти DAG, запуск кнопкой Trigger DAG, граф с четырьмя прямыми ребрами от рабочих задач к обработчикам. В Logs доступны две попытки; во второй видно `Starting attempt 2 of 2` и сообщение об успехе. В XCom загрузчика найден путь к CSV; тот же CSV виден в Details, раздел Rendered Templates, у задачи загрузки. Часть пути там маскируется как `***`; инструкция уточнена, имя файла сверено с XCom. + +Снимки проверенных состояний: [окончательный отказ](../../_attachments/practice-failed-run.png), [раскрытая TaskGroup](../../_attachments/practice-task-group.png), [путь в Rendered Templates](../../_attachments/practice-rendered-template.png). Группа раскрыта нажатием на название со стрелкой, внутри видны обе задачи. + +Markdown отрисован отдельно: восемь строк маршрута, 34 заголовка упражнений, рабочий переход к блоку ошибок. Проверены локальные ссылки и якоря в измененных документах, синтаксис девяти DAG и `git diff --check`. + +После сохранения протокола изолированные контейнеры, браузер и два тестовых тома удалены. Остальные работающие сервисы не изменялись. + +## Границы проверки + +Почтовый пример проверен на локальном SMTP-приемнике без отправки сообщений третьим лицам. Внешний провайдер, авторизация и TLS не проверялись: их параметры зависят от выбранного студентом сервера. Поведение Windows не проверялось на отдельной Windows-машине; опубликованные команды не используют синтаксис, специфичный для Bash. diff --git a/docs/validation/2026-09-28-theory-review.md b/docs/validation/2026-09-28-theory-review.md new file mode 100644 index 0000000..5ece226 --- /dev/null +++ b/docs/validation/2026-09-28-theory-review.md @@ -0,0 +1,50 @@ +# Перепроверка педагогического ревью + +Дата: 2026-09-28. Исходная версия: `f6b450c`. После независимого ревью замечания проверены по текстам, Compose и исполнением примеров в Apache Airflow 2.9.2. + +## Оценка значимости и решения + +Основные задания используют готовые DAG и правильные параметры учебной БД. Подтвержденные ошибки относятся к примерам теоретических глав: они мешают студенту, который переносит код из главы в стенд. Оценка их как блокировки всей основной практики была бы завышенной. + +| Находка | Что подтвердилось | Решение | +|---|---|---| +| Параметры Connections из другого стенда | В главе 7 предлагались `postgres / airflow / airflow`, хотя Compose и практика используют `postgres-training / training / student` | Исправлены поля формы. Добавлен полный DAG проверки подключения и наблюдаемый результат в БД | +| Задачи без привязки к DAG | Примеры `greeting_dag` и `template_tracking_example` падают при объявлении зависимостей; `template_example` создает DAG без задач | Добавлены привязки и объяснение. Примеры шаблонов дополнены импортами и настройками, чтобы их можно было запускать целиком | +| Неподготовленный итоговый пример главы 9 | Нет исходного CSV и подготовки схем БД, расходятся имена Variable. После подстановки Variable пример падает при создании TaskGroup | Вместо отдельного пайплайна глава направляет к существующему `resource_management_dag.py` и блоку 8. Пример самой TaskGroup сделан самостоятельным; схема согласована с кодом | +| Слишком широкое чтение перед упражнениями | Для XCom перед блоком 4 была указана вся глава 9 | В таблице маршрута даны ссылки на нужные разделы. Callback отмечен как подготовка к дополнительному 7.B | +| Дополнительный опыт с Clear | Нехватка понимания повторов у студентов не подтверждена. Основные задания уже показывают новые запуски и retry; в 4.1 есть объяснение различия повторной загрузки и нового CSV | Обязательная практика сохранена: 16 основных и 18 дополнительных заданий. Опыт с Clear имеет смысл добавлять после наблюдения за студентами, если они путают эти случаи | + +## Воспроизведение исходных ошибок + +Код извлечен из Python-блоков глав на `f6b450c` и исполнен в одноразовом контейнере образа `airflow-manual-practice-check:2.9.2`, ранее собранного из Dockerfile стенда. Версия внутри контейнера: `2.9.2`. + +В неполные примеры главы 7 переданы недостающие импорты и `default_args`, чтобы проверить именно привязку задач. Для итогового примера главы 9 `Variable.get` заменен на тестовое значение строки подключения: это позволило дойти до создания TaskGroup без подготовки посторонних данных. + +| Пример | Результат до правок | +|---|---| +| `greeting_dag` | `AirflowException: Tried to create relationships between tasks that don't have DAGs yet` | +| `template_example` | Разбор без исключения, список задач пуст | +| `template_tracking_example` | То же исключение при создании зависимостей | +| `customer_pipeline_enhanced` | `AirflowException: TaskGroup can only be used inside a dag` | + +## Проверка исправленных примеров + +Полные Python-блоки текущих глав исполнены без дополнений к коду. Затем вызван `DAG.test()` для логической даты `2024-01-02 UTC`; SQL-пример дополнительно запущен для `2024-01-03 UTC`. + +Окружение: тот же образ Airflow 2.9.2, отдельная SQLite для метаданных и отдельная PostgreSQL 16 с базой `training`. Подключение `my_postgres_conn` передано через окружение с полями из инструкции. Контейнеры работали во внутренней сети без опубликованных портов. Каталог репозитория смонтирован только для чтения. Временные контейнеры, сеть и данные удалены после проверки. + +| Пример | Проверенный результат | +|---|---| +| `greeting_dag` | Две успешные задачи; функция приветствия исполнилась | +| `template_example` | Одна успешная задача; результат Bash - `2024-01-02` | +| `template_tracking_example` | Две успешные задачи; результаты Bash - `2024-01-02` и `Processing for 2024-01-02` | +| `connection_example` | Оба запуска успешны; `to_regclass` возвращает `connection_demo_users`, таблица содержит 0 строк | +| `task_group_example` | Все семь задач успешны; проверены полные идентификаторы и шесть зависимостей, показанных на схеме | + +Все шесть DAG Run завершились успешно. Ссылки на файлы и разделы проверены после преобразования Markdown в HTML. Содержание упражнений после таблицы маршрута совпадает с исходной версией. + +## Границы проверки + +`DAG.test()` подтверждает разбор графа, выполнение задач, подстановку шаблонов и работу SQL. Этот прогон не проверяет работу планировщика по расписанию, форму Connections и отображение графов в UI. Основные DAG стенда не менялись; прежний [протокол практики](2026-09-28-practice.md) остается отдельной проверкой их поведения. + +Трудность и время прохождения на студентах не измерялись. Педагогическое предположение о пользе нового опыта с Clear остается гипотезой, а не открытой ошибкой реализации. diff --git a/educational-tasks.md b/educational-tasks.md new file mode 100644 index 0000000..d4296dd --- /dev/null +++ b/educational-tasks.md @@ -0,0 +1,440 @@ +# Практика по Apache Airflow + +Здесь мы знакомимся с Airflow на готовых примерах: запускаем DAG, меняем задачи и смотрим, что произошло. Свой цельный процесс обработки данных вы соберете в курсовых работах дальше по программе. + +## Как проходить практику + +В каждом блоке сначала выполните основную практику. Задания "По желанию" находятся в конце блока: их можно пропустить и перейти дальше. Сложность указана отдельно. Если дополнительному заданию нужны результаты другого упражнения, это написано в его начале. + +Основные задания одного блока выполняйте последовательно, сохраняя изменения. Перед новым блоком прочитайте условия начала. Загрузка CSV и проверки DQ используют одну таблицу, поэтому идут вместе. + +| Блок | Примеры | Что наблюдаем | Перед практикой | +|---|---|---|---| +| [1. Первый DAG](#first-dag) | `hello_world_dag.py` | Задачи, зависимости, операторы, расписание | Главы 4 и 5; для 1.3 - [расписания][schedules] | +| [2. SQL и подключения](#sql) | `sql_basic_dag.py` | Connection, SQL, ошибка подключения | [Подключения][connections] | +| [3. Работа с файлами](#files) | `file_operations_dag.py` | Результаты отдельных задач | Главы 5 и 6 | +| [4. Загрузка и DQ](#load-dq) | `csv_to_postgres.py`, `csv_to_postgres_dq.py` | XCom, данные в БД, ошибка проверки | Глава 6, [шаблоны][templates] и [XCom][xcom] | +| [5. Разбор ETL](#etl) | `data_processing_dag.py` | Параллельные шаги и путь данных | Глава 5 | +| [6. Ветвление](#branching) | `branching_dag.py` | Variables, выбранная ветка, `skipped` | Глава 6 и [Variables][variables] | +| [7. Ошибки и повторы](#errors) | `error_handling_dag.py` | Попытки, обработчики, итог запуска | Глава 6; для 7.B - [callback][alerts] | +| [8. Пулы и группировка](#resources) | `resource_management_dag.py` | Слоты, XCom, TaskGroup | [Пулы][pools], [XCom][xcom] и [TaskGroup][taskgroups] | + +[schedules]: 08%20-%20Управление%20временем.md#настройка-автоматических-запусков +[connections]: 07%20-%20Шаблоны,%20переменные%20и%20подключения%20в%20Airflow.md#централизованные-подключения-connections +[templates]: 07%20-%20Шаблоны,%20переменные%20и%20подключения%20в%20Airflow.md#динамические-шаблоны-airflow-на-основе-jinja +[variables]: 07%20-%20Шаблоны,%20переменные%20и%20подключения%20в%20Airflow.md#безопасные-переменные-variables +[xcom]: 09%20-%20Пулы,%20XCom,%20TaskGroup%20и%20алертинг%20в%20Airflow.md#обмен-данными-между-задачами-xcom-и-контекст-выполнения +[pools]: 09%20-%20Пулы,%20XCom,%20TaskGroup%20и%20алертинг%20в%20Airflow.md#управление-ресурсами-с-помощью-пулов-задач +[taskgroups]: 09%20-%20Пулы,%20XCom,%20TaskGroup%20и%20алертинг%20в%20Airflow.md#группировка-задач-с-помощью-taskgroup +[alerts]: 09%20-%20Пулы,%20XCom,%20TaskGroup%20и%20алертинг%20в%20Airflow.md#система-оповещений-алертинг + +Главы доступны в [оглавлении учебника](README.md). Инструкция запуска и параметры учебной БД находятся в [README стенда](airflow-docker/README.md), файлы примеров - в [airflow-docker/dags/](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 можно открыть консоль учебной БД: + +```bash +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`. + +После запуска проверьте файл: + +```bash +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. + +Проверьте таблицу: + +```sql +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`. Для уже созданной таблицы сначала выполните: + +```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. + +Проверьте результат в БД: + +```sql +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 успешны, сводка перечисляет результаты проверок. + +Для отрицательного сценария поменяйте знак суммы одного заказа: + +```sql +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'`. Существующую таблицу подготовьте один раз: + +```sql +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 не настроен; отсутствие письма здесь не означает ошибку основного маршрута. + +Используйте пример из [раздела об алертинге](09%20-%20Пулы,%20XCom,%20TaskGroup%20и%20алертинг%20в%20Airflow.md#система-оповещений-алертинг). Добавьте отправку письма в callback отказа. Укажите DAG, задачу, идентификатор запуска и причину ошибки. Для успешного DAG Run задайте отдельный `on_success_callback` у `DAG(...)`. + +**Что должно получиться:** после постоянной ошибки приходит сообщение об отказе, после успешного запуска - об успехе. Уведомление не меняет результат обработки данных. Запуск с отказом сохраняет `failed`; ошибку доставки ищите в логах callback. Возвращение к основной практике не требует настройки SMTP: отключите почтовый callback и верните исходный режим ошибок. + +## Когда можно двигаться дальше + +Основная практика закончена, когда вы можете найти свой запуск и его логи, добавить задачу по образцу, объяснить выбранную ветку и повторную попытку, проверить файл или строки в таблице и прочитать XCom. По последнему блоку достаточно показать ожидание слотов и работающую TaskGroup. + +Задания по желанию расширяют эти опыты. Их выполнение не требуется для перехода к следующему блоку программы.