Files
clickstream-data-platform/dags/orders_ingest.py
T
ddadmin 6bd9190142 feat(orders): добавить приём слепков в STG
Зачем:
- связать проигрывание модельных дней с пакетным приёмом заказов
- показать на одном стенде различие потокового push и пакетного pull

Что:
- добавлен топик, Kafka-чтец и реплицированное сырьё заказов
- добавлен даг orders_ingest с одним прямым чтением и идентификатором загрузки
- работники мира отправляют слепок, ждут приём и только затем двигают позицию
- решения, границы отказа и проверки отражены в ADR и архитектурных документах

Проверка:
- make lint
- make config-test
- make smoke
- make check-clickhouse
- make check-services
2026-08-18 21:33:13 +03:00

101 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Приём заказов: пакетный забор слепка из Kafka в хранилище.
Путь Kafka → STG → ODS у заказов один и живёт одним дагом; здесь его первый
шаг — забор. Расписания у дага нет: его дёргает тот, кто положил слепок в
топик, — работник пульта мира после проигрыша дня, — и ждёт конца.
Контраст с приёмом событий и есть урок. События тянет матвью, навсегда
подписанная на чтеца: приём идёт сам, пока идёт поток. Слепок заказов
приезжает раз в модельный день целой выгрузкой, у которой есть начало и конец,
и забирает её запрос по команде. Push против pull — два режима на одном стенде,
каждый там, где ему место по природе источника.
Решения и доводы целиком — ADR 0008; форма перехода STG → ODS, который
прирастёт сюда вторым шагом, — docs/architecture/orders/ingestion.md.
"""
from __future__ import annotations
import datetime
import logging
from airflow.sdk import Connection, dag, get_current_context, task
START_DATE = datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC)
# Забор — один прямой SELECT, без цикла до пустоты: одна порция ClickHouse
# берёт десятки тысяч сообщений, а слепок дня — порядка полутора тысяч строк.
# Короткая порция оставит хвост до следующего прогона, а отказ после чтения
# унесёт прочитанное с собой: офсеты коммитятся в момент чтения. Граница
# целиком — ADR 0008, «Следствия».
#
# stream_like_engine_allow_direct_select разрешает читать чтеца запросом; вторая
# половина пары объявлена на самой таблице (sql/ddl/10-stg-tables.sql).
# distributed_foreground_insert = 1 — конвенция ETL-вставок стенда: задача не
# должна зеленеть раньше, чем строки легли на шарды.
TAKE_ONE_BATCH = """
INSERT INTO stg.orders_raw_dist
SELECT
raw,
_topic AS kafka_topic,
_partition AS kafka_partition,
_offset AS kafka_offset,
_timestamp_ms AS kafka_timestamp,
hostName() AS consumer_host,
{load_id:String} AS _load_id,
now64(3) AS _load_ts
FROM stg.orders_raw_kafka
SETTINGS
stream_like_engine_allow_direct_select = 1,
distributed_foreground_insert = 1
"""
@dag(
dag_id="orders_ingest",
schedule=None,
start_date=START_DATE,
is_paused_upon_creation=False,
# Чтец у топика один, и группа потребителей у него одна. Два прогона разом
# дрались бы за неё, а слепок разъехался бы по двум _load_id; второй
# прогон подождёт своей очереди.
max_active_runs=1,
tags=["заказы"],
)
def orders_ingest():
"""Забрать приехавший слепок заказов из топика в сырьё."""
@task
def pull_batch() -> None:
# Импорт внутри задачи: обработчик DAG разбирает этот файл снова и
# снова, и импорт наверху оплачивался бы каждым разбором.
import clickhouse_connect
load_id = get_current_context()["run_id"]
connection = Connection.get("clickhouse_default")
client = clickhouse_connect.get_client(
host=connection.host,
port=connection.port,
username=connection.login,
password=connection.password,
database=connection.schema or "default",
connect_timeout=5,
send_receive_timeout=30,
)
try:
summary = client.command(TAKE_ONE_BATCH, parameters={"load_id": load_id})
finally:
client.close()
# Размер порции — read_rows: written_rows у вставки в Distributed
# считает не приехавшее.
logging.info(
"порция принята: строк %s, _load_id %s",
summary.summary["read_rows"],
load_id,
)
pull_batch()
orders_ingest()