github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/cache/local.go (about) 1 package cache 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/maypok86/otter/v2" 9 "github.com/rs/zerolog" 10 ) 11 12 // LocalConfig holds configuration for the local in-memory cache. 13 type LocalConfig struct { 14 // InitialCapacity is the initial capacity of the cache 15 InitialCapacity int 16 // MaxCapacity is the maximum capacity (number of items) in the cache 17 MaxCapacity int 18 // DefaultTTL is the default time-to-live for items (if expiration not specified in Set) 19 DefaultTTL time.Duration 20 // Logger for debug logging 21 Logger zerolog.Logger 22 } 23 24 // Local is an in-memory cache implementation using Otter. 25 // It provides fast, thread-safe caching with automatic eviction and TTL support. 26 type Local struct { 27 cache *otter.Cache[string, []byte] 28 logger zerolog.Logger 29 defaultTTL time.Duration 30 } 31 32 const KindLocal Kind = "local" 33 34 // Ensure Local implements Cache interface at compile time 35 var _ Cache = (*Local)(nil) 36 37 // NewLocal creates a new in-memory cache with the specified configuration. 38 // It uses Otter for high-performance, thread-safe caching with automatic eviction. 39 // 40 // Returns an error if: 41 // - MaxCapacity is not positive 42 // - Cache initialization fails 43 func NewLocal(config LocalConfig) (*Local, error) { 44 // Validate configuration 45 if config.MaxCapacity <= 0 { 46 return nil, fmt.Errorf("max capacity must be positive, got %d", config.MaxCapacity) 47 } 48 49 // Build Otter cache with configuration 50 opts := &otter.Options[string, []byte]{ 51 MaximumSize: config.MaxCapacity, 52 InitialCapacity: config.InitialCapacity, 53 ExpiryCalculator: otter.ExpiryWriting[string, []byte](24 * time.Hour), 54 } 55 56 // Configure default expiration policy to enable per-item TTL with SetExpiresAfter 57 // Use a very long default TTL that will be overridden per-item 58 if config.DefaultTTL > 0 { 59 opts.ExpiryCalculator = otter.ExpiryWriting[string, []byte](config.DefaultTTL) 60 } 61 62 cache, err := otter.New(opts) 63 if err != nil { 64 return nil, fmt.Errorf("failed to build cache: %w", err) 65 } 66 67 l := &Local{ 68 cache: cache, 69 logger: config.Logger.With().Str("component", "cache").Str("type", "local").Logger(), 70 defaultTTL: config.DefaultTTL, 71 } 72 73 l.logger.Info(). 74 Int("max_capacity", config.MaxCapacity). 75 Int("initial_capacity", config.InitialCapacity). 76 Dur("default_ttl", config.DefaultTTL). 77 Msg("local cache initialized") 78 79 return l, nil 80 } 81 82 // Set stores a value in the cache with the specified expiration time. 83 // The value is serialized using gob encoding before storage. 84 // 85 // If expiration is 0, the default TTL from LocalConfig is used. 86 func (l *Local) Set(ctx context.Context, key string, value any, expiration time.Duration) error { 87 // Serialize value using existing encode function. In principle this is not necessary with Otter, but we need to 88 // support the common interface that uses `any` values. 89 data, err := encode(value) 90 if err != nil { 91 l.logger.Error().Err(err).Str("key", key).Msg("failed to encode value") 92 return fmt.Errorf("cache set: %w", err) 93 } 94 95 // Use provided expiration or default TTL 96 ttl := expiration 97 if ttl == 0 { 98 ttl = l.defaultTTL 99 } 100 101 // Set the value 102 l.cache.Set(key, data) 103 104 // Set expiration if TTL is specified 105 if ttl > 0 { 106 l.cache.SetExpiresAfter(key, ttl) 107 } 108 109 l.logger.Debug().Str("key", key).Dur("ttl", ttl).Int("size", len(data)).Msg("local cache set") 110 111 return nil 112 } 113 114 // Get retrieves a value from the cache and deserializes it into dest. 115 // Returns ErrCacheMiss if the key is not found or has expired. 116 func (l *Local) Get(ctx context.Context, key string, dest any) error { 117 data, ok := l.cache.GetIfPresent(key) 118 if !ok { 119 l.logger.Debug().Str("key", key).Msg("local cache miss") 120 121 return ErrCacheMiss 122 } 123 124 if err := decode(data, dest); err != nil { 125 l.logger.Error().Err(err).Str("key", key).Msg("failed to decode value") 126 127 return fmt.Errorf("cache get: %w", err) 128 } 129 130 l.logger.Debug().Str("key", key).Int("size", len(data)).Msg("local cache hit") 131 132 return nil 133 } 134 135 // Delete removes a key from the cache. 136 // 137 // Returns nil if the key doesn't exist (idempotent). 138 func (l *Local) Delete(ctx context.Context, key string) error { 139 l.cache.Invalidate(key) 140 l.logger.Debug().Str("key", key).Msg("local cache delete") 141 142 return nil 143 } 144 145 // Flush removes all items from the cache. 146 func (l *Local) Flush(ctx context.Context) error { 147 l.cache.InvalidateAll() 148 l.logger.Info().Msg("local cache flushed") 149 150 return nil 151 } 152 153 // Close cleanly shuts down the cache. 154 // 155 // Otter performs cleanup operations to release resources. 156 func (l *Local) Close() error { 157 l.cache.CleanUp() 158 l.logger.Info().Msg("local cache closed") 159 160 return nil 161 } 162 163 // Size returns the current number of items in the cache. 164 // 165 // This is useful for monitoring and debugging. 166 func (l *Local) Size() int { 167 return l.cache.EstimatedSize() 168 } 169 170 // Capacity returns the maximum capacity of the cache. 171 func (l *Local) Capacity() int { 172 return int(l.cache.GetMaximum()) 173 }