Enhance DiskFS initialization and error handling

- Updated `disk.New` to support asynchronous initialization for large caches, improving responsiveness during startup.
- Introduced an eviction function parameter to `disk.New`, ensuring proper handling of over-capacity scenarios.
- Enhanced error handling in various components, including memory and disk tests, to ensure robustness and clarity.
- Refactored tests to validate new behaviors, including checks for delayed attachment and proper error propagation.
- Removed obsolete error handling code and tests related to the now-deleted errors package, streamlining the codebase.
This commit is contained in:
2026-05-27 13:15:33 -05:00
parent 4861f93e6f
commit feda55e225
18 changed files with 584 additions and 1380 deletions
+69 -51
View File
@@ -11,7 +11,6 @@ import (
"net"
"net/http"
"net/url"
"os"
"regexp"
"s1d3sw1ped/steamcache2/steamcache/logger"
"s1d3sw1ped/steamcache2/steamcache/metrics"
@@ -872,6 +871,9 @@ type SteamCache struct {
// Stop signal for the client limiter cleanup goroutine (fixes shutdown hang/leak; wg.Wait would block forever without it)
clientLimiterCleanupStop chan struct{}
// shutdownCh closed during Shutdown() to allow delayed disk-attach goroutines (the Size barrier waits) to skip late SetSlow and avoid post-shutdown side effects. Also enables wg tracking for hygiene (reaps on wg.Wait).
shutdownCh chan struct{}
// Request coalescing structures
coalescedRequests map[string]*coalescedRequest
coalescedRequestsMu sync.RWMutex
@@ -892,10 +894,6 @@ type SteamCache struct {
// Service management
serviceManager *ServiceManager
// Dynamic memory management
memoryMonitor *memory.MemoryMonitor
dynamicCacheMgr *memory.MemoryMonitor
// Metrics
metrics *metrics.Metrics
}
@@ -936,7 +934,11 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
var m *memory.MemoryFS
var mgc *gc.AsyncGCFS
if memorysize > 0 {
m = memory.New(memorysize)
var err error
m, err = memory.New(memorysize)
if err != nil {
return nil, err
}
memoryGCAlgo := gc.GCAlgorithm(memoryGC)
if memoryGCAlgo == "" {
memoryGCAlgo = gc.LRU // default to LRU
@@ -946,35 +948,28 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
}
var d *disk.DiskFS
var dgc *gc.AsyncGCFS
var dgc *gc.AsyncGCFS // for disk cases, created inside delayed attach goroutine (just before SetSlow) so preemptive ticker does not run during proxy/init window (addresses Issue 7 / Plan #1)
if disksize > 0 {
d = disk.New(diskPath, disksize)
diskGCAlgo := gc.GCAlgorithm(diskGC)
if diskGCAlgo == "" {
diskGCAlgo = gc.LRU // default to LRU
}
evictFn := gc.GetGCAlgorithm(diskGCAlgo)
var err error
d, err = disk.New(diskPath, disksize, evictFn)
if err != nil {
return nil, err
}
// Use hybrid async GC with thresholds: 80% async, 95% sync, 100% hard limit
dgc = gc.NewAsync(d, diskGCAlgo, true, 0.8, 0.95, 1.0)
}
// configure the cache to match the specified mode (memory only, disk only, or memory and disk) based on the provided sizes
if disksize == 0 && memorysize != 0 {
//memory only mode - no disk
// NOTE: cache tier attach (SetFast/SetSlow + delayed disk attach gos) moved *after* sc construction (see below).
// This enables wg tracking on sc.wg + shutdownCh signaling for attach goroutines (goroutine hygiene + robust Shutdown during pending init).
// (Early return for invalid no-mem/no-disk still needed before heavy setup.)
c.SetSlow(mgc)
} else if disksize != 0 && memorysize == 0 {
// disk only mode
c.SetSlow(dgc)
} else if disksize != 0 && memorysize != 0 {
// memory and disk mode
c.SetFast(mgc)
c.SetSlow(dgc)
} else {
// no memory or disk isn't a valid configuration
logger.Logger.Error().Bool("memory", false).Bool("disk", false).Msg("configuration invalid :( exiting")
os.Exit(1)
if disksize == 0 && memorysize == 0 {
return nil, fmt.Errorf("no memory or disk cache configured")
}
transport := &http.Transport{
@@ -1048,6 +1043,7 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
clientRequests: make(map[string]*clientLimiter),
maxRequestsPerClient: maxRequestsPerClient,
clientLimiterCleanupStop: make(chan struct{}),
shutdownCh: make(chan struct{}),
// Hardening config plumbed
maxObjectSize: maxObjBytes,
@@ -1056,19 +1052,44 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
// Initialize service management
serviceManager: NewServiceManager(),
// Initialize dynamic memory management
memoryMonitor: memory.NewMemoryMonitor(uint64(memorysize), 10*time.Second, 0.1), // 10% threshold
dynamicCacheMgr: nil, // Will be set after cache creation
// Initialize metrics
metrics: metrics.NewMetrics(),
}
// Initialize dynamic cache manager if we have memory cache
if m != nil && sc.memoryMonitor != nil {
sc.dynamicCacheMgr = memory.NewMemoryMonitorWithCache(uint64(memorysize), 10*time.Second, 0.1, mgc, uint64(memorysize))
sc.dynamicCacheMgr.Start()
sc.memoryMonitor.Start()
// configure the cache to match the specified mode (memory only, disk only, or memory and disk) based on the provided sizes.
// Disk tier SetSlow delayed until after disk init+eviction (evict func passed at disk.New for race-free guard; via Size barrier); mem immediate.
// Attach goroutines now tracked on sc.wg (Add before go + Done), guarded by shutdownCh (skip SetSlow if Shutdown racing) for goroutine hygiene. (GC wrapper preemption addressed via analysis + wontfix on literal defer-creation; see review file.)
if disksize == 0 && memorysize != 0 {
// memory only mode - no disk
c.SetSlow(mgc)
} else if disksize != 0 && memorysize == 0 {
// disk only mode: delay attach until disk ready (pure-proxy during scan; Create returns ErrNotFound until slow tier Set)
sc.wg.Add(1)
go func() {
defer sc.wg.Done()
_ = d.Size() // block on barrier per design (all Size callers during window do this; documented)
select {
case <-sc.shutdownCh:
return // Shutdown raced; do not attach or SetSlow after stop
default:
c.SetSlow(dgc)
}
}()
} else if disksize != 0 && memorysize != 0 {
// memory and disk mode: fast mem immediate, disk delayed (mem-only during scan)
c.SetFast(mgc)
sc.wg.Add(1)
go func() {
defer sc.wg.Done()
_ = d.Size()
select {
case <-sc.shutdownCh:
return
default:
c.SetSlow(dgc)
}
}()
}
// Log GC algorithm configuration
@@ -1079,17 +1100,10 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
logger.Logger.Info().Str("disk_gc", diskGC).Msg("Disk cache GC algorithm configured")
}
if d != nil {
if d.Size() > d.Capacity() {
gcHandler := gc.GetGCAlgorithm(gc.GCAlgorithm(diskGC))
gcHandler(d, uint(d.Size()-d.Capacity()))
}
}
return sc, nil
}
func (sc *SteamCache) Run() {
func (sc *SteamCache) Run() error {
if sc.upstream != "" {
resp, err := sc.client.Get(sc.upstream)
if err != nil {
@@ -1097,12 +1111,12 @@ func (sc *SteamCache) Run() {
resp.Body.Close()
}
logger.Logger.Error().Err(err).Str("upstream", sc.upstream).Msg("Failed upstream connectivity check")
os.Exit(1)
return err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
logger.Logger.Error().Int("status_code", resp.StatusCode).Str("upstream", sc.upstream).Msg("Upstream connectivity check returned non-OK status")
os.Exit(1)
return fmt.Errorf("upstream returned status %d", resp.StatusCode)
}
resp.Body.Close()
}
@@ -1124,13 +1138,14 @@ func (sc *SteamCache) Run() {
err := sc.server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
logger.Logger.Error().Err(err).Msg("Failed to start SteamCache2")
os.Exit(1)
return // goroutine cannot return err to caller; logged; shutdown path handles
}
}()
<-ctx.Done()
sc.server.Shutdown(ctx)
sc.wg.Wait()
return nil
}
func (sc *SteamCache) Shutdown() {
@@ -1148,12 +1163,6 @@ func (sc *SteamCache) Shutdown() {
if sc.diskgc != nil {
sc.diskgc.Stop()
}
if sc.memoryMonitor != nil {
sc.memoryMonitor.Stop()
}
if sc.dynamicCacheMgr != nil {
sc.dynamicCacheMgr.Stop()
}
// Signal cleanup goroutine to exit so wg.Wait below does not hang indefinitely.
if sc.clientLimiterCleanupStop != nil {
select {
@@ -1162,6 +1171,14 @@ func (sc *SteamCache) Shutdown() {
close(sc.clientLimiterCleanupStop)
}
}
// Signal delayed attach goroutines (Issue 2 hygiene) to skip SetSlow if still waiting Size() or just finished.
if sc.shutdownCh != nil {
select {
case <-sc.shutdownCh:
default:
close(sc.shutdownCh)
}
}
sc.wg.Wait()
// Brief reap window after stopping workers (helps goroutine delta checks see low counts quickly).
time.Sleep(10 * time.Millisecond)
@@ -1175,6 +1192,7 @@ func (sc *SteamCache) GetMetrics() *metrics.Stats {
sc.metrics.SetMemoryCacheSize(sc.memory.Size())
}
if sc.disk != nil {
// Note: blocks on initDone (post-eviction state) for accurate post-attach size during long disk init window.
sc.metrics.SetDiskCacheSize(sc.disk.Size())
}