From 126e1b3898ddcdf6606cf7dd21eb4e0d6e086937 Mon Sep 17 00:00:00 2001 From: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:37:07 +0300 Subject: [PATCH] gh-157301: Fix asyncio event loop hanging on a failed eager task start --- Lib/asyncio/tasks.py | 11 ++- Lib/test/test_asyncio/test_tasks.py | 70 ++++++++++++++++++- ...-09-11-14-14-51.gh-issue-157301.QxcE2r.rst | 2 + Modules/_asynciomodule.c | 13 ++++ 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-11-14-14-51.gh-issue-157301.QxcE2r.rst diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 19eab6d451bcbb4..c1347b0b29bfd22 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -118,11 +118,20 @@ def __init__(self, coro, *, loop=None, name=None, context=None, self._coro = coro if context is None: self._context = contextvars.copy_context() + elif not isinstance(context, contextvars.Context): + # gh-157301: the passed value must be a contextvars.Context + self._log_destroy_pending = False + raise TypeError('a contextvars.Context was expected, ' + f'got {type(context).__name__}') else: self._context = context if eager_start and self._loop.is_running(): - self.__eager_start() + try: + self.__eager_start() + except: + self._log_destroy_pending = False + raise else: self._loop.call_soon(self.__step, context=self._context) _register_task(self) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 946651104c9c882..570294f1d2e76d5 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -87,8 +87,10 @@ class BaseTaskTests: Task = None Future = None - def new_task(self, loop, coro, name='TestTask', context=None): - return self.__class__.Task(coro, loop=loop, name=name, context=context) + def new_task(self, loop, coro, name='TestTask', context=None, + eager_start=None): + return self.__class__.Task(coro, loop=loop, name=name, context=context, + eager_start=eager_start) def new_future(self, loop): return self.__class__.Future(loop=loop) @@ -2518,6 +2520,68 @@ async def main(): finally: loop.close() + def test_context_not_a_context(self): + # gh-157301 + async def coro(): + pass + + loop = asyncio.new_event_loop() + c = coro() + try: + with self.assertRaises(TypeError): + self.new_task(loop, c, context='not a context') + finally: + c.close() + loop.close() + + def test_context_not_a_context_leaves_loop_usable(self): + # gh-157301 + async def coro(): + pass + + async def main(): + c = coro() + try: + with self.assertRaises(TypeError): + self.new_task(loop, c, context='not a context', + eager_start=True) + finally: + c.close() + await asyncio.sleep(0) + + loop = asyncio.new_event_loop() + loop.call_later(support.SHORT_TIMEOUT, loop.stop) + try: + loop.run_until_complete(self.new_task(loop, main())) + finally: + loop.close() + + def test_context_already_entered_leaves_loop_usable(self): + # gh-157301 + async def coro(): + pass + + async def main(): + ctx = contextvars.copy_context() + + def inside(): + c = coro() + try: + with self.assertRaises(RuntimeError): + self.new_task(loop, c, context=ctx, eager_start=True) + finally: + c.close() + + ctx.run(inside) + await asyncio.sleep(0) + + loop = asyncio.new_event_loop() + loop.call_later(support.SHORT_TIMEOUT, loop.stop) + try: + loop.run_until_complete(self.new_task(loop, main())) + finally: + loop.close() + def test_context_2(self): cvar = contextvars.ContextVar('cvar', default='nope') @@ -2690,7 +2754,7 @@ class Break: def __str__(self): raise RuntimeError("break") - obj = object() + obj = contextvars.copy_context() initial_refcount = sys.getrefcount(obj) coro = coroutine_function() diff --git a/Misc/NEWS.d/next/Library/2026-09-11-14-14-51.gh-issue-157301.QxcE2r.rst b/Misc/NEWS.d/next/Library/2026-09-11-14-14-51.gh-issue-157301.QxcE2r.rst new file mode 100644 index 000000000000000..9a1d2e435be9066 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-11-14-14-51.gh-issue-157301.QxcE2r.rst @@ -0,0 +1,2 @@ +Fix :class:`asyncio.Task` hanging the event loop when an eager start fails +to enter the task's context. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 686baa90b119a07..88ac84560f59afc 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -2073,6 +2073,13 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop, if (self->task_context == NULL) { return -1; } + } else if (!PyContext_CheckExact(context)) { + // gh-157301: the passed value must be a contextvars.Context + self->task_log_destroy_pending = 0; + PyErr_Format(PyExc_TypeError, + "a contextvars.Context was expected, got %T", + context); + return -1; } else { Py_XSETREF(self->task_context, Py_NewRef(context)); } @@ -3138,7 +3145,13 @@ task_eager_start(asyncio_state *state, TaskObj *task) return -1; } + assert(PyContext_CheckExact(task->task_context)); if (PyContext_Enter(task->task_context) == -1) { + // gh-157301: a failed enter must not leave the task current and registered + task->task_log_destroy_pending = 0; + PyObject *curtask = swap_current_task(state, task->task_loop, prevtask); + Py_XDECREF(curtask); + unregister_eager_task(state, (PyObject *)task); Py_DECREF(prevtask); return -1; }