bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/database/sentinel/sentinel.go (about)

     1  package sentinel
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/garyburd/redigo/redis"
    12  )
    13  
    14  // Sentinel provides a way to add high availability (HA) to Redis Pool using
    15  // preconfigured addresses of Sentinel servers and name of master which Sentinels
    16  // monitor. It works with Redis >= 2.8.12 (mostly because of ROLE command that
    17  // was introduced in that version, it's possible though to support old versions
    18  // using INFO command).
    19  //
    20  // Example of the simplest usage to contact master "mymaster":
    21  //
    22  //  func newSentinelPool() *redis.Pool {
    23  //  	sntnl := &sentinel.Sentinel{
    24  //  		Addrs:      []string{":26379", ":26380", ":26381"},
    25  //  		MasterName: "mymaster",
    26  //  		Dial: func(addr string) (redis.Conn, error) {
    27  //  			timeout := 500 * time.Millisecond
    28  //  			c, err := redis.DialTimeout("tcp", addr, timeout, timeout, timeout)
    29  //  			if err != nil {
    30  //  				return nil, err
    31  //  			}
    32  //  			return c, nil
    33  //  		},
    34  //  	}
    35  //  	return &redis.Pool{
    36  //  		MaxIdle:     3,
    37  //  		MaxActive:   64,
    38  //  		Wait:        true,
    39  //  		IdleTimeout: 240 * time.Second,
    40  //  		Dial: func() (redis.Conn, error) {
    41  //  			masterAddr, err := sntnl.MasterAddr()
    42  //  			if err != nil {
    43  //  				return nil, err
    44  //  			}
    45  //  			c, err := redis.Dial("tcp", masterAddr)
    46  //  			if err != nil {
    47  //  				return nil, err
    48  //  			}
    49  //  			return c, nil
    50  //  		},
    51  //  		TestOnBorrow: func(c redis.Conn, t time.Time) error {
    52  //  			if !sentinel.TestRole(c, "master") {
    53  //  				return errors.New("Role check failed")
    54  //  			} else {
    55  //  				return nil
    56  //  			}
    57  //  		},
    58  //  	}
    59  //  }
    60  type Sentinel struct {
    61  	// Addrs is a slice with known Sentinel addresses.
    62  	Addrs []string
    63  
    64  	// MasterName is a name of Redis master Sentinel servers monitor.
    65  	MasterName string
    66  
    67  	// Dial is a user supplied function to connect to Sentinel on given address. This
    68  	// address will be chosen from Addrs slice.
    69  	// Note that as per the redis-sentinel client guidelines, a timeout is mandatory
    70  	// while connecting to Sentinels, and should not be set to 0.
    71  	Dial func(addr string) (redis.Conn, error)
    72  
    73  	// Pool is a user supplied function returning custom connection pool to Sentinel.
    74  	// This can be useful to tune options if you are not satisfied with what default
    75  	// Sentinel pool offers. See defaultPool() method for default pool implementation.
    76  	// In most cases you only need to provide Dial function and let this be nil.
    77  	Pool func(addr string) *redis.Pool
    78  
    79  	mu    sync.RWMutex
    80  	pools map[string]*redis.Pool
    81  	addr  string
    82  }
    83  
    84  // NoSentinelsAvailable is returned when all sentinels in the list are exhausted
    85  // (or none configured), and contains the last error returned by Dial (which
    86  // may be nil)
    87  type NoSentinelsAvailable struct {
    88  	lastError error
    89  }
    90  
    91  func (ns NoSentinelsAvailable) Error() string {
    92  	if ns.lastError != nil {
    93  		return fmt.Sprintf("redigo: no sentinels available; last error: %s", ns.lastError.Error())
    94  	}
    95  	return fmt.Sprintf("redigo: no sentinels available")
    96  }
    97  
    98  // putToTop puts Sentinel address to the top of address list - this means
    99  // that all next requests will use Sentinel on this address first.
   100  //
   101  // From Sentinel guidelines:
   102  //
   103  // The first Sentinel replying to the client request should be put at the
   104  // start of the list, so that at the next reconnection, we'll try first
   105  // the Sentinel that was reachable in the previous connection attempt,
   106  // minimizing latency.
   107  //
   108  // Lock must be held by caller.
   109  func (s *Sentinel) putToTop(addr string) {
   110  	addrs := s.Addrs
   111  	if addrs[0] == addr {
   112  		// Already on top.
   113  		return
   114  	}
   115  	newAddrs := []string{addr}
   116  	for _, a := range addrs {
   117  		if a == addr {
   118  			continue
   119  		}
   120  		newAddrs = append(newAddrs, a)
   121  	}
   122  	s.Addrs = newAddrs
   123  }
   124  
   125  // putToBottom puts Sentinel address to the bottom of address list.
   126  // We call this method internally when see that some Sentinel failed to answer
   127  // on application request so next time we start with another one.
   128  //
   129  // Lock must be held by caller.
   130  func (s *Sentinel) putToBottom(addr string) {
   131  	addrs := s.Addrs
   132  	if addrs[len(addrs)-1] == addr {
   133  		// Already on bottom.
   134  		return
   135  	}
   136  	newAddrs := []string{}
   137  	for _, a := range addrs {
   138  		if a == addr {
   139  			continue
   140  		}
   141  		newAddrs = append(newAddrs, a)
   142  	}
   143  	newAddrs = append(newAddrs, addr)
   144  	s.Addrs = newAddrs
   145  }
   146  
   147  // defaultPool returns a connection pool to one Sentinel. This allows
   148  // us to call concurrent requests to Sentinel using connection Do method.
   149  func (s *Sentinel) defaultPool(addr string) *redis.Pool {
   150  	return &redis.Pool{
   151  		MaxIdle:     3,
   152  		MaxActive:   10,
   153  		Wait:        true,
   154  		IdleTimeout: 240 * time.Second,
   155  		Dial: func() (redis.Conn, error) {
   156  			return s.Dial(addr)
   157  		},
   158  		TestOnBorrow: func(c redis.Conn, t time.Time) error {
   159  			_, err := c.Do("PING")
   160  			return err
   161  		},
   162  	}
   163  }
   164  
   165  func (s *Sentinel) get(addr string) redis.Conn {
   166  	pool := s.poolForAddr(addr)
   167  	return pool.Get()
   168  }
   169  
   170  func (s *Sentinel) poolForAddr(addr string) *redis.Pool {
   171  	s.mu.Lock()
   172  	if s.pools == nil {
   173  		s.pools = make(map[string]*redis.Pool)
   174  	}
   175  	pool, ok := s.pools[addr]
   176  	if ok {
   177  		s.mu.Unlock()
   178  		return pool
   179  	}
   180  	s.mu.Unlock()
   181  	newPool := s.newPool(addr)
   182  	s.mu.Lock()
   183  	p, ok := s.pools[addr]
   184  	if ok {
   185  		s.mu.Unlock()
   186  		return p
   187  	}
   188  	s.pools[addr] = newPool
   189  	s.mu.Unlock()
   190  	return newPool
   191  }
   192  
   193  func (s *Sentinel) newPool(addr string) *redis.Pool {
   194  	if s.Pool != nil {
   195  		return s.Pool(addr)
   196  	}
   197  	return s.defaultPool(addr)
   198  }
   199  
   200  // close connection pool to Sentinel.
   201  // Lock must be hold by caller.
   202  func (s *Sentinel) close() {
   203  	if s.pools != nil {
   204  		for _, pool := range s.pools {
   205  			pool.Close()
   206  		}
   207  	}
   208  	s.pools = nil
   209  }
   210  
   211  func (s *Sentinel) doUntilSuccess(f func(redis.Conn) (interface{}, error)) (interface{}, error) {
   212  	s.mu.RLock()
   213  	addrs := s.Addrs
   214  	s.mu.RUnlock()
   215  
   216  	var lastErr error
   217  
   218  	for _, addr := range addrs {
   219  		conn := s.get(addr)
   220  		reply, err := f(conn)
   221  		conn.Close()
   222  		if err != nil {
   223  			lastErr = err
   224  			s.mu.Lock()
   225  			pool, ok := s.pools[addr]
   226  			if ok {
   227  				pool.Close()
   228  				delete(s.pools, addr)
   229  			}
   230  			s.putToBottom(addr)
   231  			s.mu.Unlock()
   232  			continue
   233  		}
   234  		s.putToTop(addr)
   235  		return reply, nil
   236  	}
   237  
   238  	return nil, NoSentinelsAvailable{lastError: lastErr}
   239  }
   240  
   241  // MasterAddr returns an address of current Redis master instance.
   242  func (s *Sentinel) MasterAddr() (string, error) {
   243  	res, err := s.doUntilSuccess(func(c redis.Conn) (interface{}, error) {
   244  		return queryForMaster(c, s.MasterName)
   245  	})
   246  	if err != nil {
   247  		return "", err
   248  	}
   249  	return res.(string), nil
   250  }
   251  
   252  // SlaveAddrs returns a slice with known slave addresses of current master instance.
   253  func (s *Sentinel) SlaveAddrs() ([]string, error) {
   254  	res, err := s.doUntilSuccess(func(c redis.Conn) (interface{}, error) {
   255  		return queryForSlaveAddrs(c, s.MasterName)
   256  	})
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	return res.([]string), nil
   261  }
   262  
   263  // Slave represents a Redis slave instance which is known by Sentinel.
   264  type Slave struct {
   265  	ip    string
   266  	port  string
   267  	flags string
   268  }
   269  
   270  // Addr returns an address of slave.
   271  func (s *Slave) Addr() string {
   272  	return net.JoinHostPort(s.ip, s.port)
   273  }
   274  
   275  // Available returns if slave is in working state at moment based on information in slave flags.
   276  func (s *Slave) Available() bool {
   277  	return !strings.Contains(s.flags, "disconnected") && !strings.Contains(s.flags, "s_down")
   278  }
   279  
   280  // Slaves returns a slice with known slaves of master instance.
   281  func (s *Sentinel) Slaves() ([]*Slave, error) {
   282  	res, err := s.doUntilSuccess(func(c redis.Conn) (interface{}, error) {
   283  		return queryForSlaves(c, s.MasterName)
   284  	})
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	return res.([]*Slave), nil
   289  }
   290  
   291  // SentinelAddrs returns a slice of known Sentinel addresses Sentinel server aware of.
   292  func (s *Sentinel) SentinelAddrs() ([]string, error) {
   293  	res, err := s.doUntilSuccess(func(c redis.Conn) (interface{}, error) {
   294  		return queryForSentinels(c, s.MasterName)
   295  	})
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  	return res.([]string), nil
   300  }
   301  
   302  // Discover allows to update list of known Sentinel addresses. From docs:
   303  //
   304  // A client may update its internal list of Sentinel nodes following this procedure:
   305  // 1) Obtain a list of other Sentinels for this master using the command SENTINEL sentinels <master-name>.
   306  // 2) Add every ip:port pair not already existing in our list at the end of the list.
   307  func (s *Sentinel) Discover() error {
   308  	addrs, err := s.SentinelAddrs()
   309  	if err != nil {
   310  		return err
   311  	}
   312  	s.mu.Lock()
   313  	for _, addr := range addrs {
   314  		if !stringInSlice(addr, s.Addrs) {
   315  			s.Addrs = append(s.Addrs, addr)
   316  		}
   317  	}
   318  	s.mu.Unlock()
   319  	return nil
   320  }
   321  
   322  // Close closes current connection to Sentinel.
   323  func (s *Sentinel) Close() error {
   324  	s.mu.Lock()
   325  	s.close()
   326  	s.mu.Unlock()
   327  	return nil
   328  }
   329  
   330  // TestRole wraps GetRole in a test to verify if the role matches an expected
   331  // role string. If there was any error in querying the supplied connection,
   332  // the function returns false. Works with Redis >= 2.8.12.
   333  // It's not goroutine safe, but if you call this method on pooled connections
   334  // then you are OK.
   335  func TestRole(c redis.Conn, expectedRole string) bool {
   336  	role, err := getRole(c)
   337  	if err != nil || role != expectedRole {
   338  		return false
   339  	}
   340  	return true
   341  }
   342  
   343  // getRole is a convenience function supplied to query an instance (master or
   344  // slave) for its role. It attempts to use the ROLE command introduced in
   345  // redis 2.8.12.
   346  func getRole(c redis.Conn) (string, error) {
   347  	res, err := c.Do("ROLE")
   348  	if err != nil {
   349  		return "", err
   350  	}
   351  	rres, ok := res.([]interface{})
   352  	if ok {
   353  		return redis.String(rres[0], nil)
   354  	}
   355  	return "", errors.New("redigo: can not transform ROLE reply to string")
   356  }
   357  
   358  func queryForMaster(conn redis.Conn, masterName string) (string, error) {
   359  	res, err := redis.Strings(conn.Do("SENTINEL", "get-master-addr-by-name", masterName))
   360  	if err != nil {
   361  		return "", err
   362  	}
   363  	if len(res) < 2 {
   364  		return "", errors.New("redigo: malformed get-master-addr-by-name reply")
   365  	}
   366  	masterAddr := net.JoinHostPort(res[0], res[1])
   367  	return masterAddr, nil
   368  }
   369  
   370  func queryForSlaveAddrs(conn redis.Conn, masterName string) ([]string, error) {
   371  	slaves, err := queryForSlaves(conn, masterName)
   372  	if err != nil {
   373  		return nil, err
   374  	}
   375  	slaveAddrs := make([]string, 0)
   376  	for _, slave := range slaves {
   377  		slaveAddrs = append(slaveAddrs, slave.Addr())
   378  	}
   379  	return slaveAddrs, nil
   380  }
   381  
   382  func queryForSlaves(conn redis.Conn, masterName string) ([]*Slave, error) {
   383  	res, err := redis.Values(conn.Do("SENTINEL", "slaves", masterName))
   384  	if err != nil {
   385  		return nil, err
   386  	}
   387  	slaves := make([]*Slave, 0)
   388  	for _, a := range res {
   389  		sm, err := redis.StringMap(a, err)
   390  		if err != nil {
   391  			return slaves, err
   392  		}
   393  		slave := &Slave{
   394  			ip:    sm["ip"],
   395  			port:  sm["port"],
   396  			flags: sm["flags"],
   397  		}
   398  		slaves = append(slaves, slave)
   399  	}
   400  	return slaves, nil
   401  }
   402  
   403  func queryForSentinels(conn redis.Conn, masterName string) ([]string, error) {
   404  	res, err := redis.Values(conn.Do("SENTINEL", "sentinels", masterName))
   405  	if err != nil {
   406  		return nil, err
   407  	}
   408  	sentinels := make([]string, 0)
   409  	for _, a := range res {
   410  		sm, err := redis.StringMap(a, err)
   411  		if err != nil {
   412  			return sentinels, err
   413  		}
   414  		sentinels = append(sentinels, fmt.Sprintf("%s:%s", sm["ip"], sm["port"]))
   415  	}
   416  	return sentinels, nil
   417  }
   418  
   419  func stringInSlice(str string, slice []string) bool {
   420  	for _, s := range slice {
   421  		if s == str {
   422  			return true
   423  		}
   424  	}
   425  	return false
   426  }