-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Client.close() might not finish #9690
Comments
|
please think about it. at the absolute minimum this is a documentation issue, the close() method lists no restrictions like "must only be (indirectly) called from connect()" |
Could you explain more by "must be only be (indirectly) called from connect()"? The regular flow of using |
I consider code in methods like on_ready/on_message "called from connect()". This happens through To clarify this doesn't have anything to do with using import threading, asyncio, logging
import discord
token = # redacted
runner = asyncio.Runner()
def r(f):
return asyncio.run_coroutine_threadsafe(f, runner.get_loop()).result()
logging.basicConfig(level=logging.DEBUG)
intents = discord.Intents()
intents.guilds = True
cl = discord.Client(intents=intents)
may_wait_until_ready = threading.Event()
async def setup_hook():
may_wait_until_ready.set()
cl.setup_hook = setup_hook
t = threading.Thread(target=runner.run, args=(cl.start(token),))
print("start")
t.start()
print("wait for may_wait_until_ready")
may_wait_until_ready.wait()
print("wait_until_ready")
r(cl.wait_until_ready())
print("okay, now shut down")
r(cl.close()) When Ctrl-C is pressed the following is shown:
[EDIT: simplified the code by removing the cl_start_token function] |
Your reporting code just seems to be misuse and breakage of asyncio's threadsafe guarantees. Consider the following calls: import threading, asyncio, logging
import discord
token = # redacted
runner = asyncio.Runner()
def r(f):
return asyncio.run_coroutine_threadsafe(f, runner.get_loop()).result()
logging.basicConfig(level=logging.DEBUG)
intents = discord.Intents()
intents.guilds = True
cl = discord.Client(intents=intents)
runner.run(cl.login(token)) # 1
t = threading.Thread(target=runner.run, args=(cl.connect(),)) # 2
t.start()
r(cl.wait_until_ready()) # 3
print("okay, now shut down")
r(cl.close()) # 4 You create an event loop over at call 1 in the "main thread" and this is where the entirety of the thread unsafe loop state is held. Over at call 2 you call Essentially you have this backwards. You need to do something like this: runner = asyncio.Runner()
def r(f):
return asyncio.run_coroutine_threadsafe(f, runner.get_loop()).result()
logging.basicConfig(level=logging.DEBUG)
intents = discord.Intents()
intents.guilds = True
cl = discord.Client(intents=intents)
runner.run(cl.login(token)) # 1
t = threading.Thread(target=r, args=(cl.connect(),)) # 2
t.start()
runner.run(cl.wait_until_ready()) # 3
print("okay, now shut down")
runner.run(cl.close()) # 4 In this code calls 1, 3, and 4 are done in the main thread using the runner in the thread the loop was created on and call 2 uses the threadsafe call as intended. In this example code the exit works as expected. |
Hi, first of all, thanks for taking the time to look at this. I understand that Either way though, I posted a revised repro in a comment that doesn't split up login+connect over two invocations of Runner.run. That should work based on what you said, but still doesn't. The last bit of code you posted terminates, but by virtue of not having any true multi-threading. Anything between 3 and 4 will block heartbeats. Replacing |
The documentation mentions that the event loop objects aren't thread safe in multiple places. I do not see your revised code anywhere. Note that in an |
It's right above your first comment. #9690 (comment) I didn't know about |
That code is still incorrect for the same reason I gave. |
I don't see how.
I don't think the thread of creation matters, only the thread that is currently executing run(). I have adapted the repro once more to show that even when creation and running happen on thread 2, the same issue is visible.
I'm not sure what you mean here, the loop is -not- on the same thread. import threading, asyncio, logging
import discord
token = # redacted
logging.basicConfig(level=logging.DEBUG)
intents = discord.Intents()
intents.guilds = True
cl = discord.Client(intents=intents)
runner = None
def create_loop_and_run():
global runner
runner = asyncio.Runner()
runner.run(cl.start(token))
may_wait_until_ready = threading.Event()
async def setup_hook():
may_wait_until_ready.set()
cl.setup_hook = setup_hook
def r(f):
return asyncio.run_coroutine_threadsafe(f, runner.get_loop()).result()
t = threading.Thread(target=create_loop_and_run)
print("start")
t.start()
print("wait for may_wait_until_ready")
may_wait_until_ready.wait()
print("wait_until_ready")
r(cl.wait_until_ready())
print("okay, now shut down")
r(cl.close()) Regardless of this example, would you agree that there is an undocumented requirement on the use of the close() method? Something like "Note: close() might complete after start(). The caller is responsible for keeping the event loop running until the end of both." |
I'm not sure how to explain it any better than I already did.
There seems to be a misunderstanding on how event loops work and how they interact with asyncio objects and the The library maintains a loop reference and expects it to be the running loop.
There is no extraneous requirement for |
Oh, you mean discordpy, not asyncio. When I said "I don't think the thread of creation matters" I meant
There's only one loop in this case though, no? Also, python checks that awaits happen to Futures of matching loops regardless of whether debug=True is set on the loop (see Task.__step in asyncio/tasks.py, import threading, asyncio, logging
import discord
token = # redacted
cl = None
runner = None
may_wait_until_ready = threading.Event()
def discord_thread():
global cl, runner
logging.basicConfig(level=logging.DEBUG)
intents = discord.Intents()
intents.guilds = True
cl = discord.Client(intents=intents)
async def setup_hook():
may_wait_until_ready.set()
cl.setup_hook = setup_hook
runner = asyncio.Runner(debug=True)
runner.run(cl.start(token))
def r(f):
return asyncio.run_coroutine_threadsafe(f, runner.get_loop()).result()
t = threading.Thread(target=discord_thread)
print("start")
t.start()
print("wait for may_wait_until_ready")
may_wait_until_ready.wait()
print("wait_until_ready")
r(cl.wait_until_ready())
print("okay, now shut down")
r(cl.close()) # close gets stuck in "await self.ws.close(code=1000)" |
Hello @rrika. Have you fixed the problem with |
so, this is very ugly and but it's the workaround I settled on. I also haven't updated my version of discord.py since writing this. so it might not work anymore. import threading, asyncio, logging
import discord
token = ...
server = ...
channel = ...
runner = asyncio.Runner()
def r(f):
return asyncio.run_coroutine_threadsafe(f, runner.get_loop()).result()
logging.basicConfig(level=logging.DEBUG)
intents = discord.Intents()
intents.guilds = True
cl = discord.Client(intents=intents)
close_task = None
may_wait_until_ready = threading.Event()
async def setup_hook():
may_wait_until_ready.set()
cl.setup_hook = setup_hook
async def cl_connect():
global close_task
await cl.start(token)
print("exited connect")
if close_task:
await close_task
print("awaited close task")
else:
print("no close task")
async def cl_close():
print("cl.close")
await cl.close()
print("cl.close done")
def runner_run():
runner.run(cl_connect())
print("exited runner")
t = threading.Thread(target=runner_run)
t.start()
print("wait for may_wait_until_ready")
may_wait_until_ready.wait()
print("wait_until_ready")
r(cl.wait_until_ready())
main_thread = threading.main_thread()
def wait_for_main():
global close_task
main_thread.join()
print("done, close client")
close_task = runner.get_loop().create_task(cl_close()) # QUESTIONABLE, see comments below
threading.Thread(target=wait_for_main).start()
# whatever
sv = cl.get_guild(server)
ch = sv.get_channel(channel)
m = r(ch.fetch_message(msgid)) # do it on the remote thread with r(...) To the best of my memory:
So then during shutdown:
|
This working at all would be entirely implementation detail, and is outside of the guarantees asyncio actually makes for thread-safety. I would strongly suggest avoiding use of asyncio event loops in threads without taking the time to read what is and isn't guaranteed and ensure asyncio resources are created according to those guarantees. |
Thank you, I will try your workaround tomorrow |
If I attempted this again I'd do the launch of cl_close in two stages. First get over to the correct loop with run_coroutine_threadsafe or call_soon_threadsafe. Then once there wrap the cl.close in a task that can be awaited between cl.start and the end of the loop. |
Summary
await Client.close() can hang
Reproduction Steps
The beginning of Client.close looks like this:
Once it reaches the first 'await', execution can switch to the task running Client.connect, leave the
while not self.is_closed()
loop, leave the runner.run() context and shut down the asyncio loop. Anything awaiting the Client.close is now stuck.This is relevant when using discord.py from the REPL where the main-thread is blocked on keyboard input, but websocket communication should continue. At exit it's necessary to close the connection from outside of the Client.connect thread/loop.
Offering a version of close() that isn't async would help.
Minimal Reproducible Code
Expected Results
Program ends
Actual Results
Program hangs
Intents
guilds
System Information
Checklist
Additional Context
No response
The text was updated successfully, but these errors were encountered: