Skip to content

Commit cfb09a0

Browse files
committed
net/http: fix memory leak in http.Transport
The wantConnQueue keeps reference on wantConn which keeps reference on the request context until the next request for the same key. This can be a problem if you never do another request with the same key. So now, we force the clean of the idleConnWait/connsPerHostWait after returning the response or error for a getConn.
1 parent 16d6a52 commit cfb09a0

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

src/net/http/transport.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,18 @@ func (w *wantConn) tryDeliver(pc *persistConn, err error) bool {
12261226
return true
12271227
}
12281228

1229+
// cleanContext removes the context if the wantConn is not waiting anymore.
1230+
// It mitigates memory leak as the wantConn can be stuck in the wantConnQueue and
1231+
// this context can be big (it comes from the http.Request).
1232+
func (w *wantConn) cleanContext() {
1233+
if !w.waiting() {
1234+
w.mu.Lock()
1235+
defer w.mu.Unlock()
1236+
1237+
w.ctx = nil
1238+
}
1239+
}
1240+
12291241
// cancel marks w as no longer wanting a result (for example, due to cancellation).
12301242
// If a connection has been delivered already, cancel returns it with t.putOrCloseIdleConn.
12311243
func (w *wantConn) cancel(t *Transport, err error) {
@@ -1341,10 +1353,12 @@ func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persi
13411353
beforeDial: testHookPrePendingDial,
13421354
afterDial: testHookPostPendingDial,
13431355
}
1356+
13441357
defer func() {
13451358
if err != nil {
13461359
w.cancel(t, err)
13471360
}
1361+
w.cleanContext()
13481362
}()
13491363

13501364
// Queue for idle connection.
@@ -1362,11 +1376,46 @@ func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persi
13621376
return pc, nil
13631377
}
13641378

1379+
defer func() {
1380+
t.idleMu.Lock()
1381+
defer t.idleMu.Unlock()
1382+
1383+
if err != nil {
1384+
w.cancel(t, err)
1385+
}
1386+
1387+
q, ok := t.idleConnWait[w.key]
1388+
if ok {
1389+
q.cleanFront()
1390+
t.idleConnWait[w.key] = q
1391+
if q.len() == 0 {
1392+
delete(t.idleConnWait, w.key)
1393+
}
1394+
}
1395+
}()
1396+
13651397
cancelc := make(chan error, 1)
13661398
t.setReqCanceler(treq.cancelKey, func(err error) { cancelc <- err })
13671399

13681400
// Queue for permission to dial.
13691401
t.queueForDial(w)
1402+
defer func() {
1403+
t.connsPerHostMu.Lock()
1404+
defer t.connsPerHostMu.Unlock()
1405+
1406+
if err != nil {
1407+
w.cancel(t, err)
1408+
}
1409+
1410+
q, ok := t.connsPerHostWait[w.key]
1411+
if ok {
1412+
q.cleanFront()
1413+
t.connsPerHostWait[w.key] = q
1414+
if q.len() == 0 {
1415+
delete(t.connsPerHostWait, w.key)
1416+
}
1417+
}
1418+
}()
13701419

13711420
// Wait for completion or cancellation.
13721421
select {

src/net/http/transport_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3152,6 +3152,45 @@ func TestIdleConnChannelLeak(t *testing.T) {
31523152
}
31533153
}
31543154

3155+
func TestIdleConnWaitWantConnLeak(t *testing.T) {
3156+
// Not parallel: uses global test hooks.
3157+
var mu sync.Mutex
3158+
var n int
3159+
3160+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
3161+
mu.Lock()
3162+
n++
3163+
mu.Unlock()
3164+
w.Write([]byte("CONTENT"))
3165+
}))
3166+
defer ts.Close()
3167+
3168+
const nReqs = 5
3169+
3170+
c := ts.Client()
3171+
tr := c.Transport.(*Transport)
3172+
tr.Dial = func(netw, addr string) (net.Conn, error) {
3173+
return net.Dial(netw, ts.Listener.Addr().String())
3174+
}
3175+
3176+
// First, without keep-alives.
3177+
for _, disableKeep := range []bool{true, false} {
3178+
tr.DisableKeepAlives = disableKeep
3179+
3180+
for i := 0; i < nReqs; i++ {
3181+
resp, err := c.Get(fmt.Sprintf("http://foo-host-%d.tld/", i))
3182+
if err != nil {
3183+
t.Fatal(err)
3184+
}
3185+
resp.Body.Close()
3186+
}
3187+
3188+
if got := tr.IdleConnWaitMapSizeForTesting(); got != 0 {
3189+
t.Fatalf("for DisableKeepAlives = %v, map size = %d; want 0", disableKeep, got)
3190+
}
3191+
}
3192+
}
3193+
31553194
// Verify the status quo: that the Client.Post function coerces its
31563195
// body into a ReadCloser if it's a Closer, and that the Transport
31573196
// then closes it.

0 commit comments

Comments
 (0)