Зачем: - связать проигрывание модельных дней с пакетным приёмом заказов - показать на одном стенде различие потокового push и пакетного pull Что: - добавлен топик, Kafka-чтец и реплицированное сырьё заказов - добавлен даг orders_ingest с одним прямым чтением и идентификатором загрузки - работники мира отправляют слепок, ждут приём и только затем двигают позицию - решения, границы отказа и проверки отражены в ADR и архитектурных документах Проверка: - make lint - make config-test - make smoke - make check-clickhouse - make check-services
101 lines
4.7 KiB
Python
101 lines
4.7 KiB
Python
"""Приём заказов: пакетный забор слепка из 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()
|