|
6 | 6 | "errors"
|
7 | 7 | "fmt"
|
8 | 8 | "net"
|
| 9 | + "net/url" |
| 10 | + "strconv" |
9 | 11 | "strings"
|
10 | 12 | "sync"
|
11 | 13 | "time"
|
@@ -220,6 +222,146 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
|
220 | 222 | }
|
221 | 223 | }
|
222 | 224 |
|
| 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 | + |
223 | 365 | // NewFailoverClient returns a Redis client that uses Redis Sentinel
|
224 | 366 | // for automatic failover. It's safe for concurrent use by multiple
|
225 | 367 | // goroutines.
|
|
0 commit comments