Skip to content

AsyncTransport.close() sets _closed before aclose(); cancellation leaks the pool #333

Closed

Description

@TexasCoding

Problem

The #301 idempotency fix sets self._closed = True before awaiting self._client.aclose(). If the surrounding task is cancelled at the await point, the _closed flag is already set but aclose has not finished draining the connection pool. A retry path that catches CancelledError, performs cleanup, and calls close() again — the canonical pattern for async with under a parent timeout — gets a silent no-op, and the underlying httpx.AsyncClient pool is permanently orphaned until GC reaps the client.

The docstring's "fully race-free under cooperative scheduling" claim glosses over this: cooperative scheduling explicitly includes cancellation as an interleaving point. The sync close() has the same shape but cancellation isn't applicable.

Evidence

  • kalshi/_base_client.py:624-633
async def close(self) -> None:
    """Close the underlying httpx async client. Idempotent (#301)."""
    if self._closed:
        return
    self._closed = True
    await self._client.aclose()
  • CHANGELOG.md:80-85 — #301 documents the idempotency contract.

Suggested fix

Swap the order so the flag is only set after aclose actually returns, and use try/finally to keep idempotency under exceptions:

async def close(self) -> None:
    if self._closed:
        return
    try:
        await self._client.aclose()
    finally:
        self._closed = True

This means a cancellation through the await re-raises with _closed still False, so a retry actually drains the pool. Add a regression test that cancels the task awaiting close(), then awaits it again on the same instance, asserting httpx.AsyncClient.is_closed is True after the second call.

Source

Round-3 independent audit (reviewer: http_transport).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions