🕸️ dicechess-sync — служебная справка

dicechess-sync — приватный сервис бэкфилла: он обходит граф игроков dicechess.com, скачивает исторические партии, нормализует их и заливает в аналитику, попутно зеркаля сырьё в bronze-архив.

Эта страница — операционная справка «как запускать и настраивать». Как оно устроено по существу — в спайне:

Почему репозиторий приватный

Он содержит обратную разработку (reverse engineering) приватного API dicechess.com — публичного разрешения на автоматический сбор нет, поэтому вся специфика скрейпинга и обхода защиты вынесена в приватный контур. Публичные проекты (dicechess-analytics, dicechess-engine-scala) остаются чистыми от специфики сайта — их граница — нейтральный контракт POST /api/games.


1. Стек и устройство

  • Node ≥ 26, TypeScript исполняется нативно (type-stripping, без шага сборки). type: module.
  • Состояние — локальная SQLite (./data/sync.db, в Docker /app/data/sync.db), WAL, мигратор по PRAGMA user_version (подключение к ней описано в инструкции по подключению). Таблицы: players (фронтир, см. 04-Graf-igrokov-i-perechislenie), games + raw_game_data (каталог + сырой кэш, см. 05-Konveyer-partiy-i-sostoyaniya), app_meta.
  • Транспорт к сайту — curl-subprocess (Cloudflare фингерпринтит TLS), к аналитике — обычный fetch. Один RateLimiter на процесс. См. 06-Zashchita-ot-blokirovok-i-Cloudflare.
  • Образ — node:26-trixie-slim (OpenSSL 3.5), приватный пакет @rabestro/dicechess-raw-archive тянется через BuildKit-secret.

2. CLI-команды

Все запускаются нативным Node без сборки. Требуют DICECHESS_JWT в окружении.

КомандаНазначение
node src/sync-player.ts <userId> [maxFetch]Бэкфилл одного игрока end-to-end: enumerate → fetch (≤ maxFetch, по умолчанию ∞) → post. Резюмируемо: повторный запуск синхронизированного игрока делает инкрементальный свип от курсора synced_through_ms.
node src/sync-crawl.ts <maxPlayers> <maxFetch> [seedId,…]Одноразовый ограниченный обход графа: seed-игроки добавляются как manual, дальше спайдер по приоритету. RESYNC_BEFORE=<ISO> переочередит устаревших синхронизированных.
node src/crawl-service.tsДолгоиграющий демон (Docker CMD) — повторяет ограниченные проходы, пока фронтир не опустеет, затем простаивает (как смотреть вывод в реальном времени описано в руководстве по логам). См. §4.
node src/sync-archive.tsОдноразово дотолкнуть всё кэшированное сырьё в bronze-архив на dexus. Требует ARCHIVE_DB_URL.

Сидирование графа

Сейчас фронтир сидируется вручную — SEED_IDS (или аргументом sync-crawl), игроки попадают с discovered_via='manual', дальше граф расходится спайдером по оппонентам. Лидерборды (leaderboard/x2_leaderboard) — задуманный источник seed’ов и зарезервированы как значения discovered_via, но автоматический фетч лидербордов пока не реализован. Подробнее про обход — 04-Graf-igrokov-i-perechislenie.


3. Переменные окружения

ПеременнаяПо умолчаниюНазначение
DICECHESS_JWT— (обязательно)Bearer-JWT сайта (из браузера, живёт ~месяцы).
ANALYTICS_BASE_URLhttp://192.168.10.3:8020База ingest-аналитики (aurora).
ANALYTICS_INGEST_TOKEN''Bearer для POST /api/games.
DB_PATH./data/sync.dbПуть к SQLite.
ARCHIVE_DB_URL'' (пусто → архив пропускается)DSN bronze-архива (dexus :5433).
DRY_RUNfalse1/true → enumerate+fetch+cache, без POST.
REQUEST_MIN_DELAY_MS3000Базовый интервал между запросами к сайту. (1500 был слишком быстр — ловил 429.)
REQUEST_JITTER_MS1500Случайная добавка [0, jitter).
REQUEST_MAX_RETRIES5Ретраи запроса при throttle (ждёт cooldown, повторяет тот же вызов).
MAX_REQUESTS_PER_HOUR∞ (не задано)Часовой бюджет (окно 1 ч). Не ловит минутный всплеск — это делает spacing.
ENUMERATE_PAGE_SIZE500Размер страницы player/history. Главный рычаг против лимита перечисления (для китов поднимают до 1000).
SEED_IDS[]id игроков для посева фронтира (демон), через запятую.
BATCH_PLAYERS25Игроков на проход демона.
BATCH_FETCH200Партий-фетчей на проход демона.
INTER_PASS_MS0Пауза между проходами, пока есть работа.
IDLE_MS300000 (5 мин)Сон, когда фронтир опустел.
RESYNC_AFTER_MS0>0: переочередить игроков, синхронизированных давнее этого (инкрем. рефреш демона).

Уроки и тонкости лимитов (429, асимметрия player/history vs game-move-history, pageSize) — 06-Zashchita-ot-blokirovok-i-Cloudflare (детальное описание интервалов и пауз демона см. в документации по интервалам).


4. Демон crawl-service

Один процесс, один RateLimiter (глобальный single-flight). На старте: resetInProgress (возврат подвисших in_progresspending), посев SEED_IDS. Цикл прохода while (!stop):

flowchart TD
    A["crawlFrontier<br/>(≤ BATCH_PLAYERS enumerate,<br/>≤ BATCH_FETCH fetch, затем drain post)"] --> B["лог: players/games/fetch/post/<br/>frontier/cooldown"]
    B --> C["archive push (если ARCHIVE_DB_URL)<br/>НЕ фатально — не роняет цикл"]
    C --> D{"был прогресс?"}
    D -->|"да"| E["nap INTER_PASS_MS → проход заново"]
    D -->|"нет"| F{"RESYNC_AFTER_MS > 0?"}
    F -->|"да, есть устаревшие"| G["requeueStale → проход заново"]
    F -->|"нет"| H["idle: nap IDLE_MS"]
    E --> A
    G --> A
    H --> A

SIGTERM/SIGINT прерывают сон и останавливают цикл аккуратно; в finally закрываются архив и БД. Архив-пуш каждого прохода идемпотентен и обёрнут в try/catch — недоступность dexus никогда не валит краулинг (сырьё остаётся в локальном кэше). Полная стадия архивации — 05-Konveyer-partiy-i-sostoyaniya.


5. Деплой

  • Образ ghcr.io/rabestro/dicechess-sync:latest (+ :vX.Y.Z), мульти-арч (в т.ч. arm64 под Raspberry Pi).
  • docker-compose: restart: unless-stopped, том sync_data:/app/data (долговечное состояние), env_file: .env (описание выбора портов для веб-админки базы на RPi см. в отчете по портам).
  • Крутится на rpi4 (residential IP — обязателен для прохождения Cloudflare; с дата-центрового/ноутбучного IP curl ловит 403).

6. Резюмируемость и идемпотентность (кратко)

Весь прогресс — в SQLite, любой энтрипоинт и демон рестартятся свободно: resetInProgress чинит подвисшее, enumerate_offset резюмирует перечисление кита, synced_through_ms превращает повторный запуск в дешёвый инкрементальный свип. Идемпотентность: каталог INSERT OR IGNORE (партия из историй обоих игроков заводится один раз), ingest 201/200, архив first-writer-wins + archived_at. Реплей из сырья (replayRejected / replayNormalizeFailures) переконвертирует без обращения к сайту. Детали — 05-Konveyer-partiy-i-sostoyaniya.


Связанное: 03-Sinkhronizatsiya-obzor-i-arkhitektura, Raw-arkhiv-bronze-sloy, Apgreyd-aurora-plan-i-chek-list, SSH-tunnel-k-servernomu-Postgres.