Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit93a5746

Browse files
gschaffnergraingertagronholm
authored
Fixedasyncio.Task.cancelling issues (#790)
* Shrink TaskState to save a little memory* Fix uncancel() being called too early* Refactor to avoid duplicate computation* Test TaskInfo.has_pending_cancellation in cleanup code* Fix TaskInfo.has_pending_cancellation in cleanup code on asyncio* Test that uncancel() isn't called too earlyCo-authored-by: Thomas Grainger <tagrain@gmail.com>Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi>
1 parent39cf394 commit93a5746

File tree

3 files changed

+117
-60
lines changed

3 files changed

+117
-60
lines changed

‎docs/versionhistory.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
1818
- Fixed the return type annotations of ``readinto()`` and ``readinto1()`` methods in the
1919
``anyio.AsyncFile`` class
2020
(`#825<https://github.com/agronholm/anyio/issues/825>`_)
21+
- Fixed ``TaskInfo.has_pending_cancellation()`` on asyncio returning false positives in
22+
cleanup code on Python >= 3.11
23+
(`#832<https://github.com/agronholm/anyio/issues/832>`_; PR by @gschaffner)
24+
- Fixed cancelled cancel scopes on asyncio calling ``asyncio.Task.uncancel`` when
25+
propagating a ``CancelledError`` on exit to a cancelled parent scope
26+
(`#790<https://github.com/agronholm/anyio/pull/790>`_; PR by @gschaffner)
2127

2228
**4.6.2**
2329

‎src/anyio/_backends/_asyncio.py

Lines changed: 59 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -372,11 +372,22 @@ def _task_started(task: asyncio.Task) -> bool:
372372

373373

374374
defis_anyio_cancellation(exc:CancelledError)->bool:
375-
return (
376-
bool(exc.args)
377-
andisinstance(exc.args[0],str)
378-
andexc.args[0].startswith("Cancelled by cancel scope ")
379-
)
375+
# Sometimes third party frameworks catch a CancelledError and raise a new one, so as
376+
# a workaround we have to look at the previous ones in __context__ too for a
377+
# matching cancel message
378+
whileTrue:
379+
if (
380+
exc.args
381+
andisinstance(exc.args[0],str)
382+
andexc.args[0].startswith("Cancelled by cancel scope ")
383+
):
384+
returnTrue
385+
386+
ifisinstance(exc.__context__,CancelledError):
387+
exc=exc.__context__
388+
continue
389+
390+
returnFalse
380391

381392

382393
classCancelScope(BaseCancelScope):
@@ -397,8 +408,10 @@ def __init__(self, deadline: float = math.inf, shield: bool = False):
397408
self._cancel_handle:asyncio.Handle|None=None
398409
self._tasks:set[asyncio.Task]=set()
399410
self._host_task:asyncio.Task|None=None
400-
self._cancel_calls:int=0
401-
self._cancelling:int|None=None
411+
ifsys.version_info>= (3,11):
412+
self._pending_uncancellations:int|None=0
413+
else:
414+
self._pending_uncancellations=None
402415

403416
def__enter__(self)->CancelScope:
404417
ifself._active:
@@ -424,8 +437,6 @@ def __enter__(self) -> CancelScope:
424437

425438
self._timeout()
426439
self._active=True
427-
ifsys.version_info>= (3,11):
428-
self._cancelling=self._host_task.cancelling()
429440

430441
# Start cancelling the host task if the scope was cancelled before entering
431442
ifself._cancel_called:
@@ -470,30 +481,41 @@ def __exit__(
470481

471482
host_task_state.cancel_scope=self._parent_scope
472483

473-
# Undo all cancellations done by this scope
474-
ifself._cancellingisnotNone:
475-
whileself._cancel_calls:
476-
self._cancel_calls-=1
477-
ifself._host_task.uncancel()<=self._cancelling:
478-
break
484+
# Restart the cancellation effort in the closest visible, cancelled parent
485+
# scope if necessary
486+
self._restart_cancellation_in_parent()
479487

480488
# We only swallow the exception iff it was an AnyIO CancelledError, either
481489
# directly as exc_val or inside an exception group and there are no cancelled
482490
# parent cancel scopes visible to us here
483-
not_swallowed_exceptions=0
484-
swallow_exception=False
485-
ifexc_valisnotNone:
486-
forexciniterate_exceptions(exc_val):
487-
ifself._cancel_calledandisinstance(exc,CancelledError):
488-
ifnot (swallow_exception:=self._uncancel(exc)):
489-
not_swallowed_exceptions+=1
490-
else:
491-
not_swallowed_exceptions+=1
491+
ifself._cancel_calledandnotself._parent_cancellation_is_visible_to_us:
492+
# For each level-cancel() call made on the host task, call uncancel()
493+
whileself._pending_uncancellations:
494+
self._host_task.uncancel()
495+
self._pending_uncancellations-=1
496+
497+
# Update cancelled_caught and check for exceptions we must not swallow
498+
cannot_swallow_exc_val=False
499+
ifexc_valisnotNone:
500+
forexciniterate_exceptions(exc_val):
501+
ifisinstance(exc,CancelledError)andis_anyio_cancellation(
502+
exc
503+
):
504+
self._cancelled_caught=True
505+
else:
506+
cannot_swallow_exc_val=True
492507

493-
# Restart the cancellation effort in the closest visible, cancelled parent
494-
# scope if necessary
495-
self._restart_cancellation_in_parent()
496-
returnswallow_exceptionandnotnot_swallowed_exceptions
508+
returnself._cancelled_caughtandnotcannot_swallow_exc_val
509+
else:
510+
ifself._pending_uncancellations:
511+
assertself._parent_scopeisnotNone
512+
assertself._parent_scope._pending_uncancellationsisnotNone
513+
self._parent_scope._pending_uncancellations+= (
514+
self._pending_uncancellations
515+
)
516+
self._pending_uncancellations=0
517+
518+
returnFalse
497519
finally:
498520
self._host_task=None
499521
delexc_val
@@ -520,31 +542,6 @@ def _parent_cancellation_is_visible_to_us(self) -> bool:
520542
andself._parent_scope._effectively_cancelled
521543
)
522544

523-
def_uncancel(self,cancelled_exc:CancelledError)->bool:
524-
ifself._host_taskisNone:
525-
self._cancel_calls=0
526-
returnTrue
527-
528-
whileTrue:
529-
ifis_anyio_cancellation(cancelled_exc):
530-
# Only swallow the cancellation exception if it's an AnyIO cancel
531-
# exception and there are no other cancel scopes down the line pending
532-
# cancellation
533-
self._cancelled_caught= (
534-
self._effectively_cancelled
535-
andnotself._parent_cancellation_is_visible_to_us
536-
)
537-
returnself._cancelled_caught
538-
539-
# Sometimes third party frameworks catch a CancelledError and raise a new
540-
# one, so as a workaround we have to look at the previous ones in
541-
# __context__ too for a matching cancel message
542-
ifisinstance(cancelled_exc.__context__,CancelledError):
543-
cancelled_exc=cancelled_exc.__context__
544-
continue
545-
546-
returnFalse
547-
548545
def_timeout(self)->None:
549546
ifself._deadline!=math.inf:
550547
loop=get_running_loop()
@@ -576,8 +573,11 @@ def _deliver_cancellation(self, origin: CancelScope) -> bool:
576573
waiter=task._fut_waiter# type: ignore[attr-defined]
577574
ifnotisinstance(waiter,asyncio.Future)ornotwaiter.done():
578575
task.cancel(f"Cancelled by cancel scope{id(origin):x}")
579-
iftaskisorigin._host_task:
580-
origin._cancel_calls+=1
576+
if (
577+
taskisorigin._host_task
578+
andorigin._pending_uncancellationsisnotNone
579+
):
580+
origin._pending_uncancellations+=1
581581

582582
# Deliver cancellation to child scopes that aren't shielded or running their own
583583
# cancellation callbacks
@@ -2154,12 +2154,11 @@ def has_pending_cancellation(self) -> bool:
21542154
# If the task isn't around anymore, it won't have a pending cancellation
21552155
returnFalse
21562156

2157-
ifsys.version_info>= (3,11):
2158-
iftask.cancelling():
2159-
returnTrue
2157+
iftask._must_cancel:# type: ignore[attr-defined]
2158+
returnTrue
21602159
elif (
2161-
isinstance(task._fut_waiter,asyncio.Future)
2162-
andtask._fut_waiter.cancelled()
2160+
isinstance(task._fut_waiter,asyncio.Future)# type: ignore[attr-defined]
2161+
andtask._fut_waiter.cancelled()# type: ignore[attr-defined]
21632162
):
21642163
returnTrue
21652164

‎tests/test_taskgroups.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,38 @@ async def test_cancel_shielded_scope() -> None:
673673
awaitcheckpoint()
674674

675675

676+
asyncdeftest_shielded_cleanup_after_cancel()->None:
677+
"""Regression test for #832."""
678+
withCancelScope()asouter_scope:
679+
outer_scope.cancel()
680+
try:
681+
awaitcheckpoint()
682+
finally:
683+
assertcurrent_effective_deadline()==-math.inf
684+
assertget_current_task().has_pending_cancellation()
685+
686+
withCancelScope(shield=True):# noqa: ASYNC100
687+
assertcurrent_effective_deadline()==math.inf
688+
assertnotget_current_task().has_pending_cancellation()
689+
690+
assertcurrent_effective_deadline()==-math.inf
691+
assertget_current_task().has_pending_cancellation()
692+
693+
694+
@pytest.mark.parametrize("anyio_backend", ["asyncio"])
695+
asyncdeftest_cleanup_after_native_cancel()->None:
696+
"""Regression test for #832."""
697+
# See also https://github.com/python/cpython/pull/102815.
698+
task=asyncio.current_task()
699+
asserttask
700+
task.cancel()
701+
withpytest.raises(asyncio.CancelledError):
702+
try:
703+
awaitcheckpoint()
704+
finally:
705+
assertnotget_current_task().has_pending_cancellation()
706+
707+
676708
asyncdeftest_cancelled_not_caught()->None:
677709
withCancelScope()asscope:# noqa: ASYNC100
678710
scope.cancel()
@@ -1488,6 +1520,26 @@ async def taskfunc() -> None:
14881520
assertstr(exc_info.value.exceptions[0])=="dummy error"
14891521
assertnotcast(asyncio.Task,asyncio.current_task()).cancelling()
14901522

1523+
asyncdeftest_uncancel_cancelled_scope_based_checkpoint(self)->None:
1524+
"""See also test_cancelled_scope_based_checkpoint."""
1525+
task=asyncio.current_task()
1526+
asserttask
1527+
1528+
withCancelScope()asouter_scope:
1529+
outer_scope.cancel()
1530+
1531+
try:
1532+
# The following three lines are a way to implement a checkpoint
1533+
# function. See also https://github.com/python-trio/trio/issues/860.
1534+
withCancelScope()asinner_scope:
1535+
inner_scope.cancel()
1536+
awaitsleep_forever()
1537+
finally:
1538+
assertisinstance(sys.exc_info()[1],asyncio.CancelledError)
1539+
asserttask.cancelling()
1540+
1541+
assertnottask.cancelling()
1542+
14911543

14921544
asyncdeftest_cancel_before_entering_task_group()->None:
14931545
withCancelScope()asscope:

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp