package storage import ( "sync" "time" ) // HoldCache caches hold endpoints for (DID, repository) pairs // This avoids expensive ATProto lookups on every blob request during pulls // // NOTE: This is a simple in-memory cache for MVP. For production deployments: // - Use Redis or similar for distributed caching // - Consider implementing cache size limits // - Monitor memory usage under high load type HoldCache struct { mu sync.RWMutex cache map[string]*holdCacheEntry } type holdCacheEntry struct { holdEndpoint string expiresAt time.Time } var globalHoldCache = &HoldCache{ cache: make(map[string]*holdCacheEntry), } func init() { // Start background cleanup goroutine go func() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { globalHoldCache.Cleanup() } }() } // GetGlobalHoldCache returns the global hold cache instance func GetGlobalHoldCache() *HoldCache { return globalHoldCache } // Set stores a hold endpoint for a (DID, repository) pair with a TTL func (c *HoldCache) Set(did, repository, holdEndpoint string, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() key := did + ":" + repository c.cache[key] = &holdCacheEntry{ holdEndpoint: holdEndpoint, expiresAt: time.Now().Add(ttl), } } // Get retrieves a hold endpoint for a (DID, repository) pair // Returns empty string and false if not found or expired func (c *HoldCache) Get(did, repository string) (string, bool) { c.mu.RLock() defer c.mu.RUnlock() key := did + ":" + repository entry, ok := c.cache[key] if !ok { return "", false } // Check if expired if time.Now().After(entry.expiresAt) { // Don't delete here (would need write lock), let cleanup handle it return "", false } return entry.holdEndpoint, true } // Cleanup removes expired entries (called automatically every 5 minutes) func (c *HoldCache) Cleanup() { c.mu.Lock() defer c.mu.Unlock() now := time.Now() removed := 0 for key, entry := range c.cache { if now.After(entry.expiresAt) { delete(c.cache, key) removed++ } } // Log cleanup stats for monitoring if removed > 0 || len(c.cache) > 100 { // Log if we removed entries OR if cache is growing large // This helps identify if cache size is becoming a concern println("Hold cache cleanup: removed", removed, "entries, remaining", len(c.cache)) } }