how to gracefully close task loops in discord.py

Viewed 46

I'm currently creating a discord bot that contains two task loops called check_members and check_music.
When a user enters the offline command, I'd like to gracefully stop these loops. I wrote this piece of code in my Cog class:

class MusicBot(commands.Cog):
    # function called when bot is closing.
    See [here](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html?highlight=cog_unload#discord.ext.commands.Cog.cog_unload)
    def cog_unload(self):
        print("Debug")
        self.check_members.cancel()
        self.check_music.cancel()
        print(self.check_members.is_running())
        print(self.check_music.is_running())

    # example of a task loop I have:
    @tasks.loop(seconds=5)
    async def check_members(self):
        [code...]

In another script, I call the bot.close() function as follows:

await ctx.send("Going offline! See ya later.")
if self.voice is not None:
await self.disconnect()
await self.bot.close()
sys.exit(0)

When a user calls the offline command, that's what the bot prints out:

Debug
True
Task exception was never retrieved
future: <Task finished name='discord.py: on_message' coro=<Client._run_event() done, defined at /home/liuk23/.local/lib/python3.10/site-packages/discord/client.py:401> exception=SystemExit(0)>
Traceback (most recent call last):
  File "/home/liuk23/Desktop/coding/Discord-bot-3/main.py", line 67, in <module>
    loop.run_forever()
  File "/usr/lib/python3.10/asyncio/base_events.py", line 600, in run_forever
    self._run_once()
  File "/usr/lib/python3.10/asyncio/base_events.py", line 1896, in _run_once
    handle._run()
  File "/usr/lib/python3.10/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/home/liuk23/.local/lib/python3.10/site-packages/discord/client.py", line 409, in _run_event
    await coro(*args, **kwargs)
  File "/home/liuk23/.local/lib/python3.10/site-packages/discord/ext/commands/bot.py", line 1392, in on_message
    await self.process_commands(message)
  File "/home/liuk23/.local/lib/python3.10/site-packages/discord/ext/commands/bot.py", line 1389, in process_commands
    await self.invoke(ctx)  # type: ignore
  File "/home/liuk23/.local/lib/python3.10/site-packages/discord/ext/commands/bot.py", line 1347, in invoke
    await ctx.command.invoke(ctx)
  File "/home/liuk23/.local/lib/python3.10/site-packages/discord/ext/commands/core.py", line 986, in invoke
    await injected(*ctx.args, **ctx.kwargs)  # type: ignore
  File "/home/liuk23/.local/lib/python3.10/site-packages/discord/ext/commands/core.py", line 190, in wrapped
    ret = await coro(*args, **kwargs)
  File "/home/liuk23/Desktop/coding/Discord-bot-3/music.py", line 223, in offline
    await self.functions.offline(ctx)
  File "/home/liuk23/Desktop/coding/Discord-bot-3/funcitons.py", line 241, in offline
    sys.exit(0)
SystemExit: 0

As you can notice, the Debug text gets printed out, so the cog_unload function get successfully called.
Although I am closing the loops, I get the Task was never retrieved error. Am I misunderstanding the error?

2 Answers

From sys.exit documentation:

Raise a SystemExit exception, signaling an intention to exit the interpreter.

What is happening is that the offline task, by throwing SystemExit, stops, but since no other task is awaiting on it, that exception is never retrieved.

The underlying problem is that, to quit the application, rather than throwing through sys.exit, it would be better to stop the running loop cleanly. For example, if the loop was run with loop.run_until_complete(some_future), it's necessary to set that future with some_future.set_result(some_result).

I fixed it by stopping the main loop that holds all the bot.
In my main script I instantiate the bot in the following manner:

    try:
        # loop runs until stop is called
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    # finally block is called when loop is being stopped
    finally:
        # stop and close loop
        loop.stop()
        sys.exit(0)

By calling loop.stop() in another script, the finally block in main.py will be called and will successfully close the loop and the whole script.
Thanks to @Ulisse Bordignon for your answer

Related