pycord icon indicating copy to clipboard operation
pycord copied to clipboard

refactor: Update ``Client.run`` to have a better async I/O usage

Open DA-344 opened this issue 1 year ago • 31 comments

Summary

This PR refactors the Client.run logic to fix problems involving asyncio due to how the library used the loop:

Needs testing

Exception
  File "/home/container/main.py", line 23, in <module>
    bot.run(BOT_TOKEN)

  File "/home/container/.local/lib/python3.11/site-packages/discord/client.py", line 772, in run
    loop.run_forever()

  File "/usr/local/lib/python3.11/asyncio/base_events.py", line 608, in run_forever
    self._run_once()

  File "/usr/local/lib/python3.11/asyncio/base_events.py", line 1936, in _run_once
    handle._run()

  File "/usr/local/lib/python3.11/asyncio/events.py", line 84, in _run
    self._context.run(self._callback, *self._args)

  File "/usr/local/lib/python3.11/asyncio/selector_events.py", line 956, in _read_ready
    self._read_ready_cb()

  File "/usr/local/lib/python3.11/asyncio/selector_events.py", line 988, in _read_ready__get_buffer
    self._protocol.buffer_updated(nbytes)

  File "/usr/local/lib/python3.11/asyncio/sslproto.py", line 439, in buffer_updated
    self._do_handshake()

  File "/usr/local/lib/python3.11/asyncio/sslproto.py", line 560, in _do_handshake
    self._sslobj.do_handshake()

  File "/usr/local/lib/python3.11/ssl.py", line 979, in do_handshake
    self._sslobj.do_handshake()

Information

  • [x] This PR fixes an issue.
  • [ ] This PR adds something new (e.g. new method or parameters).
  • [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed).
  • [ ] This PR is not a code change (e.g. documentation, README, typehinting, examples, ...).

Checklist

  • [x] I have searched the open pull requests for duplicates.
  • [ ] If code changes were made then they have been tested.
    • [x] I have updated the documentation to reflect the changes.
  • [ ] If type: ignore comments were used, a comment is also left explaining why.
  • [x] I have updated the changelog to include these changes.

DA-344 avatar Nov 11 '24 19:11 DA-344

Applied all the changes Dorukyum requested.

Before merging, I would like some feedback on this discussion message

DA-344 avatar Nov 19 '24 15:11 DA-344

@DA-344 can u fix merge conflicts for the changelog? @Pycord-Development/contributors can we get some testing here :D

Lulalaby avatar Mar 23 '25 17:03 Lulalaby

@DA-344 can u fix merge conflicts for the changelog?

Okay, it should be fixed now. 👍

DA-344 avatar Mar 23 '25 17:03 DA-344

i tested this pr on my prod bot for some hours and nothing to issue to signal

Lumabots avatar May 10 '25 11:05 Lumabots

I've done some testing and it has been working as expected. Can this pr get any review more/merge ?

DA-344 avatar May 27 '25 20:05 DA-344

This is required if we ever want py 3.14 support, see my pr about that

Paillat-dev avatar May 27 '25 20:05 Paillat-dev

Not just for 3.14 support, but to fix all asyncio related issues that can be presented in any version due to how the AbstractEventLoop is currently handled.

DA-344 avatar May 27 '25 20:05 DA-344

Could be neat if we could get this merged for 2.7

Paillat-dev avatar Jul 25 '25 21:07 Paillat-dev

Also please @DA-344 fix conflicts (sry 😅)

Paillat-dev avatar Jul 25 '25 21:07 Paillat-dev

@DA-344 The test code below crashes, can you take a look ? I think it has to do with self.loop now being a property, maybe self._loop should be passed to HTTPClient but I am not 100% sure. https://github.com/Pycord-Development/pycord/blob/2ae01b796647466861eead7da7cd1c7065a1543a/discord/client.py#L249

Traceback (most recent call last):
  File "C:\Users\Jérémie\Documents\GitHub\pycord\thing.py", line 12, in <module>
    bot = discord.Bot(intents=discord.Intents.all())
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\bot.py", line 1171, in __init__
    super().__init__(*args, **options)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\bot.py", line 97, in __init__
    super().__init__(*args, **kwargs)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\cog.py", line 610, in __init__
    super().__init__(*args, **kwargs)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 250, in __init__
    loop=self.loop,
         ^^^^^^^^^
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 330, in loop
    raise RuntimeError("loop is not set")
RuntimeError: loop is not set
Test Code
import os
import discord
import asyncio
import logging

from dotenv import load_dotenv

logging.basicConfig(level=logging.INFO)

load_dotenv()

bot = discord.Bot(intents=discord.Intents.all())

@bot.event
async def on_ready():
    print(f"Logged in as {bot.user} ({bot.user.id})")

options = [
    "Option 1",
    "Option 2",
    "Option 3",
    "Option 4",
]

async def my_autocomplete(ctx: discord.AutocompleteContext):
    """Autocomplete function for the command"""
    query = ctx.value.lower()
    return [option for option in options if query in option.lower()]


class MyCog(discord.Cog):
    """Example cog for the bot"""

    def __init__(self, bot: discord.Bot):
        self.bot: discord.Bot = bot

    @discord.command(name="hello")
    @discord.option(
        name="option",
        description="An example option",
        required=True,
        autocomplete=my_autocomplete,
    )
    async def hello(self, ctx: discord.ApplicationContext, option: str):
        await ctx.respond(f"Hello! You selected: {option}")


async def load_cogs():
    bot.add_cog(MyCog(bot))


async def main():
    await load_cogs()

    try:
        await bot.start(os.getenv("TOKEN_3", ""))
    except KeyboardInterrupt:
        await bot.close()


if __name__ == "__main__":
    asyncio.run(main())

Paillat-dev avatar Jul 25 '25 21:07 Paillat-dev

@DA-344 The test code below crashes, can you take a look ? I think it has to do with self.loop now being a property, maybe self._loop should be passed to HTTPClient but I am not 100% sure.

Yup, I forgot to update that to use _loop instead of loop, should be fixed now.

DA-344 avatar Jul 26 '25 15:07 DA-344

Traceback (most recent call last):
  File "C:\Users\Jérémie\Documents\GitHub\pycord\thing.py", line 12, in <module>
    bot = discord.Bot(intents=discord.Intents.all())
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\bot.py", line 1171, in __init__
    super().__init__(*args, **options)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\bot.py", line 97, in __init__
    super().__init__(*args, **kwargs)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\cog.py", line 610, in __init__
    super().__init__(*args, **kwargs)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 262, in __init__
    self._connection: ConnectionState = self._get_state(**options)
                                        ~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 321, in _get_state
    loop=self.loop,
         ^^^^^^^^^
  File "C:\Users\Jérémie\Documents\GitHub\pycord\discord\client.py", line 332, in loop
    raise RuntimeError("loop is not set")
RuntimeError: loop is not set

Still getting the same @DA-344. I tried replacing self.loop with self._loop in line 321 there but that lead to some other issues...

Paillat-dev avatar Aug 06 '25 23:08 Paillat-dev

will review soon

DA-344 avatar Aug 14 '25 13:08 DA-344

Kk no stress

Paillat-dev avatar Aug 14 '25 14:08 Paillat-dev

Okay, everything should be now fixed

DA-344 avatar Aug 30 '25 21:08 DA-344

Tested and works.

Tested Code

Code is basically simple, because everything involving loop happens at startup.

import discord

intents = discord.Intents.default()
intents.members = True
intents.message_content = True

bot = discord.Bot(intents=intents, chunk_guilds_at_startup=False, debug_guilds=[...])


@bot.check
async def check(ctx):
    if ctx.guild.id not in bot.debug_guilds:
        return False
    if not ctx.guild.chunked:
        await ctx.guild.chunk()
    return True


@bot.command(name='test-loop')
async def test_loop(ctx: discord.ApplicationContext) -> None:
    await ctx.respond(content='pong')

if __name__ == '__main__':
    bot.run('token')

DA-344 avatar Aug 30 '25 21:08 DA-344

Pls dont merge before I test this

Paillat-dev avatar Aug 31 '25 20:08 Paillat-dev

raising RuntimeError: loop is not set

Traceback (most recent call last):
  File "/Users/luma/GitHub/pycord-fork/test.py", line 36, in <module>
    bot.loop.run_until_complete(main())
    ^^^^^^^^
  File "/Users/luma/GitHub/pycord-fork/discord/client.py", line 334, in loop
    raise RuntimeError("loop is not set")
RuntimeError: loop is not set
async def main():
    try:
        await init()
        await bot.start(BOT_TOKEN)
    finally:
        await shutdown()
        if not bot.is_closed():
            await bot.close()


if __name__ == "__main__":
    bot.loop.run_until_complete(main())

Lumabots avatar Aug 31 '25 21:08 Lumabots

its also blocking all @tasks.loop() (from my testing)

@tasks.loop(seconds=5)
async def test():
    print("start", test.current_loop)
    await asyncio.sleep(60)
    print("stop", test.current_loop)


test.start()
bot.run()

Lumabots avatar Aug 31 '25 21:08 Lumabots

Pls fix Luma's issue

This has been fixed on the latest commit.

DA-344 avatar Sep 01 '25 22:09 DA-344

I'll review and test that around 1pm

Lumabots avatar Sep 02 '25 06:09 Lumabots

tested and no issue with #2771

Lumabots avatar Sep 08 '25 18:09 Lumabots

@Lulalaby when your merge this (once review is good) pls make sure to squash so that we can put it in rc2 and then revert it easily if problems

Paillat-dev avatar Sep 08 '25 22:09 Paillat-dev

Can't seem to manually call login + connect:

Error

Traceback (most recent call last):
  File "C:\xxx\pr2645.py", line 19, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "C:xxx\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\asyncio\runners.py", line 204, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "C:\xxx\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\asyncio\runners.py", line 127, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "C:\xxx\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\asyncio\base_events.py", line 719, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "C:\xxx\pr2645.py", line 16, in main
    await client.connect()
  File "xxx\.venv\Lib\site-packages\discord\client.py", line 761, in connect
    self.ws = await asyncio.wait_for(coro, timeout=60.0)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\xxx\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\asyncio\tasks.py", line 488, in wait_for
    return await fut
           ^^^^^^^^^
  File "C:\xxx\.venv\Lib\site-packages\discord\gateway.py", line 348, in from_client
    ws = cls(socket, loop=client.loop)
                          ^^^^^^^^^^^
  File "C:xxx\.venv\Lib\site-packages\discord\client.py", line 369, in loop
    raise RuntimeError("loop is not set")
RuntimeError: loop is not set
Unclosed client session

Code

import asyncio
import discord


client = discord.Client()


@client.event
async def on_ready():
    print(f"We have logged in as {client.user}")


async def main():
    await client.login("xxx")
    await client.connect()


asyncio.run(main())

Soheab avatar Oct 09 '25 14:10 Soheab

ext.tasks seems to be broken and the changes to it aren't in the changelog?

  1. This task runs automatically
@tasks.loop(seconds=1)
# @tasks.loop(seconds=1, create_loop=False) # <- default
async def my_background_task():
    print("Background task is running...")
 

# my_background_task.start() <- does nothing
client.run(...)
  1. This task does not run
@tasks.loop(seconds=1)
# @tasks.loop(seconds=1, create_loop=False) # <- default
async def my_background_task():
    print("Background task is running...")
 

# optional: my_background_task.start()
  1. This task does not run
@tasks.loop(seconds=1, create_loop=True)
async def my_background_task():
    print("Background task is running...")
 

client.run(...)
  1. This task does not ran and logs a warning
@tasks.loop(seconds=1, create_loop=True)
async def my_background_task():
    print("Background task is running...")
 

my_background_task.start()
# console:
# Task was destroyed but it is pending!
# task: <Task pending name='pycord-ext-task (0x1f4fb9d3380): my_background_task' coro=<Loop._loop() running at C:\Users\Sohea\OneDrive\Documents\test\pycord\.venv\Lib\site-packages\discord\ext\tasks\__init__.py:213>>
# <sys>:0: RuntimeWarning: coroutine 'Loop._loop' was never awaited

This is all super confusing and so is the doc of the create_loop parameter.

Soheab avatar Oct 09 '25 14:10 Soheab

  1. This task does not run

Isn't that the same code as task number one ?

Paillat-dev avatar Oct 09 '25 15:10 Paillat-dev

Isn't that the same code as task number one ?

Yes but without running the bot, which is what I assumed create_loop allowed.

Soheab avatar Oct 09 '25 16:10 Soheab

Added medium prio, this fixes multiple issues and is a much needed cleanup, will check everything out to try and find out more about those loop issues.

Paillat-dev avatar Oct 18 '25 12:10 Paillat-dev

Can't seem to manually call login + connect:

Also got that same issue.

@DA-344 What do you think about this ? Is there any reason why we couldn't do that:

    @property
    def loop(self) -> asyncio.AbstractEventLoop:
        """The event loop that the client uses for asynchronous operations."""
        if self._loop is None:
            try:
                self._loop = asyncio.get_running_loop()
            except RuntimeError as e:
                raise RuntimeError("loop is not set") from e
        return self._loop

This should also normally allow you to remove the other places where it try: excepts asyncio.get_running_loop since it would be in the property itself

Paillat-dev avatar Oct 18 '25 13:10 Paillat-dev

Note, Needs testing w/ async autocompletes and typing context manager as well as ext.loop

Paillat-dev avatar Oct 18 '25 21:10 Paillat-dev