Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support async cancellations. #726

Merged
merged 22 commits into from
Jul 4, 2023
Merged
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
93b8a42
Add 'AsyncShieldCancellation' context manager
tomchristie Jun 15, 2023
55e4271
Merge branch 'master' into add-shield-cancellation-primitive
tomchristie Jun 21, 2023
a3ba6df
Update _synchronization.py
tomchristie Jun 21, 2023
61cef57
Linting
tomchristie Jun 21, 2023
aa86c6e
Fix docstring wording
tomchristie Jun 22, 2023
87706d1
Merge branch 'master' into add-shield-cancellation-primitive
tomchristie Jun 26, 2023
962eef9
Add interim 'nocover' to show tests passing.
tomchristie Jun 26, 2023
a7afdf5
Add failing test case for HTTP/1.1 cancellations
tomchristie Jun 26, 2023
6846a4f
Neat cleanup for HTTP/1.1 write cancellations
tomchristie Jun 26, 2023
664d02b
Drop 'nocover' for ShieldCancellation
tomchristie Jun 26, 2023
956dbfd
Add failing test case for HTTP/1.1 cancellations during response reading
tomchristie Jun 26, 2023
991888a
Resolve failing test case
tomchristie Jun 26, 2023
57bee0f
Add failing test cases for cancellations on connection pools
tomchristie Jun 26, 2023
8bd548f
Resolve failing test cases
tomchristie Jun 26, 2023
7b9a3f4
Add failing test cases for cancellations on HTTP/2 connections
tomchristie Jun 26, 2023
9c85920
Resolve failing test cases
tomchristie Jun 26, 2023
317e17c
Add failing test cases for cancellations on HTTP/2 connections when r…
tomchristie Jun 26, 2023
5dc0af8
Resolve failing test cases
tomchristie Jun 26, 2023
988bab0
Update CHANGELOG
tomchristie Jun 26, 2023
ab359f5
Fix yield behaviour
tomchristie Jun 27, 2023
29e61db
Merge branch 'master' into add-shield-cancellation-primitive
tomchristie Jul 3, 2023
2b8c94a
Merge branch 'master' into add-shield-cancellation-primitive
tomchristie Jul 3, 2023
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
68 changes: 68 additions & 0 deletions httpcore/_synchronization.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,58 @@ async def release(self) -> None:
self._anyio_semaphore.release()


class AsyncShieldCancellation:
# For certain portions of our codebase where we're dealing with
# closing connections during exception handling we want to shield
# the operation from being cancelled.
#
# with AsyncShieldCancellation():
# ... # clean-up operations, shielded from cancellation.

def __init__(self) -> None:
"""
Detect if we're running under 'asyncio' or 'trio' and create
a semaphore with the correct implementation.
tomchristie marked this conversation as resolved.
Show resolved Hide resolved
"""
self._backend = sniffio.current_async_library()

if self._backend == "trio":
if trio is None: # pragma: nocover
raise RuntimeError(
"Running under trio requires the 'trio' package to be installed."
)

self._trio_shield = trio.CancelScope(shield=True)
else:
if anyio is None: # pragma: nocover
raise RuntimeError(
"Running under asyncio requires the 'anyio' package to be installed."
)

self._anyio_shield = anyio.CancelScope(shield=True)

def __enter__(self) -> "AsyncShieldCancellation":
if not self._backend:
tomchristie marked this conversation as resolved.
Show resolved Hide resolved
self.setup()

if self._backend == "trio":
self._trio_shield.__enter__()
else:
self._anyio_shield.__enter__()
tomchristie marked this conversation as resolved.
Show resolved Hide resolved
return self

def __exit__(
self,
exc_type: Optional[Type[BaseException]] = None,
exc_value: Optional[BaseException] = None,
traceback: Optional[TracebackType] = None,
) -> None:
if self._backend == "trio":
self._trio_shield.__exit__(exc_type, exc_value, traceback)
else:
self._anyio_shield.__exit__(exc_type, exc_value, traceback)


# Our thread-based synchronization primitives...


Expand Down Expand Up @@ -212,3 +264,19 @@ def acquire(self) -> None:

def release(self) -> None:
self._semaphore.release()


class ShieldCancellation:
# Thread-synchronous codebases don't support cancellation semantics.
# We have this class because we need to mirror the async and sync
# cases within our package, but it's just a no-op.
def __enter__(self) -> "ShieldCancellation":
return self

def __exit__(
self,
exc_type: Optional[Type[BaseException]] = None,
exc_value: Optional[BaseException] = None,
traceback: Optional[TracebackType] = None,
) -> None:
pass