Sharding et reprise de session¶

Discord répartit les serveurs d'un bot entre des connexions à la gateway appelées shards : le serveur g appartient au shard (g >> 22) % shard_count. Un bot présent sur plus de 2500 serveurs doit utiliser le sharding ; Discord ferme une connexion sans sharding avec le code 4011 et start() lève ShardingRequired.
Shards dans un seul processus¶
Bot(shard_count="auto"), la valeur par défaut, demande à Discord combien de shards utiliser et les fait tous tourner. Rien d'autre n'est nécessaire : bostie démarre les shards dans le respect des limites d'identification de Discord, et chaque shard envoie ses événements aux mêmes écouteurs, au même cache et aux mêmes gestionnaires. event.shard_id indique sur quel shard un événement est arrivé.
Shards répartis entre processus¶
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 fixe le total, et shard_ids choisit les shards que ce processus fait tourner ; shard_ids nécessite un shard_count explicite, et ses ids doivent être distincts et compris dans range(shard_count).
Discord n'autorise qu'un nombre limité d'identifications simultanées par bot. bostie respecte cette limite au sein d'un processus ; entre processus, before_identify les coordonne. bostie attend before_identify(shard_id) avant chaque identification : le hook peut ainsi attendre une place dans un verrou que vos processus partagent. Dans l'exemple, identify_lock tient lieu de ce verrou partagé, conservé pendant les 5 secondes que Discord demande entre les identifications d'un même bucket. Si le hook lève une exception, bostie journalise l'erreur et procède quand même à l'identification.
Événements de shard¶
| Événement | Champs | Envoyé quand |
|---|---|---|
ShardReady |
shard_id |
la nouvelle session d'un shard est prête et ses serveurs sont arrivés |
ShardDisconnect |
shard_id, code, reason, resuming |
une connexion a été perdue ; avec resuming, le shard reprend sa session et rejoue les événements manqués, sinon il démarre une nouvelle session |
ShardStop |
shard_id, code, reason |
un shard s'est arrêté définitivement, parce que close() a été appelé ou que Discord y a mis fin |
ShardBackpressure |
shard_id, queued_bytes |
un shard a suspendu sa lecture parce que la boucle d'événements prend du retard |
Les reconnexions et reprises de session ne demandent rien de votre part. bot.latency est l'aller-retour moyen du heartbeat sur l'ensemble des shards, et bot.gateway.shard(shard_id).latency celui d'un shard.
Reprendre la session après un redémarrage¶
Un redémarrage démarre normalement une nouvelle session sur chaque shard, et les événements des minutes intermédiaires sont perdus. Avec Bot(session_file="sessions.json"), close() garde les sessions ouvertes et les écrit dans le fichier. Le start() suivant lit le fichier, le supprime, et reprend chaque session sauvegardée qui correspond au shard_count et aux ids de shard actuels : Discord rejoue alors les événements survenus pendant l'arrêt du bot. Un fichier illisible est journalisé et ignoré, et les shards sans session sauvegardée procèdent à une identification.
bot.run() se ferme sur Ctrl+C et SIGTERM : un redémarrage par un gestionnaire de processus ou un déploiement sauvegarde donc les sessions. Un plantage ne le fait pas, et le démarrage suivant procède à une identification. Une interaction rejouée plus de 5 secondes après sa création est ignorée avec un avertissement, car il n'est plus possible de répondre à son token à temps.