Skip to content

Auth: KalshiClient.close() shouldn't shut down externally-provided KalshiAuth executor #210

Closed

Description

@TexasCoding

Summary

KalshiClient.close() (sync and async) unconditionally calls self._auth.close(). from_env() constructs KalshiAuth locally so ownership is unambiguous, but the public auth=... ctor kwarg is explicitly documented for sharing — a user can build one KalshiAuth and hand it to multiple clients (REST + async REST + WS).

Closing any one of those clients calls ThreadPoolExecutor.shutdown(wait=False) and nulls _sign_executor. The next sign_request_async on a sibling client lazily re-creates the executor (so signs eventually succeed), but any sign already in flight on the old executor races with shutdown — submit after shutdown raises RuntimeError: cannot schedule new futures after shutdown and propagates up through the async transport's retry loop.

This is a latent foot-gun for any deployment that pairs KalshiClient + AsyncKalshiClient + KalshiWebSocket off one KalshiAuth (a documented pattern in docs/authentication.md).

Separately, KalshiAuth.close() itself sets _sign_executor = None then the lazy-init in _get_sign_executor silently recreates the executor on next use. So close() is not idempotent in the close-once sense — it's actually "shutdown + restart on demand".

Location

  • kalshi/client.py:140-144 — KalshiClient.close()
  • kalshi/async_client.py:171-175 — AsyncKalshiClient.close()
  • kalshi/auth.py:260-271 — KalshiAuth.close() (and the lazy re-init in _get_sign_executor)

Evidence

# kalshi/client.py:140-144
def close(self) -> None:
    self._transport.close()
    if self._auth is not None:
        self._auth.close()   # ← closes executor even if auth was provided externally
# kalshi/auth.py:268-271
with self._sign_executor_lock:
    if self._sign_executor is not None:
        self._sign_executor.shutdown(wait=False)
        self._sign_executor = None   # ← next sign_request_async() recreates

Recommended fix

Part 1 — ownership-aware close

In KalshiClient.__init__, track whether the auth was provided externally:

self._auth_owned = auth is None  # True only when we constructed it from kwargs/env

# In close():
if self._auth is not None and self._auth_owned:
    self._auth.close()

Document the ownership contract in KalshiClient.__init__ and KalshiAuth docstrings.

Part 2 — make close() terminal

In KalshiAuth, add a _closed: bool = False flag. Set in close(). In _get_sign_executor, raise:

if self._closed:
    raise RuntimeError(
        "KalshiAuth has been closed; create a new instance to sign further requests"
    )

Document close() as terminal in the docstring.

Tests:

  • test_close_does_not_shut_externally_provided_auth
  • test_close_shuts_locally_constructed_auth
  • test_closed_auth_raises_on_sign_request_async

Severity & category

medium / concurrency, dx

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