Skip to content

fix: close remote-loop transports instead of firing and forgetting - #39

Open
dfguerrerom wants to merge 2 commits into
mainfrom
fix/aclose-remote-close-race
Open

fix: close remote-loop transports instead of firing and forgetting#39
dfguerrerom wants to merge 2 commits into
mainfrom
fix/aclose-remote-close-race

Conversation

@dfguerrerom

Copy link
Copy Markdown
Owner

Follow-up from a Codex review of #38, which found a real defect in what merged there.

aclose() scheduled client.aclose() on another live loop with run_coroutine_threadsafe and dropped the returned future. Scheduling can succeed just before a queued loop.stop(), so that close never ran and the pool's sockets stayed open — a leak on exactly the teardown path meant to prevent one. await_remote_close() now waits on the future, and when the target loop is no longer running it cancels and calls release_sockets() directly. The regression test blocks the private loop, queues stop() ahead of the close, and asserts the real pooled descriptors end up closed.

Two smaller items, both residuals called out in #38:

  • AsyncSingleFlight folds credential refresh into one shared operation across loops, so two loops no longer each refresh the token. The per-loop auth_refresh_lock still coalesces within a loop.
  • release_sockets() now reads the socket via the network stream's get_extra_info("socket"), keeping the anyio wrapper walk only as a fallback. Less private-layout dependence.

test_a_closed_loop_releases_its_resources was also strengthened to assert the spent loops' descriptors are actually closed, rather than just that the registry shrank.

One trade-off worth a reviewer's eye: single-flight makes followers wait on the leader's refresh, so if the leader's loop dies mid-refresh the followers wait on a future that never resolves. The window is one short token request, and the previous behaviour traded that risk for duplicate refreshes. Flagging it rather than changing it.

Tests: 113 pass (109 before), stable over 5 consecutive runs of the concurrency file.

aclose() scheduled client.aclose() on another loop and dropped the future,
so a loop that stopped right after the scheduling succeeded left its sockets
open. It now waits on that close and releases the sockets directly when the
loop will not run again. Also folds credential refresh into one cross-loop
single-flight, and reads the pooled socket through the network stream's
accessor instead of walking anyio's wrapper layout.

Follow-up from a Codex review of #38.
The two are the same class only from 3.11; before that the futures one
derives from Exception, so pytest.raises(TimeoutError) let it through and
the test failed on 3.9 and 3.10.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant