init commit
This commit is contained in:
327
store/cache/cache.go
vendored
Normal file
327
store/cache/cache.go
vendored
Normal file
@@ -0,0 +1,327 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Interface defines the operations a cache must support.
|
||||
type Interface interface {
|
||||
// Set adds a value to the cache with the default TTL.
|
||||
Set(ctx context.Context, key string, value any)
|
||||
|
||||
// SetWithTTL adds a value to the cache with a custom TTL.
|
||||
SetWithTTL(ctx context.Context, key string, value any, ttl time.Duration)
|
||||
|
||||
// Get retrieves a value from the cache.
|
||||
Get(ctx context.Context, key string) (any, bool)
|
||||
|
||||
// Delete removes a value from the cache.
|
||||
Delete(ctx context.Context, key string)
|
||||
|
||||
// Clear removes all values from the cache.
|
||||
Clear(ctx context.Context)
|
||||
|
||||
// Size returns the number of items in the cache.
|
||||
Size() int64
|
||||
|
||||
// Close stops all background tasks and releases resources.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// item represents a cached value with metadata.
|
||||
type item struct {
|
||||
value any
|
||||
expiration time.Time
|
||||
size int // Approximate size in bytes
|
||||
}
|
||||
|
||||
// Config contains options for configuring a cache.
|
||||
type Config struct {
|
||||
// DefaultTTL is the default time-to-live for cache entries.
|
||||
DefaultTTL time.Duration
|
||||
|
||||
// CleanupInterval is how often the cache runs cleanup.
|
||||
CleanupInterval time.Duration
|
||||
|
||||
// MaxItems is the maximum number of items allowed in the cache.
|
||||
MaxItems int
|
||||
|
||||
// OnEviction is called when an item is evicted from the cache.
|
||||
OnEviction func(key string, value any)
|
||||
}
|
||||
|
||||
// DefaultConfig returns a default configuration for the cache.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
DefaultTTL: 10 * time.Minute,
|
||||
CleanupInterval: 5 * time.Minute,
|
||||
MaxItems: 1000,
|
||||
OnEviction: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// Cache is a thread-safe in-memory cache with TTL and memory management.
|
||||
type Cache struct {
|
||||
data sync.Map
|
||||
config Config
|
||||
itemCount int64 // Use atomic operations to track item count
|
||||
stopChan chan struct{}
|
||||
closedChan chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new memory cache with the given configuration.
|
||||
func New(config Config) *Cache {
|
||||
c := &Cache{
|
||||
config: config,
|
||||
stopChan: make(chan struct{}),
|
||||
closedChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
go c.cleanupLoop()
|
||||
return c
|
||||
}
|
||||
|
||||
// NewDefault creates a new memory cache with default configuration.
|
||||
func NewDefault() *Cache {
|
||||
return New(DefaultConfig())
|
||||
}
|
||||
|
||||
// Set adds a value to the cache with the default TTL.
|
||||
func (c *Cache) Set(ctx context.Context, key string, value any) {
|
||||
c.SetWithTTL(ctx, key, value, c.config.DefaultTTL)
|
||||
}
|
||||
|
||||
// SetWithTTL adds a value to the cache with a custom TTL.
|
||||
func (c *Cache) SetWithTTL(_ context.Context, key string, value any, ttl time.Duration) {
|
||||
// Estimate size of the item (very rough approximation).
|
||||
size := estimateSize(value)
|
||||
|
||||
// Check if item already exists to avoid double counting.
|
||||
if _, exists := c.data.Load(key); exists {
|
||||
c.data.Delete(key)
|
||||
} else {
|
||||
// Only increment if this is a new key.
|
||||
atomic.AddInt64(&c.itemCount, 1)
|
||||
}
|
||||
|
||||
c.data.Store(key, item{
|
||||
value: value,
|
||||
expiration: time.Now().Add(ttl),
|
||||
size: size,
|
||||
})
|
||||
|
||||
// If we're over the max items, clean up old items.
|
||||
if c.config.MaxItems > 0 && atomic.LoadInt64(&c.itemCount) > int64(c.config.MaxItems) {
|
||||
c.cleanupOldest()
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a value from the cache.
|
||||
func (c *Cache) Get(_ context.Context, key string) (any, bool) {
|
||||
value, ok := c.data.Load(key)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
itm, ok := value.(item)
|
||||
if !ok {
|
||||
// If the value is not of type item, it means it was corrupted or not set correctly.
|
||||
c.data.Delete(key)
|
||||
return nil, false
|
||||
}
|
||||
if time.Now().After(itm.expiration) {
|
||||
c.data.Delete(key)
|
||||
atomic.AddInt64(&c.itemCount, -1)
|
||||
|
||||
if c.config.OnEviction != nil {
|
||||
c.config.OnEviction(key, itm.value)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return itm.value, true
|
||||
}
|
||||
|
||||
// Delete removes a value from the cache.
|
||||
func (c *Cache) Delete(_ context.Context, key string) {
|
||||
if value, loaded := c.data.LoadAndDelete(key); loaded {
|
||||
atomic.AddInt64(&c.itemCount, -1)
|
||||
|
||||
if c.config.OnEviction != nil {
|
||||
if itm, ok := value.(item); ok {
|
||||
c.config.OnEviction(key, itm.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear removes all values from the cache.
|
||||
func (c *Cache) Clear(_ context.Context) {
|
||||
if c.config.OnEviction != nil {
|
||||
c.data.Range(func(key, value any) bool {
|
||||
itm, ok := value.(item)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if keyStr, ok := key.(string); ok {
|
||||
c.config.OnEviction(keyStr, itm.value)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
c.data = sync.Map{}
|
||||
atomic.StoreInt64(&c.itemCount, 0)
|
||||
}
|
||||
|
||||
// Size returns the number of items in the cache.
|
||||
func (c *Cache) Size() int64 {
|
||||
return atomic.LoadInt64(&c.itemCount)
|
||||
}
|
||||
|
||||
// Close stops the cache cleanup goroutine.
|
||||
func (c *Cache) Close() error {
|
||||
select {
|
||||
case <-c.stopChan:
|
||||
// Already closed
|
||||
return nil
|
||||
default:
|
||||
close(c.stopChan)
|
||||
<-c.closedChan // Wait for cleanup goroutine to exit
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupLoop periodically cleans up expired items.
|
||||
func (c *Cache) cleanupLoop() {
|
||||
ticker := time.NewTicker(c.config.CleanupInterval)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
close(c.closedChan)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.cleanup()
|
||||
case <-c.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup removes expired items.
|
||||
func (c *Cache) cleanup() {
|
||||
evicted := make(map[string]any)
|
||||
count := 0
|
||||
|
||||
c.data.Range(func(key, value any) bool {
|
||||
itm, ok := value.(item)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if time.Now().After(itm.expiration) {
|
||||
c.data.Delete(key)
|
||||
count++
|
||||
|
||||
if c.config.OnEviction != nil {
|
||||
if keyStr, ok := key.(string); ok {
|
||||
evicted[keyStr] = itm.value
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if count > 0 {
|
||||
atomic.AddInt64(&c.itemCount, -int64(count))
|
||||
|
||||
// Call eviction callbacks outside the loop to avoid blocking the range
|
||||
if c.config.OnEviction != nil {
|
||||
for k, v := range evicted {
|
||||
c.config.OnEviction(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupOldest removes the oldest items if we're over the max items.
|
||||
func (c *Cache) cleanupOldest() {
|
||||
// Remove 20% of max items at once
|
||||
threshold := max(c.config.MaxItems/5, 1)
|
||||
|
||||
currentCount := atomic.LoadInt64(&c.itemCount)
|
||||
|
||||
// If we're not over the threshold, don't do anything
|
||||
if currentCount <= int64(c.config.MaxItems) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the oldest items
|
||||
type keyExpPair struct {
|
||||
key string
|
||||
value any
|
||||
expiration time.Time
|
||||
}
|
||||
candidates := make([]keyExpPair, 0, threshold)
|
||||
|
||||
c.data.Range(func(key, value any) bool {
|
||||
itm, ok := value.(item)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if keyStr, ok := key.(string); ok && len(candidates) < threshold {
|
||||
candidates = append(candidates, keyExpPair{keyStr, itm.value, itm.expiration})
|
||||
return true
|
||||
}
|
||||
|
||||
// Find the newest item in candidates
|
||||
newestIdx := 0
|
||||
for i := 1; i < len(candidates); i++ {
|
||||
if candidates[i].expiration.After(candidates[newestIdx].expiration) {
|
||||
newestIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
// Replace it if this item is older
|
||||
if itm.expiration.Before(candidates[newestIdx].expiration) {
|
||||
candidates[newestIdx] = keyExpPair{key.(string), itm.value, itm.expiration}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Delete the oldest items
|
||||
deletedCount := 0
|
||||
for _, candidate := range candidates {
|
||||
c.data.Delete(candidate.key)
|
||||
deletedCount++
|
||||
|
||||
if c.config.OnEviction != nil {
|
||||
c.config.OnEviction(candidate.key, candidate.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Update count
|
||||
if deletedCount > 0 {
|
||||
atomic.AddInt64(&c.itemCount, -int64(deletedCount))
|
||||
}
|
||||
}
|
||||
|
||||
// estimateSize attempts to estimate the memory footprint of a value.
|
||||
func estimateSize(value any) int {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return len(v) + 24 // base size + string overhead
|
||||
case []byte:
|
||||
return len(v) + 24 // base size + slice overhead
|
||||
case map[string]any:
|
||||
return len(v) * 64 // rough estimate
|
||||
default:
|
||||
return 64 // default conservative estimate
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user