mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 19:20:31 +00:00
Make cache-write failures non-fatal in GetOrSet/Remember, replace the unsynchronised default cache with an atomic pointer, fix tag-index leaks and write-lock-on-read in the memory provider, add a janitor, closed state and byte copies, hash/namespace memcache keys with CAS tag index, gate Clear behind AllowFlush, return ErrNotFound without the key, and allowlist Redis stats. Mark audit status.
181 lines
5.5 KiB
Go
181 lines
5.5 KiB
Go
package cache
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
|
)
|
|
|
|
// ErrNotFound is returned when a key is not in the cache. The key is deliberately
|
|
// not included in the error: keys may embed credentials (e.g. session tokens).
|
|
var ErrNotFound = errors.New("cache: key not found")
|
|
|
|
// Cache is the main cache manager that wraps a Provider.
|
|
type Cache struct {
|
|
provider Provider
|
|
}
|
|
|
|
// NewCache creates a new cache manager with the specified provider.
|
|
func NewCache(provider Provider) *Cache {
|
|
return &Cache{
|
|
provider: provider,
|
|
}
|
|
}
|
|
|
|
// Get retrieves and deserializes a value from the cache.
|
|
func (c *Cache) Get(ctx context.Context, key string, dest interface{}) error {
|
|
data, exists := c.provider.Get(ctx, key)
|
|
if !exists {
|
|
return ErrNotFound
|
|
}
|
|
|
|
if err := json.Unmarshal(data, dest); err != nil {
|
|
return fmt.Errorf("failed to deserialize: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBytes retrieves raw bytes from the cache.
|
|
func (c *Cache) GetBytes(ctx context.Context, key string) ([]byte, error) {
|
|
data, exists := c.provider.Get(ctx, key)
|
|
if !exists {
|
|
return nil, ErrNotFound
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// Set serializes and stores a value in the cache with the specified TTL.
|
|
func (c *Cache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize: %w", err)
|
|
}
|
|
|
|
return c.provider.Set(ctx, key, data, ttl)
|
|
}
|
|
|
|
// SetBytes stores raw bytes in the cache with the specified TTL.
|
|
func (c *Cache) SetBytes(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
|
return c.provider.Set(ctx, key, value, ttl)
|
|
}
|
|
|
|
// SetWithTags serializes and stores a value in the cache with the specified TTL and tags.
|
|
func (c *Cache) SetWithTags(ctx context.Context, key string, value interface{}, ttl time.Duration, tags []string) error {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize: %w", err)
|
|
}
|
|
|
|
return c.provider.SetWithTags(ctx, key, data, ttl, tags)
|
|
}
|
|
|
|
// SetBytesWithTags stores raw bytes in the cache with the specified TTL and tags.
|
|
func (c *Cache) SetBytesWithTags(ctx context.Context, key string, value []byte, ttl time.Duration, tags []string) error {
|
|
return c.provider.SetWithTags(ctx, key, value, ttl, tags)
|
|
}
|
|
|
|
// Delete removes a key from the cache.
|
|
func (c *Cache) Delete(ctx context.Context, key string) error {
|
|
return c.provider.Delete(ctx, key)
|
|
}
|
|
|
|
// DeleteByTag removes all keys associated with the given tag.
|
|
func (c *Cache) DeleteByTag(ctx context.Context, tag string) error {
|
|
return c.provider.DeleteByTag(ctx, tag)
|
|
}
|
|
|
|
// DeleteByPattern removes all keys matching the pattern.
|
|
func (c *Cache) DeleteByPattern(ctx context.Context, pattern string) error {
|
|
return c.provider.DeleteByPattern(ctx, pattern)
|
|
}
|
|
|
|
// Clear removes all items from the cache.
|
|
func (c *Cache) Clear(ctx context.Context) error {
|
|
return c.provider.Clear(ctx)
|
|
}
|
|
|
|
// Exists checks if a key exists in the cache.
|
|
func (c *Cache) Exists(ctx context.Context, key string) bool {
|
|
return c.provider.Exists(ctx, key)
|
|
}
|
|
|
|
// Stats returns statistics about the cache.
|
|
func (c *Cache) Stats(ctx context.Context) (*CacheStats, error) {
|
|
return c.provider.Stats(ctx)
|
|
}
|
|
|
|
// Close closes the cache and releases any resources.
|
|
func (c *Cache) Close() error {
|
|
return c.provider.Close()
|
|
}
|
|
|
|
// GetOrSet retrieves a value from cache, or sets it if it doesn't exist.
|
|
// The loader function is called only if the key is not found in cache.
|
|
func (c *Cache) GetOrSet(ctx context.Context, key string, dest interface{}, ttl time.Duration, loader func() (interface{}, error)) error {
|
|
// Try to get from cache first
|
|
err := c.Get(ctx, key, dest)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// Load the value
|
|
value, err := loader()
|
|
if err != nil {
|
|
return fmt.Errorf("loader failed: %w", err)
|
|
}
|
|
|
|
// Store in cache. A cache-write failure must not fail the call: the authoritative
|
|
// value has already been loaded, and the cache is only an optimisation.
|
|
if err := c.Set(ctx, key, value, ttl); err != nil {
|
|
logger.Warn("cache: failed to store loaded value, continuing uncached: %v", err)
|
|
}
|
|
|
|
// Populate dest with the loaded value
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize loaded value: %w", err)
|
|
}
|
|
|
|
if err := json.Unmarshal(data, dest); err != nil {
|
|
return fmt.Errorf("failed to deserialize loaded value: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Remember is a convenience function that caches the result of a function call.
|
|
// It's similar to GetOrSet but returns the value directly.
|
|
//
|
|
// WARNING: the returned type differs between a hit and a miss. On a hit the value is
|
|
// generic decoded JSON (map[string]interface{}, []interface{}, float64, string, ...);
|
|
// on a miss it is exactly what loader returned. Do not type-assert the result to a
|
|
// concrete type; prefer GetOrSet, which decodes into a typed destination.
|
|
func (c *Cache) Remember(ctx context.Context, key string, ttl time.Duration, loader func() (interface{}, error)) (interface{}, error) {
|
|
// Try to get from cache first as bytes
|
|
data, err := c.GetBytes(ctx, key)
|
|
if err == nil {
|
|
var result interface{}
|
|
if err := json.Unmarshal(data, &result); err == nil {
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
// Load the value
|
|
value, err := loader()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loader failed: %w", err)
|
|
}
|
|
|
|
// Cache-write failures are non-fatal (see GetOrSet)
|
|
if err := c.Set(ctx, key, value, ttl); err != nil {
|
|
logger.Warn("cache: failed to store loaded value, continuing uncached: %v", err)
|
|
}
|
|
|
|
return value, nil
|
|
}
|