Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make discord.py bot run forever if client.run() returns

Problem

My discord.py bot's client.run() is returning unexpectedly every few days or so followed by an error "Task was destroyed but it is pending!".

Question

Why is client.run() returning at all? And how can I change my bot to properly handle this issue and run forever?

Code (stripped out a lot to keep it simple for discussion):

import asyncio
import discord

TOKEN = 'my bot token goes here'
CHANNEL_ID = 'my channel id goes here'

async def DiscordMsgSendTask():

  await client.wait_until_ready()
  my_channel = discord.Object(id=CHANNEL_ID)

  while not client.is_closed:

    # wait a bit to prevent busy loop
    await asyncio.sleep(2)

    # check for and handle new event here

    # if an event was handled then send a message to the channel
    embed = discord.Embed(description='Event was handled')
    await client.send_message(my_channel, embed=embed)

client = discord.Client()

while True:
  client.loop.create_task(DiscordMsgSendTask())

  try:
    client.run(TOKEN)
  except Exception as e:
    logging.warning('Exception: ' + str(e))

  client = discord.Client()

Additional Info

The bot's purpose is basically just to check for a new file in a specific directory, and then send a message to a specific discord channel to report it. This only occurs around 10 times a day, so the bot's traffic is extremely low.

In an attempt to make the bot tolerate errors/disconnects, I put it into the while True loop, which may be wrong approach.

Using Python 3.6.5 and Discord.py 0.16.12

Edit - Added traceback

Adding traceback from a previous failure earlier in the day.

2018-06-20 04:33:08 [ERROR] Task exception was never retrieved
future: <Task finished coro=<WebSocketCommonProtocol.run() done, defined at /usr/local/lib64/python3.6/site-packages/websockets/protocol.py:428> exception=ConnectionResetError(104, 'Connection reset by peer')>
Traceback (most recent call last):
  File "/usr/local/lib64/python3.6/site-packages/websockets/protocol.py", line 434, in run
    msg = yield from self.read_message()
  File "/usr/local/lib64/python3.6/site-packages/websockets/protocol.py", line 456, in read_message
    frame = yield from self.read_data_frame(max_size=self.max_size)
  File "/usr/local/lib64/python3.6/site-packages/websockets/protocol.py", line 511, in read_data_frame
    frame = yield from self.read_frame(max_size)
  File "/usr/local/lib64/python3.6/site-packages/websockets/protocol.py", line 546, in read_frame
    self.reader.readexactly, is_masked, max_size=max_size)
  File "/usr/local/lib64/python3.6/site-packages/websockets/framing.py", line 86, in read_frame
    data = yield from reader(2)
  File "/usr/lib64/python3.6/asyncio/streams.py", line 674, in readexactly
    yield from self._wait_for_data('readexactly')
  File "/usr/lib64/python3.6/asyncio/streams.py", line 464, in _wait_for_data
    yield from self._waiter
  File "/usr/lib64/python3.6/asyncio/selector_events.py", line 723, in _read_ready
    data = self._sock.recv(self.max_size)
ConnectionResetError: [Errno 104] Connection reset by peer
2018-06-20 04:33:08 [ERROR] Task was destroyed but it is pending!
task: <Task pending coro=<DiscordMsgSendTask() running at /home/bot.py:119> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7fc99bfd7a68>()]>>
2018-06-20 04:33:08 [ERROR] Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7fc999b59240>
like image 246
user5071535 Avatar asked Jan 02 '23 05:01

user5071535


1 Answers

You can't use the pre-defined run method since that only handles KeyboardInterrupt. You will need to create a exit strategy that closes all tasks while still leaving the loop running.

The following example will raise SystemExit (to simulate your unexpected fatal error (The RST - ConnectionError error)) when a message "die" is received by the bot. The SystemExit will get "caught" and restarts your bot. Upon KeyboardInterrupt is raised, the bot will exit successfully without error.

import asyncio
import discord

TOKEN = 'TOKEN_HERE'
client = discord.Client()


async def task():
    await client.wait_until_ready()
    while True:
        await asyncio.sleep(1)
        print('Running')


def handle_exit():
    print("Handling")
    client.loop.run_until_complete(client.logout())
    # For python 3.9, use asyncio.all_tasks instead
    for t in asyncio.Task.all_tasks(loop=client.loop):
        if t.done():
            t.exception()
            continue
        t.cancel()
        try:
            client.loop.run_until_complete(asyncio.wait_for(t, 5, loop=client.loop))
            t.exception()
        except (asyncio.InvalidStateError, asyncio.TimeoutError, asyncio.CancelledError):
            pass


while True:
    @client.event
    async def on_message(m):
        if m.content == 'die':
            print("Terminating")
            raise SystemExit

    client.loop.create_task(task())
    try:
        client.loop.run_until_complete(client.start(TOKEN))
    except SystemExit:
        handle_exit()
    except KeyboardInterrupt:
        handle_exit()
        client.loop.close()
        print("Program ended")
        break
    
    print("Bot restarting")
    client = discord.Client(loop=client.loop)

In Discord (someone types die):

die

In terminal (STDOUT):

Running
Running
Running
Terminating
Handling
Bot restarting
Running
Running
Running
Running
Running
<CTRL-C> (KeyboardInterrupt)
Handling
Program ended

Side note:

If you're still confused about your error, it's not your code to blame. Errno 104 is a server side fatal error that's usually unpreventable by the end users.

like image 183
Taku Avatar answered Jan 18 '23 10:01

Taku