From 16122545ec54df772da4445df0ca19b4cb0855a9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 24 May 2023 19:15:46 +0200 Subject: [PATCH] Add check for asyncio event loop to remain the same --- readthedocs/quick-references/faq.rst | 40 +++++++++++++++++++++++++++ telethon/client/telegrambaseclient.py | 6 ++++ telethon/client/users.py | 4 +++ 3 files changed, 50 insertions(+) diff --git a/readthedocs/quick-references/faq.rst b/readthedocs/quick-references/faq.rst index 19eff266..1e9268a5 100644 --- a/readthedocs/quick-references/faq.rst +++ b/readthedocs/quick-references/faq.rst @@ -261,6 +261,46 @@ same session anywhere else. If you need to use the same account from multiple places, login and use a different session for each place you need. +What does "The asyncio event loop must not change after connection" mean? +========================================================================= + +Telethon uses ``asyncio``, and makes use of things like tasks and queues +internally to manage the connection to the server and match responses to the +requests you make. Most of them are initialized after the client is connected. + +For example, if the library expects a result to a request made in loop A, but +you attempt to get that result in loop B, you will very likely find a deadlock. +To avoid a deadlock, the library checks to make sure the loop in use is the +same as the one used to initialize everything, and if not, it throws an error. + +The most common cause is ``asyncio.run``, since it creates a new event loop. +If you ``asyncio.run`` a function to create the client and set it up, and then +you ``asyncio.run`` another function to do work, things won't work, so the +library throws an error early to let you know something is wrong. + +Instead, it's often a good idea to have a single ``async def main`` and simply +``asyncio.run()`` it and do all the work there. From it, you're also able to +call other ``async def`` without having to touch ``asyncio.run`` again: + +.. code-block:: python + + # It's fine to create the client outside as long as you don't connect + client = TelegramClient(...) + + async def main(): + # Now the client will connect, so the loop must not change from now on. + # But as long as you do all the work inside main, including calling + # other async functions, things will work. + async with client: + .... + + if __name__ == '__main__': + asyncio.run(main()) + +Be sure to read the ``asyncio`` documentation if you want a better +understanding of event loop, tasks, and what functions you can use. + + What does "bases ChatGetter" mean? ================================== diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py index 146828fb..dd127654 100644 --- a/telethon/client/telegrambaseclient.py +++ b/telethon/client/telegrambaseclient.py @@ -399,6 +399,7 @@ class TelegramBaseClient(abc.ABC): self._borrowed_senders = {} self._borrow_sender_lock = asyncio.Lock() + self._loop = None # only used as a sanity check self._updates_error = None self._updates_handle = None self._keepalive_handle = None @@ -535,6 +536,11 @@ class TelegramBaseClient(abc.ABC): if self.session is None: raise ValueError('TelegramClient instance cannot be reused after logging out') + if self._loop is None: + self._loop = helpers.get_running_loop() + elif self._loop != helpers.get_running_loop(): + raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)') + if not await self._sender.connect(self._connection( self.session.server_address, self.session.port, diff --git a/telethon/client/users.py b/telethon/client/users.py index e2587a13..eda3e040 100644 --- a/telethon/client/users.py +++ b/telethon/client/users.py @@ -30,6 +30,10 @@ class UserMethods: return await self._call(self._sender, request, ordered=ordered) async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): + if self._loop is not None and self._loop != helpers.get_running_loop(): + raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)') + # if the loop is None it will fail with a connection error later on + if flood_sleep_threshold is None: flood_sleep_threshold = self.flood_sleep_threshold requests = (request if utils.is_list_like(request) else (request,))