feat(stand): make up наполняет стенд стартовым миром, опись сторожит его
Зачем Стенд поднимался пустым, и всякая приёмка следующих этапов начиналась с ручной заливки данных. Теперь `make up` сам приводит стенд к одному и тому же состоянию, а в git лежит то, чем это состояние проверяется. Что - Опись мира `data/world-inventory.json`: паспорт (зерно, версия генератора, хеш каталога) и по строке на каждый из восьми дней — дата, число событий, хеш байтов. Собирается `make inventory`, свежесть сторожит `test_inventory.py` — тем же способом, что свежесть описания выгрузки. - Разовая служба `world-init` вышла из-под профиля и играет в топик восемь дней при каждом подъёме; зависимый у неё — `airflow-init`, иначе `--wait` считает успешно отработавшую службу упавшей. - `scripts/wait-for-world.sh` — вторая половина `make up`: приём асинхронный, поэтому ждать надо доезда до `ods.event`, а не завершения заливки. Ограниченный цикл опроса, не пауза наугад. - Девятая проверка `make check-clickhouse`: подневный счёт событий против описи, рамка по датам стартового мира, счёт через `FINAL`. При расхождении называет, где искать, — в событиях или в браке. - Порог «день ≤ 30 с» снят из спеки генератора в обоих местах: замер дал 1,7 с, порог был выше факта в восемнадцать раз. На его месте — замеры с датой. Раздел 9 спеки закрыт: открытых вопросов не осталось. - Слова: «манифест» стал описью мира, «зерновой мир» — стартовым миром (решение владельца). Оба заведены в словарь CONTEXT.md. Проверка `make clean && make up` с нуля — 2 м 50 с, доехало ровно 401 185 событий. `make check-clickhouse` зелёный (8 с), `make smoke` зелёный (9 с), `make test` — 407 тестов за 71 с, `make lint`, `make typecheck`, `make config-test` зелёные. Что проверка умеет краснеть, снято двумя поломками: снос партиции 2026-06-03 дал диагноз «не доехали до ODS», негодная строка в сырье — «сломан разбор». Строки опыта убраны, день переигран, счёт вернулся. Тест свежести проверен молчаливой правкой цены в каталоге: покраснел. Ссылка: #42
This commit is contained in:
1 parent
30a1e9e567
commit
7c9eeedc40
20 files changed
+625
-132
No files matched your search
@@ -227,10 +227,10 @@ Airflow она стоит — там это обычный `SETTINGS` у зап
|
||||
роняет вставку и останавливает потребление до починки
|
||||
([ADR 0005](../adr/0005-event-ingestion.md)).
|
||||
|
||||
Постоянной сверки счётчиков при этом нет и не должно быть. У сырья срок жизни
|
||||
трое суток, а ODS хранит всё, поэтому равенство «сырьё = события + ошибки»
|
||||
разъедется само: сырьё истечёт раньше, а перезаливка модельного дня задвоит его,
|
||||
тогда как в ODS тот же повтор схлопнется. Правило, красное в норме, учит не
|
||||
Постоянной сверки **слоёв между собой** при этом нет и не должно быть. У сырья
|
||||
срок жизни трое суток, а ODS хранит всё, поэтому равенство «сырьё = события +
|
||||
ошибки» разъедется само: сырьё истечёт раньше, а перезаливка модельного дня
|
||||
задвоит его, тогда как в ODS тот же повтор схлопнется. Правило, красное в норме, учит не
|
||||
смотреть на оповещения
|
||||
([ADR 0002](../adr/0002-monitoring-scope.md)). Равенство проверено разовым
|
||||
опытом при исполнении #43, на управляемой пачке: отправили N сообщений —
|
||||
@@ -241,6 +241,15 @@ Airflow она стоит — там это обычный `SETTINGS` у зап
|
||||
`ReplacingMergeTree` зависит от того, сколько мержей успело пройти, и спека это
|
||||
прямо запрещает (раздел 6).
|
||||
|
||||
**Постоянная сверка одна, и она другого рода** — не слой против слоя, а ODS
|
||||
против [описи мира](../../data/world-inventory.json), лежащей в git
|
||||
(`make check-clickhouse`, пришла с #42). Разъехаться сама она не может, и в
|
||||
этом вся разница: опись описывает восемь дней стартового мира, счёт обрамлён
|
||||
их датами, повтор заливки схлопывается под `FINAL`, а срок хранения ей не
|
||||
помеха — ODS хранит всё. Обе стороны равенства зафиксированы: одна кодом
|
||||
генератора, другая файлом в git. У межслойной сверки такой опоры нет ни с
|
||||
одной стороны.
|
||||
|
||||
## Срок жизни сырья
|
||||
|
||||
Сырьё в STG живёт трое суток реального времени и уходит само. Трое — это окно
|
||||
|
||||
@@ -23,25 +23,29 @@
|
||||
заметит потерянного подключения Superset: он про ClickHouse и только.
|
||||
|
||||
Отсюда правило для новой проверки: **спроси, кого она спрашивает.** Счётчики
|
||||
против манифеста — вопрос к ClickHouse: строки в `ods.event` считает сам
|
||||
против описи — вопрос к ClickHouse: строки в `ods.event` считает сам
|
||||
сервер и отвечает сразу, значит дом им в `check-clickhouse`, даже если по цене
|
||||
они подошли бы смоуку.
|
||||
|
||||
## Карта целей
|
||||
|
||||
Стенд нужен трём целям из семи. Цена — замер 6 августа 2026 года, см. «Что
|
||||
Стенд нужен трём целям из семи. Цена — замер 7 августа 2026 года, см. «Что
|
||||
проверено».
|
||||
|
||||
| Цель | Что утверждает | Стенд | Цена |
|
||||
|---|---|---|---|
|
||||
| `make lint` | Код генератора отформатирован и проходит ruff | не нужен | 0,4 с |
|
||||
| `make typecheck` | Типы генератора сходятся (ty) | не нужен | 0,5 с |
|
||||
| `make test` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранное из неё [описание выгрузки](../formats/clickstream-event.md) — свежим | не нужен | 40 с |
|
||||
| `make test` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода [описание выгрузки](../formats/clickstream-event.md) и [опись мира](../../data/world-inventory.json) — свежими | не нужен | 71 с |
|
||||
| `make config-test` | Compose разбирается, файлы DAG синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с |
|
||||
| `make smoke` | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 8 с |
|
||||
| `make check-clickhouse` | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL | нужен | 7 с |
|
||||
| `make smoke` | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 9 с |
|
||||
| `make check-clickhouse` | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, счёт событий стартового мира против описи | нужен | 8 с |
|
||||
| `make check-services` | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 44 с |
|
||||
|
||||
Цена самого `make up` — 2 м 50 с с нуля (`make clean` перед ним — ещё 23 с) и
|
||||
1 м 5 с на живом стенде. В цену целей она не входит, но записана здесь по той
|
||||
же причине: с #42 подъём стенда заливает стартовый мир и стал заметно дороже.
|
||||
|
||||
## Правило: смоук обязан оставаться быстрым
|
||||
|
||||
`make smoke` — быстрая проверка для регулярного прогона: её гоняют не
|
||||
@@ -72,6 +76,14 @@ ClickHouse отвечает сразу.
|
||||
Правки генератора добавляют к этому `make lint`, `make typecheck` и
|
||||
`make test`: стенд им не нужен, а `make test` из них самая дорогая.
|
||||
|
||||
Тринадцать секунд из её семидесяти одной — пересборка восьми модельных дней
|
||||
для сверки с описью мира. Дешевле хеши не сравнить: чтобы узнать, тот ли
|
||||
получается мир, его надо получить. Место выбрано по той же оси «кого
|
||||
спрашивают» — вопрос обращён к коду генератора, стенд ему не нужен, — и
|
||||
краснеет проверка там, где надо: сразу после правки генератора. Не будь её,
|
||||
расхождение всплыло бы получасом позже, на поднятом стенде, где выглядит
|
||||
поломкой хранилища, а не забытой пересборкой.
|
||||
|
||||
## Порогов по времени здесь нет
|
||||
|
||||
Цена в таблице — замеренное число с датой замера, а не назначенный порог.
|
||||
@@ -117,9 +129,16 @@ ClickHouse отвечает сразу.
|
||||
же механизм, на котором стоит переобработка дня X.
|
||||
|
||||
Что тогда стережёт цепочку постоянно — проверки на **настоящих** данных, тех,
|
||||
что стенд произвёл сам: счётчики против манифеста зернового мира ловят
|
||||
сломанный разбор (события уехали в брак — счёт разошёлся), ничего при этом не
|
||||
вкладывая.
|
||||
что стенд произвёл сам: подневный счёт событий против описи мира ловит и
|
||||
сломанный разбор (события уехали в брак — счёт разошёлся), и потерю по дороге,
|
||||
ничего при этом не вкладывая. Это единственный постоянный сторож цепочки
|
||||
Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
|
||||
|
||||
Правило, которое такая проверка обязана выдержать и которое стоит держать в
|
||||
голове для следующей: **на растущих данных утверждать можно только про
|
||||
зафиксированный кусок.** Мир растёт — менти переиграет день, этап 5 добавит
|
||||
следующий, — и всякое утверждение про таблицу целиком однажды покраснеет
|
||||
законно, то есть впустую. Как это сделано здесь, написано в самом скрипте.
|
||||
|
||||
## Корректность процессов живёт в дагах DQ, а не в целях `make`
|
||||
|
||||
@@ -145,6 +164,13 @@ ClickHouse отвечает сразу.
|
||||
| `make check-services` | `scripts/stand-services.sh` |
|
||||
| `make check-clickhouse` | `scripts/check-clickhouse.sh` |
|
||||
|
||||
Особняком — `scripts/wait-for-world.sh`: он не проверка, а вторая половина
|
||||
`make up`. `docker compose up --wait` дожидается служб, в том числе успешно
|
||||
отработавшей заливки, но «заливка кончилась» значит «события в топике», а не
|
||||
«события в хранилище»: приём асинхронный. Скрипт ждёт доезда ограниченным
|
||||
циклом опроса. Без него `make check-clickhouse` следом краснел бы по
|
||||
устройству, а не по поломке.
|
||||
|
||||
Общее у смоука и `check-services` — счёт проверок, обращение к Compose и две
|
||||
проверки — вынесено в `scripts/stand-common.sh`; сам он не запускается.
|
||||
Оттуда же приходят два решения, которые видно по счёту прогонов:
|
||||
@@ -165,11 +191,29 @@ ClickHouse отвечает сразу.
|
||||
|
||||
## Что проверено
|
||||
|
||||
Замеры 6 августа 2026 года, стенд поднят заранее; время `make up` в цену целей
|
||||
не входит. Время взято по `time` и совпадает с тем, что цель печатает сама. Оно
|
||||
плавает от прогона к прогону: смоук дал 8 секунд дважды (без проверки Kafka,
|
||||
до её появления, — 5 и 6), `check-services` — 43 и 44. В таблице стоит большее
|
||||
из замеренных.
|
||||
**Перезамер 7 августа 2026 года при исполнении #42.** Подъём стенда с нуля
|
||||
(`make clean && make up`) — 2 м 50 с, из них 22,5 с занимает сама заливка:
|
||||
18,7 с генерация восьми дней и 3,8 с доставка 401 185 сообщений в Kafka.
|
||||
Повторный `make up` на живом стенде — 1 м 5 с: мир заливается заново, и это
|
||||
не оплошность — `WatchID` те же, повтор схлопнет `ReplacingMergeTree`.
|
||||
Смоук — 9 с, `check-clickhouse` — 8 с с новой девятой проверкой. `make test`
|
||||
вырос до 71 с: 58 с прежних тестов плюс 13 с на пересборку восьми дней для
|
||||
сверки с описью. Прежние 40 с в таблице устарели ещё до #42 — тесты добавляли
|
||||
#41 и #43.
|
||||
|
||||
Что новая проверка **умеет краснеть**, снято двумя поломками того же дня, и
|
||||
проверялись обе ветви её диагноза. Снесли партицию `2026-06-03` в
|
||||
`ods.event_rep` — проверка покраснела, показала недостающий день и назвала
|
||||
адрес: «события не доехали до ODS, начните с чтеца топика». Затем положили в
|
||||
сырьё заведомо негодную строку — брак появился, и проверка сменила диагноз на
|
||||
«сломан разбор, начните с `ods.event_errors_dist`». Строки опыта убраны, день
|
||||
переигран `make generate-batch GENERATOR_DAY=2`; счёт вернулся к 401 185, и
|
||||
это заодно показало дедупликацию: повтор дня не удвоил счёт под `FINAL`.
|
||||
|
||||
Замеры 6 августа 2026 года, стенд поднят заранее. Время взято по `time` и
|
||||
совпадает с тем, что цель печатает сама. Оно плавает от прогона к прогону:
|
||||
смоук дал 8 секунд дважды (без проверки Kafka, до её появления, — 5 и 6),
|
||||
`check-services` — 43 и 44. В таблице стоит большее из замеренных.
|
||||
|
||||
До деления `scripts/stand-smoke.sh` шёл 48 секунд на 25 проверок, из них
|
||||
42 секунды съедали шесть: Kafka с машины, два запуска пробников Airflow и три
|
||||
|
||||
@@ -253,7 +253,7 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
||||
Пятое — **опоздание** — бесплатно даёт формат доставки: часть заказов
|
||||
впервые появляется в слепке D+1/D+2 («вчера не сходилось, сегодня сошлось»),
|
||||
ориентир ~10%. Точные доли фиксируются при пересборке эталонного мира;
|
||||
манифест хранит точные счётчики по каждому классу расхождений (отмены,
|
||||
опись хранит точные счётчики по каждому классу расхождений (отмены,
|
||||
потери, дубли).
|
||||
|
||||
Не берём: сироту-фрод (`purchase` есть, а заказа не будет никогда) —
|
||||
@@ -286,7 +286,7 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
||||
`uniq(посетителей) > uniq(людей)`, менти выводит расхождение сам.
|
||||
Константа мира: каждый двухкуковый покупатель делает минимум по одному
|
||||
заказу с каждой куки — иначе вторая кука не попадает в карту соответствий
|
||||
(она строится только из покупок) и лаба не воспроизводится. Манифест
|
||||
(она строится только из покупок) и лаба не воспроизводится. Опись
|
||||
хранит число именно таких пар.
|
||||
- Витрины разводят имена честно: **«посетители»** (`uniq(ClientID)`) и
|
||||
**«известные пользователи»** (после склейки) — оба числа рядом в дашборде.
|
||||
@@ -464,19 +464,19 @@ Kafka день переигрывается генератором заново:
|
||||
пачками), Postgres остаётся только служебной базой Airflow. Смешанность
|
||||
ландшафта выражена режимами и частотами, а не второй трубой.
|
||||
|
||||
## 8. Эталонный мир и манифест
|
||||
## 8. Эталонный мир и опись
|
||||
|
||||
В git хранится только манифест эталонного мира; сам снимок (14 модельных
|
||||
В git хранится только опись эталонного мира; сам снимок (14 модельных
|
||||
дней) генерируется на месте — при `make up` и при проверках (решение
|
||||
развилки «Производительность» карты #26, подробности —
|
||||
[спека генератора](2026-08-01-generator.md), раздел 5). Манифест несёт
|
||||
[спека генератора](2026-08-01-generator.md), раздел 5). Опись несёт
|
||||
паспорт мира (каноническое зерно, версия генератора), контрольные счётчики
|
||||
и хеши по дням; проверки «пустой git diff» и «пересгенерируй день N —
|
||||
сравни хеш» живут на нём. Политика версионирования артефакта (бывший туман
|
||||
карты #10) закрыта этим же ходом: версионируется манифест.
|
||||
Контрольные числа манифеста:
|
||||
сравни хеш» живут на ней. Политика версионирования артефакта (бывший туман
|
||||
карты #10) закрыта этим же ходом: версионируется опись.
|
||||
Контрольные числа описи:
|
||||
|
||||
- заказная сторона: заказы и выручка по дням; манифест хранит точные
|
||||
- заказная сторона: заказы и выручка по дням; опись хранит точные
|
||||
счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) —
|
||||
самопроверка лабы сверки;
|
||||
- идентичность: uniq кук, uniq известных пользователей, число двухкуковых
|
||||
@@ -497,7 +497,7 @@ v2 стартует пустым, поэтому объём ниже — это
|
||||
| SQL | DDL по слоям и ролям (ON CLUSTER, Replicated*, Distributed; раскладка файлов — в доке хранилища) + трансформации событий, заказов, identity, сверки + словарь | L — ~12–15 файлов, главная сложность |
|
||||
| Airflow | DAG'и по образцу v1: etl_pipeline (партиционная переобработка, ожидание дневного батча заказов — сенсор/Datasets), world_init/next_day, helpers | M — ~5–6 файлов |
|
||||
| Superset | датасеты + дашборд с тремя новыми сюжетами | M — 2 файла |
|
||||
| Эталонный мир | пересборка снимка на месте, манифест-счётчики, чек-скрипты | M–L |
|
||||
| Эталонный мир | пересборка снимка на месте, счётчики описи, чек-скрипты | M–L |
|
||||
| Мониторинг | дашборды Grafana «данные», «кластер», «запросы»; ClickHouse источником данных, панели на SQL; Prometheus тонким полом (ADR 0002) | M — конфиги и дашборды |
|
||||
| Документация | доки v2 пишутся заново (см. раздел 12) | M, в тех же PR |
|
||||
|
||||
@@ -534,15 +534,15 @@ v2 стартует пустым, поэтому объём ниже — это
|
||||
2. DDL и генератор (слиты в один этап — DDL проверяется только настоящими
|
||||
данными): базы и таблицы событий ON CLUSTER, приём `hits` обеими нодами;
|
||||
широкое событие, таксономия, анонимность, N:1 (клиентская сторона
|
||||
целиком). В конце этапа фиксируется маленький «зерновой» мир для
|
||||
целиком). В конце этапа фиксируется маленький стартовый мир для
|
||||
стабильных приёмок следующих этапов (полная пересборка эталонного мира —
|
||||
отдельный этап 7).
|
||||
3. Заказы и каталог: генератор слепков, STG/ODS/DDS заказа, словарь.
|
||||
4. Трансформации и витрины: сессии, identity_map, выручка, сверка A+C.
|
||||
5. Airflow: `etl_pipeline` (партиционная переобработка, ожидание дневного
|
||||
батча заказов — сенсор/Datasets).
|
||||
6. Расхождения B+D и опоздания; счётчики манифеста.
|
||||
7. Эталонный мир: манифест и пересборка снимка, чек-скрипты; CI-генерация
|
||||
6. Расхождения B+D и опоздания; счётчики описи.
|
||||
7. Эталонный мир: опись и пересборка снимка, чек-скрипты; CI-генерация
|
||||
на amd64 и arm64.
|
||||
8. Superset-дашборд v2.
|
||||
9. Мониторинг и runbook «keeper упал / DDL повис в очереди». Состав дашбордов
|
||||
@@ -563,7 +563,7 @@ v2 стартует пустым, поэтому объём ниже — это
|
||||
- Лабы и курс: v2 — другой стенд, лабы для него пишутся с нуля отдельной
|
||||
работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не
|
||||
переносится. Спека даёт будущим лабам только опорные точки — контрольные
|
||||
числа манифеста (сверка, идентичность). Явное следствие: после этапа 9
|
||||
числа описи (сверка, идентичность). Явное следствие: после этапа 9
|
||||
стенд работает, но учебного пути на нём ещё нет.
|
||||
- Инкрементальный ETL (#8) — свой issue.
|
||||
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
модельный день D — функция (зерно, D). Между прогонами живут только зерно
|
||||
и позиция на оси времени.
|
||||
- **Детерминизм до байта.** Одно зерно — побайтово тот же снимок; сверка —
|
||||
хешами манифеста. Транспорт (офсеты Kafka, темп) — вне обещания.
|
||||
хешами описи. Транспорт (офсеты Kafka, темп) — вне обещания.
|
||||
- **Схема — контракт генератора.** Python-модуль с чистыми данными;
|
||||
хранилище строится по рендеренной документации, границу сторожит строгий
|
||||
приём на стороне хранилища.
|
||||
- **Один сериализатор, глупые приёмники.** День-функция выдаёт канонические
|
||||
байты; приёмники — файл, Kafka пачкой, Kafka с темпом.
|
||||
- **Числа.** Средний день ~50 тыс. событий; эталонный снимок — 14 дней;
|
||||
в git — только манифест; автоматический порог один — день ≤ 30 с.
|
||||
в git — только опись мира; автоматических порогов по времени нет.
|
||||
|
||||
## 1. Модель мира
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
человеку предыстории, чьё окно активности таких дней не оставляет,
|
||||
пара не назначается. День-функция обязана назначенные заказы
|
||||
реализовать; остальные покупки — вольные, их решает генератор торговых
|
||||
событий (#40). Манифест считает пары, реализованные в горизонте
|
||||
событий (#40). Опись считает пары, реализованные в горизонте
|
||||
снимка. Условность в данных не видна: дни назначены той же
|
||||
случайностью, просто брошенной планом один раз.
|
||||
- **Своя ось модельного времени.** Ось событий начинается в
|
||||
@@ -117,19 +117,19 @@
|
||||
Отклонено с доводами:
|
||||
|
||||
- *Мутирующее состояние мира* («мир стареет»): ломает параллельность по
|
||||
дням, требует чекпоинтов, счётчики манифеста узнаваемы только постфактум;
|
||||
дням, требует чекпоинтов, счётчики описи узнаваемы только постфактум;
|
||||
ни один урок стенда на старении не стоит.
|
||||
- *Чистая функция без слоя состава*: глобальные инварианты пришлось бы
|
||||
выводить в каждом дне заново — тот же план мира, но неявный и размазанный.
|
||||
- *Привязка модельного времени к реальному календарю* (T-1 с догоном):
|
||||
конфликтует с ускорением ×60 — за вечер мир уезжает в будущее — и делает
|
||||
даты эталонного мира зависимыми от даты запуска, манифест теряет
|
||||
даты эталонного мира зависимыми от даты запуска, опись теряет
|
||||
воспроизводимость.
|
||||
|
||||
Отклонено при исполнении #38 (2026-08-02):
|
||||
|
||||
- *Разгон вместо предыстории* («первые дни малы — магазин запустился»):
|
||||
зерновой мир и половина снимка оказались бы на разгоне, недельная лаба
|
||||
стартовый мир и половина снимка оказались бы на разгоне, недельная лаба
|
||||
сравнивала бы несравнимые недели, «средний день ~50 тыс.» перестал бы
|
||||
быть средним — пришлось бы двигать принятые числа раздела 5.
|
||||
- *Материализованный план на горизонт*: горизонт становится обязательным
|
||||
@@ -139,7 +139,7 @@
|
||||
константы — потолка одновременно живущих кук, которого в жизни нет и
|
||||
который менти нечем объяснить.
|
||||
- *Вероятностная гарантия пар* («почти наверняка купит с обеих кук»): не
|
||||
гарантия — однажды манифест покраснеет, а чинить нечем, кроме смены
|
||||
гарантия — однажды опись покраснеет, а чинить нечем, кроме смены
|
||||
зерна; при этом несклеенная пара в данных неотличима от двух незнакомцев,
|
||||
так что реализм этой лотереи невидим.
|
||||
- *Покупка пары в первый визит куки*: гарантия железная и дешёвая, но узор
|
||||
@@ -154,14 +154,14 @@
|
||||
- **Обещание — содержимое до байта.** Два прогона с одним зерном дают тот же
|
||||
набор событий: те же `WatchID`/`VisitID`, поля, метки модельного времени.
|
||||
Снимок при пересборке побайтово совпадает: канонический порядок ключей и
|
||||
строк; сверка — по хешам манифеста, а два локально пересобранных снимка
|
||||
строк; сверка — по хешам описи, а два локально пересобранных снимка
|
||||
сравнимы обычным diff — пустой означает «ничего не изменилось». Вне
|
||||
обещания — транспорт: офсеты и партиции Kafka, какая нода прочитала,
|
||||
`_load_ts`, темп живого дня.
|
||||
- **Условия обещания.** Детерминизм держится при зафиксированном `uv.lock`
|
||||
и внутри канонического контейнера — то есть везде Linux, на маке и в WSL
|
||||
тоже; единственная переменная — архитектура CPU. Истина — CI на Linux; сходимость любой машины проверяет
|
||||
скрипт «пересгенерируй день N — сравни хеш с манифестом». Расхождение на
|
||||
скрипт «пересгенерируй день N — сравни хеш с описью». Расхождение на
|
||||
любой платформе — баг генератора, а не допуск.
|
||||
- **Раздача зерна — иерархией подпотоков.** Корневое зерно → подпоток
|
||||
состава мира, ветвящийся по номеру дня на когорты плана (состав
|
||||
@@ -173,7 +173,7 @@
|
||||
торговые события, расхождения, опоздания — в фиксированном порядке.
|
||||
По построению: параллельный прогон равен последовательному; продление
|
||||
истории днём N+1 не трогает дни 1…N; правка одного компонента меняет
|
||||
только его часть снимка — в манифесте меняются хеши только затронутых
|
||||
только его часть снимка — в описи меняются хеши только затронутых
|
||||
дней, дифф двух локальных пересборок читаем.
|
||||
- **Механизм подпотоков — `numpy.random.SeedSequence`.** Сверено через
|
||||
Context7 по документации numpy (2026-08-01): `spawn(n)` порождает детей
|
||||
@@ -188,7 +188,7 @@
|
||||
представление в клиентском `purchase` (урок мастер-спеки о расхождениях).
|
||||
- **Канонический seed и паспорт мира.** Эталонный мир собирается одним
|
||||
каноническим зерном — константой репозитория; свои зёрна менти крутит без
|
||||
гарантий манифеста. Манифест хранит паспорт мира — зерно и версию
|
||||
гарантий описи. Опись хранит паспорт мира — зерно и версию
|
||||
генератора; чек-скрипты сверяют паспорт раньше счётчиков.
|
||||
- **Суточный профиль интенсивности задаёт день-функция.** Форма — волны:
|
||||
ночной провал, обеденный и вечерний пики, различие будней и выходных;
|
||||
@@ -207,7 +207,7 @@
|
||||
- *Обещание детерминизма поверх обновления зависимостей*: numpy сознательно
|
||||
улучшает алгоритмы распределений между версиями (NEP 19), Faker меняет
|
||||
словари. Фиксация — `uv.lock`; обновление зависимостей — осознанная
|
||||
пересборка манифеста одним PR.
|
||||
пересборка описи одним PR.
|
||||
|
||||
## 3. Контракт схемы
|
||||
|
||||
@@ -255,7 +255,7 @@
|
||||
- **Один канонический сериализатор, глупые приёмники.** День-функция выдаёт
|
||||
упорядоченный поток канонических байтов — единственное место, где событие
|
||||
превращается в JSON. Приёмники не знают о содержимом: файл (локальный кэш
|
||||
для пересборки и проверок манифеста), Kafka пачкой — пакетный режим,
|
||||
для пересборки и проверок описи), Kafka пачкой — пакетный режим,
|
||||
Kafka с темпом ×60 — живой день. Новых топиков нет.
|
||||
- **Одно событие — одно сообщение Kafka.** «Пачкой» относится к темпу
|
||||
отправки, а не к упаковке: приёмник шлёт события подряд без пауз, но каждое
|
||||
@@ -288,7 +288,7 @@
|
||||
- **Рабочий выбор сериализатора — orjson**: быстрее stdlib json в 5–14 раз,
|
||||
numpy-массивы и datetime сериализует нативно (заметка исследования #31).
|
||||
Смена библиотеки меняет канонические байты, поэтому проходит как
|
||||
обновление зависимости: осознанная пересборка манифеста одним PR.
|
||||
обновление зависимости: осознанная пересборка описи одним PR.
|
||||
- **Промежуточные файлы не хранятся.** Файл дня — кэш чистой функции:
|
||||
потерял — пересчитал. В git снимок не попадает (раздел 5).
|
||||
- **Обрыв любого режима — переигровка дня целиком**; дедуп склеивает
|
||||
@@ -306,7 +306,7 @@
|
||||
|
||||
- *Отдельный топик / Kafka как хранилище дней*: офсеты и партиции вне
|
||||
обещания детерминизма, retention конечен, в git топик не положишь, хеш с
|
||||
манифестом не сверишь; Kafka на стенде — труба, не хранилище (раздел 7
|
||||
описью не сверишь; Kafka на стенде — труба, не хранилище (раздел 7
|
||||
мастер-спеки).
|
||||
- *Файл как обязательная станция доставки*: доигрывание обрыва уже решено
|
||||
через дедуп, канон держит единственный сериализатор, а не диск; файл
|
||||
@@ -327,15 +327,15 @@
|
||||
- **Эталонный снимок — 14 дней**: две полные календарные недели, D0 —
|
||||
понедельник. Самая короткая длина, при которой есть замороженная зона за
|
||||
окном K = 7, дышащая зона и две волны недельной сезонности. Удлинение до
|
||||
месяца — дешёвый ход (пересборка манифеста), если понадобится.
|
||||
- **В git — только манифест, снимок не хранится.** Снимок генерируется при
|
||||
месяца — дешёвый ход (пересборка описи), если понадобится.
|
||||
- **В git — только опись, снимок не хранится.** Снимок генерируется при
|
||||
`make up` и при проверках: артефакт — кэш чистой функции, кэш в git не
|
||||
хранят. Манифест несёт паспорт мира, счётчики и хеши по дням; проверки
|
||||
«пустой git diff» и «пересгенерируй день N — сравни хеш» живут на нём.
|
||||
хранят. Опись несёт паспорт мира, счётчики и хеши по дням; проверки
|
||||
«пустой git diff» и «пересгенерируй день N — сравни хеш» живут на ней.
|
||||
Каждый `make up` — живая демонстрация детерминизма. Честная потеря —
|
||||
страховка на случай платформенного бага: раньше менти с расходящимися
|
||||
байтами мог взять готовый снимок из git, теперь он упрётся в красный чек
|
||||
манифеста; смягчение — CI гоняет генерацию на amd64 и arm64.
|
||||
описи; смягчение — CI гоняет генерацию на amd64 и arm64.
|
||||
- **Живой день — ×60 по умолчанию**: модельные сутки за 24 реальные минуты,
|
||||
суточная волна разворачивается на глазах; темп в среднем ~35 событий/с,
|
||||
в пиковые часы сильных дней — до ~100. Число —
|
||||
@@ -348,34 +348,37 @@
|
||||
Схема двухъярусная — урок ADR 0004: пороги впритык к расчёту на разном
|
||||
железе кончаются ритуальным удалением проверки.
|
||||
|
||||
Ориентиры на референсной машине — в спеке, без автоматики:
|
||||
Ориентиры на референсной машине — в спеке, без автоматики. Где стоит замер,
|
||||
там он с датой; остальное — порядок величины, пока не мерили:
|
||||
|
||||
| Операция | Ориентир |
|
||||
|---|---|
|
||||
| Генерация одного дня | секунды |
|
||||
| Пересборка эталонного мира (14 дней + манифест, без транспорта) | до минуты |
|
||||
| Заливка снимка при `make up` (Kafka → матвью → ODS) | минуты |
|
||||
| Генерация одного дня | 1,7–2,8 с (замер 7 августа 2026 года) |
|
||||
| Пересборка эталонного мира (14 дней + опись, без транспорта) | до минуты |
|
||||
| Заливка стартового мира при `make up` (8 дней, Kafka → матвью → ODS) | 22,5 с (замер 7 августа 2026 года) |
|
||||
| Лаг живого дня | секунды |
|
||||
|
||||
**Автоматический порог один: полный день (50 тыс. событий) генерируется
|
||||
≤ 30 с.** Расчёт по планке исследования (~2–5×10⁵ событий/с на ядро) — доли
|
||||
секунды; порог держит машинный разброс ×2–5 и ловит деградацию на 1–2
|
||||
порядка: Faker в горячем цикле, случайная квадратичность. Реализация —
|
||||
pytest-тест с маркером `perf` и таймаутом-обрубанием: обязателен в CI,
|
||||
исключён из быстрой локальной петли, зовётся отдельной целью при правках
|
||||
горячего цикла.
|
||||
**Автоматических порогов нет ни одного** — решение владельца 7 августа
|
||||
2026 года при исполнении #42. Здесь стоял единственный: «полный день
|
||||
генерируется ≤ 30 с». Назначен он был до того, как генератор написали, а
|
||||
первый замер настоящего кода дал **1,7 секунды на день в 50 626 событий**
|
||||
(машина стенда, 7 августа 2026 года). Порог оказался в восемнадцать раз выше
|
||||
факта: зелен при любой правдоподобной регрессии, то есть не сторож, а
|
||||
украшение. Как здесь говорят о времени, к тому дню уже решила [карта
|
||||
целей](../architecture/testing.md): цена — замеренное число с датой, а не
|
||||
назначенный предел.
|
||||
|
||||
Остальное — наблюдаемость без порогов: `make up` и smoke печатают тайминги
|
||||
(генерация и доставка отдельно), проигрыватель логирует лаг. Прототип-замер
|
||||
до этапа 2 не нужен: числа назначены с запасом порядок и больше от планки
|
||||
исследования, планка подтверждена локальной проверкой на машине стенда;
|
||||
первый замер настоящего кода — порог этапа 2.
|
||||
Наблюдаемость без порогов остаётся и делает всю работу: проигрыватель
|
||||
печатает тайминги генерации и доставки раздельно, лаг живого дня — в его
|
||||
логе, а `make up` после #42 гоняет генератор по восемь дней при каждом
|
||||
подъёме стенда: замедлись он на порядок — подъём стенда встанет колом, и
|
||||
заметит это первый же человек, который его поднял.
|
||||
|
||||
Отклонено с доводами:
|
||||
|
||||
- *Четыре жёстких CI-ворот на все бюджеты*: машинный разброс против порогов
|
||||
впритык — повторение истории с памятью (ADR 0004); порог оставлен один,
|
||||
грубый, между «×5 шума» и «×100 беды».
|
||||
впритык — повторение истории с памятью (ADR 0004). Оставленный было один
|
||||
грубый порог снят при исполнении #42 (выше).
|
||||
- *Снимок в git* (статус-кво раздела 8 мастер-спеки): основание из v1 —
|
||||
медленный генератор — съедено детерминизмом и скоростью; остаётся только
|
||||
раздутый репозиторий. *Снимок вложением релиза Gitea*: страховка без
|
||||
@@ -411,9 +414,9 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
- **Раздел 1.4**: «из контракта выводятся DDL и валидация» заменено на data
|
||||
contract — хранилище пишется по документации, границу сторожит строгий
|
||||
приём (раздел 3 здесь).
|
||||
- **Раздел 8**: артефакт `data/startup_history/` в git заменён манифестом;
|
||||
- **Раздел 8**: артефакт `data/startup_history/` в git заменён описью;
|
||||
снимок генерируется на месте (раздел 5 здесь). Туман «политика
|
||||
версионирования артефакта» закрыт этим же ходом: версионируется манифест.
|
||||
версионирования артефакта» закрыт этим же ходом: версионируется опись.
|
||||
- **Раздел 11**: пункт «до этапа 3 зафиксировать требования
|
||||
производительности» закрыт числами раздела 5.
|
||||
- Мелкие согласования там, где текст опирался на артефакт в git: источник
|
||||
@@ -444,7 +447,7 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
по расписанию (~раз в 24 минуты); генератор не дорабатывается.
|
||||
- **Этап 5, позиция на оси времени.** Проигрыватель состояния не хранит
|
||||
(раздел 9), поэтому вести позицию — работа того, кто его зовёт. Живёт она
|
||||
переменной Airflow: отсутствие переменной означает мир в зерновом
|
||||
переменной Airflow: отсутствие переменной означает мир в стартовом
|
||||
состоянии, заводит и двигает её только даг `next_day` и только по успеху.
|
||||
Довод — генератор отдельная и заменяемая сущность, привязывать его к
|
||||
хранилищу незачем, а `make clean` сносит том метаданных Airflow вместе с
|
||||
@@ -452,7 +455,7 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
Расхождение переменной с данными (менти почистил партицию руками) лечится
|
||||
документацией, а не сторожем: постоянная сверка была бы проверкой, красной
|
||||
в норме ([ADR 0002](../adr/0002-monitoring-scope.md)).
|
||||
- **Этап 7 (эталонный мир)**: пересборка — это манифест, не артефакт;
|
||||
- **Этап 7 (эталонный мир)**: пересборка — это опись, не артефакт;
|
||||
CI-генерация на amd64 и arm64.
|
||||
- **Будущие лабы**: перезаливка дня X пакетным режимом проигрывателя —
|
||||
готовая демонстрация идемпотентности конвейера.
|
||||
@@ -503,11 +506,11 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
возвратами предыстории), 170 пар.
|
||||
- **Конфигурация мира — модуль чистых данных** рядом с контрактом схемы:
|
||||
все числа мира в одном месте, написанном как приглашение любопытному
|
||||
менти крутить. Правка модуля — смена мира: чек манифеста честно
|
||||
краснеет, манифест сторожит только канон. Вне модуля — лишь то, что
|
||||
менти крутить. Правка модуля — смена мира: чек описи честно
|
||||
краснеет, опись сторожит только канон. Вне модуля — лишь то, что
|
||||
мира не меняет: своё зерно и транспортные флаги проигрывателя.
|
||||
Отклонено: внешний конфиг и env-переопределения — переменная мира,
|
||||
которую паспорт манифеста не видит; файл-конфиг в репозитории — по
|
||||
которую паспорт описи не видит; файл-конфиг в репозитории — по
|
||||
смыслу равен модулю, но платит загрузчиком и валидацией (довод
|
||||
раздела 3 против YAML).
|
||||
|
||||
@@ -590,7 +593,7 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
посетителю 6% до корзины и те же 45% и 55%, помеченному покупателю — 18%
|
||||
и 60%. Перемеренный средний день: 9 879 визитов, 47 433 pageview, 4,80
|
||||
страницы на визит, конверсия визита 2,4%; полные числа — в блоке #40,
|
||||
оттуда же они лягут в манифест.
|
||||
оттуда же они лягут в опись.
|
||||
- **Недельная волна применяется один раз.** Профиль ведёт приток, трафик
|
||||
наследует его через дневную аудиторию; измеренный размах трафика — ±7%
|
||||
против ±10% у притока. Второе умножение удвоило бы недельный размах. Если
|
||||
@@ -611,8 +614,8 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
|
||||
Решено при исполнении #40 (2026-08-02) — решения владельца до реализации.
|
||||
Три решения выходят за границы тикета: правятся план состава (#38), воронка
|
||||
(#39) и каталог товаров (#39). Сейчас это дёшево — манифест ещё не собран
|
||||
(#42); позже обошлось бы его пересборкой. Часть решений принята после
|
||||
(#39) и каталог товаров (#39). Сейчас это дёшево — опись ещё не собрана
|
||||
(#42); позже обошлось бы её пересборкой. Часть решений принята после
|
||||
холодного ревью первой редакции блока.
|
||||
|
||||
- **Корзина шире заказа.** Посетитель кладёт в корзину товары тех карточек,
|
||||
@@ -872,7 +875,7 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
цены и по медиане, потому что одни только края списка обманываются
|
||||
четырьмя исключениями. Хвост соседям: словарь ClickHouse над этим файлом
|
||||
ещё не построен (#37/#43) — колонку надо взять в его описание сразу, а не
|
||||
догонять правкой; манифесту нужен хеш каталога (хвост #42) тем более,
|
||||
догонять правкой; описи нужен хеш каталога (хвост #42) тем более,
|
||||
потому что теперь в файле живёт ещё и поведение.
|
||||
- **Доля просмотров карточек, доходящих до корзины, выросла с 7,3% до 8,6%
|
||||
и подошла к потолку вилки.** Иначе не сошлось: треть кладущих визитов вне
|
||||
@@ -885,7 +888,7 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
пока число правдоподобно, само по себе оно ничего не сторожит, и
|
||||
подгонять поведение под его край не надо. Настоящий предел здесь другой и
|
||||
считается в другой валюте — дневной бюджет событий: события корзины
|
||||
входят в те самые «около 50 тыс.», на которых стоят манифест (#42) и
|
||||
входят в те самые «около 50 тыс.», на которых стоят опись (#42) и
|
||||
порог скорости дня. Упрётся будущая правка — двигать надо бюджет и его
|
||||
причины, а не долю.
|
||||
|
||||
@@ -926,7 +929,7 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
итог виден кодом возврата. Общее у режимов: `--seed`, `--day`, `--days` и
|
||||
приёмник — `--file` либо `--brokers` с `--topic`. Состояния нет: позицию на
|
||||
оси ведёт зовущий (хвост этапу 5 — раздел 8). `--days` играет несколько
|
||||
дней подряд одним запуском — этим зальётся зерновой мир (#42), восемь
|
||||
дней подряд одним запуском — этим зальётся стартовый мир (#42), восемь
|
||||
запусков службы внутри `make up` были бы плохим ответом. Отклонено:
|
||||
*режимы флагом `--speed 0`* — различие тогда прячется за числом, тогда как
|
||||
у режимов разное устройство: у пакетного есть пачка и нет ожидания, у
|
||||
@@ -986,11 +989,30 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
модуль его не находит. Проверено запуском в контейнере; заодно день,
|
||||
сыгранный в образе, совпал побайтово с днём, сыгранным на машине.
|
||||
|
||||
Остаётся открытым, за тикетами:
|
||||
Решено при исполнении #42 (2026-08-07):
|
||||
|
||||
- как фиксируется «зерновой» мир конца этапа 2 (раздел 9 мастер-спеки):
|
||||
с манифестным решением напрашивается мини-манифест зернового мира —
|
||||
форма за #42;
|
||||
- паспорт мира в манифесте (зерно и версия генератора) файл каталога не
|
||||
накрывает: правка цены в CSV меняет мир молча. Манифесту нужен хеш
|
||||
каталога — хвост для #42.
|
||||
- **Опись мира одна, и она же растёт до эталонной.** Стартовый мир конца
|
||||
этапа 2 — первые восемь дней оси, понедельник по понедельник (решение
|
||||
владельца при нарезке, 2026-08-01): полная неделя с выходными и первый
|
||||
замкнутый цикл окна K = 7. Отдельного «младшего» файла для них не
|
||||
заводится: `data/world-inventory.json` — та самая опись, которую раздел 5
|
||||
обещает эталонному миру, пока короткая. Этап 7 продлит её до четырнадцати
|
||||
дней, а не заведёт вторую. Форма — JSON: паспорт мира (зерно, версия
|
||||
генератора, хеш каталога) и по строке на день с датой, числом событий и
|
||||
хешем. Собирается целью `make inventory`, свежесть сторожит тест
|
||||
`test_inventory.py` — тем же способом, что свежесть «описания выгрузки».
|
||||
Отклонено: *два файла, «мини» и полный* — две правды об одном мире и
|
||||
лишнее слово в словаре.
|
||||
- **Хеш каталога — в паспорте, и работа у него объяснительная.** Правка
|
||||
цены в `data/catalog/products.csv` меняет мир так же молча, как правка
|
||||
кода, но поймают её и без хеша: хеши дней разойдутся. Хеш каталога
|
||||
отвечает на следующий вопрос — **что** правили: разошлись дни и каталог —
|
||||
CSV; разошлись только дни — код.
|
||||
- **Слова.** «Манифест» по всей спеке переименован в **опись мира**, а
|
||||
«зерновой мир» — в **стартовый мир** (решение владельца 2026-08-07).
|
||||
Довод — правило языка репозитория: есть обычное русское слово — берётся
|
||||
оно. Заодно исчезла пара «манифест / мини-манифест», в которой читателю
|
||||
пришлось бы различать две сущности там, где вещь одна.
|
||||
|
||||
Открытых вопросов за разделом не осталось: интерфейс запуска закрыт #41,
|
||||
форма описи и хеш каталога — здесь.
|
||||
Reference in new issue
Block a user