88 "net"
99 "net/url"
1010 "runtime"
11+ "sort"
1112 "strconv"
1213 "strings"
1314 "time"
@@ -192,9 +193,32 @@ func (opt *Options) clone() *Options {
192193// Scheme is required.
193194// There are two connection types: by tcp socket and by unix socket.
194195// Tcp connection:
195- // redis://<user>:<password>@<host>:<port>/<db_number>
196+ // redis://<user>:<password>@<host>:<port>/<db_number>
196197// Unix connection:
197198// unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
199+ // Most Option fields can be set using query parameters, with the following restrictions:
200+ // - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
201+ // - only scalar type fields are supported (bool, int, time.Duration)
202+ // - for time.Duration fields, values must be a valid input for time.ParseDuration();
203+ // additionally a plain integer as value (i.e. without unit) is intepreted as seconds
204+ // - to disable a duration field, use value less than or equal to 0; to use the default
205+ // value, leave the value blank or remove the parameter
206+ // - only the last value is interpreted if a parameter is given multiple times
207+ // - fields "network", "addr", "username" and "password" can only be set using other
208+ // URL attributes (scheme, host, userinfo, resp.), query paremeters using these
209+ // names will be treated as unknown parameters
210+ // - unknown parameter names will result in an error
211+ // Examples:
212+ // redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
213+ // is equivalent to:
214+ // &Options{
215+ // Network: "tcp",
216+ // Addr: "localhost:6789",
217+ // DB: 1, // path "/3" was overridden by "&db=1"
218+ // DialTimeout: 3 * time.Second, // no time unit = seconds
219+ // ReadTimeout: 6 * time.Second,
220+ // MaxRetries: 2,
221+ // }
198222func ParseURL (redisURL string ) (* Options , error ) {
199223 u , err := url .Parse (redisURL )
200224 if err != nil {
@@ -216,10 +240,6 @@ func setupTCPConn(u *url.URL) (*Options, error) {
216240
217241 o .Username , o .Password = getUserPassword (u )
218242
219- if len (u .Query ()) > 0 {
220- return nil , errors .New ("redis: no options supported" )
221- }
222-
223243 h , p , err := net .SplitHostPort (u .Host )
224244 if err != nil {
225245 h = u .Host
@@ -250,7 +270,7 @@ func setupTCPConn(u *url.URL) (*Options, error) {
250270 o .TLSConfig = & tls.Config {ServerName : h }
251271 }
252272
253- return o , nil
273+ return setupConnParams ( u , o )
254274}
255275
256276func setupUnixConn (u * url.URL ) (* Options , error ) {
@@ -262,19 +282,122 @@ func setupUnixConn(u *url.URL) (*Options, error) {
262282 return nil , errors .New ("redis: empty unix socket path" )
263283 }
264284 o .Addr = u .Path
265-
266285 o .Username , o .Password = getUserPassword (u )
286+ return setupConnParams (u , o )
287+ }
267288
268- dbStr := u .Query ().Get ("db" )
269- if dbStr == "" {
270- return o , nil // if database is not set, connect to 0 db.
289+ type queryOptions struct {
290+ q url.Values
291+ err error
292+ }
293+
294+ func (o * queryOptions ) string (name string ) string {
295+ vs := o .q [name ]
296+ if len (vs ) == 0 {
297+ return ""
271298 }
299+ delete (o .q , name ) // enable detection of unknown parameters
300+ return vs [len (vs )- 1 ]
301+ }
272302
273- db , err := strconv .Atoi (dbStr )
274- if err != nil {
275- return nil , fmt .Errorf ("redis: invalid database number: %w" , err )
303+ func (o * queryOptions ) int (name string ) int {
304+ s := o .string (name )
305+ if s == "" {
306+ return 0
307+ }
308+ i , err := strconv .Atoi (s )
309+ if err == nil {
310+ return i
311+ }
312+ if o .err == nil {
313+ o .err = fmt .Errorf ("redis: invalid %s number: %s" , name , err )
314+ }
315+ return 0
316+ }
317+
318+ func (o * queryOptions ) duration (name string ) time.Duration {
319+ s := o .string (name )
320+ if s == "" {
321+ return 0
322+ }
323+ // try plain number first
324+ if i , err := strconv .Atoi (s ); err == nil {
325+ if i <= 0 {
326+ // disable timeouts
327+ return - 1
328+ }
329+ return time .Duration (i ) * time .Second
330+ }
331+ dur , err := time .ParseDuration (s )
332+ if err == nil {
333+ return dur
334+ }
335+ if o .err == nil {
336+ o .err = fmt .Errorf ("redis: invalid %s duration: %w" , name , err )
337+ }
338+ return 0
339+ }
340+
341+ func (o * queryOptions ) bool (name string ) bool {
342+ switch s := o .string (name ); s {
343+ case "true" , "1" :
344+ return true
345+ case "false" , "0" , "" :
346+ return false
347+ default :
348+ if o .err == nil {
349+ o .err = fmt .Errorf ("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q" , name , s )
350+ }
351+ return false
352+ }
353+ }
354+
355+ func (o * queryOptions ) remaining () []string {
356+ if len (o .q ) == 0 {
357+ return nil
358+ }
359+ keys := make ([]string , 0 , len (o .q ))
360+ for k := range o .q {
361+ keys = append (keys , k )
362+ }
363+ sort .Strings (keys )
364+ return keys
365+ }
366+
367+ // setupConnParams converts query parameters in u to option value in o.
368+ func setupConnParams (u * url.URL , o * Options ) (* Options , error ) {
369+ q := queryOptions {q : u .Query ()}
370+
371+ // compat: a future major release may use q.int("db")
372+ if tmp := q .string ("db" ); tmp != "" {
373+ db , err := strconv .Atoi (tmp )
374+ if err != nil {
375+ return nil , fmt .Errorf ("redis: invalid database number: %w" , err )
376+ }
377+ o .DB = db
378+ }
379+
380+ o .MaxRetries = q .int ("max_retries" )
381+ o .MinRetryBackoff = q .duration ("min_retry_backoff" )
382+ o .MaxRetryBackoff = q .duration ("max_retry_backoff" )
383+ o .DialTimeout = q .duration ("dial_timeout" )
384+ o .ReadTimeout = q .duration ("read_timeout" )
385+ o .WriteTimeout = q .duration ("write_timeout" )
386+ o .PoolFIFO = q .bool ("pool_fifo" )
387+ o .PoolSize = q .int ("pool_size" )
388+ o .MinIdleConns = q .int ("min_idle_conns" )
389+ o .MaxConnAge = q .duration ("max_conn_age" )
390+ o .PoolTimeout = q .duration ("pool_timeout" )
391+ o .IdleTimeout = q .duration ("idle_timeout" )
392+ o .IdleCheckFrequency = q .duration ("idle_check_frequency" )
393+ if q .err != nil {
394+ return nil , q .err
395+ }
396+
397+ // any parameters left?
398+ if r := q .remaining (); len (r ) > 0 {
399+ return nil , fmt .Errorf ("redis: unexpected option: %s" , strings .Join (r , ", " ))
276400 }
277- o .DB = db
278401
279402 return o , nil
280403}
0 commit comments