Repository navigation
Expand file tree
/
Copy pathcommand_nil_element_desync_test.go
More file actions
205 lines (191 loc) · 7.62 KB
/
Copy pathcommand_nil_element_desync_test.go
File metadata and controls
205 lines (191 loc) · 7.62 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
package redis
import (
"context"
"slices"
"strings"
"testing"
"github.com/redis/go-redis/v9/internal/proto"
)
// A flat scalar array can carry a nil in a non-final position. BITFIELD with
// OVERFLOW FAIL returns nil for an operation that would overflow, e.g.
// `BITFIELD k OVERFLOW FAIL INCRBY u8 0 255 INCRBY u8 0 255 GET u8 0` replies
// [255, nil, 255]. IntSliceCmd, UintSliceCmd and BoolSliceCmd used to return on
// the nil element, leaving the trailing elements on the wire and desyncing the
// pooled connection so the next command read them as its reply. They must
// consume the whole array like FloatSliceCmd and StringSliceCmd already do.
func TestSliceCmdNilElementNoDesync(t *testing.T) {
// Each reply is [ v, nil, v ] followed by the next command's reply :999.
// A correct parser consumes all three elements and leaves :999 for the next read.
t.Run("IntSliceCmd", func(t *testing.T) {
rd := proto.NewReader(strings.NewReader("*3\r\n:255\r\n_\r\n:100\r\n:999\r\n"))
cmd := NewIntSliceCmd(context.Background())
if err := cmd.readReply(rd); err != nil {
t.Fatalf("readReply: %v", err)
}
if got := cmd.val; len(got) != 3 || got[0] != 255 || got[1] != 0 || got[2] != 100 {
t.Fatalf("unexpected val: %+v", got)
}
assertNextReplyInt(t, rd, 999)
})
t.Run("UintSliceCmd", func(t *testing.T) {
rd := proto.NewReader(strings.NewReader("*3\r\n:255\r\n_\r\n:100\r\n:999\r\n"))
cmd := NewUintSliceCmd(context.Background())
if err := cmd.readReply(rd); err != nil {
t.Fatalf("readReply: %v", err)
}
if got := cmd.val; len(got) != 3 || got[0] != 255 || got[1] != 0 || got[2] != 100 {
t.Fatalf("unexpected val: %+v", got)
}
assertNextReplyInt(t, rd, 999)
})
t.Run("BoolSliceCmd", func(t *testing.T) {
rd := proto.NewReader(strings.NewReader("*3\r\n:1\r\n_\r\n:1\r\n:999\r\n"))
cmd := NewBoolSliceCmd(context.Background())
if err := cmd.readReply(rd); err != nil {
t.Fatalf("readReply: %v", err)
}
if got := cmd.val; len(got) != 3 || !got[0] || got[1] || !got[2] {
t.Fatalf("unexpected val: %+v", got)
}
assertNextReplyInt(t, rd, 999)
})
}
// An error element in a non-final position is a complete frame, e.g. EVAL with
// `return {'a', redis.error_reply('boom'), 'c'}`. UintSliceCmd, FloatSliceCmd,
// StringSliceCmd and BoolSliceCmd used to return on it with the trailing
// elements still on the wire, so in a pipeline the next command read them as
// its reply. They must consume the whole array and return the first error.
func TestSliceCmdErrorElementNoDesync(t *testing.T) {
t.Run("UintSliceCmd", func(t *testing.T) {
rd := proto.NewReader(strings.NewReader("*3\r\n:255\r\n-ERR item failed\r\n:100\r\n:999\r\n"))
cmd := NewUintSliceCmd(context.Background())
if err := cmd.readReply(rd); err == nil || err.Error() != "ERR item failed" {
t.Fatalf("readReply err = %v, want ERR item failed", err)
}
if got := cmd.val; len(got) != 3 || got[0] != 255 || got[1] != 0 || got[2] != 100 {
t.Fatalf("unexpected val: %+v", got)
}
assertNextReplyInt(t, rd, 999)
})
t.Run("FloatSliceCmd", func(t *testing.T) {
rd := proto.NewReader(strings.NewReader("*3\r\n,1.5\r\n!15\r\nERR item failed\r\n,2.5\r\n:999\r\n"))
cmd := NewFloatSliceCmd(context.Background())
if err := cmd.readReply(rd); err == nil || err.Error() != "ERR item failed" {
t.Fatalf("readReply err = %v, want ERR item failed", err)
}
if got := cmd.val; len(got) != 3 || got[0] != 1.5 || got[1] != 0 || got[2] != 2.5 {
t.Fatalf("unexpected val: %+v", got)
}
assertNextReplyInt(t, rd, 999)
})
t.Run("StringSliceCmd", func(t *testing.T) {
rd := proto.NewReader(strings.NewReader("*4\r\n$1\r\na\r\n-ERR first\r\n-ERR second\r\n$1\r\nc\r\n:999\r\n"))
cmd := NewStringSliceCmd(context.Background())
if err := cmd.readReply(rd); err == nil || err.Error() != "ERR first" {
t.Fatalf("readReply err = %v, want ERR first", err)
}
if got := cmd.val; len(got) != 4 || got[0] != "a" || got[1] != "" || got[2] != "" || got[3] != "c" {
t.Fatalf("unexpected val: %+v", got)
}
assertNextReplyInt(t, rd, 999)
})
t.Run("BoolSliceCmd", func(t *testing.T) {
rd := proto.NewReader(strings.NewReader("*3\r\n:1\r\n-ERR item failed\r\n:1\r\n:999\r\n"))
cmd := NewBoolSliceCmd(context.Background())
if err := cmd.readReply(rd); err == nil || err.Error() != "ERR item failed" {
t.Fatalf("readReply err = %v, want ERR item failed", err)
}
if got := cmd.val; len(got) != 3 || !got[0] || got[1] || !got[2] {
t.Fatalf("unexpected val: %+v", got)
}
assertNextReplyInt(t, rd, 999)
})
t.Run("TruncatedReply", func(t *testing.T) {
rd := proto.NewReader(strings.NewReader("*3\r\n$1\r\na\r\n-ERR item failed\r\n"))
cmd := NewStringSliceCmd(context.Background())
err := cmd.readReply(rd)
if err == nil || isRedisError(err) {
t.Fatalf("readReply err = %v, want the read error", err)
}
})
}
func assertNextReplyInt(t *testing.T, rd *proto.Reader, want int64) {
t.Helper()
got, err := rd.ReadInt()
if err != nil {
t.Fatalf("next reply read failed (connection desynced): %v", err)
}
if got != want {
t.Fatalf("connection desynced: next reply = %d, want %d", got, want)
}
}
// TS.MADD and CMS.INCRBY answer item by item: a rejected item becomes an error
// element and the items after it are still reported. IntSliceCmd used to
// return on the error element with the rest of the array unread. Callers take
// a Redis error from readReply as a fully read reply, so in a pipeline the
// next command was handed the leftover element and every later reply shifted
// by one. The first two replies are the ones Redis 8.10 sends.
func TestIntSliceCmdErrorElementNoDesync(t *testing.T) {
tests := []struct {
name string
reply string
wantVal []int64
wantErr string
}{
{
name: "TS.MADD sample older than retention",
reply: "*3\r\n:10000\r\n-ERR TSDB: Timestamp is older than retention\r\n:10001\r\n",
wantVal: []int64{10000, 0, 10001},
wantErr: "ERR TSDB: Timestamp is older than retention",
},
{
name: "CMS.INCRBY counter overflow",
reply: "*4\r\n:4294967294\r\n:1\r\n-CMS: INCRBY overflow\r\n:2\r\n",
wantVal: []int64{4294967294, 1, 0, 2},
wantErr: "CMS: INCRBY overflow",
},
{
name: "first of several errors is reported",
reply: "*4\r\n-ERR first\r\n:7\r\n-ERR second\r\n:8\r\n",
wantVal: []int64{0, 7, 0, 8},
wantErr: "ERR first",
},
{
name: "RESP3 blob error element",
reply: "*3\r\n:1\r\n!9\r\nERR boom!\r\n:3\r\n",
wantVal: []int64{1, 0, 3},
wantErr: "ERR boom!",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// The reply is followed by the next command's reply, :999.
rd := proto.NewReader(strings.NewReader(tt.reply + ":999\r\n"))
cmd := NewIntSliceCmd(context.Background())
err := cmd.readReply(rd)
if err == nil || err.Error() != tt.wantErr {
t.Fatalf("readReply err = %v, want %q", err, tt.wantErr)
}
if !isRedisError(err) {
t.Fatalf("readReply err = %T, want a Redis error", err)
}
if !slices.Equal(cmd.val, tt.wantVal) {
t.Fatalf("unexpected val: %+v, want %+v", cmd.val, tt.wantVal)
}
assertNextReplyInt(t, rd, 999)
})
}
}
// A read error in the middle of the array is not an error element: the reply
// is incomplete and readReply must fail with it so the connection is dropped.
func TestIntSliceCmdErrorElementTruncatedReply(t *testing.T) {
rd := proto.NewReader(strings.NewReader("*3\r\n:10000\r\n-ERR TSDB: Timestamp is older than retention\r\n"))
cmd := NewIntSliceCmd(context.Background())
err := cmd.readReply(rd)
if err == nil {
t.Fatal("readReply returned nil for a truncated reply")
}
if isRedisError(err) {
t.Fatalf("readReply err = %v, want the read error, not the element error", err)
}
}