Skip to content

Commit ff90ee1

Browse files
authored
[3.14] gh-157301: Fix asyncio event loop hanging on a failed eager task start (#157844)
gh-157301: Fix asyncio event loop hanging on a failed eager task start
1 parent 7719957 commit ff90ee1

4 files changed

Lines changed: 88 additions & 2 deletions

File tree

‎Lib/asyncio/tasks.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,20 @@ def __init__(self, coro, *, loop=None, name=None, context=None,
103103
self._coro = coro
104104
if context is None:
105105
self._context = contextvars.copy_context()
106+
elif not isinstance(context, contextvars.Context):
107+
# gh-157301: the passed value must be a contextvars.Context
108+
self._log_destroy_pending = False
109+
raise TypeError('a contextvars.Context was expected, '
110+
f'got {type(context).__name__}')
106111
else:
107112
self._context = context
108113

109114
if eager_start and self._loop.is_running():
110-
self.__eager_start()
115+
try:
116+
self.__eager_start()
117+
except:
118+
self._log_destroy_pending = False
119+
raise
111120
else:
112121
self._loop.call_soon(self.__step, context=self._context)
113122
_py_register_task(self)

‎Lib/test/test_asyncio/test_tasks.py‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2745,6 +2745,68 @@ async def main():
27452745
finally:
27462746
loop.close()
27472747

2748+
def test_context_not_a_context(self):
2749+
# gh-157301
2750+
async def coro():
2751+
pass
2752+
2753+
loop = asyncio.new_event_loop()
2754+
c = coro()
2755+
try:
2756+
with self.assertRaises(TypeError):
2757+
self.new_task(loop, c, context='not a context')
2758+
finally:
2759+
c.close()
2760+
loop.close()
2761+
2762+
def test_context_not_a_context_leaves_loop_usable(self):
2763+
# gh-157301
2764+
async def coro():
2765+
pass
2766+
2767+
async def main():
2768+
c = coro()
2769+
try:
2770+
with self.assertRaises(TypeError):
2771+
self.new_task(loop, c, context='not a context',
2772+
eager_start=True)
2773+
finally:
2774+
c.close()
2775+
await asyncio.sleep(0)
2776+
2777+
loop = asyncio.new_event_loop()
2778+
loop.call_later(support.SHORT_TIMEOUT, loop.stop)
2779+
try:
2780+
loop.run_until_complete(self.new_task(loop, main()))
2781+
finally:
2782+
loop.close()
2783+
2784+
def test_context_already_entered_leaves_loop_usable(self):
2785+
# gh-157301
2786+
async def coro():
2787+
pass
2788+
2789+
async def main():
2790+
ctx = contextvars.copy_context()
2791+
2792+
def inside():
2793+
c = coro()
2794+
try:
2795+
with self.assertRaises(RuntimeError):
2796+
self.new_task(loop, c, context=ctx, eager_start=True)
2797+
finally:
2798+
c.close()
2799+
2800+
ctx.run(inside)
2801+
await asyncio.sleep(0)
2802+
2803+
loop = asyncio.new_event_loop()
2804+
loop.call_later(support.SHORT_TIMEOUT, loop.stop)
2805+
try:
2806+
loop.run_until_complete(self.new_task(loop, main()))
2807+
finally:
2808+
loop.close()
2809+
27482810
def test_context_2(self):
27492811
cvar = contextvars.ContextVar('cvar', default='nope')
27502812

@@ -2958,7 +3020,7 @@ class Break:
29583020
def __str__(self):
29593021
raise RuntimeError("break")
29603022

2961-
obj = object()
3023+
obj = contextvars.copy_context()
29623024
initial_refcount = sys.getrefcount(obj)
29633025

29643026
coro = coroutine_function()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :class:`asyncio.Task` hanging the event loop when an eager start fails
2+
to enter the task's context.

‎Modules/_asynciomodule.c‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2358,6 +2358,13 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
23582358
if (self->task_context == NULL) {
23592359
return -1;
23602360
}
2361+
} else if (!PyContext_CheckExact(context)) {
2362+
// gh-157301: the passed value must be a contextvars.Context
2363+
self->task_log_destroy_pending = 0;
2364+
PyErr_Format(PyExc_TypeError,
2365+
"a contextvars.Context was expected, got %T",
2366+
context);
2367+
return -1;
23612368
} else {
23622369
Py_XSETREF(self->task_context, Py_NewRef(context));
23632370
}
@@ -3500,7 +3507,13 @@ task_eager_start(asyncio_state *state, TaskObj *task)
35003507
// it will continue as a regular (non-eager) asyncio task
35013508
register_task(task);
35023509

3510+
assert(PyContext_CheckExact(task->task_context));
35033511
if (PyContext_Enter(task->task_context) == -1) {
3512+
// gh-157301: a failed enter must not leave the task current and registered
3513+
task->task_log_destroy_pending = 0;
3514+
PyObject *curtask = swap_current_task(task->task_loop, prevtask);
3515+
Py_XDECREF(curtask);
3516+
unregister_task(task);
35043517
Py_DECREF(prevtask);
35053518
return -1;
35063519
}

0 commit comments

Comments
 (0)