Шардирование и возобновление¶

Discord распределяет серверы бота по соединениям со шлюзом, которые называются шардами: сервер g принадлежит шарду (g >> 22) % shard_count. Бот, который состоит больше чем в 2500 серверах, обязан шардироваться; Discord закрывает соединение без шардов с кодом 4011, а start() вызывает ShardingRequired.
Шарды в одном процессе¶
Bot(shard_count="auto"), значение по умолчанию, спрашивает у Discord, сколько шардов использовать, и запускает их все. Больше ничего не нужно: bostie запускает шарды в пределах лимитов Discord на identify, и каждый шард отправляет свои события тем же слушателям, кэшу и обработчикам. event.shard_id говорит, через какой шард пришло событие.
Шарды в нескольких процессах¶
import asyncio
import logging
import os
import bostie
from bostie.events import ShardDisconnect, ShardReady
log = logging.getLogger("my-bot")
cluster = int(os.environ.get("CLUSTER", "0"))
identify_lock = asyncio.Lock()
async def wait_for_turn(shard_id: int) -> None:
async with identify_lock:
await asyncio.sleep(5)
bot = bostie.Bot(
shard_count=16,
shard_ids=range(cluster * 4, cluster * 4 + 4),
before_identify=wait_for_turn,
session_file=f"sessions-{cluster}.json",
)
@bot.listen
async def shard_ready(event: ShardReady) -> None:
log.info("shard %d is ready", event.shard_id)
@bot.listen
async def shard_lost(event: ShardDisconnect) -> None:
action = "resuming" if event.resuming else "starting a new session"
log.info("shard %d disconnected (%s), %s", event.shard_id, event.code, action)
bot.run()
shard_count=16 фиксирует общее число, а shard_ids выбирает шарды, которые запускает этот процесс; shard_ids требует явного shard_count, а его ID должны быть различными и лежать в range(shard_count).
Discord разрешает боту ограниченное число одновременных identify. В пределах одного процесса bostie соблюдает этот лимит сам; между процессами их согласует before_identify. bostie дожидается before_identify(shard_id) перед каждым identify, так что хук может ждать слота в блокировке, общей для ваших процессов. В примере identify_lock заменяет такую общую блокировку и удерживается те 5 секунд, которые Discord просит выдерживать между identify в одном бакете. Если хук выбрасывает исключение, bostie пишет ошибку в лог и всё равно выполняет identify.
События шардов¶
| Событие | Поля | Когда отправляется |
|---|---|---|
ShardReady |
shard_id |
новая сессия шарда готова и его серверы получены |
ShardDisconnect |
shard_id, code, reason, resuming |
соединение оборвалось; при resuming шард возобновляет сессию и повторно получает пропущенные события, иначе начинает новую сессию |
ShardStop |
shard_id, code, reason |
шард остановился окончательно, потому что был вызван close() или Discord его завершил |
ShardBackpressure |
shard_id, queued_bytes |
шард приостановил чтение, потому что цикл событий отстаёт |
Переподключения и возобновления не требуют от вас никаких действий. bot.latency — среднее время кругового пути heartbeat по шардам, а bot.gateway.shard(shard_id).latency — для одного шарда.
Возобновление после перезапуска¶
Обычно перезапуск начинает новую сессию на каждом шарде, и события за минуты простоя теряются. С Bot(session_file="sessions.json") метод close() оставляет сессии открытыми и записывает их в файл. Следующий start() читает файл, удаляет его и возобновляет каждую сохранённую сессию, которая соответствует текущим shard_count и ID шардов, так что Discord повторно присылает события, произошедшие, пока бот не работал. Файл, который не удалось прочитать, пишется в лог и игнорируется, а шарды без сохранённой сессии выполняют identify.
bot.run() закрывается по Ctrl+C и SIGTERM, поэтому перезапуск менеджером процессов или при деплое сохраняет сессии. Падение — нет, и следующий запуск выполняет identify. Взаимодействие, повторно полученное позже чем через 5 секунд после создания, пропускается с предупреждением, потому что на его токен уже не успеть ответить.