Skip to content
This repository has been archived by the owner on Jan 26, 2023. It is now read-only.

Commit

Permalink
Add a proper INFO style output unmarshaller (#25)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
beautifulentropy authored Jan 28, 2022
1 parent 7d52df4 commit b2bb0e3
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 46 deletions.
88 changes: 66 additions & 22 deletions src/redis/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
31 changes: 7 additions & 24 deletions src/redis/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
})
}
Expand Down

0 comments on commit b2bb0e3

Please sign in to comment.