From b2bb0e36959a2eec180b1a03321858a48d6badbc Mon Sep 17 00:00:00 2001 From: Samantha Date: Thu, 27 Jan 2022 20:06:47 -0800 Subject: [PATCH] Add a proper INFO style output unmarshaller (#25) - Replace `yaml.Unmarshal()` in the redis/client package with `unmarshalClusterInfo()` - Fix bad mock of the 'cluster info' output in redis/client package tests Fixes #4 --- src/redis/client/client.go | 88 ++++++++++++++++++++++++--------- src/redis/client/client_test.go | 31 +++--------- 2 files changed, 73 insertions(+), 46 deletions(-) diff --git a/src/redis/client/client.go b/src/redis/client/client.go index 17f37c3..cdc23af 100644 --- a/src/redis/client/client.go +++ b/src/redis/client/client.go @@ -4,13 +4,15 @@ import ( "context" "encoding/csv" "errors" + "fmt" "io" + "reflect" "sort" + "strconv" "strings" "github.com/go-redis/redis/v8" "github.com/letsencrypt/attache/src/redis/config" - "gopkg.in/yaml.v3" ) // Client is a wrapper around an inner go-redis client. @@ -22,7 +24,7 @@ type Client struct { } func (h *Client) StateNewCheck() (bool, error) { - var infoMatchingNewNodes = redisClusterInfo{"fail", 0, 0, 0, 0, 1, 0, 0, 0, 0, 0} + var infoMatchingNewNodes = clusterInfo{"fail", 0, 0, 0, 0, 1, 0, 0, 0, 0, 0} clusterInfo, err := h.GetClusterInfo() if err != nil { return false, err @@ -79,35 +81,77 @@ func (h *Client) GetReplicaNodes() ([]redisClusterNode, error) { return nodes, nil } -type redisClusterInfo struct { - State string `yaml:"cluster_state"` - SlotsAssigned int `yaml:"cluster_slots_assigned"` - SlotsOk int `yaml:"cluster_slots_ok"` - SlotsPfail int `yaml:"cluster_slots_pfail"` - SlotsFail int `yaml:"cluster_slots_fail"` - KnownNodes int `yaml:"cluster_known_nodes"` - Size int `yaml:"cluster_size"` - CurrentEpoch int `yaml:"cluster_current_epoch"` - MyEpoch int `yaml:"cluster_my_epoch"` - StatsMessagesSent int `yaml:"cluster_stats_messages_sent"` - StatsMessagesReceived int `yaml:"cluster_stats_messages_received"` +type clusterInfo struct { + State string `name:"cluster_state"` + SlotsAssigned int64 `name:"cluster_slots_assigned"` + SlotsOk int64 `name:"cluster_slots_ok"` + SlotsPfail int64 `name:"cluster_slots_pfail"` + SlotsFail int64 `name:"cluster_slots_fail"` + KnownNodes int64 `name:"cluster_known_nodes"` + Size int64 `name:"cluster_size"` + CurrentEpoch int64 `name:"cluster_current_epoch"` + MyEpoch int64 `name:"cluster_my_epoch"` + StatsMessagesSent int64 `name:"cluster_stats_messages_sent"` + StatsMessagesReceived int64 `name:"cluster_stats_messages_received"` } -func parseClusterInfoResult(result string) (*redisClusterInfo, error) { - var clusterInfo redisClusterInfo - err := yaml.Unmarshal([]byte(strings.ReplaceAll(result, ":", ": ")), &clusterInfo) - if err != nil { - return nil, err +func setClusterInfoField(name string, value string, ci *clusterInfo) error { + outType := reflect.TypeOf(*ci) + outValue := reflect.ValueOf(ci).Elem() + for i := 0; i < outType.NumField(); i++ { + field := outType.Field(i) + fieldValue := outValue.Field(i) + + if !fieldValue.IsValid() || !fieldValue.CanSet() { + continue + } + fieldName := field.Tag.Get("name") + if fieldName != name { + continue + } + + switch field.Type.Kind() { + case reflect.Int64: + vInt, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("couldn't parse %q, value of %q, as int: %w", value, name, err) + } + fieldValue.SetInt(int64(vInt)) + return nil + + case reflect.String: + fieldValue.SetString(value) + return nil + } + } + return nil +} + +// unmarshalClusterInfo constructs a *clusterInfo by parsing the (INFO style) output +// of the 'cluster info' command as specified in: +// https://redis.io/commands/cluster-info. +func unmarshalClusterInfo(info string) (*clusterInfo, error) { + var c clusterInfo + for _, line := range strings.Split(info, "\r\n") { + // https://redis.io/commands/info#return-value + if strings.HasPrefix(line, "#") || line == "" { + continue + } + kv := strings.SplitN(line, ":", 2) + err := setClusterInfoField(kv[0], kv[1], &c) + if err != nil { + return nil, fmt.Errorf("failed to parse 'cluster info': %w", err) + } } - return &clusterInfo, nil + return &c, nil } -func (h *Client) GetClusterInfo() (*redisClusterInfo, error) { +func (h *Client) GetClusterInfo() (*clusterInfo, error) { info, err := h.Client.ClusterInfo(context.Background()).Result() if err != nil { return nil, err } - return parseClusterInfoResult(info) + return unmarshalClusterInfo(info) } type redisClusterNode struct { diff --git a/src/redis/client/client_test.go b/src/redis/client/client_test.go index 38b7494..f11e0cd 100644 --- a/src/redis/client/client_test.go +++ b/src/redis/client/client_test.go @@ -168,35 +168,18 @@ func Test_parseClusterNodesResult(t *testing.T) { } } -func Test_parseClusterInfoResult(t *testing.T) { +func Test_unmarshalClusterInfo(t *testing.T) { type args struct { result string } tests := []struct { args args - want *redisClusterInfo + want *clusterInfo wantErr bool }{ { - args{` -cluster_state:ok -cluster_slots_assigned:16384 -cluster_slots_ok:16384 -cluster_slots_pfail:0 -cluster_slots_fail:0 -cluster_known_nodes:13 -cluster_size:3 -cluster_current_epoch:10 -cluster_my_epoch:7 -cluster_stats_messages_ping_sent:88 -cluster_stats_messages_pong_sent:63 -cluster_stats_messages_meet_sent:1 -cluster_stats_messages_sent:152 -cluster_stats_messages_ping_received:63 -cluster_stats_messages_pong_received:82 -cluster_stats_messages_received:145`, - }, - &redisClusterInfo{ + args{"cluster_state:ok\r\ncluster_slots_assigned:16384\r\ncluster_slots_ok:16384\r\ncluster_slots_pfail:0\r\ncluster_slots_fail:0\r\ncluster_known_nodes:13\r\ncluster_size:3\r\ncluster_current_epoch:10\r\ncluster_my_epoch:7\r\ncluster_stats_messages_ping_sent:88\r\ncluster_stats_messages_pong_sent:63\r\ncluster_stats_messages_meet_sent:1\r\ncluster_stats_messages_sent:152\r\ncluster_stats_messages_ping_received:63\r\ncluster_stats_messages_pong_received:82\r\ncluster_stats_messages_received:145\r\n"}, + &clusterInfo{ State: "ok", SlotsAssigned: 16384, SlotsOk: 16384, @@ -214,13 +197,13 @@ cluster_stats_messages_received:145`, } for _, tt := range tests { t.Run("", func(t *testing.T) { - got, err := parseClusterInfoResult(tt.args.result) + got, err := unmarshalClusterInfo(tt.args.result) if (err != nil) != tt.wantErr { - t.Errorf("parseClusterInfoResult() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("unmarshalClusterInfo() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("parseClusterInfoResult() = %+v, want %v", got, tt.want) + t.Errorf("unmarshalClusterInfo() = %+v, want %v", got, tt.want) } }) }