https://github.com/python/cpython/commit/ff90ee1f5070a9463afa56d8bbbbc624859877b0
commit: ff90ee1f5070a9463afa56d8bbbbc624859877b0
branch: 3.14
author: Timofei Ivankov <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-11-02T19:27:54+05:30
summary:
[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
files:
A Misc/NEWS.d/next/Library/2026-09-11-14-14-51.gh-issue-157301.QxcE2r.rst
M Lib/asyncio/tasks.py
M Lib/test/test_asyncio/test_tasks.py
M Modules/_asynciomodule.c
diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py
index 9889a990daa155..208528c9ca6a90 100644
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -103,11 +103,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)
_py_register_task(self)
diff --git a/Lib/test/test_asyncio/test_tasks.py
b/Lib/test/test_asyncio/test_tasks.py
index 6c4c6e5086ac05..9b8df40ec6ece6 100644
--- a/Lib/test/test_asyncio/test_tasks.py
+++ b/Lib/test/test_asyncio/test_tasks.py
@@ -2745,6 +2745,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')
@@ -2958,7 +3020,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 00000000000000..9a1d2e435be906
--- /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 ba1db8be93efb7..e464a8ec96983f 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -2358,6 +2358,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));
}
@@ -3500,7 +3507,13 @@ task_eager_start(asyncio_state *state, TaskObj *task)
// it will continue as a regular (non-eager) asyncio task
register_task(task);
+ 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(task->task_loop, prevtask);
+ Py_XDECREF(curtask);
+ unregister_task(task);
Py_DECREF(prevtask);
return -1;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]