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