mirror of
https://github.com/hanzokv/go.git
synced 2026-09-24 00:31:51 +00:00
* [maintnotif] Cluster specific handlers (#3613) * maint notification handlers for cluster messages * unrelax all conns * trigger ci on feature branches * feat(maintnotif): lazy cluster topology reload (#3614) * lazy cluster topology reload * fix discrepancies between options structs * Update osscluster_lazy_reload_test.go Co-authored-by: Copilot <[email protected]> * Update osscluster.go Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]> * feat(e2e): mock maintnotif e2e tests (#3639) * lazy cluster topology reload * fix discrepancies between options structs * Update osscluster_lazy_reload_test.go Co-authored-by: Copilot <[email protected]> * Update osscluster.go Co-authored-by: Copilot <[email protected]> * wip fault with mock proxy * make lint happy * fix linter issues * faster tests with mocks * linter once again * add complex node test * add ci e2e * use correct redis container * e2e fix --------- Co-authored-by: Copilot <[email protected]> * fix(retry): Add retry mechanism for NOREPLICAS error (#3647) * fix(queuedNewConn): protect against nil context (#3649) * fix(maintnotif): fix smigrated parser and add cluster state reload interval option (#3663) * fix SMIGRATED parsing * fix smigrated parser * add ClusterStateReloadInterval to ClusterOptions * fix tests * set default cluster reload interval to 10s * chore(lint): format * feat(smigrated): new format & remember original host:port (#3697) * lazy cluster topology reload * fix discrepancies between options structs * Update osscluster_lazy_reload_test.go Co-authored-by: Copilot <[email protected]> * Update osscluster.go Co-authored-by: Copilot <[email protected]> * wip fault with mock proxy * make lint happy * fix linter issues * faster tests with mocks * linter once again * add complex node test * add ci e2e * use correct redis container * e2e fix * additional e2e tests * fix data race * fix random shard picker * fix e2e tests * fix for empty endpoint * fix case when semaphore is full, but still need to check idle * scenario tests * create database from config * wip * feat(client): store original addrs for later use * fix(notif): change smigrated notification * fix(lint): fix linter * fix(smigrated): use array * fix(e2e): wip * Update options.go Co-authored-by: Copilot <[email protected]> * Update redis.go Co-authored-by: Copilot <[email protected]> * fix(notif): if the conn has no original addr, trigger reload with first target * chore(e2e): wip cluster e2e * chore(e2e): fix nil pointer from e2e tests * chore(e2e): fix tests and reports * chore(e2e): proper logging in e2e * chore(e2e): add pubsub in the tests as well * chore(e2e): mockproxy fixes * chore(e2e): mockproxy fixes * chore(e2e): mockproxy fixes * chore(e2e): mockproxy fixes v3 * stop background routines * fix(e2e): tests --------- Co-authored-by: Copilot <[email protected]> * chore(docs): add example app (#3651) * chore(lint): fmt * chore(docs): improve docs * chore(docs): update features.md * chore(github): remove example * chore(maintnotif): rename option and address pr comments * fix(e2e): command runner should use client timeout * chore(e2e): refactor tests * fix(e2e): set default timeout to 90m * fix(e2e): skip tests if proxy cannot be started --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: ofekshenawa <[email protected]> Co-authored-by: Elena Kolevska <[email protected]>
688 lines
19 KiB
Go
688 lines
19 KiB
Go
package redis
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9/internal/pool"
|
|
"github.com/redis/go-redis/v9/internal/proto"
|
|
|
|
. "github.com/bsm/ginkgo/v2"
|
|
. "github.com/bsm/gomega"
|
|
)
|
|
|
|
var ctx = context.TODO()
|
|
|
|
var _ = Describe("newClusterState", func() {
|
|
var state *clusterState
|
|
|
|
createClusterState := func(slots []ClusterSlot) *clusterState {
|
|
opt := &ClusterOptions{}
|
|
opt.init()
|
|
nodes := newClusterNodes(opt)
|
|
state, err := newClusterState(nodes, slots, "10.10.10.10:1234")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
return state
|
|
}
|
|
|
|
Describe("sorting", func() {
|
|
BeforeEach(func() {
|
|
state = createClusterState([]ClusterSlot{{
|
|
Start: 1000,
|
|
End: 1999,
|
|
}, {
|
|
Start: 0,
|
|
End: 999,
|
|
}, {
|
|
Start: 2000,
|
|
End: 2999,
|
|
}})
|
|
})
|
|
|
|
It("sorts slots", func() {
|
|
Expect(state.slots).To(Equal([]*clusterSlot{
|
|
{start: 0, end: 999, nodes: nil},
|
|
{start: 1000, end: 1999, nodes: nil},
|
|
{start: 2000, end: 2999, nodes: nil},
|
|
}))
|
|
})
|
|
})
|
|
|
|
Describe("loopback", func() {
|
|
BeforeEach(func() {
|
|
state = createClusterState([]ClusterSlot{{
|
|
Nodes: []ClusterNode{{Addr: "127.0.0.1:7001"}},
|
|
}, {
|
|
Nodes: []ClusterNode{{Addr: "127.0.0.1:7002"}},
|
|
}, {
|
|
Nodes: []ClusterNode{{Addr: "1.2.3.4:1234"}},
|
|
}, {
|
|
Nodes: []ClusterNode{{Addr: ":1234"}},
|
|
}})
|
|
})
|
|
|
|
It("replaces loopback hosts in addresses", func() {
|
|
slotAddr := func(slot *clusterSlot) string {
|
|
return slot.nodes[0].Client.Options().Addr
|
|
}
|
|
|
|
Expect(slotAddr(state.slots[0])).To(Equal("10.10.10.10:7001"))
|
|
Expect(slotAddr(state.slots[1])).To(Equal("10.10.10.10:7002"))
|
|
Expect(slotAddr(state.slots[2])).To(Equal("1.2.3.4:1234"))
|
|
Expect(slotAddr(state.slots[3])).To(Equal(":1234"))
|
|
})
|
|
})
|
|
|
|
Describe("NodeAddress", func() {
|
|
BeforeEach(func() {
|
|
state = createClusterState([]ClusterSlot{{
|
|
Nodes: []ClusterNode{{Addr: "127.0.0.1:7001"}},
|
|
}, {
|
|
Nodes: []ClusterNode{{Addr: "127.0.0.1:7002"}},
|
|
}, {
|
|
Nodes: []ClusterNode{{Addr: "1.2.3.4:1234"}},
|
|
}, {
|
|
Nodes: []ClusterNode{{Addr: ":1234"}},
|
|
}})
|
|
})
|
|
|
|
It("preserves node address from ClusterSlots before loopback replacement", func() {
|
|
slotNodeAddress := func(slot *clusterSlot) string {
|
|
return slot.nodes[0].Client.NodeAddress()
|
|
}
|
|
|
|
// Node addresses should be preserved even when Addr is transformed
|
|
Expect(slotNodeAddress(state.slots[0])).To(Equal("127.0.0.1:7001"))
|
|
Expect(slotNodeAddress(state.slots[1])).To(Equal("127.0.0.1:7002"))
|
|
Expect(slotNodeAddress(state.slots[2])).To(Equal("1.2.3.4:1234"))
|
|
Expect(slotNodeAddress(state.slots[3])).To(Equal(":1234"))
|
|
})
|
|
|
|
It("has different Addr and NodeAddress when loopback is replaced", func() {
|
|
// For loopback addresses, Addr should be transformed but NodeAddress preserved
|
|
slot0 := state.slots[0]
|
|
Expect(slot0.nodes[0].Client.Options().Addr).To(Equal("10.10.10.10:7001"))
|
|
Expect(slot0.nodes[0].Client.NodeAddress()).To(Equal("127.0.0.1:7001"))
|
|
|
|
// For non-loopback addresses, Addr and NodeAddress should be the same
|
|
slot2 := state.slots[2]
|
|
Expect(slot2.nodes[0].Client.Options().Addr).To(Equal("1.2.3.4:1234"))
|
|
Expect(slot2.nodes[0].Client.NodeAddress()).To(Equal("1.2.3.4:1234"))
|
|
})
|
|
})
|
|
|
|
Describe("NodeAddress with FQDN", func() {
|
|
BeforeEach(func() {
|
|
// Simulate FQDN endpoints that might be resolved/transformed
|
|
state = createClusterState([]ClusterSlot{{
|
|
Nodes: []ClusterNode{{Addr: "redis-master.example.com:6379"}},
|
|
}, {
|
|
Nodes: []ClusterNode{{Addr: "redis-replica-1.example.com:6379"}},
|
|
}, {
|
|
Nodes: []ClusterNode{{Addr: "redis-replica-2.example.com:6379"}},
|
|
}})
|
|
})
|
|
|
|
It("preserves FQDN node addresses", func() {
|
|
slotNodeAddress := func(slot *clusterSlot) string {
|
|
return slot.nodes[0].Client.NodeAddress()
|
|
}
|
|
|
|
Expect(slotNodeAddress(state.slots[0])).To(Equal("redis-master.example.com:6379"))
|
|
Expect(slotNodeAddress(state.slots[1])).To(Equal("redis-replica-1.example.com:6379"))
|
|
Expect(slotNodeAddress(state.slots[2])).To(Equal("redis-replica-2.example.com:6379"))
|
|
})
|
|
})
|
|
|
|
Describe("NodeAddress with multiple nodes per slot", func() {
|
|
BeforeEach(func() {
|
|
state = createClusterState([]ClusterSlot{{
|
|
Start: 0,
|
|
End: 5460,
|
|
Nodes: []ClusterNode{
|
|
{Addr: "127.0.0.1:7001"}, // master
|
|
{Addr: "127.0.0.1:7004"}, // replica
|
|
},
|
|
}, {
|
|
Start: 5461,
|
|
End: 10922,
|
|
Nodes: []ClusterNode{
|
|
{Addr: "master-2.redis.local:6379"}, // master
|
|
{Addr: "replica-2.redis.local:6379"}, // replica
|
|
},
|
|
}})
|
|
})
|
|
|
|
It("preserves node addresses for all nodes in a slot", func() {
|
|
// First slot - loopback addresses
|
|
slot0 := state.slots[0]
|
|
Expect(slot0.nodes[0].Client.NodeAddress()).To(Equal("127.0.0.1:7001"))
|
|
Expect(slot0.nodes[1].Client.NodeAddress()).To(Equal("127.0.0.1:7004"))
|
|
|
|
// Verify Addr is transformed for loopback
|
|
Expect(slot0.nodes[0].Client.Options().Addr).To(Equal("10.10.10.10:7001"))
|
|
Expect(slot0.nodes[1].Client.Options().Addr).To(Equal("10.10.10.10:7004"))
|
|
|
|
// Second slot - FQDN addresses (no transformation)
|
|
slot1 := state.slots[1]
|
|
Expect(slot1.nodes[0].Client.NodeAddress()).To(Equal("master-2.redis.local:6379"))
|
|
Expect(slot1.nodes[1].Client.NodeAddress()).To(Equal("replica-2.redis.local:6379"))
|
|
|
|
// Verify Addr is same as NodeAddress for non-loopback
|
|
Expect(slot1.nodes[0].Client.Options().Addr).To(Equal("master-2.redis.local:6379"))
|
|
Expect(slot1.nodes[1].Client.Options().Addr).To(Equal("replica-2.redis.local:6379"))
|
|
})
|
|
})
|
|
})
|
|
|
|
// TestNodeAddress tests that NodeAddress is correctly preserved
|
|
// when cluster nodes are created from ClusterSlots responses.
|
|
func TestNodeAddress(t *testing.T) {
|
|
t.Run("preserves node address when loopback is replaced", func(t *testing.T) {
|
|
opt := &ClusterOptions{}
|
|
opt.init()
|
|
nodes := newClusterNodes(opt)
|
|
defer nodes.Close()
|
|
|
|
// Create cluster state with loopback addresses
|
|
// Origin is non-loopback, so loopback addresses will be replaced
|
|
slots := []ClusterSlot{{
|
|
Start: 0,
|
|
End: 5460,
|
|
Nodes: []ClusterNode{{Addr: "127.0.0.1:7001"}},
|
|
}, {
|
|
Start: 5461,
|
|
End: 10922,
|
|
Nodes: []ClusterNode{{Addr: "127.0.0.1:7002"}},
|
|
}}
|
|
|
|
state, err := newClusterState(nodes, slots, "10.10.10.10:1234")
|
|
if err != nil {
|
|
t.Fatalf("newClusterState failed: %v", err)
|
|
}
|
|
|
|
// Verify Addr is transformed (loopback replaced with origin host)
|
|
if got := state.slots[0].nodes[0].Client.Options().Addr; got != "10.10.10.10:7001" {
|
|
t.Errorf("Addr = %q, want %q", got, "10.10.10.10:7001")
|
|
}
|
|
if got := state.slots[1].nodes[0].Client.Options().Addr; got != "10.10.10.10:7002" {
|
|
t.Errorf("Addr = %q, want %q", got, "10.10.10.10:7002")
|
|
}
|
|
|
|
// Verify NodeAddress is preserved (original from ClusterSlots)
|
|
if got := state.slots[0].nodes[0].Client.NodeAddress(); got != "127.0.0.1:7001" {
|
|
t.Errorf("NodeAddress = %q, want %q", got, "127.0.0.1:7001")
|
|
}
|
|
if got := state.slots[1].nodes[0].Client.NodeAddress(); got != "127.0.0.1:7002" {
|
|
t.Errorf("NodeAddress = %q, want %q", got, "127.0.0.1:7002")
|
|
}
|
|
})
|
|
|
|
t.Run("preserves FQDN node addresses", func(t *testing.T) {
|
|
opt := &ClusterOptions{}
|
|
opt.init()
|
|
nodes := newClusterNodes(opt)
|
|
defer nodes.Close()
|
|
|
|
slots := []ClusterSlot{{
|
|
Start: 0,
|
|
End: 5460,
|
|
Nodes: []ClusterNode{{Addr: "redis-master.example.com:6379"}},
|
|
}, {
|
|
Start: 5461,
|
|
End: 10922,
|
|
Nodes: []ClusterNode{{Addr: "redis-replica.example.com:6379"}},
|
|
}}
|
|
|
|
state, err := newClusterState(nodes, slots, "10.10.10.10:1234")
|
|
if err != nil {
|
|
t.Fatalf("newClusterState failed: %v", err)
|
|
}
|
|
|
|
// For non-loopback addresses, Addr and NodeAddress should be the same
|
|
if got := state.slots[0].nodes[0].Client.Options().Addr; got != "redis-master.example.com:6379" {
|
|
t.Errorf("Addr = %q, want %q", got, "redis-master.example.com:6379")
|
|
}
|
|
if got := state.slots[0].nodes[0].Client.NodeAddress(); got != "redis-master.example.com:6379" {
|
|
t.Errorf("NodeAddress = %q, want %q", got, "redis-master.example.com:6379")
|
|
}
|
|
|
|
if got := state.slots[1].nodes[0].Client.Options().Addr; got != "redis-replica.example.com:6379" {
|
|
t.Errorf("Addr = %q, want %q", got, "redis-replica.example.com:6379")
|
|
}
|
|
if got := state.slots[1].nodes[0].Client.NodeAddress(); got != "redis-replica.example.com:6379" {
|
|
t.Errorf("NodeAddress = %q, want %q", got, "redis-replica.example.com:6379")
|
|
}
|
|
})
|
|
|
|
t.Run("preserves node address for multiple nodes per slot", func(t *testing.T) {
|
|
opt := &ClusterOptions{}
|
|
opt.init()
|
|
nodes := newClusterNodes(opt)
|
|
defer nodes.Close()
|
|
|
|
slots := []ClusterSlot{{
|
|
Start: 0,
|
|
End: 5460,
|
|
Nodes: []ClusterNode{
|
|
{Addr: "127.0.0.1:7001"}, // master
|
|
{Addr: "127.0.0.1:7004"}, // replica
|
|
},
|
|
}}
|
|
|
|
state, err := newClusterState(nodes, slots, "10.10.10.10:1234")
|
|
if err != nil {
|
|
t.Fatalf("newClusterState failed: %v", err)
|
|
}
|
|
|
|
// Master node
|
|
if got := state.slots[0].nodes[0].Client.Options().Addr; got != "10.10.10.10:7001" {
|
|
t.Errorf("Master Addr = %q, want %q", got, "10.10.10.10:7001")
|
|
}
|
|
if got := state.slots[0].nodes[0].Client.NodeAddress(); got != "127.0.0.1:7001" {
|
|
t.Errorf("Master NodeAddress = %q, want %q", got, "127.0.0.1:7001")
|
|
}
|
|
|
|
// Replica node
|
|
if got := state.slots[0].nodes[1].Client.Options().Addr; got != "10.10.10.10:7004" {
|
|
t.Errorf("Replica Addr = %q, want %q", got, "10.10.10.10:7004")
|
|
}
|
|
if got := state.slots[0].nodes[1].Client.NodeAddress(); got != "127.0.0.1:7004" {
|
|
t.Errorf("Replica NodeAddress = %q, want %q", got, "127.0.0.1:7004")
|
|
}
|
|
})
|
|
|
|
t.Run("GetOrCreate without node address defaults to Addr", func(t *testing.T) {
|
|
opt := &ClusterOptions{}
|
|
opt.init()
|
|
nodes := newClusterNodes(opt)
|
|
defer nodes.Close()
|
|
|
|
// GetOrCreate without node address (e.g., from MOVED/ASK error)
|
|
node, err := nodes.GetOrCreate("10.10.10.10:7001")
|
|
if err != nil {
|
|
t.Fatalf("GetOrCreate failed: %v", err)
|
|
}
|
|
|
|
// NodeAddress should default to Addr when not provided
|
|
if got := node.Client.NodeAddress(); got != "10.10.10.10:7001" {
|
|
t.Errorf("NodeAddress = %q, want %q", got, "10.10.10.10:7001")
|
|
}
|
|
|
|
// Addr should be set correctly
|
|
if got := node.Client.Options().Addr; got != "10.10.10.10:7001" {
|
|
t.Errorf("Addr = %q, want %q", got, "10.10.10.10:7001")
|
|
}
|
|
})
|
|
|
|
t.Run("GetOrCreateWithNodeAddress sets node address", func(t *testing.T) {
|
|
opt := &ClusterOptions{}
|
|
opt.init()
|
|
nodes := newClusterNodes(opt)
|
|
defer nodes.Close()
|
|
|
|
// GetOrCreateWithNodeAddress with node address
|
|
node, err := nodes.GetOrCreateWithNodeAddress("10.10.10.10:7001", "127.0.0.1:7001")
|
|
if err != nil {
|
|
t.Fatalf("GetOrCreateWithNodeAddress failed: %v", err)
|
|
}
|
|
|
|
// NodeAddress should be set
|
|
if got := node.Client.NodeAddress(); got != "127.0.0.1:7001" {
|
|
t.Errorf("NodeAddress = %q, want %q", got, "127.0.0.1:7001")
|
|
}
|
|
|
|
// Addr should be the transformed address
|
|
if got := node.Client.Options().Addr; got != "10.10.10.10:7001" {
|
|
t.Errorf("Addr = %q, want %q", got, "10.10.10.10:7001")
|
|
}
|
|
})
|
|
}
|
|
|
|
type fixedHash string
|
|
|
|
func (h fixedHash) Get(string) string {
|
|
return string(h)
|
|
}
|
|
|
|
func TestRingSetAddrsAndRebalanceRace(t *testing.T) {
|
|
const (
|
|
ringShard1Name = "ringShardOne"
|
|
ringShard2Name = "ringShardTwo"
|
|
|
|
ringShard1Port = "6390"
|
|
ringShard2Port = "6391"
|
|
)
|
|
|
|
ring := NewRing(&RingOptions{
|
|
Addrs: map[string]string{
|
|
ringShard1Name: ":" + ringShard1Port,
|
|
},
|
|
// Disable heartbeat
|
|
HeartbeatFrequency: 1 * time.Hour,
|
|
NewConsistentHash: func(shards []string) ConsistentHash {
|
|
switch len(shards) {
|
|
case 1:
|
|
return fixedHash(ringShard1Name)
|
|
case 2:
|
|
return fixedHash(ringShard2Name)
|
|
default:
|
|
t.Fatalf("Unexpected number of shards: %v", shards)
|
|
return nil
|
|
}
|
|
},
|
|
})
|
|
defer ring.Close()
|
|
|
|
// Continuously update addresses by adding and removing one address
|
|
updatesDone := make(chan struct{})
|
|
defer func() { close(updatesDone) }()
|
|
go func() {
|
|
for i := 0; ; i++ {
|
|
select {
|
|
case <-updatesDone:
|
|
return
|
|
default:
|
|
if i%2 == 0 {
|
|
ring.SetAddrs(map[string]string{
|
|
ringShard1Name: ":" + ringShard1Port,
|
|
})
|
|
} else {
|
|
ring.SetAddrs(map[string]string{
|
|
ringShard1Name: ":" + ringShard1Port,
|
|
ringShard2Name: ":" + ringShard2Port,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
timer := time.NewTimer(1 * time.Second)
|
|
for running := true; running; {
|
|
select {
|
|
case <-timer.C:
|
|
running = false
|
|
default:
|
|
shard, err := ring.sharding.GetByKey("whatever")
|
|
if err == nil && shard == nil {
|
|
t.Fatal("shard is nil")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkRingShardingRebalanceLocked(b *testing.B) {
|
|
opts := &RingOptions{
|
|
Addrs: make(map[string]string),
|
|
// Disable heartbeat
|
|
HeartbeatFrequency: 1 * time.Hour,
|
|
}
|
|
for i := 0; i < 100; i++ {
|
|
opts.Addrs[fmt.Sprintf("shard%d", i)] = fmt.Sprintf(":63%02d", i)
|
|
}
|
|
|
|
ring := NewRing(opts)
|
|
defer ring.Close()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
ring.sharding.rebalanceLocked()
|
|
}
|
|
}
|
|
|
|
type testCounter struct {
|
|
mu sync.Mutex
|
|
t *testing.T
|
|
m map[string]int
|
|
}
|
|
|
|
func newTestCounter(t *testing.T) *testCounter {
|
|
return &testCounter{t: t, m: make(map[string]int)}
|
|
}
|
|
|
|
func (ct *testCounter) increment(key string) {
|
|
ct.mu.Lock()
|
|
defer ct.mu.Unlock()
|
|
ct.m[key]++
|
|
}
|
|
|
|
func (ct *testCounter) expect(values map[string]int) {
|
|
ct.mu.Lock()
|
|
defer ct.mu.Unlock()
|
|
ct.t.Helper()
|
|
if !reflect.DeepEqual(values, ct.m) {
|
|
ct.t.Errorf("expected %v != actual %v", values, ct.m)
|
|
}
|
|
}
|
|
|
|
func TestRingShardsCleanup(t *testing.T) {
|
|
const (
|
|
ringShard1Name = "ringShardOne"
|
|
ringShard2Name = "ringShardTwo"
|
|
|
|
ringShard1Addr = "shard1.test"
|
|
ringShard2Addr = "shard2.test"
|
|
)
|
|
|
|
t.Run("closes unused shards", func(t *testing.T) {
|
|
closeCounter := newTestCounter(t)
|
|
|
|
ring := NewRing(&RingOptions{
|
|
Addrs: map[string]string{
|
|
ringShard1Name: ringShard1Addr,
|
|
ringShard2Name: ringShard2Addr,
|
|
},
|
|
NewClient: func(opt *Options) *Client {
|
|
c := NewClient(opt)
|
|
c.baseClient.onClose = c.baseClient.wrappedOnClose(func() error {
|
|
closeCounter.increment(opt.Addr)
|
|
return nil
|
|
})
|
|
return c
|
|
},
|
|
})
|
|
closeCounter.expect(map[string]int{})
|
|
|
|
// no change due to the same addresses
|
|
ring.SetAddrs(map[string]string{
|
|
ringShard1Name: ringShard1Addr,
|
|
ringShard2Name: ringShard2Addr,
|
|
})
|
|
closeCounter.expect(map[string]int{})
|
|
|
|
ring.SetAddrs(map[string]string{
|
|
ringShard1Name: ringShard1Addr,
|
|
})
|
|
closeCounter.expect(map[string]int{ringShard2Addr: 1})
|
|
|
|
ring.SetAddrs(map[string]string{
|
|
ringShard2Name: ringShard2Addr,
|
|
})
|
|
closeCounter.expect(map[string]int{ringShard1Addr: 1, ringShard2Addr: 1})
|
|
|
|
ring.Close()
|
|
closeCounter.expect(map[string]int{ringShard1Addr: 1, ringShard2Addr: 2})
|
|
})
|
|
|
|
t.Run("closes created shards if ring was closed", func(t *testing.T) {
|
|
createCounter := newTestCounter(t)
|
|
closeCounter := newTestCounter(t)
|
|
|
|
var (
|
|
ring *Ring
|
|
shouldClose int32
|
|
)
|
|
|
|
ring = NewRing(&RingOptions{
|
|
Addrs: map[string]string{
|
|
ringShard1Name: ringShard1Addr,
|
|
},
|
|
NewClient: func(opt *Options) *Client {
|
|
if atomic.LoadInt32(&shouldClose) != 0 {
|
|
ring.Close()
|
|
}
|
|
createCounter.increment(opt.Addr)
|
|
c := NewClient(opt)
|
|
c.baseClient.onClose = c.baseClient.wrappedOnClose(func() error {
|
|
closeCounter.increment(opt.Addr)
|
|
return nil
|
|
})
|
|
return c
|
|
},
|
|
})
|
|
createCounter.expect(map[string]int{ringShard1Addr: 1})
|
|
closeCounter.expect(map[string]int{})
|
|
|
|
atomic.StoreInt32(&shouldClose, 1)
|
|
|
|
ring.SetAddrs(map[string]string{
|
|
ringShard2Name: ringShard2Addr,
|
|
})
|
|
createCounter.expect(map[string]int{ringShard1Addr: 1, ringShard2Addr: 1})
|
|
closeCounter.expect(map[string]int{ringShard1Addr: 1, ringShard2Addr: 1})
|
|
})
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
type timeoutErr struct {
|
|
error
|
|
}
|
|
|
|
func (e timeoutErr) Timeout() bool {
|
|
return true
|
|
}
|
|
|
|
func (e timeoutErr) Temporary() bool {
|
|
return true
|
|
}
|
|
|
|
func (e timeoutErr) Error() string {
|
|
return "i/o timeout"
|
|
}
|
|
|
|
var _ = Describe("withConn", func() {
|
|
var client *Client
|
|
|
|
BeforeEach(func() {
|
|
client = NewClient(&Options{
|
|
PoolSize: 1,
|
|
})
|
|
})
|
|
|
|
AfterEach(func() {
|
|
client.Close()
|
|
})
|
|
|
|
It("should replace the connection in the pool when there is no error", func() {
|
|
var conn *pool.Conn
|
|
|
|
client.withConn(ctx, func(ctx context.Context, c *pool.Conn) error {
|
|
conn = c
|
|
return nil
|
|
})
|
|
|
|
newConn, err := client.connPool.Get(ctx)
|
|
Expect(err).To(BeNil())
|
|
Expect(newConn).To(Equal(conn))
|
|
})
|
|
|
|
It("should replace the connection in the pool when there is an error not related to a bad connection", func() {
|
|
var conn *pool.Conn
|
|
|
|
client.withConn(ctx, func(ctx context.Context, c *pool.Conn) error {
|
|
conn = c
|
|
return proto.RedisError("LOADING")
|
|
})
|
|
|
|
newConn, err := client.connPool.Get(ctx)
|
|
Expect(err).To(BeNil())
|
|
Expect(newConn).To(Equal(conn))
|
|
})
|
|
|
|
It("should remove the connection from the pool when it times out", func() {
|
|
var conn *pool.Conn
|
|
|
|
client.withConn(ctx, func(ctx context.Context, c *pool.Conn) error {
|
|
conn = c
|
|
return timeoutErr{}
|
|
})
|
|
|
|
newConn, err := client.connPool.Get(ctx)
|
|
Expect(err).To(BeNil())
|
|
Expect(newConn).NotTo(Equal(conn))
|
|
Expect(client.connPool.Len()).To(Equal(1))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("ClusterClient", func() {
|
|
var client *ClusterClient
|
|
|
|
BeforeEach(func() {
|
|
client = &ClusterClient{}
|
|
})
|
|
|
|
Describe("cmdSlot", func() {
|
|
It("select slot from args for GETKEYSINSLOT command", func() {
|
|
cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", 100, 200)
|
|
|
|
slot := client.cmdSlot(cmd, -1)
|
|
Expect(slot).To(Equal(100))
|
|
})
|
|
|
|
It("select slot from args for COUNTKEYSINSLOT command", func() {
|
|
cmd := NewStringSliceCmd(ctx, "cluster", "countkeysinslot", 100)
|
|
|
|
slot := client.cmdSlot(cmd, -1)
|
|
Expect(slot).To(Equal(100))
|
|
})
|
|
|
|
It("follows preferred random slot", func() {
|
|
cmd := NewStatusCmd(ctx, "ping")
|
|
|
|
slot := client.cmdSlot(cmd, 101)
|
|
Expect(slot).To(Equal(101))
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("isLoopback", func() {
|
|
DescribeTable("should correctly identify loopback addresses",
|
|
func(host string, expected bool) {
|
|
result := isLoopback(host)
|
|
Expect(result).To(Equal(expected))
|
|
},
|
|
// IP addresses
|
|
Entry("IPv4 loopback", "127.0.0.1", true),
|
|
Entry("IPv6 loopback", "::1", true),
|
|
Entry("IPv4 non-loopback", "192.168.1.1", false),
|
|
Entry("IPv6 non-loopback", "2001:db8::1", false),
|
|
|
|
// Well-known loopback hostnames
|
|
Entry("localhost lowercase", "localhost", true),
|
|
Entry("localhost uppercase", "LOCALHOST", true),
|
|
Entry("localhost mixed case", "LocalHost", true),
|
|
|
|
// Docker-specific loopbacks
|
|
Entry("host.docker.internal", "host.docker.internal", true),
|
|
Entry("HOST.DOCKER.INTERNAL", "HOST.DOCKER.INTERNAL", true),
|
|
Entry("custom.docker.internal", "custom.docker.internal", true),
|
|
Entry("app.docker.internal", "app.docker.internal", true),
|
|
|
|
// Non-loopback hostnames
|
|
Entry("redis hostname", "redis-cluster", false),
|
|
Entry("FQDN", "redis.example.com", false),
|
|
Entry("docker but not internal", "redis.docker.com", false),
|
|
|
|
// Edge cases
|
|
Entry("empty string", "", false),
|
|
Entry("invalid IP", "256.256.256.256", false),
|
|
Entry("partial docker internal", "docker.internal", false),
|
|
)
|
|
})
|