Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d96097d50 | ||
|
|
26730c6483 | ||
|
|
f6b450c922 | ||
|
|
1277bff690 | ||
|
|
d8dfe83651 | ||
|
|
cf1b770a65 | ||
|
|
623d5861c1 |
No files matched your search
@@ -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 предоставляет множество готовых операторов:
|
||||
|
||||
@@ -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,60 +142,47 @@ 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
|
||||
|
||||
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 users(
|
||||
CREATE TABLE IF NOT EXISTS public.connection_demo_users (
|
||||
user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL
|
||||
);
|
||||
@@ -196,7 +191,13 @@ create_table = PostgresOperator(
|
||||
)
|
||||
```
|
||||
|
||||
Для успешной работы с внешними системами сначала необходимо создать соответствующее подключение, а затем использовать его идентификатор в операторах вашего 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).
|
||||
|
||||
@@ -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
|
||||
from datetime import datetime
|
||||
|
||||
from airflow import DAG
|
||||
from airflow.operators.empty import EmptyOperator
|
||||
from airflow.utils.task_group import TaskGroup
|
||||
|
||||
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 = DummyOperator(task_id="extract_source_1")
|
||||
extract_2 = DummyOperator(task_id="extract_source_2")
|
||||
extract_3 = DummyOperator(task_id="extract_source_3")
|
||||
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
|
||||
|
||||
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 = 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 = DummyOperator(task_id="load_to_target_1")
|
||||
load_2 = DummyOperator(task_id="load_to_target_2")
|
||||
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"""
|
||||
Привет, <br>
|
||||
Я закончил работу над расчётом за "{calculation_dt}". Это заняло {duration} часов/минут/секунд.<br>
|
||||
Логи и запуски тоже можно посмотреть <a href="http://airflow-monitoring.example.com/tree?dag_id=daily_guests_features">тут</a>.
|
||||
<br>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
Навеки твой,<br>
|
||||
Airflow бот <br>
|
||||
"""
|
||||
|
||||
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],
|
||||
def notify_failure(context):
|
||||
ti = context["ti"]
|
||||
send_email_smtp(
|
||||
to=MAIL_TO,
|
||||
subject=f"Ошибка {ti.dag_id}: {ti.task_id}",
|
||||
html_content=(
|
||||
f"<p>Запуск: {escape(context['run_id'])}</p>"
|
||||
f"<p>Причина: {escape(str(context['exception']))}</p>"
|
||||
f'<p><a href="{escape(ti.log_url, quote=True)}">Лог задачи</a></p>'
|
||||
),
|
||||
)
|
||||
|
||||
... >> 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,
|
||||
def notify_success(context):
|
||||
send_email_smtp(
|
||||
to=MAIL_TO,
|
||||
subject=f"DAG {context['dag'].dag_id} завершен успешно",
|
||||
html_content=f"<p>Запуск: {escape(context['run_id'])}</p>",
|
||||
)
|
||||
```
|
||||
|
||||
Теперь разобьем задачи на логические группы и добавим Jinja-шаблоны для доступа к контексту выполнения:
|
||||
Для примера с ошибками задайте `on_failure_callback=notify_failure` у двух рабочих задач, а `on_success_callback=notify_success` у самого `DAG(...)`. Если упадут обе задачи, придут два сообщения об отказе. Сообщение об успехе отправится только после успешного DAG Run. Не включайте одновременно `email_on_failure=True`, если не хотите получать еще и встроенное письмо о той же ошибке.
|
||||
|
||||
В этом примере создается начальная задача 'pipeline_start', которая использует Jinja-шаблоны для вывода информации о запуске DAG, включая идентификатор запуска (run_id) и информацию о DAG Run. Затем задачи группируются в логическую группу 'data_processing_stage', что улучшает структуру и читаемость 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
|
||||
# Начальная задача с информацией о запуске
|
||||
start_task = BashOperator(
|
||||
task_id='pipeline_start',
|
||||
bash_command='echo "Pipeline started! Run ID: {{ run_id }} | DAG Run: {{ dag_run }}"',
|
||||
dag=dag,
|
||||
)
|
||||
Проверка: сначала воспроизведите окончательный отказ, затем успешный запуск. Сверьте текст каждого письма с состоянием соответствующего DAG Run. Упражнения находятся в [блоке ошибок](educational-tasks.md#errors) и [задании на почту](educational-tasks.md#resources).
|
||||
|
||||
# Группа задач по предварительной обработке данных
|
||||
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
|
||||
```
|
||||
Откройте [resource_management_dag.py](airflow-docker/dags/resource_management_dag.py). Он имитирует чтение двух источников и передает небольшие результаты через XCom. Для запуска нужен пул `training_pool` с двумя слотами; создайте его в Admin > Pools.
|
||||
|
||||
Поскольку последовательность задач внутри групп указывается при их создании, в конце необходимо определить порядок выполнения самих групп, чтобы планировщик понимал общую логику выполнения.
|
||||
В исходном DAG задачи `read_customers` и `read_orders` не зависят друг от друга, но занимают два и один слот соответственно. Поэтому они выполняются последовательно. `calculate_metrics` получает их результаты через `xcom_pull`, а `log_metrics` пишет в лог словарь `customers=20`, `orders=30`.
|
||||
|
||||
Итоговый DAG будет выглядеть следующим образом:
|
||||
В [заданиях 8.1-8.3](educational-tasks.md#resources) вы измените вес задачи в пуле, добавите метрику и объедините задачи чтения в TaskGroup. Там описаны изменения, ожидаемые результаты и места проверки. Файлы CSV и подключение к БД для этого примера не нужны: чтение источников имитируется.
|
||||
|
||||
```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,
|
||||
)
|
||||
|
||||
# Начальная задача
|
||||
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). Отправка настоящей почты остается заданием по желанию.
|
||||
@@ -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`
|
||||
|
||||
@@ -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.*
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
+71
-239
@@ -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["Каталог<br/>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["Пользователь<br/>(браузер)"]
|
||||
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) сохраняет согласованные решения, разбор исходных заданий и результаты проверок.
|
||||
@@ -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`: достаточно одного окончательного отказа, ждать завершения другой рабочей задачи ему не обязательно.
|
||||
@@ -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
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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
|
||||
task_id="process_csv_branch", python_callable=process_csv_data,
|
||||
)
|
||||
|
||||
process_json_task = PythonOperator(
|
||||
task_id='process_json_branch',
|
||||
python_callable=process_json_data,
|
||||
dag=dag
|
||||
task_id="process_json_branch", python_callable=process_json_data,
|
||||
)
|
||||
|
||||
merge_task = PythonOperator(
|
||||
task_id='merge_results',
|
||||
task_id="merge_results",
|
||||
python_callable=merge_results,
|
||||
trigger_rule='none_failed_or_skipped', # Выполняется, когда одна из веток завершена
|
||||
dag=dag
|
||||
trigger_rule="none_failed_min_one_success",
|
||||
)
|
||||
end_task = EmptyOperator(task_id="end_task")
|
||||
|
||||
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]
|
||||
start_task >> choose_task >> [process_csv_task, process_json_task]
|
||||
[process_csv_task, process_json_task] >> merge_task >> end_task
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
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=unreliable_task,
|
||||
dag=dag
|
||||
task_id="unreliable_task", python_callable=run_first_task,
|
||||
)
|
||||
|
||||
retry_task = PythonOperator(
|
||||
task_id='retry_task',
|
||||
python_callable=retry_task,
|
||||
dag=dag
|
||||
task_id="retry_task", python_callable=run_retry_task,
|
||||
)
|
||||
|
||||
success_handler_task = PythonOperator(
|
||||
task_id='success_handler',
|
||||
python_callable=success_handler,
|
||||
trigger_rule='all_success', # Выполняется только если все предыдущие задачи успешны
|
||||
dag=dag
|
||||
task_id="success_handler",
|
||||
python_callable=handle_success,
|
||||
trigger_rule="all_success",
|
||||
retries=0,
|
||||
)
|
||||
|
||||
failure_handler_task = PythonOperator(
|
||||
task_id='failure_handler',
|
||||
python_callable=failure_handler,
|
||||
trigger_rule='one_failed', # Выполняется если хотя бы одна предыдущая задача завершилась с ошибкой
|
||||
dag=dag
|
||||
task_id="failure_handler",
|
||||
python_callable=handle_failure,
|
||||
trigger_rule="one_failed",
|
||||
retries=0,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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!
|
||||
@@ -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.
|
||||
@@ -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"}}}
|
||||
@@ -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.
|
||||
@@ -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 остается гипотезой, а не открытой ошибкой реализации.
|
||||
@@ -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 выбрать нужную.
|
||||
|
||||
<a id="first-dag"></a>
|
||||
|
||||
## 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; ограничение можно оставить.
|
||||
|
||||
<a id="sql"></a>
|
||||
|
||||
## 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`.
|
||||
|
||||
<a id="files"></a>
|
||||
|
||||
## 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`. В графе три задачи имеют общего предшественника, но не зависят друг от друга. Повтор не добавляет дубликаты: файл отдела перезаписывается.
|
||||
|
||||
<a id="load-dq"></a>
|
||||
|
||||
## 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-запуск.
|
||||
|
||||
<a id="etl"></a>
|
||||
|
||||
## 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 этой проверки находится в логе ее последней попытки.
|
||||
|
||||
<a id="branching"></a>
|
||||
|
||||
## 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. Сравните их логи вручную. В каждом запуске работает одна ветка; файл результата другой ветки из старого запуска для сравнения не нужен.
|
||||
|
||||
<a id="errors"></a>
|
||||
|
||||
## 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` перед переходом дальше.
|
||||
|
||||
<a id="resources"></a>
|
||||
|
||||
## 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.
|
||||
|
||||
Задания по желанию расширяют эти опыты. Их выполнение не требуется для перехода к следующему блоку программы.
|
||||
Reference in new issue
Block a user