You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题根源

  1. 文件资源竞争:两个定时任务同时读写result.png文件,当第一个任务未完成写入时,第二个任务尝试打开同一文件,引发IO冲突,进而导致Discord发送文件时出现网络异常。
  2. 阻塞事件循环:PIL图片处理是同步阻塞操作,长时间占用事件循环会导致Discord网关连接超时,被服务器主动重置。
  3. 全局变量无保护:多任务并发读写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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 19:54:20