DisnakeDev / disnake

An API wrapper for Discord written in Python.
https://docs.disnake.dev
MIT License
719 stars 137 forks source link

Command preparation and sync fails when bot is started on an unmanaged event loop #1138

Open Starz0r opened 10 months ago

Starz0r commented 10 months ago

Summary

When passing a event loop to the bot constructor, any command preparation will fail.

Reproduction Steps

  1. Construct a bot.
  2. Pass in a newly created event loop
  3. Annotate a function for a slash command
  4. Run the bot

Minimal Reproducible Code

from typing import Final
import asyncio
import sys
import disnake

EVLOOP: Final[asyncio.AbstractEventLoop] = asyncio.new_event_loop()
DISCORD_CLIENT: commands.InteractionBot(loop=EVLOOP)

@DISCORD_CLIENT.slash_command()
async def ping(ctx: disnake.ApplicationCommandInteraction) -> None:
    await ctx.send("pong!")

async def main():
    await DISCORD_CLIENT.start("PUT A TOKEN HERE", reconnect=True)

if __name__ == "__main__":
    sys.exit(EVLOOP.run_until_complete(main()))

Expected Results

Command preparation and sync to complete successfully.

Actual Results

Command preparations and sync does not run or finish as the tasks are bound to the wrong event loop.

Intents

None

System Information

- Python v3.9.1-final
- disnake v2.9.1-final
    - disnake importlib.metadata: v2.9.1
- aiohttp v3.9.1
- system info: Windows 10 10.0.19041 AMD64

Checklist

Additional Context

My personal traceback in question:

INFO:disnake.client:logging in using static token
ERROR:asyncio:Task exception was never retrieved
future: <Task finished name='disnake: app_command_preparation' coro=<InteractionBotBase._prepare_application_commands() done, defined at C:\my_virtualenv\lib\site-packages\disnake\ext\commands\interaction_bot_base.py:879> exception=RuntimeError("Task <Task pending name='disnake: app_command_preparation' coro=<InteractionBotBase._prepare_application_commands() running at C:\\my_virtualenv\\lib\\site-packages\\disnake\\ext\\commands\\interaction_bot_base.py:884>> got Future <Future pending> attached to a different loop")>
Traceback (most recent call last):
  File "C:\my_virtualenv\lib\site-packages\disnake\ext\commands\interaction_bot_base.py", line 884, in _prepare_application_commands
    await self.wait_until_first_connect()
  File "C:\my_virtualenv\lib\site-packages\disnake\client.py", line 1517, in wait_until_first_connect
    await self._first_connect.wait()
  File "C:\python3.9.1\lib\asyncio\locks.py", line 226, in wait
    await fut
RuntimeError: Task <Task pending name='disnake: app_command_preparation' coro=<InteractionBotBase._prepare_application_commands() running at C:\my_virtualenv\lib\site-packages\disnake\ext\commands\interaction_bot_base.py:884>> got Future <Future pending> attached to a different loop
ERROR:asyncio:Task exception was never retrieved
future: <Task finished name='Task-10' coro=<CommonBotBase._fill_owners() done, defined at C:\my_virtualenv\lib\site-packages\disnake\ext\commands\common_bot_base.py:92> exception=RuntimeError("Task <Task pending name='Task-10' coro=<CommonBotBase._fill_owners() running at C:\\my_virtualenv\\lib\\site-packages\\disnake\\ext\\commands\\common_bot_base.py:96>> got Future <Future pending> attached to a different loop")>
Traceback (most recent call last):
  File "C:\my_virtualenv\lib\site-packages\disnake\ext\commands\common_bot_base.py", line 96, in _fill_owners
    await self.wait_until_first_connect()  # type: ignore
  File "C:\my_virtualenv\lib\site-packages\disnake\client.py", line 1517, in wait_until_first_connect
    await self._first_connect.wait()
  File "C:\python3.9.1\lib\asyncio\locks.py", line 226, in wait
    await fut
RuntimeError: Task <Task pending name='Task-10' coro=<CommonBotBase._fill_owners() running at C:\my_virtualenv\lib\site-packages\disnake\ext\commands\common_bot_base.py:96>> got Future <Future pending> attached to a different loop
DEBUG:disnake.http:GET https://discord.com/api/v10/users/@me with None has returned 200
DEBUG:disnake.http:A rate limit bucket has been exhausted (bucket: None:None:/users/@me, retry: 0.001).

The Bad News: Slash commands don't sync, which is unfortunate, but…

The Good News: The bot doesn't crash! Which is good! And if the commands were already created and synced by another working instance, then those commands will still dispatch properly if someone calls them!

NOTE: Using DISCORD_CLIENT.run(...) does not error, but I need to be able to manage my own event loop for this bot, and since it's a probable parameter you can pass it, I'd expect that this would be supported as well?

elenakrittik commented 10 months ago

If i remember correctly, disnake uses asyncio.get_event_loop/asyncio.get_running_loop in certain parts, which means that the loop you passed via loop= may be a different loop than the one returned by the above-mentioned functions. Use asyncio.set_event_loop(EVLOOP) before the sys.exit(...) line to ensure the loops match. Not sure whether this is a "bug", though.

euhake commented 10 months ago

Did you get any solution? I have the same problem, when I use bot.start my commands don't sync

euhake commented 10 months ago

this here worked for me

import asyncio, disnake
from disnake.ext import commands 

EVLOOP = asyncio.new_event_loop()
asyncio.set_event_loop(EVLOOP)

bot = commands.Bot(
    command_prefix=commands.when_mentioned,
    intents=disnake.Intents.all(),
)

@bot.slash_command(description='test')
async def test(inter: disnake.ApplicationCommandInteraction) -> None:
    await inter.response.send_message('working')

async def main():
    EVLOOP = asyncio.get_event_loop()
    server_task = asyncio.create_task('run your server')
    discord_bot_task = asyncio.create_task(bot.start(''))

    await asyncio.gather(socketio_task, discord_bot_task)

EVLOOP.run_until_complete(main())
Starz0r commented 8 months ago

Took me awhile to get back to this. To be honest, I somewhat forgot until I had to come back to maintaining my bot.

My problem with the library is that it should save whatever event loop it was spawned on and only run tasks on that, which isn't the case. If I need to call a Disnake function in another event loop, I assume that internally it will be shunted off to the correct place, but it isn't. I solved my issue by wrapping calls like channel.send(...) in a Task with the correct event loop attached, and sending that off to asyncio.run_coroutine_threadsafe(...). Personally, I don't think I should need to take these steps, but perhaps this could just be a limitation of asyncio in Python in general?