Skip to content

Commit 4140bcb

Browse files
bpo-45390: Propagate CancelledError's message from cancelled task to its awaiter (GH-31383)
Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 59585d6 commit 4140bcb

File tree

5 files changed

+79
-39
lines changed

5 files changed

+79
-39
lines changed

Doc/library/asyncio-task.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,9 @@ Task Object
843843
.. versionchanged:: 3.9
844844
Added the *msg* parameter.
845845

846+
.. versionchanged:: 3.11
847+
The ``msg`` parameter is propagated from cancelled task to its awaiter.
848+
846849
.. _asyncio_example_task_cancel:
847850

848851
The following example illustrates how coroutines can intercept

Lib/asyncio/futures.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ def _make_cancelled_error(self):
132132
This should only be called once when handling a cancellation since
133133
it erases the saved context exception value.
134134
"""
135+
if self._cancelled_exc is not None:
136+
exc = self._cancelled_exc
137+
self._cancelled_exc = None
138+
return exc
139+
135140
if self._cancel_message is None:
136141
exc = exceptions.CancelledError()
137142
else:

Lib/test/test_asyncio/test_tasks.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,11 @@ async def coro():
124124
t.cancel('my message')
125125
self.assertEqual(t._cancel_message, 'my message')
126126

127-
with self.assertRaises(asyncio.CancelledError):
127+
with self.assertRaises(asyncio.CancelledError) as cm:
128128
self.loop.run_until_complete(t)
129129

130+
self.assertEqual('my message', cm.exception.args[0])
131+
130132
def test_task_cancel_message_setter(self):
131133
async def coro():
132134
pass
@@ -135,9 +137,11 @@ async def coro():
135137
t._cancel_message = 'my new message'
136138
self.assertEqual(t._cancel_message, 'my new message')
137139

138-
with self.assertRaises(asyncio.CancelledError):
140+
with self.assertRaises(asyncio.CancelledError) as cm:
139141
self.loop.run_until_complete(t)
140142

143+
self.assertEqual('my new message', cm.exception.args[0])
144+
141145
def test_task_del_collect(self):
142146
class Evil:
143147
def __del__(self):
@@ -590,11 +594,11 @@ async def coro():
590594
with self.assertRaises(asyncio.CancelledError) as cm:
591595
loop.run_until_complete(task)
592596
exc = cm.exception
593-
self.assertEqual(exc.args, ())
597+
self.assertEqual(exc.args, expected_args)
594598

595599
actual = get_innermost_context(exc)
596600
self.assertEqual(actual,
597-
(asyncio.CancelledError, expected_args, 2))
601+
(asyncio.CancelledError, expected_args, 0))
598602

599603
def test_cancel_with_message_then_future_exception(self):
600604
# Test Future.exception() after calling cancel() with a message.
@@ -624,11 +628,39 @@ async def coro():
624628
with self.assertRaises(asyncio.CancelledError) as cm:
625629
loop.run_until_complete(task)
626630
exc = cm.exception
627-
self.assertEqual(exc.args, ())
631+
self.assertEqual(exc.args, expected_args)
628632

629633
actual = get_innermost_context(exc)
630634
self.assertEqual(actual,
631-
(asyncio.CancelledError, expected_args, 2))
635+
(asyncio.CancelledError, expected_args, 0))
636+
637+
def test_cancellation_exception_context(self):
638+
loop = asyncio.new_event_loop()
639+
self.set_event_loop(loop)
640+
fut = loop.create_future()
641+
642+
async def sleep():
643+
fut.set_result(None)
644+
await asyncio.sleep(10)
645+
646+
async def coro():
647+
inner_task = self.new_task(loop, sleep())
648+
await fut
649+
loop.call_soon(inner_task.cancel, 'msg')
650+
try:
651+
await inner_task
652+
except asyncio.CancelledError as ex:
653+
raise ValueError("cancelled") from ex
654+
655+
task = self.new_task(loop, coro())
656+
with self.assertRaises(ValueError) as cm:
657+
loop.run_until_complete(task)
658+
exc = cm.exception
659+
self.assertEqual(exc.args, ('cancelled',))
660+
661+
actual = get_innermost_context(exc)
662+
self.assertEqual(actual,
663+
(asyncio.CancelledError, ('msg',), 1))
632664

633665
def test_cancel_with_message_before_starting_task(self):
634666
loop = asyncio.new_event_loop()
@@ -648,11 +680,11 @@ async def coro():
648680
with self.assertRaises(asyncio.CancelledError) as cm:
649681
loop.run_until_complete(task)
650682
exc = cm.exception
651-
self.assertEqual(exc.args, ())
683+
self.assertEqual(exc.args, ('my message',))
652684

653685
actual = get_innermost_context(exc)
654686
self.assertEqual(actual,
655-
(asyncio.CancelledError, ('my message',), 2))
687+
(asyncio.CancelledError, ('my message',), 0))
656688

657689
def test_cancel_yield(self):
658690
async def task():
@@ -2296,15 +2328,17 @@ async def main():
22962328
try:
22972329
loop.run_until_complete(main())
22982330
except asyncio.CancelledError as exc:
2299-
self.assertEqual(exc.args, ())
2300-
exc_type, exc_args, depth = get_innermost_context(exc)
2301-
self.assertEqual((exc_type, exc_args),
2302-
(asyncio.CancelledError, expected_args))
2303-
# The exact traceback seems to vary in CI.
2304-
self.assertIn(depth, (2, 3))
2331+
self.assertEqual(exc.args, expected_args)
2332+
actual = get_innermost_context(exc)
2333+
self.assertEqual(
2334+
actual,
2335+
(asyncio.CancelledError, expected_args, 0),
2336+
)
23052337
else:
2306-
self.fail('gather did not propagate the cancellation '
2307-
'request')
2338+
self.fail(
2339+
'gather() does not propagate CancelledError '
2340+
'raised by inner task to the gather() caller.'
2341+
)
23082342

23092343
def test_exception_traceback(self):
23102344
# See http://bugs.python.org/issue28843
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Propagate :exc:`asyncio.CancelledError` message from inner task to outer
2+
awaiter.

Modules/_asynciomodule.c

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ typedef enum {
7777
int prefix##_blocking; \
7878
PyObject *dict; \
7979
PyObject *prefix##_weakreflist; \
80-
_PyErr_StackItem prefix##_cancelled_exc_state;
80+
PyObject *prefix##_cancelled_exc;
8181

8282
typedef struct {
8383
FutureObj_HEAD(fut)
@@ -496,7 +496,7 @@ future_init(FutureObj *fut, PyObject *loop)
496496
Py_CLEAR(fut->fut_exception);
497497
Py_CLEAR(fut->fut_source_tb);
498498
Py_CLEAR(fut->fut_cancel_msg);
499-
_PyErr_ClearExcState(&fut->fut_cancelled_exc_state);
499+
Py_CLEAR(fut->fut_cancelled_exc);
500500

501501
fut->fut_state = STATE_PENDING;
502502
fut->fut_log_tb = 0;
@@ -612,25 +612,32 @@ future_set_exception(FutureObj *fut, PyObject *exc)
612612
}
613613

614614
static PyObject *
615-
create_cancelled_error(PyObject *msg)
615+
create_cancelled_error(FutureObj *fut)
616616
{
617617
PyObject *exc;
618+
if (fut->fut_cancelled_exc != NULL) {
619+
/* transfer ownership */
620+
exc = fut->fut_cancelled_exc;
621+
fut->fut_cancelled_exc = NULL;
622+
return exc;
623+
}
624+
PyObject *msg = fut->fut_cancel_msg;
618625
if (msg == NULL || msg == Py_None) {
619626
exc = PyObject_CallNoArgs(asyncio_CancelledError);
620627
} else {
621628
exc = PyObject_CallOneArg(asyncio_CancelledError, msg);
622629
}
630+
PyException_SetContext(exc, fut->fut_cancelled_exc);
631+
Py_CLEAR(fut->fut_cancelled_exc);
623632
return exc;
624633
}
625634

626635
static void
627636
future_set_cancelled_error(FutureObj *fut)
628637
{
629-
PyObject *exc = create_cancelled_error(fut->fut_cancel_msg);
638+
PyObject *exc = create_cancelled_error(fut);
630639
PyErr_SetObject(asyncio_CancelledError, exc);
631640
Py_DECREF(exc);
632-
633-
_PyErr_ChainStackItem(&fut->fut_cancelled_exc_state);
634641
}
635642

636643
static int
@@ -793,7 +800,7 @@ FutureObj_clear(FutureObj *fut)
793800
Py_CLEAR(fut->fut_exception);
794801
Py_CLEAR(fut->fut_source_tb);
795802
Py_CLEAR(fut->fut_cancel_msg);
796-
_PyErr_ClearExcState(&fut->fut_cancelled_exc_state);
803+
Py_CLEAR(fut->fut_cancelled_exc);
797804
Py_CLEAR(fut->dict);
798805
return 0;
799806
}
@@ -809,11 +816,8 @@ FutureObj_traverse(FutureObj *fut, visitproc visit, void *arg)
809816
Py_VISIT(fut->fut_exception);
810817
Py_VISIT(fut->fut_source_tb);
811818
Py_VISIT(fut->fut_cancel_msg);
819+
Py_VISIT(fut->fut_cancelled_exc);
812820
Py_VISIT(fut->dict);
813-
814-
_PyErr_StackItem *exc_state = &fut->fut_cancelled_exc_state;
815-
Py_VISIT(exc_state->exc_value);
816-
817821
return 0;
818822
}
819823

@@ -1369,15 +1373,7 @@ static PyObject *
13691373
_asyncio_Future__make_cancelled_error_impl(FutureObj *self)
13701374
/*[clinic end generated code: output=a5df276f6c1213de input=ac6effe4ba795ecc]*/
13711375
{
1372-
PyObject *exc = create_cancelled_error(self->fut_cancel_msg);
1373-
_PyErr_StackItem *exc_state = &self->fut_cancelled_exc_state;
1374-
1375-
if (exc_state->exc_value) {
1376-
PyException_SetContext(exc, Py_NewRef(exc_state->exc_value));
1377-
_PyErr_ClearExcState(exc_state);
1378-
}
1379-
1380-
return exc;
1376+
return create_cancelled_error(self);
13811377
}
13821378

13831379
/*[clinic input]
@@ -2677,7 +2673,7 @@ task_step_impl(TaskObj *task, PyObject *exc)
26772673

26782674
if (!exc) {
26792675
/* exc was not a CancelledError */
2680-
exc = create_cancelled_error(task->task_cancel_msg);
2676+
exc = create_cancelled_error((FutureObj*)task);
26812677

26822678
if (!exc) {
26832679
goto fail;
@@ -2751,8 +2747,8 @@ task_step_impl(TaskObj *task, PyObject *exc)
27512747
Py_XDECREF(et);
27522748

27532749
FutureObj *fut = (FutureObj*)task;
2754-
_PyErr_StackItem *exc_state = &fut->fut_cancelled_exc_state;
2755-
exc_state->exc_value = ev;
2750+
/* transfer ownership */
2751+
fut->fut_cancelled_exc = ev;
27562752

27572753
return future_cancel(fut, NULL);
27582754
}

0 commit comments

Comments
 (0)