Skip to content

Commit e72ee81

Browse files
committed
gh-157301: Fix asyncio event loop hanging on a failed eager task start
1 parent de38c76 commit e72ee81

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
@@ -2630,6 +2630,68 @@ async def main():
26302630
finally:
26312631
loop.close()
26322632

2633+
def test_context_not_a_context(self):
2634+
# gh-157301
2635+
async def coro():
2636+
pass
2637+
2638+
loop = asyncio.new_event_loop()
2639+
c = coro()
2640+
try:
2641+
with self.assertRaises(TypeError):
2642+
self.new_task(loop, c, context='not a context')
2643+
finally:
2644+
c.close()
2645+
loop.close()
2646+
2647+
def test_context_not_a_context_leaves_loop_usable(self):
2648+
# gh-157301
2649+
async def coro():
2650+
pass
2651+
2652+
async def main():
2653+
c = coro()
2654+
try:
2655+
with self.assertRaises(TypeError):
2656+
self.new_task(loop, c, context='not a context',
2657+
eager_start=True)
2658+
finally:
2659+
c.close()
2660+
await asyncio.sleep(0)
2661+
2662+
loop = asyncio.new_event_loop()
2663+
loop.call_later(support.SHORT_TIMEOUT, loop.stop)
2664+
try:
2665+
loop.run_until_complete(self.new_task(loop, main()))
2666+
finally:
2667+
loop.close()
2668+
2669+
def test_context_already_entered_leaves_loop_usable(self):
2670+
# gh-157301
2671+
async def coro():
2672+
pass
2673+
2674+
async def main():
2675+
ctx = contextvars.copy_context()
2676+
2677+
def inside():
2678+
c = coro()
2679+
try:
2680+
with self.assertRaises(RuntimeError):
2681+
self.new_task(loop, c, context=ctx, eager_start=True)
2682+
finally:
2683+
c.close()
2684+
2685+
ctx.run(inside)
2686+
await asyncio.sleep(0)
2687+
2688+
loop = asyncio.new_event_loop()
2689+
loop.call_later(support.SHORT_TIMEOUT, loop.stop)
2690+
try:
2691+
loop.run_until_complete(self.new_task(loop, main()))
2692+
finally:
2693+
loop.close()
2694+
26332695
def test_context_2(self):
26342696
cvar = contextvars.ContextVar('cvar', default='nope')
26352697

@@ -2843,7 +2905,7 @@ class Break:
28432905
def __str__(self):
28442906
raise RuntimeError("break")
28452907

2846-
obj = object()
2908+
obj = contextvars.copy_context()
28472909
initial_refcount = sys.getrefcount(obj)
28482910

28492911
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
@@ -2341,6 +2341,13 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
23412341
if (self->task_context == NULL) {
23422342
return -1;
23432343
}
2344+
} else if (!PyContext_CheckExact(context)) {
2345+
// gh-157301: the passed value must be a contextvars.Context
2346+
self->task_log_destroy_pending = 0;
2347+
PyErr_Format(PyExc_TypeError,
2348+
"a contextvars.Context was expected, got %T",
2349+
context);
2350+
return -1;
23442351
} else {
23452352
Py_XSETREF(self->task_context, Py_NewRef(context));
23462353
}
@@ -3483,7 +3490,13 @@ task_eager_start(asyncio_state *state, TaskObj *task)
34833490
// it will continue as a regular (non-eager) asyncio task
34843491
register_task(task);
34853492

3493+
assert(PyContext_CheckExact(task->task_context));
34863494
if (PyContext_Enter(task->task_context) == -1) {
3495+
// gh-157301: a failed enter must not leave the task current and registered
3496+
task->task_log_destroy_pending = 0;
3497+
PyObject *curtask = swap_current_task(task->task_loop, prevtask);
3498+
Py_XDECREF(curtask);
3499+
unregister_task(task);
34873500
Py_DECREF(prevtask);
34883501
return -1;
34893502
}

0 commit comments

Comments
 (0)