cluster: cleanup tests

This commit is contained in:
Vladimir Mihailenco 2018-05-17 16:09:56 +03:00
parent 18b2e30835
commit 5c742fff78
4 changed files with 222 additions and 90 deletions

View File

@ -8,6 +8,7 @@ import (
"math" "math"
"math/rand" "math/rand"
"net" "net"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -35,6 +36,7 @@ type ClusterOptions struct {
// Enables read-only commands on slave nodes. // Enables read-only commands on slave nodes.
ReadOnly bool ReadOnly bool
// Allows routing read-only commands to the closest master or slave node. // Allows routing read-only commands to the closest master or slave node.
// It automatically enables ReadOnly.
RouteByLatency bool RouteByLatency bool
// Allows routing read-only commands to the random master or slave node. // Allows routing read-only commands to the random master or slave node.
RouteRandomly bool RouteRandomly bool
@ -150,6 +152,10 @@ func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode {
return &node return &node
} }
func (n *clusterNode) String() string {
return n.Client.String()
}
func (n *clusterNode) Close() error { func (n *clusterNode) Close() error {
return n.Client.Close() return n.Client.Close()
} }
@ -379,15 +385,17 @@ func (c *clusterNodes) Random() (*clusterNode, error) {
type clusterState struct { type clusterState struct {
nodes *clusterNodes nodes *clusterNodes
masters []*clusterNode Masters []*clusterNode
slaves []*clusterNode Slaves []*clusterNode
slots [][]*clusterNode slots [][]*clusterNode
generation uint32 generation uint32
} }
func newClusterState(nodes *clusterNodes, slots []ClusterSlot, origin string) (*clusterState, error) { func newClusterState(
nodes *clusterNodes, slots []ClusterSlot, origin string,
) (*clusterState, error) {
c := clusterState{ c := clusterState{
nodes: nodes, nodes: nodes,
generation: nodes.NextGeneration(), generation: nodes.NextGeneration(),
@ -413,9 +421,9 @@ func newClusterState(nodes *clusterNodes, slots []ClusterSlot, origin string) (*
nodes = append(nodes, node) nodes = append(nodes, node)
if i == 0 { if i == 0 {
c.masters = appendNode(c.masters, node) c.Masters = appendUniqueNode(c.Masters, node)
} else { } else {
c.slaves = appendNode(c.slaves, node) c.Slaves = appendUniqueNode(c.Slaves, node)
} }
} }
@ -497,6 +505,28 @@ func (c *clusterState) slotNodes(slot int) []*clusterNode {
return nil return nil
} }
func (c *clusterState) IsConsistent() bool {
if len(c.Masters) > len(c.Slaves) {
return false
}
for _, master := range c.Masters {
s := master.Client.Info("replication").Val()
if !strings.Contains(s, "role:master") {
return false
}
}
for _, slave := range c.Slaves {
s := slave.Client.Info("replication").Val()
if !strings.Contains(s, "role:slave") {
return false
}
}
return true
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
type clusterStateHolder struct { type clusterStateHolder struct {
@ -516,7 +546,18 @@ func newClusterStateHolder(fn func() (*clusterState, error)) *clusterStateHolder
} }
} }
func (c *clusterStateHolder) Load() (*clusterState, error) { func (c *clusterStateHolder) Reload() (*clusterState, error) {
state, err := c.reload()
if err != nil {
return nil, err
}
if !state.IsConsistent() {
c.LazyReload()
}
return state, nil
}
func (c *clusterStateHolder) reload() (*clusterState, error) {
state, err := c.load() state, err := c.load()
if err != nil { if err != nil {
c.lastErrMu.Lock() c.lastErrMu.Lock()
@ -535,9 +576,15 @@ func (c *clusterStateHolder) LazyReload() {
go func() { go func() {
defer atomic.StoreUint32(&c.reloading, 0) defer atomic.StoreUint32(&c.reloading, 0)
_, err := c.Load() for {
if err == nil { state, err := c.reload()
time.Sleep(time.Second) if err != nil {
return
}
time.Sleep(100 * time.Millisecond)
if state.IsConsistent() {
return
}
} }
}() }()
} }
@ -596,7 +643,7 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient {
c.cmdable.setProcessor(c.Process) c.cmdable.setProcessor(c.Process)
_, _ = c.state.Load() _, _ = c.state.Reload()
if opt.IdleCheckFrequency > 0 { if opt.IdleCheckFrequency > 0 {
go c.reaper(opt.IdleCheckFrequency) go c.reaper(opt.IdleCheckFrequency)
} }
@ -890,7 +937,7 @@ func (c *ClusterClient) ForEachMaster(fn func(client *Client) error) error {
var wg sync.WaitGroup var wg sync.WaitGroup
errCh := make(chan error, 1) errCh := make(chan error, 1)
for _, master := range state.masters { for _, master := range state.Masters {
wg.Add(1) wg.Add(1)
go func(node *clusterNode) { go func(node *clusterNode) {
defer wg.Done() defer wg.Done()
@ -923,7 +970,7 @@ func (c *ClusterClient) ForEachSlave(fn func(client *Client) error) error {
var wg sync.WaitGroup var wg sync.WaitGroup
errCh := make(chan error, 1) errCh := make(chan error, 1)
for _, slave := range state.slaves { for _, slave := range state.Slaves {
wg.Add(1) wg.Add(1)
go func(node *clusterNode) { go func(node *clusterNode) {
defer wg.Done() defer wg.Done()
@ -967,11 +1014,11 @@ func (c *ClusterClient) ForEachNode(fn func(client *Client) error) error {
} }
} }
for _, node := range state.masters { for _, node := range state.Masters {
wg.Add(1) wg.Add(1)
go worker(node) go worker(node)
} }
for _, node := range state.slaves { for _, node := range state.Slaves {
wg.Add(1) wg.Add(1)
go worker(node) go worker(node)
} }
@ -994,7 +1041,7 @@ func (c *ClusterClient) PoolStats() *PoolStats {
return &acc return &acc
} }
for _, node := range state.masters { for _, node := range state.Masters {
s := node.Client.connPool.Stats() s := node.Client.connPool.Stats()
acc.Hits += s.Hits acc.Hits += s.Hits
acc.Misses += s.Misses acc.Misses += s.Misses
@ -1005,7 +1052,7 @@ func (c *ClusterClient) PoolStats() *PoolStats {
acc.StaleConns += s.StaleConns acc.StaleConns += s.StaleConns
} }
for _, node := range state.slaves { for _, node := range state.Slaves {
s := node.Client.connPool.Stats() s := node.Client.connPool.Stats()
acc.Hits += s.Hits acc.Hits += s.Hits
acc.Misses += s.Misses acc.Misses += s.Misses
@ -1438,7 +1485,7 @@ func isLoopbackAddr(addr string) bool {
return ip.IsLoopback() return ip.IsLoopback()
} }
func appendNode(nodes []*clusterNode, node *clusterNode) []*clusterNode { func appendUniqueNode(nodes []*clusterNode, node *clusterNode) []*clusterNode {
for _, n := range nodes { for _, n := range nodes {
if n == node { if n == node {
return nodes return nodes

View File

@ -50,7 +50,15 @@ func (s *clusterScenario) addrs() []string {
func (s *clusterScenario) clusterClient(opt *redis.ClusterOptions) *redis.ClusterClient { func (s *clusterScenario) clusterClient(opt *redis.ClusterOptions) *redis.ClusterClient {
opt.Addrs = s.addrs() opt.Addrs = s.addrs()
return redis.NewClusterClient(opt) client := redis.NewClusterClient(opt)
Eventually(func() bool {
state, err := client.GetState()
if err != nil {
return false
}
return state.IsConsistent()
}, 30*time.Second).Should(BeTrue())
return client
} }
func startCluster(scenario *clusterScenario) error { func startCluster(scenario *clusterScenario) error {
@ -116,14 +124,7 @@ func startCluster(scenario *clusterScenario) error {
} }
// Wait until all nodes have consistent info. // Wait until all nodes have consistent info.
for _, client := range scenario.clients { wanted := []redis.ClusterSlot{{
err := eventually(func() error {
res, err := client.ClusterSlots().Result()
if err != nil {
return err
}
wanted := []redis.ClusterSlot{
{
Start: 0, Start: 0,
End: 4999, End: 4999,
Nodes: []redis.ClusterNode{{ Nodes: []redis.ClusterNode{{
@ -153,7 +154,12 @@ func startCluster(scenario *clusterScenario) error {
Id: "", Id: "",
Addr: "127.0.0.1:8225", Addr: "127.0.0.1:8225",
}}, }},
}, }}
for _, client := range scenario.clients {
err := eventually(func() error {
res, err := client.ClusterSlots().Result()
if err != nil {
return err
} }
return assertSlotsEqual(res, wanted) return assertSlotsEqual(res, wanted)
}, 30*time.Second) }, 30*time.Second)
@ -213,6 +219,7 @@ func stopCluster(scenario *clusterScenario) error {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var _ = Describe("ClusterClient", func() { var _ = Describe("ClusterClient", func() {
var failover bool
var opt *redis.ClusterOptions var opt *redis.ClusterOptions
var client *redis.ClusterClient var client *redis.ClusterClient
@ -233,15 +240,42 @@ var _ = Describe("ClusterClient", func() {
Expect(cnt).To(Equal(int64(1))) Expect(cnt).To(Equal(int64(1)))
}) })
It("follows redirects", func() { It("GET follows redirects", func() {
Expect(client.Set("A", "VALUE", 0).Err()).NotTo(HaveOccurred()) err := client.Set("A", "VALUE", 0).Err()
Expect(err).NotTo(HaveOccurred())
slot := hashtag.Slot("A") if !failover {
client.SwapSlotNodes(slot) Eventually(func() int64 {
nodes, err := client.Nodes("A")
if err != nil {
return 0
}
return nodes[1].Client.DBSize().Val()
}, 30*time.Second).Should(Equal(int64(1)))
Eventually(func() string { Eventually(func() error {
return client.Get("A").Val() return client.SwapNodes("A")
}, 30*time.Second).Should(Equal("VALUE")) }, 30*time.Second).ShouldNot(HaveOccurred())
}
v, err := client.Get("A").Result()
Expect(err).NotTo(HaveOccurred())
Expect(v).To(Equal("VALUE"))
})
It("SET follows redirects", func() {
if !failover {
Eventually(func() error {
return client.SwapNodes("A")
}, 30*time.Second).ShouldNot(HaveOccurred())
}
err := client.Set("A", "VALUE", 0).Err()
Expect(err).NotTo(HaveOccurred())
v, err := client.Get("A").Result()
Expect(err).NotTo(HaveOccurred())
Expect(v).To(Equal("VALUE"))
}) })
It("distributes keys", func() { It("distributes keys", func() {
@ -250,7 +284,8 @@ var _ = Describe("ClusterClient", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
} }
for _, master := range cluster.masters() { client.ForEachMaster(func(master *redis.Client) error {
defer GinkgoRecover()
Eventually(func() string { Eventually(func() string {
return master.Info("keyspace").Val() return master.Info("keyspace").Val()
}, 30*time.Second).Should(Or( }, 30*time.Second).Should(Or(
@ -258,7 +293,8 @@ var _ = Describe("ClusterClient", func() {
ContainSubstring("keys=29"), ContainSubstring("keys=29"),
ContainSubstring("keys=40"), ContainSubstring("keys=40"),
)) ))
} return nil
})
}) })
It("distributes keys when using EVAL", func() { It("distributes keys when using EVAL", func() {
@ -333,9 +369,12 @@ var _ = Describe("ClusterClient", func() {
keys := []string{"A", "B", "C", "D", "E", "F", "G"} keys := []string{"A", "B", "C", "D", "E", "F", "G"}
It("follows redirects", func() { It("follows redirects", func() {
if !failover {
for _, key := range keys { for _, key := range keys {
slot := hashtag.Slot(key) Eventually(func() error {
client.SwapSlotNodes(slot) return client.SwapNodes(key)
}, 30*time.Second).ShouldNot(HaveOccurred())
}
} }
for i, key := range keys { for i, key := range keys {
@ -354,9 +393,12 @@ var _ = Describe("ClusterClient", func() {
return nil return nil
}) })
if !failover {
for _, key := range keys { for _, key := range keys {
slot := hashtag.Slot(key) Eventually(func() error {
client.SwapSlotNodes(slot) return client.SwapNodes(key)
}, 30*time.Second).ShouldNot(HaveOccurred())
}
} }
for _, key := range keys { for _, key := range keys {
@ -456,9 +498,10 @@ var _ = Describe("ClusterClient", func() {
opt = redisClusterOptions() opt = redisClusterOptions()
client = cluster.clusterClient(opt) client = cluster.clusterClient(opt)
_ = client.ForEachMaster(func(master *redis.Client) error { err := client.ForEachMaster(func(master *redis.Client) error {
return master.FlushDB().Err() return master.FlushDB().Err()
}) })
Expect(err).NotTo(HaveOccurred())
}) })
AfterEach(func() { AfterEach(func() {
@ -469,7 +512,8 @@ var _ = Describe("ClusterClient", func() {
}) })
It("returns pool stats", func() { It("returns pool stats", func() {
Expect(client.PoolStats()).To(BeAssignableToTypeOf(&redis.PoolStats{})) stats := client.PoolStats()
Expect(stats).To(BeAssignableToTypeOf(&redis.PoolStats{}))
}) })
It("removes idle connections", func() { It("removes idle connections", func() {
@ -489,8 +533,9 @@ var _ = Describe("ClusterClient", func() {
opt.MaxRedirects = -1 opt.MaxRedirects = -1
client := cluster.clusterClient(opt) client := cluster.clusterClient(opt)
slot := hashtag.Slot("A") Eventually(func() error {
client.SwapSlotNodes(slot) return client.SwapNodes("A")
}, 30*time.Second).ShouldNot(HaveOccurred())
err := client.Get("A").Err() err := client.Get("A").Err()
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
@ -627,6 +672,8 @@ var _ = Describe("ClusterClient", func() {
Describe("ClusterClient failover", func() { Describe("ClusterClient failover", func() {
BeforeEach(func() { BeforeEach(func() {
failover = true
opt = redisClusterOptions() opt = redisClusterOptions()
opt.MinRetryBackoff = 250 * time.Millisecond opt.MinRetryBackoff = 250 * time.Millisecond
opt.MaxRetryBackoff = time.Second opt.MaxRetryBackoff = time.Second
@ -637,21 +684,34 @@ var _ = Describe("ClusterClient", func() {
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
_ = client.ForEachSlave(func(slave *redis.Client) error { err = client.ForEachSlave(func(slave *redis.Client) error {
defer GinkgoRecover() defer GinkgoRecover()
Eventually(func() int64 { Eventually(func() int64 {
return slave.DBSize().Val() return slave.DBSize().Val()
}, 30*time.Second).Should(Equal(int64(0))) }, 30*time.Second).Should(Equal(int64(0)))
return slave.ClusterFailover().Err() return nil
}) })
Expect(err).NotTo(HaveOccurred())
state, err := client.GetState()
Expect(err).NotTo(HaveOccurred())
Expect(state.IsConsistent()).To(BeTrue())
for _, slave := range state.Slaves {
err = slave.Client.ClusterFailover().Err()
Expect(err).NotTo(HaveOccurred())
Eventually(func() bool {
state, _ := client.LoadState()
return state.IsConsistent()
}, 30*time.Second).Should(BeTrue())
}
}) })
AfterEach(func() { AfterEach(func() {
_ = client.ForEachMaster(func(master *redis.Client) error { failover = false
return master.FlushDB().Err()
})
Expect(client.Close()).NotTo(HaveOccurred()) Expect(client.Close()).NotTo(HaveOccurred())
}) })
@ -664,23 +724,28 @@ var _ = Describe("ClusterClient", func() {
opt.RouteByLatency = true opt.RouteByLatency = true
client = cluster.clusterClient(opt) client = cluster.clusterClient(opt)
_ = client.ForEachMaster(func(master *redis.Client) error { err := client.ForEachMaster(func(master *redis.Client) error {
return master.FlushDB().Err() return master.FlushDB().Err()
}) })
Expect(err).NotTo(HaveOccurred())
_ = client.ForEachSlave(func(slave *redis.Client) error { err = client.ForEachSlave(func(slave *redis.Client) error {
Eventually(func() int64 { Eventually(func() int64 {
return client.DBSize().Val() return client.DBSize().Val()
}, 30*time.Second).Should(Equal(int64(0))) }, 30*time.Second).Should(Equal(int64(0)))
return nil return nil
}) })
Expect(err).NotTo(HaveOccurred())
}) })
AfterEach(func() { AfterEach(func() {
_ = client.ForEachMaster(func(master *redis.Client) error { err := client.ForEachSlave(func(slave *redis.Client) error {
return master.FlushDB().Err() return slave.ReadWrite().Err()
}) })
Expect(client.Close()).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
err = client.Close()
Expect(err).NotTo(HaveOccurred())
}) })
assertClusterClient() assertClusterClient()

View File

@ -266,6 +266,8 @@ type Cmdable interface {
GeoDist(key string, member1, member2, unit string) *FloatCmd GeoDist(key string, member1, member2, unit string) *FloatCmd
GeoHash(key string, members ...string) *StringSliceCmd GeoHash(key string, members ...string) *StringSliceCmd
Command() *CommandsInfoCmd Command() *CommandsInfoCmd
ReadOnly() *StatusCmd
ReadWrite() *StatusCmd
} }
type StatefulCmdable interface { type StatefulCmdable interface {
@ -274,8 +276,6 @@ type StatefulCmdable interface {
Select(index int) *StatusCmd Select(index int) *StatusCmd
SwapDB(index1, index2 int) *StatusCmd SwapDB(index1, index2 int) *StatusCmd
ClientSetName(name string) *BoolCmd ClientSetName(name string) *BoolCmd
ReadOnly() *StatusCmd
ReadWrite() *StatusCmd
} }
var _ Cmdable = (*Client)(nil) var _ Cmdable = (*Client)(nil)
@ -2054,13 +2054,13 @@ func (c *cmdable) ClusterSlaves(nodeID string) *StringSliceCmd {
return cmd return cmd
} }
func (c *statefulCmdable) ReadOnly() *StatusCmd { func (c *cmdable) ReadOnly() *StatusCmd {
cmd := NewStatusCmd("readonly") cmd := NewStatusCmd("readonly")
c.process(cmd) c.process(cmd)
return cmd return cmd
} }
func (c *statefulCmdable) ReadWrite() *StatusCmd { func (c *cmdable) ReadWrite() *StatusCmd {
cmd := NewStatusCmd("readwrite") cmd := NewStatusCmd("readwrite")
c.process(cmd) c.process(cmd)
return cmd return cmd

View File

@ -1,9 +1,11 @@
package redis package redis
import ( import (
"fmt"
"net" "net"
"time" "time"
"github.com/go-redis/redis/internal/hashtag"
"github.com/go-redis/redis/internal/pool" "github.com/go-redis/redis/internal/pool"
) )
@ -19,6 +21,14 @@ func (c *PubSub) ReceiveMessageTimeout(timeout time.Duration) (*Message, error)
return c.receiveMessage(timeout) return c.receiveMessage(timeout)
} }
func (c *ClusterClient) GetState() (*clusterState, error) {
return c.state.Get()
}
func (c *ClusterClient) LoadState() (*clusterState, error) {
return c.loadState()
}
func (c *ClusterClient) SlotAddrs(slot int) []string { func (c *ClusterClient) SlotAddrs(slot int) []string {
state, err := c.state.Get() state, err := c.state.Get()
if err != nil { if err != nil {
@ -32,15 +42,25 @@ func (c *ClusterClient) SlotAddrs(slot int) []string {
return addrs return addrs
} }
// SwapSlot swaps a slot's master/slave address for testing MOVED redirects. func (c *ClusterClient) Nodes(key string) ([]*clusterNode, error) {
func (c *ClusterClient) SwapSlotNodes(slot int) { state, err := c.state.Reload()
state, err := c.state.Get()
if err != nil { if err != nil {
panic(err) return nil, err
} }
slot := hashtag.Slot(key)
nodes := state.slots[slot] nodes := state.slots[slot]
if len(nodes) == 2 { if len(nodes) != 2 {
return nil, fmt.Errorf("slot=%d does not have enough nodes: %v", slot, nodes)
}
return nodes, nil
}
func (c *ClusterClient) SwapNodes(key string) error {
nodes, err := c.Nodes(key)
if err != nil {
return err
}
nodes[0], nodes[1] = nodes[1], nodes[0] nodes[0], nodes[1] = nodes[1], nodes[0]
} return nil
} }