Skip to content

Commit fc9b015

Browse files
authored
[3.13] gh-157301: Fix asyncio event loop hanging on a failed eager task start (#157845)
gh-157301: Fix asyncio event loop hanging on a failed eager task start
1 parent 5634116 commit fc9b015

4 files changed

Lines changed: 92 additions & 4 deletions

File tree

‎Lib/asyncio/tasks.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,20 @@ def __init__(self, coro, *, loop=None, name=None, context=None,
118118
self._coro = coro
119119
if context is None:
120120
self._context = contextvars.copy_context()
121+
elif not isinstance(context, contextvars.Context):
122+
# gh-157301: the passed value must be a contextvars.Context
123+
self._log_destroy_pending = False
124+
raise TypeError('a contextvars.Context was expected, '
125+
f'got {type(context).__name__}')
121126
else:
122127
self._context = context
123128

124129
if eager_start and self._loop.is_running():
125-
self.__eager_start()
130+
try:
131+
self.__eager_start()
132+
except:
133+
self._log_destroy_pending = False
134+
raise
126135
else:
127136
self._loop.call_soon(self.__step, context=self._context)
128137
_register_task(self)

‎Lib/test/test_asyncio/test_tasks.py‎

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ class BaseTaskTests:
8787
Task = None
8888
Future = None
8989

90-
def new_task(self, loop, coro, name='TestTask', context=None):
91-
return self.__class__.Task(coro, loop=loop, name=name, context=context)
90+
def new_task(self, loop, coro, name='TestTask', context=None,
91+
eager_start=None):
92+
return self.__class__.Task(coro, loop=loop, name=name, context=context,
93+
eager_start=eager_start)
9294

9395
def new_future(self, loop):
9496
return self.__class__.Future(loop=loop)
@@ -2518,6 +2520,68 @@ async def main():
25182520
finally:
25192521
loop.close()
25202522

2523+
def test_context_not_a_context(self):
2524+
# gh-157301
2525+
async def coro():
2526+
pass
2527+
2528+
loop = asyncio.new_event_loop()
2529+
c = coro()
2530+
try:
2531+
with self.assertRaises(TypeError):
2532+
self.new_task(loop, c, context='not a context')
2533+
finally:
2534+
c.close()
2535+
loop.close()
2536+
2537+
def test_context_not_a_context_leaves_loop_usable(self):
2538+
# gh-157301
2539+
async def coro():
2540+
pass
2541+
2542+
async def main():
2543+
c = coro()
2544+
try:
2545+
with self.assertRaises(TypeError):
2546+
self.new_task(loop, c, context='not a context',
2547+
eager_start=True)
2548+
finally:
2549+
c.close()
2550+
await asyncio.sleep(0)
2551+
2552+
loop = asyncio.new_event_loop()
2553+
loop.call_later(support.SHORT_TIMEOUT, loop.stop)
2554+
try:
2555+
loop.run_until_complete(self.new_task(loop, main()))
2556+
finally:
2557+
loop.close()
2558+
2559+
def test_context_already_entered_leaves_loop_usable(self):
2560+
# gh-157301
2561+
async def coro():
2562+
pass
2563+
2564+
async def main():
2565+
ctx = contextvars.copy_context()
2566+
2567+
def inside():
2568+
c = coro()
2569+
try:
2570+
with self.assertRaises(RuntimeError):
2571+
self.new_task(loop, c, context=ctx, eager_start=True)
2572+
finally:
2573+
c.close()
2574+
2575+
ctx.run(inside)
2576+
await asyncio.sleep(0)
2577+
2578+
loop = asyncio.new_event_loop()
2579+
loop.call_later(support.SHORT_TIMEOUT, loop.stop)
2580+
try:
2581+
loop.run_until_complete(self.new_task(loop, main()))
2582+
finally:
2583+
loop.close()
2584+
25212585
def test_context_2(self):
25222586
cvar = contextvars.ContextVar('cvar', default='nope')
25232587

@@ -2690,7 +2754,7 @@ class Break:
26902754
def __str__(self):
26912755
raise RuntimeError("break")
26922756

2693-
obj = object()
2757+
obj = contextvars.copy_context()
26942758
initial_refcount = sys.getrefcount(obj)
26952759

26962760
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
@@ -2073,6 +2073,13 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
20732073
if (self->task_context == NULL) {
20742074
return -1;
20752075
}
2076+
} else if (!PyContext_CheckExact(context)) {
2077+
// gh-157301: the passed value must be a contextvars.Context
2078+
self->task_log_destroy_pending = 0;
2079+
PyErr_Format(PyExc_TypeError,
2080+
"a contextvars.Context was expected, got %T",
2081+
context);
2082+
return -1;
20762083
} else {
20772084
Py_XSETREF(self->task_context, Py_NewRef(context));
20782085
}
@@ -3138,7 +3145,13 @@ task_eager_start(asyncio_state *state, TaskObj *task)
31383145
return -1;
31393146
}
31403147

3148+
assert(PyContext_CheckExact(task->task_context));
31413149
if (PyContext_Enter(task->task_context) == -1) {
3150+
// gh-157301: a failed enter must not leave the task current and registered
3151+
task->task_log_destroy_pending = 0;
3152+
PyObject *curtask = swap_current_task(state, task->task_loop, prevtask);
3153+
Py_XDECREF(curtask);
3154+
unregister_eager_task(state, (PyObject *)task);
31423155
Py_DECREF(prevtask);
31433156
return -1;
31443157
}

0 commit comments

Comments
 (0)