Eliminate multi-assignment churn in the connection pool

Merged
KL
3mo ago
+296-22

Summary

Investigating #1074: with many requests queued, throughput collapses. Profiling showed _assign_requests_to_connections at ~70% of runtime, called ~14x per request instead of 2.

The cause: assignment does not change an HTTP/1.1 connection's state, so a newly idle connection was handed to every queued request (and re-picked by later passes until the winner sent on it). Every loser woke, failed with ConnectionNotAvailable, re-queued, and triggered another full O(n) assignment pass - quadratic churn at high queue depth. #974 fixed the per-pass cost but intentionally preserved this behavior; this PR removes it, porting the "Bug 2" half of encode/httpcore#1035.

Changes

  • Assign each idle HTTP/1.1 connection to exactly one queued request: pop it from the availability snapshot within a pass, and exclude reserved-idle connections when building the snapshot.
  • Add can_multiplex() to the connection interface (False by default, True for established HTTP/2) so multiplexing connections are exempt and h2 bursts keep multiplexing.
  • Stop scanning the request queue once nothing can be assigned, and skip expiry probes and surplus-keepalive eviction for reserved connections - the latter could previously close a connection just handed to a request.
  • Regression tests: exactly 2 assignment passes per request (fails on main with 32 for 10 requests), and a warm-idle h2 burst must be fully assigned within one pass.

Benchmarks

1000 concurrent GETs against a local uvicorn server:

scenariomainthis PR
default pool5.07s0.33s
max_connections=1100s0.53s
sequential / low concurrency0.55sunchanged

Parallel is now faster than sequential (0.33s vs 0.55s), confirmed on the discussion author's own benchmark. Incremental reservation bookkeeping to remove the last O(n)-per-pass term follows in #1076.

Verification

  • Full suite passes with 100% coverage; mypy/ruff/unasync clean.
  • HTTP/2 verified against hypercorn over TLS and real services: cold and warm bursts multiplex on a single connection, unchanged.
  • Concurrent streaming (sha256-verified), cancellation storms, keepalive expiry, PoolTimeout, and threaded sync Client all behave as before.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.