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 }