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  }