new flat KV structure

This commit is contained in:
Pierre
2026-02-08 21:53:42 +01:00
parent 4c3a0df4b4
commit 79d86b62d3
20 changed files with 2335 additions and 110 deletions
+242
View File
@@ -0,0 +1,242 @@
package cache
import (
"container/list"
"sync"
"time"
)
// LRUIndex maintains an in-memory LRU index of cache entries
type LRUIndex struct {
mu sync.RWMutex
entries map[string]*list.Element // path -> list element
lruList *list.List // LRU order (front = most recent)
maxSize int64 // Maximum total size in bytes
currSize int64 // Current total size in bytes
// Stats
hitCount int64
missCount int64
evictionCount int64
}
// lruEntry wraps CacheEntry for the linked list
type lruEntry struct {
entry *CacheEntry
}
// NewLRUIndex creates a new LRU index
func NewLRUIndex(maxSizeGB int) *LRUIndex {
return &LRUIndex{
entries: make(map[string]*list.Element),
lruList: list.New(),
maxSize: int64(maxSizeGB) * 1024 * 1024 * 1024, // GB to bytes
}
}
// Get retrieves a cache entry and updates its LRU position
// Returns nil if not found
func (idx *LRUIndex) Get(path string) *CacheEntry {
idx.mu.Lock()
defer idx.mu.Unlock()
elem, exists := idx.entries[path]
if !exists {
idx.missCount++
return nil
}
// Move to front (most recently used)
idx.lruList.MoveToFront(elem)
// Update last accessed time
entry := elem.Value.(*lruEntry).entry
entry.LastAccessed = time.Now()
idx.hitCount++
return entry
}
// Put adds or updates a cache entry
func (idx *LRUIndex) Put(entry *CacheEntry) {
idx.mu.Lock()
defer idx.mu.Unlock()
if elem, exists := idx.entries[entry.OriginalPath]; exists {
// Update existing entry
oldEntry := elem.Value.(*lruEntry).entry
idx.currSize -= oldEntry.Size
idx.currSize += entry.Size
elem.Value.(*lruEntry).entry = entry
idx.lruList.MoveToFront(elem)
} else {
// Add new entry
le := &lruEntry{entry: entry}
elem := idx.lruList.PushFront(le)
idx.entries[entry.OriginalPath] = elem
idx.currSize += entry.Size
}
}
// Remove removes a cache entry by path
// Returns the removed entry or nil if not found
func (idx *LRUIndex) Remove(path string) *CacheEntry {
idx.mu.Lock()
defer idx.mu.Unlock()
elem, exists := idx.entries[path]
if !exists {
return nil
}
entry := elem.Value.(*lruEntry).entry
idx.lruList.Remove(elem)
delete(idx.entries, path)
idx.currSize -= entry.Size
return entry
}
// GetLRUEntry returns the least recently used entry without removing it
// Returns nil if the index is empty
func (idx *LRUIndex) GetLRUEntry() *CacheEntry {
idx.mu.RLock()
defer idx.mu.RUnlock()
back := idx.lruList.Back()
if back == nil {
return nil
}
return back.Value.(*lruEntry).entry
}
// PopLRUEntry removes and returns the least recently used entry
// Returns nil if the index is empty
func (idx *LRUIndex) PopLRUEntry() *CacheEntry {
idx.mu.Lock()
defer idx.mu.Unlock()
back := idx.lruList.Back()
if back == nil {
return nil
}
entry := back.Value.(*lruEntry).entry
idx.lruList.Remove(back)
delete(idx.entries, entry.OriginalPath)
idx.currSize -= entry.Size
idx.evictionCount++
return entry
}
// GetExpiredEntries returns all entries that have exceeded the TTL
func (idx *LRUIndex) GetExpiredEntries(ttl time.Duration) []*CacheEntry {
idx.mu.RLock()
defer idx.mu.RUnlock()
var expired []*CacheEntry
cutoff := time.Now().Add(-ttl)
for elem := idx.lruList.Back(); elem != nil; elem = elem.Prev() {
entry := elem.Value.(*lruEntry).entry
if entry.CachedAt.Before(cutoff) {
expired = append(expired, entry)
}
}
return expired
}
// Size returns the current total size of cached files
func (idx *LRUIndex) Size() int64 {
idx.mu.RLock()
defer idx.mu.RUnlock()
return idx.currSize
}
// Count returns the number of entries in the index
func (idx *LRUIndex) Count() int {
idx.mu.RLock()
defer idx.mu.RUnlock()
return len(idx.entries)
}
// MaxSize returns the maximum cache size
func (idx *LRUIndex) MaxSize() int64 {
return idx.maxSize
}
// NeedsEviction returns true if the cache size exceeds the maximum
func (idx *LRUIndex) NeedsEviction() bool {
idx.mu.RLock()
defer idx.mu.RUnlock()
return idx.currSize > idx.maxSize
}
// Stats returns cache statistics
func (idx *LRUIndex) Stats() *CacheStats {
idx.mu.RLock()
defer idx.mu.RUnlock()
total := idx.hitCount + idx.missCount
var hitRate float64
if total > 0 {
hitRate = float64(idx.hitCount) / float64(total) * 100
}
return &CacheStats{
Enabled: true,
TotalSize: idx.currSize,
MaxSize: idx.maxSize,
EntryCount: len(idx.entries),
HitCount: idx.hitCount,
MissCount: idx.missCount,
EvictionCount: idx.evictionCount,
HitRate: hitRate,
}
}
// GetAllEntries returns all cache entries (for persistence)
func (idx *LRUIndex) GetAllEntries() map[string]*CacheEntry {
idx.mu.RLock()
defer idx.mu.RUnlock()
result := make(map[string]*CacheEntry, len(idx.entries))
for path, elem := range idx.entries {
result[path] = elem.Value.(*lruEntry).entry
}
return result
}
// LoadFromMap loads entries from a map (for loading persisted index)
func (idx *LRUIndex) LoadFromMap(entries map[string]*CacheEntry) {
idx.mu.Lock()
defer idx.mu.Unlock()
// Clear existing entries
idx.lruList.Init()
idx.entries = make(map[string]*list.Element)
idx.currSize = 0
// Load entries sorted by LastAccessed (oldest first for proper LRU order)
// For simplicity, we just add them in arbitrary order - the access pattern will sort them
for _, entry := range entries {
le := &lruEntry{entry: entry}
elem := idx.lruList.PushBack(le) // Add to back (oldest)
idx.entries[entry.OriginalPath] = elem
idx.currSize += entry.Size
}
}
// Clear removes all entries from the index
func (idx *LRUIndex) Clear() {
idx.mu.Lock()
defer idx.mu.Unlock()
idx.lruList.Init()
idx.entries = make(map[string]*list.Element)
idx.currSize = 0
}