trpc.group/trpc-go/trpc-go@v1.0.3/pool/connpool/connection_pool.go (about)

     1  //
     2  //
     3  // Tencent is pleased to support the open source community by making tRPC available.
     4  //
     5  // Copyright (C) 2023 THL A29 Limited, a Tencent company.
     6  // All rights reserved.
     7  //
     8  // If you have downloaded a copy of the tRPC source code from Tencent,
     9  // please note that tRPC source code is licensed under the  Apache 2.0 License,
    10  // A copy of the Apache 2.0 License is included in this file.
    11  //
    12  //
    13  
    14  package connpool
    15  
    16  import (
    17  	"context"
    18  	"errors"
    19  	"io"
    20  	"net"
    21  	"strings"
    22  	"sync"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	"trpc.group/trpc-go/trpc-go/codec"
    27  	"trpc.group/trpc-go/trpc-go/internal/report"
    28  	"trpc.group/trpc-go/trpc-go/log"
    29  )
    30  
    31  const (
    32  	defaultDialTimeout     = 200 * time.Millisecond
    33  	defaultIdleTimeout     = 50 * time.Second
    34  	defaultMaxIdle         = 65536
    35  	defaultCheckInterval   = 3 * time.Second
    36  	defaultPoolIdleTimeout = 2 * defaultIdleTimeout
    37  )
    38  
    39  var globalBuffer []byte = make([]byte, 1)
    40  
    41  // DefaultConnectionPool is the default connection pool, replaceable.
    42  var DefaultConnectionPool = NewConnectionPool()
    43  
    44  // connection pool error message.
    45  var (
    46  	ErrPoolLimit  = errors.New("connection pool limit")  // ErrPoolLimit number of connections exceeds the limit error.
    47  	ErrPoolClosed = errors.New("connection pool closed") // ErrPoolClosed connection pool closed error.
    48  	ErrConnClosed = errors.New("conn closed")            // ErrConnClosed connection closed.
    49  	ErrNoDeadline = errors.New("dial no deadline")       // ErrNoDeadline has no deadline set.
    50  	ErrConnInPool = errors.New("conn already in pool")   // ErrNoDeadline has no deadline set.
    51  )
    52  
    53  // HealthChecker idle connection health check function.
    54  // The function supports quick check and comprehensive check.
    55  // Quick check is called when an idle connection is obtained,
    56  // and only checks whether the connection status is abnormal.
    57  // The function returns true to indicate that the connection is available normally.
    58  type HealthChecker func(pc *PoolConn, isFast bool) bool
    59  
    60  // NewConnectionPool creates a connection pool.
    61  func NewConnectionPool(opt ...Option) Pool {
    62  	// Default value, tentative, need to debug to determine the specific value.
    63  	opts := &Options{
    64  		MaxIdle:         defaultMaxIdle,
    65  		IdleTimeout:     defaultIdleTimeout,
    66  		DialTimeout:     defaultDialTimeout,
    67  		PoolIdleTimeout: defaultPoolIdleTimeout,
    68  		Dial:            Dial,
    69  	}
    70  	for _, o := range opt {
    71  		o(opts)
    72  	}
    73  	return &pool{
    74  		opts:            opts,
    75  		connectionPools: new(sync.Map),
    76  	}
    77  }
    78  
    79  // pool connection pool factory, maintains connection pools corresponding to all addresses,
    80  // and connection pool option information.
    81  type pool struct {
    82  	opts            *Options
    83  	connectionPools *sync.Map
    84  }
    85  
    86  type dialFunc = func(ctx context.Context) (net.Conn, error)
    87  
    88  func (p *pool) getDialFunc(network string, address string, opts GetOptions) dialFunc {
    89  	dialOpts := &DialOptions{
    90  		Network:       network,
    91  		Address:       address,
    92  		LocalAddr:     opts.LocalAddr,
    93  		CACertFile:    opts.CACertFile,
    94  		TLSCertFile:   opts.TLSCertFile,
    95  		TLSKeyFile:    opts.TLSKeyFile,
    96  		TLSServerName: opts.TLSServerName,
    97  		IdleTimeout:   p.opts.IdleTimeout,
    98  	}
    99  
   100  	return func(ctx context.Context) (net.Conn, error) {
   101  		select {
   102  		case <-ctx.Done():
   103  			return nil, ctx.Err()
   104  		default:
   105  		}
   106  		d, ok := ctx.Deadline()
   107  		if !ok {
   108  			return nil, ErrNoDeadline
   109  		}
   110  
   111  		opts := *dialOpts
   112  		opts.Timeout = time.Until(d)
   113  		return p.opts.Dial(&opts)
   114  	}
   115  }
   116  
   117  // Get is used to get the connection from the connection pool.
   118  func (p *pool) Get(network string, address string, opts GetOptions) (net.Conn, error) {
   119  	ctx, cancel := opts.getDialCtx(p.opts.DialTimeout)
   120  	if cancel != nil {
   121  		defer cancel()
   122  	}
   123  	key := getNodeKey(network, address, opts.Protocol)
   124  	if v, ok := p.connectionPools.Load(key); ok {
   125  		return v.(*ConnectionPool).Get(ctx)
   126  	}
   127  
   128  	newPool := &ConnectionPool{
   129  		Dial:               p.getDialFunc(network, address, opts),
   130  		MinIdle:            p.opts.MinIdle,
   131  		MaxIdle:            p.opts.MaxIdle,
   132  		MaxActive:          p.opts.MaxActive,
   133  		Wait:               p.opts.Wait,
   134  		MaxConnLifetime:    p.opts.MaxConnLifetime,
   135  		IdleTimeout:        p.opts.IdleTimeout,
   136  		framerBuilder:      opts.FramerBuilder,
   137  		customReader:       opts.CustomReader,
   138  		forceClosed:        p.opts.ForceClose,
   139  		PushIdleConnToTail: p.opts.PushIdleConnToTail,
   140  		onCloseFunc:        func() { p.connectionPools.Delete(key) },
   141  		poolIdleTimeout:    p.opts.PoolIdleTimeout,
   142  	}
   143  
   144  	if newPool.MaxActive > 0 {
   145  		newPool.token = make(chan struct{}, p.opts.MaxActive)
   146  	}
   147  
   148  	newPool.checker = newPool.defaultChecker
   149  	if p.opts.Checker != nil {
   150  		newPool.checker = p.opts.Checker
   151  	}
   152  
   153  	// Avoid the problem of writing concurrently to the pool map during initialization.
   154  	v, ok := p.connectionPools.LoadOrStore(key, newPool)
   155  	if !ok {
   156  		newPool.RegisterChecker(defaultCheckInterval, newPool.checker)
   157  		newPool.keepMinIdles()
   158  		return newPool.Get(ctx)
   159  	}
   160  	return v.(*ConnectionPool).Get(ctx)
   161  }
   162  
   163  // ConnectionPool is the connection pool.
   164  type ConnectionPool struct {
   165  	Dial        func(context.Context) (net.Conn, error) // initialize the connection.
   166  	MinIdle     int                                     // Minimum number of idle connections.
   167  	MaxIdle     int                                     // Maximum number of idle connections, 0 means no limit.
   168  	MaxActive   int                                     // Maximum number of active connections, 0 means no limit.
   169  	IdleTimeout time.Duration                           // idle connection timeout.
   170  	// Whether to wait when the maximum number of active connections is reached.
   171  	Wait               bool
   172  	MaxConnLifetime    time.Duration // Maximum lifetime of the connection.
   173  	mu                 sync.Mutex    // Control concurrent locks.
   174  	checker            HealthChecker // Idle connection health check function.
   175  	closed             bool          // Whether the connection pool has been closed.
   176  	token              chan struct{} // control concurrency by applying token.
   177  	idleSize           int           // idle connections size.
   178  	idle               connList      // idle connection list.
   179  	framerBuilder      codec.FramerBuilder
   180  	forceClosed        bool // Force close the connection, suitable for streaming scenarios.
   181  	PushIdleConnToTail bool // connection to ip will be push tail when ConnectionPool.put method is called.
   182  	// customReader creates a reader encapsulating the underlying connection.
   183  	customReader    func(io.Reader) io.Reader
   184  	onCloseFunc     func()        // execute when checker goroutine judge the connection_pool is useless.
   185  	used            int32         // size of connections used by user, atomic.
   186  	lastGetTime     int64         // last get connection millisecond timestamp, atomic.
   187  	poolIdleTimeout time.Duration // pool idle timeout.
   188  }
   189  
   190  func (p *ConnectionPool) keepMinIdles() {
   191  	p.mu.Lock()
   192  	count := p.MinIdle - p.idleSize
   193  	if count > 0 {
   194  		p.idleSize += count
   195  	}
   196  	p.mu.Unlock()
   197  
   198  	for i := 0; i < count; i++ {
   199  		go func() {
   200  			ctx, cancel := context.WithTimeout(context.Background(), defaultDialTimeout)
   201  			defer cancel()
   202  			if err := p.addIdleConn(ctx); err != nil {
   203  				p.mu.Lock()
   204  				p.idleSize--
   205  				p.mu.Unlock()
   206  			}
   207  		}()
   208  	}
   209  }
   210  
   211  func (p *ConnectionPool) addIdleConn(ctx context.Context) error {
   212  	p.mu.Lock()
   213  	if p.closed {
   214  		p.mu.Unlock()
   215  		return ErrPoolClosed
   216  	}
   217  	p.mu.Unlock()
   218  
   219  	c, err := p.dial(ctx)
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	// put in idle list
   225  	pc := p.newPoolConn(c)
   226  	p.mu.Lock()
   227  	if p.closed {
   228  		pc.closed = true
   229  		pc.Conn.Close()
   230  	} else {
   231  		pc.t = time.Now()
   232  		if !p.PushIdleConnToTail {
   233  			p.idle.pushHead(pc)
   234  		} else {
   235  			p.idle.pushTail(pc)
   236  		}
   237  	}
   238  	p.mu.Unlock()
   239  	return nil
   240  }
   241  
   242  // Get gets the connection from the connection pool.
   243  func (p *ConnectionPool) Get(ctx context.Context) (*PoolConn, error) {
   244  	var (
   245  		pc  *PoolConn
   246  		err error
   247  	)
   248  	if pc, err = p.get(ctx); err != nil {
   249  		report.ConnectionPoolGetConnectionErr.Incr()
   250  		return nil, err
   251  	}
   252  	return pc, nil
   253  }
   254  
   255  // Close releases the connection.
   256  func (p *ConnectionPool) Close() error {
   257  	p.mu.Lock()
   258  	if p.closed {
   259  		p.mu.Unlock()
   260  		return nil
   261  	}
   262  	p.closed = true
   263  	p.idle.count = 0
   264  	p.idleSize = 0
   265  	pc := p.idle.head
   266  	p.idle.head, p.idle.tail = nil, nil
   267  	p.mu.Unlock()
   268  	for ; pc != nil; pc = pc.next {
   269  		pc.Conn.Close()
   270  		pc.closed = true
   271  	}
   272  	return nil
   273  }
   274  
   275  // get gets the connection from the connection pool.
   276  func (p *ConnectionPool) get(ctx context.Context) (*PoolConn, error) {
   277  	if err := p.getToken(ctx); err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	atomic.StoreInt64(&p.lastGetTime, time.Now().UnixMilli())
   282  	atomic.AddInt32(&p.used, 1)
   283  
   284  	// try to get an idle connection.
   285  	if pc := p.getIdleConn(); pc != nil {
   286  		return pc, nil
   287  	}
   288  
   289  	// get new connection.
   290  	pc, err := p.getNewConn(ctx)
   291  	if err != nil {
   292  		p.freeToken()
   293  		return nil, err
   294  	}
   295  	return pc, nil
   296  }
   297  
   298  // if p.Wait is True, return err when timeout.
   299  // if p.Wait is False, return err when token empty immediately.
   300  func (p *ConnectionPool) getToken(ctx context.Context) error {
   301  	if p.MaxActive <= 0 {
   302  		return nil
   303  	}
   304  
   305  	if p.Wait {
   306  		select {
   307  		case p.token <- struct{}{}:
   308  			return nil
   309  		case <-ctx.Done():
   310  			return ctx.Err()
   311  		}
   312  	} else {
   313  		select {
   314  		case p.token <- struct{}{}:
   315  			return nil
   316  		default:
   317  			return ErrPoolLimit
   318  		}
   319  	}
   320  }
   321  
   322  func (p *ConnectionPool) freeToken() {
   323  	if p.MaxActive <= 0 {
   324  		return
   325  	}
   326  	<-p.token
   327  }
   328  
   329  func (p *ConnectionPool) getIdleConn() *PoolConn {
   330  	p.mu.Lock()
   331  	for p.idle.head != nil {
   332  		pc := p.idle.head
   333  		p.idle.popHead()
   334  		p.idleSize--
   335  		p.mu.Unlock()
   336  		if p.checker(pc, true) {
   337  			return pc
   338  		}
   339  		pc.Conn.Close()
   340  		pc.closed = true
   341  		p.mu.Lock()
   342  	}
   343  	p.mu.Unlock()
   344  	return nil
   345  }
   346  
   347  func (p *ConnectionPool) getNewConn(ctx context.Context) (*PoolConn, error) {
   348  	// If the connection pool has been closed, return an error directly.
   349  	p.mu.Lock()
   350  	if p.closed {
   351  		p.mu.Unlock()
   352  		return nil, ErrPoolClosed
   353  	}
   354  	p.mu.Unlock()
   355  
   356  	c, err := p.dial(ctx)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  
   361  	report.ConnectionPoolGetNewConnection.Incr()
   362  	return p.newPoolConn(c), nil
   363  }
   364  
   365  func (p *ConnectionPool) newPoolConn(c net.Conn) *PoolConn {
   366  	pc := &PoolConn{
   367  		Conn:       c,
   368  		created:    time.Now(),
   369  		pool:       p,
   370  		forceClose: p.forceClosed,
   371  		inPool:     false,
   372  	}
   373  	if p.framerBuilder != nil {
   374  		pc.fr = p.framerBuilder.New(p.customReader(pc))
   375  		pc.copyFrame = !codec.IsSafeFramer(pc.fr)
   376  	}
   377  	return pc
   378  }
   379  
   380  func (p *ConnectionPool) checkHealthOnce() {
   381  	p.mu.Lock()
   382  	n := p.idle.count
   383  	for i := 0; i < n && p.idle.head != nil; i++ {
   384  		pc := p.idle.head
   385  		p.idle.popHead()
   386  		p.idleSize--
   387  		p.mu.Unlock()
   388  		if p.checker(pc, false) {
   389  			p.mu.Lock()
   390  			p.idleSize++
   391  			p.idle.pushTail(pc)
   392  		} else {
   393  			pc.Conn.Close()
   394  			pc.closed = true
   395  			p.mu.Lock()
   396  		}
   397  	}
   398  	p.mu.Unlock()
   399  }
   400  
   401  func (p *ConnectionPool) checkRoutine(interval time.Duration) {
   402  	for {
   403  		time.Sleep(interval)
   404  		p.mu.Lock()
   405  		closed := p.closed
   406  		p.mu.Unlock()
   407  		if closed {
   408  			return
   409  		}
   410  		p.checkHealthOnce()
   411  
   412  		if p.checkPoolIdleTimeout() {
   413  			return
   414  		}
   415  
   416  		// Check if the minimum number of idle connections is met.
   417  		p.checkMinIdle()
   418  	}
   419  }
   420  
   421  func (p *ConnectionPool) checkMinIdle() {
   422  	if p.MinIdle <= 0 {
   423  		return
   424  	}
   425  	p.keepMinIdles()
   426  }
   427  
   428  // checkPoolIdleTimeout check whether the connection_pool is useless
   429  func (p *ConnectionPool) checkPoolIdleTimeout() bool {
   430  	p.mu.Lock()
   431  	lastGetTime := atomic.LoadInt64(&p.lastGetTime)
   432  	if lastGetTime == 0 || p.poolIdleTimeout == 0 {
   433  		p.mu.Unlock()
   434  		return false
   435  	}
   436  	if time.Now().UnixMilli()-lastGetTime > p.poolIdleTimeout.Milliseconds() &&
   437  		p.onCloseFunc != nil && atomic.LoadInt32(&p.used) == 0 {
   438  		p.mu.Unlock()
   439  		p.onCloseFunc()
   440  		if err := p.Close(); err != nil {
   441  			log.Errorf("failed to close ConnectionPool, error: %v", err)
   442  		}
   443  		return true
   444  	}
   445  	p.mu.Unlock()
   446  	return false
   447  }
   448  
   449  // RegisterChecker registers the idle connection check method.
   450  func (p *ConnectionPool) RegisterChecker(interval time.Duration, checker HealthChecker) {
   451  	if interval <= 0 || checker == nil {
   452  		return
   453  	}
   454  	p.mu.Lock()
   455  	p.checker = checker
   456  	p.mu.Unlock()
   457  	go p.checkRoutine(interval)
   458  }
   459  
   460  // defaultChecker is the default idle connection check method,
   461  // returning true means the connection is available normally.
   462  func (p *ConnectionPool) defaultChecker(pc *PoolConn, isFast bool) bool {
   463  	// Check whether the connection status is abnormal:
   464  	// closed, network exception or sticky packet processing exception.
   465  	if pc.isRemoteError(isFast) {
   466  		return false
   467  	}
   468  	// Based on performance considerations, the quick check only does the RemoteErr check.
   469  	if isFast {
   470  		return true
   471  	}
   472  	// Check if the connection has exceeded the maximum idle time, if so close the connection.
   473  	if p.IdleTimeout > 0 && pc.t.Add(p.IdleTimeout).Before(time.Now()) {
   474  		report.ConnectionPoolIdleTimeout.Incr()
   475  		return false
   476  	}
   477  	// Check if the connection is still alive.
   478  	if p.MaxConnLifetime > 0 && pc.created.Add(p.MaxConnLifetime).Before(time.Now()) {
   479  		report.ConnectionPoolLifetimeExceed.Incr()
   480  		return false
   481  	}
   482  	return true
   483  }
   484  
   485  // dial establishes a connection.
   486  func (p *ConnectionPool) dial(ctx context.Context) (net.Conn, error) {
   487  	if p.Dial != nil {
   488  		return p.Dial(ctx)
   489  	}
   490  	return nil, errors.New("must pass Dial to pool")
   491  }
   492  
   493  // put tries to release the connection to the connection pool.
   494  // forceClose depends on GetOptions.ForceClose and will be true
   495  // if the connection fails to read or write.
   496  func (p *ConnectionPool) put(pc *PoolConn, forceClose bool) error {
   497  	if pc.closed {
   498  		return nil
   499  	}
   500  	p.mu.Lock()
   501  	if !p.closed && !forceClose {
   502  		pc.t = time.Now()
   503  		if !p.PushIdleConnToTail {
   504  			p.idle.pushHead(pc)
   505  		} else {
   506  			p.idle.pushTail(pc)
   507  		}
   508  		if p.idleSize >= p.MaxIdle {
   509  			pc = p.idle.tail
   510  			p.idle.popTail()
   511  		} else {
   512  			p.idleSize++
   513  			pc = nil
   514  		}
   515  	}
   516  	p.mu.Unlock()
   517  	if pc != nil {
   518  		pc.closed = true
   519  		pc.Conn.Close()
   520  	}
   521  	p.freeToken()
   522  	atomic.AddInt32(&p.used, -1)
   523  	return nil
   524  }
   525  
   526  // PoolConn is the connection in the connection pool.
   527  type PoolConn struct {
   528  	net.Conn
   529  	fr         codec.Framer
   530  	t          time.Time
   531  	created    time.Time
   532  	next, prev *PoolConn
   533  	pool       *ConnectionPool
   534  	closed     bool
   535  	forceClose bool
   536  	copyFrame  bool
   537  	inPool     bool
   538  }
   539  
   540  // ReadFrame reads the frame.
   541  func (pc *PoolConn) ReadFrame() ([]byte, error) {
   542  	if pc.closed {
   543  		return nil, ErrConnClosed
   544  	}
   545  	if pc.fr == nil {
   546  		pc.pool.put(pc, true)
   547  		return nil, errors.New("framer not set")
   548  	}
   549  	data, err := pc.fr.ReadFrame()
   550  	if err != nil {
   551  		// ReadFrame failure may be socket Read interface timeout failure
   552  		// or the unpacking fails, in both cases the connection should be closed.
   553  		pc.pool.put(pc, true)
   554  		return nil, err
   555  	}
   556  
   557  	// Framer does not support concurrent read safety, copy the data.
   558  	if pc.copyFrame {
   559  		buf := make([]byte, len(data))
   560  		copy(buf, data)
   561  		return buf, err
   562  	}
   563  	return data, err
   564  }
   565  
   566  // isRemoteError tries to receive a byte to detect whether the peer has actively closed the connection.
   567  // If the peer returns an io.EOF error, it is indicated that the peer has been closed.
   568  // Idle connections should not read data, if the data is read, it means the upper layer's
   569  // sticky packet processing is not done, the connection should also be discarded.
   570  // return true if there is an error in the connection.
   571  func (pc *PoolConn) isRemoteError(isFast bool) bool {
   572  	var err error
   573  	if isFast {
   574  		err = checkConnErrUnblock(pc.Conn, globalBuffer)
   575  	} else {
   576  		err = checkConnErr(pc.Conn, globalBuffer)
   577  	}
   578  	if err != nil {
   579  		report.ConnectionPoolRemoteErr.Incr()
   580  		return true
   581  	}
   582  	return false
   583  }
   584  
   585  // reset resets the connection state.
   586  func (pc *PoolConn) reset() {
   587  	if pc == nil {
   588  		return
   589  	}
   590  	pc.Conn.SetDeadline(time.Time{})
   591  }
   592  
   593  // Write sends data on the connection.
   594  func (pc *PoolConn) Write(b []byte) (int, error) {
   595  	if pc.closed {
   596  		return 0, ErrConnClosed
   597  	}
   598  	n, err := pc.Conn.Write(b)
   599  	if err != nil {
   600  		pc.pool.put(pc, true)
   601  	}
   602  	return n, err
   603  }
   604  
   605  // Read reads data on the connection.
   606  func (pc *PoolConn) Read(b []byte) (int, error) {
   607  	if pc.closed {
   608  		return 0, ErrConnClosed
   609  	}
   610  	n, err := pc.Conn.Read(b)
   611  	if err != nil {
   612  		pc.pool.put(pc, true)
   613  	}
   614  	return n, err
   615  }
   616  
   617  // Close overrides the Close method of net.Conn and puts it back into the connection pool.
   618  func (pc *PoolConn) Close() error {
   619  	if pc.closed {
   620  		return ErrConnClosed
   621  	}
   622  	if pc.inPool {
   623  		return ErrConnInPool
   624  	}
   625  	pc.reset()
   626  	return pc.pool.put(pc, pc.forceClose)
   627  }
   628  
   629  // GetRawConn gets raw connection in PoolConn.
   630  func (pc *PoolConn) GetRawConn() net.Conn {
   631  	return pc.Conn
   632  }
   633  
   634  // connList maintains idle connections and uses stacks to maintain connections.
   635  //
   636  // The stack method has an advantage over the queue. When the request volume is relatively small but the request
   637  // distribution is still relatively uniform, the queue method will cause the occupied connection to be delayed.
   638  type connList struct {
   639  	count      int
   640  	head, tail *PoolConn
   641  }
   642  
   643  func (l *connList) pushHead(pc *PoolConn) {
   644  	pc.inPool = true
   645  	pc.next = l.head
   646  	pc.prev = nil
   647  	if l.count == 0 {
   648  		l.tail = pc
   649  	} else {
   650  		l.head.prev = pc
   651  	}
   652  	l.count++
   653  	l.head = pc
   654  }
   655  
   656  func (l *connList) popHead() {
   657  	pc := l.head
   658  	l.count--
   659  	if l.count == 0 {
   660  		l.head, l.tail = nil, nil
   661  	} else {
   662  		pc.next.prev = nil
   663  		l.head = pc.next
   664  	}
   665  	pc.next, pc.prev = nil, nil
   666  	pc.inPool = false
   667  }
   668  
   669  func (l *connList) pushTail(pc *PoolConn) {
   670  	pc.inPool = true
   671  	pc.next = nil
   672  	pc.prev = l.tail
   673  	if l.count == 0 {
   674  		l.head = pc
   675  	} else {
   676  		l.tail.next = pc
   677  	}
   678  	l.count++
   679  	l.tail = pc
   680  }
   681  
   682  func (l *connList) popTail() {
   683  	pc := l.tail
   684  	l.count--
   685  	if l.count == 0 {
   686  		l.head, l.tail = nil, nil
   687  	} else {
   688  		pc.prev.next = nil
   689  		l.tail = pc.prev
   690  	}
   691  	pc.next, pc.prev = nil, nil
   692  	pc.inPool = false
   693  }
   694  
   695  func getNodeKey(network, address, protocol string) string {
   696  	const underline = "_"
   697  	var key strings.Builder
   698  	key.Grow(len(network) + len(address) + len(protocol) + 2)
   699  	key.WriteString(network)
   700  	key.WriteString(underline)
   701  	key.WriteString(address)
   702  	key.WriteString(underline)
   703  	key.WriteString(protocol)
   704  	return key.String()
   705  }