feat(dm): добавлены витрины выручки и трафика
Зачем: - Показать два ритма пересборки агрегатов на готовых витринах DM. Что: - Добавлены физические модели и публичные договоры revenue_daily и daily_traffic. - Добавлен dm_transform с оконной заменой выручки и полным пересчетом трафика. - Отрицательный опыт локального соединения через публичные _v зафиксирован; выбран явный GLOBAL LEFT JOIN. - Замены партиций сериализованы после подтвержденного OOM при трех одновременных задачах. - Обновлены архитектурные документы и мастер-спека. Проверка: - make lint, make config-test, make smoke, make check-clickhouse, make check-services. - Чистая сборка world_recreate, dds_transform и dm_transform; меры сверены с прямыми агрегатами.
This commit is contained in:
1 parent
c9bcacd63b
commit
da0477f0f1
12 files changed
+436
-25
No files matched your search
@@ -0,0 +1,128 @@
|
||||
"""Трансформации слоя DM: сборка стендовых витрин.
|
||||
|
||||
Выручка наследует ритм заказов и заменяет дневные партиции изменяемого окна.
|
||||
Трафик наследует ретроспективность карты идентичностей и пересчитывает всю
|
||||
историю новой версией. Объем каждого прогона выводится из состояния слоев.
|
||||
|
||||
Форма DAG и динамическая замена сверены с публичным API Airflow 3 через
|
||||
Context7; DDL и REPLACE PARTITION — с документацией ClickHouse.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from airflow.exceptions import AirflowException
|
||||
from airflow.providers.clickhousedb.hooks.clickhouse import ClickHouseHook
|
||||
from airflow.providers.common.sql.operators.sql import SQLExecuteQueryOperator
|
||||
from airflow.sdk import dag, task, task_group
|
||||
|
||||
START_DATE = datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC)
|
||||
|
||||
SQL_ROOT = Path("/opt/airflow/sql")
|
||||
CLICKHOUSE_CONNECTION = "clickhouse_default"
|
||||
|
||||
|
||||
def _days(first_day: datetime.date, last_day: datetime.date) -> list[str]:
|
||||
"""Дни замены строками от левой до правой границы включительно."""
|
||||
return [
|
||||
(first_day + datetime.timedelta(days=shift)).isoformat()
|
||||
for shift in range((last_day - first_day).days + 1)
|
||||
]
|
||||
|
||||
|
||||
@task
|
||||
def replacements(scope: dict[str, object]) -> list[dict[str, object]]:
|
||||
"""Разложить дни в аргументы размноженной задачи замены.
|
||||
|
||||
Отдельный словарь нужен каждому экземпляру ``expand_kwargs``; тот же
|
||||
прием разобран в ``dds_transform.py`` на замене партиций заказов.
|
||||
"""
|
||||
return [{"params": {"day": day}} for day in scope["days"]]
|
||||
|
||||
|
||||
@dag(
|
||||
dag_id="dm_transform",
|
||||
schedule=None,
|
||||
start_date=START_DATE,
|
||||
is_paused_upon_creation=False,
|
||||
max_active_runs=1,
|
||||
max_active_tasks=3,
|
||||
template_searchpath=str(SQL_ROOT),
|
||||
tags=["dm"],
|
||||
)
|
||||
def dm_transform():
|
||||
"""Пересобрать стендовые витрины по текущему состоянию DDS."""
|
||||
|
||||
@task_group(group_id="revenue_daily")
|
||||
def revenue_daily():
|
||||
"""Выручка: изменяемое окно заказов и пропущенная история."""
|
||||
|
||||
@task
|
||||
def scope() -> dict[str, object]:
|
||||
"""Спросить у хранилища, какие дни пересобирает запуск."""
|
||||
query = (SQL_ROOT / "dm" / "revenue_daily_scope.sql").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
hook = ClickHouseHook(clickhouse_conn_id=CLICKHOUSE_CONNECTION)
|
||||
row = hook.get_first(query)
|
||||
if row is None:
|
||||
raise AirflowException(
|
||||
"нет слепка заказов в ODS или готовых заказов в DDS"
|
||||
)
|
||||
first_day, last_day = row
|
||||
days = _days(first_day, last_day)
|
||||
logging.info(
|
||||
"объем пересборки: %s … %s, дней %d",
|
||||
first_day,
|
||||
last_day,
|
||||
len(days),
|
||||
)
|
||||
return {
|
||||
"first_day": first_day.isoformat(),
|
||||
"last_day": last_day.isoformat(),
|
||||
"days": days,
|
||||
}
|
||||
|
||||
days = scope()
|
||||
|
||||
rebuild = SQLExecuteQueryOperator(
|
||||
task_id="rebuild",
|
||||
conn_id=CLICKHOUSE_CONNECTION,
|
||||
sql="dm/revenue_daily_rebuild.sql",
|
||||
parameters={
|
||||
"load_id": "{{ run_id }}",
|
||||
"first_day": days["first_day"],
|
||||
"last_day": days["last_day"],
|
||||
},
|
||||
split_statements=True,
|
||||
do_xcom_push=False,
|
||||
)
|
||||
|
||||
replace = SQLExecuteQueryOperator.partial(
|
||||
task_id="replace",
|
||||
conn_id=CLICKHOUSE_CONNECTION,
|
||||
sql="dm/revenue_daily_replace.sql",
|
||||
do_xcom_push=False,
|
||||
# Каждая замена поднимает отдельный процесс работника. Три
|
||||
# одновременные задачи выбили его по памяти на первом прогоне;
|
||||
# самим дням параллельность ничего не дает.
|
||||
max_active_tis_per_dag=1,
|
||||
).expand_kwargs(replacements(days))
|
||||
|
||||
rebuild >> replace
|
||||
|
||||
revenue_daily()
|
||||
|
||||
SQLExecuteQueryOperator(
|
||||
task_id="daily_traffic",
|
||||
conn_id=CLICKHOUSE_CONNECTION,
|
||||
sql="dm/daily_traffic_rebuild.sql",
|
||||
parameters={"load_id": "{{ run_id }}"},
|
||||
do_xcom_push=False,
|
||||
)
|
||||
|
||||
|
||||
dm_transform()
|
||||
Reference in new issue
Block a user