Skip to content

Commit 6c6dddc

Browse files
fukua95ndyakov
andauthored
feat: add ParseFailoverURL (#3362)
* add ParseFailoverURL for FailoverOptions * fix 2 warning Co-authored-by: Nedyalko Dyakov <[email protected]>
1 parent 42c3284 commit 6c6dddc

File tree

3 files changed

+411
-2
lines changed

3 files changed

+411
-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 {

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)