Место заказов в стартовом мире #74

Closed
opened 2026-08-07 20:44:29 +03:00 by ddmitry · 2 comments
Owner

Part of #69.

Вопрос

Появляются ли слепки заказов в стартовом мире, который make up заливает на пустой стенд, и что это делает с описью?

Сегодня стартовый мир — восемь модельных дней событий; опись (data/world-inventory.json) хранит паспорт мира и хеши дней, world-init заливает их при подъёме, а make check-clickhouse сверяет счёт по дням с описью (#42).

Решить: входят ли заказы в тот же артефакт, как выглядит их хеш при окне изменяемости K (слепок дня D меняется ещё K дней), и что тогда сторожит проверка на стенде.

Part of #69. ## Вопрос Появляются ли слепки заказов в стартовом мире, который `make up` заливает на пустой стенд, и что это делает с описью? Сегодня стартовый мир — восемь модельных дней событий; опись (`data/world-inventory.json`) хранит паспорт мира и хеши дней, `world-init` заливает их при подъёме, а `make check-clickhouse` сверяет счёт по дням с описью (#42). Решить: входят ли заказы в тот же артефакт, как выглядит их хеш при окне изменяемости K (слепок дня D меняется ещё K дней), и что тогда сторожит проверка на стенде.
ddmitry added the wayfinder:grilling label 2026-08-07 20:44:29 +03:00
Author
Owner

Вход из #72 — находка холодного ревью, которую там закрыть нечем.

При сдвиге отправки на день (#71) прогон дня D отправляет слепок дня D−1, поэтому
слепок последнего сыгранного дня не отправляется вовсе. На стартовом мире это день
7, и у 18 из 73 пар поздний назначенный заказ падает как раз на него (замерено
на каноническом зерне: plan.counters(seed, 8).pairs = 73, из них 18 имеют
max(pair_order_days) == 7).

Следствие: карта соответствий dds.identity_map строится из моста «событие
purchase ↔ заказ», а этих заказов в хранилище не будет — четверть пар стартового
мира в карту не попадёт. Счётчик пар при этом их считает: он смотрит только на
горизонт (plan.py, np.all(pair_order_days < days)) и про слепки не знает
ничего. Опись покраснеет не от ошибки, а по построению.

Две двери, и обе здесь:

  • стартовый мир досылает последний слепок — тогда закрытых окном дней станет два,
    и число в §6 резолюции #72 (N − 7) поедет;
  • либо счётчик пар учится спрашивать не «уложились ли назначенные дни в горизонт»,
    а «доехали ли эти заказы в отправленном слепке».

Со стороны #72 подпёрто тем, что назначенные планом заказы не опаздывают и их
события не теряются, — но заказ, у которого слепка нет вовсе, этим не спасти.

Вход из #72 — находка холодного ревью, которую там закрыть нечем. При сдвиге отправки на день (#71) прогон дня D отправляет слепок дня D−1, поэтому слепок последнего сыгранного дня не отправляется вовсе. На стартовом мире это день 7, и **у 18 из 73 пар поздний назначенный заказ падает как раз на него** (замерено на каноническом зерне: `plan.counters(seed, 8).pairs` = 73, из них 18 имеют `max(pair_order_days) == 7`). Следствие: карта соответствий `dds.identity_map` строится из моста «событие `purchase` ↔ заказ», а этих заказов в хранилище не будет — четверть пар стартового мира в карту не попадёт. Счётчик пар при этом их считает: он смотрит только на горизонт (`plan.py`, `np.all(pair_order_days < days)`) и про слепки не знает ничего. Опись покраснеет не от ошибки, а по построению. Две двери, и обе здесь: - стартовый мир досылает последний слепок — тогда закрытых окном дней станет два, и число в §6 резолюции #72 (`N − 7`) поедет; - либо счётчик пар учится спрашивать не «уложились ли назначенные дни в горизонт», а «доехали ли эти заказы в отправленном слепке». Со стороны #72 подпёрто тем, что назначенные планом заказы не опаздывают и их события не теряются, — но заказ, у которого слепка нет вовсе, этим не спасти.
ddmitry self-assigned this 2026-08-16 11:19:29 +03:00
Author
Owner

Резолюция

Рамка. Стартовый мир — не полный мир, а мир, остановленный на границе суток
7|8. Генератор играет два источника с разными темпами: поток Метрики и ночную
выгрузку бэкенда магазина. Разные темпы и дают всё остальное.

1. Слепков семь — дни 0…6

world-init играет восемь дней одним прогоном batch --day 0 --days 8. Со
сдвигом (#71) прогон дня D отправляет слепок дня D−1, поэтому уезжают слепки
дней 0…6. Слепок дня 7 снят на границе суток и уедет первым же ходом мира.

Особого режима у стартового мира не заводится: команда остаётся прежней, правило
отправки живёт в одном месте — в проигрывателе. Генератору тикет не стоит ничего.

Окно изменяемости хеш слепка не трогает. Вопрос тикета снят ещё #71: слепок
дня D — чистая функция (зерно, D), окно живёт внутри него. Дышат заказы, а не
байты слепка, поэтому строка-слепок получает обычный хеш, как день.

2. Что из этого видно снаружи

У последнего прожитого дня клики есть, а заказов нет. Глубже: заказ дышит
статусами ещё K дней, а последний приехавший слепок — день 6. Поэтому день 0
виден дожившим до конца окна, день 1 — прожившим шесть дней, …, день 6 — только
что родившимся. График выручки заваливается к правому краю и дозаполняется,
пока мир едет.

Это не издержка стенда, а главный наблюдаемый эффект второго источника: ночная
выгрузка отстаёт, витрина дозаполняется задним числом, свежие дни предварительны.
«Почему у нас последние дни на дашборде проседают» — обычный вопрос на
собеседовании, и уверенно на него отвечает тот, кто это видел.

3. Что кладётся в опись

Опись хранит только то, чего движение мира не меняет.

что стабильно? в описи
события дня и их хеш да, навсегда есть
байты каждого слепка да, навсегда добавляются: семь строк, дни 0…6
счётчики дня с закрытым окном да, навсегда добавляются: на стартовом мире день 0
выручка открытых дней нет: дышит, пока окно открыто нет
число склеенных пар, uniq известных пользователей нет: растёт с каждым слепком нет

За правилом стоит разведение двух вещей, которые выглядят одинаково:

  • Хеш — растяжка. Он не обещает, что числа те же вечно; он обещает, что их
    сдвиг не пройдёт молча. Читателя ему не нужно — его читает git diff. Правка
    генератора обходится ему в «пересобрал опись, принял diff».
  • Счётчик — опора. Обещание конкретного числа тому, кто его прочтёт. Платит
    за него каждая правка генератора: число надо пересчитать и убедиться, что оно
    означает то же самое.

Отсюда: хеш кладём всегда, счётчик — только когда назван его читатель.

Читателей у счётчиков описи ровно два. Живой сегодня один: make check-clickhouse читает события по дням — всё ли доехало в ods.event. Второй
придёт этапом 4: проверка сверки читает счётчики классов расхождений — правильно
ли расклассифицировано. Если к тому времени читателя у них так и не окажется, та
же бритва режет и их.

У числа склеенных пар и uniq известных пользователей читателя нет вовсе:
генератор они не сторожат (сторожит хеш), транспорт не сторожат (сторожит счёт
событий), а как опора лабы протухают со второго дня жизни стенда.

4. Что сторожит проверка на стенде

Опись описывает мир, а не доставку. Хеш слепка сторожит генератор — те ли
байты он выдал. Доехали ли они до ods.order_snapshot — вопрос стенда, и
отвечают на него его проверки и даги качества. Граница там же, где кончается
работа генератора: положить в Kafka.

Поэтому счёта строк у слепка в описи нет. У событий такое число есть, и make check-clickhouse сверяет по нему приход в ods.event, — но у слепков читателя
сегодня назвать нечем, а счётчик впрок правило выше и запрещает. Понадобится
проверка приёма заказов — число заведётся тогда же, вместе с ней.

Счётчики сторожат только дни с закрытым окном — на стартовом мире один, на
эталонном семь (N − 7, из #72). Это сходится с основанием, по которому размеры
миров и выбирались: inventory.py называет восемь дней «первым замкнутым циклом
окна K = 7», четырнадцать дней эталонного дают неделю таких.

Расхождения с принятым

Вносятся тем же коммитом, что и спека второго источника.

Мастер-спека:

  1. §5: «Опись хранит число именно таких пар» — снимается. Число растёт, пока
    мир едет. Лаба склейки не страдает: её ядро — неравенство
    uniq(посетителей) > uniq(людей), а оно верно в любой день.
  2. §8: контрольные числа идентичности не заводятся вовсе. «Uniq известных
    пользователей» и «число двухкуковых покупателей» растут, пока мир едет. «Uniq
    кук» стабилен, но читателя у него тоже нет — а правило одно на обе стороны, и
    счётчик без читателя не кладётся. Назовётся читатель — вернётся вместе с ним.
  3. §8: «заказная сторона: заказы и выручка по дням» читать как «по дням с
    закрытым окном». У открытых дней эти числа верны до первого хода мира.

Спека генератора получила рамку владельца отдельным разделом «Чем меряется
генератор» (коммит 34267e8): генератор меряется выходом, повторимы эффекты, а
не числа, хеши обещают видимость сдвига. Правило описи выше — прямое следствие.

Отклонено

  • Стартовый мир дошлёт восьмой слепок. Покупает 18 пар ценой того, что на
    свежем стенде исчезает класс awaiting_order — сюжет «вчера не сходилось,
    сегодня сошлось», ради которого #71 сдвиг и делал. Заводит в CLI рычаг «дошли
    последний слепок», который дальше тащит каждое изменение, и делает стартовый
    мир особым случаем ровно там, где #71 его убирал. И изображает магазин с
    мгновенной выгрузкой, каких не бывает.
  • Счётчик пар учится спрашивать про слепки (вторая дверь тикета). Число вышло
    бы верным ровно до первого хода мира, а мир заведён, чтобы ехать: рядом стоит
    пульт с выключателем.
  • Опись несёт оба числа — в плане и на стенде. Второе число без читателя;
    разницу дешевле объяснить словами в спеке, чем сторожить числом.
  • Отменить сдвиг #71. Вопрос снялся бы вовсе, но awaiting_order пропал бы
    навсегда, а не только на свежем стенде.
  • Подогнать план под горизонт — назначенные заказы не падают на последний день.
    Мир перестал бы быть чистой функцией зерна и начал зависеть от того, где мы
    остановились.

Замеры

Каноническое зерно, снято здесь, а не перенесено:

  • пар в плане на стартовом мире 73, доедет в слепках 55; на эталонном 192 и 172.
    Доехавшие считаются как plan.counters(seed, N−1).pairs — тот же счётчик на
    горизонте на день короче;
  • распределение пар по последнему назначенному дню: на день 7 приходится 18 —
    край горизонта, а не свойство дня.

Швы соседям

  • #73 (закрыт). Фраза «назначенные покупки не теряются и назначенные заказы
    не опаздывают — сохраняет обе стороны моста до появления в dds.identity_map»
    сильнее, чем верно: заказ может отсутствовать не из-за потери или опоздания, а
    потому что его слепок ещё не выгружен. Механики #73 это не задевает. Там же
    четвёртое наблюдаемое свойство опирается на числа посетителей, известных
    пользователей и пар — свойство мира остаётся, но числом в описи не
    записывается.
  • #81. Строк-слепков семь; хеш байтов у каждой считается только после того,
    как решена форма записи на проводе и канонический порядок строк. Туда же
    вопрос: шлёт ли прогон дня 0 пустой слепок дня −1 или не шлёт ничего.
  • Этап 4 (#6). На свежем стенде лаба сверки видит целый день
    awaiting_order и недозаполненный правый край выручки. Это норма, а не
    поломка; как это назвать менти — за витринами.
  • Этап 7 (#9). Эталонный мир в четырнадцать дней даёт тринадцать слепков и
    семь дней с закрытым окном. Правки потребуют только числа.
## Резолюция **Рамка.** Стартовый мир — не полный мир, а мир, остановленный на границе суток 7|8. Генератор играет два источника с разными темпами: поток Метрики и ночную выгрузку бэкенда магазина. Разные темпы и дают всё остальное. ### 1. Слепков семь — дни 0…6 `world-init` играет восемь дней одним прогоном `batch --day 0 --days 8`. Со сдвигом (#71) прогон дня D отправляет слепок дня D−1, поэтому уезжают слепки дней 0…6. Слепок дня 7 снят на границе суток и уедет первым же ходом мира. Особого режима у стартового мира не заводится: команда остаётся прежней, правило отправки живёт в одном месте — в проигрывателе. Генератору тикет не стоит ничего. **Окно изменяемости хеш слепка не трогает.** Вопрос тикета снят ещё #71: слепок дня D — чистая функция (зерно, D), окно живёт внутри него. Дышат заказы, а не байты слепка, поэтому строка-слепок получает обычный хеш, как день. ### 2. Что из этого видно снаружи У последнего прожитого дня клики есть, а заказов нет. Глубже: заказ дышит статусами ещё K дней, а последний приехавший слепок — день 6. Поэтому день 0 виден дожившим до конца окна, день 1 — прожившим шесть дней, …, день 6 — только что родившимся. **График выручки заваливается к правому краю** и дозаполняется, пока мир едет. Это не издержка стенда, а главный наблюдаемый эффект второго источника: ночная выгрузка отстаёт, витрина дозаполняется задним числом, свежие дни предварительны. «Почему у нас последние дни на дашборде проседают» — обычный вопрос на собеседовании, и уверенно на него отвечает тот, кто это видел. ### 3. Что кладётся в опись > **Опись хранит только то, чего движение мира не меняет.** | что | стабильно? | в описи | | --- | --- | --- | | события дня и их хеш | да, навсегда | есть | | байты каждого слепка | да, навсегда | добавляются: семь строк, дни 0…6 | | счётчики дня с закрытым окном | да, навсегда | добавляются: на стартовом мире день 0 | | выручка открытых дней | нет: дышит, пока окно открыто | нет | | число склеенных пар, uniq известных пользователей | нет: растёт с каждым слепком | нет | За правилом стоит разведение двух вещей, которые выглядят одинаково: - **Хеш — растяжка.** Он не обещает, что числа те же вечно; он обещает, что их сдвиг не пройдёт молча. Читателя ему не нужно — его читает `git diff`. Правка генератора обходится ему в «пересобрал опись, принял diff». - **Счётчик — опора.** Обещание конкретного числа тому, кто его прочтёт. Платит за него каждая правка генератора: число надо пересчитать и убедиться, что оно означает то же самое. Отсюда: **хеш кладём всегда, счётчик — только когда назван его читатель.** Читателей у счётчиков описи ровно два. Живой сегодня один: `make check-clickhouse` читает события по дням — всё ли доехало в `ods.event`. Второй придёт этапом 4: проверка сверки читает счётчики классов расхождений — правильно ли расклассифицировано. Если к тому времени читателя у них так и не окажется, та же бритва режет и их. У числа склеенных пар и uniq известных пользователей читателя нет вовсе: генератор они не сторожат (сторожит хеш), транспорт не сторожат (сторожит счёт событий), а как опора лабы протухают со второго дня жизни стенда. ### 4. Что сторожит проверка на стенде **Опись описывает мир, а не доставку.** Хеш слепка сторожит генератор — те ли байты он выдал. Доехали ли они до `ods.order_snapshot` — вопрос стенда, и отвечают на него его проверки и даги качества. Граница там же, где кончается работа генератора: положить в Kafka. Поэтому счёта строк у слепка в описи нет. У событий такое число есть, и `make check-clickhouse` сверяет по нему приход в `ods.event`, — но у слепков читателя сегодня назвать нечем, а счётчик впрок правило выше и запрещает. Понадобится проверка приёма заказов — число заведётся тогда же, вместе с ней. Счётчики сторожат только дни с закрытым окном — на стартовом мире один, на эталонном семь (`N − 7`, из #72). Это сходится с основанием, по которому размеры миров и выбирались: `inventory.py` называет восемь дней «первым замкнутым циклом окна K = 7», четырнадцать дней эталонного дают неделю таких. ## Расхождения с принятым Вносятся тем же коммитом, что и спека второго источника. **Мастер-спека:** 1. **§5**: «Опись хранит число именно таких пар» — снимается. Число растёт, пока мир едет. Лаба склейки не страдает: её ядро — неравенство `uniq(посетителей) > uniq(людей)`, а оно верно в любой день. 2. **§8**: контрольные числа идентичности не заводятся вовсе. «Uniq известных пользователей» и «число двухкуковых покупателей» растут, пока мир едет. «Uniq кук» стабилен, но читателя у него тоже нет — а правило одно на обе стороны, и счётчик без читателя не кладётся. Назовётся читатель — вернётся вместе с ним. 3. **§8**: «заказная сторона: заказы и выручка по дням» читать как «по дням с закрытым окном». У открытых дней эти числа верны до первого хода мира. **Спека генератора** получила рамку владельца отдельным разделом «Чем меряется генератор» (коммит `34267e8`): генератор меряется выходом, повторимы эффекты, а не числа, хеши обещают видимость сдвига. Правило описи выше — прямое следствие. ## Отклонено - *Стартовый мир дошлёт восьмой слепок.* Покупает 18 пар ценой того, что на свежем стенде исчезает класс `awaiting_order` — сюжет «вчера не сходилось, сегодня сошлось», ради которого #71 сдвиг и делал. Заводит в CLI рычаг «дошли последний слепок», который дальше тащит каждое изменение, и делает стартовый мир особым случаем ровно там, где #71 его убирал. И изображает магазин с мгновенной выгрузкой, каких не бывает. - *Счётчик пар учится спрашивать про слепки* (вторая дверь тикета). Число вышло бы верным ровно до первого хода мира, а мир заведён, чтобы ехать: рядом стоит пульт с выключателем. - *Опись несёт оба числа — в плане и на стенде.* Второе число без читателя; разницу дешевле объяснить словами в спеке, чем сторожить числом. - *Отменить сдвиг #71.* Вопрос снялся бы вовсе, но `awaiting_order` пропал бы навсегда, а не только на свежем стенде. - *Подогнать план под горизонт* — назначенные заказы не падают на последний день. Мир перестал бы быть чистой функцией зерна и начал зависеть от того, где мы остановились. ## Замеры Каноническое зерно, снято здесь, а не перенесено: - пар в плане на стартовом мире 73, доедет в слепках 55; на эталонном 192 и 172. Доехавшие считаются как `plan.counters(seed, N−1).pairs` — тот же счётчик на горизонте на день короче; - распределение пар по последнему назначенному дню: на день 7 приходится 18 — край горизонта, а не свойство дня. ## Швы соседям - **#73 (закрыт).** Фраза «назначенные покупки не теряются и назначенные заказы не опаздывают — сохраняет обе стороны моста до появления в `dds.identity_map`» сильнее, чем верно: заказ может отсутствовать не из-за потери или опоздания, а потому что его слепок ещё не выгружен. Механики #73 это не задевает. Там же четвёртое наблюдаемое свойство опирается на числа посетителей, известных пользователей и пар — свойство мира остаётся, но числом в описи не записывается. - **#81.** Строк-слепков семь; хеш байтов у каждой считается только после того, как решена форма записи на проводе и канонический порядок строк. Туда же вопрос: шлёт ли прогон дня 0 пустой слепок дня −1 или не шлёт ничего. - **Этап 4 (#6).** На свежем стенде лаба сверки видит целый день `awaiting_order` и недозаполненный правый край выручки. Это норма, а не поломка; как это назвать менти — за витринами. - **Этап 7 (#9).** Эталонный мир в четырнадцать дней даёт тринадцать слепков и семь дней с закрытым окном. Правки потребуют только числа.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: ddmitry/clickstream-data-platform#74