Skip to content

Commit f76c430

Browse files
[3.15] gh-157301: Fix asyncio event loop hanging on a failed eager task start (GH-157302) (#157785)
Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
1 parent 960f69c commit f76c430

4 files changed

Lines changed: 87 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:
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
@@ -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

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
@@ -2333,6 +2333,13 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
23332333
if (self->task_context == NULL) {
23342334
return -1;
23352335
}
2336+
} else if (!PyContext_CheckExact(context)) {
2337+
// gh-157301: the passed value must be a contextvars.Context
2338+
self->task_log_destroy_pending = 0;
2339+
PyErr_Format(PyExc_TypeError,
2340+
"a contextvars.Context was expected, got %T",
2341+
context);
2342+
return -1;
23362343
} else {
23372344
Py_XSETREF(self->task_context, Py_NewRef(context));
23382345
}
@@ -3478,7 +3485,13 @@ task_eager_start(_PyThreadStateImpl *ts, asyncio_state *state, TaskObj *task)
34783485
// it will continue as a regular (non-eager) asyncio task
34793486
register_task(ts, task);
34803487

3488+
assert(PyContext_CheckExact(task->task_context));
34813489
if (_PyContext_Enter(&ts->base, task->task_context) == -1) {
3490+
// gh-157301: a failed enter must not leave the task current and registered
3491+
task->task_log_destroy_pending = 0;
3492+
PyObject *curtask = swap_current_task(ts, task->task_loop, prevtask);
3493+
Py_XDECREF(curtask);
3494+
unregister_task(task);
34823495
Py_DECREF(prevtask);
34833496
return -1;
34843497
}

0 commit comments

Comments
 (0)