// vfs/disk/disk.go package disk import ( "fmt" "io" "os" "path/filepath" "s1d3sw1ped/steamcache2/steamcache/logger" "s1d3sw1ped/steamcache2/vfs" "s1d3sw1ped/steamcache2/vfs/locks" "s1d3sw1ped/steamcache2/vfs/lru" "s1d3sw1ped/steamcache2/vfs/types" "s1d3sw1ped/steamcache2/vfs/vfserror" "sort" "strings" "sync" "sync/atomic" "time" "github.com/edsrzf/mmap-go" ) // maxEvictBatch bounds the candidate snapshot during RLock/Lock collect in Evict* (mirrors memory). const maxEvictBatch = 4096 // Ensure DiskFS implements VFS. var _ vfs.VFS = (*DiskFS)(nil) // DiskFS is a virtual file system that stores files on disk. type DiskFS struct { root string info map[string]*vfs.FileInfo capacity int64 size int64 mu sync.RWMutex keyLocks []sync.Map // Sharded lock pools for better concurrency LRU *lru.LRUList[*vfs.FileInfo] timeUpdater *vfs.BatchedTimeUpdate // Batched time updates for better performance // initDone is closed once background population of size/info/LRU finishes; Size() receives on it for the barrier. initDone chan struct{} // initCloseOnce ensures initDone closed exactly once even on panic in bg populator (panic safety for Issue 1). initCloseOnce sync.Once startupEvict func(vfs.VFS, uint) uint // passed to New (via gc.GetGCAlgorithm); invoked as last step of bg init if over cap (no post-ctor race) } // shardPath converts a Steam cache key to a sharded directory path to reduce inode pressure func (d *DiskFS) shardPath(key string) string { if !strings.HasPrefix(key, "steam/") { return key } // Extract hash part hashPart := key[6:] // Remove "steam/" prefix if len(hashPart) < 4 { // For very short hashes, single level sharding if len(hashPart) >= 2 { shard1 := hashPart[:2] return filepath.Join("steam", shard1, hashPart) } return filepath.Join("steam", hashPart) } // Optimal 2-level sharding for Steam hashes (typically 40 chars) shard1 := hashPart[:2] // First 2 chars shard2 := hashPart[2:4] // Next 2 chars return filepath.Join("steam", shard1, shard2, hashPart) } // pathForKey returns the full on-disk path for a key (sharded + normalized). // Extracted to reduce duplication in Evict*/Delete/Open paths (still safe to call under lock for evict). func (d *DiskFS) pathForKey(key string) string { shardedPath := d.shardPath(key) path := filepath.Join(d.root, shardedPath) path = strings.ReplaceAll(path, "\\", "/") return path } // filePathToKey reverses a physical on-disk path (under root) back to logical cache key. // Used by bg init-time scan (from New) to populate info/LRU for correct Size after barrier. func (d *DiskFS) filePathToKey(fullPath string) string { rel, err := filepath.Rel(d.root, fullPath) if err != nil { return filepath.Base(fullPath) } rel = strings.ReplaceAll(rel, "\\", "/") if strings.HasPrefix(rel, "steam/") { if hash := filepath.Base(rel); hash != "" && hash != "." { return "steam/" + hash } } return rel } // New creates a new DiskFS. // The evict param (from gc.GetGCAlgorithm, or nil) is stored before launching the bg // population goroutine, eliminating any post-New handoff window/race for the relocated // startup over-capacity guard (now the last step inside calculateSizeAndPopulateIndex). // New returns fast even for millions of files (async bg scan + streaming batch inserts). // Callers (e.g. steamcache.New) that need populated state or post-guard size must call Size() // (or ops that do) which blocks on the internal init barrier until population + optional guard complete. // See README "Large Cache Initialization" for migration/observable behavior during the proxy window. func New(root string, capacity int64, evict func(vfs.VFS, uint) uint) (*DiskFS, error) { if capacity <= 0 { return nil, fmt.Errorf("disk capacity must be greater than 0") } // Create root directory if it doesn't exist. Propagate error (ctor now returns err for hygiene). // 0700 (not 0755): cache contents are user data from untrusted CDN responses; least-privilege for LAN appliance. if err := os.MkdirAll(root, 0700); err != nil { return nil, fmt.Errorf("failed to create root directory %s: %w", root, err) } // Initialize sharded locks keyLocks := make([]sync.Map, locks.NumLockShards) d := &DiskFS{ root: root, info: make(map[string]*vfs.FileInfo), capacity: capacity, size: 0, keyLocks: keyLocks, LRU: lru.NewLRUList[*vfs.FileInfo](), timeUpdater: vfs.NewBatchedTimeUpdate(100 * time.Millisecond), // Update time every 100ms startupEvict: evict, } d.initDone = make(chan struct{}) // Launch heavy population asynchronously so New returns fast (scans millions of files without blocking ctor or using O(N) temp RAM). // The initDone barrier ensures first Size() and subsequent ops (including late tier attach) see fully populated + post-eviction state. go d.calculateSizeAndPopulateIndex() return d, nil } // calculateSizeAndPopulateIndex runs in background from New to avoid blocking startup or O(N) RAM for large caches (millions of Steam files). // It streams batch inserts (bounded by maxEvictBatch) to keep lock times short and eliminate giant temporary slice. // Startup over-capacity eviction (if needed) runs as the very last step (using the evict func passed to New, selected via gc.GetGCAlgorithm). // Only then is initDone closed so Size() and waiters see consistent post-eviction state. // Panic recovery ensures initDone is always closed (unblocks Size callers) even on scan/IO panic; uses Once for safety. func (d *DiskFS) calculateSizeAndPopulateIndex() { defer func() { if r := recover(); r != nil { logger.Logger.Error().Interface("recovered_panic", r).Msg("calculateSizeAndPopulateIndex panicked; ensuring initDone closed to unblock Size waiters and prevent hang") } d.initCloseOnce.Do(func() { close(d.initDone) }) }() tstart := time.Now() // Channel for collecting file information (now includes metadata for info/LRU population) fileChan := make(chan discoveredFile, 1000) // Progress tracking var totalFiles int64 var processedFiles int64 progressTicker := time.NewTicker(2 * time.Second) defer progressTicker.Stop() // Wait group for workers var wg sync.WaitGroup // Start directory scanner wg.Add(1) go func() { defer wg.Done() defer close(fileChan) d.scanFilesForSize(d.root, fileChan, &totalFiles) }() // Collect results with progress reporting + streaming batch population (no O(N) discovered slice, bounded locks) var totalSize int64 const batchSize = maxEvictBatch var batch []discoveredFile // Use a separate goroutine to collect results done := make(chan struct{}) go func() { defer close(done) for { select { case df, ok := <-fileChan: if !ok { return } totalSize += df.size processedFiles++ batch = append(batch, df) if len(batch) >= batchSize { d.insertBatch(batch) batch = batch[:0] } case <-progressTicker.C: if totalFiles > 0 { logger.Logger.Debug(). Int64("processed", processedFiles). Int64("total", totalFiles). Int64("size", totalSize). Float64("progress", float64(processedFiles)/float64(totalFiles)*100). Msg("Background size calculation progress") } } } }() // Wait for scanning to complete wg.Wait() <-done // Final partial batch + set (no size stomp: inserts do the += for discovered; concurrent Creates are additive via their paths) if len(batch) > 0 { d.insertBatch(batch) } logger.Logger.Info(). Int64("files_scanned", processedFiles). Int64("total_size", totalSize). Str("duration", time.Since(tstart).String()). Msg("Size and index population completed") // Run over-capacity startup eviction here (LAST step of bg init) using freshly populated index+size. // The func (passed at New time via gc.GetGCAlgorithm) is guaranteed visible (no post-ctor handoff). // Snapshot size under RLock to eliminate data race on d.size vs concurrent Create/Evict (fixes -race on guard decision). d.mu.RLock() overCapacity := d.size > d.capacity needed := uint(0) if overCapacity { needed = uint(d.size - d.capacity) // #nosec G115 -- diff guaranteed >0 by overCapacity check; eviction API takes uint (bytes); fits in practice for cache sizes } d.mu.RUnlock() if overCapacity && d.startupEvict != nil { d.startupEvict(d, needed) } // Signal readiness: Size() and callers (late tier attach + Evict*) now see correct populated + post-eviction state. // Use Once (recover path also uses it) to guarantee exactly one close even under panic. d.initCloseOnce.Do(func() { close(d.initDone) }) } // insertBatch populates info/LRU under lock for a bounded batch (follows maxEvictBatch pattern for short critical sections). // Size is incremented here only for files actually added (prevents double-count vs. concurrent Create during window). func (d *DiskFS) insertBatch(batch []discoveredFile) { d.mu.Lock() for _, df := range batch { if _, exists := d.info[df.key]; !exists { fi := vfs.NewFileInfoFromOS(df.osInfo, df.key) d.info[df.key] = fi d.LRU.Add(df.key, fi) d.size += df.size } } d.mu.Unlock() } // discoveredFile carries metadata for (bg) init-time population of info/LRU. type discoveredFile struct { key string size int64 osInfo os.FileInfo } // scanFilesForSize performs recursive file scanning for size + metadata (to populate LRU/info via bg streaming in New). func (d *DiskFS) scanFilesForSize(dirPath string, fileChan chan<- discoveredFile, totalFiles *int64) { // Use ReadDir for faster directory listing entries, err := os.ReadDir(dirPath) if err != nil { return } // Count files first for progress tracking fileCount := 0 for _, entry := range entries { if !entry.IsDir() { fileCount++ } } atomic.AddInt64(totalFiles, int64(fileCount)) // Process entries concurrently with limited workers semaphore := make(chan struct{}, 16) // More workers for size calculation var wg sync.WaitGroup for _, entry := range entries { entryPath := filepath.Join(dirPath, entry.Name()) if entry.IsDir() { // Recursively scan subdirectories wg.Add(1) go func(path string) { defer wg.Done() semaphore <- struct{}{} // Acquire semaphore defer func() { <-semaphore }() // Release semaphore d.scanFilesForSize(path, fileChan, totalFiles) }(entryPath) } else { // Process file for size + key (for LRU/info population) wg.Add(1) go func(entry os.DirEntry) { defer wg.Done() semaphore <- struct{}{} // Acquire semaphore defer func() { <-semaphore }() // Release semaphore fullPath := filepath.Join(dirPath, entry.Name()) key := d.filePathToKey(fullPath) // Get file info for size calculation info, err := entry.Info() if err != nil { return } // Send discovered file info fileChan <- discoveredFile{ key: key, size: info.Size(), osInfo: info, } }(entry) } } wg.Wait() } // Name returns the name of this VFS func (d *DiskFS) Name() string { return "DiskFS" } // Size returns the current size. // The receive on initDone ensures that after New callers observe the real on-disk total + populated info/LRU // (barrier unblocks only after bg streaming population + any startup eviction finishes). // All subsequent calls are non-blocking (closed chan receive is instantaneous). // During long init for huge caches, this (and callers like GetMetrics, attach logic) will block until ready; // this is the documented contract enabling "no disk activity until ready" for TieredCache. func (d *DiskFS) Size() int64 { <-d.initDone d.mu.RLock() defer d.mu.RUnlock() return d.size } // Capacity returns the maximum capacity func (d *DiskFS) Capacity() int64 { return d.capacity } // getKeyLock returns a lock for the given key using sharding func (d *DiskFS) getKeyLock(key string) *sync.RWMutex { return locks.GetKeyLock(d.keyLocks, key) } // Create creates a new file func (d *DiskFS) Create(key string, size int64) (io.WriteCloser, error) { if key == "" { return nil, vfserror.ErrInvalidKey } if key[0] == '/' { return nil, vfserror.ErrInvalidKey } // Sanitize key to prevent path traversal key = filepath.Clean(key) key = strings.ReplaceAll(key, "\\", "/") if strings.Contains(key, "..") { return nil, vfserror.ErrInvalidKey } keyMu := d.getKeyLock(key) keyMu.Lock() defer keyMu.Unlock() d.mu.Lock() // Check if file already exists and handle overwrite if fi, exists := d.info[key]; exists { d.size -= fi.Size d.LRU.Remove(key) delete(d.info, key) } path := d.pathForKey(key) d.mu.Unlock() dir := filepath.Dir(path) // 0700 (not 0755): per-shard cache dirs hold untrusted CDN content; restrict to owner only (G301 addressed). if err := os.MkdirAll(dir, 0700); err != nil { return nil, err } file, err := os.Create(path) // #nosec G304 -- path built by pathForKey from sanitized (Clean, no ..) hash-derived key under trusted disk.root; no untrusted file inclusion if err != nil { return nil, err } fi := vfs.NewFileInfo(key, size) d.mu.Lock() d.info[key] = fi d.LRU.Add(key, fi) // Initialize access time with current time fi.UpdateAccessBatched(d.timeUpdater) // Add to size for new files (not discovered files) d.size += size d.mu.Unlock() return &diskWriteCloser{ file: file, disk: d, key: key, declaredSize: size, }, nil } // diskWriteCloser implements io.WriteCloser for disk files with size adjustment type diskWriteCloser struct { file *os.File disk *DiskFS key string declaredSize int64 } func (dwc *diskWriteCloser) Write(p []byte) (n int, err error) { return dwc.file.Write(p) } func (dwc *diskWriteCloser) Close() error { // Get the actual file size stat, err := dwc.file.Stat() if err != nil { _ = dwc.file.Close() // best-effort close on stat error path; primary error is returned return err } actualSize := stat.Size() // Update the size in FileInfo if it differs from declared size dwc.disk.mu.Lock() if fi, exists := dwc.disk.info[dwc.key]; exists { sizeDiff := actualSize - fi.Size fi.Size = actualSize dwc.disk.size += sizeDiff } dwc.disk.mu.Unlock() return dwc.file.Close() } // Open opens a file for reading with lazy discovery func (d *DiskFS) Open(key string) (io.ReadCloser, error) { if key == "" { return nil, vfserror.ErrInvalidKey } if key[0] == '/' { return nil, vfserror.ErrInvalidKey } // Sanitize key to prevent path traversal key = filepath.Clean(key) key = strings.ReplaceAll(key, "\\", "/") if strings.Contains(key, "..") { return nil, vfserror.ErrInvalidKey } // First, try to get the file info d.mu.RLock() fi, exists := d.info[key] d.mu.RUnlock() if !exists { // Try lazy discovery var err error fi, err = d.Stat(key) if err != nil { return nil, err } } // Update access time and LRU (use TryLock to avoid serializing all readers on the global mu despite sharding; approximate LRU under load is acceptable) if d.mu.TryLock() { fi.UpdateAccessBatched(d.timeUpdater) d.LRU.MoveToFront(key, d.timeUpdater) d.mu.Unlock() } path := d.pathForKey(key) file, err := os.Open(path) // #nosec G304 -- path built by pathForKey from sanitized (Clean, no ..) hash-derived key under trusted disk.root; no untrusted file inclusion if err != nil { return nil, err } // Use memory mapping for large files (>1MB) to improve performance const mmapThreshold = 1024 * 1024 // 1MB if fi.Size > mmapThreshold { // Close the regular file handle _ = file.Close() // best-effort; mmap path takes over or falls back // Try memory mapping mmapFile, err := os.Open(path) if err != nil { return nil, err } mapped, err := mmap.Map(mmapFile, mmap.RDONLY, 0) if err != nil { _ = mmapFile.Close() // best-effort close before fallback open // Fallback to regular file reading (intentional 3rd open of same path after mmap failure; pre-existing pattern, no leak) return os.Open(path) } return &mmapReadCloser{ data: mapped, file: mmapFile, offset: 0, }, nil } return file, nil } // mmapReadCloser implements io.ReadCloser for memory-mapped files type mmapReadCloser struct { data mmap.MMap file *os.File offset int } func (m *mmapReadCloser) Read(p []byte) (n int, err error) { if m.offset >= len(m.data) { return 0, io.EOF } n = copy(p, m.data[m.offset:]) m.offset += n return n, nil } func (m *mmapReadCloser) Close() error { _ = m.data.Unmap() // best-effort; unmap failure non-fatal for read-only mapping return m.file.Close() } // Delete removes a file func (d *DiskFS) Delete(key string) error { if key == "" { return vfserror.ErrInvalidKey } if key[0] == '/' { return vfserror.ErrInvalidKey } keyMu := d.getKeyLock(key) keyMu.Lock() defer keyMu.Unlock() d.mu.Lock() fi, exists := d.info[key] if !exists { d.mu.Unlock() return vfserror.ErrNotFound } d.size -= fi.Size d.LRU.Remove(key) delete(d.info, key) d.mu.Unlock() path := d.pathForKey(key) err := os.Remove(path) if err != nil { return err } return nil } // Stat returns file information with lazy discovery func (d *DiskFS) Stat(key string) (*vfs.FileInfo, error) { if key == "" { return nil, vfserror.ErrInvalidKey } if key[0] == '/' { return nil, vfserror.ErrInvalidKey } keyMu := d.getKeyLock(key) // First, try to get the file info with read lock keyMu.RLock() d.mu.RLock() if fi, ok := d.info[key]; ok { d.mu.RUnlock() keyMu.RUnlock() return fi, nil } d.mu.RUnlock() keyMu.RUnlock() // Lazy discovery: check if file exists on disk and index it path := d.pathForKey(key) info, err := os.Stat(path) if err != nil { return nil, vfserror.ErrNotFound } // File exists, add it to the index with write lock keyMu.Lock() defer keyMu.Unlock() // Double-check after acquiring write lock d.mu.Lock() if fi, ok := d.info[key]; ok { d.mu.Unlock() return fi, nil } // Create and add file info fi := vfs.NewFileInfoFromOS(info, key) d.info[key] = fi d.LRU.Add(key, fi) fi.UpdateAccessBatched(d.timeUpdater) // Note: size not updated on lazy discovery (preserves prior behavior; initial on-disk accounted via bg populate at New time, // subsequent files come via Create which accounts size). d.mu.Unlock() return fi, nil } // EvictLRU evicts the least recently used files to free up space // Collect under short exclusive Lock (to serialize concurrent EvictLRU on LRUList), batch under WLock. func (d *DiskFS) EvictLRU(bytesNeeded uint) uint { d.mu.Lock() var toEvict []string need := int64(bytesNeeded) cur := d.size for cur > d.capacity-need && d.LRU.Len() > 0 && len(toEvict) < maxEvictBatch { elem := d.LRU.Back() if elem == nil { break } fi := elem.Value.(*vfs.FileInfo) toEvict = append(toEvict, fi.Key) cur -= fi.Size } d.mu.Unlock() if len(toEvict) == 0 { return 0 } d.mu.Lock() var evicted uint for _, key := range toEvict { if fi, exists := d.info[key]; exists { d.LRU.Remove(key) delete(d.info, key) path := d.pathForKey(key) _ = os.Remove(path) // #nosec G304 -- path from sanitized key; best-effort eviction delete under lock. Best effort; performed under WLock to guarantee on-disk deletion is coordinated with metadata removal. This eliminates resurrection via lazy Stat/Open discovery and prevents late unlinks from deleting content of same-key recreates (critical for in-memory metadata safety model + user's explicit non-race requirement on hot eviction path). d.size -= fi.Size evicted += uint(fi.Size) shardIndex := locks.GetShardIndex(key) d.keyLocks[shardIndex].Delete(key) } } d.mu.Unlock() return evicted } // EvictBySize evicts files by size (ascending = smallest first, descending = largest first) // Scalar snapshot (key+size) under RLock + live re-fetch under WLock for race-free accounting + os.Remove. type evictCandidate struct { key string size int64 } func (d *DiskFS) EvictBySize(bytesNeeded uint, ascending bool) uint { d.mu.RLock() var candidates []evictCandidate for key, fi := range d.info { candidates = append(candidates, evictCandidate{key: key, size: fi.Size}) if len(candidates) >= maxEvictBatch { break } } d.mu.RUnlock() if len(candidates) == 0 { return 0 } sort.Slice(candidates, func(i, j int) bool { if ascending { return candidates[i].size < candidates[j].size } return candidates[i].size > candidates[j].size }) d.mu.Lock() var evicted uint for _, c := range candidates { if d.size <= d.capacity-int64(bytesNeeded) { break } key := c.key if liveFi, exists := d.info[key]; exists { d.LRU.Remove(key) delete(d.info, key) path := d.pathForKey(key) _ = os.Remove(path) // best effort; performed under WLock (reverted from post-unlock) to guarantee on-disk deletion is coordinated with metadata removal. This eliminates resurrection via lazy Stat/Open discovery and prevents late unlinks from deleting content of same-key recreates (critical for in-memory metadata safety model + user's explicit non-race requirement on hot eviction path). d.size -= liveFi.Size evicted += uint(liveFi.Size) shardIndex := locks.GetShardIndex(key) d.keyLocks[shardIndex].Delete(key) } } d.mu.Unlock() return evicted } // EvictFIFO evicts files using FIFO (oldest creation time first) // Snapshot ctime under RLock, live re-fetch + remove under WLock. func (d *DiskFS) EvictFIFO(bytesNeeded uint) uint { d.mu.RLock() var candidates []struct { key string cTime time.Time } for key, fi := range d.info { candidates = append(candidates, struct { key string cTime time.Time }{key: key, cTime: fi.CTime}) if len(candidates) >= maxEvictBatch { break } } d.mu.RUnlock() if len(candidates) == 0 { return 0 } sort.Slice(candidates, func(i, j int) bool { return candidates[i].cTime.Before(candidates[j].cTime) }) d.mu.Lock() var evicted uint for _, c := range candidates { if d.size <= d.capacity-int64(bytesNeeded) { break } key := c.key if liveFi, exists := d.info[key]; exists { d.LRU.Remove(key) delete(d.info, key) path := d.pathForKey(key) _ = os.Remove(path) // best effort; performed under WLock (reverted from post-unlock) to guarantee on-disk deletion is coordinated with metadata removal. This eliminates resurrection via lazy Stat/Open discovery and prevents late unlinks from deleting content of same-key recreates (critical for in-memory metadata safety model + user's explicit non-race requirement on hot eviction path). d.size -= liveFi.Size evicted += uint(liveFi.Size) shardIndex := locks.GetShardIndex(key) d.keyLocks[shardIndex].Delete(key) } } d.mu.Unlock() return evicted } // EvictLFU evicts least frequently used files first (by AccessCount ascending). // Ties broken by ATime (older first). Uses snapshot + live re-fetch under WLock. func (d *DiskFS) EvictLFU(bytesNeeded uint) uint { d.mu.RLock() var candidates []struct { key string accessCount int aTime time.Time } for key, fi := range d.info { candidates = append(candidates, struct { key string accessCount int aTime time.Time }{key: key, accessCount: fi.AccessCount, aTime: fi.ATime}) if len(candidates) >= maxEvictBatch { break } } d.mu.RUnlock() if len(candidates) == 0 { return 0 } sort.Slice(candidates, func(i, j int) bool { if candidates[i].accessCount != candidates[j].accessCount { return candidates[i].accessCount < candidates[j].accessCount } return candidates[i].aTime.Before(candidates[j].aTime) }) d.mu.Lock() var evicted uint for _, c := range candidates { if d.size <= d.capacity-int64(bytesNeeded) { break } key := c.key if liveFi, exists := d.info[key]; exists { d.LRU.Remove(key) delete(d.info, key) path := d.pathForKey(key) _ = os.Remove(path) // best effort; performed under WLock (reverted from post-unlock) to guarantee on-disk deletion is coordinated with metadata removal. This eliminates resurrection via lazy Stat/Open discovery and prevents late unlinks from deleting content of same-key recreates (critical for in-memory metadata safety model + user's explicit non-race requirement on hot eviction path). d.size -= liveFi.Size evicted += uint(liveFi.Size) shardIndex := locks.GetShardIndex(key) d.keyLocks[shardIndex].Delete(key) } } d.mu.Unlock() return evicted } // EvictHybrid evicts using time-decayed score (recency + frequency from GetTimeDecayedScore; lower value first). // This makes "hybrid" a meaningful size + recency + frequency policy. // Snapshot + decayed score under the appropriate locks. func (d *DiskFS) EvictHybrid(bytesNeeded uint) uint { d.mu.RLock() var candidates []struct { key string accessCount int aTime time.Time } for key, fi := range d.info { candidates = append(candidates, struct { key string accessCount int aTime time.Time }{key: key, accessCount: fi.AccessCount, aTime: fi.ATime}) if len(candidates) >= maxEvictBatch { break } } d.mu.RUnlock() if len(candidates) == 0 { return 0 } sort.Slice(candidates, func(i, j int) bool { // Use shared canonical DecayedScore from types (eliminates dupe with memory + FileInfo method). scoreI := types.DecayedScore(candidates[i].aTime, candidates[i].accessCount) scoreJ := types.DecayedScore(candidates[j].aTime, candidates[j].accessCount) return scoreI < scoreJ }) d.mu.Lock() var evicted uint for _, c := range candidates { if d.size <= d.capacity-int64(bytesNeeded) { break } key := c.key if liveFi, exists := d.info[key]; exists { d.LRU.Remove(key) delete(d.info, key) path := d.pathForKey(key) _ = os.Remove(path) // best effort; performed under WLock (reverted from post-unlock) to guarantee on-disk deletion is coordinated with metadata removal. This eliminates resurrection via lazy Stat/Open discovery and prevents late unlinks from deleting content of same-key recreates (critical for in-memory metadata safety model + user's explicit non-race requirement on hot eviction path). d.size -= liveFi.Size evicted += uint(liveFi.Size) shardIndex := locks.GetShardIndex(key) d.keyLocks[shardIndex].Delete(key) } } d.mu.Unlock() return evicted }