feat(generator): сериализатор, приёмники, проигрыватель и запуск контейнером #61

Merged
ddmitry merged 2 commits from feat/41-serializer-sinks-cli into main 2026-08-07 14:37:25 +03:00
Owner

Closes #41 (тикнуть чек-лист #4 и закрыть тикет руками после слияния —
русские ключевые слова Gitea не понимает).

Что сделано

Генератор доведён до стенда: топик hits наполняется настоящими данными в
обоих режимах.

  • Канонический сериализатор на orjson — единственное место, где событие
    целиком становится JSON. 47 ключей всегда, «пусто» это пустое значение,
    даты ISO-8601, ecommerce строкой. Вложенный блок ecommerce в
    commerce.py вторым сериализатором не считается: правило про событие, а не
    про блок внутри него.
  • Приёмники — файл (одно событие — одна строка) и Kafka (одно событие —
    одно сообщение). Ключа у сообщения нет: WatchID уникален у каждого
    события, ключом он был бы ключом лишь на вид, а обещать «один ключ — один
    раздел» на неповторяющихся ключах нечего.
  • Проигрыватель и CLI — режимы batch и live (темп ×60), несколько
    дней одним запуском, ограниченная пачка, раздельные тайминги генерации и
    доставки, лаг в логе.
  • Свой образ генератора — зависимости из uv.lock, база закреплена до
    патча, раскладка репозитория сохранена ради каталога товаров. Образ Airflow
    не тронут ни строкой.
  • Обвязка запуска — разовая служба compose под профилем, цели
    generate-batch и generate-live, .dockerignore, tmp/ в .gitignore.
  • Спека пополнена решениями (разделы 4, 8, 9), README — быстрым стартом.

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

Решения, принятые при исполнении

  • Размещение — генератор в своём контейнере; даг этапа 5 будет запускать
    контейнер, а не импортировать пакет. Цена названа и принята: проброс сокета
    докера в Airflow, на учебном стенде размен допустим.
  • Клиент Kafkaconfluent-kafka, необязательной группой зависимостей.
  • Ключ сообщения — ключа нет. Умолчания librdkafka сняты не с рендера
    документации (она их не называет), а с самой библиотеки через
    rd_kafka_topic_conf_dump: partitioner = consistent_random,
    sticky.partitioning.linger.ms = 10.
  • Умолчаний нет у того, что описывает среду или позицию. День на оси и
    имя топика приходят от зовущего; не передали — отказ с кодом 2 до того, как
    сгенерировано хоть одно событие. Умолчания зерна, числа дней и темпа
    остаются: они описывают мир, а не среду.
  • База образа записана литералом, а не берётся из .env: она из того же
    закреплённого набора, что и uv.lock, и в неотслеживаемом файле ей не
    место. Двум существующим образам стенда это ещё предстоит — заведён #60.

След разовых критериев

Требуют прогона целого дня через стенд, постоянными целями make быть не
могут. Снято на чистом стенде, поднятом с нуля.

  • Пакетный день до stg.hits_raw_dist. Отправлено 50 626, счёт по
    Distributed — 50 626. Сходится.
  • Топик прочитан обеими нодами. clickhouse-01 — раздел 0, 26 368
    сообщений, офсеты 0–26367. clickhouse-02 — раздел 1, 24 258, офсеты
    0–24257. Офсеты непрерывны по каждому разделу: ни одно сообщение не несло
    двух событий.
  • Живой день ×60. Модельное время 01:00 на 60-й реальной секунде,
    02:00 на 120-й; лаг печатается. Флаг ускорения проверен отдельно:
    день на --speed 1000 уходит за 86 секунд, 49 722 события доставлены и
    подтверждены запросом.
  • Форма на проводе. В колонке raw EventDate читается как
    2026-06-01, UTCEventTime — как 2026-06-01T12:34:56Z, ecommerce
    лежит строкой с экранированным JSON.
  • Побайтовый детерминизм. Два прогона дня в независимых процессах дают
    один sha256; день, сыгранный в контейнере, совпал байт в байт с днём,
    сыгранным на машине.

Убирать за прогонами не нужно: типизированного ODS ещё нет, день доехал
только до stg.hits_raw_dist, а у сырья срок жизни трое суток. Стенд снесён
вместе с томами.

Проверка

make test — 406 passed. make lint, make typecheck, make config-test
зелёные. make up с нуля, make smoke (20/0), make check-services (7/0).

Как это ревьюировалось

Две слепые линии ревью разных родословных — 15 находок; саморевью
исполнителя — ещё 7. Все закрыты: исправлением либо аргументированным
отказом. Два довода отклонены осознанно (--speed nan и отказ на пустой
прогон): в обоих случаях ошибку совершает человек над собственной командой и
видит последствие сразу.

После триажа прошёл проход на вычитание с правом только резать: срезано
149 строк и 6 тестов. Всё срезанное появилось после ревью, а не при замысле —
сторожа на находки, проверки на сторожей и третьи пересказы уже записанных
решений. Обе линии перепроверили, что несущего не убрано.

Closes #41 (тикнуть чек-лист #4 и закрыть тикет руками после слияния — русские ключевые слова Gitea не понимает). ## Что сделано Генератор доведён до стенда: топик `hits` наполняется настоящими данными в обоих режимах. - **Канонический сериализатор** на orjson — единственное место, где событие целиком становится JSON. 47 ключей всегда, «пусто» это пустое значение, даты ISO-8601, `ecommerce` строкой. Вложенный блок `ecommerce` в `commerce.py` вторым сериализатором не считается: правило про событие, а не про блок внутри него. - **Приёмники** — файл (одно событие — одна строка) и Kafka (одно событие — одно сообщение). Ключа у сообщения нет: `WatchID` уникален у каждого события, ключом он был бы ключом лишь на вид, а обещать «один ключ — один раздел» на неповторяющихся ключах нечего. - **Проигрыватель и CLI** — режимы `batch` и `live` (темп ×60), несколько дней одним запуском, ограниченная пачка, раздельные тайминги генерации и доставки, лаг в логе. - **Свой образ генератора** — зависимости из `uv.lock`, база закреплена до патча, раскладка репозитория сохранена ради каталога товаров. Образ Airflow не тронут ни строкой. - **Обвязка запуска** — разовая служба compose под профилем, цели `generate-batch` и `generate-live`, `.dockerignore`, `tmp/` в `.gitignore`. - **Спека** пополнена решениями (разделы 4, 8, 9), README — быстрым стартом. Вторым коммитом — принцип в `AGENTS.md`: усложнение оплачивается учебной ценностью, и проход на вычитание обязателен перед приёмкой заметной работы. Он выведен из этого же прогона, но к реализации отношения не имеет, поэтому лежит отдельно. ## Решения, принятые при исполнении - **Размещение** — генератор в своём контейнере; даг этапа 5 будет запускать контейнер, а не импортировать пакет. Цена названа и принята: проброс сокета докера в Airflow, на учебном стенде размен допустим. - **Клиент Kafka** — `confluent-kafka`, необязательной группой зависимостей. - **Ключ сообщения** — ключа нет. Умолчания librdkafka сняты не с рендера документации (она их не называет), а с самой библиотеки через `rd_kafka_topic_conf_dump`: `partitioner = consistent_random`, `sticky.partitioning.linger.ms = 10`. - **Умолчаний нет у того, что описывает среду или позицию.** День на оси и имя топика приходят от зовущего; не передали — отказ с кодом 2 до того, как сгенерировано хоть одно событие. Умолчания зерна, числа дней и темпа остаются: они описывают мир, а не среду. - **База образа** записана литералом, а не берётся из `.env`: она из того же закреплённого набора, что и `uv.lock`, и в неотслеживаемом файле ей не место. Двум существующим образам стенда это ещё предстоит — заведён #60. ## След разовых критериев Требуют прогона целого дня через стенд, постоянными целями `make` быть не могут. Снято на чистом стенде, поднятом с нуля. - **Пакетный день до `stg.hits_raw_dist`.** Отправлено 50 626, счёт по Distributed — 50 626. Сходится. - **Топик прочитан обеими нодами.** `clickhouse-01` — раздел 0, 26 368 сообщений, офсеты 0–26367. `clickhouse-02` — раздел 1, 24 258, офсеты 0–24257. Офсеты непрерывны по каждому разделу: ни одно сообщение не несло двух событий. - **Живой день ×60.** Модельное время `01:00` на 60-й реальной секунде, `02:00` на 120-й; лаг печатается. Флаг ускорения проверен отдельно: день на `--speed 1000` уходит за 86 секунд, 49 722 события доставлены и подтверждены запросом. - **Форма на проводе.** В колонке `raw` `EventDate` читается как `2026-06-01`, `UTCEventTime` — как `2026-06-01T12:34:56Z`, `ecommerce` лежит строкой с экранированным JSON. - **Побайтовый детерминизм.** Два прогона дня в независимых процессах дают один `sha256`; день, сыгранный в контейнере, совпал байт в байт с днём, сыгранным на машине. Убирать за прогонами не нужно: типизированного ODS ещё нет, день доехал только до `stg.hits_raw_dist`, а у сырья срок жизни трое суток. Стенд снесён вместе с томами. ## Проверка `make test` — 406 passed. `make lint`, `make typecheck`, `make config-test` — зелёные. `make up` с нуля, `make smoke` (20/0), `make check-services` (7/0). ## Как это ревьюировалось Две слепые линии ревью разных родословных — 15 находок; саморевью исполнителя — ещё 7. Все закрыты: исправлением либо аргументированным отказом. Два довода отклонены осознанно (`--speed nan` и отказ на пустой прогон): в обоих случаях ошибку совершает человек над собственной командой и видит последствие сразу. После триажа прошёл **проход на вычитание** с правом только резать: срезано 149 строк и 6 тестов. Всё срезанное появилось после ревью, а не при замысле — сторожа на находки, проверки на сторожей и третьи пересказы уже записанных решений. Обе линии перепроверили, что несущего не убрано.
ddmitry added 2 commits 2026-08-07 14:18:29 +03:00
- Зачем:
  - у вопроса «чему на этом научится менти?» не хватало второй половины:
    он отсеивал бесполезное, но не взвешивал цену полезного. Стенд
    учебный, и чем он сложнее, тем хуже как учебный материал.
  - прогон #41 показал механизм: две слепые линии ревью дали 15 находок,
    саморевью ещё 7, и каждая по устройству триажа превратилась в правку.
    Шага, на котором кто-нибудь вычитает, в конвейере не было ни разу —
    только воронка. Проход на вычитание потом срезал 149 строк и 6 тестов,
    и всё срезанное появилось после ревью, а не при замысле.
- Что:
  - в раздел «Цель репозитория» добавлены три абзаца: внимание менти как
    конечная валюта и вопрос «что он платит и что получает»; почему
    оборонительный код дороже прочего и врёт про опасность; проход на
    вычитание как обязательный шаг перед приёмкой заметной работы.
  - записано частное правило, выведенное владельцем на двух резах подряд:
    не сторожить ошибку, которую человек делает сам себе и тут же видит —
    разбирательство с ней и есть урок.
- Проверка:
  - правка текстовая, целей make не задевает; make config-test зелёный.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
  - до сих пор генератор умел собирать день, но не умел его отдать: топик
    hits наполнялся пробником, а не настоящими данными. Тикет #41 доводит
    события до стенда и закрывает форму на проводе, на которую обопрётся
    типизированный ODS (#43).
  - сериализатор один по решению спеки: второе место, печатающее событие в
    JSON, разошлось бы с первым молча.
- Что:
  - serialize.py — канонический сериализатор на orjson: единственное место,
    где событие целиком становится JSON; 47 ключей всегда, «пусто» это
    пустое значение, даты ISO-8601, ecommerce строкой. Вложенный блок
    ecommerce в commerce.py вторым сериализатором не считается — правило
    про событие, а не про блок внутри него.
  - sinks.py — приёмники: файл (одно событие — одна строка) и Kafka (одно
    событие — одно сообщение). Ключа у сообщения нет: WatchID уникален,
    ключом он был бы ключом лишь на вид.
  - player.py, cli.py — проигрыватель и интерфейс запуска: режимы batch и
    live (темп ×60), несколько дней одним запуском, ограниченная пачка,
    раздельные тайминги генерации и доставки, лаг в логе.
  - день на оси и имя топика умолчаний не имеют: параметр, описывающий
    среду или позицию, приходит от зовущего, иначе отказ до генерации.
    Умолчания зерна, числа дней и темпа остаются — они описывают мир.
  - generator/Dockerfile — свой образ: зависимости из uv.lock, база
    закреплена до патча, раскладка репозитория сохранена ради каталога
    товаров. Образ Airflow не тронут.
  - разовая служба compose под профилем, цели generate-batch и
    generate-live, .dockerignore, tmp/ в .gitignore.
  - решения внесены в спеку (разделы 4, 8, 9), быстрый старт — в README.
- Проверка:
  - make test 406 passed, make lint, make typecheck, make config-test.
  - побайтовый детерминизм: два прогона дня в независимых процессах дают
    один sha256; день в контейнере совпадает с днём на машине.
  - на стенде: пакетный день доехал до stg.hits_raw_dist, счёт по
    Distributed сошёлся — отправлено 50626, в таблице 50626.
  - топик прочитан обеими нодами: clickhouse-01 раздел 0 (26368),
    clickhouse-02 раздел 1 (24258).
  - живой день: модельное время 01:00 на 60-й секунде, 02:00 на 120-й —
    темп ×60, лаг печатается.
  - форма на проводе в колонке raw: даты читаются глазами, ecommerce лежит
    строкой.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ddmitry merged commit 77fa2905a2 into main 2026-08-07 14:37:25 +03:00
ddmitry deleted branch feat/41-serializer-sinks-cli 2026-08-07 14:37:27 +03:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ddmitry/clickstream-data-platform#61