redis/cluster.go

307 lines
6.2 KiB
Go
Raw Normal View History

2015-01-24 15:12:48 +03:00
package redis
import (
"math/rand"
"strings"
"sync"
"sync/atomic"
"time"
)
type ClusterClient struct {
commandable
2015-04-04 16:46:57 +03:00
addrs []string
2015-04-08 12:28:21 +03:00
slots [][]string
2015-03-30 17:53:28 +03:00
slotsMx sync.RWMutex // protects slots & addrs cache
2015-03-30 17:10:53 +03:00
2015-04-04 16:46:57 +03:00
clients map[string]*Client
2015-04-07 12:30:06 +03:00
clientsMx sync.RWMutex
2015-03-30 17:10:53 +03:00
opt *ClusterOptions
2015-01-24 15:12:48 +03:00
_reload uint32
}
// NewClusterClient initializes a new cluster-aware client using given options.
// A list of seed addresses must be provided.
2015-04-04 16:46:57 +03:00
func NewClusterClient(opt *ClusterOptions) *ClusterClient {
2015-01-24 15:12:48 +03:00
client := &ClusterClient{
2015-04-08 12:28:50 +03:00
addrs: opt.Addrs,
2015-04-04 16:46:57 +03:00
clients: make(map[string]*Client),
2015-01-24 15:12:48 +03:00
opt: opt,
_reload: 1,
}
client.commandable.process = client.process
go client.reaper(time.NewTicker(5 * time.Minute))
2015-04-04 16:46:57 +03:00
return client
2015-01-24 15:12:48 +03:00
}
2015-04-04 16:46:57 +03:00
// Close closes the cluster client.
2015-01-24 15:12:48 +03:00
func (c *ClusterClient) Close() error {
2015-04-04 16:46:57 +03:00
// TODO: close should make client unusable
c.setSlots(nil)
return nil
2015-01-24 15:12:48 +03:00
}
// ------------------------------------------------------------------------
2015-04-04 16:46:57 +03:00
// getClient returns a Client for a given address.
func (c *ClusterClient) getClient(addr string) *Client {
c.clientsMx.RLock()
client, ok := c.clients[addr]
if ok {
c.clientsMx.RUnlock()
return client
2015-01-24 15:12:48 +03:00
}
2015-04-04 16:46:57 +03:00
c.clientsMx.RUnlock()
2015-01-24 15:12:48 +03:00
2015-04-04 16:46:57 +03:00
c.clientsMx.Lock()
client, ok = c.clients[addr]
2015-01-24 15:12:48 +03:00
if !ok {
opt := c.opt.clientOptions()
opt.Addr = addr
client = NewTCPClient(opt)
2015-04-04 16:46:57 +03:00
c.clients[addr] = client
2015-01-24 15:12:48 +03:00
}
2015-04-04 16:46:57 +03:00
c.clientsMx.Unlock()
2015-01-24 15:12:48 +03:00
return client
}
2015-04-04 16:46:57 +03:00
// randomClient returns a Client for the first live node.
func (c *ClusterClient) randomClient() (client *Client, err error) {
for i := 0; i < 10; i++ {
n := rand.Intn(len(c.addrs))
client = c.getClient(c.addrs[n])
err = client.Ping().Err()
if err == nil {
return client, nil
}
}
return nil, err
}
2015-01-24 15:12:48 +03:00
// Process a command
func (c *ClusterClient) process(cmd Cmder) {
2015-04-04 16:46:57 +03:00
var client *Client
2015-01-24 15:12:48 +03:00
var ask bool
c.reloadIfDue()
2015-04-04 16:46:57 +03:00
slot := hashSlot(cmd.clusterKey())
2015-03-30 17:53:28 +03:00
c.slotsMx.RLock()
2015-04-08 12:28:21 +03:00
addrs := c.slots[slot]
2015-04-07 12:30:06 +03:00
c.slotsMx.RUnlock()
2015-01-24 15:12:48 +03:00
2015-04-08 12:28:21 +03:00
if len(addrs) > 0 {
client = c.getClient(addrs[0]) // First address is master.
2015-04-04 16:46:57 +03:00
} else {
var err error
client, err = c.randomClient()
if err != nil {
cmd.setErr(err)
return
}
}
for attempt := 0; attempt <= c.opt.getMaxRedirects(); attempt++ {
2015-01-24 15:12:48 +03:00
if ask {
2015-04-04 16:46:57 +03:00
pipe := client.Pipeline()
2015-01-24 15:12:48 +03:00
pipe.Process(NewCmd("ASKING"))
pipe.Process(cmd)
_, _ = pipe.Exec()
ask = false
} else {
2015-04-04 16:46:57 +03:00
client.Process(cmd)
2015-01-24 15:12:48 +03:00
}
// If there is no (real) error, we are done!
err := cmd.Err()
2015-04-04 16:46:57 +03:00
if err == nil || err == Nil || err == TxFailedErr {
2015-01-24 15:12:48 +03:00
return
}
2015-04-07 12:30:06 +03:00
// On network errors try random node.
2015-04-04 16:46:57 +03:00
if isNetworkError(err) {
2015-04-07 12:30:06 +03:00
client, err = c.randomClient()
if err != nil {
return
2015-01-24 15:12:48 +03:00
}
cmd.reset()
continue
}
// Check the error message, return if unexpected
parts := strings.SplitN(err.Error(), " ", 3)
if len(parts) != 3 {
return
}
// Handle MOVE and ASK redirections, return on any other error
switch parts[0] {
case "MOVED":
2015-04-04 16:46:57 +03:00
c.scheduleReload()
client = c.getClient(parts[2])
2015-01-24 15:12:48 +03:00
case "ASK":
ask = true
2015-04-04 16:46:57 +03:00
client = c.getClient(parts[2])
2015-01-24 15:12:48 +03:00
default:
return
}
cmd.reset()
}
}
2015-04-04 16:46:57 +03:00
// Closes all clients and returns last error if there are any.
func (c *ClusterClient) resetClients() (err error) {
c.clientsMx.Lock()
for addr, client := range c.clients {
if e := client.Close(); e != nil {
err = e
}
delete(c.clients, addr)
2015-01-24 15:12:48 +03:00
}
2015-04-04 16:46:57 +03:00
c.clientsMx.Unlock()
return err
}
2015-01-24 15:12:48 +03:00
2015-04-04 16:46:57 +03:00
func (c *ClusterClient) setSlots(slots []ClusterSlotInfo) {
2015-03-30 17:53:28 +03:00
c.slotsMx.Lock()
2015-01-24 15:12:48 +03:00
2015-04-08 12:28:21 +03:00
c.slots = make([][]string, hashSlots)
2015-04-07 12:30:06 +03:00
c.resetClients()
seen := make(map[string]struct{})
2015-04-08 12:28:21 +03:00
for _, addr := range c.addrs {
seen[addr] = struct{}{}
}
2015-04-04 16:46:57 +03:00
for _, info := range slots {
2015-04-07 12:30:06 +03:00
for slot := info.Start; slot <= info.End; slot++ {
2015-04-08 12:28:21 +03:00
c.slots[slot] = info.Addrs
2015-04-07 12:30:06 +03:00
}
2015-04-08 12:28:21 +03:00
for _, addr := range info.Addrs {
if _, ok := seen[addr]; !ok {
c.addrs = append(c.addrs, addr)
seen[addr] = struct{}{}
}
2015-01-24 15:12:48 +03:00
}
}
2015-04-04 16:46:57 +03:00
c.slotsMx.Unlock()
2015-01-24 15:12:48 +03:00
}
2015-04-04 16:46:57 +03:00
// Closes all connections and reloads slot cache, if due.
func (c *ClusterClient) reloadIfDue() (err error) {
if !atomic.CompareAndSwapUint32(&c._reload, 1, 0) {
return
}
2015-01-24 15:12:48 +03:00
2015-04-04 16:46:57 +03:00
client, err := c.randomClient()
if err != nil {
return err
2015-01-24 15:12:48 +03:00
}
2015-04-04 16:46:57 +03:00
slots, err := client.ClusterSlots().Result()
if err != nil {
return err
}
c.setSlots(slots)
2015-01-24 15:12:48 +03:00
2015-04-04 16:46:57 +03:00
return nil
2015-01-24 15:12:48 +03:00
}
2015-04-04 16:46:57 +03:00
// Schedules slots reload on next request.
func (c *ClusterClient) scheduleReload() {
atomic.StoreUint32(&c._reload, 1)
2015-01-24 15:12:48 +03:00
}
// reaper closes idle connections to the cluster.
func (c *ClusterClient) reaper(ticker *time.Ticker) {
for _ = range ticker.C {
2015-04-08 12:28:21 +03:00
for _, client := range c.clients {
pool := client.connPool
// pool.First removes idle connections from the pool for us. So
// just put returned connection back.
if cn := pool.First(); cn != nil {
pool.Put(cn)
}
}
}
}
2015-01-24 15:12:48 +03:00
//------------------------------------------------------------------------------
type ClusterOptions struct {
// A seed-list of host:port addresses of known cluster nodes
Addrs []string
// An optional password
Password string
// The maximum number of MOVED/ASK redirects to follow, before
// giving up. Default: 16
MaxRedirects int
// The maximum number of TCP sockets per connection. Default: 5
PoolSize int
// Timeout settings
DialTimeout, ReadTimeout, WriteTimeout, IdleTimeout time.Duration
}
func (opt *ClusterOptions) getPoolSize() int {
if opt.PoolSize < 1 {
return 5
}
return opt.PoolSize
}
func (opt *ClusterOptions) getDialTimeout() time.Duration {
if opt.DialTimeout == 0 {
return 5 * time.Second
}
return opt.DialTimeout
}
func (opt *ClusterOptions) getMaxRedirects() int {
if opt.MaxRedirects < 1 {
return 16
}
return opt.MaxRedirects
}
func (opt *ClusterOptions) clientOptions() *Options {
return &Options{
DB: 0,
Password: opt.Password,
DialTimeout: opt.getDialTimeout(),
ReadTimeout: opt.ReadTimeout,
WriteTimeout: opt.WriteTimeout,
PoolSize: opt.getPoolSize(),
IdleTimeout: opt.IdleTimeout,
}
}
//------------------------------------------------------------------------------
const hashSlots = 16384
2015-03-23 11:23:33 +03:00
// hashSlot returns a consistent slot number between 0 and 16383
// for any given string key.
func hashSlot(key string) int {
2015-01-24 15:12:48 +03:00
if s := strings.IndexByte(key, '{'); s > -1 {
if e := strings.IndexByte(key[s+1:], '}'); e > 0 {
key = key[s+1 : s+e+1]
}
}
if key == "" {
return rand.Intn(hashSlots)
}
return int(crc16sum(key)) % hashSlots
}