mirror of
				https://github.com/redis/go-redis.git
				synced 2025-11-04 02:33:24 +03:00 
			
		
		
		
	* e2e wip * cleanup * remove unused fault injector mock * errChan in test * remove log messages tests * cleanup log messages * s/hitless/maintnotifications/ * fix moving when none * better logs * test with second client after action has started * Fixes Signed-off-by: Elena Kolevska <elena@kolevska.com> * Test fix Signed-off-by: Elena Kolevska <elena@kolevska.com> * feat(e2e-test): Extended e2e tests * imroved e2e test resiliency --------- Signed-off-by: Elena Kolevska <elena@kolevska.com> Co-authored-by: Elena Kolevska <elena@kolevska.com> Co-authored-by: Elena Kolevska <elena-kolevska@users.noreply.github.com> Co-authored-by: Hristo Temelski <hristo.temelski@redis.com>
		
			
				
	
	
		
			745 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			745 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package redis
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/tls"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"net"
 | 
						|
	"net/url"
 | 
						|
	"runtime"
 | 
						|
	"sort"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/redis/go-redis/v9/auth"
 | 
						|
	"github.com/redis/go-redis/v9/internal/pool"
 | 
						|
	"github.com/redis/go-redis/v9/internal/proto"
 | 
						|
	"github.com/redis/go-redis/v9/internal/util"
 | 
						|
	"github.com/redis/go-redis/v9/maintnotifications"
 | 
						|
	"github.com/redis/go-redis/v9/push"
 | 
						|
)
 | 
						|
 | 
						|
// Limiter is the interface of a rate limiter or a circuit breaker.
 | 
						|
type Limiter interface {
 | 
						|
	// Allow returns nil if operation is allowed or an error otherwise.
 | 
						|
	// If operation is allowed client must ReportResult of the operation
 | 
						|
	// whether it is a success or a failure.
 | 
						|
	Allow() error
 | 
						|
	// ReportResult reports the result of the previously allowed operation.
 | 
						|
	// nil indicates a success, non-nil error usually indicates a failure.
 | 
						|
	ReportResult(result error)
 | 
						|
}
 | 
						|
 | 
						|
// Options keeps the settings to set up redis connection.
 | 
						|
type Options struct {
 | 
						|
 | 
						|
	// Network type, either tcp or unix.
 | 
						|
	//
 | 
						|
	// default: is tcp.
 | 
						|
	Network string
 | 
						|
 | 
						|
	// Addr is the address formated as host:port
 | 
						|
	Addr string
 | 
						|
 | 
						|
	// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
 | 
						|
	ClientName string
 | 
						|
 | 
						|
	// Dialer creates new network connection and has priority over
 | 
						|
	// Network and Addr options.
 | 
						|
	Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
 | 
						|
 | 
						|
	// Hook that is called when new connection is established.
 | 
						|
	OnConnect func(ctx context.Context, cn *Conn) error
 | 
						|
 | 
						|
	// Protocol 2 or 3. Use the version to negotiate RESP version with redis-server.
 | 
						|
	//
 | 
						|
	// default: 3.
 | 
						|
	Protocol int
 | 
						|
 | 
						|
	// Username is used to authenticate the current connection
 | 
						|
	// with one of the connections defined in the ACL list when connecting
 | 
						|
	// to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
 | 
						|
	Username string
 | 
						|
 | 
						|
	// Password is an optional password. Must match the password specified in the
 | 
						|
	// `requirepass` server configuration option (if connecting to a Redis 5.0 instance, or lower),
 | 
						|
	// or the User Password when connecting to a Redis 6.0 instance, or greater,
 | 
						|
	// that is using the Redis ACL system.
 | 
						|
	Password string
 | 
						|
 | 
						|
	// CredentialsProvider allows the username and password to be updated
 | 
						|
	// before reconnecting. It should return the current username and password.
 | 
						|
	CredentialsProvider func() (username string, password string)
 | 
						|
 | 
						|
	// CredentialsProviderContext is an enhanced parameter of CredentialsProvider,
 | 
						|
	// done to maintain API compatibility. In the future,
 | 
						|
	// there might be a merge between CredentialsProviderContext and CredentialsProvider.
 | 
						|
	// There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider.
 | 
						|
	CredentialsProviderContext func(ctx context.Context) (username string, password string, err error)
 | 
						|
 | 
						|
	// StreamingCredentialsProvider is used to retrieve the credentials
 | 
						|
	// for the connection from an external source. Those credentials may change
 | 
						|
	// during the connection lifetime. This is useful for managed identity
 | 
						|
	// scenarios where the credentials are retrieved from an external source.
 | 
						|
	//
 | 
						|
	// Currently, this is a placeholder for the future implementation.
 | 
						|
	StreamingCredentialsProvider auth.StreamingCredentialsProvider
 | 
						|
 | 
						|
	// DB is the database to be selected after connecting to the server.
 | 
						|
	DB int
 | 
						|
 | 
						|
	// MaxRetries is the maximum number of retries before giving up.
 | 
						|
	// -1 (not 0) disables retries.
 | 
						|
	//
 | 
						|
	// default: 3 retries
 | 
						|
	MaxRetries int
 | 
						|
 | 
						|
	// MinRetryBackoff is the minimum backoff between each retry.
 | 
						|
	// -1 disables backoff.
 | 
						|
	//
 | 
						|
	// default: 8 milliseconds
 | 
						|
	MinRetryBackoff time.Duration
 | 
						|
 | 
						|
	// MaxRetryBackoff is the maximum backoff between each retry.
 | 
						|
	// -1 disables backoff.
 | 
						|
	// default: 512 milliseconds;
 | 
						|
	MaxRetryBackoff time.Duration
 | 
						|
 | 
						|
	// DialTimeout for establishing new connections.
 | 
						|
	//
 | 
						|
	// default: 5 seconds
 | 
						|
	DialTimeout time.Duration
 | 
						|
 | 
						|
	// DialerRetries is the maximum number of retry attempts when dialing fails.
 | 
						|
	//
 | 
						|
	// default: 5
 | 
						|
	DialerRetries int
 | 
						|
 | 
						|
	// DialerRetryTimeout is the backoff duration between retry attempts.
 | 
						|
	//
 | 
						|
	// default: 100 milliseconds
 | 
						|
	DialerRetryTimeout time.Duration
 | 
						|
 | 
						|
	// ReadTimeout for socket reads. If reached, commands will fail
 | 
						|
	// with a timeout instead of blocking. Supported values:
 | 
						|
	//
 | 
						|
	//	- `-1` - no timeout (block indefinitely).
 | 
						|
	//	- `-2` - disables SetReadDeadline calls completely.
 | 
						|
	//
 | 
						|
	// default: 3 seconds
 | 
						|
	ReadTimeout time.Duration
 | 
						|
 | 
						|
	// WriteTimeout for socket writes. If reached, commands will fail
 | 
						|
	// with a timeout instead of blocking.  Supported values:
 | 
						|
	//
 | 
						|
	//	- `-1` - no timeout (block indefinitely).
 | 
						|
	//	- `-2` - disables SetWriteDeadline calls completely.
 | 
						|
	//
 | 
						|
	// default: 3 seconds
 | 
						|
	WriteTimeout time.Duration
 | 
						|
 | 
						|
	// ContextTimeoutEnabled controls whether the client respects context timeouts and deadlines.
 | 
						|
	// See https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts
 | 
						|
	ContextTimeoutEnabled bool
 | 
						|
 | 
						|
	// ReadBufferSize is the size of the bufio.Reader buffer for each connection.
 | 
						|
	// Larger buffers can improve performance for commands that return large responses.
 | 
						|
	// Smaller buffers can improve memory usage for larger pools.
 | 
						|
	//
 | 
						|
	// default: 32KiB (32768 bytes)
 | 
						|
	ReadBufferSize int
 | 
						|
 | 
						|
	// WriteBufferSize is the size of the bufio.Writer buffer for each connection.
 | 
						|
	// Larger buffers can improve performance for large pipelines and commands with many arguments.
 | 
						|
	// Smaller buffers can improve memory usage for larger pools.
 | 
						|
	//
 | 
						|
	// default: 32KiB (32768 bytes)
 | 
						|
	WriteBufferSize int
 | 
						|
 | 
						|
	// PoolFIFO type of connection pool.
 | 
						|
	//
 | 
						|
	//	- true for FIFO pool
 | 
						|
	//	- false for LIFO pool.
 | 
						|
	//
 | 
						|
	// Note that FIFO has slightly higher overhead compared to LIFO,
 | 
						|
	// but it helps closing idle connections faster reducing the pool size.
 | 
						|
	// default: false
 | 
						|
	PoolFIFO bool
 | 
						|
 | 
						|
	// PoolSize is the base number of socket connections.
 | 
						|
	// Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
 | 
						|
	// If there is not enough connections in the pool, new connections will be allocated in excess of PoolSize,
 | 
						|
	// you can limit it through MaxActiveConns
 | 
						|
	//
 | 
						|
	// default: 10 * runtime.GOMAXPROCS(0)
 | 
						|
	PoolSize int
 | 
						|
 | 
						|
	// PoolTimeout is the amount of time client waits for connection if all connections
 | 
						|
	// are busy before returning an error.
 | 
						|
	//
 | 
						|
	// default: ReadTimeout + 1 second
 | 
						|
	PoolTimeout time.Duration
 | 
						|
 | 
						|
	// MinIdleConns is the minimum number of idle connections which is useful when establishing
 | 
						|
	// new connection is slow. The idle connections are not closed by default.
 | 
						|
	//
 | 
						|
	// default: 0
 | 
						|
	MinIdleConns int
 | 
						|
 | 
						|
	// MaxIdleConns is the maximum number of idle connections.
 | 
						|
	// The idle connections are not closed by default.
 | 
						|
	//
 | 
						|
	// default: 0
 | 
						|
	MaxIdleConns int
 | 
						|
 | 
						|
	// MaxActiveConns is the maximum number of connections allocated by the pool at a given time.
 | 
						|
	// When zero, there is no limit on the number of connections in the pool.
 | 
						|
	// If the pool is full, the next call to Get() will block until a connection is released.
 | 
						|
	MaxActiveConns int
 | 
						|
 | 
						|
	// ConnMaxIdleTime is the maximum amount of time a connection may be idle.
 | 
						|
	// Should be less than server's timeout.
 | 
						|
	//
 | 
						|
	// Expired connections may be closed lazily before reuse.
 | 
						|
	// If d <= 0, connections are not closed due to a connection's idle time.
 | 
						|
	// -1 disables idle timeout check.
 | 
						|
	//
 | 
						|
	// default: 30 minutes
 | 
						|
	ConnMaxIdleTime time.Duration
 | 
						|
 | 
						|
	// ConnMaxLifetime is the maximum amount of time a connection may be reused.
 | 
						|
	//
 | 
						|
	// Expired connections may be closed lazily before reuse.
 | 
						|
	// If <= 0, connections are not closed due to a connection's age.
 | 
						|
	//
 | 
						|
	// default: 0
 | 
						|
	ConnMaxLifetime time.Duration
 | 
						|
 | 
						|
	// TLSConfig to use. When set, TLS will be negotiated.
 | 
						|
	TLSConfig *tls.Config
 | 
						|
 | 
						|
	// Limiter interface used to implement circuit breaker or rate limiter.
 | 
						|
	Limiter Limiter
 | 
						|
 | 
						|
	// readOnly enables read only queries on slave/follower nodes.
 | 
						|
	readOnly bool
 | 
						|
 | 
						|
	// DisableIndentity - Disable set-lib on connect.
 | 
						|
	//
 | 
						|
	// default: false
 | 
						|
	//
 | 
						|
	// Deprecated: Use DisableIdentity instead.
 | 
						|
	DisableIndentity bool
 | 
						|
 | 
						|
	// DisableIdentity is used to disable CLIENT SETINFO command on connect.
 | 
						|
	//
 | 
						|
	// default: false
 | 
						|
	DisableIdentity bool
 | 
						|
 | 
						|
	// Add suffix to client name. Default is empty.
 | 
						|
	// IdentitySuffix - add suffix to client name.
 | 
						|
	IdentitySuffix string
 | 
						|
 | 
						|
	// UnstableResp3 enables Unstable mode for Redis Search module with RESP3.
 | 
						|
	// When unstable mode is enabled, the client will use RESP3 protocol and only be able to use RawResult
 | 
						|
	UnstableResp3 bool
 | 
						|
 | 
						|
	// Push notifications are always enabled for RESP3 connections (Protocol: 3)
 | 
						|
	// and are not available for RESP2 connections. No configuration option is needed.
 | 
						|
 | 
						|
	// PushNotificationProcessor is the processor for handling push notifications.
 | 
						|
	// If nil, a default processor will be created for RESP3 connections.
 | 
						|
	PushNotificationProcessor push.NotificationProcessor
 | 
						|
 | 
						|
	// FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing.
 | 
						|
	// When a node is marked as failing, it will be avoided for this duration.
 | 
						|
	// Default is 15 seconds.
 | 
						|
	FailingTimeoutSeconds int
 | 
						|
 | 
						|
	// MaintNotificationsConfig provides custom configuration for maintnotifications.
 | 
						|
	// When MaintNotificationsConfig.Mode is not "disabled", the client will handle
 | 
						|
	// cluster upgrade notifications gracefully and manage connection/pool state
 | 
						|
	// transitions seamlessly. Requires Protocol: 3 (RESP3) for push notifications.
 | 
						|
	// If nil, maintnotifications are in "auto" mode and will be enabled if the server supports it.
 | 
						|
	MaintNotificationsConfig *maintnotifications.Config
 | 
						|
}
 | 
						|
 | 
						|
func (opt *Options) init() {
 | 
						|
	if opt.Addr == "" {
 | 
						|
		opt.Addr = "localhost:6379"
 | 
						|
	}
 | 
						|
	if opt.Network == "" {
 | 
						|
		if strings.HasPrefix(opt.Addr, "/") {
 | 
						|
			opt.Network = "unix"
 | 
						|
		} else {
 | 
						|
			opt.Network = "tcp"
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if opt.Protocol < 2 {
 | 
						|
		opt.Protocol = 3
 | 
						|
	}
 | 
						|
	if opt.DialTimeout == 0 {
 | 
						|
		opt.DialTimeout = 5 * time.Second
 | 
						|
	}
 | 
						|
	if opt.DialerRetries == 0 {
 | 
						|
		opt.DialerRetries = 5
 | 
						|
	}
 | 
						|
	if opt.DialerRetryTimeout == 0 {
 | 
						|
		opt.DialerRetryTimeout = 100 * time.Millisecond
 | 
						|
	}
 | 
						|
	if opt.Dialer == nil {
 | 
						|
		opt.Dialer = NewDialer(opt)
 | 
						|
	}
 | 
						|
	if opt.PoolSize == 0 {
 | 
						|
		opt.PoolSize = 10 * runtime.GOMAXPROCS(0)
 | 
						|
	}
 | 
						|
	if opt.ReadBufferSize == 0 {
 | 
						|
		opt.ReadBufferSize = proto.DefaultBufferSize
 | 
						|
	}
 | 
						|
	if opt.WriteBufferSize == 0 {
 | 
						|
		opt.WriteBufferSize = proto.DefaultBufferSize
 | 
						|
	}
 | 
						|
	switch opt.ReadTimeout {
 | 
						|
	case -2:
 | 
						|
		opt.ReadTimeout = -1
 | 
						|
	case -1:
 | 
						|
		opt.ReadTimeout = 0
 | 
						|
	case 0:
 | 
						|
		opt.ReadTimeout = 3 * time.Second
 | 
						|
	}
 | 
						|
	switch opt.WriteTimeout {
 | 
						|
	case -2:
 | 
						|
		opt.WriteTimeout = -1
 | 
						|
	case -1:
 | 
						|
		opt.WriteTimeout = 0
 | 
						|
	case 0:
 | 
						|
		opt.WriteTimeout = opt.ReadTimeout
 | 
						|
	}
 | 
						|
	if opt.PoolTimeout == 0 {
 | 
						|
		if opt.ReadTimeout > 0 {
 | 
						|
			opt.PoolTimeout = opt.ReadTimeout + time.Second
 | 
						|
		} else {
 | 
						|
			opt.PoolTimeout = 30 * time.Second
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if opt.ConnMaxIdleTime == 0 {
 | 
						|
		opt.ConnMaxIdleTime = 30 * time.Minute
 | 
						|
	}
 | 
						|
 | 
						|
	switch opt.MaxRetries {
 | 
						|
	case -1:
 | 
						|
		opt.MaxRetries = 0
 | 
						|
	case 0:
 | 
						|
		opt.MaxRetries = 3
 | 
						|
	}
 | 
						|
	switch opt.MinRetryBackoff {
 | 
						|
	case -1:
 | 
						|
		opt.MinRetryBackoff = 0
 | 
						|
	case 0:
 | 
						|
		opt.MinRetryBackoff = 8 * time.Millisecond
 | 
						|
	}
 | 
						|
	switch opt.MaxRetryBackoff {
 | 
						|
	case -1:
 | 
						|
		opt.MaxRetryBackoff = 0
 | 
						|
	case 0:
 | 
						|
		opt.MaxRetryBackoff = 512 * time.Millisecond
 | 
						|
	}
 | 
						|
 | 
						|
	opt.MaintNotificationsConfig = opt.MaintNotificationsConfig.ApplyDefaultsWithPoolConfig(opt.PoolSize, opt.MaxActiveConns)
 | 
						|
 | 
						|
	// auto-detect endpoint type if not specified
 | 
						|
	endpointType := opt.MaintNotificationsConfig.EndpointType
 | 
						|
	if endpointType == "" || endpointType == maintnotifications.EndpointTypeAuto {
 | 
						|
		// Auto-detect endpoint type if not specified
 | 
						|
		endpointType = maintnotifications.DetectEndpointType(opt.Addr, opt.TLSConfig != nil)
 | 
						|
	}
 | 
						|
	opt.MaintNotificationsConfig.EndpointType = endpointType
 | 
						|
}
 | 
						|
 | 
						|
func (opt *Options) clone() *Options {
 | 
						|
	clone := *opt
 | 
						|
 | 
						|
	// Deep clone MaintNotificationsConfig to avoid sharing between clients
 | 
						|
	if opt.MaintNotificationsConfig != nil {
 | 
						|
		configClone := *opt.MaintNotificationsConfig
 | 
						|
		clone.MaintNotificationsConfig = &configClone
 | 
						|
	}
 | 
						|
 | 
						|
	return &clone
 | 
						|
}
 | 
						|
 | 
						|
// NewDialer returns a function that will be used as the default dialer
 | 
						|
// when none is specified in Options.Dialer.
 | 
						|
func (opt *Options) NewDialer() func(context.Context, string, string) (net.Conn, error) {
 | 
						|
	return NewDialer(opt)
 | 
						|
}
 | 
						|
 | 
						|
// NewDialer returns a function that will be used as the default dialer
 | 
						|
// when none is specified in Options.Dialer.
 | 
						|
func NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, error) {
 | 
						|
	return func(ctx context.Context, network, addr string) (net.Conn, error) {
 | 
						|
		netDialer := &net.Dialer{
 | 
						|
			Timeout:   opt.DialTimeout,
 | 
						|
			KeepAlive: 5 * time.Minute,
 | 
						|
		}
 | 
						|
		if opt.TLSConfig == nil {
 | 
						|
			return netDialer.DialContext(ctx, network, addr)
 | 
						|
		}
 | 
						|
		return tls.DialWithDialer(netDialer, network, addr, opt.TLSConfig)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// ParseURL parses a URL into Options that can be used to connect to Redis.
 | 
						|
// Scheme is required.
 | 
						|
// There are two connection types: by tcp socket and by unix socket.
 | 
						|
// Tcp connection:
 | 
						|
//
 | 
						|
//	redis://<user>:<password>@<host>:<port>/<db_number>
 | 
						|
//
 | 
						|
// Unix connection:
 | 
						|
//
 | 
						|
//	unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
 | 
						|
//
 | 
						|
// Most Option fields can be set using query parameters, with the following restrictions:
 | 
						|
//   - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
 | 
						|
//   - only scalar type fields are supported (bool, int, time.Duration)
 | 
						|
//   - for time.Duration fields, values must be a valid input for time.ParseDuration();
 | 
						|
//     additionally a plain integer as value (i.e. without unit) is interpreted as seconds
 | 
						|
//   - to disable a duration field, use value less than or equal to 0; to use the default
 | 
						|
//     value, leave the value blank or remove the parameter
 | 
						|
//   - only the last value is interpreted if a parameter is given multiple times
 | 
						|
//   - fields "network", "addr", "username" and "password" can only be set using other
 | 
						|
//     URL attributes (scheme, host, userinfo, resp.), query parameters using these
 | 
						|
//     names will be treated as unknown parameters
 | 
						|
//   - unknown parameter names will result in an error
 | 
						|
//   - use "skip_verify=true" to ignore TLS certificate validation
 | 
						|
//
 | 
						|
// Examples:
 | 
						|
//
 | 
						|
//	redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
 | 
						|
//	is equivalent to:
 | 
						|
//	&Options{
 | 
						|
//		Network:     "tcp",
 | 
						|
//		Addr:        "localhost:6789",
 | 
						|
//		DB:          1,               // path "/3" was overridden by "&db=1"
 | 
						|
//		DialTimeout: 3 * time.Second, // no time unit = seconds
 | 
						|
//		ReadTimeout: 6 * time.Second,
 | 
						|
//		MaxRetries:  2,
 | 
						|
//	}
 | 
						|
func ParseURL(redisURL string) (*Options, error) {
 | 
						|
	u, err := url.Parse(redisURL)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	switch u.Scheme {
 | 
						|
	case "redis", "rediss":
 | 
						|
		return setupTCPConn(u)
 | 
						|
	case "unix":
 | 
						|
		return setupUnixConn(u)
 | 
						|
	default:
 | 
						|
		return nil, fmt.Errorf("redis: invalid URL scheme: %s", u.Scheme)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func setupTCPConn(u *url.URL) (*Options, error) {
 | 
						|
	o := &Options{Network: "tcp"}
 | 
						|
 | 
						|
	o.Username, o.Password = getUserPassword(u)
 | 
						|
 | 
						|
	h, p := getHostPortWithDefaults(u)
 | 
						|
	o.Addr = net.JoinHostPort(h, p)
 | 
						|
 | 
						|
	f := strings.FieldsFunc(u.Path, func(r rune) bool {
 | 
						|
		return r == '/'
 | 
						|
	})
 | 
						|
	switch len(f) {
 | 
						|
	case 0:
 | 
						|
		o.DB = 0
 | 
						|
	case 1:
 | 
						|
		var err error
 | 
						|
		if o.DB, err = strconv.Atoi(f[0]); err != nil {
 | 
						|
			return nil, fmt.Errorf("redis: invalid database number: %q", f[0])
 | 
						|
		}
 | 
						|
	default:
 | 
						|
		return nil, fmt.Errorf("redis: invalid URL path: %s", u.Path)
 | 
						|
	}
 | 
						|
 | 
						|
	if u.Scheme == "rediss" {
 | 
						|
		o.TLSConfig = &tls.Config{
 | 
						|
			ServerName: h,
 | 
						|
			MinVersion: tls.VersionTLS12,
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return setupConnParams(u, o)
 | 
						|
}
 | 
						|
 | 
						|
// getHostPortWithDefaults is a helper function that splits the url into
 | 
						|
// a host and a port. If the host is missing, it defaults to localhost
 | 
						|
// and if the port is missing, it defaults to 6379.
 | 
						|
func getHostPortWithDefaults(u *url.URL) (string, string) {
 | 
						|
	host, port, err := net.SplitHostPort(u.Host)
 | 
						|
	if err != nil {
 | 
						|
		host = u.Host
 | 
						|
	}
 | 
						|
	if host == "" {
 | 
						|
		host = "localhost"
 | 
						|
	}
 | 
						|
	if port == "" {
 | 
						|
		port = "6379"
 | 
						|
	}
 | 
						|
	return host, port
 | 
						|
}
 | 
						|
 | 
						|
func setupUnixConn(u *url.URL) (*Options, error) {
 | 
						|
	o := &Options{
 | 
						|
		Network: "unix",
 | 
						|
	}
 | 
						|
 | 
						|
	if strings.TrimSpace(u.Path) == "" { // path is required with unix connection
 | 
						|
		return nil, errors.New("redis: empty unix socket path")
 | 
						|
	}
 | 
						|
	o.Addr = u.Path
 | 
						|
	o.Username, o.Password = getUserPassword(u)
 | 
						|
	return setupConnParams(u, o)
 | 
						|
}
 | 
						|
 | 
						|
type queryOptions struct {
 | 
						|
	q   url.Values
 | 
						|
	err error
 | 
						|
}
 | 
						|
 | 
						|
func (o *queryOptions) has(name string) bool {
 | 
						|
	return len(o.q[name]) > 0
 | 
						|
}
 | 
						|
 | 
						|
func (o *queryOptions) string(name string) string {
 | 
						|
	vs := o.q[name]
 | 
						|
	if len(vs) == 0 {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
	delete(o.q, name) // enable detection of unknown parameters
 | 
						|
	return vs[len(vs)-1]
 | 
						|
}
 | 
						|
 | 
						|
func (o *queryOptions) strings(name string) []string {
 | 
						|
	vs := o.q[name]
 | 
						|
	delete(o.q, name)
 | 
						|
	return vs
 | 
						|
}
 | 
						|
 | 
						|
func (o *queryOptions) int(name string) int {
 | 
						|
	s := o.string(name)
 | 
						|
	if s == "" {
 | 
						|
		return 0
 | 
						|
	}
 | 
						|
	i, err := strconv.Atoi(s)
 | 
						|
	if err == nil {
 | 
						|
		return i
 | 
						|
	}
 | 
						|
	if o.err == nil {
 | 
						|
		o.err = fmt.Errorf("redis: invalid %s number: %s", name, err)
 | 
						|
	}
 | 
						|
	return 0
 | 
						|
}
 | 
						|
 | 
						|
func (o *queryOptions) duration(name string) time.Duration {
 | 
						|
	s := o.string(name)
 | 
						|
	if s == "" {
 | 
						|
		return 0
 | 
						|
	}
 | 
						|
	// try plain number first
 | 
						|
	if i, err := strconv.Atoi(s); err == nil {
 | 
						|
		if i <= 0 {
 | 
						|
			// disable timeouts
 | 
						|
			return -1
 | 
						|
		}
 | 
						|
		return time.Duration(i) * time.Second
 | 
						|
	}
 | 
						|
	dur, err := time.ParseDuration(s)
 | 
						|
	if err == nil {
 | 
						|
		return dur
 | 
						|
	}
 | 
						|
	if o.err == nil {
 | 
						|
		o.err = fmt.Errorf("redis: invalid %s duration: %w", name, err)
 | 
						|
	}
 | 
						|
	return 0
 | 
						|
}
 | 
						|
 | 
						|
func (o *queryOptions) bool(name string) bool {
 | 
						|
	switch s := o.string(name); s {
 | 
						|
	case "true", "1":
 | 
						|
		return true
 | 
						|
	case "false", "0", "":
 | 
						|
		return false
 | 
						|
	default:
 | 
						|
		if o.err == nil {
 | 
						|
			o.err = fmt.Errorf("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q", name, s)
 | 
						|
		}
 | 
						|
		return false
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (o *queryOptions) remaining() []string {
 | 
						|
	if len(o.q) == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	keys := make([]string, 0, len(o.q))
 | 
						|
	for k := range o.q {
 | 
						|
		keys = append(keys, k)
 | 
						|
	}
 | 
						|
	sort.Strings(keys)
 | 
						|
	return keys
 | 
						|
}
 | 
						|
 | 
						|
// setupConnParams converts query parameters in u to option value in o.
 | 
						|
func setupConnParams(u *url.URL, o *Options) (*Options, error) {
 | 
						|
	q := queryOptions{q: u.Query()}
 | 
						|
 | 
						|
	// compat: a future major release may use q.int("db")
 | 
						|
	if tmp := q.string("db"); tmp != "" {
 | 
						|
		db, err := strconv.Atoi(tmp)
 | 
						|
		if err != nil {
 | 
						|
			return nil, fmt.Errorf("redis: invalid database number: %w", err)
 | 
						|
		}
 | 
						|
		o.DB = db
 | 
						|
	}
 | 
						|
 | 
						|
	o.Protocol = q.int("protocol")
 | 
						|
	o.ClientName = q.string("client_name")
 | 
						|
	o.MaxRetries = q.int("max_retries")
 | 
						|
	o.MinRetryBackoff = q.duration("min_retry_backoff")
 | 
						|
	o.MaxRetryBackoff = q.duration("max_retry_backoff")
 | 
						|
	o.DialTimeout = q.duration("dial_timeout")
 | 
						|
	o.ReadTimeout = q.duration("read_timeout")
 | 
						|
	o.WriteTimeout = q.duration("write_timeout")
 | 
						|
	o.PoolFIFO = q.bool("pool_fifo")
 | 
						|
	o.PoolSize = q.int("pool_size")
 | 
						|
	o.PoolTimeout = q.duration("pool_timeout")
 | 
						|
	o.MinIdleConns = q.int("min_idle_conns")
 | 
						|
	o.MaxIdleConns = q.int("max_idle_conns")
 | 
						|
	o.MaxActiveConns = q.int("max_active_conns")
 | 
						|
	if q.has("conn_max_idle_time") {
 | 
						|
		o.ConnMaxIdleTime = q.duration("conn_max_idle_time")
 | 
						|
	} else {
 | 
						|
		o.ConnMaxIdleTime = q.duration("idle_timeout")
 | 
						|
	}
 | 
						|
	if q.has("conn_max_lifetime") {
 | 
						|
		o.ConnMaxLifetime = q.duration("conn_max_lifetime")
 | 
						|
	} else {
 | 
						|
		o.ConnMaxLifetime = q.duration("max_conn_age")
 | 
						|
	}
 | 
						|
	if q.err != nil {
 | 
						|
		return nil, q.err
 | 
						|
	}
 | 
						|
	if o.TLSConfig != nil && q.has("skip_verify") {
 | 
						|
		o.TLSConfig.InsecureSkipVerify = q.bool("skip_verify")
 | 
						|
	}
 | 
						|
 | 
						|
	// any parameters left?
 | 
						|
	if r := q.remaining(); len(r) > 0 {
 | 
						|
		return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", "))
 | 
						|
	}
 | 
						|
 | 
						|
	return o, nil
 | 
						|
}
 | 
						|
 | 
						|
func getUserPassword(u *url.URL) (string, string) {
 | 
						|
	var user, password string
 | 
						|
	if u.User != nil {
 | 
						|
		user = u.User.Username()
 | 
						|
		if p, ok := u.User.Password(); ok {
 | 
						|
			password = p
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return user, password
 | 
						|
}
 | 
						|
 | 
						|
func newConnPool(
 | 
						|
	opt *Options,
 | 
						|
	dialer func(ctx context.Context, network, addr string) (net.Conn, error),
 | 
						|
) (*pool.ConnPool, error) {
 | 
						|
	poolSize, err := util.SafeIntToInt32(opt.PoolSize, "PoolSize")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	minIdleConns, err := util.SafeIntToInt32(opt.MinIdleConns, "MinIdleConns")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	maxIdleConns, err := util.SafeIntToInt32(opt.MaxIdleConns, "MaxIdleConns")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	maxActiveConns, err := util.SafeIntToInt32(opt.MaxActiveConns, "MaxActiveConns")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return pool.NewConnPool(&pool.Options{
 | 
						|
		Dialer: func(ctx context.Context) (net.Conn, error) {
 | 
						|
			return dialer(ctx, opt.Network, opt.Addr)
 | 
						|
		},
 | 
						|
		PoolFIFO:                 opt.PoolFIFO,
 | 
						|
		PoolSize:                 poolSize,
 | 
						|
		PoolTimeout:              opt.PoolTimeout,
 | 
						|
		DialTimeout:              opt.DialTimeout,
 | 
						|
		DialerRetries:            opt.DialerRetries,
 | 
						|
		DialerRetryTimeout:       opt.DialerRetryTimeout,
 | 
						|
		MinIdleConns:             minIdleConns,
 | 
						|
		MaxIdleConns:             maxIdleConns,
 | 
						|
		MaxActiveConns:           maxActiveConns,
 | 
						|
		ConnMaxIdleTime:          opt.ConnMaxIdleTime,
 | 
						|
		ConnMaxLifetime:          opt.ConnMaxLifetime,
 | 
						|
		ReadBufferSize:           opt.ReadBufferSize,
 | 
						|
		WriteBufferSize:          opt.WriteBufferSize,
 | 
						|
		PushNotificationsEnabled: opt.Protocol == 3,
 | 
						|
	}), nil
 | 
						|
}
 | 
						|
 | 
						|
func newPubSubPool(opt *Options, dialer func(ctx context.Context, network, addr string) (net.Conn, error),
 | 
						|
) (*pool.PubSubPool, error) {
 | 
						|
	poolSize, err := util.SafeIntToInt32(opt.PoolSize, "PoolSize")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	minIdleConns, err := util.SafeIntToInt32(opt.MinIdleConns, "MinIdleConns")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	maxIdleConns, err := util.SafeIntToInt32(opt.MaxIdleConns, "MaxIdleConns")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	maxActiveConns, err := util.SafeIntToInt32(opt.MaxActiveConns, "MaxActiveConns")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return pool.NewPubSubPool(&pool.Options{
 | 
						|
		PoolFIFO:                 opt.PoolFIFO,
 | 
						|
		PoolSize:                 poolSize,
 | 
						|
		PoolTimeout:              opt.PoolTimeout,
 | 
						|
		DialTimeout:              opt.DialTimeout,
 | 
						|
		DialerRetries:            opt.DialerRetries,
 | 
						|
		DialerRetryTimeout:       opt.DialerRetryTimeout,
 | 
						|
		MinIdleConns:             minIdleConns,
 | 
						|
		MaxIdleConns:             maxIdleConns,
 | 
						|
		MaxActiveConns:           maxActiveConns,
 | 
						|
		ConnMaxIdleTime:          opt.ConnMaxIdleTime,
 | 
						|
		ConnMaxLifetime:          opt.ConnMaxLifetime,
 | 
						|
		ReadBufferSize:           32 * 1024,
 | 
						|
		WriteBufferSize:          32 * 1024,
 | 
						|
		PushNotificationsEnabled: opt.Protocol == 3,
 | 
						|
	}, dialer), nil
 | 
						|
}
 |