-
Notifications
You must be signed in to change notification settings - Fork 254
Expand file tree
/
Copy pathsentinel_refresh_test.go
More file actions
266 lines (245 loc) · 8.06 KB
/
Copy pathsentinel_refresh_test.go
File metadata and controls
266 lines (245 loc) · 8.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package rueidis
import (
"sync"
"sync/atomic"
"testing"
"time"
)
// TestSentinelTopologyRefreshIntervalRecoversMissedSwitch shows the client
// recovering from a missed +switch-master event.
//
// A subscribed client can still miss the event: the sentinel connection can
// drop at the same moment the master dies, and by the time a watch is
// re-established the +switch-master was already published. Redis PUB/SUB has no
// replay, so a subscriber that reconnects after the event never hears it.
// Without periodic reconciliation the client stays bound to the old master; the
// silent healthy watch in this mock reproduces that end state.
func TestSentinelTopologyRefreshIntervalRecoversMissedSwitch(t *testing.T) {
var switched atomic.Bool
var roleAsked atomic.Int64
// Sentinel answers with :1 first, then :2 — i.e. the master moved, but the
// client is never told via PUB/SUB.
s0 := &mockConn{
DoFn: func(cmd Completed) RedisResult { return RedisResult{} },
DoMultiFn: func(multi ...Completed) *redisresults {
addr := "1"
if switched.Load() {
addr = "2"
}
return &redisresults{s: []RedisResult{
{val: slicemsg('*', []RedisMessage{})},
{val: slicemsg('*', []RedisMessage{strmsg('+', ""), strmsg('+', addr)})},
}}
},
}
node := func(role string) *mockConn {
return &mockConn{
DoFn: func(cmd Completed) RedisResult {
roleAsked.Add(1)
return RedisResult{val: slicemsg('*', []RedisMessage{strmsg('+', role)})}
},
}
}
m1, m2 := node("master"), node("master")
client, err := newSentinelClient(
&ClientOption{
InitAddress: []string{":0"},
Sentinel: SentinelOption{TopologyRefreshInterval: 50 * time.Millisecond},
},
func(dst string, opt *ClientOption) conn {
switch dst {
case ":0":
return s0
case ":1":
return m1
case ":2":
return m2
}
return nil
},
newRetryer(defaultRetryDelayFn),
)
if err != nil {
t.Fatalf("unexpected err %v", err)
}
defer client.Close()
if got := client.mAddr.Load().(string); got != ":1" {
t.Fatalf("expected initial master :1, got %v", got)
}
// Master moves; NO +switch-master is delivered.
switched.Store(true)
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if client.mAddr.Load().(string) == ":2" {
return // reconciled without any event
}
time.Sleep(20 * time.Millisecond)
}
t.Fatal("client never reconciled to the new master :2 — with no periodic " +
"refresh a missed +switch-master pins it to the old master indefinitely")
}
func TestSentinelNegativeTopologyRefreshInterval(t *testing.T) {
_, err := newSentinelClient(
&ClientOption{
InitAddress: []string{":0"},
Sentinel: SentinelOption{TopologyRefreshInterval: -1},
},
func(dst string, opt *ClientOption) conn { return &mockConn{} },
newRetryer(defaultRetryDelayFn),
)
if err != ErrInvalidTopologyRefreshInterval {
t.Fatalf("expected ErrInvalidTopologyRefreshInterval, got %v", err)
}
}
// TestSentinelTopologyRefreshmentStopsOnClose pins that the background
// reconciler exits immediately on Close rather than on its next tick. With a
// multi-second interval, polling c.stop alone kept the goroutine alive well
// past shutdown — which the suite's leak detector caught, and which would be
// a real shutdown delay for an application closing its client (e.g. on
// graceful process restart).
func TestSentinelTopologyRefreshmentStopsOnClose(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
s0 := &mockConn{
DoFn: func(cmd Completed) RedisResult { return RedisResult{} },
DoMultiFn: func(multi ...Completed) *redisresults {
return &redisresults{s: []RedisResult{
{val: slicemsg('*', []RedisMessage{})},
{val: slicemsg('*', []RedisMessage{strmsg('+', ""), strmsg('+', "1")})},
}}
},
}
m := &mockConn{DoFn: func(cmd Completed) RedisResult {
return RedisResult{val: slicemsg('*', []RedisMessage{strmsg('+', "master")})}
}}
client, err := newSentinelClient(
&ClientOption{
InitAddress: []string{":0"},
// Far longer than the test: only prompt exit on Close can pass.
Sentinel: SentinelOption{TopologyRefreshInterval: time.Hour},
},
func(dst string, opt *ClientOption) conn {
if dst == ":0" {
return s0
}
return m
},
newRetryer(defaultRetryDelayFn),
)
if err != nil {
t.Fatalf("unexpected err %v", err)
}
client.Close()
// Close must be idempotent: closeCh is closed under a CAS on stop.
client.Close()
}
// TestSentinelTopologyRefreshKeepsReplica pins that reconciling does not move a
// client off a replica that is still healthy.
//
// Sentinel reports every replica and the client picks one. That pick used to be
// re-drawn at random on every refresh, which was almost invisible while refresh
// only ran on a sentinel event. On an interval it is not: _switchTarget closes
// the connection it replaces immediately, so with three replicas roughly two
// ticks in three tore down a working connection, and the requests in flight on
// it, for no reason at all. The replica must only change when the one in use
// stops being eligible.
//
// The ticks are performed by calling client.refresh() directly — the same call
// runTopologyRefreshment makes — rather than running a real ticker, so the
// count of reconciliations is exact.
func TestSentinelTopologyRefreshKeepsReplica(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
replicaAddrs := []string{":10", ":11", ":12"}
var down atomic.Value // address currently reported with s-down-time
down.Store("")
replicasReply := func() RedisResult {
out := make([]RedisMessage, 0, len(replicaAddrs))
for _, addr := range replicaAddrs {
fields := []RedisMessage{
strmsg('+', "ip"), strmsg('+', ""),
strmsg('+', "port"), strmsg('+', addr[1:]),
}
if addr == down.Load().(string) {
fields = append(fields, strmsg('+', "s-down-time"), strmsg('+', "1000"))
}
out = append(out, slicemsg('*', fields))
}
return RedisResult{val: slicemsg('*', out)}
}
s0 := &mockConn{
DoFn: func(cmd Completed) RedisResult { return RedisResult{} },
DoMultiFn: func(multi ...Completed) *redisresults {
return &redisresults{s: []RedisResult{
{val: slicemsg('*', []RedisMessage{})},
{val: slicemsg('*', []RedisMessage{strmsg('+', ""), strmsg('+', "1")})},
replicasReply(),
}}
},
}
var mu sync.Mutex
dials := map[string]int{}
closes := map[string]int{}
node := func(dst, role string) *mockConn {
return &mockConn{
DoFn: func(cmd Completed) RedisResult {
return RedisResult{val: slicemsg('*', []RedisMessage{strmsg('+', role)})}
},
CloseFn: func() {
mu.Lock()
defer mu.Unlock()
closes[dst]++
},
}
}
client, err := newSentinelClient(
&ClientOption{
InitAddress: []string{":0"},
SendToReplicas: func(cmd Completed) bool { return true },
},
func(dst string, opt *ClientOption) conn {
mu.Lock()
dials[dst]++
mu.Unlock()
switch dst {
case ":0":
return s0
case ":1":
return node(dst, "master")
}
return node(dst, "slave")
},
newRetryer(defaultRetryDelayFn),
)
if err != nil {
t.Fatalf("unexpected err %v", err)
}
defer client.Close()
first := client.rAddr.Load().(string)
for i := 0; i < 20; i++ {
if err := client.refresh(); err != nil {
t.Fatalf("refresh %d: %v", i, err)
}
if got := client.rAddr.Load().(string); got != first {
t.Fatalf("refresh %d moved the client from replica %s to %s while both were healthy", i, first, got)
}
}
mu.Lock()
replicaDials, replicaCloses := 0, 0
for _, addr := range replicaAddrs {
replicaDials += dials[addr]
replicaCloses += closes[addr]
}
mu.Unlock()
if replicaDials != 1 || replicaCloses != 0 {
t.Fatalf("20 reconciliations should touch no replica connection, got %d dials and %d closes",
replicaDials, replicaCloses)
}
// Stickiness must not outlive eligibility: once sentinel reports the replica
// in use as subjectively down, the client has to move to another one.
down.Store(first)
if err := client.refresh(); err != nil {
t.Fatalf("refresh after s-down: %v", err)
}
if got := client.rAddr.Load().(string); got == first {
t.Fatalf("client stayed on replica %s after sentinel reported it down", got)
}
}