-
-
Notifications
You must be signed in to change notification settings - Fork 121
Expand file tree
/
Copy pathredis_shard_integration_test.go
More file actions
171 lines (158 loc) · 4.76 KB
/
Copy pathredis_shard_integration_test.go
File metadata and controls
171 lines (158 loc) · 4.76 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
//go:build integration
package centrifuge
import (
"context"
"io"
"net"
"sync"
"testing"
"time"
"github.com/redis/rueidis"
"github.com/stretchr/testify/require"
)
func TestNewRedisShard(t *testing.T) {
testCases := []struct {
name string
config RedisShardConfig
expectedCluster bool
}{
{
name: "redis standalone",
config: RedisShardConfig{
Address: "redis://127.0.0.1:6379",
},
},
{
name: "redis cluster",
config: RedisShardConfig{
Address: "redis://127.0.0.1:7001",
},
expectedCluster: true,
},
{
name: "valkey standalone",
config: RedisShardConfig{
Address: "redis://127.0.0.1:16379",
},
},
{
name: "valkey cluster",
config: RedisShardConfig{
Address: "redis://127.0.0.1:17000",
},
expectedCluster: true,
},
{
name: "dragonfly standalone",
config: RedisShardConfig{
Address: "redis://127.0.0.1:36379",
},
},
{
name: "dragonfly cluster emulated",
config: RedisShardConfig{
Address: "redis://127.0.0.1:37000",
},
expectedCluster: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s, err := NewRedisShard(&Node{}, tc.config)
require.NoError(t, err)
require.Equal(t, tc.expectedCluster, s.isCluster)
})
}
}
// testTCPProxy forwards TCP connections to a real Redis, so a test can build
// a working shard and then take Redis "down" from the client's perspective by
// stopping the proxy: established connections break and new dials to the
// proxy port are refused — the same errors a crashed Redis produces.
type testTCPProxy struct {
ln net.Listener
upstream string
mu sync.Mutex
conns []net.Conn
}
func startTestTCPProxy(tb testing.TB, upstream string) *testTCPProxy {
tb.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(tb, err)
p := &testTCPProxy{ln: ln, upstream: upstream}
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
up, err := net.Dial("tcp", upstream)
if err != nil {
_ = c.Close()
continue
}
p.mu.Lock()
p.conns = append(p.conns, c, up)
p.mu.Unlock()
go func() { _, _ = io.Copy(up, c); _ = up.Close(); _ = c.Close() }()
go func() { _, _ = io.Copy(c, up); _ = c.Close(); _ = up.Close() }()
}
}()
return p
}
func (p *testTCPProxy) addr() string { return p.ln.Addr().String() }
// stop kills the proxy: no new connections, all existing ones broken.
func (p *testTCPProxy) stop() {
_ = p.ln.Close()
p.mu.Lock()
defer p.mu.Unlock()
for _, c := range p.conns {
_ = c.Close()
}
p.conns = nil
}
// TestRedisShardReadFailsFastDuringOutage pins that Redis commands issued
// without a context deadline fail within the IOTimeout envelope when Redis is
// unreachable — for read-only commands too.
//
// Centrifuge never passes per-request deadlines; every call is expected to
// fail within roughly IOTimeout when Redis is down. Writes and Lua scripts
// always behaved that way. Read-only commands did not: rueidis retries them
// on network errors until the context is done, and with context.Background()
// that means never — a presence-style read issued during an outage hung
// (retrying roughly once per second) until Redis came back. DisableRetry
// makes reads fail like writes. This test fails without it: the GET below
// keeps retrying against the dead address and does not return.
func TestRedisShardReadFailsFastDuringOutage(t *testing.T) {
proxy := startTestTCPProxy(t, "127.0.0.1:6379")
t.Cleanup(proxy.stop)
node := testNode(t)
t.Cleanup(func() { _ = node.Shutdown(context.Background()) })
s, err := NewRedisShard(node, RedisShardConfig{
Address: proxy.addr(),
IOTimeout: time.Second,
ConnectTimeout: time.Second,
})
require.NoError(t, err)
t.Cleanup(s.Close)
// Sanity: reads work through the proxy (rueidis.Nil for a missing key
// is a server reply, i.e. success at the transport level).
resp := s.client.Do(context.Background(), s.client.B().Get().Key("centrifuge-retry-test").Build())
if err := resp.Error(); err != nil {
require.True(t, rueidis.IsRedisNil(err), "unexpected error before outage: %v", err)
}
// Redis goes away.
proxy.stop()
// A read-only command with no context deadline must return an error
// within the IOTimeout envelope instead of retrying forever.
done := make(chan error, 1)
go func() {
done <- s.client.Do(context.Background(), s.client.B().Get().Key("centrifuge-retry-test").Build()).Error()
}()
select {
case err := <-done:
require.Error(t, err, "read against dead Redis must fail, not succeed")
require.False(t, rueidis.IsRedisNil(err), "expected a network error, got a server reply")
case <-time.After(5 * time.Second):
t.Fatal("read-only command did not return within 5s during Redis outage — client-level retries are hiding the failure")
}
}