...

Source file src/github.com/redis/go-redis/v9/options.go

Documentation: github.com/redis/go-redis/v9

     1  package redis
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"errors"
     7  	"fmt"
     8  	"net"
     9  	"net/url"
    10  	"runtime"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/redis/go-redis/v9/auth"
    17  	"github.com/redis/go-redis/v9/internal/pool"
    18  	"github.com/redis/go-redis/v9/internal/proto"
    19  )
    20  
    21  // Limiter is the interface of a rate limiter or a circuit breaker.
    22  type Limiter interface {
    23  	// Allow returns nil if operation is allowed or an error otherwise.
    24  	// If operation is allowed client must ReportResult of the operation
    25  	// whether it is a success or a failure.
    26  	Allow() error
    27  	// ReportResult reports the result of the previously allowed operation.
    28  	// nil indicates a success, non-nil error usually indicates a failure.
    29  	ReportResult(result error)
    30  }
    31  
    32  // Options keeps the settings to set up redis connection.
    33  type Options struct {
    34  
    35  	// Network type, either tcp or unix.
    36  	//
    37  	// default: is tcp.
    38  	Network string
    39  
    40  	// Addr is the address formated as host:port
    41  	Addr string
    42  
    43  	// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
    44  	ClientName string
    45  
    46  	// Dialer creates new network connection and has priority over
    47  	// Network and Addr options.
    48  	Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
    49  
    50  	// Hook that is called when new connection is established.
    51  	OnConnect func(ctx context.Context, cn *Conn) error
    52  
    53  	// Protocol 2 or 3. Use the version to negotiate RESP version with redis-server.
    54  	//
    55  	// default: 3.
    56  	Protocol int
    57  
    58  	// Username is used to authenticate the current connection
    59  	// with one of the connections defined in the ACL list when connecting
    60  	// to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
    61  	Username string
    62  
    63  	// Password is an optional password. Must match the password specified in the
    64  	// `requirepass` server configuration option (if connecting to a Redis 5.0 instance, or lower),
    65  	// or the User Password when connecting to a Redis 6.0 instance, or greater,
    66  	// that is using the Redis ACL system.
    67  	Password string
    68  
    69  	// CredentialsProvider allows the username and password to be updated
    70  	// before reconnecting. It should return the current username and password.
    71  	CredentialsProvider func() (username string, password string)
    72  
    73  	// CredentialsProviderContext is an enhanced parameter of CredentialsProvider,
    74  	// done to maintain API compatibility. In the future,
    75  	// there might be a merge between CredentialsProviderContext and CredentialsProvider.
    76  	// There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider.
    77  	CredentialsProviderContext func(ctx context.Context) (username string, password string, err error)
    78  
    79  	// StreamingCredentialsProvider is used to retrieve the credentials
    80  	// for the connection from an external source. Those credentials may change
    81  	// during the connection lifetime. This is useful for managed identity
    82  	// scenarios where the credentials are retrieved from an external source.
    83  	//
    84  	// Currently, this is a placeholder for the future implementation.
    85  	StreamingCredentialsProvider auth.StreamingCredentialsProvider
    86  
    87  	// DB is the database to be selected after connecting to the server.
    88  	DB int
    89  
    90  	// MaxRetries is the maximum number of retries before giving up.
    91  	// -1 (not 0) disables retries.
    92  	//
    93  	// default: 3 retries
    94  	MaxRetries int
    95  
    96  	// MinRetryBackoff is the minimum backoff between each retry.
    97  	// -1 disables backoff.
    98  	//
    99  	// default: 8 milliseconds
   100  	MinRetryBackoff time.Duration
   101  
   102  	// MaxRetryBackoff is the maximum backoff between each retry.
   103  	// -1 disables backoff.
   104  	// default: 512 milliseconds;
   105  	MaxRetryBackoff time.Duration
   106  
   107  	// DialTimeout for establishing new connections.
   108  	//
   109  	// default: 5 seconds
   110  	DialTimeout time.Duration
   111  
   112  	// ReadTimeout for socket reads. If reached, commands will fail
   113  	// with a timeout instead of blocking. Supported values:
   114  	//
   115  	//	- `-1` - no timeout (block indefinitely).
   116  	//	- `-2` - disables SetReadDeadline calls completely.
   117  	//
   118  	// default: 3 seconds
   119  	ReadTimeout time.Duration
   120  
   121  	// WriteTimeout for socket writes. If reached, commands will fail
   122  	// with a timeout instead of blocking.  Supported values:
   123  	//
   124  	//	- `-1` - no timeout (block indefinitely).
   125  	//	- `-2` - disables SetWriteDeadline calls completely.
   126  	//
   127  	// default: 3 seconds
   128  	WriteTimeout time.Duration
   129  
   130  	// ContextTimeoutEnabled controls whether the client respects context timeouts and deadlines.
   131  	// See https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts
   132  	ContextTimeoutEnabled bool
   133  
   134  	// ReadBufferSize is the size of the bufio.Reader buffer for each connection.
   135  	// Larger buffers can improve performance for commands that return large responses.
   136  	// Smaller buffers can improve memory usage for larger pools.
   137  	//
   138  	// default: 32KiB (32768 bytes)
   139  	ReadBufferSize int
   140  
   141  	// WriteBufferSize is the size of the bufio.Writer buffer for each connection.
   142  	// Larger buffers can improve performance for large pipelines and commands with many arguments.
   143  	// Smaller buffers can improve memory usage for larger pools.
   144  	//
   145  	// default: 32KiB (32768 bytes)
   146  	WriteBufferSize int
   147  
   148  	// PoolFIFO type of connection pool.
   149  	//
   150  	//	- true for FIFO pool
   151  	//	- false for LIFO pool.
   152  	//
   153  	// Note that FIFO has slightly higher overhead compared to LIFO,
   154  	// but it helps closing idle connections faster reducing the pool size.
   155  	PoolFIFO bool
   156  
   157  	// PoolSize is the base number of socket connections.
   158  	// Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
   159  	// If there is not enough connections in the pool, new connections will be allocated in excess of PoolSize,
   160  	// you can limit it through MaxActiveConns
   161  	//
   162  	// default: 10 * runtime.GOMAXPROCS(0)
   163  	PoolSize int
   164  
   165  	// PoolTimeout is the amount of time client waits for connection if all connections
   166  	// are busy before returning an error.
   167  	//
   168  	// default: ReadTimeout + 1 second
   169  	PoolTimeout time.Duration
   170  
   171  	// MinIdleConns is the minimum number of idle connections which is useful when establishing
   172  	// new connection is slow. The idle connections are not closed by default.
   173  	//
   174  	// default: 0
   175  	MinIdleConns int
   176  
   177  	// MaxIdleConns is the maximum number of idle connections.
   178  	// The idle connections are not closed by default.
   179  	//
   180  	// default: 0
   181  	MaxIdleConns int
   182  
   183  	// MaxActiveConns is the maximum number of connections allocated by the pool at a given time.
   184  	// When zero, there is no limit on the number of connections in the pool.
   185  	// If the pool is full, the next call to Get() will block until a connection is released.
   186  	MaxActiveConns int
   187  
   188  	// ConnMaxIdleTime is the maximum amount of time a connection may be idle.
   189  	// Should be less than server's timeout.
   190  	//
   191  	// Expired connections may be closed lazily before reuse.
   192  	// If d <= 0, connections are not closed due to a connection's idle time.
   193  	// -1 disables idle timeout check.
   194  	//
   195  	// default: 30 minutes
   196  	ConnMaxIdleTime time.Duration
   197  
   198  	// ConnMaxLifetime is the maximum amount of time a connection may be reused.
   199  	//
   200  	// Expired connections may be closed lazily before reuse.
   201  	// If <= 0, connections are not closed due to a connection's age.
   202  	//
   203  	// default: 0
   204  	ConnMaxLifetime time.Duration
   205  
   206  	// TLSConfig to use. When set, TLS will be negotiated.
   207  	TLSConfig *tls.Config
   208  
   209  	// Limiter interface used to implement circuit breaker or rate limiter.
   210  	Limiter Limiter
   211  
   212  	// readOnly enables read only queries on slave/follower nodes.
   213  	readOnly bool
   214  
   215  	// DisableIndentity - Disable set-lib on connect.
   216  	//
   217  	// default: false
   218  	//
   219  	// Deprecated: Use DisableIdentity instead.
   220  	DisableIndentity bool
   221  
   222  	// DisableIdentity is used to disable CLIENT SETINFO command on connect.
   223  	//
   224  	// default: false
   225  	DisableIdentity bool
   226  
   227  	// Add suffix to client name. Default is empty.
   228  	// IdentitySuffix - add suffix to client name.
   229  	IdentitySuffix string
   230  
   231  	// UnstableResp3 enables Unstable mode for Redis Search module with RESP3.
   232  	// When unstable mode is enabled, the client will use RESP3 protocol and only be able to use RawResult
   233  	UnstableResp3 bool
   234  
   235  	// FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing.
   236  	// When a node is marked as failing, it will be avoided for this duration.
   237  	// Default is 15 seconds.
   238  	FailingTimeoutSeconds int
   239  }
   240  
   241  func (opt *Options) init() {
   242  	if opt.Addr == "" {
   243  		opt.Addr = "localhost:6379"
   244  	}
   245  	if opt.Network == "" {
   246  		if strings.HasPrefix(opt.Addr, "/") {
   247  			opt.Network = "unix"
   248  		} else {
   249  			opt.Network = "tcp"
   250  		}
   251  	}
   252  	if opt.Protocol < 2 {
   253  		opt.Protocol = 3
   254  	}
   255  	if opt.DialTimeout == 0 {
   256  		opt.DialTimeout = 5 * time.Second
   257  	}
   258  	if opt.Dialer == nil {
   259  		opt.Dialer = NewDialer(opt)
   260  	}
   261  	if opt.PoolSize == 0 {
   262  		opt.PoolSize = 10 * runtime.GOMAXPROCS(0)
   263  	}
   264  	if opt.ReadBufferSize == 0 {
   265  		opt.ReadBufferSize = proto.DefaultBufferSize
   266  	}
   267  	if opt.WriteBufferSize == 0 {
   268  		opt.WriteBufferSize = proto.DefaultBufferSize
   269  	}
   270  	switch opt.ReadTimeout {
   271  	case -2:
   272  		opt.ReadTimeout = -1
   273  	case -1:
   274  		opt.ReadTimeout = 0
   275  	case 0:
   276  		opt.ReadTimeout = 3 * time.Second
   277  	}
   278  	switch opt.WriteTimeout {
   279  	case -2:
   280  		opt.WriteTimeout = -1
   281  	case -1:
   282  		opt.WriteTimeout = 0
   283  	case 0:
   284  		opt.WriteTimeout = opt.ReadTimeout
   285  	}
   286  	if opt.PoolTimeout == 0 {
   287  		if opt.ReadTimeout > 0 {
   288  			opt.PoolTimeout = opt.ReadTimeout + time.Second
   289  		} else {
   290  			opt.PoolTimeout = 30 * time.Second
   291  		}
   292  	}
   293  	if opt.ConnMaxIdleTime == 0 {
   294  		opt.ConnMaxIdleTime = 30 * time.Minute
   295  	}
   296  
   297  	switch opt.MaxRetries {
   298  	case -1:
   299  		opt.MaxRetries = 0
   300  	case 0:
   301  		opt.MaxRetries = 3
   302  	}
   303  	switch opt.MinRetryBackoff {
   304  	case -1:
   305  		opt.MinRetryBackoff = 0
   306  	case 0:
   307  		opt.MinRetryBackoff = 8 * time.Millisecond
   308  	}
   309  	switch opt.MaxRetryBackoff {
   310  	case -1:
   311  		opt.MaxRetryBackoff = 0
   312  	case 0:
   313  		opt.MaxRetryBackoff = 512 * time.Millisecond
   314  	}
   315  }
   316  
   317  func (opt *Options) clone() *Options {
   318  	clone := *opt
   319  	return &clone
   320  }
   321  
   322  // NewDialer returns a function that will be used as the default dialer
   323  // when none is specified in Options.Dialer.
   324  func NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, error) {
   325  	return func(ctx context.Context, network, addr string) (net.Conn, error) {
   326  		netDialer := &net.Dialer{
   327  			Timeout:   opt.DialTimeout,
   328  			KeepAlive: 5 * time.Minute,
   329  		}
   330  		if opt.TLSConfig == nil {
   331  			return netDialer.DialContext(ctx, network, addr)
   332  		}
   333  		return tls.DialWithDialer(netDialer, network, addr, opt.TLSConfig)
   334  	}
   335  }
   336  
   337  // ParseURL parses a URL into Options that can be used to connect to Redis.
   338  // Scheme is required.
   339  // There are two connection types: by tcp socket and by unix socket.
   340  // Tcp connection:
   341  //
   342  //	redis://<user>:<password>@<host>:<port>/<db_number>
   343  //
   344  // Unix connection:
   345  //
   346  //	unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
   347  //
   348  // Most Option fields can be set using query parameters, with the following restrictions:
   349  //   - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
   350  //   - only scalar type fields are supported (bool, int, time.Duration)
   351  //   - for time.Duration fields, values must be a valid input for time.ParseDuration();
   352  //     additionally a plain integer as value (i.e. without unit) is interpreted as seconds
   353  //   - to disable a duration field, use value less than or equal to 0; to use the default
   354  //     value, leave the value blank or remove the parameter
   355  //   - only the last value is interpreted if a parameter is given multiple times
   356  //   - fields "network", "addr", "username" and "password" can only be set using other
   357  //     URL attributes (scheme, host, userinfo, resp.), query parameters using these
   358  //     names will be treated as unknown parameters
   359  //   - unknown parameter names will result in an error
   360  //   - use "skip_verify=true" to ignore TLS certificate validation
   361  //
   362  // Examples:
   363  //
   364  //	redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
   365  //	is equivalent to:
   366  //	&Options{
   367  //		Network:     "tcp",
   368  //		Addr:        "localhost:6789",
   369  //		DB:          1,               // path "/3" was overridden by "&db=1"
   370  //		DialTimeout: 3 * time.Second, // no time unit = seconds
   371  //		ReadTimeout: 6 * time.Second,
   372  //		MaxRetries:  2,
   373  //	}
   374  func ParseURL(redisURL string) (*Options, error) {
   375  	u, err := url.Parse(redisURL)
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  
   380  	switch u.Scheme {
   381  	case "redis", "rediss":
   382  		return setupTCPConn(u)
   383  	case "unix":
   384  		return setupUnixConn(u)
   385  	default:
   386  		return nil, fmt.Errorf("redis: invalid URL scheme: %s", u.Scheme)
   387  	}
   388  }
   389  
   390  func setupTCPConn(u *url.URL) (*Options, error) {
   391  	o := &Options{Network: "tcp"}
   392  
   393  	o.Username, o.Password = getUserPassword(u)
   394  
   395  	h, p := getHostPortWithDefaults(u)
   396  	o.Addr = net.JoinHostPort(h, p)
   397  
   398  	f := strings.FieldsFunc(u.Path, func(r rune) bool {
   399  		return r == '/'
   400  	})
   401  	switch len(f) {
   402  	case 0:
   403  		o.DB = 0
   404  	case 1:
   405  		var err error
   406  		if o.DB, err = strconv.Atoi(f[0]); err != nil {
   407  			return nil, fmt.Errorf("redis: invalid database number: %q", f[0])
   408  		}
   409  	default:
   410  		return nil, fmt.Errorf("redis: invalid URL path: %s", u.Path)
   411  	}
   412  
   413  	if u.Scheme == "rediss" {
   414  		o.TLSConfig = &tls.Config{
   415  			ServerName: h,
   416  			MinVersion: tls.VersionTLS12,
   417  		}
   418  	}
   419  
   420  	return setupConnParams(u, o)
   421  }
   422  
   423  // getHostPortWithDefaults is a helper function that splits the url into
   424  // a host and a port. If the host is missing, it defaults to localhost
   425  // and if the port is missing, it defaults to 6379.
   426  func getHostPortWithDefaults(u *url.URL) (string, string) {
   427  	host, port, err := net.SplitHostPort(u.Host)
   428  	if err != nil {
   429  		host = u.Host
   430  	}
   431  	if host == "" {
   432  		host = "localhost"
   433  	}
   434  	if port == "" {
   435  		port = "6379"
   436  	}
   437  	return host, port
   438  }
   439  
   440  func setupUnixConn(u *url.URL) (*Options, error) {
   441  	o := &Options{
   442  		Network: "unix",
   443  	}
   444  
   445  	if strings.TrimSpace(u.Path) == "" { // path is required with unix connection
   446  		return nil, errors.New("redis: empty unix socket path")
   447  	}
   448  	o.Addr = u.Path
   449  	o.Username, o.Password = getUserPassword(u)
   450  	return setupConnParams(u, o)
   451  }
   452  
   453  type queryOptions struct {
   454  	q   url.Values
   455  	err error
   456  }
   457  
   458  func (o *queryOptions) has(name string) bool {
   459  	return len(o.q[name]) > 0
   460  }
   461  
   462  func (o *queryOptions) string(name string) string {
   463  	vs := o.q[name]
   464  	if len(vs) == 0 {
   465  		return ""
   466  	}
   467  	delete(o.q, name) // enable detection of unknown parameters
   468  	return vs[len(vs)-1]
   469  }
   470  
   471  func (o *queryOptions) strings(name string) []string {
   472  	vs := o.q[name]
   473  	delete(o.q, name)
   474  	return vs
   475  }
   476  
   477  func (o *queryOptions) int(name string) int {
   478  	s := o.string(name)
   479  	if s == "" {
   480  		return 0
   481  	}
   482  	i, err := strconv.Atoi(s)
   483  	if err == nil {
   484  		return i
   485  	}
   486  	if o.err == nil {
   487  		o.err = fmt.Errorf("redis: invalid %s number: %s", name, err)
   488  	}
   489  	return 0
   490  }
   491  
   492  func (o *queryOptions) duration(name string) time.Duration {
   493  	s := o.string(name)
   494  	if s == "" {
   495  		return 0
   496  	}
   497  	// try plain number first
   498  	if i, err := strconv.Atoi(s); err == nil {
   499  		if i <= 0 {
   500  			// disable timeouts
   501  			return -1
   502  		}
   503  		return time.Duration(i) * time.Second
   504  	}
   505  	dur, err := time.ParseDuration(s)
   506  	if err == nil {
   507  		return dur
   508  	}
   509  	if o.err == nil {
   510  		o.err = fmt.Errorf("redis: invalid %s duration: %w", name, err)
   511  	}
   512  	return 0
   513  }
   514  
   515  func (o *queryOptions) bool(name string) bool {
   516  	switch s := o.string(name); s {
   517  	case "true", "1":
   518  		return true
   519  	case "false", "0", "":
   520  		return false
   521  	default:
   522  		if o.err == nil {
   523  			o.err = fmt.Errorf("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q", name, s)
   524  		}
   525  		return false
   526  	}
   527  }
   528  
   529  func (o *queryOptions) remaining() []string {
   530  	if len(o.q) == 0 {
   531  		return nil
   532  	}
   533  	keys := make([]string, 0, len(o.q))
   534  	for k := range o.q {
   535  		keys = append(keys, k)
   536  	}
   537  	sort.Strings(keys)
   538  	return keys
   539  }
   540  
   541  // setupConnParams converts query parameters in u to option value in o.
   542  func setupConnParams(u *url.URL, o *Options) (*Options, error) {
   543  	q := queryOptions{q: u.Query()}
   544  
   545  	// compat: a future major release may use q.int("db")
   546  	if tmp := q.string("db"); tmp != "" {
   547  		db, err := strconv.Atoi(tmp)
   548  		if err != nil {
   549  			return nil, fmt.Errorf("redis: invalid database number: %w", err)
   550  		}
   551  		o.DB = db
   552  	}
   553  
   554  	o.Protocol = q.int("protocol")
   555  	o.ClientName = q.string("client_name")
   556  	o.MaxRetries = q.int("max_retries")
   557  	o.MinRetryBackoff = q.duration("min_retry_backoff")
   558  	o.MaxRetryBackoff = q.duration("max_retry_backoff")
   559  	o.DialTimeout = q.duration("dial_timeout")
   560  	o.ReadTimeout = q.duration("read_timeout")
   561  	o.WriteTimeout = q.duration("write_timeout")
   562  	o.PoolFIFO = q.bool("pool_fifo")
   563  	o.PoolSize = q.int("pool_size")
   564  	o.PoolTimeout = q.duration("pool_timeout")
   565  	o.MinIdleConns = q.int("min_idle_conns")
   566  	o.MaxIdleConns = q.int("max_idle_conns")
   567  	o.MaxActiveConns = q.int("max_active_conns")
   568  	if q.has("conn_max_idle_time") {
   569  		o.ConnMaxIdleTime = q.duration("conn_max_idle_time")
   570  	} else {
   571  		o.ConnMaxIdleTime = q.duration("idle_timeout")
   572  	}
   573  	if q.has("conn_max_lifetime") {
   574  		o.ConnMaxLifetime = q.duration("conn_max_lifetime")
   575  	} else {
   576  		o.ConnMaxLifetime = q.duration("max_conn_age")
   577  	}
   578  	if q.err != nil {
   579  		return nil, q.err
   580  	}
   581  	if o.TLSConfig != nil && q.has("skip_verify") {
   582  		o.TLSConfig.InsecureSkipVerify = q.bool("skip_verify")
   583  	}
   584  
   585  	// any parameters left?
   586  	if r := q.remaining(); len(r) > 0 {
   587  		return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", "))
   588  	}
   589  
   590  	return o, nil
   591  }
   592  
   593  func getUserPassword(u *url.URL) (string, string) {
   594  	var user, password string
   595  	if u.User != nil {
   596  		user = u.User.Username()
   597  		if p, ok := u.User.Password(); ok {
   598  			password = p
   599  		}
   600  	}
   601  	return user, password
   602  }
   603  
   604  func newConnPool(
   605  	opt *Options,
   606  	dialer func(ctx context.Context, network, addr string) (net.Conn, error),
   607  ) *pool.ConnPool {
   608  	return pool.NewConnPool(&pool.Options{
   609  		Dialer: func(ctx context.Context) (net.Conn, error) {
   610  			return dialer(ctx, opt.Network, opt.Addr)
   611  		},
   612  		PoolFIFO:        opt.PoolFIFO,
   613  		PoolSize:        opt.PoolSize,
   614  		PoolTimeout:     opt.PoolTimeout,
   615  		DialTimeout:     opt.DialTimeout,
   616  		MinIdleConns:    opt.MinIdleConns,
   617  		MaxIdleConns:    opt.MaxIdleConns,
   618  		MaxActiveConns:  opt.MaxActiveConns,
   619  		ConnMaxIdleTime: opt.ConnMaxIdleTime,
   620  		ConnMaxLifetime: opt.ConnMaxLifetime,
   621  		ReadBufferSize:  opt.ReadBufferSize,
   622  		WriteBufferSize: opt.WriteBufferSize,
   623  	})
   624  }
   625  

View as plain text