Source code analysis of go redis connection pool

First of all, let's see what the Pooler interface statement does

It can be divided into four categories: 1. Establish connection and close connection; 2. Manage Conn in the pool; 3. Monitor statistics; 4. Close the whole pool
 

type Pooler interface {
	NewConn() (*Conn, error)
	CloseConn(*Conn) error

	Get() (*Conn, error)
	Put(*Conn)
	Remove(*Conn)

	Len() int
	IdleLen() int
	Stats() *Stats

	Close() error
}

Let's take a look at the specific structure types that implement the entire interface

type ConnPool struct {
	opt *Options          //Initialized configuration items

	dialErrorsNum uint32 // Number of atomic connection errors
 
	lastDialError   error  //Last error type of connection error
	lastDialErrorMu sync.RWMutex

	queue chan struct{}   //Synchronization channel of conn idle in the pool

	connsMu sync.Mutex    
	conns   []*Conn       //active conns

	idleConnsMu sync.RWMutex
	idleConns   []*Conn   //idle conns

	stats Stats

	_closed uint32 // atomic / / whether the pool closes the tag
}

Take a look at the process of pool initialization

var _ Pooler = (*ConnPool)(nil) //Interface check

func NewConnPool(opt *Options) *ConnPool {
	p := &ConnPool{
		opt: opt,

		queue:     make(chan struct{}, opt.PoolSize), //Synchronous
		conns:     make([]*Conn, 0, opt.PoolSize),
		idleConns: make([]*Conn, 0, opt.PoolSize),
	}

	if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 {
		go p.reaper(opt.IdleCheckFrequency) //Scheduled tasks, cleaning up expired Conns
	}

	return p
}

//Reaper literally means reaper, clearing
func (p *ConnPool) reaper(frequency time.Duration) {
	ticker := time.NewTicker(frequency)
	defer ticker.Stop()

	for range ticker.C {
		if p.closed() {
			break
		}
        //Clean up useless conns regularly
		n, err := p.ReapStaleConns()
		if err != nil {
			internal.Logf("ReapStaleConns failed: %s", err)
			continue
		}
		atomic.AddUint32(&p.stats.StaleConns, uint32(n))
	}
}


func (p *ConnPool) ReapStaleConns() (int, error) {
	var n int
	for {
        //Write a to the channel, indicating that a task is occupied
		p.getTurn()

		p.idleConnsMu.Lock()
		cn := p.reapStaleConn()
		p.idleConnsMu.Unlock()

		if cn != nil {
			p.removeConn(cn)
		}
        
        //After processing, release the occupied channel position
		p.freeTurn()

		if cn != nil {
			p.closeConn(cn)
			n++
		} else {
			break
		}
	}
	return n, nil
}


func (p *ConnPool) reapStaleConn() *Conn {
	if len(p.idleConns) == 0 {
		return nil
	}
    
    //Take the first free conn
	cn := p.idleConns[0]
    //Judge whether it is time-out and nobody uses it
	if !cn.IsStale(p.opt.IdleTimeout) {
		return nil
	}
    
    //Remove from the free list if no one is using it after timeout 
	p.idleConns = append(p.idleConns[:0], p.idleConns[1:]...)

	return cn
}

//Processing to determine whether time-out occurs
func (cn *Conn) IsStale(timeout time.Duration) bool {
	return timeout > 0 && time.Since(cn.UsedAt()) > timeout
}

//Removing a connection is a simple traversal
func (p *ConnPool) removeConn(cn *Conn) {
	p.connsMu.Lock()
	for i, c := range p.conns {
		if c == cn {
			p.conns = append(p.conns[:i], p.conns[i+1:]...)
			break
		}
	}
	p.connsMu.Unlock()
}

How to create a new connection

func (p *ConnPool) NewConn() (*Conn, error) {
	cn, err := p.newConn()
	if err != nil {
		return nil, err
	}

	p.connsMu.Lock()
    //Join conns after creation
	p.conns = append(p.conns, cn)
	p.connsMu.Unlock()
	return cn, nil
}

func (p *ConnPool) newConn() (*Conn, error) {
	if p.closed() {
		return nil, ErrClosed
	}
    //Determine if there has been an error
	if atomic.LoadUint32(&p.dialErrorsNum) >= uint32(p.opt.PoolSize) {
		return nil, p.getLastDialError()
	}

	netConn, err := p.opt.Dialer()
	if err != nil {
		p.setLastDialError(err)
        //When dialer is wrong to a certain extent, tryDial is a circle of retry's dials
		if atomic.AddUint32(&p.dialErrorsNum, 1) == uint32(p.opt.PoolSize) {
			go p.tryDial()
		}
		return nil, err
	}

	return NewConn(netConn), nil
}


func (p *ConnPool) tryDial() {
	for {
		if p.closed() {
			return
		}
        //It's a constant dailer until it succeeds
		conn, err := p.opt.Dialer()
		if err != nil {
			p.setLastDialError(err)
			time.Sleep(time.Second)
			continue
		}
        //dialer succeeds, set dialErrorsNum to 0
		atomic.StoreUint32(&p.dialErrorsNum, 0)
		_ = conn.Close()
		return
	}
}

Look at the most important thing. How to Get a conn from the pool

func (p *ConnPool) Get() (*Conn, error) {
	if p.closed() {
		return nil, ErrClosed
	}
    
    //If the pool is full, the wait will be blocked, but it will not be blocked all the time. There is a timeout mechanism for synchronization through channel 
	err := p.waitTurn()
	if err != nil {
		return nil, err
	}

	for {
		p.idleConnsMu.Lock()
		cn := p.popIdle()
		p.idleConnsMu.Unlock()

		if cn == nil {
			break
		}
        //Take out the idle and judge whether conn is expired
		if cn.IsStale(p.opt.IdleTimeout) {
			p.CloseConn(cn)
			continue
		}
     
		atomic.AddUint32(&p.stats.Hits, 1)
		return cn, nil
	}
    
    //No conn, miss
	atomic.AddUint32(&p.stats.Misses, 1)
    //Create a new connection
	newcn, err := p.NewConn()
	if err != nil {
		p.freeTurn()
		return nil, err
	}

	return newcn, nil
}

//p. When the Get method succeeds, the queue will be written in the channel, and when the Put method succeeds, the queue will be read in the channel, and the position will be released
func (p *ConnPool) waitTurn() error {
	select {
	case p.queue <- struct{}{}:
		return nil
	default:
		timer := timers.Get().(*time.Timer)
		timer.Reset(p.opt.PoolTimeout)
     
		select {
		case p.queue <- struct{}{}:
			if !timer.Stop() {
                //Critical point processing: to prevent ticker from writing to timer.C at this time, but select to this case
				<-timer.C
			}
			timers.Put(timer)
			return nil
		case <-timer.C:
             //Timeout write mechanism
			timers.Put(timer)
			atomic.AddUint32(&p.stats.Timeouts, 1)
			return ErrPoolTimeout
		}
	}
}

If you understand the Get method, the dual Put method should be very easy to understand:

func (p *ConnPool) Put(cn *Conn) {
    //Setting of buffer
	buf := cn.Rd.PeekBuffered()
	if buf != nil {
		internal.Logf("connection has unread data: %.100q", buf)
		p.Remove(cn)
		return
	}

	p.idleConnsMu.Lock()
    //Put it into the free conns for getting
	p.idleConns = append(p.idleConns, cn)
	p.idleConnsMu.Unlock()
    //When Get succeeds, the channel is written, and then the channel is released
	p.freeTurn()
}

Posted by shazam on Fri, 31 Jan 2020 08:42:47 -0800