Files
x0gp/server/PLAN.md
T
x0gp 978d36c505 feat(server): initial implementation
This commit captures the full server code accumulated across
several development sessions. It includes the following layers,
applied in roughly this order during development:

1. Base infrastructure
   - cmd/poc-server HTTP+WebSocket server, /health, /stats
   - internal/transport wire types (Envelope + all message types)
   - internal/catalog, internal/realtime, internal/stats
   - internal/storage/postgres with migrations 001-004
   - .env, .env.example, docker-compose, Dockerfile, scripts/

2. Swagger documentation
   - go get github.com/swaggo/http-swagger
   - swag init produces docs/docs.go
   - /swagger/index.html UI, /swagger/doc.json spec
   - annotations on health/stats/ws and catalog handlers

3. Races API
   - internal/races/{store,service,keyset,types}.go
   - migration 005_races.sql: finished_races, race_plans, race_queue
   - GET /api/races with keyset pagination, status filter
   - GET /api/races/upcoming
   - POST /api/races/queue/join, GET, DELETE
   - POST/GET /api/races/plans, DELETE /{id}
   - lobby.RaceMeta simplification (no host_id/host_name)

4. Races seeder
   - internal/races/seed with deterministic generator
   - --seed-races / --reset CLI flags in main.go
   - 30 finished + 5 live + 5 plans + 4 queue entries

5. Drivers and clans
   - internal/drivers, internal/clans (CRUD, validation)
   - migration 008_drivers_clans.sql
   - /api/clans and /api/drivers endpoints
   - 3-letter uppercase nickname/tag validation
   - lobby.DriverMeta: Nickname, AvatarURL, ClanID, ClanTag

6. Podium
   - internal/transport.RacePodiumEntry
   - lobby.SetDriverProfile for in-memory metadata sync
   - migration 007_podium.sql (podium JSONB column)
   - seeder populates top-3 per finished race

7. Live races persistence
   - migration 009_live_persistence.sql: live_races,
     live_race_drivers, lobby_drivers
   - internal/races/live_store.go: LiveStore with write-side
     mirror for lobby.Service mutations
   - Service.collectLive and Upcoming read from Postgres
   - main.go RestoreFromDB rehydrates lobby on startup

8. Unify live + finished into one races table
   - migration 010_unify_races.sql: rename finished_races to
     races, expand status CHECK, merge live rows
   - PgStore now hosts both write paths; LiveStore is a
     thin facade implementing lobby.Persistence
   - seeder resetAll drops only finished/cancelled rows and
     race_plans / race_queue / lobby_drivers

Each layer is consistent on its own; cross-layer changes are
visible in the file history. A future refactor may split this
commit into the per-stage boundaries listed above.
2026-06-22 22:01:09 +04:00

25 KiB
Raw Permalink Blame History

x0gp — Server Plan (Go)

Документ фиксирует серверную архитектуру: транспорт, модули, протокол, хранение, биллинг, деплой.

Дата фиксации архитектурных решений: 2026-06-18. Дата обновления (домашний сервер для PoC и MVP): 2026-06-18. Дата обновления v2 (Orange Pi 5 Pro + Coolify + статический IP): 2026-06-18.

Архитектурные выборы, на которых основан план:

  1. Транспорт клиент-сервер: WebSocket + бинарный protobuf.
  2. Управление машинками: Wi-Fi, ESP32-S3 агент.
  3. Модель доверия: authoritative server + client-side prediction.
  4. Позиционирование: IR-магнитные секторы + overhead-CV (гибрид).

1. Цели и не-цели

1.1. Цели

  • Round-trip "команда на экране -> отклик машинки" в худшем случае 200 мс, в типичном 80-120 мс.
  • Поддержка одновременно 50-100 активных заездов на 1 Edge-инстанс.
  • Устойчивость к обрывам связи (клиент теряет сеть на 0.5-2 с без потери состояния).
  • Anti-cheat: защита от подмены телеметрии, авто-команд, грязного руления.
  • Биллинг заездов и турниров (ЮKassa для РФ, Stripe для остального).
  • Горизонтальное масштабирование (stateless API, sticky-сессии на WS).

1.2. Не-цели (для MVP)

  • Физика шин (скорость не считаем, считаем положение с CV).
  • Запись/воспроизведение VR-реплеев.
  • Полноценный matchmaking с ELO (только базовый лобби).
  • Мобильное приложение (только Web, мобилка во второй очереди).

2. Высокоуровневая архитектура

                +----------------------+
                | React (Web client)   |
                | React Native (mobile)|
                +----------+-----------+
                           |
                     WebSocket (binary protobuf)
                           |
                +----------v-----------+
                |   Edge / API Gateway |
                |   (Go, stateless)    |
                +----+------+-----+----+
                     |      |     |
       +-------------+      |     +-------------+
       |                    |                   |
+------v------+    +--------v-------+    +------v------+
| Race Engine |    | Realtime Hub   |    | REST API    |
| (stateful)  |    | (WS fan-out)   |    | (chi/echo)  |
+-------------+    +----------------+    +-------------+
       |                   |                    |
       +---------+---------+                    |
                 |                              |
        +--------v--------+                     |
        |     Redis       |  pub/sub, state    |
        +-----------------+                    |
                 |                              |
        +--------v--------+                     |
        |  Postgres / TS  |<-------------------+
        +-----------------+

Edge / API Gateway — единая точка входа, TLS, rate-limit, auth, маршрутизация. Race Engine — authoritative state гонки, tick 60 Hz. Realtime Hub — WebSocket-концентратор, fan-out игрокам, сериализация. REST API — управление пользователями, лобби, платежами, история.


3. Транспорт

3.1. WebSocket (основной)

  • Библиотека: gorilla/websocket или nbio (последняя — non-blocking, лучше для тысяч соединений).
  • Формат кадров: бинарный protobuf (для latency) + опционально JSON для не-real-time сообщений (чат лобби).
  • Subprotocol: x0gp.v1.
  • Ping/pong: каждые 5 с, читаем RTT для диагностики.

3.2. Альтернатива на будущее

  • WebRTC DataChannel — если потребуется суб-50 мс jitter.
  • Собственный UDP+KCP — если появится конкурентная потребность.
  • В плане не реализуем, но держим "port" в протоколе для миграции.

3.3. REST API

  • HTTP/2, chi router.
  • OpenAPI 3.1 спецификация, кодогенерация клиента.
  • Авторизация: JWT (access 15 мин) + refresh token (30 дней).
  • Idempotency-Key для платёжных эндпоинтов.

4. Структура Go-модулей

server/
  cmd/
    api-gateway/           # REST + WS, stateless
    race-engine/           # stateful, authoritative tick
    agent-linker/          # запускается в помещении (на gateway-ПК)
    indexer/               # пишет телеметрию в Timescale
    replay-recorder/       # (опц.) пишет replay в S3
  internal/
    auth/                  # JWT, сессии, refresh, MFA
    billing/               # ЮKassa/Stripe, биллинг сессий
    race/                  # сессии, круги, DNF, рестарты, секторы
    realtime/              # WS hub, fan-out, per-client очереди
    transport/             # protobuf-схемы, кодеки
    control/               # приём команд, rate-limit, prediction snapshots
    prediction/            # client-side prediction серверные корректировки
    telemetry/             # агрегация, пересылка в TS
    anti_cheat/            # эвристики: sanity check, повторы, geo
    tournament/            # bracket, лэддер, регистрация
    storage/
      postgres/            # sqlx/pgx, миграции
      redis/               # сессии, pub/sub, лобби
      timescale/           # телеметрия
    obs/                   # логирование, метрики, трейсы
  proto/
    client_server.proto    # команды, телеметрия для игрока
    server_agent.proto     # команды и состояние для машинки
  api/
    openapi.yaml
  migrations/              # SQL
  deploy/
    docker-compose.yml     # dev
    helm/                  # prod
  scripts/
    loadtest/              # 100 виртуальных гонщиков

5. Протокол (protobuf)

5.1. Клиент -> Сервер

  • ClientHello { version, token, locale }
  • JoinRace { raceId, carClass }
  • LeaveRace {}
  • InputState { seq, tsMs, steering (-1..1), throttle (0..1), brake (0..1), gear (-1..0..1), buttons } (60-120 Hz)
  • ChatMessage { text } (rate-limited)
  • Ping { tsMs }

5.2. Сервер -> Клиент

  • ServerHello { sessionId, raceTick, config }
  • RaceSnapshot { tick, tsMs, cars: [{id, x, y, heading, speed, lap, sector, lastLapMs}], leaderboard } (30-60 Hz)
  • InputAck { seq, appliedAtMs }
  • Correction { deltaSteering, deltaThrottle, reason } (редко, при расхождении)
  • RaceEvent { type: START|LAP|SECTOR|DNF|END, payload }
  • TournamentUpdate { ... }
  • Pong { tsMs }

5.3. Сервер -> Бортовой агент

  • AgentCommand { carId, steering, throttle, brake, gear, flags, expiresAtMs } (60-120 Hz)
  • AgentConfig { maxSteeringRate, maxThrottleRate, rampLimits }

5.4. Бортовой агент -> Сервер

  • AgentHello { carId, fwVersion, hwId, rssi }
  • AgentTelemetry { carId, tsMs, steeringActual, throttleActual, rpm, voltage, current, tempEsc, tempMotor, imu: {ax, ay, az, gx, gy, gz}, irSensor: {front, rear}, batteryPct } (10-30 Hz)
  • Heartbeat { tsMs, uptimeSec, errors } (1 Hz)
  • AgentLog { level, msg }

5.5. Gateway-агент -> Edge-сервер

  • TrackHello { trackId, hwId, publicKey, version }
  • CarFrame { carId, tsMs, position: {x,y,z}, heading, velocity, confidence } (из CV, 30-60 Hz)
  • SectorEvent { carId, sector, tsMs, lap }
  • ReplayChunk { raceId, tsMs, jpegBytes } (по запросу)
  • LocalCommand { carId, payload } (forwarded from server)

6. Race Engine (authoritative tick)

6.1. Tick

  • Фиксированный шаг: 16.67 мс (60 Hz).
  • На каждом тике:
    1. Принять все InputState от игроков за последние N мс (батч).
    2. Применить к authoritative state машинок (с учётом ramp limits).
    3. Сформировать команды бортовым агентам.
    4. Отправить команды агенту в помещении.
    5. Через ~33 мс получить телеметрию и CV-обновления.
    6. Скорректировать state.
    7. Сформировать RaceSnapshot для клиентов.

6.2. Client-side prediction

  • Клиент хранит последний применённый state и локально симулирует физику между snapshot-ами.
  • При получении Snapshot клиент сверяет, если расхождение > порога — плавная коррекция (lerp 100-200 мс), без "телепорта".
  • Сервер шлёт Correction только если расхождение > критического.

6.3. Anti-cheat эвристики

  • Sanity check входящих команд (steering/throttle в допустимом диапазоне, скорость изменения не выше физического предела).
  • Сравнение CV-позиции с заявленной (на сервере) — если расходятся > N метров, пометка DQF.
  • Повторы replay: после заезда сверяем логгированные команды с CV-треком.
  • Device fingerprint + поведенческий профиль.
  • Rate limit: > 200 Hz InputState = бан.

6.4. Состояния гонки

  • LOBBY -> COUNTDOWN (3..1) -> RACING -> FINISHED.
  • DNF по: таймаут без input 5 с, потеря связи с агентом > 3 с, флаг администратора.
  • RESTART: только на LoBBY или между раундами.

7. Realtime Hub

  • Один WS-коннект на игрока.
  • Подписки: race:{raceId}, tournament:{id}, user:{id}.
  • Fan-out: при Snapshot race-движок публикует в Redis pub/sub, Hub читает и шлёт подписанным клиентам.
  • Backpressure: если клиент не успевает читать — дропаем старые Snapshot, шлём только последний.
  • Sticky-сессии: cookie x0gp_sticky при балансировке на L7.

8. Хранение

8.1. PostgreSQL (relational)

Таблицы (основные):

  • users (id, email, phone, password_hash, created_at, locale)
  • sessions (id, user_id, refresh_hash, ip, ua, expires_at)
  • wallets (user_id, balance, currency)
  • payments (id, user_id, provider, ext_id, amount, status, created_at)
  • races (id, track_id, class, started_at, ended_at, winner_id, replay_url)
  • race_results (race_id, user_id, car_id, lap_times[], best_lap, dnf_reason)
  • tournaments (id, name, format, entry_fee, prize_pool, status)
  • tournament_entries (tournament_id, user_id, seed, eliminated_at)
  • cars (id, hw_id, model, fw_version, status, last_seen)
  • tracks (id, name, layout_id, geo_city, status)

Миграции: golang-migrate/migrate.

8.2. Redis

  • session:{token} -> userId (TTL 15 мин)
  • refresh:{token} -> userId (TTL 30 дн)
  • race:{id}:state -> serialized race state (TTL 1 час)
  • lobby:{id} -> set of userIds
  • ratelimit:{user}:{op} -> token bucket
  • pub/sub channel race_events

8.3. TimescaleDB (time-series)

  • hypertable telemetry (ts, race_id, car_id, field, value)
  • 7 дней горячих данных, потом cold в S3.
  • continuous aggregates для "лучший круг на треке", "топ-скорости".

9. Биллинг

9.1. Провайдеры

  • ЮKassa — для РФ.
  • Stripe — для остального мира (фаза 3).

9.2. Сценарии

  • Пополнение кошелька (one-time payment).
  • Списание за заезд (раз в N минут или за сессию).
  • Входной взнос в турнир.
  • Призовые начисления победителям.

9.3. Идемпотентность и реконсиляция

  • Каждый платёж имеет Idempotency-Key на стороне клиента и ext_id от провайдера.
  • Webhook-эндпоинт /webhooks/yookassa с проверкой подписи.
  • Ежедневный job сверяет балансы с провайдером.

10. Авторизация

  • Email + password (argon2id) — базовый сценарий.
  • OAuth Yandex / VK / Google — для удобства.
  • MFA TOTP — обязательно для админов и pro-игроков.
  • JWT: HS256 в PoC, RS256 в проде.
  • Refresh-token rotation, device-binding.

11. Anti-cheat

Уровни:

  1. Sanity — диапазоны, скорости, ramp limits (всегда).
  2. CV-check — расхождение с overhead-камерой > 1 м -> пометка.
  3. Replay verify — после заезда можно пересчитать (асинхронно).
  4. Behavioral — паттерны (слишком "идеальные" круги = подозрительно).
  5. Manual review — оператор смотрит replay при споре.

12. Observability

  • Логи: slog (stdlib) в JSON, отправка в Loki.
  • Метрики: prometheus/client_golang, экспорт /metrics.
  • Трейсы: OpenTelemetry, OTLP в Jaeger/Tempo.
  • Ключевые метрики:
    • ws_connections_total
    • rtt_ms_p50/p95/p99 (по (clientSendTs - serverRecvTs))
    • tick_lag_ms (если тик опаздывает > 5 мс — алерт).
    • input_rate_per_client
    • dropped_snapshots_per_client
    • gateway_online{trackId} (heartbeat агенты в помещении).

13. Деплой и инфраструктура

Базовое решение: PoC и MVP крутятся на Orange Pi 5 Pro через Coolify. Перенос на облако/VPS — когда появится коммерческая нагрузка и SLA обязательства.

13.1. Orange Pi 5 Pro (PoC и MVP)

Платформа: Orange Pi 5 Pro (Rockchip RK3588S, ARM64). Уже есть в наличии.

Характеристики:

  • CPU: 8 cores (4× Cortex-A76 @ 2.4 GHz + 4× Cortex-A55 @ 1.8 GHz)
  • RAM: 4 / 8 / 16 / 32 GB LPDDR4x (зависит от версии)
  • GPU: Mali-G610 MP4
  • NPU: 6 TOPS (RKNN) — для CV-инференса (YOLO/ByteTrack) на борту
  • Сеть: 2× 2.5 GbE (один WAN, один LAN с трассой — физическая изоляция)
  • Wi-Fi 6 (резерв)
  • USB 3.0 × 2, PCIe 2.1 x1, SATA
  • ОС: Ubuntu (уже стоит, с Coolify)

Coolify как платформа деплоя:

  • Self-hosted PaaS, уже развёрнут.
  • UI для управления сервисами, доменами, SSL, env-vars.
  • Push-to-deploy из Git-репо (GitHub/Gitea).
  • Сервисы = Docker Compose под капотом, но с UI и без боли.
  • Встроенные: PostgreSQL, Redis, MinIO (S3-совместимое), Plausible, Uptime Kuma.
  • Версионирование деплоев, rollback в 1 клик.

Доступ из интернета (статический IP уже есть):

  • Основной вариант: Caddy reverse proxy + Let's Encrypt напрямую. Coolify умеет провизить LE-сертификаты из коробки.
  • Подходит, т.к. провайдер даёт статический IP, 443/tcp открыт на роутере через port-forward на Orange Pi.
  • Cloudflare Tunnel не нужен (нет смысла добавлять лишний hop).
  • WAF: Crowdsec через Coolify (опц., можно отложить).

Архитектура:

+---------------- ORANGE PI 5 PRO (Ubuntu + Coolify) ----------------+
|                                                                       |
|  Coolify (docker compose orchestration)                               |
|    +-- api-gateway        ------------------------------------+      |
|    +-- race-engine        ----------------------------------+      |
|    +-- postgres+timescale (через Coolify UI)                  |      |
|    +-- redis              -----------------------------------+      |
|    +-- caddy (LE)         -----------------------------------+      |
|    +-- prometheus+grafana -----------------------------------+      |
|    +-- minio (S3 replays) -----------------------------------+      |
|                                                                       |
|  Natively (systemd) - требуют прямой доступ к железу:               |
|    +-- agent-linker   (Wi-Fi к машинкам)                            |
|    +-- cv-tracker     (USB камеры, NPU RKNN)                        |
|    +-- node_exporter  (мониторинг)                                  |
|                                                                       |
+---------------------------------------------------------------------+
   2.5 GbE #1 (WAN, к роутеру)         2.5 GbE #2 (LAN к трассе)
        |                                       |
        vv
   +--------------+                   +---------------------+
   | Internet     |                   | Switch (или точка-  |
   | (clients)    |                   | точка)              |
   +--------------+                   +----------+----------+
                                                 |
                                          +------v------+
                                          | x0gp-track  |
                                          | AP + камеры |
                                          +-------------+

Изоляция сети трассы:

  • LAN-порт Orange Pi → выделенный коммутатор (или напрямую к точке доступа x0gp-track).
  • В этой сети только машинки и камеры — никаких домашних устройств.
  • AP настраивается на изолированный SSID / VLAN.
  • Это даёт предсказуемый latency (нет соседского трафика) и безопасность.

Использование NPU RK3588S:

  • RKNN Toolkit 2 для конвертации моделей (PyTorch → ONNX → RKNN).
  • Поддерживаются YOLOv8/v9, ByteTrack через onnxruntime-rknn.
  • Целевой инференс: 60-120 FPS на одной камере 1080p (с запасом).
  • Если NPU не подходит — fallback на CPU ARM A76.
  • Альтернативный inference-сервер: Ultralytics + onnxruntime-rknn.

ARM64-специфика:

  • Docker-образы должны быть linux/arm64 или multi-arch.
  • Postgres+TimescaleDB, Redis, Caddy, Prometheus, Grafana — есть для arm64.
  • Go-сервисы — GOARCH=arm64 при сборке (по умолчанию если собирать на Orange Pi).
  • CV-инференс на Python — pin версии onnxruntime-rknn под aarch64.

Бэкапы:

  • Ежедневный pg_dump (cron в Coolify или отдельный scheduled task).
  • Backup в MinIO (S3-совместимое) внутри Coolify.
  • Опционально off-site: rclone → Backblaze B2 / Yandex Object Storage.
  • Retention: 14 дней локально, 30 дней off-site.

Безопасность:

  • SSH: только ключи, fail2ban.
  • Coolify UI: за reverse proxy с basic auth или Cloudflare Access.
  • Порты наружу: 80/tcp + 443/tcp (для Caddy/LE).
  • Postgres и Redis слушают только 127.0.0.1 внутри Coolify.
  • UPS для Orange Pi (microSD карты не любят внезапные отключения).

13.2. Production (после MVP, когда появится коммерческая нагрузка)

  • VPS в Москве и СПб (2 региона) — Selectel / Yandex Cloud / Timeweb.
  • Managed Postgres + Redis (для снятия с Orange Pi задач бэкапа и HA).
  • Kubernetes (Yandex Cloud / Selectel / AWS) — HPA по метрикам ws_connections, tick_lag.
  • Redis Cluster, Postgres с read-replica.
  • CDN для статики (CloudFront / Cloudflare).
  • WAF (CrowdSec).
  • Orange Pi остаётся как gateway-агент + CV-инференс в помещении с трассой (NPU продолжаем использовать для позиционирования).
  • Edge-сервер (Go) переезжает на VPS, к которому подключаются несколько Orange Pi из разных помещений.

14. Производительность и SLO

Метрика Цель
API p99 latency < 100 мс
WS input->ack < 30 мс на Edge
Tick stability 60 Hz +/- 2 мс
Один race-engine 100 активных гонок
Один api-gateway 5000 WS-клиентов
Доступность 99.5% (PoC), 99.9% (prod)

15. Roadmap (server)

Фаза Срок Что делаем
S0: PoC моно-сервис 1-2 нед Один Go-бинарь: WS + REST + race state in-memory + локальный agent-linker. SQLite. Без биллинга.
S1: MVP модульный 4-6 нед Разделение на api-gateway / race-engine / agent-linker. Postgres + Redis. Auth, JWT, лобби.
S2: CV + телеметрия 8-10 нед Интеграция с overhead-CV. TimescaleDB. Replay recorder (S3-совместимое).
S3: Биллинг 10-12 нед ЮKassa, кошелёк, сессии под оплатой. Webhooks. Админка.
S4: Турниры 14-18 нед Bracket, лэддер, призовые. Анти-чит уровня 2-3.
S5: Продакшн 4-6 мес K8s, мульти-регион, SLO, on-call. Stripe для остального мира. Мобильное приложение.

16. Открытые вопросы

  • Стек frontend: React + Vite, какие библиотеки для WS (reconnect, backoff)? Кандидаты: reconnecting-websocket, socket.io-client (overkill, но быстро), свой тонкий клиент на WebSocket API.
  • Cloudflare Tunnel vs WireGuard vs статический IP — что выберем для доступа к домашнему серверу? Дефолт: Cloudflare Tunnel (проще, есть free-tier, базовый WAF). Уточним после тестов latency.
  • Бэкапы Postgres на NAS — есть ли дома NAS или использовать внешнее S3-совместимое (Backblaze B2 / Yandex Object Storage) для off-site бэкапов?
  • CDN/streaming для replay — Mux / Cloudflare Stream / своё? Out of scope для MVP, но в плане держим "порт".
  • Интеграция со стриминговыми платформами (Twitch) — out of scope для MVP, но в плане держим "порт".
  • Язык CV-сервиса: Go (gocv) или Python (быстрее итерация, ONNX-рантайм)? Дефолт: Python на PoC для скорости итераций, Go для prod-инференса (меньше ресурсов, проще деплой).
  • Конкретные характеристики домашнего сервера (CPU/RAM/диск/ОС) — нужно для оценки, сколько одновременных гонок он потянет. В PLAN.md пока заложен разумный минимум (4 CPU / 16 GB / 500 GB SSD).
  • ЮKassa-аккаунт — есть ли уже ИП/ООО для приёма платежей, или регистрировать при переходе на биллинг?