Skip to main content

CancelLens: a lens inspecting an interrupted async path

CancelLens

Checks CPython License Runtime dependencies

CancelLens tests what happens when an asyncio operation is cancelled while suspended. It observes actual suspension points, repeats the operation on fresh state, and sends real Task.cancel() requests. Your oracle checks resource release, state consistency, and whether another operation can use the same pool or resource.

It works with ordinary pytest tests, native Python coroutines and real I/O. Cancellation sweeps are an existing technique; a passing sweep is evidence for the checked schedules and your invariants, not a proof of general cancellation safety.

Installation

Install a published release:

python -m pip install cancellens

For a source checkout:

uv sync --locked
uv build
uv pip install dist/cancellens-0.1.0-py3-none-any.whl

The runtime uses only the standard library. Tests can use pytest without a plugin or pytest-asyncio.

Quick start

import asyncio

from cancellens import Case, Completion, require, sweep


def test_pool_survives_cancellation():
    async def factory():
        return asyncio.Semaphore(1)

    async def operation(pool):
        await pool.acquire()
        try:
            await asyncio.sleep(0)
        finally:
            pool.release()

    async def oracle(pool, completion: Completion):
        require(completion.state in ("returned", "cancelled"), "operation terminated")
        require(not pool.locked(), "pool slot returned")
        await pool.acquire()  # Check reuse of the SAME resource.
        pool.release()

    async def teardown(pool):
        if pool.locked():
            pool.release()

    asyncio.run(sweep(Case(factory, operation, oracle, teardown))).assert_passed()

Move pool.release() out of finally to the happy path and cancellation loses the slot. The oracle reports FAIL before teardown repairs it. The pool example includes both versions.

Case lifecycle

Every attempt creates fresh state through four async callbacks:

  1. factory() creates and returns the state. It owns cleanup of incomplete setup.
  2. operation(state) returns a native Python coroutine and runs in a real Task.
  3. oracle(state, completion) checks state before harness cleanup. It can explicitly await application-owned cleanup and exercise the same resource again.
  4. teardown(state) cleans up the attempt, including failed attempts. A successfully returned factory resource is retained for teardown even if caller cancellation interrupts the handoff.

Use require(condition, "invariant name") or raise InvariantViolation to report a contract failure. Other oracle exceptions, including AssertionError, are errors in evaluating the test. Use static, non-sensitive invariant text: it appears in reports.

Completion.state is returned, cancelled, raised, pending, or not_started. value and exception are available in memory. Operation return values, exceptions, and suppressed cancellation are judged by your oracle. Operation Task completion preserves cancelled() and exception() for done callbacks.

Schedules, outcomes and coverage

A sweep runs two uncancelled baselines and compares their complete traces. It then cancels at each baseline point. With max_depth=2, the default, it also tests a second cancellation at later points observed on each valid single-cancel path. Those points can include cleanup that never ran in the successful baseline.

Requests are sent only after the required trace prefix matches. The second request also requires confirmed delivery of the first. There are no random delays selecting cleanup targets, and no extra awaits are inserted into the operation.

  • PASS: the schedules in the report's scope were evaluated and satisfied the oracle.
  • FAIL: an attributable schedule violated an explicit invariant.
  • UNRESOLVED: a target or delivery could not be confirmed, replay diverged, a budget cut coverage short, or no suspension points existed.
  • ERROR: setup, instrumentation, oracle code or infrastructure failed.

Each attempt retains its schedule, trace, cancellation acceptance/delivery, operation completion, oracle and teardown outcomes. FAIL and ERROR are preserved independently of cleanup failures. An oracle violation on an invalid schedule remains visible but is not promoted to an established cancellation bug.

report.coverage exposes planned/valid single and double schedules, baseline points, attempt count, completeness and reasons. Incomplete coverage cannot produce PASS. report.assert_passed() gives a short pytest failure with schedule and coverage.

from cancellens import Limits

limits = Limits(
    max_points=100,
    max_attempts=100,  # Includes both baselines.
    max_depth=2,  # Choose 1 for a single-cancel sweep.
    phase_timeout=2.0,
    total_timeout=30.0,
    cleanup_timeout=0.1,
)

Reports and replay

import json
from cancellens import Schedule, replay

# Inside an async test, with a Case already defined:
report = await sweep(case)
saved = json.loads(report.to_json())
assert saved["schema_version"] == 1
schedule_data = next(a["schedule"] for a in saved["attempts"] if a["schedule"]["cancel_at"])
replayed = await replay(case, Schedule.from_dict(schedule_data))
replayed.assert_passed()

A schedule stores one-based targets and the full observed prefix through its last target. Replay checks visible frame locations, bytecode offsets, code fingerprints and yielded types. Different Python minor versions are refused; changed source or scheduling may make a schedule unresolved. A replay report covers the selected schedule, not a whole sweep. An empty schedule is an uncancelled replay control.

Reports omit locals, object reprs, absolute paths, SQL, response bodies, operation values and exception messages. Code metadata, exception types and explicit invariant text remain visible. See the design and report reference.

Supported environments

CPython 3.10–3.14, standard asyncio event loop, default Task factory. CI runs the suite on Linux and Windows across all five versions. macOS is also used for local validation. Python 3.15+, PyPy, uvloop, free-threaded builds, custom/eager factories and Trio/AnyIO cancellation semantics are outside v0.1's supported scope.

Observation follows nested Python awaits. Child Tasks and shielded work are boundaries: only the caller's wait is visible, and shielded cleanup may outlive the caller. The oracle must check or explicitly await it. On CPython 3.10, a cancelled child Task can lose the cancellation request marker; uncertain delivery is UNRESOLVED. Child suppression or replacement of the marker can also prevent attribution on later versions.

C awaitables and coroutine_wrapper can hide deeper source frames. No exact source location is promised for every extension awaitable. Wrapper frames and Task.get_coro() identity are not preserved. Lifecycle phases run in separate Tasks, so pass setup configuration explicitly in case state rather than relying on changed ContextVars. Prefix repeatability does not imply program-wide determinism or exhaustive interleavings.

Deadlines and cleanup

In-process deadlines are cooperative. They cannot stop a blocked event loop, arbitrary native code, a cancellation-resistant coroutine or a running thread. After a deadline, the oracle observes state before teardown and emergency cancellation. The exploration budget can be extended by the current oracle/teardown windows and final drain.

Caller cancellation propagates after attempted bounded teardown; another external cancellation can interrupt finalization. Cases own application Tasks, threads and external resources. CancelLens does not automatically roll back arbitrary side effects.

Use an external watchdog for potentially hanging tests:

uv run python validation/watchdog.py --timeout 90 -- uv run pytest -q

The validation watchdog terminates/kills the child process group on POSIX and the immediate child on Windows. It cannot undo external side effects.

External validation: aiosqlite

The aiosqlite case uses real SQLite connections, temporary files and upstream worker threads. Event-controlled connector hooks select cancelled opening without replacing the upstream lifecycle. Independent native queries and worker joins check the resource before final cleanup.

  • Base 9b127cecc7de843bf4f343b6da63b641866533fb: FAIL because the native connection remains open.
  • Proposed fix 005b3aa77a67606d2264996aeff4f69448fb79fc: PASS for the same logical opening schedule.

Recorded evidence contains repeated fresh sweeps, opening replays and uncancelled controls. This verifies the selected proposed patch's opening contract, not all aiosqlite cancellation behavior. The optional HTTPX/httpcore double-cancel integration is not verified in v0.1.

Contributing and releases

See Contributing for locked development setup, regression checks and integration reproduction. Changes are listed in the changelog.

CancelLens uses independent coroutine-protocol instrumentation, the standard loop, real I/O and user-defined resource oracles. No code from these projects is incorporated.

Metadata

Release files for cancellens 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cancellens 0.1.0
File Size Uploaded Provenance Actions
cancellens-0.1.0.tar.gz 104.1 kB With provenance Details for cancellens-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cancellens 0.1.0
File Interpreter ABI Platform Actions
cancellens-0.1.0-py3-none-any.whl Python 3 none any Details for cancellens-0.1.0-py3-none-any.whl

Total release size: 123.2 kB

Release files / cancellens-0.1.0.tar.gz

Download URL cancellens-0.1.0.tar.gz
Size 104.1 kB
Tags Source
SHA-256 checksum
How to use checksums
51e972ddd6e4b188568603debca39f8618c96cacb5fbc7cab52d6e0c2521cbbf
BLAKE2b-256 checksum
How to use checksums
fca1f3fb80e0a5b823aeb78935162f25b9b4d4890cea02c141f3d983bff2a687
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / cancellens-0.1.0-py3-none-any.whl

Download URL cancellens-0.1.0-py3-none-any.whl
Size 19.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e80118f7c3f232ebb3772731ed6a3eb0b91ee1d75fdc2fb31ad25e18182adabd
BLAKE2b-256 checksum
How to use checksums
439ed992c47596b8e7bf2a8cd1228af67ec150c3681f1629b59e451412115401
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Security for cancellens 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cancellens-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page