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

View as plain text