Skip to content

Commit 8cd300f

Browse files
committed
Merge branch 'master' into optimize/reduce-copy
2 parents 944b067 + 43e7fb5 commit 8cd300f

File tree

4 files changed

+512
-2
lines changed

4 files changed

+512
-2
lines changed

example_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func ExampleMapStringStringCmd_Scan() {
359359
// Get the map. The same approach works for HmGet().
360360
res := rdb.HGetAll(ctx, "map")
361361
if res.Err() != nil {
362-
panic(err)
362+
panic(res.Err())
363363
}
364364

365365
type data struct {
@@ -392,7 +392,7 @@ func ExampleSliceCmd_Scan() {
392392

393393
res := rdb.MGet(ctx, "name", "count", "empty", "correct")
394394
if res.Err() != nil {
395-
panic(err)
395+
panic(res.Err())
396396
}
397397

398398
type data struct {

internal/util/strconv_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package util
2+
3+
import (
4+
"math"
5+
"testing"
6+
)
7+
8+
func TestAtoi(t *testing.T) {
9+
tests := []struct {
10+
input []byte
11+
expected int
12+
wantErr bool
13+
}{
14+
{[]byte("123"), 123, false},
15+
{[]byte("-456"), -456, false},
16+
{[]byte("abc"), 0, true},
17+
}
18+
19+
for _, tt := range tests {
20+
result, err := Atoi(tt.input)
21+
if (err != nil) != tt.wantErr {
22+
t.Errorf("Atoi(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
23+
}
24+
if result != tt.expected && !tt.wantErr {
25+
t.Errorf("Atoi(%q) = %d, want %d", tt.input, result, tt.expected)
26+
}
27+
}
28+
}
29+
30+
func TestParseInt(t *testing.T) {
31+
tests := []struct {
32+
input []byte
33+
base int
34+
bitSize int
35+
expected int64
36+
wantErr bool
37+
}{
38+
{[]byte("123"), 10, 64, 123, false},
39+
{[]byte("-7F"), 16, 64, -127, false},
40+
{[]byte("zzz"), 36, 64, 46655, false},
41+
{[]byte("invalid"), 10, 64, 0, true},
42+
}
43+
44+
for _, tt := range tests {
45+
result, err := ParseInt(tt.input, tt.base, tt.bitSize)
46+
if (err != nil) != tt.wantErr {
47+
t.Errorf("ParseInt(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr)
48+
}
49+
if result != tt.expected && !tt.wantErr {
50+
t.Errorf("ParseInt(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected)
51+
}
52+
}
53+
}
54+
55+
func TestParseUint(t *testing.T) {
56+
tests := []struct {
57+
input []byte
58+
base int
59+
bitSize int
60+
expected uint64
61+
wantErr bool
62+
}{
63+
{[]byte("255"), 10, 8, 255, false},
64+
{[]byte("FF"), 16, 16, 255, false},
65+
{[]byte("-1"), 10, 8, 0, true}, // negative should error for unsigned
66+
}
67+
68+
for _, tt := range tests {
69+
result, err := ParseUint(tt.input, tt.base, tt.bitSize)
70+
if (err != nil) != tt.wantErr {
71+
t.Errorf("ParseUint(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr)
72+
}
73+
if result != tt.expected && !tt.wantErr {
74+
t.Errorf("ParseUint(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected)
75+
}
76+
}
77+
}
78+
79+
func TestParseFloat(t *testing.T) {
80+
tests := []struct {
81+
input []byte
82+
bitSize int
83+
expected float64
84+
wantErr bool
85+
}{
86+
{[]byte("3.14"), 64, 3.14, false},
87+
{[]byte("-2.71"), 64, -2.71, false},
88+
{[]byte("NaN"), 64, math.NaN(), false},
89+
{[]byte("invalid"), 64, 0, true},
90+
}
91+
92+
for _, tt := range tests {
93+
result, err := ParseFloat(tt.input, tt.bitSize)
94+
if (err != nil) != tt.wantErr {
95+
t.Errorf("ParseFloat(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
96+
}
97+
if !tt.wantErr && !(math.IsNaN(tt.expected) && math.IsNaN(result)) && result != tt.expected {
98+
t.Errorf("ParseFloat(%q) = %v, want %v", tt.input, result, tt.expected)
99+
}
100+
}
101+
}

sentinel.go

+142
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"errors"
77
"fmt"
88
"net"
9+
"net/url"
10+
"strconv"
911
"strings"
1012
"sync"
1113
"time"
@@ -220,6 +222,146 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
220222
}
221223
}
222224

225+
// ParseFailoverURL parses a URL into FailoverOptions that can be used to connect to Redis.
226+
// The URL must be in the form:
227+
//
228+
// redis://<user>:<password>@<host>:<port>/<db_number>
229+
// or
230+
// rediss://<user>:<password>@<host>:<port>/<db_number>
231+
//
232+
// To add additional addresses, specify the query parameter, "addr" one or more times. e.g:
233+
//
234+
// redis://<user>:<password>@<host>:<port>/<db_number>?addr=<host2>:<port2>&addr=<host3>:<port3>
235+
// or
236+
// rediss://<user>:<password>@<host>:<port>/<db_number>?addr=<host2>:<port2>&addr=<host3>:<port3>
237+
//
238+
// Most Option fields can be set using query parameters, with the following restrictions:
239+
// - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
240+
// - only scalar type fields are supported (bool, int, time.Duration)
241+
// - for time.Duration fields, values must be a valid input for time.ParseDuration();
242+
// additionally a plain integer as value (i.e. without unit) is interpreted as seconds
243+
// - to disable a duration field, use value less than or equal to 0; to use the default
244+
// value, leave the value blank or remove the parameter
245+
// - only the last value is interpreted if a parameter is given multiple times
246+
// - fields "network", "addr", "sentinel_username" and "sentinel_password" can only be set using other
247+
// URL attributes (scheme, host, userinfo, resp.), query parameters using these
248+
// names will be treated as unknown parameters
249+
// - unknown parameter names will result in an error
250+
//
251+
// Example:
252+
//
253+
// redis://user:password@localhost:6789?master_name=mymaster&dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791
254+
// is equivalent to:
255+
// &FailoverOptions{
256+
// MasterName: "mymaster",
257+
// Addr: ["localhost:6789", "localhost:6790", "localhost:6791"]
258+
// DialTimeout: 3 * time.Second, // no time unit = seconds
259+
// ReadTimeout: 6 * time.Second,
260+
// }
261+
func ParseFailoverURL(redisURL string) (*FailoverOptions, error) {
262+
u, err := url.Parse(redisURL)
263+
if err != nil {
264+
return nil, err
265+
}
266+
return setupFailoverConn(u)
267+
}
268+
269+
func setupFailoverConn(u *url.URL) (*FailoverOptions, error) {
270+
o := &FailoverOptions{}
271+
272+
o.SentinelUsername, o.SentinelPassword = getUserPassword(u)
273+
274+
h, p := getHostPortWithDefaults(u)
275+
o.SentinelAddrs = append(o.SentinelAddrs, net.JoinHostPort(h, p))
276+
277+
switch u.Scheme {
278+
case "rediss":
279+
o.TLSConfig = &tls.Config{ServerName: h, MinVersion: tls.VersionTLS12}
280+
case "redis":
281+
o.TLSConfig = nil
282+
default:
283+
return nil, fmt.Errorf("redis: invalid URL scheme: %s", u.Scheme)
284+
}
285+
286+
f := strings.FieldsFunc(u.Path, func(r rune) bool {
287+
return r == '/'
288+
})
289+
switch len(f) {
290+
case 0:
291+
o.DB = 0
292+
case 1:
293+
var err error
294+
if o.DB, err = strconv.Atoi(f[0]); err != nil {
295+
return nil, fmt.Errorf("redis: invalid database number: %q", f[0])
296+
}
297+
default:
298+
return nil, fmt.Errorf("redis: invalid URL path: %s", u.Path)
299+
}
300+
301+
return setupFailoverConnParams(u, o)
302+
}
303+
304+
func setupFailoverConnParams(u *url.URL, o *FailoverOptions) (*FailoverOptions, error) {
305+
q := queryOptions{q: u.Query()}
306+
307+
o.MasterName = q.string("master_name")
308+
o.ClientName = q.string("client_name")
309+
o.RouteByLatency = q.bool("route_by_latency")
310+
o.RouteRandomly = q.bool("route_randomly")
311+
o.ReplicaOnly = q.bool("replica_only")
312+
o.UseDisconnectedReplicas = q.bool("use_disconnected_replicas")
313+
o.Protocol = q.int("protocol")
314+
o.Username = q.string("username")
315+
o.Password = q.string("password")
316+
o.MaxRetries = q.int("max_retries")
317+
o.MinRetryBackoff = q.duration("min_retry_backoff")
318+
o.MaxRetryBackoff = q.duration("max_retry_backoff")
319+
o.DialTimeout = q.duration("dial_timeout")
320+
o.ReadTimeout = q.duration("read_timeout")
321+
o.WriteTimeout = q.duration("write_timeout")
322+
o.ContextTimeoutEnabled = q.bool("context_timeout_enabled")
323+
o.PoolFIFO = q.bool("pool_fifo")
324+
o.PoolSize = q.int("pool_size")
325+
o.MinIdleConns = q.int("min_idle_conns")
326+
o.MaxIdleConns = q.int("max_idle_conns")
327+
o.MaxActiveConns = q.int("max_active_conns")
328+
o.ConnMaxLifetime = q.duration("conn_max_lifetime")
329+
o.ConnMaxIdleTime = q.duration("conn_max_idle_time")
330+
o.PoolTimeout = q.duration("pool_timeout")
331+
o.DisableIdentity = q.bool("disableIdentity")
332+
o.IdentitySuffix = q.string("identitySuffix")
333+
o.UnstableResp3 = q.bool("unstable_resp3")
334+
335+
if q.err != nil {
336+
return nil, q.err
337+
}
338+
339+
if tmp := q.string("db"); tmp != "" {
340+
db, err := strconv.Atoi(tmp)
341+
if err != nil {
342+
return nil, fmt.Errorf("redis: invalid database number: %w", err)
343+
}
344+
o.DB = db
345+
}
346+
347+
addrs := q.strings("addr")
348+
for _, addr := range addrs {
349+
h, p, err := net.SplitHostPort(addr)
350+
if err != nil || h == "" || p == "" {
351+
return nil, fmt.Errorf("redis: unable to parse addr param: %s", addr)
352+
}
353+
354+
o.SentinelAddrs = append(o.SentinelAddrs, net.JoinHostPort(h, p))
355+
}
356+
357+
// any parameters left?
358+
if r := q.remaining(); len(r) > 0 {
359+
return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", "))
360+
}
361+
362+
return o, nil
363+
}
364+
223365
// NewFailoverClient returns a Redis client that uses Redis Sentinel
224366
// for automatic failover. It's safe for concurrent use by multiple
225367
// goroutines.

0 commit comments

Comments
 (0)