Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 BaseException:
self._log_destroy_pending = False
raise
else:
self._loop.call_soon(self.__step, context=self._context)
_py_register_task(self)
Expand Down
62 changes: 62 additions & 0 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2592,6 +2592,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')

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :class:`asyncio.Task` hanging the event loop when an eager start fails
to enter the task's context.
13 changes: 13 additions & 0 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2312,6 +2312,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));
}
Expand Down Expand Up @@ -3453,7 +3460,13 @@ task_eager_start(_PyThreadStateImpl *ts, asyncio_state *state, TaskObj *task)
// it will continue as a regular (non-eager) asyncio task
register_task(ts, task);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a assert here on task_context of PyContext_CheckExact

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

assert(PyContext_CheckExact(task->task_context));
if (_PyContext_Enter(&ts->base, 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(ts, task->task_loop, prevtask);
Py_XDECREF(curtask);
unregister_task(task);
Py_DECREF(prevtask);
return -1;
}
Expand Down
Loading