Os bots da Codraise rodam 24/7. De vez em quando um deles simplesmente sumia: parava de responder, o Discord marcava como reconectando e, uns 90 segundos depois, ele voltava como se nada tivesse acontecido.
Sem erro, sem traceback, sem crash. O pior tipo de bug.
O sintoma
O log mostrava sempre a mesma coisa antes do sumiço:
Shard ID None heartbeat blocked for more than 10 seconds.
Essa mensagem do discord.py é uma pista enorme. Ela quer dizer que o event loop ficou travado: alguma coisa ocupou a thread principal e o bot não conseguiu mandar o heartbeat pro Discord. Sem heartbeat, o Discord derruba a conexão.
O culpado
Em asyncio, tudo roda numa thread só. Se uma função síncrona e lenta roda dentro de uma corrotina, ela segura todo mundo: comandos, eventos, heartbeat.
No meu caso era o leitor de RSS de notícias:
async def buscar_noticias():
feed = feedparser.parse(URL_DO_FEED) # síncrono: faz a requisição HTTP aqui mesmo
...
feedparser.parse baixa o feed na hora, de forma bloqueante. Quando o site do feed demorava a responder, o bot inteiro ficava parado esperando.
A correção
Mandar o trabalho pesado pra outra thread com asyncio.to_thread, e colocar cache pra não buscar o mesmo feed toda hora:
import asyncio, time
_cache = {"quando": 0.0, "feed": None}
async def buscar_noticias():
if time.time() - _cache["quando"] < 600:
return _cache["feed"]
feed = await asyncio.to_thread(feedparser.parse, URL_DO_FEED)
_cache.update(quando=time.time(), feed=feed)
return feed
Agora o download acontece fora do event loop e o bot continua respondendo enquanto espera.
A regra que ficou
Depois disso virou regra nos meus bots: qualquer coisa pesada vai pra to_thread. Requisição com biblioteca síncrona, renderização de imagem com Pillow, leitura de arquivo grande, tudo. Se aparecer heartbeat blocked, a primeira pergunta é: o que está rodando síncrono dentro de um async def?
