mirror of
https://github.com/redis/go-redis.git
synced 2025-07-18 00:20:57 +03:00
- Add PushNotificationProcessor field to pool.Conn for connection-level processing - Modify connection pool Put() and isHealthyConn() to handle push notifications - Process pending push notifications before discarding connections - Pass push notification processor to connections during creation - Update connection pool options to include push notification processor - Add comprehensive test for connection health check integration This prevents connections with buffered push notification data from being incorrectly discarded by the connection health check, ensuring push notifications are properly processed and connections are reused.
614 lines
17 KiB
Go
614 lines
17 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"
|
|
)
|
|
|
|
// 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
|
|
|
|
// 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
|
|
|
|
// 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.
|
|
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
|
|
|
|
// PushNotifications enables general push notification processing.
|
|
// When enabled, the client will process RESP3 push notifications and
|
|
// route them to registered handlers.
|
|
//
|
|
// For RESP3 connections (Protocol: 3), push notifications are automatically enabled.
|
|
// To disable push notifications for RESP3, use Protocol: 2 instead.
|
|
// For RESP2 connections, push notifications are not available.
|
|
//
|
|
// default: automatically enabled for RESP3, disabled for RESP2
|
|
PushNotifications bool
|
|
|
|
// PushNotificationProcessor is the processor for handling push notifications.
|
|
// If nil, a default processor will be created when PushNotifications is enabled.
|
|
PushNotificationProcessor *PushNotificationProcessor
|
|
}
|
|
|
|
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.Dialer == nil {
|
|
opt.Dialer = NewDialer(opt)
|
|
}
|
|
if opt.PoolSize == 0 {
|
|
opt.PoolSize = 10 * runtime.GOMAXPROCS(0)
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
func (opt *Options) clone() *Options {
|
|
clone := *opt
|
|
return &clone
|
|
}
|
|
|
|
// 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 {
|
|
return pool.NewConnPool(&pool.Options{
|
|
Dialer: func(ctx context.Context) (net.Conn, error) {
|
|
return dialer(ctx, opt.Network, opt.Addr)
|
|
},
|
|
PoolFIFO: opt.PoolFIFO,
|
|
PoolSize: opt.PoolSize,
|
|
PoolTimeout: opt.PoolTimeout,
|
|
DialTimeout: opt.DialTimeout,
|
|
MinIdleConns: opt.MinIdleConns,
|
|
MaxIdleConns: opt.MaxIdleConns,
|
|
MaxActiveConns: opt.MaxActiveConns,
|
|
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
|
ConnMaxLifetime: opt.ConnMaxLifetime,
|
|
// Pass push notification processor for connection initialization
|
|
PushNotificationProcessor: opt.PushNotificationProcessor,
|
|
})
|
|
}
|