Repository navigation
Fix goroutine leak in the proxy - #14811
Conversation
|
I think I prefer this one of the two; the overall change is less involved. I do like the suggestion of filtering out I would suggest dropping the wrap of errors with func isConnTeardown(err error) bool {
if se, ok := err.(syscall.Errno); ok {
return se == syscall.ECONNRESET || se == syscall.EPIPE
}
return false
} |
19aafd3 to
990f2e5
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #14811 +/- ##
==========================================
- Coverage 52.13% 52.06% -0.07%
==========================================
Files 197 197
Lines 16012 16008 -4
==========================================
- Hits 8348 8335 -13
- Misses 6356 6362 +6
- Partials 1308 1311 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Thanks. I'll drop the Errors from a TCP conn don't arrive as a bare
func isConnTeardown(err error) bool {
var se syscall.Errno
if errors.As(err, &se) {
return se == syscall.ECONNRESET || se == syscall.EPIPE
}
return false
}I also dropped the |
Signed-off-by: Leo King <spicysharkco@gmail.com> Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
78623d5 to
46cc90b
Compare

Proposed Changes
Proxystarts onepipegoroutine per direction and returns the first error it receives onerrc.errcwas unbuffered, and thedoneflag that kept a second sender away was a plain bool written by both goroutines. When both directions failed at about the same time (the usual case on a TCP reset), both goroutines could pass thedonecheck; the second one then blocked forever on the send, holding its 32 KiB buffer and both connections.errcnow has room for both errors, so eachpipegoroutine sends once and returns. Thedoneflag and theerrmethod are gone, which also removes the data race.Proxystill returns the first error and logs it unless it isio.EOF. It now logs once per connection; with the race, both goroutines could log. The second goroutine exits as soon as itsReadorWritefails, whichProxytriggers by closing both connections on return.I kept "return on the first error" rather than the wait-for-both-directions pattern in remotedialer's
client_dialer.go, soProxycannot hang on a connection whoseClosedoes not unblock a pendingRead. Following @bheemareddy's review suggestion, which matches the classification in #14810, connection resets and broken pipes (syscall.ECONNRESET,syscall.EPIPE, matched witherrors.As) are now logged at debug level instead of asProxy error:warnings. Other errors are still logged as warnings, andio.EOFis still not logged.Types of Changes
Bugfix
Verification
agent) and an aggregated APIService backed by an in-cluster Service, as in Goroutine leak inpkg/daemons/control/proxy: second pipe goroutine blocks forever on unbufferederrcwhen both directions fail (data race ondone) #14787.Proxy error: ... connection reset by peer, dump goroutines through the kubelet:kubectl get --raw "/api/v1/nodes/$N/proxy/debug/pprof/goroutine?debug=2".pkg/daemons/control/proxy. Before this change, the count ofproxy.(*proxy).errgoroutines only grows.Testing
Added
Test_UnitProxyBothDirectionsFailinpkg/daemons/control/proxy. It runsProxy10000 times on connections whose reads and writes always fail, then waits for the goroutine count to return to where it started. On the old code it fails (1029 goroutines left over) and-racereports the race ondone; with this change it passes 20 of 20 runs ofgo test -race -count=20 ./pkg/daemons/control/proxy/. It catches the bug on multi-core runners; withGOMAXPROCS=1the old code can pass. A blocked goroutine is not visible to an e2e test, so this is a unit test; I can drop it if you prefer.Added
Test_UnitProxyLogLevel, which checks the level forio.EOF, a TCP reset, a broken pipe and an unrelated error.Linked Issues
Fixes #14787
User-Facing Change