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  }