🎯 Hunter на Cloud Run
Развёрнуто 2026-07-29. Бот — приватный репозиторий dicechess-hunter, закрытая половина
open-core: рукописный king-hunt оценщик (вероятность взятия короля вместо расстояния Чебышева +
дайс-взвешенная безопасность фигур). Стратегия закрыта, транспорт — публичный MIT
dicechess-bot-runtime. Про сам алгоритм см. algorithm-improvement-ideas и
why-bot-hangs-queen — терм безопасности в hunter это прямой ответ на вторую заметку.
Что и где живёт
| GCP проект | dice-chess-lab |
| Регион | europe-west3 (Франкфурт) |
| Сервис | hunter, URL https://hunter-743631906150.europe-west3.run.app |
| Образ | europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/hunter |
| Идентичность бота | lab/hunter → bot:team:lab:hunter |
| Ресурсы | --concurrency 1 --cpu 2 --memory 1Gi --timeout 60s |
Почему Франкфурт. play-api живёт на Oracle eu-frankfurt-1 (см. oracle-cloud-runbook),
и совпадение регионов минимизирует round-trip вебхука — а это напрямую защищает часы через
OVERHEAD_BUFFER_MS. Попутно выяснилось, что ONNX-бот gcp/expectimax-onnx-3 сидит в
us-central1 и платит трансатлантик на каждом ходу: потенциальное бесплатное улучшение времени,
если руки дойдут.
Почему --concurrency 1. Работа на ход упирается в процессор, делить инстанс между запросами
смысла нет, а Strategy.chooseMoves всё равно synchronized.
Runbook
Подготовка проекта (один раз):
gcloud config set project dice-chess-lab
gcloud services enable run.googleapis.com artifactregistry.googleapis.com
gcloud artifacts repositories create dicechess --repository-format=docker --location=europe-west3
gcloud auth configure-docker europe-west3-docker.pkg.devСборка и публикация образа. Только из bash — в PowerShell нет export, а BuildKit читает
переменную именно из окружения:
export GITHUB_TOKEN=$(gh auth token)
DOCKER_BUILDKIT=1 docker build --platform linux/amd64 \
--secret id=github_token,env=GITHUB_TOKEN \
-t europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/hunter:v1 .
docker push europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/hunter:v1Деплой. --allow-unauthenticated необходим: play-api должен достучаться до вебхука, а
подлинность обеспечивает HMAC-подпись внутри рантайма, не IAM:
gcloud run deploy hunter \
--image europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/hunter:v1 \
--region europe-west3 --allow-unauthenticated \
--concurrency 1 --cpu 2 --memory 1Gi --timeout 60sПроверка живости до регистрации — заодно прогревает контейнер перед рукопожатием:
curl -i -X POST "https://hunter-743631906150.europe-west3.run.app/api/webhook" \
-H "Content-Type: application/json" -d '{}'Ожидается 401: маршрут есть, обработчик жив, запрос без подписи корректно отвергнут.
Регистрация. Порядок важен и решает проблему курицы-яйца сам:
# 1. Постоянная идентичность. Токен показывается ОДИН раз.
curl -X POST "https://play-api.jc.id.lv/bot/register" \
-H "Content-Type: application/json" -d '{"team":"lab","name":"hunter"}'
export BOT_TOKEN=<токен из ответа>
# 2. Вебхук. play-api сам сходит на URL с nonce и проверит эхо.
# Рукопожатие проходит БЕЗ секрета — Main это явно поддерживает.
curl -X POST "https://play-api.jc.id.lv/bot/webhook" \
-H "Authorization: Bearer $BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"url":"https://hunter-743631906150.europe-west3.run.app/api/webhook"}'
# 3. Только теперь секрет попадает в сервис (создаётся новая ревизия).
gcloud run services update hunter --region europe-west3 \
--update-env-vars DICECHESS_WEBHOOK_SECRET=<секрет из ответа>Открыть для игры людям — после этого бот появляется в каталоге на play.jc.id.lv:
curl -X POST "https://play-api.jc.id.lv/bot/open-to-humans" \
-H "Authorization: Bearer $BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"description":"Hand-crafted king-hunt evaluator: ..."}'
curl -s "https://play-api.jc.id.lv/lobby/bots" | jq '.bots[] | select(.name=="hunter")'Наблюдение за работой:
gcloud beta run services logs tail hunter --region europe-west3 # tail только в beta-треке!
gcloud logging read 'resource.type=cloud_run_revision AND resource.labels.service_name=hunter AND httpRequest.requestMethod=POST' \
--limit 20 --format="table(timestamp,httpRequest.latency)" --freshness=1h⚠️ Подводные камни
Всё ниже стоило времени, так что записано именно поэтому.
gcloud run deploy --source не работает. Он собирает через Cloud Build, а Dockerfile требует
BuildKit-секрет для приватного GitHub Packages (артефакт движка). Cloud Build секреты BuildKit в
этом потоке не передаёт → sbt update падает на неразрешённой зависимости. Только локальная сборка
- push +
--image.
--platform linux/amd64 обязателен. Мак на Apple Silicon, Cloud Run на amd64. В Dockerfile
стадия сборки помечена --platform=$BUILDPLATFORM, поэтому собирается нативно быстро (байткод JVM
архитектурно нейтрален), а рантайм получается amd64 — но только если флаг передан.
export, а не VAR=val && docker. Вторая форма переменную не экспортирует, BuildKit увидит
пустой секрет. Та же грабля, что и раньше в проекте.
Wake-проба НЕ покрывает лестницу. POST /lobby/bots/{team}/{name}/wake — фича каталога,
её вызывает сайт по клику на карточку, и она честно форсирует холодный старт до того, как пойдут
часы. Планировщик лестницы её не вызывает. Значит для scale-to-zero бота на лестнице каждая партия
после простоя начинается с секунд подъёма JVM → флаги по времени → по правилу #150 бот, проигравший
по времени все партии двух подряд зеркальных пар, автоматически снимается с лестницы.
Лечение — --min-instances 1 (стоит центы за ночь, возвращается в 0 утром). Ставить только если
лестница реально играет, иначе тёплый инстанс жжёт деньги без партий.
Токен восстановлению не подлежит. И токен бота, и секрет вебхука отдаются один раз. Ротация
токена (POST /bot/token) требует текущего токена, то есть при утере не спасает. Путь
восстановления один: оператор на хосте play-api добавляет запись в PLAY_BOT_TOKENS формата
team|name|token, назначая известный токен со стороны сервера. Это правка окружения и рестарт.
gcloud run services logs tail есть только в beta. В stable-треке — только read.
Измеренное
Локальная арена (эта машина, 20 партий, контроль 1+0) против aggressive — см. arena:
| Вариант | Счёт | Флаги | p50 | max |
|---|---|---|---|---|
limit=12 | 75.0% | 0/0 | 32 мс | 228 мс |
limit=all | 75.0% | 0/0 | 538 мс | 2511 мс |
| KCP выключен | 50.0% | 0/0 | 40 мс | 232 мс |
Два вывода: дорогая фаза (вероятность взятия короля) стоит 25 процентных пунктов, а
candidateLimit=12 уже на плато — расширение до всех кандидатов не меняет ни одного решения при
17-кратной цене. Ср. насыщение K=24 у ONNX-бота в two-ply-expectimax-over-value-model.
В проде задержка примерно в 6 раз выше: медиана ~190 мс против 32 мс локально, максимум 1158 мс (похоже на прогрев JIT). Сеть почти не при чём — оба конца во Франкфурте, это разница процессоров. Для игры с человеком незаметно; для лестницы 5+3 запас всё ещё большой; для блица 1+0 запас тонкий.
Важно: добавление vCPU задержку не уменьшит — поиск однопоточный, упирается в одно ядро. Рычаги
два: опустить candidateLimit ниже 12 (нижнюю границу плато мы не искали — возможно, 6–8 дают
те же 75% вдвое быстрее) либо распараллелить вторую фазу, где кандидаты независимы.
Открыто
- Найти нижнюю границу плато
candidateLimit— самый дешёвый выигрыш по времени. - Веса необкатаны: ни один не подбирался под замер.
- На лестницу не записан (
POST /bot/ladder/join) — осознанно, до решения мерить силу черезGET /strength(SPRT по зеркальным парам, см. api#181).