Repository navigation
Conversation
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. |
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0554774ef3
ℹ️ 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".
|
We see the same Minimal repro of the GC path: open streams, read the first chunk, then drop them while they are still unreachable from a reference cycle. import asyncio, gc, httpx2
async def handle(reader, writer):
while (await reader.readline()) not in (b"\r\n", b""):
pass
writer.write(b"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n5\r\nhello\r\n")
await writer.drain()
await reader.read()
async def main():
server = await asyncio.start_server(handle, "127.0.0.1", 0)
url = f"http://127.0.0.1:{server.sockets[0].getsockname()[1]}/"
client = httpx2.AsyncClient()
for _ in range(5):
response = await client.send(client.build_request("GET", url), stream=True)
chunks = response.aiter_bytes()
await anext(chunks)
holder = [chunks, response]
holder.append(holder)
del chunks, response, holder
gc.collect()
for _ in range(20):
await asyncio.sleep(0.05)
await client.aclose()
server.close()
asyncio.run(main())Results on Python 3.13.16:
The shutdown repro from #1280 also goes from 5 errors to 0 with this PR. So this PR fixes the GC-finalizer path too, not only shutdown. Thanks! One observation, in case it is useful: with this PR, after a consumer stops iterating |
Closes #1280.
When the event loop shuts down with streamed responses still open,
loop.shutdown_asyncgens()closes every async generator concurrently. If one generator closes another insideaclosing, and the inner one awaits during cleanup (closing the response or connection), asyncio and the outer generator both callaclose()on it, and one of them getsRuntimeError: aclose(): asynchronous generator is already running. This has been there since #137.This removes the nesting instead of the cleanup:
PoolByteStream.__aiter__returns the inner stream iterator directly. It was a pure pass-through generator.Response.aiter_raw()andResponse.aiter_bytes()return one shared generator (identity decoder vs content decoder) instead ofaiter_bytes()wrappingaiter_raw().Connections are still closed at shutdown. A narrower fix that skipped cleanup on
GeneratorExitsilenced the errors but leaked sockets, so I dropped it.aiter_rawandaiter_bytesare now plain methods returning an async generator, soinspect.isasyncgenfunction()returnsFalsefor them. Calling and iterating them works the same.AI Disclaimer
This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.