Генератор слепков: где живёт и чем связан с событиями #71

Closed
opened 2026-08-07 20:44:21 +03:00 by ddmitry · 1 comment
Owner

Part of #69.

Вопрос

Где живёт генератор заказов — отдельным компонентом или вторым режимом существующего пакета, — и как заказ остаётся согласованным со своим событием purchase от того же зерна?

Ограничение снизу жёсткое: в генераторе кликстрима порядок бросков внутри подпотока — часть контракта, и новая случайность, вставленная в середину, сдвигает весь мир (спека генератора, docs/specs/2026-08-01-generator.md). Значит вопрос не только «где код», но и «из какого подпотока берёт случайность заказ».

Смежное: как собирается слепок окна K = 7 модельных дней и что происходит при пропущенном дне (мастер-спека обещает самовосстановление следующим слепком).

Part of #69. ## Вопрос Где живёт генератор заказов — отдельным компонентом или вторым режимом существующего пакета, — и как заказ остаётся согласованным со своим событием `purchase` от того же зерна? Ограничение снизу жёсткое: в генераторе кликстрима порядок бросков внутри подпотока — часть контракта, и новая случайность, вставленная в середину, сдвигает весь мир (спека генератора, `docs/specs/2026-08-01-generator.md`). Значит вопрос не только «где код», но и «из какого подпотока берёт случайность заказ». Смежное: как собирается слепок окна K = 7 модельных дней и что происходит при пропущенном дне (мастер-спека обещает самовосстановление следующим слепком).
ddmitry added the wayfinder:grilling label 2026-08-07 20:44:21 +03:00
ddmitry self-assigned this 2026-08-12 22:03:09 +03:00
Author
Owner

Резолюция

Рамка. Заказ и событие purchase — не два порождения, а две проекции одного
факта мира. Корзина, цены, купон и номер заказа уже посчитаны торговой половиной
дня-функции, поэтому согласованность не удерживается, а получается по построению.

1. Где живёт

Заказ — второй выход торговой половины: commerce отдаёт заказы дня структурой,
заказная половина навешивает жизнь заказа и собирает слепок. Ни одного нового
броска в подпотоке COMMERCE — мир не сдвигается.

Отклонено: выводить заказ разбором собственного вывода (purchaseID,
purchaseRevenue, сырой ecommerce) — бэкенд стал бы читателем трекера ровно
там, где стенд учит, что это разные источники; независимая модель бэкенда
(заказ первичен, событие — эхо, как в жизни) — кто купил, решает воронка, а
воронка это трафик, то есть опрокидывание всего генератора; источника заказов
нет, слепок собирает SQL стенда из событий
— второй источник исчезает вместе с
уроком «две версии правды», а ADR 0008 уже построен под топик.

Следствие: у всякого заказа изначально ровно одно событие purchase. Заказ без
события в трекере — не отдельная порода заказов, а класс B, и делает его #72
выбрасыванием события после присвоения номера.

2. Как запускается

Третья команда того же пакета — snapshot --day D [--days N], свой приёмник,
топик orders. Два источника — два запуска: трекер и бэкенд видны глазами
как два производителя, каждый со своим топиком. Довод учебный: один прогон,
отдающий оба потока, тихо сказал бы, что это одна система.

Отклонено: один прогон с двумя выходами (--orders-topic) — экономии,
ради которой его стоило бы терпеть, он не даёт: со сдвигом на день (пункт 4) окно
слепка и сыгранный день не пересекаются вовсе, переиспользовать нечего. А правило
«приёмник выбирается тем, что для него назвали» он ломает, и в живом дне сводит в
один прогон два разных режима доставки; отдельный пакет и образ — общими остались бы план состава, каталог,
числа мира и торговая половина, то есть библиотека на двоих ради одной команды;
генератор пишет слепок файлом, в топик льёт даг — второй путь доставки и второй
сериализатор; слепок едет топиком hits — убивает два режима приёма ADR 0008.

3. Как собирается окно K = 7

Переигровкой дней D−6…D: заказы дня — производная всей воронки дня, дешёвого пути
к ним нет. Цена — семь проигрышей дня (~14 с) на слепок; подряд идущие дни в
одном прогоне считаются по разу, у начала оси окно усекается само.

Отклонено: кэш заказов дня на томе — заводит состояние между прогонами, том и
инвалидацию, а спека генератора уже отвергала промежуточные файлы; дешёвый путь
к заказам мимо сборки 47 колонок
— заказу нужны и время события, и порядок
потока, и WatchID, экономятся только посточные проходы: оптимизация, а не
устройство; окно держит хранилище (генератор шлёт заказы своего дня, полный
слепок собирает SQL из ods.order_snapshot) — топик перестаёт нести слепок,
замена партиции теряет смысл, ADR 0008 переписывается; K = 1 — это страховочный
срез 1 мастер-спеки, он в резерве, а не применён.

4. Когда отправляется

Снимается на границе суток, отправляется следующим прогоном. Даг, играющий
день D, отправляет слепок дня D−1 — ночная выгрузка бэкенда за вчера, как в бою.
Содержимое слепка — чистая функция (зерно, D), от момента отправки не зависит,
поэтому «когда отправили» лежит по транспортную сторону.

Следствия:

  • Живой день перестаёт быть особым случаем. Своего дага у него нет и не
    обещано: live --day D — долгий процесс на ~24 реальные минуты, а
    24-минутный даг из спеки генератора (раздел 8) — это ETL со стороны хранилища,
    а не запускатель генератора. Слепок живого дня отправит следующий прогон.
  • Пропущенный день лечится окном, отдельного механизма самовосстановления
    писать не надо: слепок переснимается и даёт те же байты.
  • Покупки текущего дня в сверке всегда awaiting_order — ровно тот сюжет
    «вчера не сходилось, сегодня сошлось», ради которого мастер-спека этот класс и
    завела.
  • Цена — не больше одного лишнего проигрыша дня. Без сдвига окно слепка
    замыкается на только что сыгранном дне, и внимательная реализация обошлась бы
    семью проигрышами; со сдвигом окно и сыгранный день не пересекаются, и их всегда
    восемь. Около двух секунд за то, что живой день перестал быть особым случаем.

Словарю это не противоречит: «граница суток — слепок заказов снимается на ней»
остаётся верным. Снимается на ней, отправляется позже.

5. Откуда случайность

Свой компонент дня в конце перечисления Component, ветвление по дню рождения
заказа
. Добавление в конец перечисления ничего не сдвигает — подпоток задан
позицией в дереве, а не порядком вычислений. DISCREPANCIES и LATECOMERS
остаются зарезервированными за #72.

Вся судьба заказа решается при рождении, поэтому слепок любого дня — чтение
готовой судьбы, а не накопление состояния.

Отклонено: дописывать броски в конец COMMERCE — правка заказа и правка
торгового поведения стали бы одним рычагом, а «дописывай строго в конец» —
негласным правилом без сторожа; своя ветвь корня рядом с составом мира и днями
ось дней завелась бы второй раз; бросать состояние в подпотоке дня слепка
траектория заказа зависела бы от того, какие слепки снимали.

6. Момент оплаты — бросок, а не правило

При фиксированном сроке updated_at − created_at одинаков у всех оплаченных, а
точные равенства менти читает как склейку в разметке, а не как поведение людей
(урок правки #50).

Форма — таблица целых весов «сколько часов — с каким весом», как
RETURN_DELAY_WEIGHTS. Плавающие распределения не зовутся: дисциплина
целочисленной случайности (спека генератора, раздел 2) снимает межархитектурные
расхождения, а на побайтовом совпадении стоят CI и опись.

Хвост меряется в часах, но тянется на дни: уложись все оплаты в первые сутки,
слепок d+1 вёз бы уже окончательное состояние, а дни d+2…d+6 — неизменные
строки, и «дыхание» выручки внутри окна исчезло бы.

Вес за краем окна означает «не оплачен никогда». Бросок решает не «когда
оплатят», а «оплатят ли и когда». Правила усечения не нужно ни в каком виде;
обещание «за окном заказ неизменяем» становится буквально верным — мир сам
говорит «не оплачен», и хранилище с ним согласно; а стенд получает урок сверх
прежнего: не всякий заказ — выручка. Долю неоплаченных писать в таблице явной
строкой, а не оставлять читателю складывать хвост в уме.

Конкретные веса — калибровка при реализации; здесь решена форма.

Правило шире момента оплаты

Вся судьба заказа обязана уложиться в окно K либо не случиться вовсе. Отмена
подчиняется тому же пределу.

Швы соседям

  • #72. Опоздание считать от дня снятия слепка, а не отправки, иначе сдвиг
    удвоится и «десять процентов приезжают на D+1» станет D+2. Отмена укладывается
    в окно по правилу выше. И появился исход, которого нет в таблице раздела 4
    мастер-спеки: заказ навсегда в created при доехавшем purchase — суммы
    сходятся, событие на месте, выручки нет. Получает ли он своё имя в
    mismatch_class — решать там.
  • #74. Стартовый мир играет восемь дней одним запуском; сколько слепков он
    отправляет при сдвиге на день и куда девается последний.
  • #80. К моменту забора в топике может лежать больше одного слепка, и
    причина не в живом дне (он своего слепка не отправляет), а в падении между
    шагами: офсеты коммитятся при чтении (ADR 0008), поэтому прогон, упавший после
    отправки и до забора, оставляет свой слепок в топике, а следующий кладёт рядом
    второй. Шаг забора обязан заменить столько партиций, сколько приехало.
  • Спеке второго источника. ADR 0008 говорит: «Тот же даг проигрывает
    модельный день генератором, поэтому переливается ровно то, что он положил в
    топик». Со сдвигом фраза остаётся верной буквально — даг и правда сам положил
    слепок дня D−1, — но читается как «переливается слепок сыгранного дня», а он
    предыдущего. Уточнить тем же коммитом, что и спека (заметки карты #69).

Что всплыло по дороге

Чем деньги, вложенный JSON позиций и даты выглядят на проводе, не решено ни
одним документом, а урок класса C стоит именно на различии Float64 и Decimal.
Заведено тикетом #81, блокирующим #80.

## Резолюция **Рамка.** Заказ и событие `purchase` — не два порождения, а две проекции одного факта мира. Корзина, цены, купон и номер заказа уже посчитаны торговой половиной дня-функции, поэтому согласованность не удерживается, а получается по построению. ### 1. Где живёт Заказ — второй выход торговой половины: `commerce` отдаёт заказы дня структурой, заказная половина навешивает жизнь заказа и собирает слепок. Ни одного нового броска в подпотоке `COMMERCE` — мир не сдвигается. Отклонено: *выводить заказ разбором собственного вывода* (`purchaseID`, `purchaseRevenue`, сырой `ecommerce`) — бэкенд стал бы читателем трекера ровно там, где стенд учит, что это разные источники; *независимая модель бэкенда* (заказ первичен, событие — эхо, как в жизни) — кто купил, решает воронка, а воронка это трафик, то есть опрокидывание всего генератора; *источника заказов нет, слепок собирает SQL стенда из событий* — второй источник исчезает вместе с уроком «две версии правды», а ADR 0008 уже построен под топик. Следствие: у всякого заказа изначально ровно одно событие `purchase`. Заказ без события в трекере — не отдельная порода заказов, а класс B, и делает его #72 выбрасыванием события после присвоения номера. ### 2. Как запускается Третья команда того же пакета — `snapshot --day D [--days N]`, свой приёмник, топик `orders`. **Два источника — два запуска**: трекер и бэкенд видны глазами как два производителя, каждый со своим топиком. Довод учебный: один прогон, отдающий оба потока, тихо сказал бы, что это одна система. Отклонено: *один прогон с двумя выходами* (`--orders-topic`) — экономии, ради которой его стоило бы терпеть, он не даёт: со сдвигом на день (пункт 4) окно слепка и сыгранный день не пересекаются вовсе, переиспользовать нечего. А правило «приёмник выбирается тем, что для него назвали» он ломает, и в живом дне сводит в один прогон два разных режима доставки; *отдельный пакет и образ* — общими остались бы план состава, каталог, числа мира и торговая половина, то есть библиотека на двоих ради одной команды; *генератор пишет слепок файлом, в топик льёт даг* — второй путь доставки и второй сериализатор; *слепок едет топиком `hits`* — убивает два режима приёма ADR 0008. ### 3. Как собирается окно K = 7 Переигровкой дней D−6…D: заказы дня — производная всей воронки дня, дешёвого пути к ним нет. Цена — семь проигрышей дня (~14 с) на слепок; подряд идущие дни в одном прогоне считаются по разу, у начала оси окно усекается само. Отклонено: *кэш заказов дня на томе* — заводит состояние между прогонами, том и инвалидацию, а спека генератора уже отвергала промежуточные файлы; *дешёвый путь к заказам мимо сборки 47 колонок* — заказу нужны и время события, и порядок потока, и `WatchID`, экономятся только посточные проходы: оптимизация, а не устройство; *окно держит хранилище* (генератор шлёт заказы своего дня, полный слепок собирает SQL из `ods.order_snapshot`) — топик перестаёт нести слепок, замена партиции теряет смысл, ADR 0008 переписывается; *K = 1* — это страховочный срез 1 мастер-спеки, он в резерве, а не применён. ### 4. Когда отправляется **Снимается на границе суток, отправляется следующим прогоном.** Даг, играющий день D, отправляет слепок дня D−1 — ночная выгрузка бэкенда за вчера, как в бою. Содержимое слепка — чистая функция (зерно, D), от момента отправки не зависит, поэтому «когда отправили» лежит по транспортную сторону. Следствия: - **Живой день перестаёт быть особым случаем.** Своего дага у него нет и не обещано: `live --day D` — долгий процесс на ~24 реальные минуты, а 24-минутный даг из спеки генератора (раздел 8) — это ETL со стороны хранилища, а не запускатель генератора. Слепок живого дня отправит следующий прогон. - **Пропущенный день лечится окном**, отдельного механизма самовосстановления писать не надо: слепок переснимается и даёт те же байты. - **Покупки текущего дня в сверке всегда `awaiting_order`** — ровно тот сюжет «вчера не сходилось, сегодня сошлось», ради которого мастер-спека этот класс и завела. - **Цена — не больше одного лишнего проигрыша дня.** Без сдвига окно слепка замыкается на только что сыгранном дне, и внимательная реализация обошлась бы семью проигрышами; со сдвигом окно и сыгранный день не пересекаются, и их всегда восемь. Около двух секунд за то, что живой день перестал быть особым случаем. Словарю это не противоречит: «граница суток — слепок заказов снимается на ней» остаётся верным. Снимается на ней, отправляется позже. ### 5. Откуда случайность Свой компонент дня в конце перечисления `Component`, ветвление по **дню рождения заказа**. Добавление в конец перечисления ничего не сдвигает — подпоток задан позицией в дереве, а не порядком вычислений. `DISCREPANCIES` и `LATECOMERS` остаются зарезервированными за #72. **Вся судьба заказа решается при рождении**, поэтому слепок любого дня — чтение готовой судьбы, а не накопление состояния. Отклонено: *дописывать броски в конец `COMMERCE`* — правка заказа и правка торгового поведения стали бы одним рычагом, а «дописывай строго в конец» — негласным правилом без сторожа; *своя ветвь корня рядом с составом мира и днями* — ось дней завелась бы второй раз; *бросать состояние в подпотоке дня слепка* — траектория заказа зависела бы от того, какие слепки снимали. ### 6. Момент оплаты — бросок, а не правило При фиксированном сроке `updated_at − created_at` одинаков у всех оплаченных, а точные равенства менти читает как склейку в разметке, а не как поведение людей (урок правки #50). Форма — **таблица целых весов «сколько часов — с каким весом»**, как `RETURN_DELAY_WEIGHTS`. Плавающие распределения не зовутся: дисциплина целочисленной случайности (спека генератора, раздел 2) снимает межархитектурные расхождения, а на побайтовом совпадении стоят CI и опись. Хвост меряется в часах, но тянется на дни: уложись все оплаты в первые сутки, слепок d+1 вёз бы уже окончательное состояние, а дни d+2…d+6 — неизменные строки, и «дыхание» выручки внутри окна исчезло бы. **Вес за краем окна означает «не оплачен никогда».** Бросок решает не «когда оплатят», а «оплатят ли и когда». Правила усечения не нужно ни в каком виде; обещание «за окном заказ неизменяем» становится буквально верным — мир сам говорит «не оплачен», и хранилище с ним согласно; а стенд получает урок сверх прежнего: не всякий заказ — выручка. Долю неоплаченных писать в таблице явной строкой, а не оставлять читателю складывать хвост в уме. Конкретные веса — калибровка при реализации; здесь решена форма. ### Правило шире момента оплаты **Вся судьба заказа обязана уложиться в окно K либо не случиться вовсе.** Отмена подчиняется тому же пределу. ## Швы соседям - **#72.** Опоздание считать от дня **снятия** слепка, а не отправки, иначе сдвиг удвоится и «десять процентов приезжают на D+1» станет D+2. Отмена укладывается в окно по правилу выше. И появился исход, которого нет в таблице раздела 4 мастер-спеки: заказ навсегда в `created` при доехавшем `purchase` — суммы сходятся, событие на месте, выручки нет. Получает ли он своё имя в `mismatch_class` — решать там. - **#74.** Стартовый мир играет восемь дней одним запуском; сколько слепков он отправляет при сдвиге на день и куда девается последний. - **#80.** К моменту забора в топике может лежать больше одного слепка, и причина не в живом дне (он своего слепка не отправляет), а в падении между шагами: офсеты коммитятся при чтении (ADR 0008), поэтому прогон, упавший после отправки и до забора, оставляет свой слепок в топике, а следующий кладёт рядом второй. Шаг забора обязан заменить столько партиций, сколько приехало. - **Спеке второго источника.** ADR 0008 говорит: «Тот же даг проигрывает модельный день генератором, поэтому переливается ровно то, что он положил в топик». Со сдвигом фраза остаётся верной буквально — даг и правда сам положил слепок дня D−1, — но читается как «переливается слепок сыгранного дня», а он предыдущего. Уточнить тем же коммитом, что и спека (заметки карты #69). ## Что всплыло по дороге Чем деньги, вложенный JSON позиций и даты выглядят **на проводе**, не решено ни одним документом, а урок класса C стоит именно на различии Float64 и Decimal. Заведено тикетом #81, блокирующим #80.
Sign in to join this conversation.