github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/cache/memcached.go (about)

     1  package cache
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/bradfitz/gomemcache/memcache"
    11  	"github.com/rs/zerolog"
    12  )
    13  
    14  const (
    15  	// DefaultTimeout is the default connection timeout for memcached servers
    16  	DefaultTimeout = 100 * time.Millisecond
    17  
    18  	// DefaultMaxIdleConns is the default maximum number of idle connections per server
    19  	DefaultMaxIdleConns = 2
    20  
    21  	// DefaultRefreshInterval is the default interval for re-resolving server addresses
    22  	DefaultRefreshInterval = 60 * time.Second
    23  
    24  	// DNSResolveTimeout is the timeout for DNS resolution operations
    25  	DNSResolveTimeout = 5 * time.Second
    26  
    27  	// MaxKeyLength is the maximum key length supported by memcached
    28  	MaxKeyLength = 250
    29  )
    30  
    31  // Resolver is an interface for DNS resolution to allow injection in tests.
    32  type Resolver interface {
    33  	LookupIP(ctx context.Context, network, host string) ([]net.IP, error)
    34  }
    35  
    36  // MemcachedConfig holds configuration for the cache client.
    37  type MemcachedConfig struct {
    38  	// Servers is the list of memcached server addresses (host:port)
    39  	Servers []string
    40  	// Logger is the logger instance for the cache client
    41  	Logger zerolog.Logger
    42  	// Timeout is the connection timeout for memcached operations (optional, defaults to 100ms)
    43  	Timeout time.Duration
    44  	// MaxIdleConns is the maximum number of idle connections per server (optional, defaults to 2)
    45  	MaxIdleConns int
    46  	// RefreshInterval is the interval for re-resolving server addresses (optional, defaults to 60s)
    47  	RefreshInterval time.Duration
    48  	// Resolver is the DNS resolver to use (optional, defaults to net.DefaultResolver)
    49  	// This is primarily for testing purposes.
    50  	Resolver Resolver
    51  }
    52  
    53  // MemcachedClient wraps the memcached client with additional functionality.
    54  type MemcachedClient struct {
    55  	mc              *memcache.Client
    56  	logger          zerolog.Logger
    57  	serverList      *memcache.ServerList
    58  	originalServers []string
    59  	cancel          context.CancelFunc
    60  	resolver        Resolver
    61  }
    62  
    63  const KindMemcached Kind = "memcached"
    64  
    65  // Ensure Client implements Cache interface at compile time
    66  var _ Cache = (*MemcachedClient)(nil)
    67  
    68  // NewMemcachedClient creates a new memcached cache client with the provided configuration.
    69  // It validates the configuration and returns an error if invalid.
    70  // Returns a Cache interface that can be used for all cache operations.
    71  func NewMemcachedClient(ctx context.Context, config MemcachedConfig) (*MemcachedClient, error) {
    72  	// Validate that at least one server is provided
    73  	if len(config.Servers) == 0 {
    74  		return nil, fmt.Errorf("at least one memcached server must be provided")
    75  	}
    76  
    77  	// Validate server addresses
    78  	for i, server := range config.Servers {
    79  		server = strings.TrimSpace(server)
    80  		if server == "" {
    81  			return nil, fmt.Errorf("server address at index %d is empty", i)
    82  		}
    83  		// Validate host:port format
    84  		if _, _, err := net.SplitHostPort(server); err != nil {
    85  			return nil, fmt.Errorf("invalid server address %q: %w", server, err)
    86  		}
    87  
    88  		config.Servers[i] = server
    89  	}
    90  
    91  	// Apply defaults
    92  	if config.Timeout == 0 {
    93  		config.Timeout = DefaultTimeout
    94  	}
    95  
    96  	if config.MaxIdleConns == 0 {
    97  		config.MaxIdleConns = DefaultMaxIdleConns
    98  	}
    99  
   100  	if config.RefreshInterval == 0 {
   101  		config.RefreshInterval = DefaultRefreshInterval
   102  	}
   103  
   104  	// Use default resolver if none provided
   105  	resolver := config.Resolver
   106  	if resolver == nil {
   107  		resolver = net.DefaultResolver
   108  	}
   109  
   110  	// Create a ServerList selector for dynamic address resolution
   111  	serverList := &memcache.ServerList{}
   112  
   113  	// Resolve initial server addresses
   114  	logger := config.Logger.With().Str("component", "cache").Logger()
   115  	resolved := resolveServers(ctx, config.Servers, resolver, logger)
   116  
   117  	// Ensure we have at least one resolved server
   118  	if len(resolved) == 0 {
   119  		return nil, fmt.Errorf("no servers could be resolved from the provided list: %v", config.Servers)
   120  	}
   121  
   122  	// Set initial servers in the selector
   123  	if err := serverList.SetServers(resolved...); err != nil {
   124  		return nil, fmt.Errorf("failed to set initial servers: %w", err)
   125  	}
   126  
   127  	// Create memcache client with the selector
   128  	mc := memcache.NewFromSelector(serverList)
   129  	mc.Timeout = config.Timeout
   130  	mc.MaxIdleConns = config.MaxIdleConns
   131  
   132  	// Create context for the refresh goroutine
   133  	refreshCtx, cancel := context.WithCancel(ctx)
   134  
   135  	client := &MemcachedClient{
   136  		mc:              mc,
   137  		logger:          logger,
   138  		serverList:      serverList,
   139  		originalServers: config.Servers,
   140  		cancel:          cancel,
   141  		resolver:        resolver,
   142  	}
   143  
   144  	// Start background goroutine to periodically refresh server addresses
   145  	go client.refreshServers(refreshCtx, config.RefreshInterval)
   146  
   147  	client.logger.Info().
   148  		Strs("servers", config.Servers).
   149  		Int("resolved_count", len(resolved)).
   150  		Dur("timeout", config.Timeout).
   151  		Int("max_idle_conns", config.MaxIdleConns).
   152  		Dur("refresh_interval", config.RefreshInterval).
   153  		Msg("cache client initialized with dynamic address resolution")
   154  
   155  	return client, nil
   156  }
   157  
   158  // Set stores a value in the cache with the specified expiration time.
   159  // The value is serialized using gob encoding before storage.
   160  //
   161  // Key must be non-empty and at most 250 bytes.
   162  // Expiration is converted to seconds (memcached requires int32 seconds).
   163  // Use expiration of 0 for no expiration (cache default).
   164  //
   165  // Returns an error if:
   166  //   - key is invalid (empty or too long)
   167  //   - value cannot be serialized
   168  //   - memcached operation fails
   169  func (c *MemcachedClient) Set(ctx context.Context, key string, value any, expiration time.Duration) error {
   170  	// Validate key
   171  	if err := validateKey(key); err != nil {
   172  		return err
   173  	}
   174  
   175  	// Serialize value
   176  	data, err := encode(value)
   177  	if err != nil {
   178  		c.logger.Error().Err(err).Str("key", key).Msg("failed to encode value")
   179  		return fmt.Errorf("cache set: %w", err)
   180  	}
   181  
   182  	// Convert expiration to seconds (memcached uses int32 seconds)
   183  	expirationSec := int32(expiration.Seconds())
   184  
   185  	// Store in memcached
   186  	item := &memcache.Item{
   187  		Key:        key,
   188  		Value:      data,
   189  		Expiration: expirationSec,
   190  	}
   191  
   192  	if err := c.mc.Set(item); err != nil {
   193  		c.logger.Error().Err(err).Str("key", key).Msg("cache set failed")
   194  		return fmt.Errorf("cache set: %w", err)
   195  	}
   196  
   197  	c.logger.Debug().
   198  		Str("key", key).
   199  		Int("size", len(data)).
   200  		Dur("expiration", expiration).
   201  		Msg("cache set")
   202  
   203  	return nil
   204  }
   205  
   206  // Get retrieves a value from the cache and deserializes it into dest.
   207  // The dest parameter must be a pointer to the target type.
   208  //
   209  // Returns ErrCacheMiss if the key is not found in the cache.
   210  // Returns an error if:
   211  //   - key is invalid (empty or too long)
   212  //   - dest is not a pointer
   213  //   - value cannot be deserialized
   214  //   - memcached operation fails (other than cache miss)
   215  func (c *MemcachedClient) Get(ctx context.Context, key string, dest any) error {
   216  	// Validate key
   217  	if err := validateKey(key); err != nil {
   218  		return err
   219  	}
   220  
   221  	// Get from memcached
   222  	item, err := c.mc.Get(key)
   223  	if err == memcache.ErrCacheMiss {
   224  		c.logger.Debug().Str("key", key).Msg("cache miss")
   225  		return ErrCacheMiss
   226  	}
   227  
   228  	if err != nil {
   229  		c.logger.Error().Err(err).Str("key", key).Msg("cache get failed")
   230  		return fmt.Errorf("cache get: %w", err)
   231  	}
   232  
   233  	// Deserialize value
   234  	if err := decode(item.Value, dest); err != nil {
   235  		c.logger.Error().Err(err).Str("key", key).Msg("failed to decode value")
   236  		return fmt.Errorf("cache get: %w", err)
   237  	}
   238  
   239  	c.logger.Debug().
   240  		Str("key", key).
   241  		Int("size", len(item.Value)).
   242  		Msg("cache hit")
   243  
   244  	return nil
   245  }
   246  
   247  // Delete removes a key from the cache.
   248  //
   249  // Returns nil if the key doesn't exist (not an error).
   250  // Returns an error if:
   251  //   - key is invalid (empty or too long)
   252  //   - memcached operation fails (other than cache miss)
   253  func (c *MemcachedClient) Delete(ctx context.Context, key string) error {
   254  	// Validate key
   255  	if err := validateKey(key); err != nil {
   256  		return err
   257  	}
   258  
   259  	// Delete from memcached
   260  	if err := c.mc.Delete(key); err == memcache.ErrCacheMiss {
   261  		// Key doesn't exist, not an error
   262  		c.logger.Debug().Str("key", key).Msg("cache delete - key not found")
   263  		return nil
   264  	} else if err != nil {
   265  		c.logger.Error().Err(err).Str("key", key).Msg("cache delete failed")
   266  		return fmt.Errorf("cache delete: %w", err)
   267  	}
   268  
   269  	c.logger.Debug().Str("key", key).Msg("cache delete")
   270  
   271  	return nil
   272  }
   273  
   274  // Flush removes all items from the cache.
   275  // Use this operation cautiously as it affects all cache users.
   276  func (c *MemcachedClient) Flush(ctx context.Context) error {
   277  	if err := c.mc.FlushAll(); err != nil {
   278  		c.logger.Error().Err(err).Msg("cache flush failed")
   279  		return fmt.Errorf("cache flush: %w", err)
   280  	}
   281  
   282  	c.logger.Info().Msg("cache flushed")
   283  
   284  	return nil
   285  }
   286  
   287  // Close stops the background server address refresh goroutine by cancelling its context.
   288  //
   289  // This method is optional if the parent context passed to NewMemcachedClient is already
   290  // cancelled during application shutdown. The refresh goroutine will automatically stop
   291  // when the parent context is cancelled.
   292  //
   293  // Call this method explicitly only if you need to stop the refresh goroutine before
   294  // the application shuts down or if you're managing the client lifecycle independently.
   295  //
   296  // Note: This is not part of the Cache interface, so it must be called explicitly
   297  // if cleanup is needed.
   298  func (c *MemcachedClient) Close() error {
   299  	if c.cancel != nil {
   300  		c.cancel()
   301  		c.logger.Debug().Msg("cache client closed")
   302  	}
   303  
   304  	return nil
   305  }
   306  
   307  // validateKey validates a memcached key.
   308  func validateKey(key string) error {
   309  	if key == "" {
   310  		return fmt.Errorf("key cannot be empty")
   311  	}
   312  
   313  	if len(key) > MaxKeyLength {
   314  		return fmt.Errorf("key length %d exceeds maximum %d", len(key), MaxKeyLength)
   315  	}
   316  
   317  	return nil
   318  }
   319  
   320  // resolveServers resolves all hostnames in the server list to their IP addresses.
   321  // Each hostname is resolved to potentially multiple IP addresses.
   322  // Returns a slice of "ip:port" strings ready for use with memcache.ServerList.SetServers.
   323  // IP addresses are used as-is without resolution.
   324  // Hostnames that fail to resolve are skipped (not included in the result).
   325  func resolveServers(ctx context.Context, servers []string, resolver Resolver, logger zerolog.Logger) []string {
   326  	var resolved []string
   327  
   328  	for _, server := range servers {
   329  		host, port, err := net.SplitHostPort(server)
   330  		if err != nil {
   331  			logger.Warn().Err(err).Str("server", server).Msg("failed to parse server address, skipping")
   332  			continue
   333  		}
   334  
   335  		// Check if host is already an IP address
   336  		if ip := net.ParseIP(host); ip != nil {
   337  			// Already an IP address, use as-is
   338  			resolved = append(resolved, server)
   339  			logger.Debug().Str("server", server).Msg("using IP address directly")
   340  
   341  			continue
   342  		}
   343  
   344  		// Try to resolve the hostname
   345  		ips, err := resolver.LookupIP(ctx, "ip", host)
   346  		if err != nil {
   347  			logger.Warn().Err(err).Str("host", host).Msg("failed to resolve hostname, skipping server")
   348  
   349  			continue
   350  		}
   351  
   352  		if len(ips) == 0 {
   353  			logger.Warn().Str("host", host).Msg("no IPs resolved for hostname, skipping server")
   354  
   355  			continue
   356  		}
   357  
   358  		// Add all resolved IPs with the port
   359  		for _, ip := range ips {
   360  			addr := net.JoinHostPort(ip.String(), port)
   361  			resolved = append(resolved, addr)
   362  		}
   363  
   364  		logger.Debug().
   365  			Str("host", host).
   366  			Int("ip_count", len(ips)).
   367  			Strs("resolved", resolved[len(resolved)-len(ips):]).
   368  			Msg("resolved server addresses")
   369  	}
   370  
   371  	return resolved
   372  }
   373  
   374  // refreshServers periodically re-resolves server addresses and updates the ServerList.
   375  func (c *MemcachedClient) refreshServers(ctx context.Context, interval time.Duration) {
   376  	ticker := time.NewTicker(interval)
   377  	defer ticker.Stop()
   378  
   379  	for {
   380  		select {
   381  		case <-ctx.Done():
   382  			c.logger.Debug().Msg("stopping server address refresh")
   383  
   384  			return
   385  
   386  		case <-ticker.C:
   387  			// Create a timeout context for DNS resolution to prevent hanging
   388  			resolveCtx, cancel := context.WithTimeout(ctx, DNSResolveTimeout)
   389  			resolved := resolveServers(resolveCtx, c.originalServers, c.resolver, c.logger)
   390  
   391  			cancel()
   392  
   393  			if len(resolved) == 0 {
   394  				c.logger.Error().
   395  					Strs("servers", c.originalServers).
   396  					Msg("failed to refresh server addresses")
   397  
   398  				continue
   399  			}
   400  
   401  			if err := c.serverList.SetServers(resolved...); err != nil {
   402  				c.logger.Error().
   403  					Err(err).
   404  					Strs("servers", resolved).
   405  					Msg("failed to update server addresses")
   406  
   407  				continue
   408  			}
   409  
   410  			c.logger.Debug().
   411  				Int("server_count", len(resolved)).
   412  				Strs("servers", resolved).
   413  				Msg("refreshed server addresses")
   414  		}
   415  	}
   416  }