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