Source file src/net/http/transport_pool.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package http
     6  
     7  import (
     8  	"context"
     9  	"maps"
    10  	"slices"
    11  	"sync"
    12  	"time"
    13  )
    14  
    15  // clientConnPool is the connection pool state for Transport,
    16  // split out into a separate struct for organizational purposes.
    17  //
    18  // Every Transport has a pool.
    19  //
    20  // The pool has a set of entries for targets (things we can dial).
    21  // Entries are removed from the pool when nothing references them any more
    22  // (no conns, no dials, no waiting requests for a conn, etc.)
    23  // An entry manages the set of connections for a target.
    24  //
    25  // The pool also has an LRU of connections, used to maintain the cross-target MaxIdleConn limit.
    26  //
    27  // Pool entries have a channel-based lock which permits get-connection requests
    28  // to wait for available connections and/or dial slots; see clientConnPoolEntry for details.
    29  //
    30  // Lock ordering is (first to last): clientConnPoolEntry >> clientConnPool >> persistConn.
    31  //
    32  // Locking and unlocking entries is a channel operation, so we take some care to minimize
    33  // the number of lock operations required in the hot path.
    34  //
    35  // The pool and entry locks should never be held during blocking operations
    36  // (such as dials or closing a net.Conn).
    37  type clientConnPool struct {
    38  	mu        sync.Mutex
    39  	ents      map[connectMethodKey]*clientConnPoolEntry
    40  	lru       intrusiveList[poolConn]
    41  	closeIdle bool
    42  }
    43  
    44  type poolConn struct {
    45  	cc genericClientConn
    46  
    47  	// Guarded by clientConnPool.mu.
    48  	lruLink intrusiveLink[poolConn]
    49  
    50  	// Guarded by clientConnPoolEntry's lock.
    51  	poolEnt  *clientConnPoolEntry
    52  	isInPool bool      // set when counted in pool entry's numConns
    53  	idleAt   time.Time // time it last became idle
    54  
    55  	putIdleHook func(error)
    56  }
    57  
    58  func (pc *poolConn) isBroken() bool {
    59  	return pc.cc.Err() != nil
    60  }
    61  
    62  type clientConnPoolEntry struct {
    63  	key connectMethodKey
    64  
    65  	// These three channels (each with a 1-element buffer) act as a lock/condition variable.
    66  	//
    67  	//   - if the entry is locked, no channel contains a value; otherwise
    68  	//   - if idleConns contains a usable conn, cangetc contains a value; otherwise
    69  	//   - if a new conn may be dialed, candialc contains a value; otherwise
    70  	//   - lockc contains a value.
    71  	//
    72  	// Lock ordering: Only lock one entry at a time. Lock entries before the pool mutex.
    73  	cangetc  chan struct{}
    74  	candialc chan struct{}
    75  	lockc    chan struct{}
    76  
    77  	// Idle (available for use) connections, most recently used last.
    78  	idleConns []*poolConn
    79  
    80  	// Connection with IsAlt() == true.
    81  	//
    82  	// This is historically a pseudo-"connection" backed by an http2.Transport,
    83  	// which manages its own connection pool. Lack of visibility into this pool
    84  	// causes a variety of bugs, such as connections-per-host tracking not accounting
    85  	// for individual HTTP/2 connections.
    86  	//
    87  	// This may also be an HTTP/3 connection.
    88  	//
    89  	// TODO: Improve management of HTTP/2 and HTTP/3 connections.
    90  	altConn *poolConn
    91  
    92  	// Total connections, idle and otherwise.
    93  	numConns int
    94  
    95  	// Requests waiting for a connection.
    96  	numWaiters int
    97  
    98  	// Dials in progress.
    99  	dials intrusiveList[poolDial]
   100  
   101  	// Idle timer, fires when we should next check to see if conns
   102  	// should be closed for idleness.
   103  	idleTimer    *time.Timer
   104  	idleTimerSet bool
   105  
   106  	// Set when the entry is removed from its pool.
   107  	removed bool
   108  }
   109  
   110  type poolDial struct {
   111  	ent     *clientConnPoolEntry
   112  	cm      connectMethod
   113  	cancel  context.CancelFunc // call to cancel the dial
   114  	resultc chan pconnOrError  // sends the dial result to the requester
   115  	donec   <-chan struct{}    // closed when the requester no longer needs the dial result
   116  	link    intrusiveLink[poolDial]
   117  }
   118  
   119  // lock unconditionally locks the entry.
   120  func (ent *clientConnPoolEntry) lock() {
   121  	select {
   122  	case <-ent.lockc:
   123  	case <-ent.cangetc:
   124  	case <-ent.candialc:
   125  	}
   126  }
   127  
   128  // unlock unlocks the entry.
   129  //
   130  // If the entry is no longer in use, unlock removes it from the pool.
   131  // The pool mutex must not be held when calling unlock.
   132  func (ent *clientConnPoolEntry) unlock(tr *Transport) {
   133  	if !ent.removed && ent.numConns == 0 && ent.numWaiters == 0 && ent.dials.size == 0 && len(ent.idleConns) == 0 {
   134  		// This entry is no longer in use; remove it from the pool.
   135  		ent.removed = true
   136  		if ent.idleTimer != nil {
   137  			ent.idleTimer.Stop()
   138  		}
   139  		tr.pool.mu.Lock()
   140  		delete(tr.pool.ents, ent.key)
   141  		tr.pool.mu.Unlock()
   142  	}
   143  	switch {
   144  	case len(ent.idleConns) > 0 || ent.altConn != nil:
   145  		ent.cangetc <- struct{}{}
   146  	case tr.MaxConnsPerHost <= 0 || ent.numConns+ent.dials.size < tr.MaxConnsPerHost:
   147  		ent.candialc <- struct{}{}
   148  	default:
   149  		ent.lockc <- struct{}{}
   150  	}
   151  }
   152  
   153  // registerConnLocked adds pconn to the entry's bookkeeping.
   154  func (ent *clientConnPoolEntry) registerConnLocked(pconn *poolConn) {
   155  	if pconn.isBroken() || pconn.isInPool {
   156  		return
   157  	}
   158  	pconn.isInPool = true
   159  	ent.numConns++
   160  }
   161  
   162  // unregisterConnLocked disassociates pconn from the entry.
   163  func (ent *clientConnPoolEntry) unregisterConnLocked(pconn *poolConn) {
   164  	if !pconn.isInPool {
   165  		// This can happen if a newly-created connection breaks before we finish
   166  		// registering it with the pool.
   167  		return
   168  	}
   169  	pconn.isInPool = false
   170  	ent.numConns--
   171  }
   172  
   173  // deleteIdleLocked deletes every element in idleConns that matches f.
   174  func (ent *clientConnPoolEntry) deleteIdleLocked(t *Transport, f func(*poolConn) bool) {
   175  	ent.idleConns = slices.DeleteFunc(ent.idleConns, func(pconn *poolConn) bool {
   176  		delete := f(pconn)
   177  		if delete {
   178  			ent.unregisterConnLocked(pconn)
   179  			t.removeFromIdleLRU(pconn)
   180  		}
   181  		return delete
   182  	})
   183  }
   184  
   185  // getLockedPoolEntry returns the pool entry for key, creating it if necessary.
   186  // The entry will be locked.
   187  //
   188  // If clearCloseIdle is set, pool.closeIdle is set to false.
   189  // (This isn't especially principled from an abstraction perspective,
   190  // but it lets us get the entry and update closeIdle under one mutex acquisition.)
   191  func (t *Transport) getLockedPoolEntry(key connectMethodKey, clearCloseIdle bool) *clientConnPoolEntry {
   192  	getEnt := func() (*clientConnPoolEntry, bool) {
   193  		t.pool.mu.Lock()
   194  		defer t.pool.mu.Unlock()
   195  		if clearCloseIdle {
   196  			t.pool.closeIdle = false
   197  		}
   198  		if t.pool.ents == nil {
   199  			t.pool.ents = map[connectMethodKey]*clientConnPoolEntry{}
   200  			t.pool.lru.link = func(pconn *poolConn) *intrusiveLink[poolConn] {
   201  				return &pconn.lruLink
   202  			}
   203  		}
   204  		if ent, ok := t.pool.ents[key]; ok {
   205  			return ent, true
   206  		}
   207  		ent := &clientConnPoolEntry{
   208  			key:      key,
   209  			lockc:    make(chan struct{}, 1),
   210  			cangetc:  make(chan struct{}, 1),
   211  			candialc: make(chan struct{}, 1),
   212  		}
   213  		ent.dials.link = func(dial *poolDial) *intrusiveLink[poolDial] { return &dial.link }
   214  		t.pool.ents[key] = ent
   215  		return ent, false
   216  	}
   217  	for {
   218  		ent, needLock := getEnt()
   219  		if needLock {
   220  			ent.lock()
   221  		}
   222  		if !ent.removed {
   223  			return ent
   224  		}
   225  		// This entry was removed from the pool in the time between the lookup
   226  		// and us locking it. Try again.
   227  		ent.unlock(t)
   228  	}
   229  }
   230  
   231  // poolCopyEntries returns the current set of pool entries as a slice.
   232  func (t *Transport) poolCopyEntries() []*clientConnPoolEntry {
   233  	t.pool.mu.Lock()
   234  	defer t.pool.mu.Unlock()
   235  	return slices.Collect(maps.Values(t.pool.ents))
   236  }
   237  
   238  // poolCloseIdleConnections closes idle connections in the pool.
   239  func (t *Transport) poolCloseIdleConnections() {
   240  	t.pool.mu.Lock()
   241  	t.pool.closeIdle = true
   242  	t.pool.mu.Unlock()
   243  
   244  	for _, ent := range t.poolCopyEntries() {
   245  		var toClose []*poolConn
   246  		func() {
   247  			ent.lock()
   248  			defer ent.unlock(t)
   249  			if ent.altConn != nil && ent.numWaiters == 0 {
   250  				ent.unregisterConnLocked(ent.altConn)
   251  				toClose = append(toClose, ent.altConn)
   252  				ent.altConn = nil
   253  			}
   254  			ent.deleteIdleLocked(t, func(pconn *poolConn) bool {
   255  				toClose = append(toClose, pconn)
   256  				return true
   257  			})
   258  
   259  			// Cancel dials in progress where the initiating request
   260  			// is no longer waiting.
   261  			for dial := range ent.dials.all() {
   262  				select {
   263  				case <-dial.donec:
   264  					dial.cancel()
   265  				default:
   266  				}
   267  			}
   268  		}()
   269  		for _, pconn := range toClose {
   270  			pconn.cc.CloseWithError(errCloseIdleConns)
   271  		}
   272  	}
   273  }
   274  
   275  // poolConnStateHook is called when the state of a pooled connection changes.
   276  func (t *Transport) poolConnStateHook(pconn *poolConn) {
   277  	var (
   278  		closeErr    error
   279  		needExpire  bool
   280  		putIdleHook func(error)
   281  	)
   282  	func() {
   283  		ent := pconn.poolEnt
   284  		ent.lock()
   285  		defer ent.unlock(t)
   286  		if pconn.cc == nil || pconn.cc.IsAlt() {
   287  			return
   288  		}
   289  		if pconn.isBroken() {
   290  			t.poolRemoveConnLocked(ent, pconn)
   291  			return
   292  		}
   293  		if pconn.cc.Available() == 0 {
   294  			return
   295  		}
   296  		putIdleHook = pconn.putIdleHook
   297  		pconn.putIdleHook = nil
   298  		closeErr, needExpire = t.poolPutIdleConnLocked(ent, pconn)
   299  		if closeErr != nil {
   300  			t.poolRemoveConnLocked(ent, pconn)
   301  		}
   302  	}()
   303  	if closeErr != nil {
   304  		pconn.cc.CloseWithError(closeErr)
   305  	}
   306  	if putIdleHook != nil && closeErr != errKeepAlivesDisabled {
   307  		putIdleHook(closeErr)
   308  	}
   309  	if needExpire {
   310  		t.expireIdleLRU()
   311  	}
   312  }
   313  
   314  // poolPutIdleConnLocked places pconn in its entry's idle list.
   315  // The entry must be locked.
   316  //
   317  // It returns an error if pconn should be closed.
   318  //
   319  // It returns needExpire=true if the caller should call t.expireIdleLRU
   320  // after dropping the entry lock. (We check inside poolPutIdleConnLocked to avoid
   321  // a needless pool mutex acquisition in the common case when expiry is not needed.)
   322  func (t *Transport) poolPutIdleConnLocked(ent *clientConnPoolEntry, pconn *poolConn) (_ error, needExpire bool) {
   323  	t.pool.mu.Lock()
   324  	defer t.pool.mu.Unlock()
   325  
   326  	if pconn.isBroken() || !pconn.isInPool {
   327  		return errConnBroken, false
   328  	}
   329  	if t.pool.lru.contains(pconn) {
   330  		// This conn is already idle.
   331  		// Move it to the back of the idle list.
   332  		t.pool.lru.remove(pconn)
   333  	} else {
   334  		// This conn is entering the idle list.
   335  		if t.keepAlivesDisabled() {
   336  			return errKeepAlivesDisabled, false
   337  		}
   338  		if len(ent.idleConns) >= t.maxIdleConnsPerHost() {
   339  			return errTooManyIdleHost, false
   340  		}
   341  		if pconn.cc.IsAlt() && ent.altConn != nil {
   342  			// We already have a reusable, shared connection;
   343  			// discard this one.
   344  			return errTooManyIdleHost, false
   345  		}
   346  		if t.pool.closeIdle {
   347  			return errCloseIdle, false
   348  		}
   349  		if !pconn.cc.IsAlt() {
   350  			ent.idleConns = append(ent.idleConns, pconn)
   351  		} else {
   352  			ent.altConn = pconn
   353  		}
   354  	}
   355  
   356  	if !pconn.cc.IsAlt() {
   357  		t.pool.lru.pushBack(pconn)
   358  	}
   359  
   360  	// Arm the idle timer (if needed).
   361  	pconn.idleAt = time.Now()
   362  	if t.IdleConnTimeout > 0 && !ent.idleTimerSet && !pconn.cc.IsAlt() {
   363  		if ent.idleTimer != nil {
   364  			ent.idleTimer.Reset(t.IdleConnTimeout)
   365  		} else {
   366  			ent.idleTimer = time.AfterFunc(t.IdleConnTimeout, func() {
   367  				t.poolIdleTimeout(ent)
   368  			})
   369  		}
   370  		ent.idleTimerSet = true
   371  	}
   372  	needExpire = t.MaxIdleConns != 0 && t.pool.lru.size > t.MaxIdleConns
   373  	return nil, needExpire
   374  }
   375  
   376  func (t *Transport) poolIdleTimeout(ent *clientConnPoolEntry) {
   377  	ent.lock()
   378  	defer ent.unlock(t)
   379  	ent.idleTimerSet = false
   380  	timeout := t.IdleConnTimeout
   381  	if timeout <= 0 {
   382  		return
   383  	}
   384  	now := time.Now()
   385  	var next time.Time
   386  	ent.deleteIdleLocked(t, func(pconn *poolConn) bool {
   387  		if expiry := pconn.idleAt.Add(timeout); !expiry.After(now) {
   388  			go pconn.cc.CloseWithError(errIdleConnTimeout)
   389  			return true
   390  		} else if next.IsZero() || expiry.Before(next) {
   391  			next = expiry
   392  		}
   393  		return false
   394  	})
   395  	if !next.IsZero() {
   396  		ent.idleTimer.Reset(next.Sub(now))
   397  		ent.idleTimerSet = true
   398  	}
   399  }
   400  
   401  // poolRemoveConn entirely removes pconn from the pool.
   402  func (t *Transport) poolRemoveConn(pconn *poolConn) {
   403  	if pconn == nil || pconn.poolEnt == nil {
   404  		return
   405  	}
   406  	ent := pconn.poolEnt
   407  	ent.lock()
   408  	defer ent.unlock(t)
   409  	t.poolRemoveConnLocked(ent, pconn)
   410  }
   411  
   412  func (t *Transport) poolRemoveConnLocked(ent *clientConnPoolEntry, pconn *poolConn) {
   413  	if ent.altConn == pconn {
   414  		ent.altConn = nil
   415  	}
   416  	if idx := slices.Index(ent.idleConns, pconn); idx >= 0 {
   417  		ent.idleConns = slices.Delete(ent.idleConns, idx, idx+1)
   418  		t.removeFromIdleLRU(pconn)
   419  	}
   420  	ent.unregisterConnLocked(pconn)
   421  }
   422  
   423  type pconnOrError struct {
   424  	pconn *poolConn
   425  	err   error
   426  }
   427  
   428  // poolGetConn gets a usable connection from the pool,
   429  // dialing a new one if necessary.
   430  func (t *Transport) poolGetConn(ctx context.Context, cm connectMethod) (*poolConn, error) {
   431  	getConnCancelCause := func(ctx context.Context) error {
   432  		err := context.Cause(ctx)
   433  		if err == errRequestCanceled {
   434  			err = errRequestCanceledConn
   435  		}
   436  		return err
   437  	}
   438  
   439  	// If the request context is already canceled, don't go looking for a conn.
   440  	if err := ctx.Err(); err != nil {
   441  		return nil, getConnCancelCause(ctx)
   442  	}
   443  
   444  	// Fast path: Check to see if there's a usable conn.
   445  	const clearCloseIdle = true
   446  	ent, pconn := func() (*clientConnPoolEntry, *poolConn) {
   447  		ent := t.getLockedPoolEntry(cm.key(), clearCloseIdle)
   448  		defer ent.unlock(t)
   449  		pconn := t.poolTryGetForEntryLocked(ent)
   450  		if pconn == nil {
   451  			// Increment the waiters count to keep this pool entry alive
   452  			// while we're using it.
   453  			ent.numWaiters++
   454  		}
   455  		return ent, pconn
   456  	}()
   457  	if pconn != nil {
   458  		return pconn, nil
   459  	}
   460  
   461  	// Remove ourselves from the waiters before returning.
   462  	// Running this in a defer ensures it happens after any later defers.
   463  	// In particular, we must close getdonec before trying to lock the entry
   464  	// to decrement numWaiters, to avoid a deadlock race when the request context
   465  	// expires at the same time as a dial finishes.
   466  	waiting := true
   467  	defer func() {
   468  		if !waiting {
   469  			return
   470  		}
   471  		ent.lock()
   472  		defer ent.unlock(t)
   473  		ent.numWaiters--
   474  	}()
   475  
   476  	var (
   477  		candialc    = ent.candialc
   478  		dialresultc chan pconnOrError
   479  		getdonec    chan struct{}
   480  	)
   481  	for {
   482  
   483  		select {
   484  		case <-ent.cangetc:
   485  			// Reading from cangetc locks the entry, and indicates that there is
   486  			// an available connection in the idle list.
   487  			pconn := func() *poolConn {
   488  				defer ent.unlock(t)
   489  				pconn := t.poolTryGetForEntryLocked(ent)
   490  				if pconn != nil {
   491  					ent.numWaiters--
   492  					waiting = false
   493  				}
   494  				return pconn
   495  			}()
   496  			if pconn != nil {
   497  				return pconn, nil
   498  			}
   499  			// We didn't find a usable connection. Most likely we lost a race against
   500  			// a closing connection (the conn is now dead, but it hadn't finished
   501  			// removing itself from the pool). Go back and try again.
   502  		case <-candialc:
   503  			// Reading from candialc locks the entry, and indicates that we may
   504  			// start a new dial.
   505  			func() {
   506  				defer ent.unlock(t)
   507  				// Create a context for the dial which inherits the
   508  				// request context's values, but not its cancellation
   509  				// (since the dial may outlive the request).
   510  				//
   511  				// Note that we only cancel this context to cancel the dial.
   512  				// If the dial finishes, we leave it uncanceled since cancelling
   513  				// it can (for some user-provided DialContext functions)
   514  				// close the connection.
   515  				dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
   516  				dialresultc = make(chan pconnOrError)
   517  				getdonec = make(chan struct{})
   518  				dial := &poolDial{
   519  					ent:     ent,
   520  					cm:      cm,
   521  					cancel:  cancel,
   522  					resultc: dialresultc,
   523  					donec:   getdonec,
   524  				}
   525  				ent.dials.pushBack(dial)
   526  				go t.poolDialForEntry(dialCtx, dial)
   527  				candialc = nil // don't dial again
   528  			}()
   529  			defer close(getdonec)
   530  		case res := <-dialresultc:
   531  			// A dial we started has completed and provided us with
   532  			// either a connection, or a dial failure.
   533  			//
   534  			// poolDialForEntry decrements numWaiters in this case
   535  			// (avoiding a redundant lock on the entry).
   536  			waiting = false
   537  			return res.pconn, res.err
   538  		case <-ctx.Done():
   539  			// The request has timed out.
   540  			return nil, getConnCancelCause(ctx)
   541  		}
   542  	}
   543  }
   544  
   545  // poolTryGetForEntryLocked attempts to fetch an idle connection from ent.
   546  // ent must be locked.
   547  // It returns nil if no usable connection was found.
   548  func (t *Transport) poolTryGetForEntryLocked(ent *clientConnPoolEntry) *poolConn {
   549  	if ent.altConn != nil {
   550  		// If we've got an HTTP/2 or HTTP/3 connection, use it.
   551  		return ent.altConn
   552  	}
   553  
   554  	// If IdleConnTimeout is set, calculate the oldest
   555  	// poolConn.idleAt time we're willing to use a cached idle
   556  	// conn.
   557  	var oldTime time.Time
   558  	if t.IdleConnTimeout > 0 {
   559  		oldTime = time.Now().Add(-t.IdleConnTimeout)
   560  	}
   561  
   562  	for len(ent.idleConns) > 0 {
   563  		pconn := ent.idleConns[len(ent.idleConns)-1]
   564  
   565  		// See whether this connection has been idle too long, considering
   566  		// only the wall time (the Round(0)), in case this is a laptop or VM
   567  		// coming out of suspend with previously cached idle connections.
   568  		tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
   569  		if tooOld {
   570  			// Async cleanup. Launch in its own goroutine (as if a
   571  			// time.AfterFunc called it).
   572  			go pconn.cc.CloseWithError(errIdleConnTimeout)
   573  		}
   574  		if pconn.isBroken() || tooOld {
   575  			// If either persistConn.readLoop has marked the connection
   576  			// broken, but poolConnStateHook has not yet removed it
   577  			// from the idle list, or if this persistConn is too old (it was
   578  			// idle too long), then ignore it and look for another. In both
   579  			// cases it's already in the process of being closed.
   580  			idx := len(ent.idleConns) - 1
   581  			ent.idleConns = slices.Delete(ent.idleConns, idx, idx+1)
   582  			ent.unregisterConnLocked(pconn)
   583  			t.removeFromIdleLRU(pconn)
   584  			continue
   585  		}
   586  		// HTTP/1: only one client can use pconn.
   587  		// Remove it from the list.
   588  		idx := len(ent.idleConns) - 1
   589  		ent.idleConns = slices.Delete(ent.idleConns, idx, idx+1)
   590  		t.removeFromIdleLRU(pconn)
   591  		return pconn
   592  	}
   593  	return nil
   594  }
   595  
   596  // poolDialForEntry dials a new connection.
   597  //
   598  // Note that dials always use a context derived from a request context,
   599  // although a request may use a connection that was dialed with a different request's context.
   600  func (t *Transport) poolDialForEntry(ctx context.Context, dial *poolDial) {
   601  	ent := dial.ent
   602  	pconn := &poolConn{
   603  		poolEnt: ent,
   604  	}
   605  	cc, err := t.dialConn(ctx, dial.cm, false, func() { t.poolConnStateHook(pconn) })
   606  	if err != nil {
   607  		pconn = nil
   608  	}
   609  	needClose, needExpire := func() (needClose, needExpire bool) {
   610  		ent.lock()
   611  		defer ent.unlock(t)
   612  
   613  		// The dial is complete: Remove the dial from the entry and register
   614  		// the new connection (if any).
   615  		ent.dials.remove(dial)
   616  		if err == nil {
   617  			pconn.cc = cc
   618  			ent.registerConnLocked(pconn)
   619  		}
   620  
   621  		// HTTP/2 and HTTP/3 connections can be shared.
   622  		// Add to the idle connection pool before trying to deliver to w.
   623  		deliver := pconnOrError{pconn: pconn, err: err}
   624  		if err == nil && pconn.cc.IsAlt() {
   625  			err, needExpire = t.poolPutIdleConnLocked(ent, pconn)
   626  			if err != nil {
   627  				// This connection should not go into the idle list.
   628  				//
   629  				// Note: Setting needClose replicates some odd behaviors
   630  				// from the earlier pool implementation. If this is an HTTP/2
   631  				// connection, calling pconn.close does not actually close
   632  				// anything: The "connection" is a reference to the HTTP/2
   633  				// transport, which manages its own connection pool.
   634  				// If this is an HTTP/3 connection, pconn.close does close
   635  				// the connection and may immediately break it while a RoundTrip
   636  				// is in progress.
   637  				//
   638  				// TODO: Fix the above behaviors.
   639  				ent.unregisterConnLocked(pconn)
   640  				needClose = true
   641  				if ent.altConn != nil {
   642  					// We already have a sharable connection in the pool.
   643  					// Keep the existing one, and discard this new one.
   644  					//
   645  					// Try to deliver the existing conn to poolGetConn.
   646  					// We must always deliver a result to poolGetConn
   647  					// to avoid it potentially hanging forever waiting
   648  					// for a pooled conn. (In this branch, we observe that
   649  					// there is currently a pooled conn, but it could become
   650  					// unusable between now and poolGetConn trying to
   651  					// retrieve it.)
   652  					deliver = pconnOrError{pconn: ent.altConn}
   653  				}
   654  			}
   655  		}
   656  
   657  		// Try to deliver the result back to poolGetConn.
   658  		//
   659  		// Note that we're doing this with the entry still locked, so poolGetConn
   660  		// needs to take care not to block on locking the entry until after it
   661  		// closes donec. poolGetConn is free to lock the entry in a non-blocking fashion.
   662  		delivered := false
   663  		select {
   664  		case dial.resultc <- deliver:
   665  			ent.numWaiters--
   666  			delivered = true
   667  		case <-dial.donec:
   668  		}
   669  		if err != nil || delivered || needClose || pconn.cc.IsAlt() {
   670  			// Either an error, or an unsharable HTTP/1 connection which was delivered
   671  			// to the waiter, or a shared HTTP/2 or /3 connection already in the pool.
   672  			return needClose, false
   673  		}
   674  
   675  		err, needExpire = t.poolPutIdleConnLocked(ent, pconn)
   676  		if err != nil {
   677  			// This connection should not go into the idle list.
   678  			ent.unregisterConnLocked(pconn)
   679  			return true, false
   680  		}
   681  		return false, needExpire
   682  	}()
   683  	if needClose {
   684  		pconn.cc.CloseWithError(err)
   685  	}
   686  	if needExpire {
   687  		t.expireIdleLRU()
   688  	}
   689  }
   690  
   691  // removeFromIdleLRU removes pconn from the idle-conns LRU.
   692  func (t *Transport) removeFromIdleLRU(pconn *poolConn) {
   693  	t.pool.mu.Lock()
   694  	defer t.pool.mu.Unlock()
   695  	t.pool.lru.remove(pconn)
   696  }
   697  
   698  // expireIdleLRU removes idle conns from the pool until the number of idle conns
   699  // is less than or equal to MaxIdleConns.
   700  func (t *Transport) expireIdleLRU() {
   701  	t.pool.mu.Lock()
   702  	defer t.pool.mu.Unlock()
   703  	if t.MaxIdleConns == 0 {
   704  		return
   705  	}
   706  	maxIdle := max(t.MaxIdleConns, 0)
   707  	// expireEntry expires conns from the head of the LRU which belong to ent.
   708  	// It locks the entry and pool.
   709  	expireEntry := func(ent *clientConnPoolEntry) {
   710  		ent.lock()
   711  		defer ent.unlock(t)
   712  		t.pool.mu.Lock()
   713  		defer t.pool.mu.Unlock()
   714  		// Expire conns for the entry we currently have locked.
   715  		// It's possible the head of the LRU changed while we were juggling locks;
   716  		// that's okay. Worst case, we loop back and try again with the new head.
   717  		for t.pool.lru.size > maxIdle {
   718  			pconn := t.pool.lru.head
   719  			if pconn.poolEnt != ent {
   720  				break
   721  			}
   722  			if idx := slices.Index(ent.idleConns, pconn); idx >= 0 {
   723  				ent.idleConns = slices.Delete(ent.idleConns, idx, idx+1)
   724  				ent.unregisterConnLocked(pconn)
   725  				go pconn.cc.CloseWithError(errTooManyIdle)
   726  			}
   727  			t.pool.lru.remove(pconn)
   728  		}
   729  	}
   730  	for t.pool.lru.size > maxIdle {
   731  		func() {
   732  			// Lock ordering is ent >> pool, so we need to drop the pool mutex
   733  			// before locking the entry.
   734  			ent := t.pool.lru.head.poolEnt
   735  			t.pool.mu.Unlock()
   736  			defer t.pool.mu.Lock()
   737  			expireEntry(ent)
   738  		}()
   739  	}
   740  }
   741  

View as plain text