Skip to main content

Python asyncio-native cancellation context library

Project description

aiofence

aiofence

codecov

Multi-reason cancellation contexts for Python asyncio. Inspired by Go's context.Context, aiofence provides a cancellation context that propagates hierarchically through your application via ContextVar — no need to thread events, flags, or tokens through every call signature. Declare cancellation sources once at the boundary — inner code just wraps cancellable work in a context manager and doesn't care about the actual reasons, though it can inspect them if needed.

Motivation

asyncio can cancel tasks mechanically — task.cancel(), asyncio.timeout() — but it can't tell you why you were cancelled or let you control when. Your task is abruptly killed at the next await, whether it's ready or not. When multiple cancellation sources exist (timeout, client disconnect, graceful shutdown), user code is forced to propagate events and flags through every call signature, spawn background listeners that watch for signals and cancel your task, and shield cleanup code so that a second cancel request doesn't kill the task mid-cleanup. Without a single centralized object that owns all of this, it gets messy fast:

async def handle_request(request, shutdown_event, timeout=30):
    try:
        async with asyncio.timeout(timeout):
            while not shutdown_event.is_set():
                chunk = await get_next_chunk()
                if request.is_disconnected():
                    break
                await process(chunk)
    except TimeoutError:
        ...
    except asyncio.CancelledError:
        # shutdown? disconnect? something else?
        ...

For a deeper dive into the problem and design rationale, see this Medium post.

aiofence solves this. Declare all cancellation sources once, composably. The callee doesn't even know cancellation exists:

with Fence(TimeoutTrigger(30), EventTrigger(shutdown, code="shutdown")) as fence:
    result = await fetch_and_transform()

if not fence.cancelled:
    await save(result)
else:
    print(fence.reasons)       # (CancelReason(message='timed out after 30s', ...),)
    print(fence.cancelled_by("shutdown"))  # True / False

What about asyncio.shield()?

shield() prevents cancellation from reaching shielded code, but it works from the opposite direction — you protect everything that must not be cancelled. In practice this means wrapping database writes, state transitions, logging, and cleanup individually, and each function needs to know whether it's cancel-safe.

aiofence comes at it differently: most code doesn't know cancellation exists. You only wrap the expensive, safely-interruptible parts — the operations you want to cancel. For example, in an LLM inference service, you don't want to cancel database queries or response formatting. You want to cancel the LLM call that's burning GPU time for a client that already disconnected:

with Fence(EventTrigger(client_disconnect), TimeoutTrigger(budget)) as fence:
    result = await llm.generate(prompt)  # cancellable

await db.save(result or fallback)  # always runs, no shield needed

Why not anyio?

anyio is one of the best async libraries in the Python ecosystem, and its CancelScope is a more powerful and general cancellation model than what asyncio provides natively. aiofence is narrower in scope and makes different trade-offs:

  1. Drop-in for existing asyncio code. anyio introduces its own cancellation semantics (CancelScope, level-triggered cancellation, nested scope trees). If your app is already built on pure asyncio, adopting anyio's model is a significant migration. aiofence works directly with asyncio's cancel()/uncancel() counter protocol — no new runtime, no new cancellation model.

  2. Different design philosophy. anyio's approach is a broad CancelScope over the whole operation, with CancelScope(shield=True) around the parts that must survive. aiofence takes the inverse: most code runs unaware of cancellation, and you wrap only the expensive, safely-interruptible parts with a Fence.

How It Works

Fence is a sync context manager that arms triggers against the current asyncio task. When a trigger fires, the task is cancelled via asyncio's native cancel()/uncancel() counter protocol. On exit, the CancelledError is suppressed and the counter is balanced. The caller inspects fence.cancelled and fence.reasons after the block. Just asyncio's own machinery, used correctly.

Requirements

Python 3.12+. No dependencies.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aiofence-0.0.2.tar.gz (7.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aiofence-0.0.2-py3-none-any.whl (8.6 kB view details)

Uploaded Python 3

File details

Details for the file aiofence-0.0.2.tar.gz.

File metadata

  • Download URL: aiofence-0.0.2.tar.gz
  • Upload date:
  • Size: 7.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aiofence-0.0.2.tar.gz
Algorithm Hash digest
SHA256 3184347bd9551e0137007582b20b98d9fe46f8dfc5ba6d44b4d55a9327742a05
MD5 7171b7fcf72471f10f8e7b1245b38003
BLAKE2b-256 5510f07930a9ab66e7f482fd529af1e7fdb09a85ff50782fdf3c129225251eb2

See more details on using hashes here.

File details

Details for the file aiofence-0.0.2-py3-none-any.whl.

File metadata

  • Download URL: aiofence-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 8.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aiofence-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 5294dc9b0924534739e9035b297ac0b26c93fa0065b6e69aa7ab6909e232bfc3
MD5 558d1a256ded03ef851cff2604b24145
BLAKE2b-256 fd0185233ffb0546e9efd26983e95e84847d0d5e3978d204314e48c2c5841030

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page