1package redis 2 3import ( 4 "context" 5 "crypto/tls" 6 "errors" 7 "fmt" 8 "net" 9 "net/url" 10 "runtime" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/go-redis/redis/v7/internal/pool" 16) 17 18// Limiter is the interface of a rate limiter or a circuit breaker. 19type Limiter interface { 20 // Allow returns nil if operation is allowed or an error otherwise. 21 // If operation is allowed client must ReportResult of the operation 22 // whether it is a success or a failure. 23 Allow() error 24 // ReportResult reports the result of the previously allowed operation. 25 // nil indicates a success, non-nil error usually indicates a failure. 26 ReportResult(result error) 27} 28 29type Options struct { 30 // The network type, either tcp or unix. 31 // Default is tcp. 32 Network string 33 // host:port address. 34 Addr string 35 36 // Dialer creates new network connection and has priority over 37 // Network and Addr options. 38 Dialer func(ctx context.Context, network, addr string) (net.Conn, error) 39 40 // Hook that is called when new connection is established. 41 OnConnect func(*Conn) error 42 43 // Optional password. Must match the password specified in the 44 // requirepass server configuration option. 45 Password string 46 // Database to be selected after connecting to the server. 47 DB int 48 49 // Maximum number of retries before giving up. 50 // Default is to not retry failed commands. 51 MaxRetries int 52 // Minimum backoff between each retry. 53 // Default is 8 milliseconds; -1 disables backoff. 54 MinRetryBackoff time.Duration 55 // Maximum backoff between each retry. 56 // Default is 512 milliseconds; -1 disables backoff. 57 MaxRetryBackoff time.Duration 58 59 // Dial timeout for establishing new connections. 60 // Default is 5 seconds. 61 DialTimeout time.Duration 62 // Timeout for socket reads. If reached, commands will fail 63 // with a timeout instead of blocking. Use value -1 for no timeout and 0 for default. 64 // Default is 3 seconds. 65 ReadTimeout time.Duration 66 // Timeout for socket writes. If reached, commands will fail 67 // with a timeout instead of blocking. 68 // Default is ReadTimeout. 69 WriteTimeout time.Duration 70 71 // Maximum number of socket connections. 72 // Default is 10 connections per every CPU as reported by runtime.NumCPU. 73 PoolSize int 74 // Minimum number of idle connections which is useful when establishing 75 // new connection is slow. 76 MinIdleConns int 77 // Connection age at which client retires (closes) the connection. 78 // Default is to not close aged connections. 79 MaxConnAge time.Duration 80 // Amount of time client waits for connection if all connections 81 // are busy before returning an error. 82 // Default is ReadTimeout + 1 second. 83 PoolTimeout time.Duration 84 // Amount of time after which client closes idle connections. 85 // Should be less than server's timeout. 86 // Default is 5 minutes. -1 disables idle timeout check. 87 IdleTimeout time.Duration 88 // Frequency of idle checks made by idle connections reaper. 89 // Default is 1 minute. -1 disables idle connections reaper, 90 // but idle connections are still discarded by the client 91 // if IdleTimeout is set. 92 IdleCheckFrequency time.Duration 93 94 // Enables read only queries on slave nodes. 95 readOnly bool 96 97 // TLS Config to use. When set TLS will be negotiated. 98 TLSConfig *tls.Config 99 100 // Limiter interface used to implemented circuit breaker or rate limiter. 101 Limiter Limiter 102} 103 104func (opt *Options) init() { 105 if opt.Addr == "" { 106 opt.Addr = "localhost:6379" 107 } 108 if opt.Network == "" { 109 if strings.HasPrefix(opt.Addr, "/") { 110 opt.Network = "unix" 111 } else { 112 opt.Network = "tcp" 113 } 114 } 115 if opt.Dialer == nil { 116 opt.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) { 117 netDialer := &net.Dialer{ 118 Timeout: opt.DialTimeout, 119 KeepAlive: 5 * time.Minute, 120 } 121 if opt.TLSConfig == nil { 122 return netDialer.DialContext(ctx, network, addr) 123 } 124 return tls.DialWithDialer(netDialer, network, addr, opt.TLSConfig) 125 } 126 } 127 if opt.PoolSize == 0 { 128 opt.PoolSize = 10 * runtime.NumCPU() 129 } 130 if opt.DialTimeout == 0 { 131 opt.DialTimeout = 5 * time.Second 132 } 133 switch opt.ReadTimeout { 134 case -1: 135 opt.ReadTimeout = 0 136 case 0: 137 opt.ReadTimeout = 3 * time.Second 138 } 139 switch opt.WriteTimeout { 140 case -1: 141 opt.WriteTimeout = 0 142 case 0: 143 opt.WriteTimeout = opt.ReadTimeout 144 } 145 if opt.PoolTimeout == 0 { 146 opt.PoolTimeout = opt.ReadTimeout + time.Second 147 } 148 if opt.IdleTimeout == 0 { 149 opt.IdleTimeout = 5 * time.Minute 150 } 151 if opt.IdleCheckFrequency == 0 { 152 opt.IdleCheckFrequency = time.Minute 153 } 154 155 if opt.MaxRetries == -1 { 156 opt.MaxRetries = 0 157 } 158 switch opt.MinRetryBackoff { 159 case -1: 160 opt.MinRetryBackoff = 0 161 case 0: 162 opt.MinRetryBackoff = 8 * time.Millisecond 163 } 164 switch opt.MaxRetryBackoff { 165 case -1: 166 opt.MaxRetryBackoff = 0 167 case 0: 168 opt.MaxRetryBackoff = 512 * time.Millisecond 169 } 170} 171 172func (opt *Options) clone() *Options { 173 clone := *opt 174 return &clone 175} 176 177// ParseURL parses an URL into Options that can be used to connect to Redis. 178func ParseURL(redisURL string) (*Options, error) { 179 o := &Options{Network: "tcp"} 180 u, err := url.Parse(redisURL) 181 if err != nil { 182 return nil, err 183 } 184 185 if u.Scheme != "redis" && u.Scheme != "rediss" { 186 return nil, errors.New("invalid redis URL scheme: " + u.Scheme) 187 } 188 189 if u.User != nil { 190 if p, ok := u.User.Password(); ok { 191 o.Password = p 192 } 193 } 194 195 if len(u.Query()) > 0 { 196 return nil, errors.New("no options supported") 197 } 198 199 h, p, err := net.SplitHostPort(u.Host) 200 if err != nil { 201 h = u.Host 202 } 203 if h == "" { 204 h = "localhost" 205 } 206 if p == "" { 207 p = "6379" 208 } 209 o.Addr = net.JoinHostPort(h, p) 210 211 f := strings.FieldsFunc(u.Path, func(r rune) bool { 212 return r == '/' 213 }) 214 switch len(f) { 215 case 0: 216 o.DB = 0 217 case 1: 218 if o.DB, err = strconv.Atoi(f[0]); err != nil { 219 return nil, fmt.Errorf("invalid redis database number: %q", f[0]) 220 } 221 default: 222 return nil, errors.New("invalid redis URL path: " + u.Path) 223 } 224 225 if u.Scheme == "rediss" { 226 o.TLSConfig = &tls.Config{ServerName: h} 227 } 228 return o, nil 229} 230 231func newConnPool(opt *Options) *pool.ConnPool { 232 return pool.NewConnPool(&pool.Options{ 233 Dialer: func(ctx context.Context) (net.Conn, error) { 234 return opt.Dialer(ctx, opt.Network, opt.Addr) 235 }, 236 PoolSize: opt.PoolSize, 237 MinIdleConns: opt.MinIdleConns, 238 MaxConnAge: opt.MaxConnAge, 239 PoolTimeout: opt.PoolTimeout, 240 IdleTimeout: opt.IdleTimeout, 241 IdleCheckFrequency: opt.IdleCheckFrequency, 242 }) 243} 244