Repository navigation
roundtripper: give back the stream an unused round tripper holds - #15
Conversation
NewRoundTripper confirms its connection before returning, and that connection holds one of the session's streams until a request takes it. kindling discards connected round trippers that lose a race, so on a client that keeps losing short races (Unbounded's signaling polls) the streams leak. The session then never looks idle, so it's never retired, and once MaxStreams have leaked every dial fails with "soar: too many streams" until the app restarts. One Windows beta client did that ~1,800 times in two days. The returned transport now implements io.Closer, which releases an untaken connection, and an untaken connection closes itself after preDialedTTL for callers that never close it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQFpfCXs3JqrfyDuiGabF5
|
Warning Review limit reachedOnly developers with an assigned seat can use this organization's usage-based review budget, and seats here are assigned manually. Ask an admin to assign a seat, or change the review continuation mode in Billing. Next included review available in 49 minutes. View limit details
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
🟡 Changes recommended
The explicit-close regression test can pass through automatic expiry, masking a broken Close cleanup path.
1 open issue
What changed in this PR
This PR prevents unused Soar HTTP transports from indefinitely holding session streams, avoiding stream exhaustion in long-running clients.
Changes:
- Wraps
http.Transportwith explicit cleanup for parked and idle connections. - Automatically releases unclaimed connections after 10 seconds.
- Adds regression tests for explicit cleanup and automatic expiry.
| File | Description |
|---|---|
roundtripper.go |
Adds synchronized cleanup through Close and an expiry timer. |
e2e_test.go |
Tests stream recovery after closing or abandoning unused transports. |
Review effort: Balanced
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQFpfCXs3JqrfyDuiGabF5

Summary
This fixes a stream leak that permanently breaks Soar on a long-running client.
NewRoundTripperconfirms its connection before returning, and that connection holds one of the session's streams until a request takes it. kindling drops round trippers that connect after another transport has already won, and nothing closed their parked connection.What happened in the field: one Windows client on 10.0.3-beta-10 failed every Soar attempt about 1,800 times over two days with
soar: too many streams. It had won 75 times before that.freddie.iantem.iorun every ~14 s. Soar often connects after another transport has already answered, so each of those polls can leak one stream.MaxStreams), every dial fails instantly until the app restarts.sequenceDiagram autonumber participant K as kindling<br/>race_transport.go participant R as Soar transport<br/>roundtripper.go participant S as Soar session<br/>session.go K->>R: race_transport.go:627<br/>NewRoundTripper at 8 s R->>S: roundtripper.go:24<br/>DialContext opens a stream Note over R: roundtripper.go:32<br/>confirmed conn parked in preDialed ⚠️ R-->>K: round tripper, holding the stream Note over K: another transport already won rect rgba(255, 200, 200, 0.3) Note over K: race_transport.go:632, drainReady :558<br/>round tripper dropped, never closed 🐛 Note over S: stream stays open, session never idle end K->>R: next races, about every 14 s R->>S: openStream S-->>R: session.go:147<br/>soar: too many streams, after 64 leakskindling line numbers are from current
main. A companion kindling PR closes the round trippers it drops. This PR makes Soar safe on its own as well, so neither side depends on the other.Changes
roundtripper.go:*roundTripperthat embeds*http.Transportand implementsio.Closer.Closecloses the parked connection if no request took it, and closes idle connections. Requests already in flight are unaffected.http.RoundTripper, so the public API doesn't change.preDialedTTL(10 s), for callers that never callClose. A request that arrives after that just dials a fresh stream through the same transport.preDialed.mu.e2e_test.go: two tests, each withMaxStreams: 2.TestClosedRoundTrippersGiveBackTheirStreams: closing unused round trippers frees their streams.TestUnusedRoundTripperGivesBackItsStream: unused round trippers free their streams after the TTL, even withoutClose.Closetest pinspreDialedTTLto an hour, so expiry can't stand in for a brokenClose. It fails withsoar: too many streamswhenClosestops releasing the connection, and the expiry test fails the same way when expiry is disabled.soar-rs needs no change. It has no round tripper that parks a connection, and its
Conncloses its stream onDrop.Verification
gofmt,go vet ./...andgo test -race -count=1 ./...all pass, in the root module and inbench.preDialed.closemade a no-op, both new tests fail withsoar: too many streams. With onlyroundTripper.Closebroken, theClosetest fails on its own.Pre-PR review (local Codex gate)
CODEX GATE: verdict=approve critical=0 important=0 minor=0 reviewed=7approve. Besides the diff, the reviewer readclient.go,stream.goandsession.go.🤖 Generated with Claude Code
https://claude.ai/code/session_01QQFpfCXs3JqrfyDuiGabF5