perf: O(1) qsize, AWS Full Jitter, buffered recordings#111
Conversation
Replace the O(n) linear scan over the deque (subtracting the at-most-one sentinel) with an integer counter incremented in put() and decremented in get()/__anext__() and on DROP_OLDEST eviction. Measured 600x regression at maxsize=1000 in the audit becomes a single attribute read. Sentinel never counts toward the running total, so qsize() continues to report logical occupancy unchanged. Regression test exercises put, get, DROP_OLDEST eviction, and put_sentinel paths to pin the counter against logical occupancy. Closes #103 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the fixed-magnitude additive jitter (base * 2**attempt + uniform(0, 0.5)) with AWS "Full Jitter": uniform(0, min(cap, base * 2**attempt)). The previous formula collapsed to <3% jitter at higher attempts and forced every parallel client into a narrow ~0.5s window on attempt 0, defeating the purpose of jitter during retry storms. Full Jitter spreads colliding retries evenly across the entire backoff window. retry_max_delay continues to cap the exponential term before randomization. Tests assert: every sample stays within [0, capped_upper]; attempt-0 samples cover the full window with distinct values; the cap clamps high attempts. Closes #104 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RecordingTransport / AsyncRecordingTransport previously did load_pairs -> append -> save_pairs (with atomic .tmp rename) for every single request. With N sequential requests to one endpoint that's O(N^2) work over the session: the last request re-parses and re-writes all 999 prior pairs. Switch to a session-scoped _RecorderBuffer that lazily loads any existing on-disk pairs once per (method, path), appends to an in-memory list, and flushes only the dirty keys to disk on close()/aclose(). Re-recording into the same fixtures dir still extends prior pairs (load-once preserves the original semantics). Regression test counts save_pairs invocations: 50 requests to one endpoint must produce exactly one save_pairs call at flush time, holding a list of 50 pairs. Second test verifies cross-session append safety. Closes #105 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #111: O(1) qsize, AWS Full Jitter, buffered recordingsOverall this is a solid, well-scoped performance PR. Each fix is independently motivated, the test coverage matches the changes, and the implementation choices are sound. A few things worth calling out. #103 — O(1)
|
| Change | Test quality |
|---|---|
| #103 qsize counter | Good — covers put/get/drop/sentinel transitions |
| #104 full jitter | Good — upper-bound invariant + spread check + cap-clamp |
| #105 buffered flush | Good — call-count regression + cross-session semantics |
Verdict
Approve with minor suggestions. No blocking issues. All three changes are correct and well-tested. Suggested follow-ups (none blocking):
- Add a test mixing
get()and__anext__()on the same queue to fully close the counter coverage gap. - Add a note to
_compute_backofforKalshiConfig.retry_base_delaythat Full Jitter allows near-zero first-retry delays (the old minimum floor from additive jitter is gone). - Restore the sequential-recordings comment in
AsyncRecordingTransport.handle_async_request— it matters more there than in the sync version.
Summary
Three disjoint performance fixes from the v1.2 audit:
MessageQueue.qsize()is O(n) linear scan #103MessageQueue.qsize()is now O(1) via a counter updated on put/get/__anext__/DROP_OLDEST eviction (sentinel never counts). Picked the counter approach per task hint.uniform(0, min(cap, base * 2**attempt)). Cap applied before randomization so the upper bound never exceedsretry_max_delay.RecordingTransportdoes O(N²) work per recording session #105RecordingTransportrewrite: new_RecorderBufferlazy-loads existing on-disk pairs once per(method, path), tracks a dirty set, flushes only touched keys on close. Cross-session append semantics preserved. Was O(N²) per session; now O(1) amortized per request.Test deltas
tests/ws/test_backpressure.py(+)tests/test_backoff.py(new)random.seedtests/test_mock_transport.py(+)save_pairscall for 50 sequential requests (no wall-clock)Full suite: 1413 passed (+6), 48 skipped.
ruff+mypy --strictclean.Closes #103
Closes #104
Closes #105
Test plan
uv run pytest tests/ --ignore=tests/integration -vuv run ruff check .uv run mypy kalshi/