You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							195 lines
						
					
					
						
							4.7 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							195 lines
						
					
					
						
							4.7 KiB
						
					
					
				| package log_buffer | |
| 
 | |
| import ( | |
| 	"container/list" | |
| 	"sync" | |
| 	"time" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| ) | |
| 
 | |
| // DiskBufferCache is a small LRU cache for recently-read historical data buffers | |
| // This reduces Filer load when multiple consumers are catching up on historical messages | |
| type DiskBufferCache struct { | |
| 	maxSize   int | |
| 	ttl       time.Duration | |
| 	cache     map[string]*cacheEntry | |
| 	lruList   *list.List | |
| 	mu        sync.RWMutex | |
| 	hits      int64 | |
| 	misses    int64 | |
| 	evictions int64 | |
| } | |
| 
 | |
| type cacheEntry struct { | |
| 	key        string | |
| 	data       []byte | |
| 	offset     int64 | |
| 	timestamp  time.Time | |
| 	lruElement *list.Element | |
| 	isNegative bool // true if this is a negative cache entry (data not found) | |
| } | |
| 
 | |
| // NewDiskBufferCache creates a new cache with the specified size and TTL | |
| // Recommended size: 3-5 buffers (each ~8MB) | |
| // Recommended TTL: 30-60 seconds | |
| func NewDiskBufferCache(maxSize int, ttl time.Duration) *DiskBufferCache { | |
| 	cache := &DiskBufferCache{ | |
| 		maxSize: maxSize, | |
| 		ttl:     ttl, | |
| 		cache:   make(map[string]*cacheEntry), | |
| 		lruList: list.New(), | |
| 	} | |
| 
 | |
| 	// Start background cleanup goroutine | |
| 	go cache.cleanupLoop() | |
| 
 | |
| 	return cache | |
| } | |
| 
 | |
| // Get retrieves a buffer from the cache | |
| // Returns (data, offset, found) | |
| // If found=true and data=nil, this is a negative cache entry (data doesn't exist) | |
| func (c *DiskBufferCache) Get(key string) ([]byte, int64, bool) { | |
| 	c.mu.Lock() | |
| 	defer c.mu.Unlock() | |
| 
 | |
| 	entry, exists := c.cache[key] | |
| 	if !exists { | |
| 		c.misses++ | |
| 		return nil, 0, false | |
| 	} | |
| 
 | |
| 	// Check if entry has expired | |
| 	if time.Since(entry.timestamp) > c.ttl { | |
| 		c.evict(entry) | |
| 		c.misses++ | |
| 		return nil, 0, false | |
| 	} | |
| 
 | |
| 	// Move to front of LRU list (most recently used) | |
| 	c.lruList.MoveToFront(entry.lruElement) | |
| 	c.hits++ | |
| 
 | |
| 	if entry.isNegative { | |
| 		glog.V(4).Infof("📦 CACHE HIT (NEGATIVE): key=%s - data not found (hits=%d misses=%d)", | |
| 			key, c.hits, c.misses) | |
| 	} else { | |
| 		glog.V(4).Infof("📦 CACHE HIT: key=%s offset=%d size=%d (hits=%d misses=%d)", | |
| 			key, entry.offset, len(entry.data), c.hits, c.misses) | |
| 	} | |
| 
 | |
| 	return entry.data, entry.offset, true | |
| } | |
| 
 | |
| // Put adds a buffer to the cache | |
| // If data is nil, this creates a negative cache entry (data doesn't exist) | |
| func (c *DiskBufferCache) Put(key string, data []byte, offset int64) { | |
| 	c.mu.Lock() | |
| 	defer c.mu.Unlock() | |
| 
 | |
| 	isNegative := data == nil | |
| 
 | |
| 	// Check if entry already exists | |
| 	if entry, exists := c.cache[key]; exists { | |
| 		// Update existing entry | |
| 		entry.data = data | |
| 		entry.offset = offset | |
| 		entry.timestamp = time.Now() | |
| 		entry.isNegative = isNegative | |
| 		c.lruList.MoveToFront(entry.lruElement) | |
| 		if isNegative { | |
| 			glog.V(4).Infof("📦 CACHE UPDATE (NEGATIVE): key=%s - data not found", key) | |
| 		} else { | |
| 			glog.V(4).Infof("📦 CACHE UPDATE: key=%s offset=%d size=%d", key, offset, len(data)) | |
| 		} | |
| 		return | |
| 	} | |
| 
 | |
| 	// Evict oldest entry if cache is full | |
| 	if c.lruList.Len() >= c.maxSize { | |
| 		oldest := c.lruList.Back() | |
| 		if oldest != nil { | |
| 			c.evict(oldest.Value.(*cacheEntry)) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Add new entry | |
| 	entry := &cacheEntry{ | |
| 		key:        key, | |
| 		data:       data, | |
| 		offset:     offset, | |
| 		timestamp:  time.Now(), | |
| 		isNegative: isNegative, | |
| 	} | |
| 	entry.lruElement = c.lruList.PushFront(entry) | |
| 	c.cache[key] = entry | |
| 
 | |
| 	if isNegative { | |
| 		glog.V(4).Infof("📦 CACHE PUT (NEGATIVE): key=%s - data not found (cache_size=%d/%d)", | |
| 			key, c.lruList.Len(), c.maxSize) | |
| 	} else { | |
| 		glog.V(4).Infof("📦 CACHE PUT: key=%s offset=%d size=%d (cache_size=%d/%d)", | |
| 			key, offset, len(data), c.lruList.Len(), c.maxSize) | |
| 	} | |
| } | |
| 
 | |
| // evict removes an entry from the cache (must be called with lock held) | |
| func (c *DiskBufferCache) evict(entry *cacheEntry) { | |
| 	delete(c.cache, entry.key) | |
| 	c.lruList.Remove(entry.lruElement) | |
| 	c.evictions++ | |
| 	glog.V(4).Infof("📦 CACHE EVICT: key=%s (evictions=%d)", entry.key, c.evictions) | |
| } | |
| 
 | |
| // cleanupLoop periodically removes expired entries | |
| func (c *DiskBufferCache) cleanupLoop() { | |
| 	ticker := time.NewTicker(c.ttl / 2) | |
| 	defer ticker.Stop() | |
| 
 | |
| 	for range ticker.C { | |
| 		c.cleanup() | |
| 	} | |
| } | |
| 
 | |
| // cleanup removes expired entries | |
| func (c *DiskBufferCache) cleanup() { | |
| 	c.mu.Lock() | |
| 	defer c.mu.Unlock() | |
| 
 | |
| 	now := time.Now() | |
| 	var toEvict []*cacheEntry | |
| 
 | |
| 	// Find expired entries | |
| 	for _, entry := range c.cache { | |
| 		if now.Sub(entry.timestamp) > c.ttl { | |
| 			toEvict = append(toEvict, entry) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Evict expired entries | |
| 	for _, entry := range toEvict { | |
| 		c.evict(entry) | |
| 	} | |
| 
 | |
| 	if len(toEvict) > 0 { | |
| 		glog.V(3).Infof("📦 CACHE CLEANUP: evicted %d expired entries", len(toEvict)) | |
| 	} | |
| } | |
| 
 | |
| // Stats returns cache statistics | |
| func (c *DiskBufferCache) Stats() (hits, misses, evictions int64, size int) { | |
| 	c.mu.RLock() | |
| 	defer c.mu.RUnlock() | |
| 	return c.hits, c.misses, c.evictions, c.lruList.Len() | |
| } | |
| 
 | |
| // Clear removes all entries from the cache | |
| func (c *DiskBufferCache) Clear() { | |
| 	c.mu.Lock() | |
| 	defer c.mu.Unlock() | |
| 
 | |
| 	c.cache = make(map[string]*cacheEntry) | |
| 	c.lruList = list.New() | |
| 	glog.V(2).Infof("📦 CACHE CLEARED") | |
| }
 |