🧩 Боты на платформах: параллельные партии, рантаймы и лимиты
О чём это
Один вопрос, у которого на разных платформах разные ответы: что происходит, когда у бота одновременно начинаются две партии, а он думает над ходом долго? Ответ определяется не алгоритмом, а тем, сколько независимых процессов платформа поднимает под N параллельных запросов. Отсюда же вытекают деньги: где бесплатный тариф покрывает бота целиком, где он выгорает за день, и где вообще невозможно получить счёт. Проверено 30.07.2026 по коду play-api, конфигам живых сервисов и текущим прайс-листам.
Ревизия 04.08.2026: раздел про Oracle A1 переписан и выводы в §6 изменены. Бесплатный A1 оказался месячным пулом OCPU-часов, а не потолком на сумму ядер, и с августа урезан вдвое — Oracle перестал быть площадкой с надёжным бесплатным контролем. Добавлена таксономия форм лимита (§5), объясняющая, почему из четырёх форм опасна только одна.
Первое, что нужно уложить: ход инициирует сервер, а не бот. Из Webhooks.scala в play-api — когда наступает очередь зарегистрированного бота, сервер сам делает синхронный POST на его вебхук с состоянием партии и ждёт в ответ {"moves":["e2e4",...]}.
sequenceDiagram
participant R as GameRoom (play-api)
participant W as Webhooks (dispatcher)
participant B as Бот (платформа)
R->>W: очередь бота, есть бросок костей
W->>W: timeout = min(конфиг, остаток часов)
W->>B: POST /api/webhook {state, seat}
Note over B: думает, но обязан<br/>вернуть ход до дедлайна
B-->>W: 200 {"moves":[...]}
W->>R: submitTurn (как обычный игрок)
Три свойства этого контракта задают всё остальное:
Одна попытка, без ретраев. В докстринге диспетчера это названо прямо: «reliability is the clock». Таймаут, не-200 или мусор в ответе — сервер просто ничего не делает, и партию решают часы, точно так же как для polling-бота, который перестал опрашивать. Нет очереди, нет dead-letter, нет состояния диспетчера.
Бот — не второй писатель. Раннер вебхука подписан на комнату как обычный источник команд и кормит submitTurn. Single-writer доктрина GameRoom не нарушается.
Доставка структурно ограничена. Бот получает максимум один POST на один свой ход в партии, где он сидит. Количество партий ограничено сверху LADDER_MAX_CONCURRENT_PAIRS (по умолчанию 4) и потоком челленджей — накачать очередь запросов извне невозможно.
Три потолка времени
Долгое размышление ограничено тремя независимыми потолками, и срабатывает самый низкий:
Обрезает шортлист, но всегда возвращает ход — anytime-контракт TimeBudgetedSearch
play-api (Webhooks)
min(WEBHOOK_TIMEOUT_SECONDS, остаток часов бота), минимум 1 мс
Бросает попытку, ничего не применяет — дальше решают часы
Платформа
Cloud Run 60 с · Workers 10 мс CPU · Lambda до 15 мин
Рвёт запрос принудительно
Бот не «успевает посмотреть всех кандидатов» — он умеет вовремя остановиться
Это принципиально. HunterSearch — TimeBudgetedSearch, дедлайн доходит до него, и дорогая фаза (рескор шортлиста через KingCaptureProbability) режется по факту истечения времени. Поэтому в замерах ноль флагов не означает «всё просчитано» — означает «отдал лучшее из просмотренного до дедлайна». Гарантия здесь — легальный ход всегда, а не полный перебор.
2. Главная переменная: сколько процессов на N партий
Вот развилка, из которой растут все различия между платформами.
В Strategy.chooseMoves стоит synchronized — и это не перестраховка, комментарий в коде объясняет причину: «a webhook server can be handed two turns at once and the search reuses nothing thread-safe by contract». То есть внутри одного процесса все ходы по всем партиям считаются строго по одному.
Дальше всё зависит от того, что платформа делает со вторым одновременным запросом:
Режим A: один процесс на все партии
Кто так работает: homelab (rpi3/rpi4/dexus), Oracle A1 VM, любой обычный контейнер с concurrency > 1.
Второй запрос приходит в тот же процесс и встаёт в очередь за локом. Пока бот думает над ходом в первой партии, запросы по второй и третьей ждут. Если худший ход занимает 24 секунды (замерено для hunter-all с candidateLimit=all на dexus), то соседние партии получают ответ на 24 секунды позже — и эти секунды списываются с их часов.
Риск реализуется тем сильнее, чем толще хвост задержки. Для limit=12 (max ~230 мс) очередь незаметна; для limit=all — заметна.
Режим B: процесс на партию
Кто так работает: Cloud Run с containerConcurrency: 1, Azure Functions, AWS Lambda, Cloudflare Workers.
Второй одновременный запрос не встаёт в очередь — платформа поднимает второй инстанс. Партии считаются в разных процессах, полностью параллельно, и synchronized никогда не встречает конкуренции. Потолок параллелизма — настройка масштабирования (maxScale: 10 у нашего Cloud Run сервиса).
Плата — двойная:
Холодный старт. Каждый новый инстанс поднимается с нуля. Для JVM это 5–20 с — на синхронном контракте с одной попыткой это прямой проигрыш по времени. Для native-образа или JS — десятки миллисекунд.
Тарификация. Секунды считаются с каждого инстанса отдельно. Две параллельные партии = двойное списание за это время.
Wake-проба покрывает каталог, но не лестницу
POST /lobby/bots/{team}/{name}/wake сайт зовёт по клику в каталоге — до старта часов, чтобы принудительно разбудить scale-to-zero бота. Планировщик лестницы её не вызывает. Поэтому лестничная партия после простоя начинается с холодного старта, и он съедается из банка времени. Два подряд флага по времени → сервер автоматически парки́рует бота (#150).
3. Три рантайма одного движка
Неочевидное следствие: платформа диктует рантайм, а рантайм — какие алгоритмы вообще влезают. У нас в проекте живут три разных сборки одного и того же dicechess-engine-scala:
Из README dicechess-bot-azure — формулировка, которую стоит запомнить целиком: контракт синхронный, с жёстким бюджетом и одной попыткой доставки, поэтому холодный старт JVM в 5–20 с — «real competitive liability». Native-образ стартует за десятки миллисекунд, а движок — чистая битбордовая арифметика без рефлексии, поэтому образ собирается с --no-fallback без конфигов.
Из README dicechess-bot-cloudflare — измеренные на самом движке цифры под лимит Workers:
Алгоритм
p50
худшее
Влезает в 10 мс free?
aggressive-book
~0.4 мс
~3.4 мс
да, с запасом
monte-carlo
сотни мс
—
нет
hunter (limit=12)
~32 мс
~230 мс
нет
Почему hunter не поедет на Workers free
KingCaptureProbability — это DFS по 216 исходам броска, и оценщик хочет два вызова на позицию. Даже на плато (limit=12) это ~32 мс медианы, то есть втрое над бесплатным потолком Workers. На платном плане (30 с по умолчанию) влезет легко. Это ровно тот случай, когда платформа отсекает алгоритм, а не наоборот.
4. Платформы по одной
🏠 Homelab
Хост
CPU
RAM
Роль
rpi3 (192.168.20.99)
4× Cortex-A53
905 МБ
lab/hunter-k6
rpi4 (192.168.10.9)
4× Cortex-A72
3.7 ГБ
lab/hunter-k48 + homelab-сервисы
dexus (192.168.10.4)
4× i5-2500
3.2 ГБ
lab/hunter-all
aurora (192.168.10.3)
—
—
прод, ботов не селим
Параллелизм: режим A — один контейнер, лок сериализует. Реально кусает только hunter-all.
Публичный доступ: через named tunnel на aurora — Public Hostname в Zero Trust указывает на http://<LAN-IP>:<порт>. L3-роутинг между подсетями .10.x и .20.x работает (в отличие от WoL-броадкаста, который через границу не проходит).
Цена: электричество. Малинки — единицы ватт, dexus (i5-2500, TDP 95 Вт) — десятки, и он теперь не засыпает: крон авто-выключения на rpi4 отключён под always-on бота.
Лимит памяти на rpi3 — реальная граница, а не теория
При 905 МБ физической памяти два контейнера с лимитами 512 + 384 МБ = 896 МБ занимали почти всё, и хост ушёл в swap на microSD (535 МБ в свапе). Свап на SD — медленный I/O прямо на пути ответа вебхука. Вылечено переносом hunter-k48 на rpi4. Сумма лимитов контейнеров должна оставлять хосту запас, иначе платформа-без-биллинга наказывает задержкой.
☁️ Google Cloud Run
Живой сервис hunter (проект dice-chess-lab, регион europe-west3), фактический конфиг:
containerConcurrency: 1 # → режим B, инстанс на партию
cpu: 2, memory: 1Gi
maxScale: 10 # потолок параллельных партий
timeoutSeconds: 60
startup-cpu-boost: true # смягчает холодный старт JVM
minScale: не задан # → scale-to-zero
Бесплатно в месяц: 180 000 vCPU-секунд, 360 000 GiB-секунд, 2 млн запросов.
Что это значит в единицах бота — считаем через --cpu 2:
2vCPU180000vCPU-s=90000s=25h
То есть 25 часов размышления в месяц — при --cpu 1 было бы 50.
Запросы никогда не станут узким местом: партия — это ~13 POST’ов на сторону, то есть 2 млн запросов ≈ 150 тыс. партий. Всегда бьёт CPU, и всегда первым.
Бесплатный тариф — на проект, а не на сервис. И его выедает сосед
Вот почему lab/hunter стоит €3/мес, хотя по расчёту первые ~25 часов должны быть бесплатны: gcp/scala-monte-carlo при €113/мес сжигает ≈4.7 млн vCPU-секунд, то есть 26× месячной бесплатной квоты — примерно за первые сутки. Дальше все сервисы проекта платят с первой секунды. Счёт hunter’а — не признак дорогого hunter’а, а признак того, что квоту уже съели.
Как удержаться в лимите:
--cpu 1 вместо 2 — удваивает бесплатные часы (для 1-ply оценщика потери силы почти нет);
--max-instances — жёсткий потолок параллелизма, значит и скорости выгорания;
--concurrency > 1 — партии делят инстанс (но тогда возвращается лок из режима A);
убрать тяжёлого соседа с лестницы (POST /bot/ladder/leave) — самый крупный рычаг;
LADDER_MAX_CONCURRENT_PAIRS на стороне play-api — глобальный дроссель для всех ботов сразу.
Чего НЕ делает бюджет: billing budget в GCP только присылает письмо. Он не останавливает трату. Жёсткий стоп требует связки budget → Pub/Sub → Cloud Function, отключающей биллинг проекта. Подробности и текущие цифры — отдельная заметка.
Открытый вопрос: применим ли free tier в europe-west3
Источники противоречат друг другу: часть утверждает, что бесплатная квота Cloud Run действует только в us-central1/us-east1/us-west1, часть — что Франкфурт тоже входит. Эмпирически наш сервис в europe-west3тарифицируется, но это объясняется и выеданием квоты соседом (см. выше), поэтому чистого эксперимента нет. Проверять по своему счёту, а не по статьям.
🟠 Oracle Cloud (Ampere A1)
Always Free — это месячный пул на всю тенантность:1 500 OCPU-часов и 9 000 GB-часов в месяц, обнуляемых 1-го числа. Плюс вне пула: 200 ГБ блочного хранилища, 10 ТБ исходящего трафика/мес, VCN и сеть целиком, 2 публичных IPv4.
Параллелизм: это обычная VM → режим A. Один процесс, лок сериализует — ровно как homelab, но в облаке и без счёта за электричество. oracle/v1–v3 играют так за €0.
Пул, а не потолок: короткий всплеск стоит долгой доли месяца
«4 OCPU бесплатно» — не ограничение на размер машины и не потолок в моменте, а бюджет ресурсо-часов на календарный месяц. Измерено по посуточным данным: 15–22.07 мы держали 12–18 ядер (277–432 OCPU-часа в сутки) и платили €0.00; списания начались 23.07 не когда 17.07 подняли 16-ядерный флот, а когда накопленная сумма прошла порог. И наоборот: 64-ядерный инстанс, проживший 3 августа меньше семи часов, съел 29% месячного пула, показав в тот день €0.00.
Значит 1 500 OCPU-часов — это либо одна машина 2 OCPU на весь месяц, либо 16-ядерная арена на ~4 суток, но не то и другое. Платится площадь, а не высота.
С августа 2026 пул урезан вдвое
В консоли висит баннер «Always Free Ampere A1 Compute entitlements have changed». Измеренный июльский пул был 3 000 OCPU-часов (≈4 OCPU непрерывно) — текущая дока Oracle даёт 1 500 (≈2 OCPU). Практическое следствие: одна машина 2 OCPU / 12 ГБ круглосуточно это 1 488 из 1 500, 99% пула, поэтому две таких машины в бесплатный режим уже не влезают.
Как удержаться: планировать месячный бюджет OCPU-часов, а не размеры инстансов, и сверять накопленный расход — команда в Oracle Cloud: что бесплатно. Цена перерасхода измерена по счёту: €0.0093 за OCPU-час (≈€0.22 за OCPU-день).
Поправка к прежней версии этого раздела
Здесь было сказано, что контроль у Oracle «самый честный из всех облаков», потому что превысить лимит можно «только сознательно провизионив больше ресурсов, случайного выгорания по трафику не бывает». Это следствие неверной модели потолка. Пул выжигается без всякого нового провизионинга — достаточно одного короткого широкого эксперимента, и остаток месяца платный. Жёсткого стопа у Oracle нет вообще.
🔵 Azure Functions
Наш azure/scala-aggressive-book — Function App на custom handler с GraalVM native-образом (--runtime custom, enableForwardingHttpRequest).
Бесплатный грант (Consumption plan), на подписку в месяц: 1 млн исполнений + 400 000 GB-секунд.
Для native-образа это фактически неограниченно под нашу нагрузку: ход считается миллисекундами, при минимальной тарифной единице 128 МБ × 100 мс это ~0.013 GB-с на исполнение, то есть грант по GB-с покрывает десятки миллионов ходов. Узкое место — 1 млн исполнений, а это ≈77 тыс. партий в месяц. Лестница столько не сыграет.
Единственный настоящий жёсткий стоп из всех облаков
У Azure Functions есть dailyMemoryTimeQuota — суточная квота в GB-секундах, при превышении которой Function App переводится в Disabled до 00:00 UTC. Это не письмо, а реальная остановка.
az functionapp update --resource-group <rg> --name <app> \ --set dailyMemoryTimeQuota=13000
Нюансы: увеличение квоты применяется только со следующих суток, и после снятия нужен рестарт приложения. Для бота это идеальный предохранитель — худшее последствие в том, что бот замолчит и его запаркует лестница, а не в том, что придёт счёт.
🟡 AWS
Lambda — always free, навсегда: 1 млн запросов + 400 000 GB-секунд в месяц. Не истекает, в отличие от остального.
Профиль тот же, что у Azure Functions: под native-бинарь (custom runtime provided.al2023) грант практически не расходуется, узкое место — 1 млн вызовов, чего лестнице хватит с многократным запасом. Режим B, параллелизм ограничивается reserved concurrency.
Контейнерные варианты:
App Runner — с 30.04.2026 закрыт для новых клиентов, как площадку для нового бота не рассматриваем;
EC2 t4g.small — бесплатный трайл до 750 ч/мес продлён до 31.12.2026; это ARM-машина, то есть режим A, аналог Oracle A1 с дедлайном;
Fargate — бесплатного тарифа нет.
Модель free tier у AWS изменилась в июле 2025
Вместо прежних «12 месяцев бесплатно» теперь $200 кредитов + 6-месячный free plan. Always-free сервисы (в том числе Lambda) сохранились, но общая рамка стала кредитной и конечной — планировать долгоживущего бота стоит на always-free строчках, а не на кредитах.
Как удержаться: reserved concurrency на Lambda (потолок параллелизма), AWS Budgets с budget actions — в отличие от GCP, они умеют не только уведомлять, но и применять политику (например, отобрать права на запуск). Жёсткого «стоп-биллинга» на аккаунт всё равно нет.
🟣 Cloudflare Workers
Наши cloudflare/scala-aggressive-book и cloudflare/greedy — Workers на TypeScript-обёртке (~40 строк) вокруг Scala.js-сборки движка. WebCrypto — глобал в Workers, поэтому nodejs_compat не нужен.
Бесплатный план: 100 000 запросов/сутки, 10 мс CPU на запрос.
Параллелизм: режим B, но без холодного старта вообще — изолят поднимается мгновенно. Для синхронного контракта с одной попыткой это лучший профиль из всех платформ.
Единственная платформа, где на бесплатном плане невозможно получить счёт
Free plan у Workers — жёсткий по конструкции: превышение суток отдаёт ошибку, превышение 10 мс CPU убивает этот запрос, а не аккаунт. И заметьте, что означает убитый запрос в нашем контракте: один ход проигран по часам, партия продолжается — это не падение бота. Контроль расхода тут не нужен, потому что расхода нет.
Ограничение — обратная сторона: 10 мс отсекает всё, кроме дешёвых оценщиков (см. таблицу в разделе 3).
5. Сводка: бесплатные лимиты
Платформа
Бесплатно в месяц
Режим
Что бьёт первым
Жёсткий стоп?
Cloudflare Workers
100 тыс. запросов/сутки, 10 мс CPU
B, без cold start
CPU на запрос
✅ по конструкции
Azure Functions
1 млн исполнений + 400 тыс. GB-с
B, cold start ~мс
исполнения (≈77 тыс. партий)
✅ dailyMemoryTimeQuota
AWS Lambda
1 млн запросов + 400 тыс. GB-с
B, cold start ~мс
запросы
⚠️ только reserved concurrency + budget actions
Oracle A1
1 500 OCPU-ч + 9 000 GB-ч (пул на тенантность)
A, always-on
накопленные OCPU-часы
❌ пул выжигается всплеском, счёт молчит
GCP Cloud Run
180 тыс. vCPU-с, 360 тыс. GiB-с, 2 млн запросов
B, cold start 5–20 с (JVM)
vCPU-секунды
❌ бюджет только уведомляет
Homelab
— (электричество)
A
RAM, затем swap
—
Форма лимита важнее его размера
Цифры в таблице выше гниют за недели — прайс-листы меняются, Oracle урезал пул вдвое прямо между двумя ревизиями этой заметки. А форма лимита живёт годами, и именно она определяет, каким образом ты влетишь в счёт. Форм всего четыре:
Форма
Где
Как влетаешь
Потолок на запрос
Cloudflare Workers (10 мс CPU)
Никак. Превышение убивает запрос, а не аккаунт — расхода не существует по конструкции
Грант на исполнения
Azure Functions, AWS Lambda (1 млн вызовов + GB-с)
Практически никак под native-бинарь: ход стоит миллисекунды, грант покрывает десятки миллионов ходов
Месячный пул ресурсо-часов
Oracle A1, GCP Cloud Run
Легко и молча. Пул общий и накопительный, выжигается всплеском или соседом; счёт показывает €0.00, пока пул не высох
Кредиты с датой смерти
AWS ($200 / 6 мес), промо Oracle (€250 / 30 дн)
Отложенно: пока кредит жив, он гасит перерасход и создаёт впечатление, что конфигурация бесплатна. Счёт приходит в день, когда кредит кончился, а конфигурация — нет
Опасна только третья форма, и мы попались на ней дважды — по одному разу на каждый её механизм:
Сосед выедает пул.lab/hunter стоил €3/мес, хотя по расчёту первые ~25 часов бесплатны: gcp/scala-monte-carlo сжигал 26× месячной квоты проекта за первые сутки, после чего платили все сервисы проекта с первой секунды. Счёт hunter’а был признаком не дорогого hunter’а, а съеденной квоты.
Всплеск выедает пул. 64-ядерный инстанс на Oracle, проживший меньше семи часов, забрал 29% месячного бюджета — и показал в тот день €0.00.
Общее правило для пулов: нулевой счёт ничего не доказывает
computed-amount не отличает «ничего не потратили» от «съели треть месяца». Смотреть надо накопленное количество ресурсо-часов против размера пула. И не списывать нули на задержку биллинга: проверяется возвратом к тому же дню через сутки.
6. Практические выводы
Что где держать. Тяжёлые CPU-голодные алгоритмы (KCP, monte-carlo) не должны жить на посекундной тарификации Cloud Run. Но и Oracle A1 больше не бесплатная мышца: с августа 2026 пула хватает ровно на одну машину 2 OCPU круглосуточно, так что секунда размышления там тоже расходуется — просто из месячного бюджета, а не из счёта. Единственное место, где секунда действительно ничего не стоит, — homelab. Cloud Run хорош ровно для того, для чего мы его используем сейчас: честное сравнение алгоритмов на одинаковом процессоре, за понятные деньги и на ограниченное время.
Дешёвые оценщики — на edge.aggressive-book на Workers free — идеальный профиль: без холодного старта, без счёта, с запасом по CPU в 3× на медиане.
Native-образ снимает главный минус serverless. Azure Functions и Lambda с GraalVM-бинарём дают режим B (параллельные партии) без платы холодным стартом и практически без расхода бесплатного гранта. Для 1-ply оценщика это объективно лучшая комбинация цены и профиля задержки.
Один процесс на все партии — это архитектурный выбор, а не деталь.synchronized в Strategy безопасен и дёшев ровно до тех пор, пока хвост задержки короткий. Как только появляется вариант с толстым хвостом (limit=all, 24 с в максимуме), режим A превращается в очередь, которая тратит часы соседних партий. Либо режим B, либо короткий хвост — совмещать «один процесс» и «долгий поиск» нельзя.
Считать в единицах бота, а не в единицах прайс-листа. «180 000 vCPU-секунд» ничего не говорит; «25 часов размышления в месяц при --cpu 2» — говорит всё. Полезно один раз перевести бесплатный лимит каждой платформы в часы/партии и держать эту таблицу под рукой.
И главное про контроль: жёсткий стоп есть только у Cloudflare (по конструкции: превышение убивает запрос, а не аккаунт) и Azure (dailyMemoryTimeQuota переводит приложение в Disabled до полуночи). У GCP бюджет — это письмо, а не тормоз; у AWS — политика, а не тормоз; у Oracle — ничего: пул кончается молча, счёт ещё сутки показывает €0.00, а PAYG-аккаунт просто начинает списывать. Если задача звучит как «бот не должен стоить денег ни при каких обстоятельствах», выбор площадки этим и определяется — и Oracle из этого списка выпал.
Ещё одна поправка к прежней версии
Здесь Oracle стоял в списке платформ с жёстким контролем, наравне с Cloudflare и Azure. Это было следствием модели «потолок на сумму OCPU»: если превысить можно только сознательным провизионингом, то и контроль надёжен. С месячным пулом это рассыпается.
Внутренние источники фактов: Webhooks.scala и AGENTS.md в dicechess-play-api, Strategy.scala в dicechess-hunter, README dicechess-bot-azure и dicechess-bot-cloudflare, gcloud run services describe hunter.