fix(stand): находки ревью — диагноз не утверждает причину, комментарии не врут

Зачем
Холодное ревью нашло три места, где написанное сильнее сделанного.

Что
- Непустая таблица брака больше не выдаётся за доказательство сломанного
  разбора. Модельного дня у брака нет, обрамить его нечем, и строки прежних
  уроков лежат в нём месяц: после первого же урока с мусором проверка
  давала бы неверный диагноз навсегда. Теперь она даёт признак, по которому
  причину отличают, — сошлась недостача с числом брака или нет.
- Комментарий у world-init обещал, что расхождение числа дней с
  STARTING_DAYS поймают счётчики. Это неправда в одну сторону: лишний день
  ложится за рамкой дат описи. Обещание убрано, дыра названа.
- Довод «даг next_day этапа 5 продолжит ось» опирался на несуществующий
  этап; заменён настоящей причиной — заливка замыкает цепь разовых служб.
- Потолок ожидания 300 с получил обоснование замером с кратностью, а сам
  скрипт — честную оговорку: его обещание работает на пустом стенде, на
  живом ждать нечего.
- Третья, пропущенная ссылка на снятый порог скорости дня убрана из спеки.
- Даты замеров в карте целей разведены: #42 менял три цели, а не шесть.

Проверка
Обе ветви диагноза сняты заново на живом стенде: без брака — «не доехали»,
с браком — признак различения. Стенд восстановлен, все 9 проверок зелёные,
брака 0. make lint, typecheck, config-test, test (407 тестов) зелёные.

Ссылка: #42
This commit is contained in:
2026-08-07 18:47:13 +03:00
parent 7c9eeedc40
commit c696fce40b
5 changed files with 36 additions and 21 deletions
+6 -7
View File
@@ -251,8 +251,9 @@ services:
# Что это за дни и каким мир обязан выйти — data/world-inventory.json.
#
# Число дней стоит здесь числом: YAML не читает Python, и одно из двух мест
# (второе — `STARTING_DAYS` в inventory.py) лишнее по построению. Разъедутся
# они — покраснеют счётчики `make check-clickhouse`.
# (второе — `STARTING_DAYS` в inventory.py) лишнее по построению. Правя одно,
# правьте второе — на страже тут никто не стоит: залей эта служба лишний
# день, он лёг бы за рамкой дат описи и остался бы незамеченным.
#
# Повторный `make up` заливает мир заново, и это не оплошность: `WatchID` у
# событий те же, ReplacingMergeTree схлопнет повтор в ODS. Сырьё в STG при
@@ -309,11 +310,9 @@ services:
# упавшим для --wait.
clickhouse-init:
condition: service_completed_successfully
# По той же причине — и ещё по одной. Заливка стартового мира идёт
# последней в цепи разовых служб, и зависимого ей взять больше негде.
# Заодно это правда про порядок: даг `next_day` этапа 5 продолжает ось с
# того дня, на котором заливка остановилась, — Airflow приходит в мир,
# который уже есть.
# По той же причине. Заливка стартового мира идёт последней в цепи
# разовых служб, и зависимого ей взять больше негде: долгоживущие службы
# данных не ждут, а `airflow-init` эту цепь и так замыкает.
world-init:
condition: service_completed_successfully
entrypoint: ["/bin/bash"]
+10 -7
View File
@@ -29,8 +29,9 @@
## Карта целей
Стенд нужен трём целям из семи. Цена — замер 7 августа 2026 года, см. «Что
проверено».
Стенд нужен трём целям из семи. Цена — замер, см. «Что проверено»: `make test`,
`make smoke` и `make check-clickhouse` перемерены 7 августа 2026 года, их #42
менял; остальные три стоят с замера 6 августа.
| Цель | Что утверждает | Стенд | Цена |
|---|---|---|---|
@@ -204,11 +205,13 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
Что новая проверка **умеет краснеть**, снято двумя поломками того же дня, и
проверялись обе ветви её диагноза. Снесли партицию `2026-06-03` в
`ods.event_rep` — проверка покраснела, показала недостающий день и назвала
адрес: «события не доехали до ODS, начните с чтеца топика». Затем положили в
сырьё заведомо негодную строку — брак появился, и проверка сменила диагноз на
«сломан разбор, начните с `ods.event_errors_dist`». Строки опыта убраны, день
переигран `make generate-batch GENERATOR_DAY=2`; счёт вернулся к 401 185, и
это заодно показало дедупликацию: повтор дня не удвоил счёт под `FINAL`.
адрес: «таблица брака пуста, события не доехали до ODS, начните с чтеца
топика». Затем положили в сырьё заведомо негодную строку — брак появился, и
диагноз сменился на второй, с признаком для различения: «сойдётся недостача с
числом брака — сломан разбор, не сойдётся — брак от прежних опытов». Строки
опыта убраны, день переигран `make generate-batch GENERATOR_DAY=2`; счёт
вернулся к 401 185, и это заодно показало дедупликацию: повтор дня не удвоил
счёт под `FINAL`.
Замеры 6 августа 2026 года, стенд поднят заранее. Время взято по `time` и
совпадает с тем, что цель печатает сама. Оно плавает от прогона к прогону:
+2 -3
View File
@@ -888,9 +888,8 @@
пока число правдоподобно, само по себе оно ничего не сторожит, и
подгонять поведение под его край не надо. Настоящий предел здесь другой и
считается в другой валюте — дневной бюджет событий: события корзины
входят в те самые «около 50 тыс.», на которых стоят опись (#42) и
порог скорости дня. Упрётся будущая правка — двигать надо бюджет и его
причины, а не долю.
входят в те самые «около 50 тыс.», на которых стоит опись (#42). Упрётся
будущая правка — двигать надо бюджет и его причины, а не долю.
Решено при исполнении #41 (2026-08-07). Первые два пункта — решения владельца
грилингом 7 августа 2026 года, внесённые как есть; остальные приняты при
+9 -4
View File
@@ -94,6 +94,11 @@ on_signal() {
# Таблица брака здесь не второе утверждение, а объяснение первого. Утверждай мы
# «брака нет», проверка краснела бы навсегда после первого же урока, где менти
# нарочно отправил в топик мусор, — и краснела бы не о том.
#
# По той же причине непустой брак сам по себе ничего не доказывает: модельного
# дня у брака нет, обрамить его нечем, и строки прежних уроков лежат в нём
# месяц. Поэтому объяснение не утверждает причину, а даёт признак, по которому
# её отличают: сошлась недостача с числом брака — разбор, не сошлась — доставка.
check_starting_world() {
local expected actual broken first_date last_date
@@ -122,11 +127,11 @@ check_starting_world() {
FORMAT TSV")"
printf 'Опись мира ожидает (дата, событий):\n%s\n' "$expected" >&2
printf 'В ods.event лежит:\n%s\n' "${actual:-— ничего —}" >&2
if [[ -n "$broken" ]]; then
printf 'В ods.event_errors по классам брака:\n%s\n' "$broken" >&2
fail 'счёт разошёлся с описью, и в таблице брака есть строки: сломан разбор — начните с ods.event_errors_dist и матвью ods.event_mv'
fi
if [[ -z "$broken" ]]; then
fail 'счёт разошёлся с описью, а таблица брака пуста: события не доехали до ODS — начните с чтеца топика stg.hits_raw_kafka и матвью приёма stg.hits_raw_mv'
fi
printf 'В ods.event_errors по классам брака:\n%s\n' "$broken" >&2
fail 'счёт разошёлся с описью, и в таблице брака есть строки. Сойдётся недостача с их числом — сломан разбор, смотрите матвью ods.event_mv; не сойдётся — брак остался от прежних опытов, а события не доехали: смотрите чтеца топика stg.hits_raw_kafka'
}
assert_ddl_queue_completed() {
+9
View File
@@ -15,10 +15,19 @@
# брака. Сломайся разбор — события уедут в `*_errors`, сумма сойдётся, ожидание
# кончится, и поломку назовёт `make check-clickhouse`. Ждать здесь одних годных
# событий значило бы висеть пять минут вместо внятного ответа.
#
# Обещание у скрипта ровно одно и только на пустом стенде: «мир доехал». На
# живом стенде мир уже лежит в ODS, условие истинно на первой же попытке, и
# ждать тут нечего — повторная заливка ничего не добавляет, её схлопнет
# дедупликация. Утверждать после неё «доехала именно эта заливка» скрипт не
# может и не пытается: у события нет отметки, которым прогоном оно приехало.
set -Eeuo pipefail
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly INVENTORY="${ROOT_DIR}/data/world-inventory.json"
# Потолок ожидания — 300 с: больше чем вдесятеро от замеренных 22,5 с, за
# которые заливка отдаёт восемь дней в Kafka (карта проверок, «Что проверено»).
# Это не бюджет, а предел, за которым ждать бессмысленно и надо читать журнал.
readonly ATTEMPTS=100
readonly PAUSE_SECONDS=3
# Как часто отчитываться о ходе: молчащая минуту команда выглядит зависшей.