1 package redis 2 3 import ( 4 "fmt" 5 6 "github.com/letsencrypt/boulder/cmd" 7 "github.com/letsencrypt/boulder/config" 8 blog "github.com/letsencrypt/boulder/log" 9 "github.com/prometheus/client_golang/prometheus" 10 "github.com/redis/go-redis/v9" 11 ) 12 13 // Config contains the configuration needed to act as a Redis client. 14 type Config struct { 15 // TLS contains the configuration to speak TLS with Redis. 16 TLS cmd.TLSConfig 17 18 // Username used to authenticate to each Redis instance. 19 Username string `validate:"required"` 20 21 // PasswordFile is the path to a file holding the password used to 22 // authenticate to each Redis instance. 23 cmd.PasswordConfig 24 25 // ShardAddrs is a map of shard names to IP address:port pairs. The go-redis 26 // `Ring` client will shard reads and writes across the provided Redis 27 // Servers based on a consistent hashing algorithm. 28 ShardAddrs map[string]string `validate:"omitempty,required_without=Lookups,min=1,dive,hostname_port"` 29 30 // Lookups each entry contains a service and domain name that will be used 31 // to construct a SRV DNS query to lookup Redis backends. For example: if 32 // the resource record is 'foo.service.consul', then the 'Service' is 'foo' 33 // and the 'Domain' is 'service.consul'. The expected dNSName to be 34 // authenticated in the server certificate would be 'foo.service.consul'. 35 Lookups []cmd.ServiceDomain `validate:"omitempty,required_without=ShardAddrs,min=1,dive"` 36 37 // LookupFrequency is the frequency of periodic SRV lookups. Defaults to 30 38 // seconds. 39 LookupFrequency config.Duration `validate:"-"` 40 41 // LookupDNSAuthority can only be specified with Lookups. It's a single 42 // <hostname|IPv4|[IPv6]>:<port> of the DNS server to be used for resolution 43 // of Redis backends. If the address contains a hostname it will be resolved 44 // using system DNS. If the address contains a port, the client will use it 45 // directly, otherwise port 53 is used. If this field is left unspecified 46 // the system DNS will be used for resolution. 47 LookupDNSAuthority string `validate:"excluded_without=Lookups,omitempty,ip|hostname|hostname_port"` 48 49 // Enables read-only commands on replicas. 50 ReadOnly bool 51 // Allows routing read-only commands to the closest primary or replica. 52 // It automatically enables ReadOnly. 53 RouteByLatency bool 54 // Allows routing read-only commands to a random primary or replica. 55 // It automatically enables ReadOnly. 56 RouteRandomly bool 57 58 // PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO). 59 PoolFIFO bool 60 61 // Maximum number of retries before giving up. 62 // Default is to not retry failed commands. 63 MaxRetries int `validate:"min=0"` 64 // Minimum backoff between each retry. 65 // Default is 8 milliseconds; -1 disables backoff. 66 MinRetryBackoff config.Duration `validate:"-"` 67 // Maximum backoff between each retry. 68 // Default is 512 milliseconds; -1 disables backoff. 69 MaxRetryBackoff config.Duration `validate:"-"` 70 71 // Dial timeout for establishing new connections. 72 // Default is 5 seconds. 73 DialTimeout config.Duration `validate:"-"` 74 // Timeout for socket reads. If reached, commands will fail 75 // with a timeout instead of blocking. Use value -1 for no timeout and 0 for default. 76 // Default is 3 seconds. 77 ReadTimeout config.Duration `validate:"-"` 78 // Timeout for socket writes. If reached, commands will fail 79 // with a timeout instead of blocking. 80 // Default is ReadTimeout. 81 WriteTimeout config.Duration `validate:"-"` 82 83 // Maximum number of socket connections. 84 // Default is 5 connections per every CPU as reported by runtime.NumCPU. 85 // If this is set to an explicit value, that's not multiplied by NumCPU. 86 // PoolSize applies per cluster node and not for the whole cluster. 87 // https://pkg.go.dev/github.com/go-redis/redis#ClusterOptions 88 PoolSize int `validate:"min=0"` 89 // Minimum number of idle connections which is useful when establishing 90 // new connection is slow. 91 MinIdleConns int `validate:"min=0"` 92 // Connection age at which client retires (closes) the connection. 93 // Default is to not close aged connections. 94 MaxConnAge config.Duration `validate:"-"` 95 // Amount of time client waits for connection if all connections 96 // are busy before returning an error. 97 // Default is ReadTimeout + 1 second. 98 PoolTimeout config.Duration `validate:"-"` 99 // Amount of time after which client closes idle connections. 100 // Should be less than server's timeout. 101 // Default is 5 minutes. -1 disables idle timeout check. 102 IdleTimeout config.Duration `validate:"-"` 103 // Frequency of idle checks made by idle connections reaper. 104 // Default is 1 minute. -1 disables idle connections reaper, 105 // but idle connections are still discarded by the client 106 // if IdleTimeout is set. 107 // Deprecated: This field has been deprecated and will be removed. 108 IdleCheckFrequency config.Duration `validate:"-"` 109 } 110 111 // Ring is a wrapper around the go-redis/v9 Ring client that adds support for 112 // (optional) periodic SRV lookups. 113 type Ring struct { 114 *redis.Ring 115 lookup *lookup 116 } 117 118 // NewRingFromConfig returns a new *redis.Ring client. If periodic SRV lookups 119 // are supplied, a goroutine will be started to periodically perform lookups. 120 // Callers should defer a call to StopLookups() to ensure that this goroutine is 121 // gracefully shutdown. 122 func NewRingFromConfig(c Config, stats prometheus.Registerer, log blog.Logger) (*Ring, error) { 123 password, err := c.Pass() 124 if err != nil { 125 return nil, fmt.Errorf("loading password: %w", err) 126 } 127 128 tlsConfig, err := c.TLS.Load(stats) 129 if err != nil { 130 return nil, fmt.Errorf("loading TLS config: %w", err) 131 } 132 133 inner := redis.NewRing(&redis.RingOptions{ 134 Addrs: c.ShardAddrs, 135 Username: c.Username, 136 Password: password, 137 TLSConfig: tlsConfig, 138 139 MaxRetries: c.MaxRetries, 140 MinRetryBackoff: c.MinRetryBackoff.Duration, 141 MaxRetryBackoff: c.MaxRetryBackoff.Duration, 142 DialTimeout: c.DialTimeout.Duration, 143 ReadTimeout: c.ReadTimeout.Duration, 144 WriteTimeout: c.WriteTimeout.Duration, 145 146 PoolSize: c.PoolSize, 147 MinIdleConns: c.MinIdleConns, 148 ConnMaxLifetime: c.MaxConnAge.Duration, 149 PoolTimeout: c.PoolTimeout.Duration, 150 ConnMaxIdleTime: c.IdleTimeout.Duration, 151 }) 152 if len(c.ShardAddrs) > 0 { 153 // Client was statically configured with a list of shards. 154 MustRegisterClientMetricsCollector(inner, stats, c.ShardAddrs, c.Username) 155 } 156 157 var lookup *lookup 158 if len(c.Lookups) != 0 { 159 lookup, err = newLookup(c.Lookups, c.LookupDNSAuthority, c.LookupFrequency.Duration, inner, log, stats) 160 if err != nil { 161 return nil, err 162 } 163 lookup.start() 164 } 165 166 return &Ring{ 167 Ring: inner, 168 lookup: lookup, 169 }, nil 170 } 171 172 // StopLookups stops the goroutine responsible for keeping the shards of the 173 // inner *redis.Ring up-to-date. It is a no-op if the Ring was not constructed 174 // with periodic lookups or if the lookups have already been stopped. 175 func (r *Ring) StopLookups() { 176 if r == nil || r.lookup == nil { 177 // No-op. 178 return 179 } 180 r.lookup.stop() 181 } 182