refactor(etl): упрощена проверка готовности Kafka

- Зачем:
  - верхние смещения можно получить тем же административным клиентом без одноразового Consumer и фиктивной группы.
- Что:
  - чтение хвоста переведено на AdminClient.list_offsets с OffsetSpec.latest.
  - назначение system.one названо рядом с запросом, а документация и запись проверок сокращены и переставлены по смыслу.
- Проверка:
  - make config-test; make lint.
  - живые проверки готового и отсутствующего разделов Kafka; полный etl_pipeline.
This commit is contained in:
ddadmin committed 2026-08-27 11:17:43 +03:00
1 parent 12a5c109b2
commit 61e662c19c
4 files changed
+25 -28

No files matched your search

+18 -21
View File
@@ -53,9 +53,9 @@ WORLD_POSITION = "world_position"
def _kafka_readiness() -> tuple[bool, str]: def _kafka_readiness() -> tuple[bool, str]:
"""Получить подтвержденные и верхние смещения обоих разделов событий.""" """Проверить, дочитала ли группа оба раздела событий."""
from confluent_kafka import Consumer, ConsumerGroupTopicPartitions, TopicPartition from confluent_kafka import ConsumerGroupTopicPartitions, TopicPartition
from confluent_kafka.admin import AdminClient from confluent_kafka.admin import AdminClient, OffsetSpec
admin = AdminClient({"bootstrap.servers": KAFKA_BOOTSTRAP_SERVERS}) admin = AdminClient({"bootstrap.servers": KAFKA_BOOTSTRAP_SERVERS})
groups = admin.list_consumer_groups(request_timeout=10).result() groups = admin.list_consumer_groups(request_timeout=10).result()
@@ -78,27 +78,22 @@ def _kafka_readiness() -> tuple[bool, str]:
high_watermarks: dict[int, int] = {} high_watermarks: dict[int, int] = {}
watermark_errors: dict[int, str] = {} watermark_errors: dict[int, str] = {}
consumer = Consumer( # OffsetSpec.latest возвращает следующее смещение после хвоста; сверено
# через Context7 и на установленном confluent-kafka 2.15.0.
latest = admin.list_offsets(
{ {
"bootstrap.servers": KAFKA_BOOTSTRAP_SERVERS, TopicPartition(HITS_TOPIC, partition): OffsetSpec.latest()
# Клиент требует group.id, но без подписки и poll() в группу не for partition in HITS_PARTITIONS
# входит: здесь он только запрашивает верхние смещения топика. },
"group.id": "etl-readiness", request_timeout=10,
"enable.auto.commit": False,
}
) )
for topic_partition, future in latest.items():
try: try:
for partition in HITS_PARTITIONS: offset = future.result().offset
try: if offset >= 0:
bounds = consumer.get_watermark_offsets( high_watermarks[topic_partition.partition] = offset
TopicPartition(HITS_TOPIC, partition), timeout=10
)
if bounds is not None and bounds[1] >= 0:
high_watermarks[partition] = bounds[1]
except Exception as error: except Exception as error:
watermark_errors[partition] = str(error) watermark_errors[topic_partition.partition] = str(error)
finally:
consumer.close()
all_known = group_exists all_known = group_exists
total_lag = 0 total_lag = 0
@@ -139,7 +134,7 @@ def _kafka_readiness() -> tuple[bool, str]:
def _delivery_queue_readiness() -> tuple[bool, str]: def _delivery_queue_readiness() -> tuple[bool, str]:
"""Получить число недоставленных файлов событий на каждой ноде.""" """Проверить, пуста ли очередь доставки событий на каждой ноде."""
query = f""" query = f"""
SELECT SELECT
node, node,
@@ -153,6 +148,8 @@ def _delivery_queue_readiness() -> tuple[bool, str]:
) AS last_exception ) AS last_exception
FROM FROM
( (
-- У пустой очереди нет строк; system.one оставляет обе ноды
-- видимыми и в зеленом опросе.
SELECT SELECT
hostName() AS node, hostName() AS node,
toUInt64(0) AS queued_files, toUInt64(0) AS queued_files,
+4 -4
View File
@@ -16,6 +16,10 @@
`dds_transform`, затем `dm_transform`. Будущее звено DQ встанет после DM. `dds_transform`, затем `dm_transform`. Будущее звено DQ встанет после DM.
Его состав описан в [доке слоя DM](dm.md). Его состав описан в [доке слоя DM](dm.md).
Ждущие вызовы сделаны `TriggerDagRunOperator`. Проверка через Context7 по
ближайшей доступной документации Airflow 3.x (версия 3.1.6) подтвердила, что
оператор опрашивает точный созданный им прогон и краснеет вслед за потомком.
Слойные даги устроены по образцу `orders_ingest`: без расписания, один Слойные даги устроены по образцу `orders_ingest`: без расписания, один
активный прогон, запускаются триггером конвейера или рукой при разборе активный прогон, запускаются триггером конвейера или рукой при разборе
происшествий. Витрины внутри `dm_transform` — отдельные задачи. происшествий. Витрины внутри `dm_transform` — отдельные задачи.
@@ -98,10 +102,6 @@ ODS и ещё не собранный в DDS, затем при необходи
подтвердить сообщение раньше, чем Distributed-таблица доставила файл на шард. подтвердить сообщение раньше, чем Distributed-таблица доставила файл на шард.
Два барьера закрывают обе части пути до того, как сессии выберут свой объем. Два барьера закрывают обе части пути до того, как сессии выберут свой объем.
Ждущие вызовы сделаны `TriggerDagRunOperator`. Проверка через Context7 по
ближайшей доступной документации Airflow 3.x (версия 3.1.6) подтвердила, что
оператор опрашивает точный созданный им прогон и краснеет вслед за потомком.
## Идемпотентность ## Идемпотентность
Партиция не заменяется простой вставкой. День собирается в таблицу-двойник — Партиция не заменяется простой вставкой. День собирается в таблицу-двойник —
+2 -1
View File
@@ -240,7 +240,8 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
**Приемка #156 27 августа 2026 года.** `make rebuild-storage` занял 7 минут. **Приемка #156 27 августа 2026 года.** `make rebuild-storage` занял 7 минут.
На чистом прикладном мире `world_initialize` сам довел позицию до `8`, дождался На чистом прикладном мире `world_initialize` сам довел позицию до `8`, дождался
`etl_pipeline` и оставил заполненными DDS и обе витрины. После пересборки прошли `etl_pipeline` и оставил заполненными DDS и обе витрины. После пересборки прошли
`make smoke` 21/21 и `make check-clickhouse` 10/10. `make smoke` 21/21 и `make check-clickhouse` 10/10. Перед стендовыми сценариями
прошли `make config-test` и `make lint`.
Два прогона `world_next_day` не изменили слои; один ручной `etl_pipeline` без Два прогона `world_next_day` не изменили слои; один ручной `etl_pipeline` без
параметров догнал оба дня. Полный прогон `world_live_day` занял 27 м 2 с и параметров догнал оба дня. Полный прогон `world_live_day` занял 27 м 2 с и
+1 -2
View File
@@ -227,8 +227,7 @@ ODS истинно сразу — этим оно и ограничено, об
мир уже есть. мир уже есть.
- **`make rebuild-storage`** возвращает канонический мир: позиция проходит - **`make rebuild-storage`** возвращает канонический мир: позиция проходит
`-1` → `0` → `WORLD_STARTING_DAYS`, в ODS ровно даты описи, топики созданы `-1` → `0` → `WORLD_STARTING_DAYS`, в ODS ровно даты описи, топики созданы
заново, офсеты групп идут с нуля, DDS и DM собраны. Время прогона измеряется заново, офсеты групп идут с нуля, DDS и DM собраны.
заново.
- **Красные пути проверяются своими отказами.** Сторож работника при - **Красные пути проверяются своими отказами.** Сторож работника при
отсутствующей, нулевой и отрицательной позиции; `world_initialize` на `-1`; отсутствующей, нулевой и отрицательной позиции; `world_initialize` на `-1`;
оборванная очистка, оставляющая `-1`. Проверка, не видевшая своей поломки, оборванная очистка, оставляющая `-1`. Проверка, не видевшая своей поломки,