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() }