polish(ws): seq tracker sync hot path + orphan dispatch correlation + reconnect exc_info + recv hot-loop polish + _stop teardown order#374
Conversation
…o; recv wait_for; _stop order Five low-severity polish/perf/correctness fixes against the WS subsystem, all sourced from the round-3 audit closure plan (wave W3). #330 (perf): ``SequenceTracker.track`` was ``async def`` solely so the rare gap path could ``await self._on_gap``. Every legitimate sequenced frame paid coroutine-object allocation + ``__await__`` teardown on the hot recv loop. Split into a synchronous ``track_sync(...) -> (bool, SequenceGap | None)`` that does all bookkeeping without awaiting; the recv loop calls it and only awaits ``_on_gap`` when a gap is returned. The original ``async def track`` is kept as a thin backwards-compat wrapper for callers and tests that exercise the old surface. #354 (correctness): ``_handle_orphan_subscribed`` sent ``unsubscribe`` for orphan sids with no correlation, and ``_handle_server_unsubscribe`` tore down state for ANY ``unsubscribed`` envelope unconditionally — so if the server reused that sid for a freshly-completed subscribe before the orphan ack landed, the new sub's seq watermark and orderbook books got clobbered. Track orphan sids in a ``_pending_orphan_unsub`` set; the unsubscribe handler short-circuits when the ack matches a pending orphan, leaving the now-legitimate owner's state intact. #355 (observability): ``ConnectionManager.reconnect`` logged each attempt failure at DEBUG with no ``exc_info`` and dropped the last exception, so the terminal ``KalshiConnectionError`` surfaced with no ``__cause__``. Operators saw "Max reconnect attempts exceeded" with no hint whether the root cause was auth, TLS, or DNS. Capture the last exception, include ``exc_info=True`` on the DEBUG attempt log, and chain the final raise via ``from last_exc`` — matches the REST transport's pattern. #356 (perf): the recv hot loop's per-frame ``asyncio.wait_for(recv(), timeout=_RECV_POLL_S)`` allocated a TimerHandle + Task wrap each iteration. Replaced with ``async with asyncio.timeout(_RECV_POLL_S)`` (Python 3.11+) which reuses the loop's underlying timer plumbing with a single context-manager allocation per frame. Behaviorally equivalent: ``TimeoutError`` is still raised on the quiet-channel poll-and-recheck path. #357 (correctness): ``_stop()`` previously cancelled the recv task and broadcast queue sentinels BEFORE closing the connection — so frames buffered inside the ``websockets`` library at the moment of ``_stop()`` could neither be drained (recv loop gone) nor signalled (queues already closed). Reordered to close-then-drain: close the connection first so in-flight ``recv()`` raises ``ConnectionClosed`` and the loop drains naturally; broadcast sentinels last so iterator consumers see the drained tail before the close sentinel terminates their ``async for``. Closes #330, #354, #355, #356, #357
Code Review — PR #374: WS subsystem polish/perf (W3-C)Reviewed all five changes. Overall this is solid, well-motivated work with good test coverage. A few issues worth addressing before merge. OverviewFive targeted fixes across the WS subsystem: a sync fast-path for the sequence tracker, orphan-subscribe ack correlation, reconnect exception chaining, a per-frame allocation cut in the recv loop, and a teardown ordering fix. All five are behaviorally correct and the PR summary accurately describes what changed. Issues1.
|
| Item | Severity | Verdict |
|---|---|---|
_stop() unguarded close() can skip sentinels |
Medium | Fix before merge |
_process_frame accesses _on_gap directly |
Low | Worth a quick fire_gap method |
Tests for _stop() error path |
Low | Nice to have |
| Everything else | — | Looks good |
The sequencing logic, orphan correlation, exception chaining, and asyncio.timeout swap are all correct. The _stop() guard is the one thing worth fixing before this ships.
|
Round-2 review addressed in 952e02a:
Verification: |
Code Review — PR #374: WS Subsystem Polish Wave W3-COverall: Well-scoped, high-quality wave. Each fix is small, clearly motivated by an issue, and independently testable. The changes are correctness improvements with measurable performance upside on the recv hot path. A few observations below.
|
| Area | Assessment |
|---|---|
| Correctness | ✅ All five fixes are correct for the stated race/ordering/performance issues |
| Performance | ✅ track_sync + asyncio.timeout eliminate hot-path allocations |
| Security | ✅ No concerns |
| Test coverage | ✅ Comprehensive; one gap noted (orphan set across reconnect) |
| Conventions | ✅ Follows project patterns: try/finally teardown, exc_info=True, explicit __cause__ chaining |
Blockers: none. The above are observations/minor nits. Ready to merge as-is.
Summary
Round-3 audit closure wave W3-C: five low-severity WS subsystem polish/perf/correctness fixes. The recv hot loop avoids per-frame coroutine and
wait_forallocations; orphan-subscribe acks no longer clobber sids the server has reused; reconnect failures preserve their root cause;_stop()follows the canonical close-then-drain order so the final frames reach iterator consumers.Issues closed
SequenceTracker.trackbecomes a sync fast path on the recv hot loopexc_info; terminal raise chains the last attempt's exceptionasyncio.wait_forreplaced byasync with asyncio.timeout(...)_stop()closes the connection BEFORE broadcasting queue sentinelsBehavioral changes
SequenceTracker.track_sync(...)is a new sync method on the public surface (the asynctrackwrapper remains for back-compat).KalshiConnectionErrorfrom exhausted reconnects now exposes__cause__(the last underlying failure). Code that exception-matches solely by message is unaffected; code that inspects__cause__will start seeing populated values._stop()ordering change is observable as: the last in-flight frames now reach iterator consumers BEFORE the close sentinel, instead of being silently dropped.Tests
tests/ws/test_sequence.py—test_issue_330_*covers track_sync happy path, forward gap, reset, duplicate, non-sequenced passthrough, and the async wrapper compat.tests/ws/test_dispatch.py—test_issue_354_orphan_unsubscribe_ack_does_not_clobber_reused_sidexercises the orphan→reuse race;test_issue_354_legitimate_server_unsubscribe_still_tears_downis the regression guard for the cleanup path.tests/ws/test_connection.py—test_issue_355_reconnect_logs_exc_info_and_chains_last_excasserts bothLogRecord.exc_infoon per-attempt logs and__cause__on the terminal raise.tests/ws/test_client.py—TestIssue356RecvHotLoopTimeoutcovers behavior + source-level guard;TestIssue357StopOrderingcovers runtime order + source-level guard.Source
Round-3 independent audit closure plan, wave W3.