Discord机器人ClientOSError求助:定时任务触发对等方连接重置
Discord机器人定时任务触发ClientOSError错误排查与修复
问题描述
开发的Discord机器人用于统计用户语音频道时长、消息发送量,生成周/月排行榜并以图片形式推送。通过client.loop.create_task启动month_vipe和week_vipe两个定时任务时,先运行的任务会触发错误:
aiohttp.client_exceptions.ClientOSError: [Errno 104] Discord bot connection reset due to peer error
完整代码
import asyncio import discord from discord.ext import commands import json import time from PIL import Image from PIL import ImageDraw from PIL import ImageFont client = commands.Bot(command_prefix='+', intents = discord.Intents(messages = True, guild_messages = True, members = True, guilds = True, voice_states = True)) myFont = ImageFont.truetype('minecraft.ttf', 39) ever = {} week = {} month = {} active = {} issent = 0 @client.event async def on_ready(): global ever global week global month print("Ready") for guild in client.guilds: for member in guild.members: ever[member.id] = {"voice": 0, "text": 0} week[member.id] = {"voice": 0, "text": 0} month[member.id] = {"voice": 0, "text": 0} client.loop.create_task(month_vipe()) client.loop.create_task(week_vipe()) @client.event async def on_member_join(member): ever[member.id] = {"voice": 0, "text": 0} week[member.id] = {"voice": 0, "text": 0} month[member.id] = {"voice": 0, "text": 0} @client.event async def on_message(ctx): ever[ctx.author.id]["text"] = ever[ctx.author.id]["text"] + 1 week[ctx.author.id]["text"] = week[ctx.author.id]["text"] + 1 month[ctx.author.id]["text"] = month[ctx.author.id]["text"] + 1 @client.event async def on_voice_state_update(member, before, after): global ever global week global month user = member.id if before.channel is None and after.channel is not None: print(f"{user} joined") active[user] = time.time() if after.channel is None and before.channel is not None: print(f"{user} left") ever[user]["voice"] += time.time() - active[user] week[user]["voice"] += time.time() - active[user] month[user]["voice"] += time.time() - active[user] async def week_vipe(): global week while True: reyt = {} print('week vipr func') for guild in client.guilds: for member in guild.members: voice = round(week[member.id]["voice"] / 120) reyt[member.id] = week[member.id]["text"] + voice sorted_reyt = {k: b for k, b in sorted(reyt.items(), key=lambda element: element[1], reverse=True)} my_image = Image.open("maket.png") image_editable = ImageDraw.Draw(my_image) text = f"недiльний рейтинг" image_editable.text((230,131), text, (125, 95, 53), font=myFont) my_image.save("result.png") for i in range(3): keys_list = list(sorted_reyt) username = client.get_user(keys_list[i]) reyting = sorted_reyt[keys_list[i]] if i+1 == 1: my_image = Image.open("result.png") image_editable = ImageDraw.Draw(my_image) text = f"{i+1}. {username}" image_editable.text((620,211), text, (125, 95, 53), font=myFont) my_image.save("result.png") if i+1 == 2: my_image = Image.open("result.png") image_editable = ImageDraw.Draw(my_image) text = f"{i+1}. {username}" image_editable.text((620,429), text, (125, 95, 53), font=myFont) my_image.save("result.png") if i+1 == 3: my_image = Image.open("result.png") image_editable = ImageDraw.Draw(my_image) text = f"{i+1}. {username}" image_editable.text((620,647), text, (125, 95, 53), font=myFont) my_image.save("result.png") chat = client.get_channel(1005796493757780019) await chat.send(file=discord.File('result.png')) print('week reyt sent') for guild in client.guilds: for member in guild.members: week[member.id] = {"voice": 0, "text": 0} await asyncio.sleep(60)#604800 async def month_vipe(): global month global issent while True: reyt = {} print('month vipr func') for guild in client.guilds: for member in guild.members: voice = round(month[member.id]["voice"] / 120) reyt[member.id] = month[member.id]["text"] + voice sorted_reyt = {k: b for k, b in sorted(reyt.items(), key=lambda element: element[1], reverse=True)} my_image = Image.open("maket.png") image_editable = ImageDraw.Draw(my_image) text = f"місячний рейтинг" image_editable.text((230,131), text, (125, 95, 53), font=myFont) my_image.save("result.png") for i in range(3): keys_list = list(sorted_reyt) username = client.get_user(keys_list[i]) reyting = sorted_reyt[keys_list[i]] if i+1 == 1: my_image = Image.open("result.png") image_editable = ImageDraw.Draw(my_image) text = f"{i+1}. {username}" image_editable.text((620,211), text, (125, 95, 53), font=myFont) my_image.save("result.png") if i+1 == 2: my_image = Image.open("result.png") image_editable = ImageDraw.Draw(my_image) text = f"{i+1}. {username}" image_editable.text((620,429), text, (125, 95, 53), font=myFont) my_image.save("result.png") if i+1 == 3: my_image = Image.open("result.png") image_editable = ImageDraw.Draw(my_image) text = f"{i+1}. {username}" image_editable.text((620,647), text, (125, 95, 53), font=myFont) my_image.save("result.png") chat = client.get_channel(1005796493757780019) print('sending month reyt') await chat.send(file=discord.File('result.png')) print('month reyt sent') issent = 1 for guild in client.guilds: for member in guild.members: month[member.id] = {"voice": 0, "text": 0} await asyncio.sleep(240)#2592000
问题根源
- 文件资源竞争:两个定时任务同时读写
result.png文件,当第一个任务未完成写入时,第二个任务尝试打开同一文件,引发IO冲突,进而导致Discord发送文件时出现网络异常。 - 阻塞事件循环:PIL图片处理是同步阻塞操作,长时间占用事件循环会导致Discord网关连接超时,被服务器主动重置。
- 全局变量无保护:多任务并发读写
week、month等全局字典,可能导致数据不一致,间接引发后续逻辑错误。
修复方案
1. 避免文件资源竞争
给每个任务生成独立的文件,或用异步互斥锁确保同一时间只有一个任务操作文件:
from asyncio import Lock file_lock = Lock() # 在week_vipe中 async with file_lock: my_image.save("result_week.png") await chat.send(file=discord.File("result_week.png")) # 在month_vipe中 async with file_lock: my_image.save("result_month.png") await chat.send(file=discord.File("result_month.png"))
2. 异步化阻塞操作
将PIL图片处理放到线程池执行,避免阻塞事件循环:
from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor() async def generate_ranking_image(title, sorted_reyt): def _generate(): my_image = Image.open("maket.png") image_editable = ImageDraw.Draw(my_image) image_editable.text((230,131), title, (125, 95, 53), font=myFont) for i in range(3): keys_list = list(sorted_reyt) username = client.get_user(keys_list[i]) y_pos = 211 + i * 218 image_editable.text((620, y_pos), f"{i+1}. {username}", (125, 95, 53), font=myFont) filename = f"result_{title.replace(' ', '_')}.png" my_image.save(filename) return filename return await client.loop.run_in_executor(executor, _generate)
3. 保护全局变量
用异步锁保护全局字典的读写操作:
week_lock = Lock() month_lock = Lock() # 在on_message中 async with week_lock: week[ctx.author.id]["text"] += 1 async with month_lock: month[ctx.author.id]["text"] += 1 # 在on_voice_state_update中 async with week_lock: week[user]["voice"] += time.time() - active[user] async with month_lock: month[user]["voice"] += time.time() - active[user]
4. 延迟启动定时任务
确保机器人完全就绪后再启动任务,避免过早触发导致客户端未初始化完成:
@client.event async def on_ready(): # ... 原有初始化代码 await asyncio.sleep(5) # 延迟5秒确保连接稳定 client.loop.create_task(month_vipe()) client.loop.create_task(week_vipe())
内容的提问来源于stack exchange,提问作者Wolky
相关产品推荐
相关产品推荐

