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

Commit b2bb0e3

Browse files
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
1 parent 7d52df4 commit b2bb0e3

File tree

2 files changed

+73
-46
lines changed

2 files changed

+73
-46
lines changed

src/redis/client/client.go

+66-22
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import (
44
"context"
55
"encoding/csv"
66
"errors"
7+
"fmt"
78
"io"
9+
"reflect"
810
"sort"
11+
"strconv"
912
"strings"
1013

1114
"github.com/go-redis/redis/v8"
1215
"github.com/letsencrypt/attache/src/redis/config"
13-
"gopkg.in/yaml.v3"
1416
)
1517

1618
// Client is a wrapper around an inner go-redis client.
@@ -22,7 +24,7 @@ type Client struct {
2224
}
2325

2426
func (h *Client) StateNewCheck() (bool, error) {
25-
var infoMatchingNewNodes = redisClusterInfo{"fail", 0, 0, 0, 0, 1, 0, 0, 0, 0, 0}
27+
var infoMatchingNewNodes = clusterInfo{"fail", 0, 0, 0, 0, 1, 0, 0, 0, 0, 0}
2628
clusterInfo, err := h.GetClusterInfo()
2729
if err != nil {
2830
return false, err
@@ -79,35 +81,77 @@ func (h *Client) GetReplicaNodes() ([]redisClusterNode, error) {
7981
return nodes, nil
8082
}
8183

82-
type redisClusterInfo struct {
83-
State string `yaml:"cluster_state"`
84-
SlotsAssigned int `yaml:"cluster_slots_assigned"`
85-
SlotsOk int `yaml:"cluster_slots_ok"`
86-
SlotsPfail int `yaml:"cluster_slots_pfail"`
87-
SlotsFail int `yaml:"cluster_slots_fail"`
88-
KnownNodes int `yaml:"cluster_known_nodes"`
89-
Size int `yaml:"cluster_size"`
90-
CurrentEpoch int `yaml:"cluster_current_epoch"`
91-
MyEpoch int `yaml:"cluster_my_epoch"`
92-
StatsMessagesSent int `yaml:"cluster_stats_messages_sent"`
93-
StatsMessagesReceived int `yaml:"cluster_stats_messages_received"`
84+
type clusterInfo struct {
85+
State string `name:"cluster_state"`
86+
SlotsAssigned int64 `name:"cluster_slots_assigned"`
87+
SlotsOk int64 `name:"cluster_slots_ok"`
88+
SlotsPfail int64 `name:"cluster_slots_pfail"`
89+
SlotsFail int64 `name:"cluster_slots_fail"`
90+
KnownNodes int64 `name:"cluster_known_nodes"`
91+
Size int64 `name:"cluster_size"`
92+
CurrentEpoch int64 `name:"cluster_current_epoch"`
93+
MyEpoch int64 `name:"cluster_my_epoch"`
94+
StatsMessagesSent int64 `name:"cluster_stats_messages_sent"`
95+
StatsMessagesReceived int64 `name:"cluster_stats_messages_received"`
9496
}
9597

96-
func parseClusterInfoResult(result string) (*redisClusterInfo, error) {
97-
var clusterInfo redisClusterInfo
98-
err := yaml.Unmarshal([]byte(strings.ReplaceAll(result, ":", ": ")), &clusterInfo)
99-
if err != nil {
100-
return nil, err
98+
func setClusterInfoField(name string, value string, ci *clusterInfo) error {
99+
outType := reflect.TypeOf(*ci)
100+
outValue := reflect.ValueOf(ci).Elem()
101+
for i := 0; i < outType.NumField(); i++ {
102+
field := outType.Field(i)
103+
fieldValue := outValue.Field(i)
104+
105+
if !fieldValue.IsValid() || !fieldValue.CanSet() {
106+
continue
107+
}
108+
fieldName := field.Tag.Get("name")
109+
if fieldName != name {
110+
continue
111+
}
112+
113+
switch field.Type.Kind() {
114+
case reflect.Int64:
115+
vInt, err := strconv.Atoi(value)
116+
if err != nil {
117+
return fmt.Errorf("couldn't parse %q, value of %q, as int: %w", value, name, err)
118+
}
119+
fieldValue.SetInt(int64(vInt))
120+
return nil
121+
122+
case reflect.String:
123+
fieldValue.SetString(value)
124+
return nil
125+
}
126+
}
127+
return nil
128+
}
129+
130+
// unmarshalClusterInfo constructs a *clusterInfo by parsing the (INFO style) output
131+
// of the 'cluster info' command as specified in:
132+
// https://redis.io/commands/cluster-info.
133+
func unmarshalClusterInfo(info string) (*clusterInfo, error) {
134+
var c clusterInfo
135+
for _, line := range strings.Split(info, "\r\n") {
136+
// https://redis.io/commands/info#return-value
137+
if strings.HasPrefix(line, "#") || line == "" {
138+
continue
139+
}
140+
kv := strings.SplitN(line, ":", 2)
141+
err := setClusterInfoField(kv[0], kv[1], &c)
142+
if err != nil {
143+
return nil, fmt.Errorf("failed to parse 'cluster info': %w", err)
144+
}
101145
}
102-
return &clusterInfo, nil
146+
return &c, nil
103147
}
104148

105-
func (h *Client) GetClusterInfo() (*redisClusterInfo, error) {
149+
func (h *Client) GetClusterInfo() (*clusterInfo, error) {
106150
info, err := h.Client.ClusterInfo(context.Background()).Result()
107151
if err != nil {
108152
return nil, err
109153
}
110-
return parseClusterInfoResult(info)
154+
return unmarshalClusterInfo(info)
111155
}
112156

113157
type redisClusterNode struct {

src/redis/client/client_test.go

+7-24
Original file line numberDiff line numberDiff line change
@@ -168,35 +168,18 @@ func Test_parseClusterNodesResult(t *testing.T) {
168168
}
169169
}
170170

171-
func Test_parseClusterInfoResult(t *testing.T) {
171+
func Test_unmarshalClusterInfo(t *testing.T) {
172172
type args struct {
173173
result string
174174
}
175175
tests := []struct {
176176
args args
177-
want *redisClusterInfo
177+
want *clusterInfo
178178
wantErr bool
179179
}{
180180
{
181-
args{`
182-
cluster_state:ok
183-
cluster_slots_assigned:16384
184-
cluster_slots_ok:16384
185-
cluster_slots_pfail:0
186-
cluster_slots_fail:0
187-
cluster_known_nodes:13
188-
cluster_size:3
189-
cluster_current_epoch:10
190-
cluster_my_epoch:7
191-
cluster_stats_messages_ping_sent:88
192-
cluster_stats_messages_pong_sent:63
193-
cluster_stats_messages_meet_sent:1
194-
cluster_stats_messages_sent:152
195-
cluster_stats_messages_ping_received:63
196-
cluster_stats_messages_pong_received:82
197-
cluster_stats_messages_received:145`,
198-
},
199-
&redisClusterInfo{
181+
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"},
182+
&clusterInfo{
200183
State: "ok",
201184
SlotsAssigned: 16384,
202185
SlotsOk: 16384,
@@ -214,13 +197,13 @@ cluster_stats_messages_received:145`,
214197
}
215198
for _, tt := range tests {
216199
t.Run("", func(t *testing.T) {
217-
got, err := parseClusterInfoResult(tt.args.result)
200+
got, err := unmarshalClusterInfo(tt.args.result)
218201
if (err != nil) != tt.wantErr {
219-
t.Errorf("parseClusterInfoResult() error = %v, wantErr %v", err, tt.wantErr)
202+
t.Errorf("unmarshalClusterInfo() error = %v, wantErr %v", err, tt.wantErr)
220203
return
221204
}
222205
if !reflect.DeepEqual(got, tt.want) {
223-
t.Errorf("parseClusterInfoResult() = %+v, want %v", got, tt.want)
206+
t.Errorf("unmarshalClusterInfo() = %+v, want %v", got, tt.want)
224207
}
225208
})
226209
}

0 commit comments

Comments
 (0)