mirror of
https://github.com/redis/go-redis.git
synced 2026-10-22 19:49:00 +03:00
fix(probabilistic): tolerate new fields in *.INFO replies (#4010)
* fix(probabilistic): tolerate new fields in *.INFO replies Redis 8.12 adds "cell size" to CMS.INFO (the new CELL_SIZE option for CMS.INITBYDIM / CMS.INITBYPROB). The parser rejected any unknown key, so CMSInfo failed against the unstable image with "redis: CMS.INFO unexpected key cell size". Expose the new value as CMSInfo.CellSize and, as XINFO already does, drain and skip unknown map fields instead of erroring. Apply the same forward-compatible default to BF.INFO, CF.INFO, TOPK.INFO and TDIGEST.INFO. BF.INFO with an explicit unknown attribute still errors, since that is a caller mistake. * Changes field name to updated value
This commit is contained in:
@@ -350,10 +350,15 @@ func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
"EXPANSION": &result.ExpansionRate,
|
||||
}
|
||||
|
||||
// Helper function to read and assign a value based on the key
|
||||
readAndAssignValue := func(key string) error {
|
||||
// Helper function to read and assign a value based on the key.
|
||||
// Unknown keys are drained and skipped when skipUnknown is set so that
|
||||
// fields added by newer servers don't break the parser.
|
||||
readAndAssignValue := func(key string, skipUnknown bool) error {
|
||||
fieldPtr, exists := respMapping[key]
|
||||
if !exists {
|
||||
if skipUnknown {
|
||||
return rd.DiscardNext()
|
||||
}
|
||||
return fmt.Errorf("redis: BLOOM.INFO unexpected key %s", key)
|
||||
}
|
||||
|
||||
@@ -377,7 +382,7 @@ func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
if key, ok := cmd.args[2].(string); ok && n == 1 {
|
||||
if err := readAndAssignValue(key); err != nil {
|
||||
if err := readAndAssignValue(key, false); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -393,7 +398,7 @@ func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := readAndAssignValue(key); err != nil {
|
||||
if err := readAndAssignValue(key, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -706,7 +711,8 @@ func (cmd *CFInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
result.MaxIteration, err = rd.ReadInt()
|
||||
|
||||
default:
|
||||
return fmt.Errorf("redis: CF.INFO unexpected key %s", key)
|
||||
// skip unknown fields so newer servers don't break the parser
|
||||
err = rd.DiscardNext()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -809,6 +815,10 @@ type CMSInfo struct {
|
||||
Width int64
|
||||
Depth int64
|
||||
Count int64
|
||||
// CellSize is the size in bytes of each counter (1, 2, 4 or 8).
|
||||
// Reported since Redis 8.12, alongside the CELL_SIZE option of
|
||||
// CMS.INITBYDIM / CMS.INITBYPROB; zero on older servers.
|
||||
CellSize int64
|
||||
}
|
||||
|
||||
type CMSInfoCmd struct {
|
||||
@@ -867,8 +877,11 @@ func (cmd *CMSInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
result.Depth, err = rd.ReadInt()
|
||||
case "count":
|
||||
result.Count, err = rd.ReadInt()
|
||||
case "cell_size":
|
||||
result.CellSize, err = rd.ReadInt()
|
||||
default:
|
||||
return fmt.Errorf("redis: CMS.INFO unexpected key %s", key)
|
||||
// skip unknown fields so newer servers don't break the parser
|
||||
err = rd.DiscardNext()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -1074,7 +1087,8 @@ func (cmd *TopKInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
case "decay":
|
||||
result.Decay, err = rd.ReadFloat()
|
||||
default:
|
||||
return fmt.Errorf("redis: topk.info unexpected key %s", key)
|
||||
// skip unknown fields so newer servers don't break the parser
|
||||
err = rd.DiscardNext()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -1342,7 +1356,8 @@ func (cmd *TDigestInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
case "Memory usage":
|
||||
result.MemoryUsage, err = rd.ReadInt()
|
||||
default:
|
||||
return fmt.Errorf("redis: tdigest.info unexpected key %s", key)
|
||||
// skip unknown fields so newer servers don't break the parser
|
||||
err = rd.DiscardNext()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -482,6 +482,17 @@ var _ = Describe("Probabilistic commands", Label("probabilistic"), func() {
|
||||
Expect(info.Depth).To(BeEquivalentTo(int64(10)))
|
||||
})
|
||||
|
||||
It("should report CMSInfo cell size", Label("cms", "cmsinfo"), func() {
|
||||
SkipBeforeRedisVersion("8.11", "CMS.INFO reports cell_size since Redis 8.12 (8.11 pre-release)")
|
||||
|
||||
err := client.CMSInitByDim(ctx, "testcms1", 5, 10).Err()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
info, err := client.CMSInfo(ctx, "testcms1").Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(info.CellSize).To(BeEquivalentTo(int64(4)))
|
||||
})
|
||||
|
||||
It("should CMSInitByProb", Label("cms", "cmsinitbyprob"), func() {
|
||||
err := client.CMSInitByProb(ctx, "testcms1", 0.002, 0.01).Err()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
93
probabilistic_unit_test.go
Normal file
93
probabilistic_unit_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
)
|
||||
|
||||
// The probabilistic *.INFO replies are open-ended maps; newer servers add
|
||||
// fields (e.g. CMS.INFO "cell_size" in Redis 8.12). Unknown fields must be
|
||||
// drained so the reader stays aligned with the following reply instead of
|
||||
// failing the whole command.
|
||||
|
||||
func TestCMSInfoCmdParsesCellSizeAndIgnoresUnknownField(t *testing.T) {
|
||||
reply := "%5\r\n" +
|
||||
"+width\r\n:2000\r\n" +
|
||||
"+depth\r\n:5\r\n" +
|
||||
"+count\r\n:0\r\n" +
|
||||
"+cell_size\r\n:4\r\n" +
|
||||
"+new-cms-field\r\n$5\r\nhello\r\n" // unknown field, string value
|
||||
|
||||
cmd := NewCMSInfoCmd(context.Background())
|
||||
rd := proto.NewReader(strings.NewReader(reply + "+NEXT\r\n"))
|
||||
if err := cmd.readReply(rd); err != nil {
|
||||
t.Fatalf("readReply() returned unexpected error: %v", err)
|
||||
}
|
||||
want := CMSInfo{Width: 2000, Depth: 5, Count: 0, CellSize: 4}
|
||||
if got := cmd.Val(); got != want {
|
||||
t.Fatalf("CMSInfo = %+v, want %+v", got, want)
|
||||
}
|
||||
if next, err := rd.ReadString(); err != nil || next != "NEXT" {
|
||||
t.Fatalf("stream desynced: next=%q err=%v", next, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbabilisticInfoCmdsIgnoreUnknownField(t *testing.T) {
|
||||
// unknown field with a nested (array) value, to exercise a full discard
|
||||
unknown := "+new-field\r\n*2\r\n:1\r\n$3\r\nfoo\r\n"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reply string
|
||||
cmd interface {
|
||||
Cmder
|
||||
readReply(rd *proto.Reader) error
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "BF.INFO",
|
||||
reply: "%2\r\n+Capacity\r\n:100\r\n" + unknown,
|
||||
cmd: NewBFInfoCmd(context.Background(), "bf.info", "key"),
|
||||
},
|
||||
{
|
||||
name: "CF.INFO",
|
||||
reply: "%2\r\n+Size\r\n:100\r\n" + unknown,
|
||||
cmd: NewCFInfoCmd(context.Background()),
|
||||
},
|
||||
{
|
||||
name: "TOPK.INFO",
|
||||
reply: "%2\r\n+k\r\n:3\r\n" + unknown,
|
||||
cmd: NewTopKInfoCmd(context.Background()),
|
||||
},
|
||||
{
|
||||
name: "TDIGEST.INFO",
|
||||
reply: "%2\r\n+Compression\r\n:100\r\n" + unknown,
|
||||
cmd: NewTDigestInfoCmd(context.Background()),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rd := proto.NewReader(strings.NewReader(tc.reply + "+NEXT\r\n"))
|
||||
if err := tc.cmd.readReply(rd); err != nil {
|
||||
t.Fatalf("readReply() returned unexpected error: %v", err)
|
||||
}
|
||||
if next, err := rd.ReadString(); err != nil || next != "NEXT" {
|
||||
t.Fatalf("stream desynced: next=%q err=%v", next, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Asking BF.INFO for a specific unknown attribute is a caller error and must
|
||||
// still be reported rather than silently skipped.
|
||||
func TestBFInfoCmdSingleUnknownAttributeStillErrors(t *testing.T) {
|
||||
cmd := NewBFInfoCmd(context.Background(), "bf.info", "key", "BOGUS")
|
||||
rd := proto.NewReader(strings.NewReader("*1\r\n:1\r\n"))
|
||||
if err := cmd.readReply(rd); err == nil {
|
||||
t.Fatal("readReply() = nil, want error for unknown attribute")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user