You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
traceback.clear_frames manages to deadlock a background task #79932
My use case:
I have a background task, say called "coordination". In that task, I want to catch any errors and push those to the user waiting in the main task and only continue running the background coroutine after the user manually resolves the exception.
Issue:
When testing the behaviour with unittest.Case and using assertRaises to catch the exception, the background coroutine manages to just freeze. I have narrowed it down to traceback.clear_frames in assertRaises that causes a GeneratorExit in the background coroutine.
I believe this issue is a duplicate to https://bugs.python.org/issue29211, but wanted to provide another actual use case where it can pop up. Also even if the generator raises a GeneratorExit, why did the background thread freeze is still a mystery to me.
So yes, the clear_frames function will force a running generator to close. See https://github.com/python/cpython/blob/3.7/Objects/frameobject.c#L566, it explicitly does a Finalize. Would that be the desired behaviour for assertRaises is not clear. I find it strange that catching an exception is closing my running coroutine.
The reproduce example can be lowered to something like::
importasyncioasyncdefbackground(error_future):
try:
raiseValueErrorexceptExceptionasexc:
error_future.set_exception(exc)
awaitasyncio.sleep(1)
asyncdefmain():
loop=asyncio.get_event_loop()
error_future=loop.create_future()
task=asyncio.create_task(background(error_future))
awaitasyncio.wait([error_future])
exc=error_future.exception()
importtracebacktraceback.clear_frames(exc.__traceback__)
# Will block forever, as task will never be waken upawaittaskif__name__=="__main__":
asyncio.run(main())
Very interesting. Thanks for reporting this issue. I'll definitely take a look at this before 3.8 is released. For 3.7 you'll likely have to find a workaround.
tvoinarovskyi commented on Jan 17, 2019
tvoinarovskyimannequin
MannequinAuthor
More actions
For now, it seems like copy.copy(exception) before raising seems to prevent this behaviour. So for all exceptions originating from background tasks I raise a copy to the user, rather than the original exception. It prints correct stack, so no real impact on the library.
Why not change assertRaises() to not close suspended generator/coroutines?
I think we could either change generator.frame() to disallow it or traceback.clear_frames() to skip those frames. assertRaises is too high level to care about this.
Currently traceback.clear_frames() swallows exceptions from clear(), and the exception comes from clearing a running (but not a suspended) frame. So either we change frame.clear() to also raise on a suspended frame, or we change the code in traceback.clear_frame() to check for suspended/running instead of relying on the exception.
I've created a PR to raise an exception when the frame is suspended deprecate clearing suspended frames, add an option to raise a RuntimeError in that case, and use this option in traceback.clear_frames.
Activity
tvoinarovskyi commentedon Jan 16, 2019
My use case:
I have a background task, say called "coordination". In that task, I want to catch any errors and push those to the user waiting in the main task and only continue running the background coroutine after the user manually resolves the exception.
Issue:
When testing the behaviour with
unittest.Caseand usingassertRaisesto catch the exception, the background coroutine manages to just freeze. I have narrowed it down totraceback.clear_framesinassertRaisesthat causes a GeneratorExit in the background coroutine.I believe this issue is a duplicate to https://bugs.python.org/issue29211, but wanted to provide another actual use case where it can pop up. Also even if the generator raises a GeneratorExit, why did the background thread freeze is still a mystery to me.
Script to reproduce in my case is attached.
tvoinarovskyi commentedon Jan 16, 2019
So yes, the
clear_framesfunction will force a running generator to close. See https://github.com/python/cpython/blob/3.7/Objects/frameobject.c#L566, it explicitly does a Finalize. Would that be the desired behaviour for assertRaises is not clear. I find it strange that catching an exception is closing my running coroutine.The reproduce example can be lowered to something like::
1st1 commentedon Jan 16, 2019
Very interesting. Thanks for reporting this issue. I'll definitely take a look at this before 3.8 is released. For 3.7 you'll likely have to find a workaround.
tvoinarovskyi commentedon Jan 17, 2019
For now, it seems like
copy.copy(exception)before raising seems to prevent this behaviour. So for all exceptions originating from background tasks I raise a copy to the user, rather than the original exception. It prints correct stack, so no real impact on the library.kumaraditya303 commentedon Oct 27, 2022
traceback.clear_framescan close generators too so this is not special to coroutines andasyncio. I guess the simple solution here is "don't do that".gvanrossum commentedon Oct 27, 2022
@iritkatriel ^^
iritkatriel commentedon Oct 28, 2022
I think this is the fix:
But when I apply it we get two failures from tests that seem to imply that the current behaviour is a feature:
pythongh-79932: do not allow to clear a suspended frame
kumaraditya303 commentedon Oct 29, 2022
I had already tried this and hence I wrote
simple solution here is "don't do that".:)iritkatriel commentedon Oct 29, 2022
I think we should consider changing it.
iritkatriel commentedon Oct 29, 2022
Note the confusion in the comments in test_clear_executing_generator:
# Attempting to clear an executing generator frame is forbidden.and then (when the frame is suspended)
# Clearing the frame closes the generatorThe second one was added later - the bpo issue justifies with just "the variable f wasn't used, let's use it for something".
Was there ever a conscious decision that clearing a suspended frame should be allowed, and that it closes the generator?
We could add a close() functionSince generator has a close() function, we could deprecate clear() on a suspended frame.markshannon commentedon Oct 31, 2022
That closing a frame closes the generator, yes. Something to do with PEP 442.
That clearing a suspended frame should be allowed, I don't know.
Why not change
assertRaises()to not close suspended generator/coroutines?iritkatriel commentedon Oct 31, 2022
I think we could either change generator.frame() to disallow it or traceback.clear_frames() to skip those frames. assertRaises is too high level to care about this.
Currently traceback.clear_frames() swallows exceptions from clear(), and the exception comes from clearing a running (but not a suspended) frame. So either we change frame.clear() to also raise on a suspended frame, or we change the code in traceback.clear_frame() to check for suspended/running instead of relying on the exception.
pythongh-79932: deprecate clearing a suspended frame and add option t…
pythongh-79932: deprecate clearing a suspended frame and add option t…
iritkatriel commentedon Nov 6, 2023
I've created a PR to raise an exception when the frame is suspended
deprecate clearing suspended frames, add an option to raise a RuntimeError in that case, and use this option in.traceback.clear_frames#111792
gh-79932: raise exception if frame.clear() is called on a suspended f…
pythongh-79932: raise exception if frame.clear() is called on a suspe…
pythongh-79932: raise exception if frame.clear() is called on a suspe…
pythongh-79932: raise exception if frame.clear() is called on a suspe…