// Package pregel provides caching support for Pregel execution. package pregel import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "sync" "time" "ragflow/internal/harness/graph/types" ) // Cache is the interface for caching node outputs. type Cache interface { // Get retrieves a value from the cache. Get(ctx context.Context, key string) (any, bool) // Set stores a value in the cache. Set(ctx context.Context, key string, value any, ttl time.Duration) // Delete removes a value from the cache. Delete(ctx context.Context, key string) // Clear clears all values from the cache. Clear() } // MemoryCache is an in-memory cache implementation. type MemoryCache struct { mu sync.RWMutex data map[string]*cacheEntry maxSize int eviction EvictionPolicy } type cacheEntry struct { value any expiration time.Time lastAccess time.Time hits int64 } // EvictionPolicy determines how entries are evicted when cache is full. type EvictionPolicy int const ( // EvictLRU evicts least recently used entries. EvictLRU EvictionPolicy = iota // EvictLFU evicts least frequently used entries. EvictLFU // EvictRandom evicts random entries. EvictRandom ) // NewMemoryCache creates a new in-memory cache. func NewMemoryCache(maxSize int, eviction EvictionPolicy) *MemoryCache { return &MemoryCache{ data: make(map[string]*cacheEntry), maxSize: maxSize, eviction: eviction, } } // Get retrieves a value from the cache. func (c *MemoryCache) Get(ctx context.Context, key string) (any, bool) { c.mu.Lock() defer c.mu.Unlock() entry, ok := c.data[key] if !ok { return nil, false } // Check expiration if !entry.expiration.IsZero() && time.Now().After(entry.expiration) { return nil, false } entry.hits++ entry.lastAccess = time.Now() return entry.value, true } // Set stores a value in the cache. func (c *MemoryCache) Set(ctx context.Context, key string, value any, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() // Evict entries if cache is full if len(c.data) >= c.maxSize { c.evict() } expiration := time.Time{} if ttl > 0 { expiration = time.Now().Add(ttl) } c.data[key] = &cacheEntry{ value: value, expiration: expiration, lastAccess: time.Now(), hits: 0, } } // Delete removes a value from the cache. func (c *MemoryCache) Delete(ctx context.Context, key string) { c.mu.Lock() defer c.mu.Unlock() delete(c.data, key) } // Clear clears all values from the cache. func (c *MemoryCache) Clear() { c.mu.Lock() defer c.mu.Unlock() c.data = make(map[string]*cacheEntry) } // evict removes an entry based on the eviction policy. func (c *MemoryCache) evict() { if len(c.data) == 0 { return } var keyToDelete string switch c.eviction { case EvictLRU: // Find least recently used entry var oldest time.Time for k, v := range c.data { if oldest.IsZero() || v.lastAccess.Before(oldest) { oldest = v.lastAccess keyToDelete = k } } case EvictLFU: // Find least frequently used var minHits int64 = -1 for k, v := range c.data { if minHits == -1 || v.hits < minHits { minHits = v.hits keyToDelete = k } } case EvictRandom: // Delete first entry (Go map iteration is randomized) for k := range c.data { keyToDelete = k break } } if keyToDelete != "" { delete(c.data, keyToDelete) } } // GenerateCacheKey generates a cache key from the given input. func GenerateCacheKey(nodeName string, input any) string { data, err := json.Marshal(input) if err != nil { // Fall back to the type name so different inputs still produce different keys. return fmt.Sprintf("%s:%T", nodeName, input) } hash := sha256.Sum256(data) return fmt.Sprintf("%s:%s", nodeName, hex.EncodeToString(hash[:])) } // CachedExecutor wraps a function with caching. type CachedExecutor struct { cache Cache cachePolicy *types.CachePolicy } // NewCachedExecutor creates a new cached executor. func NewCachedExecutor(cache Cache, policy *types.CachePolicy) *CachedExecutor { return &CachedExecutor{ cache: cache, cachePolicy: policy, } } // Execute executes a function with caching. func (e *CachedExecutor) Execute(ctx context.Context, nodeName string, input any, fn func(context.Context, any) (any, error)) (any, error) { // Generate cache key var key string if e.cachePolicy != nil && e.cachePolicy.KeyFunc != nil { key = e.cachePolicy.KeyFunc(ctx, input) } else { key = GenerateCacheKey(nodeName, input) } // Check cache if cached, ok := e.cache.Get(ctx, key); ok { return cached, nil } // Execute function result, err := fn(ctx, input) if err != nil { return nil, err } // Cache result var ttl time.Duration if e.cachePolicy != nil && e.cachePolicy.TTL != nil { ttl = *e.cachePolicy.TTL } e.cache.Set(ctx, key, result, ttl) return result, nil } // NoopCache is a cache that doesn't store anything. type NoopCache struct{} // Get always returns false. func (n *NoopCache) Get(ctx context.Context, key string) (any, bool) { return nil, false } // Set is a no-op. func (n *NoopCache) Set(ctx context.Context, key string, value any, ttl time.Duration) {} // Delete is a no-op. func (n *NoopCache) Delete(ctx context.Context, key string) {} // Clear is a no-op. func (n *NoopCache) Clear() {}