Skip to content

Commit 58a5bc2

Browse files
committed
gh-157301: Fix asyncio event loop hanging on a failed eager task start
1 parent 77dd973 commit 58a5bc2

4 files changed

Lines changed: 86 additions & 1 deletion

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 BaseException:
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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,68 @@ async def main():
25922592
finally:
25932593
loop.close()
25942594

2595+
def test_context_not_a_context(self):
2596+
# gh-157301
2597+
async def coro():
2598+
pass
2599+
2600+
loop = asyncio.new_event_loop()
2601+
c = coro()
2602+
try:
2603+
with self.assertRaises(TypeError):
2604+
self.new_task(loop, c, context='not a context')
2605+
finally:
2606+
c.close()
2607+
loop.close()
2608+
2609+
def test_context_not_a_context_leaves_loop_usable(self):
2610+
# gh-157301
2611+
async def coro():
2612+
pass
2613+
2614+
async def main():
2615+
c = coro()
2616+
try:
2617+
with self.assertRaises(TypeError):
2618+
self.new_task(loop, c, context='not a context',
2619+
eager_start=True)
2620+
finally:
2621+
c.close()
2622+
await asyncio.sleep(0)
2623+
2624+
loop = asyncio.new_event_loop()
2625+
loop.call_later(support.SHORT_TIMEOUT, loop.stop)
2626+
try:
2627+
loop.run_until_complete(self.new_task(loop, main()))
2628+
finally:
2629+
loop.close()
2630+
2631+
def test_context_already_entered_leaves_loop_usable(self):
2632+
# gh-157301
2633+
async def coro():
2634+
pass
2635+
2636+
async def main():
2637+
ctx = contextvars.copy_context()
2638+
2639+
def inside():
2640+
c = coro()
2641+
try:
2642+
with self.assertRaises(RuntimeError):
2643+
self.new_task(loop, c, context=ctx, eager_start=True)
2644+
finally:
2645+
c.close()
2646+
2647+
ctx.run(inside)
2648+
await asyncio.sleep(0)
2649+
2650+
loop = asyncio.new_event_loop()
2651+
loop.call_later(support.SHORT_TIMEOUT, loop.stop)
2652+
try:
2653+
loop.run_until_complete(self.new_task(loop, main()))
2654+
finally:
2655+
loop.close()
2656+
25952657
def test_context_2(self):
25962658
cvar = contextvars.ContextVar('cvar', default='nope')
25972659

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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2312,6 +2312,13 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
23122312
if (self->task_context == NULL) {
23132313
return -1;
23142314
}
2315+
} else if (!PyContext_CheckExact(context)) {
2316+
// gh-157301: the passed value must be a contextvars.Context
2317+
self->task_log_destroy_pending = 0;
2318+
PyErr_Format(PyExc_TypeError,
2319+
"a contextvars.Context was expected, got %T",
2320+
context);
2321+
return -1;
23152322
} else {
23162323
Py_XSETREF(self->task_context, Py_NewRef(context));
23172324
}
@@ -3454,6 +3461,11 @@ task_eager_start(_PyThreadStateImpl *ts, asyncio_state *state, TaskObj *task)
34543461
register_task(ts, task);
34553462

34563463
if (_PyContext_Enter(&ts->base, task->task_context) == -1) {
3464+
// gh-157301: a failed enter must not leave the task current and registered
3465+
task->task_log_destroy_pending = 0;
3466+
PyObject *curtask = swap_current_task(ts, task->task_loop, prevtask);
3467+
Py_XDECREF(curtask);
3468+
unregister_task(task);
34573469
Py_DECREF(prevtask);
34583470
return -1;
34593471
}

0 commit comments

Comments
 (0)