Repository navigation
Switch to httpx2 and reuse connections for streaming requests - #1282
Conversation
Read HTTP/1.1 streams to the end so their connection returns to the pool, and give each in-flight request its own pool. Signed-off-by: QHarshil <harshil_c@hotmail.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 99db2f674b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A custom transport turns off httpx's environment proxy handling, so mount a per-request pool for each proxy from the environment. Signed-off-by: QHarshil <harshil_c@hotmail.com>
sjmonson
left a comment
There was a problem hiding this comment.
I see some logic in ensuring we properly close streams, but the per-request transports doesn't make sense.
Drop the per-request pools. After the final event on HTTP/1.1, a task reads the rest of the response so the request returns at once, and process_shutdown waits for those tasks. Signed-off-by: QHarshil <harshil_c@hotmail.com>
httpcore2 rewrites the pool's request assignment, which removes the stall a shared pool hit once connections were reused. Tests mock with pytest-httpx2. Signed-off-by: QHarshil <harshil_c@hotmail.com>
|
Received a single approval. Will automatically queue for merge once two maintainers approve, all change requests are resolved, and DCO passes. |
|
Queued — the merge queue status continues in this comment ↓. |
Merge Queue Status
This pull request spent 31 minutes 13 seconds in the queue, including 30 minutes 23 seconds running CI. Required conditions to merge
|
jaredoconnell
left a comment
There was a problem hiding this comment.
Looks good to me. It even performs better on my mock server.
main moved the mock server tests from httpx to httpx2 in vllm-project#1282, so the two new tests no longer resolved httpx.AsyncClient. Assisted-by: Claude Code claude-opus-5-5 Signed-off-by: MinJun Choi <mj04300017@gmail.com>
## Summary Fixes `max_concurrent_requests` for streaming requests. Right now the slot is released as soon as the handler returns its `ResponseStream`, before any tokens are generated, so streaming requests were never limited. This holds the slot until the stream actually finishes. ## Details - [x] In `_run_generation`, a streaming response's `streaming_fn` is wrapped so the slot is released when the stream ends, including when the client disconnects partway through. Non-streaming requests work the same as before. - [x] Two new tests: overlapping streams now run one at a time, and a stream the client abandons still frees its slot so the next request doesn't hang. ## Test Plan - `lint-check`, `type-check`, `test-unit` (3,658 passed), `test-integration` (32 passed) and `test-e2e` (14 passed) all pass. - The streaming test fails on `main`, and the disconnect test fails if the slot is only released after a stream completes normally. - Same repro as the issue, with this change: the second streaming request now waits for the slot (0.51s / 1.01s), same as non-streaming. ## Related Issues - Resolves #1261 --- - [x] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [x] Includes code generated or substantially modified by an AI agent - [x] Includes tests generated or substantially modified by an AI agent --- # git log commit 3cd601e Author: MinJun Choi <mj04300017@gmail.com> Date: Thu Oct 8 13:39:40 2026 +0900 Hold the mock server's concurrency slot for the whole stream max_concurrent_requests is meant to cap in-flight generation requests, but for streaming requests it didn't cap anything. The semaphore was only held while the handler ran, and a streaming handler returns its ResponseStream right away. Sanic runs the actual generation afterwards, so the slot was already free by the time the first token went out. Now the slot is released when the stream finishes instead, including when the client disconnects partway through. Non-streaming requests behave as before. Assisted-by: Claude Code claude-opus-5-5 Signed-off-by: MinJun Choi <mj04300017@gmail.com> commit 416e962 Author: MinJun Choi <mj04300017@gmail.com> Date: Thu Oct 8 13:39:40 2026 +0900 Test max_concurrent_requests with streaming requests The existing test only covered non-streaming requests, which is how the streaming gap went unnoticed. Add one test that overlapping streams run one at a time, and one that a stream the client abandons still frees its slot, so the next request doesn't hang. Assisted-by: Claude Code claude-opus-5-5 Signed-off-by: MinJun Choi <mj04300017@gmail.com> commit 9b3167e Author: MinJun Choi <mj04300017@gmail.com> Date: Fri Oct 9 22:31:23 2026 +0900 Compare stream finish times in the streaming concurrency test The test passed when the slower of two overlapping streams took at least 0.3s. Without the fix the streams run together and the slower one took about 0.26s, so a busy CI runner could push it past the bound and let the test pass on a broken server. Check the gap between the two finish times instead. Run together, they finish within about a millisecond of each other; serialized, the second finishes about a ttft later. Over ten runs each, the gap was 0.000-0.001s without the fix and 0.205-0.220s with it, and a slower runner delays both streams without closing the gap. Assisted-by: Claude Code claude-opus-5-5 Signed-off-by: MinJun Choi <mj04300017@gmail.com> commit 75e83c9 Author: MinJun Choi <mj04300017@gmail.com> Date: Fri Oct 16 11:11:17 2026 +0900 Use httpx2 in the streaming concurrency tests main moved the mock server tests from httpx to httpx2 in #1282, so the two new tests no longer resolved httpx.AsyncClient. Assisted-by: Claude Code claude-opus-5-5 Signed-off-by: MinJun Choi <mj04300017@gmail.com> --------- Assisted-by: Claude Code claude-opus-5-5 Signed-off-by: MinJun Choi <mj04300017@gmail.com>
## Summary Fixes #1432. The Prometheus collector added in #1250 imports `httpx`, which is no longer a declared dependency after #1282 moved the codebase to `httpx2`. On a pip install of `main`, every `guidellm run` fails with `ModuleNotFoundError: No module named 'httpx'`. ## Related Issues - Resolves #1432 ## Details - `src/guidellm/collectors/prometheus.py`: `httpx` → `httpx2` (`AsyncClient`, `AsyncBaseTransport`, `HTTPError`, `InvalidURL`). - `tests/unit/collectors/test_prometheus.py`: `httpx` → `httpx2` (`Request`, `Response`, `MockTransport`, `ConnectError`). `httpx2` exports all of these with the same API, and `InvalidURL` is still a separate exception from `HTTPError`, so the `except` clause behaves the same. No functional change. ## Test Plan In a venv with `httpx` uninstalled (`pip show httpx` → not found), WSL2 / Python 3.14.4: - `python -m pytest tests/unit/collectors/test_prometheus.py`: 20 passed - `guidellm run` against `guidellm mock-server` with synthetic data (concurrent@10, 20 requests): 20/20 completed, 0 errored. Before this change, the same command failed at import with `ModuleNotFoundError: No module named 'httpx'`. ## Use of AI The change was drafted with Claude's help. I applied it and ran the tests above locally myself. --- - [x] "I certify that all code in this PR is my own, except as noted below." - [x] Includes code generated or substantially modified by an AI agent - [ ] Includes tests generated or substantially modified by an AI agent --------- Assisted-by: Claude Signed-off-by: Swetha Kommuri <swethakommuri10@gmail.com>
Summary
Streaming requests never reuse a connection. The stream loop stops at
[DONE]before the response ends, so the connection is closed, and at high request rates the client runs out of local ports. At 800 req/s for 60 s against a local server, 12,078 requests failed withConnectErroron main. This reads the rest of each HTTP/1.1 response in the background so its connection goes back to the pool, and switches to httpx2, whose pool handles that reuse without the stall httpx's pool has. Fixes cause 2 of #1238 and closes #1300.Details
httpxwithhttpx2in the dependencies, source, and tests. Tests mock requests withpytest-httpx2.process_shutdownwaits for these tasks. HTTP/2 streams stop at the final event.SSL_CERT_FILE/SSL_CERT_DIRwhen set. httpx used certifi's bundle.Test Plan
tox -e lint-check,type-check,test-integration: pass.tox -e test-unit,test-e2e: only the libtorchcodec failures intest_audio.pyandtest_encoders.pyand thetest_realtime_ws_e2eerror, same as main.Related Issues
Use of AI
git log
commit 99db2f6
Author: QHarshil harshil_c@hotmail.com
Date: Fri Oct 9 21:21:57 2026 -0700
commit 8b7ebf5
Author: QHarshil harshil_c@hotmail.com
Date: Fri Oct 9 21:48:25 2026 -0700
commit d87fa2c
Author: QHarshil harshil_c@hotmail.com
Date: Wed Oct 14 12:10:35 2026 -0700
commit 14036da
Author: QHarshil harshil_c@hotmail.com
Date: Wed Oct 14 12:10:35 2026 -0700
Signed-off-by: QHarshil harshil_c@hotmail.com