docs(world): описан жизненный цикл прикладного мира

- Зачем:
  - границы владения Compose и Airflow должны быть видны менти из одного источника истины.
- Что:
  - добавлены спека и ADR жизненного цикла мира;
  - обновлены README, архитектурные справочники и прежние решения;
  - правило полной очистки записано как явное и проверяемое требование к агенту.
- Проверка:
  - документация вычитана через `ai-text-lint` и `writing-for-agents`;
  - выполнена литературная вычитка русского текста;
  - `git diff --check`.
This commit is contained in:
ddadmin committed 2026-08-21 23:02:15 +03:00
1 parent fc5244cc67
commit 7e6554507a
12 files changed
+499 -104

No files matched your search

+8 -7
View File
@@ -6,13 +6,14 @@
Генератор играет два источника с разными темпами — поток Метрики и ночную
выгрузку магазина; разные темпы дают всё остальное.
`world-init` играет восемь дней одним прогоном `batch --day 0 --days 8`;
слепки отправляет второй запуск — команда `snapshot` того же диапазона (два
источника — два запуска, [слепок и его доставка](snapshot.md)), и со сдвигом
отправки уезжают **слепки дней 0…6**. Слепок дня 7 снят на границе суток и
уедет первым же ходом мира. Особого режима у стартового мира нет: правило
отправки живёт в одном месте — в проигрывателе; генератору это решение не
стоит ничего.
Даг `world_initialize` играет восемь дней одним прогоном
`batch --day 0 --days 8`; слепки отправляет второй запуск — команда `snapshot`
того же диапазона (два источника — два запуска,
[слепок и его доставка](snapshot.md)). Со сдвигом отправки уезжают **слепки
дней 0…6**, затем даг `orders_ingest` принимает их в хранилище. Слепок дня 7
снят на границе суток и уедет первым же ходом мира. Особого режима у стартового
мира нет: правило отправки живёт в одном месте — в проигрывателе; генератору
это решение не стоит ничего.
Что видно снаружи: у последнего прожитого дня клики есть, а заказов нет;
глубже — день 0 виден дожившим до конца окна, день 6 — только что родившимся.
+22 -31
View File
@@ -480,44 +480,35 @@ Airflow читает и собирает эти файлы штатным шаб
Порядок остальных задают два правила. Первое: матвью принадлежит слою своей
цели, а не источника, — разбор из STG в ODS лежит среди файлов ODS, потому что
наполняет ODS. Второе: матвью приёма создаётся последней из всех, и потому нарушает
нумерацию слоёв. Kafka-движок начинает читать топик ровно тогда, когда к нему
наполняет ODS. Второе: матвью приёма создаётся последней из всех, и потому
нарушает нумерацию слоёв. Kafka-движок начинает читать топик, когда к нему
привязывают первую матвью; создай её раньше разбора — и всё, что доедет в
зазоре, ляжет в сырьё и не попадёт в ODS никуда, ни в событие, ни в ошибки. На
пустом топике зазор безвреден, поэтому первый прогон о нём не скажет. Проснётся
он, когда тома ClickHouse снесены, а данные Kafka целы, — то есть на обычной
отладке.
Применение — двумя одноразовыми сервисами при `make up`, по образцу уже
работающих `airflow-init` и `superset-init`. Сначала `kafka-init` создаёт топики
— `hits` с двумя партициями и `orders` с одной ([ADR
0008](../adr/0008-order-ingestion.md)), — затем `clickhouse-init` дожидается его
завершения и применяет файлы с ноды 1: всё `ON CLUSTER`, кроме чтеца заказов —
он объявлен только на этой ноде. Порядок страхует `hits` от автосоздания с
одной партицией. Переключателей тут два, и путать их не надо: брокер
автосоздание разрешает, а потребитель librdkafka внутри ClickHouse его не
просит — оба конца измерены, см. «Что проверено». То есть стенд держится на
умолчании клиента, а урок «обе ноды читают топик» умирает тихо, поэтому топик и
создаётся явно, до применения DDL.
Файлы применяет даг `world_initialize` с ноды 1 отдельной учётной записью
`lifecycle`: всё `ON CLUSTER`, кроме чтеца заказов — он объявлен только на этой
ноде. Перед DDL даг создаёт `hits` с двумя партициями и `orders` с одной
([ADR 0008](../adr/0008-order-ingestion.md)). Этот порядок не даёт `hits`
автоматически создаться с одной партицией. Брокер автосоздание разрешает, а
потребитель librdkafka внутри ClickHouse его не просит. Оба конца измерены;
результаты приведены в разделе «Что проверено». Успех дага означает больше,
чем применённый DDL: оба стартовых
источника приняты, а позиция мира установлена последней задачей. Полный граф и
состояния отказа описаны в [спеке жизненного цикла](../specs/2026-08-21-world-lifecycle.md).
Образцы копируются не целиком, и в двух местах. `clickhouse-init` обязан ждать
готовности **обеих** нод: `ON CLUSTER` ждёт исполнения на всех хостах и по
таймауту бросает, а `airflow-init` ждёт только первую ноду, `superset-init` —
только вторую. И второе: оба образца переживают `make up --wait` лишь потому, что
от них зависят долгоживущие сервисы. Что делает `--wait` с одноразовым сервисом
без зависимых, проверено при исполнении #37 на Docker Compose 2.40.3: считает
его упавшим и возвращает единицу, хотя контейнер вышел с нулём. Поэтому
зависимые есть и у новой пары: `clickhouse-init` ждёт `kafka-init`, а
`airflow-init` — `clickhouse-init`, и цепочка упирается в долгоживущий Airflow.
Побочная выгода важнее обхода `--wait`: к моменту старта Airflow DDL заведомо
применён.
Повторный `make up` поверх живого тома проходит зелёным: весь DDL идёт через
`CREATE ... IF NOT EXISTS`. Оборотная сторона — изменённый объект тем же
запуском не применяется, причём молча. Отдельного механизма для этого нет и не
нужно: правка существующего DDL случается, только пока стенд пишут, а лекарство
уже есть — `make clean && make up`. Мир регенерируется, сырьё живёт трое суток,
терять нечего.
Повторный `make up` поверх готового мира проходит зелёным без изменений:
`world_initialize` ветвится раньше DDL. Поэтому изменённый объект обычным
подъёмом не применяется. В разработке меняют или пересоздают только затронутый
объект, партицию либо базу слоя. Если нужен целиком канонический мир,
`make rebuild-storage` запускает `world_recreate`: тот удаляет прикладные базы
`stg`, `dm`, `dds`, `ods`, `dic`, позиции групп потребителей и прикладные топики,
затем вызывает `world_initialize`. Службы и их тома остаются на месте, а позиция
возвращается к концу стартовых дней. Полный `make clean` для изменения DDL не
нужен. Когда какая область очистки уместна перед проверкой, задаёт
[карта проверок](testing.md).
## Карта таблиц
+56 -13
View File
@@ -71,9 +71,15 @@
тогда окружение окупится, и условие возврата — именно это, а не «стало
неудобно».
Цена самого `make up` — 8 м 18 с с нуля и 3 м 35 с на живом стенде. В цену
целей она не входит, но записана здесь по той же причине: подъём пересчитывает
оба источника стартового мира и ждёт их приёма.
Цена самого `make up` с пустых томов — 8 м 18 с до переезда жизненного цикла в
Airflow. В цену целей она не входит. На готовом мире подъём больше не
пересчитывает данные: `world_initialize` зеленеет без изменений.
`make rebuild-storage` тоже не проверка. Команда через Airflow удаляет
прикладные базы ClickHouse, топики и позиции групп Kafka, заново применяет DDL,
заливает стартовый мир и возвращает его пульт к концу стартовых дней. Службы и
их тома она сохраняет, поэтому возврат к каноническим данным не требует
холодного запуска всего стенда.
## Правило: смоук обязан оставаться быстрым
@@ -96,11 +102,19 @@ ClickHouse отвечает сразу.
Деление целей ценно ровно до тех пор, пока оно не превратилось в «гонять
всегда всё».
Состояние перед проверкой восстанавливается в самой узкой достаточной области:
сначала объект или партиция, затем база слоя. Если нужен целиком чистый
прикладной мир, `make rebuild-storage` пересобирает его базы, топики, позиции
потребителей и данные. `make clean` нужен, только когда меняется или сломано
постоянное состояние вне этих баз либо проверяется сам запуск с пустых томов.
Требование к обоснованию полного сброса задаёт `AGENTS.md`. Неизвестную поломку
сначала исследуют: полный сброс уничтожает её следы.
| Когда | Что гонять |
|---|---|
| Правка в работе | `make config-test` и `make smoke` |
| PR или задача | плюс цели, которых правка касалась: DAG-и, Superset или Kafka — `check-services`; DDL, кластер или данные — `check-clickhouse` |
| Приёмка этапа | `make up` с нуля и все три цели на стенде |
| Приёмка этапа | все три цели на стенде; запуск с пустых томов — только для изменений начальной сборки или постоянного состояния служб |
Правки генератора добавляют к этому `make lint`, `make typecheck` и `make test`
из каталога `generator/`: стенд им не нужен, а `make test` из них самая
@@ -194,12 +208,12 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
| `make check-services` | `scripts/stand-services.sh` |
| `make check-clickhouse` | `scripts/check-clickhouse.sh` |
Особняком — два сценария продолжения `make up`, оба не проверки.
`scripts/wait-for-world.sh` ждёт, пока события доедут из Kafka в ODS:
`docker compose up --wait` видит конец отправки, но приём идёт асинхронно.
Затем `scripts/ingest-starting-orders.sh` разово запускает единственный даг
приёма заказов `orders_ingest` и ждёт его конца. Оба ждут ограниченным циклом,
а не паузой наугад.
Особняком стоит `scripts/run-dag.sh`: это не проверка, а общий REST-переход от
`make` к Airflow. `make up` запускает через него `world_initialize`, а
`make rebuild-storage` — `world_recreate`; сценарий ждёт появления дага в API,
печатает идентификаторы дага и прогона и возвращает вызывающему его итог.
Ожидание доставки событий и запуск `orders_ingest` теперь видны задачами
`world_initialize`, а не спрятаны в shell.
Общее у смоука и `check-services` — счёт проверок, обращение к Compose и две
проверки — вынесено в `scripts/stand-common.sh`; сам он не запускается.
@@ -227,9 +241,38 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
19/2: назвал нездоровый сервис и отсутствие одной из четырёх целей. После
возврата сервиса прогон снова дал 21/21.
**Перезамер 20 августа 2026 года при исполнении #96.** `make up` после
`make clean` прошёл за 8 м 18 с, повтор на живом стенде — за 3 м 35 с. Служба
семи слепков заняла около 28 секунд между концом `world-init` и своим концом.
**Приёмка #119 21 августа 2026 года.** `make rebuild-storage` через Airflow
прошёл за 3 м 9 с: позиция прошла задачи `-1` → `0` → `8`, топики получили
новые идентификаторы, группы дочитали новые топики без отставания, а ODS принял
401 185 событий без брака и 2 851 актуальный заказ. Повторный `make up` занял
2 м 41 с вместе со штатным повторным запуском служб инициализации;
`world_initialize` завершился за один цикл опроса и оставил счётчики
неизменными. Отдельно
проверены красные отказы `world_initialize` при `-1` и `world_next_day` при
отсутствующей позиции, `0` и `-1`; `lifecycle` отказано в чтении
`system.users`, но разрешено чтение `system.settings`.
Итоговый холодный `make up` из пустых томов занял 9 м 39 с после 25 с на
`make clean`. Даг создал отсутствующую позицию и закончил на `8`; ODS снова
получил 401 185 событий без брака и 2 851 заказ. У `hits` два раздела, у
`orders` один, отставание обеих групп равно нулю. На этом стенде прошли смоук
20/20, проверка ClickHouse 10/10 и межсервисная проверка 7/7.
Оборванную очистку проверили после записи `world_position=-1`: остановили
первую ноду ClickHouse, и `world_recreate` покраснел на удалении баз. Следующий
`make up` тоже покраснел и оставил `-1`. После возврата ноды повторный
`make rebuild-storage` восстановил мир.
**Прототип 21 августа 2026 года при исследовании #110.** Предшествующая
shell-версия узкой пересборки за 2 м 25 с пересоздала прикладные базы, отправила
401 185 событий и 6 656 версий заказов и вернула позицию с 11 на 8. Замер
оставлен как ориентир цены, но не подтверждает нынешнюю реализацию Airflow: тот
вариант не очищал Kafka и был отклонён как второй оркестратор мира.
**Перезамер 20 августа 2026 года при исполнении #96, до переезда в Airflow.**
`make up` после `make clean` прошёл за 8 м 18 с, повтор на живом стенде — за
3 м 35 с. Отправка семи слепков заняла около 28 секунд.
Прежний замер ниже — 2 м 50 с и 1 м 5 с до появления приёма заказов и пульта
мира. Весь рост приписывать слепкам нельзя: на холодном прогоне основное время
ушло ещё до сценариев ожидания: `airflow-init` завершился примерно на 320-й