Files
steamcache2/vfs/gc/gc.go
Justin Harms bd123bc63a
All checks were successful
Release Tag / release (push) Successful in 9s
Refactor module naming and update references to steamcache2
- Changed module name from `s1d3sw1ped/SteamCache2` to `s1d3sw1ped/steamcache2` for consistency.
- Updated all import paths and references throughout the codebase to reflect the new module name.
- Adjusted README and Makefile to use the updated module name, ensuring clarity in usage instructions.
2025-09-21 23:10:21 -05:00

403 lines
10 KiB
Go

// vfs/gc/gc.go
package gc
import (
"context"
"io"
"s1d3sw1ped/steamcache2/vfs"
"s1d3sw1ped/steamcache2/vfs/disk"
"s1d3sw1ped/steamcache2/vfs/memory"
"sync"
"sync/atomic"
"time"
)
// GCAlgorithm represents different garbage collection strategies
type GCAlgorithm string
const (
LRU GCAlgorithm = "lru"
LFU GCAlgorithm = "lfu"
FIFO GCAlgorithm = "fifo"
Largest GCAlgorithm = "largest"
Smallest GCAlgorithm = "smallest"
Hybrid GCAlgorithm = "hybrid"
)
// GCFS wraps a VFS with garbage collection capabilities
type GCFS struct {
vfs vfs.VFS
algorithm GCAlgorithm
gcFunc func(vfs.VFS, uint) uint
}
// New creates a new GCFS with the specified algorithm
func New(wrappedVFS vfs.VFS, algorithm GCAlgorithm) *GCFS {
gcfs := &GCFS{
vfs: wrappedVFS,
algorithm: algorithm,
}
switch algorithm {
case LRU:
gcfs.gcFunc = gcLRU
case LFU:
gcfs.gcFunc = gcLFU
case FIFO:
gcfs.gcFunc = gcFIFO
case Largest:
gcfs.gcFunc = gcLargest
case Smallest:
gcfs.gcFunc = gcSmallest
case Hybrid:
gcfs.gcFunc = gcHybrid
default:
// Default to LRU
gcfs.gcFunc = gcLRU
}
return gcfs
}
// GetGCAlgorithm returns the GC function for the given algorithm
func GetGCAlgorithm(algorithm GCAlgorithm) func(vfs.VFS, uint) uint {
switch algorithm {
case LRU:
return gcLRU
case LFU:
return gcLFU
case FIFO:
return gcFIFO
case Largest:
return gcLargest
case Smallest:
return gcSmallest
case Hybrid:
return gcHybrid
default:
return gcLRU
}
}
// Create wraps the underlying Create method
func (gc *GCFS) Create(key string, size int64) (io.WriteCloser, error) {
// Check if we need to GC before creating
if gc.vfs.Size()+size > gc.vfs.Capacity() {
needed := uint((gc.vfs.Size() + size) - gc.vfs.Capacity())
gc.gcFunc(gc.vfs, needed)
}
return gc.vfs.Create(key, size)
}
// Open wraps the underlying Open method
func (gc *GCFS) Open(key string) (io.ReadCloser, error) {
return gc.vfs.Open(key)
}
// Delete wraps the underlying Delete method
func (gc *GCFS) Delete(key string) error {
return gc.vfs.Delete(key)
}
// Stat wraps the underlying Stat method
func (gc *GCFS) Stat(key string) (*vfs.FileInfo, error) {
return gc.vfs.Stat(key)
}
// Name wraps the underlying Name method
func (gc *GCFS) Name() string {
return gc.vfs.Name() + "(GC:" + string(gc.algorithm) + ")"
}
// Size wraps the underlying Size method
func (gc *GCFS) Size() int64 {
return gc.vfs.Size()
}
// Capacity wraps the underlying Capacity method
func (gc *GCFS) Capacity() int64 {
return gc.vfs.Capacity()
}
// EvictionStrategy defines an interface for cache eviction
type EvictionStrategy interface {
Evict(vfs vfs.VFS, bytesNeeded uint) uint
}
// GC functions
// gcLRU implements Least Recently Used eviction
func gcLRU(v vfs.VFS, bytesNeeded uint) uint {
return evictLRU(v, bytesNeeded)
}
// gcLFU implements Least Frequently Used eviction
func gcLFU(v vfs.VFS, bytesNeeded uint) uint {
return evictLFU(v, bytesNeeded)
}
// gcFIFO implements First In First Out eviction
func gcFIFO(v vfs.VFS, bytesNeeded uint) uint {
return evictFIFO(v, bytesNeeded)
}
// gcLargest implements largest file first eviction
func gcLargest(v vfs.VFS, bytesNeeded uint) uint {
return evictLargest(v, bytesNeeded)
}
// gcSmallest implements smallest file first eviction
func gcSmallest(v vfs.VFS, bytesNeeded uint) uint {
return evictSmallest(v, bytesNeeded)
}
// gcHybrid implements a hybrid eviction strategy
func gcHybrid(v vfs.VFS, bytesNeeded uint) uint {
return evictHybrid(v, bytesNeeded)
}
// evictLRU performs LRU eviction by removing least recently used files
func evictLRU(v vfs.VFS, bytesNeeded uint) uint {
// Try to use specific eviction methods if available
switch fs := v.(type) {
case *memory.MemoryFS:
return fs.EvictLRU(bytesNeeded)
case *disk.DiskFS:
return fs.EvictLRU(bytesNeeded)
default:
// No fallback - return 0 (no eviction performed)
return 0
}
}
// evictLFU performs LFU (Least Frequently Used) eviction
func evictLFU(v vfs.VFS, bytesNeeded uint) uint {
// For now, fall back to size-based eviction
// TODO: Implement proper LFU tracking
return evictBySize(v, bytesNeeded)
}
// evictFIFO performs FIFO (First In First Out) eviction
func evictFIFO(v vfs.VFS, bytesNeeded uint) uint {
switch fs := v.(type) {
case *memory.MemoryFS:
return fs.EvictFIFO(bytesNeeded)
case *disk.DiskFS:
return fs.EvictFIFO(bytesNeeded)
default:
// No fallback - return 0 (no eviction performed)
return 0
}
}
// evictLargest evicts largest files first
func evictLargest(v vfs.VFS, bytesNeeded uint) uint {
return evictBySizeDesc(v, bytesNeeded)
}
// evictSmallest evicts smallest files first
func evictSmallest(v vfs.VFS, bytesNeeded uint) uint {
return evictBySizeAsc(v, bytesNeeded)
}
// evictBySize evicts files based on size (smallest first)
func evictBySize(v vfs.VFS, bytesNeeded uint) uint {
return evictBySizeAsc(v, bytesNeeded)
}
// evictBySizeAsc evicts smallest files first
func evictBySizeAsc(v vfs.VFS, bytesNeeded uint) uint {
switch fs := v.(type) {
case *memory.MemoryFS:
return fs.EvictBySize(bytesNeeded, true) // true = ascending (smallest first)
case *disk.DiskFS:
return fs.EvictBySize(bytesNeeded, true) // true = ascending (smallest first)
default:
// No fallback - return 0 (no eviction performed)
return 0
}
}
// evictBySizeDesc evicts largest files first
func evictBySizeDesc(v vfs.VFS, bytesNeeded uint) uint {
switch fs := v.(type) {
case *memory.MemoryFS:
return fs.EvictBySize(bytesNeeded, false) // false = descending (largest first)
case *disk.DiskFS:
return fs.EvictBySize(bytesNeeded, false) // false = descending (largest first)
default:
// No fallback - return 0 (no eviction performed)
return 0
}
}
// evictHybrid implements a hybrid eviction strategy
func evictHybrid(v vfs.VFS, bytesNeeded uint) uint {
// Use LRU as primary strategy, but consider size as tiebreaker
return evictLRU(v, bytesNeeded)
}
// AdaptivePromotionDeciderFunc is a placeholder for the adaptive promotion logic
var AdaptivePromotionDeciderFunc = func() interface{} {
return nil
}
// AsyncGCFS wraps a GCFS with asynchronous garbage collection capabilities
type AsyncGCFS struct {
*GCFS
gcQueue chan gcRequest
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
gcRunning int32
preemptive bool
asyncThreshold float64 // Async GC threshold as percentage of capacity (e.g., 0.8 = 80%)
syncThreshold float64 // Sync GC threshold as percentage of capacity (e.g., 0.95 = 95%)
hardLimit float64 // Hard limit threshold (e.g., 1.0 = 100%)
}
type gcRequest struct {
bytesNeeded uint
priority int // Higher number = higher priority
}
// NewAsync creates a new AsyncGCFS with asynchronous garbage collection
func NewAsync(wrappedVFS vfs.VFS, algorithm GCAlgorithm, preemptive bool, asyncThreshold, syncThreshold, hardLimit float64) *AsyncGCFS {
ctx, cancel := context.WithCancel(context.Background())
asyncGC := &AsyncGCFS{
GCFS: New(wrappedVFS, algorithm),
gcQueue: make(chan gcRequest, 100), // Buffer for GC requests
ctx: ctx,
cancel: cancel,
preemptive: preemptive,
asyncThreshold: asyncThreshold,
syncThreshold: syncThreshold,
hardLimit: hardLimit,
}
// Start the background GC worker
asyncGC.wg.Add(1)
go asyncGC.gcWorker()
// Start preemptive GC if enabled
if preemptive {
asyncGC.wg.Add(1)
go asyncGC.preemptiveGC()
}
return asyncGC
}
// Create wraps the underlying Create method with hybrid GC (async + sync hard limits)
func (agc *AsyncGCFS) Create(key string, size int64) (io.WriteCloser, error) {
currentSize := agc.vfs.Size()
capacity := agc.vfs.Capacity()
projectedSize := currentSize + size
// Calculate utilization percentages
currentUtilization := float64(currentSize) / float64(capacity)
projectedUtilization := float64(projectedSize) / float64(capacity)
// Hard limit check - never exceed the hard limit
if projectedUtilization > agc.hardLimit {
needed := uint(projectedSize - capacity)
// Immediate sync GC to prevent exceeding hard limit
agc.gcFunc(agc.vfs, needed)
} else if projectedUtilization > agc.syncThreshold {
// Near hard limit - do immediate sync GC
needed := uint(projectedSize - int64(float64(capacity)*agc.syncThreshold))
agc.gcFunc(agc.vfs, needed)
} else if currentUtilization > agc.asyncThreshold {
// Above async threshold - queue for async GC
needed := uint(projectedSize - int64(float64(capacity)*agc.asyncThreshold))
select {
case agc.gcQueue <- gcRequest{bytesNeeded: needed, priority: 2}:
default:
// Queue full, do immediate GC
agc.gcFunc(agc.vfs, needed)
}
}
return agc.vfs.Create(key, size)
}
// gcWorker processes GC requests asynchronously
func (agc *AsyncGCFS) gcWorker() {
defer agc.wg.Done()
ticker := time.NewTicker(100 * time.Millisecond) // Check every 100ms
defer ticker.Stop()
for {
select {
case <-agc.ctx.Done():
return
case req := <-agc.gcQueue:
atomic.StoreInt32(&agc.gcRunning, 1)
agc.gcFunc(agc.vfs, req.bytesNeeded)
atomic.StoreInt32(&agc.gcRunning, 0)
case <-ticker.C:
// Process any pending GC requests
select {
case req := <-agc.gcQueue:
atomic.StoreInt32(&agc.gcRunning, 1)
agc.gcFunc(agc.vfs, req.bytesNeeded)
atomic.StoreInt32(&agc.gcRunning, 0)
default:
// No pending requests
}
}
}
}
// preemptiveGC runs background GC to keep cache utilization below threshold
func (agc *AsyncGCFS) preemptiveGC() {
defer agc.wg.Done()
ticker := time.NewTicker(5 * time.Second) // Check every 5 seconds
defer ticker.Stop()
for {
select {
case <-agc.ctx.Done():
return
case <-ticker.C:
currentSize := agc.vfs.Size()
capacity := agc.vfs.Capacity()
currentUtilization := float64(currentSize) / float64(capacity)
// Check if we're above the async threshold
if currentUtilization > agc.asyncThreshold {
// Calculate how much to free to get back to async threshold
targetSize := int64(float64(capacity) * agc.asyncThreshold)
if currentSize > targetSize {
overage := currentSize - targetSize
select {
case agc.gcQueue <- gcRequest{bytesNeeded: uint(overage), priority: 0}:
default:
// Queue full, skip this round
}
}
}
}
}
}
// Stop stops the async GC workers
func (agc *AsyncGCFS) Stop() {
agc.cancel()
agc.wg.Wait()
}
// IsGCRunning returns true if GC is currently running
func (agc *AsyncGCFS) IsGCRunning() bool {
return atomic.LoadInt32(&agc.gcRunning) == 1
}
// ForceGC forces immediate garbage collection to free the specified number of bytes
func (agc *AsyncGCFS) ForceGC(bytesNeeded uint) {
agc.gcFunc(agc.vfs, bytesNeeded)
}