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 }