16 Commits

Author SHA1 Message Date
e24af47697 feat: add upstream and verbose flags to command line interface
All checks were successful
Release Tag / release (push) Successful in 13s
feat: add upstream support allowing to chain cache servers if needed
fix: tweaked garbage collection to be better
2025-01-23 11:14:39 -06:00
931c43d7a8 fix: remove draft status from release configuration
All checks were successful
Release Tag / release (push) Successful in 9s
2025-01-22 21:01:35 -06:00
6fe80c82ad fix: reduce cache hits averaging size for improved performance
All checks were successful
Release Tag / release (push) Successful in 9s
2025-01-22 20:59:24 -06:00
4a69c4ba66 fix: add empty line in logging for improved readability
All checks were successful
Release Tag / release (push) Successful in 9s
2025-01-22 20:51:32 -06:00
2d0fe6571d feat: enhance garbage collection logging with total GC time and improved stat formatting
All checks were successful
Release Tag / release (push) Successful in 9s
2025-01-22 20:48:05 -06:00
550948951e feat: implement enhanced garbage collection statistics logging
All checks were successful
Release Tag / release (push) Successful in 12s
2025-01-22 20:27:12 -06:00
4a23eecae0 fix: reduce log interval to 1 second for more frequent statistics updates
All checks were successful
Release Tag / release (push) Successful in 28s
2025-01-22 19:37:30 -06:00
fed9bbe218 fix: go back to the old averaging
All checks were successful
Release Tag / release (push) Successful in 12s
2025-01-22 19:35:17 -06:00
7401c040dc feat: add configurations for memory only, disk only, and memory & disk modes
All checks were successful
Release Tag / release (push) Successful in 14s
2025-01-22 19:28:45 -06:00
ca069a20ee fix: track cache hits and misses in average cache state 2025-01-22 18:43:07 -06:00
3e8a92b865 fix: log memory statistics only if memory is enabled
All checks were successful
Release Tag / release (push) Successful in 9s
2025-01-22 18:29:19 -06:00
b7652ed7cc fix: initialize average cache state with cleared values
All checks were successful
Release Tag / release (push) Successful in 9s
2025-01-22 18:25:16 -06:00
08b8d0ce3d fix: enhance logging for memory and disk statistics in cache 2025-01-22 18:23:53 -06:00
53847db0e9 fix: improve cache state clearing and average calculation logic 2025-01-22 18:23:47 -06:00
d11c294010 chore: update goreleaser configuration for draft releases and name template
All checks were successful
Release Tag / release (push) Successful in 8s
2025-01-22 18:11:33 -06:00
f951f5fd8e chore: rename workflows for clarity and consistency
All checks were successful
Release Tag / release (push) Successful in 9s
2025-01-22 18:00:55 -06:00
19 changed files with 584 additions and 232 deletions

View File

@@ -1,4 +1,4 @@
name: Release name: Release Tag
on: on:
push: push:
tags: tags:
@@ -6,7 +6,6 @@ on:
jobs: jobs:
release: release:
name: Build versioned release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@@ -1,4 +1,4 @@
name: CI name: PR Check
on: on:
- pull_request - pull_request

View File

@@ -36,8 +36,9 @@ changelog:
- "^test:" - "^test:"
release: release:
name_template: '{{.ProjectName}}-{{.Version}}'
footer: >- footer: >-
--- ---
Released by [GoReleaser](https://github.com/goreleaser/goreleaser). Released by [GoReleaser](https://github.com/goreleaser/goreleaser).

35
.vscode/launch.json vendored
View File

@@ -5,7 +5,7 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Launch Package", "name": "Launch Memory & Disk",
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
@@ -17,6 +17,39 @@
"10G", "10G",
"--disk-path", "--disk-path",
"tmp/disk", "tmp/disk",
"--upstream",
"http://192.168.2.88:80",
"--verbose",
],
},
{
"name": "Launch Disk Only",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"args": [
"--disk",
"10G",
"--disk-path",
"tmp/disk",
"--upstream",
"http://192.168.2.88:80",
"--verbose",
],
},
{
"name": "Launch Memory Only",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"args": [
"--memory",
"1G",
"--upstream",
"http://192.168.2.88:80",
"--verbose",
], ],
} }
] ]

View File

@@ -4,6 +4,7 @@ import (
"os" "os"
"s1d3sw1ped/SteamCache2/steamcache" "s1d3sw1ped/SteamCache2/steamcache"
"github.com/rs/zerolog"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -13,6 +14,10 @@ var (
disk string disk string
diskmultiplier int diskmultiplier int
diskpath string diskpath string
upstream string
pprof bool
verbose bool
) )
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -24,6 +29,10 @@ var rootCmd = &cobra.Command{
By caching game files, SteamCache2 ensures that subsequent downloads of the same files are served from the local cache, By caching game files, SteamCache2 ensures that subsequent downloads of the same files are served from the local cache,
significantly improving download times and reducing the load on the internet connection.`, significantly improving download times and reducing the load on the internet connection.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if verbose {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
sc := steamcache.New( sc := steamcache.New(
":80", ":80",
memory, memory,
@@ -31,6 +40,8 @@ var rootCmd = &cobra.Command{
disk, disk,
diskmultiplier, diskmultiplier,
diskpath, diskpath,
upstream,
pprof,
) )
sc.Run() sc.Run()
}, },
@@ -46,9 +57,15 @@ func Execute() {
} }
func init() { func init() {
rootCmd.Flags().StringVarP(&memory, "memory", "m", "100MB", "The size of the memory cache") rootCmd.Flags().StringVarP(&memory, "memory", "m", "0", "The size of the memory cache")
rootCmd.Flags().IntVarP(&memorymultiplier, "memory-multiplier", "M", 10, "The multiplier for the memory cache") rootCmd.Flags().IntVarP(&memorymultiplier, "memory-gc", "M", 10, "The gc value for the memory cache")
rootCmd.Flags().StringVarP(&disk, "disk", "d", "10GB", "The size of the disk cache") rootCmd.Flags().StringVarP(&disk, "disk", "d", "0", "The size of the disk cache")
rootCmd.Flags().IntVarP(&diskmultiplier, "disk-multiplier", "D", 10, "The multiplier for the disk cache") rootCmd.Flags().IntVarP(&diskmultiplier, "disk-gc", "D", 100, "The gc value for the disk cache")
rootCmd.Flags().StringVarP(&diskpath, "disk-path", "p", "tmp/steamcache2-disk", "The path to the disk cache") rootCmd.Flags().StringVarP(&diskpath, "disk-path", "p", "", "The path to the disk cache")
rootCmd.Flags().StringVarP(&upstream, "upstream", "u", "", "The upstream server to proxy requests overrides the host header from the client but forwards the original host header to the upstream server")
rootCmd.Flags().BoolVarP(&pprof, "pprof", "P", false, "Enable pprof")
rootCmd.Flags().MarkHidden("pprof")
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
} }

View File

@@ -14,11 +14,15 @@ type AvgCacheState struct {
// New creates a new average cache state with the given size. // New creates a new average cache state with the given size.
func New(size int) *AvgCacheState { func New(size int) *AvgCacheState {
return &AvgCacheState{ a := &AvgCacheState{
size: size, size: size,
avgs: make([]cachestate.CacheState, size), avgs: make([]cachestate.CacheState, size),
mu: sync.Mutex{}, mu: sync.Mutex{},
} }
a.Clear()
return a
} }
// Clear resets the average cache state to zero. // Clear resets the average cache state to zero.
@@ -26,7 +30,9 @@ func (a *AvgCacheState) Clear() {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
a.avgs = make([]cachestate.CacheState, a.size) // zeroed for i := 0; i < len(a.avgs); i++ {
a.avgs[i] = cachestate.CacheStateMiss
}
} }
// Add adds a cache state to the average cache state. // Add adds a cache state to the average cache state.
@@ -45,16 +51,13 @@ func (a *AvgCacheState) Avg() float64 {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
var hits, misses int var hits int
for _, cs := range a.avgs { for _, cs := range a.avgs {
switch cs { if cs == cachestate.CacheStateHit {
case cachestate.CacheStateHit:
hits++ hits++
case cachestate.CacheStateMiss:
misses++
} }
} }
total := hits + misses
return float64(hits) / float64(total) return float64(hits) / float64(len(a.avgs))
} }

View File

@@ -1,75 +1,49 @@
package steamcache package steamcache
import ( import (
"s1d3sw1ped/SteamCache2/steamcache/logger" "runtime/debug"
"s1d3sw1ped/SteamCache2/vfs" "s1d3sw1ped/SteamCache2/vfs"
"s1d3sw1ped/SteamCache2/vfs/cachestate" "s1d3sw1ped/SteamCache2/vfs/cachestate"
"time" "time"
"github.com/docker/go-units"
"golang.org/x/exp/rand" "golang.org/x/exp/rand"
) )
func randomgc(vfss vfs.VFS, stats []*vfs.FileInfo) int64 { func init() {
// Pick a random file to delete // Set the GC percentage to 50%. This is a good balance between performance and memory usage.
randfile := stats[rand.Intn(len(stats))] debug.SetGCPercent(50)
sz := randfile.Size()
err := vfss.Delete(randfile.Name())
if err != nil {
// If we failed to delete the file, log it and return 0
// logger.Logger.Error().Err(err).Msgf("Failed to delete %s", randfile.Name())
return 0
}
return sz
} }
func memorygc(vfss vfs.VFS, size int) { // RandomGC randomly deletes files until we've reclaimed enough space.
tstart := time.Now() func randomgc(vfss vfs.VFS, size uint) (uint, uint) {
// Randomly delete files until we've reclaimed enough space.
random := func(vfss vfs.VFS, stats []*vfs.FileInfo) int64 {
randfile := stats[rand.Intn(len(stats))]
sz := randfile.Size()
err := vfss.Delete(randfile.Name())
if err != nil {
return 0
}
return sz
}
deletions := 0 deletions := 0
targetreclaim := int64(size) targetreclaim := int64(size)
var reclaimed int64 var reclaimed int64
stats := vfss.StatAll() stats := vfss.StatAll()
for { for {
reclaimed += randomgc(vfss, stats)
deletions++
if reclaimed >= targetreclaim { if reclaimed >= targetreclaim {
break break
} }
} reclaimed += random(vfss, stats)
logger.Logger.Info().
Str("name", vfss.Name()).
Str("duration", time.Since(tstart).String()).
Str("reclaimed", units.HumanSize(float64(reclaimed))).
Int("deletions", deletions).
Msgf("GC")
}
func diskgc(vfss vfs.VFS, size int) {
tstart := time.Now()
deletions := 0
targetreclaim := int64(size)
var reclaimed int64
stats := vfss.StatAll()
for {
reclaimed += randomgc(vfss, stats)
deletions++ deletions++
if reclaimed >= targetreclaim {
break
}
} }
logger.Logger.Info(). return uint(reclaimed), uint(deletions)
Str("name", vfss.Name()).
Str("duration", time.Since(tstart).String()).
Str("reclaimed", units.HumanSize(float64(reclaimed))).
Int("deletions", deletions).
Msgf("GC")
} }
func cachehandler(fi *vfs.FileInfo, cs cachestate.CacheState) bool { func cachehandler(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
return time.Since(fi.AccessTime()) < time.Minute*10 // Put files in the cache if they've been accessed twice in the last 10 minutes return time.Since(fi.AccessTime()) < time.Second*10 // Put hot files in the fast vfs if equipped
} }

View File

@@ -6,4 +6,8 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
func init() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
var Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger() var Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger()

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"runtime"
"s1d3sw1ped/SteamCache2/steamcache/avgcachestate" "s1d3sw1ped/SteamCache2/steamcache/avgcachestate"
"s1d3sw1ped/SteamCache2/steamcache/logger" "s1d3sw1ped/SteamCache2/steamcache/logger"
"s1d3sw1ped/SteamCache2/version" "s1d3sw1ped/SteamCache2/version"
@@ -20,23 +21,31 @@ import (
"sync" "sync"
"time" "time"
pprof "net/http/pprof"
"github.com/docker/go-units" "github.com/docker/go-units"
) )
type SteamCache struct { type SteamCache struct {
address string pprof bool
vfs vfs.VFS address string
upstream string
vfs vfs.VFS
memory *memory.MemoryFS memory *memory.MemoryFS
disk *disk.DiskFS disk *disk.DiskFS
memorygc *gc.GCFS
diskgc *gc.GCFS
hits *avgcachestate.AvgCacheState hits *avgcachestate.AvgCacheState
dirty bool dirty bool
mu sync.Mutex mu sync.Mutex
} }
func New(address string, memorySize string, memoryMultiplier int, diskSize string, diskMultiplier int, diskPath string) *SteamCache { func New(address string, memorySize string, memoryMultiplier int, diskSize string, diskMultiplier int, diskPath, upstream string, pprof bool) *SteamCache {
memorysize, err := units.FromHumanSize(memorySize) memorysize, err := units.FromHumanSize(memorySize)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -47,35 +56,66 @@ func New(address string, memorySize string, memoryMultiplier int, diskSize strin
panic(err) panic(err)
} }
m := memory.New(memorysize) c := cache.New(
d := disk.New(diskPath, disksize) cachehandler,
)
var m *memory.MemoryFS
var mgc *gc.GCFS
if memorysize > 0 {
m = memory.New(memorysize)
mgc = gc.New(m, memoryMultiplier, randomgc)
}
var d *disk.DiskFS
var dgc *gc.GCFS
if disksize > 0 {
d = disk.New(diskPath, disksize)
dgc = gc.New(d, diskMultiplier, randomgc)
}
// 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
c.SetSlow(mgc)
logger.Logger.Info().Bool("memory", true).Bool("disk", false).Msg("configuration")
} else if disksize != 0 && memorysize == 0 {
// disk only mode
c.SetSlow(dgc)
logger.Logger.Info().Bool("memory", false).Bool("disk", true).Msg("configuration")
} else if disksize != 0 && memorysize != 0 {
// memory and disk mode
c.SetFast(mgc)
c.SetSlow(dgc)
logger.Logger.Info().Bool("memory", true).Bool("disk", true).Msg("configuration")
} 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)
}
sc := &SteamCache{ sc := &SteamCache{
address: address, pprof: pprof,
vfs: syncfs.New( upstream: upstream,
cache.New( address: address,
gc.New( vfs: syncfs.New(c),
m,
memoryMultiplier,
memorygc,
),
gc.New(
d,
diskMultiplier,
diskgc,
),
cachehandler,
),
),
memory: m, memory: m,
disk: d, disk: d,
hits: avgcachestate.New(10000), memorygc: mgc,
diskgc: dgc,
hits: avgcachestate.New(100),
} }
if d.Size() > d.Capacity() { if d != nil {
diskgc(d, int(d.Size()-d.Capacity())) if d.Size() > d.Capacity() {
randomgc(d, uint(d.Size()-d.Capacity()))
}
} }
return sc return sc
@@ -84,12 +124,21 @@ func New(address string, memorySize string, memoryMultiplier int, diskSize strin
func (sc *SteamCache) Run() { func (sc *SteamCache) Run() {
logger.Logger.Info().Str("address", sc.address).Str("version", version.Version).Msg("listening") logger.Logger.Info().Str("address", sc.address).Str("version", version.Version).Msg("listening")
if sc.upstream != "" {
_, err := http.Get(sc.upstream)
if err != nil {
logger.Logger.Error().Err(err).Str("upstream", sc.upstream).Msg("Failed to connect to upstream server")
os.Exit(1)
}
logger.Logger.Info().Str("upstream", sc.upstream).Msg("connected")
}
sc.mu.Lock() sc.mu.Lock()
sc.dirty = true sc.dirty = true
sc.mu.Unlock() sc.mu.Unlock()
sc.LogStats() sc.LogStats()
t := time.NewTicker(10 * time.Second) t := time.NewTicker(1 * time.Second)
go func() { go func() {
for range t.C { for range t.C {
sc.LogStats() sc.LogStats()
@@ -111,16 +160,71 @@ func (sc *SteamCache) LogStats() {
sc.mu.Lock() sc.mu.Lock()
defer sc.mu.Unlock() defer sc.mu.Unlock()
if sc.dirty { if sc.dirty {
logger.Logger.Info().Msg("") // empty line to separate log entries for better readability
if sc.memory != nil { // only log memory if memory is enabled
lifetimeBytes, lifetimeFiles, reclaimedBytes, deletedFiles, gcTime := sc.memorygc.Stats()
logger.Logger.Info().
Str("size", units.HumanSize(float64(sc.memory.Size()))).
Str("capacity", units.HumanSize(float64(sc.memory.Capacity()))).
Str("files", fmt.Sprintf("%d", len(sc.memory.StatAll()))).
Msg("memory")
logger.Logger.Info().
Str("data_total", units.HumanSize(float64(lifetimeBytes))).
Uint("files_total", lifetimeFiles).
Str("data", units.HumanSize(float64(reclaimedBytes))).
Uint("files", deletedFiles).
Str("gc_time", gcTime.String()).
Msg("memory_gc")
}
if sc.disk != nil { // only log disk if disk is enabled
lifetimeBytes, lifetimeFiles, reclaimedBytes, deletedFiles, gcTime := sc.diskgc.Stats()
logger.Logger.Info().
Str("size", units.HumanSize(float64(sc.disk.Size()))).
Str("capacity", units.HumanSize(float64(sc.disk.Capacity()))).
Str("files", fmt.Sprintf("%d", len(sc.disk.StatAll()))).
Msg("disk")
logger.Logger.Info().
Str("data_total", units.HumanSize(float64(lifetimeBytes))).
Uint("files_total", lifetimeFiles).
Str("data", units.HumanSize(float64(reclaimedBytes))).
Uint("files", deletedFiles).
Str("gc_time", gcTime.String()).
Msg("disk_gc")
}
// log golang Garbage Collection stats
var m runtime.MemStats
runtime.ReadMemStats(&m)
logger.Logger.Info().
Str("alloc", units.HumanSize(float64(m.Alloc))).
Str("sys", units.HumanSize(float64(m.Sys))).
Msg("go_gc")
logger.Logger.Info(). logger.Logger.Info().
Str("memory", fmt.Sprintf("%s/%s", units.HumanSize(float64(sc.memory.Size())), units.HumanSize(float64(sc.memory.Capacity())))).Int("memory-files", len(sc.memory.StatAll())).
Str("disk", fmt.Sprintf("%s/%s", units.HumanSize(float64(sc.disk.Size())), units.HumanSize(float64(sc.disk.Capacity())))).Int("disk-files", len(sc.disk.StatAll())).
Str("hitrate", fmt.Sprintf("%.2f%%", sc.hits.Avg()*100)). Str("hitrate", fmt.Sprintf("%.2f%%", sc.hits.Avg()*100)).
Msg("stats") Msg("cache")
sc.dirty = false sc.dirty = false
} }
} }
func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if sc.pprof && r.URL.Path == "/debug/pprof/" {
pprof.Index(w, r)
return
} else if sc.pprof && strings.HasPrefix(r.URL.Path, "/debug/pprof/") {
pprof.Handler(strings.TrimPrefix(r.URL.Path, "/debug/pprof/")).ServeHTTP(w, r)
return
}
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
http.Error(w, "Only GET method is supported", http.StatusMethodNotAllowed) http.Error(w, "Only GET method is supported", http.StatusMethodNotAllowed)
return return
@@ -133,54 +237,68 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
if r.Header.Get("User-Agent") != "Valve/Steam HTTP Client 1.0" {
http.Error(w, "Only Valve/Steam HTTP Client 1.0 is supported", http.StatusForbidden)
return
}
if strings.Contains(r.URL.String(), "manifest") {
w.Header().Add("X-LanCache-Processed-By", "SteamCache2")
forward(w, r)
return
}
// tstart := time.Now()
// defer func() {
// logger.Logger.Info().Str("method", r.Method).Str("url", r.URL.String()).Str("status", w.Header().Get("X-LanCache-Status")).Dur("duration", time.Since(tstart)).Msg("Request")
// }()
sc.mu.Lock() sc.mu.Lock()
sc.dirty = true sc.dirty = true
sc.mu.Unlock() sc.mu.Unlock()
w.Header().Add("X-LanCache-Processed-By", "SteamCache2") // SteamPrefill uses this header to determine if the request was processed by the cache maybe steam uses it too w.Header().Add("X-LanCache-Processed-By", "SteamCache2") // SteamPrefill uses this header to determine if the request was processed by the cache maybe steam uses it too
cacheKey := r.URL.String() cacheKey := strings.ReplaceAll(r.URL.String()[1:], "\\", "/") // replace all backslashes with forward slashes shouldn't be necessary but just in case
if cacheKey == "" {
// if vfs is also a vfs.GetSer, we can use it to get the cache state http.Error(w, "Invalid URL", http.StatusBadRequest)
return
}
data, err := sc.vfs.Get(cacheKey) data, err := sc.vfs.Get(cacheKey)
if err == nil { if err == nil {
sc.hits.Add(cachestate.CacheStateHit) sc.hits.Add(cachestate.CacheStateHit)
w.Header().Add("X-LanCache-Status", "HIT") w.Header().Add("X-LanCache-Status", "HIT")
w.Write(data) w.Write(data)
logger.Logger.Debug().Str("key", r.URL.String()).Msg("cache")
return return
} }
htt := "http://" var req *http.Request
if r.Header.Get("X-Sls-Https") == "enable" { if sc.upstream != "" { // if an upstream server is configured, proxy the request to the upstream server
htt = "https://" ur, err := url.JoinPath(sc.upstream, r.URL.String())
if err != nil {
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
return
}
req, err = http.NewRequest(http.MethodGet, ur, nil)
if err != nil {
http.Error(w, "Failed to create request", http.StatusInternalServerError)
return
}
req.Host = r.Host
logger.Logger.Debug().Str("key", cacheKey).Str("host", sc.upstream).Msg("upstream")
} else { // if no upstream server is configured, proxy the request to the host specified in the request
host := r.Host
if r.Header.Get("X-Sls-Https") == "enable" {
host = "https://" + host
} else {
host = "http://" + host
}
ur, err := url.JoinPath(host, r.URL.String())
if err != nil {
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
return
}
req, err = http.NewRequest(http.MethodGet, ur, nil)
if err != nil {
http.Error(w, "Failed to create request", http.StatusInternalServerError)
return
}
logger.Logger.Debug().Str("key", cacheKey).Str("host", host).Msg("forward")
} }
base := htt + r.Host req.Header.Add("X-Sls-Https", r.Header.Get("X-Sls-Https"))
req.Header.Add("User-Agent", r.Header.Get("User-Agent"))
hosturl, err := url.JoinPath(base, cacheKey) resp, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
return
}
resp, err := http.Get(hosturl)
if err != nil { if err != nil {
http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError) http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError)
return return
@@ -203,40 +321,3 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-LanCache-Status", "MISS") w.Header().Add("X-LanCache-Status", "MISS")
w.Write(body) w.Write(body)
} }
func forward(w http.ResponseWriter, r *http.Request) {
htt := "http://"
if r.Header.Get("X-Sls-Https") == "enable" {
htt = "https://"
}
base := htt + r.Host
cacheKey := r.URL.String()
hosturl, err := url.JoinPath(base, cacheKey)
if err != nil {
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
return
}
resp, err := http.Get(hosturl)
if err != nil {
http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
http.Error(w, "Failed to fetch the requested URL", resp.StatusCode)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Failed to read response body", http.StatusInternalServerError)
return
}
w.Write(body)
}

View File

@@ -0,0 +1,71 @@
package steamcache
import (
"os"
"path/filepath"
"testing"
)
func TestCaching(t *testing.T) {
t.Parallel()
td := t.TempDir()
os.WriteFile(filepath.Join(td, "key2"), []byte("value2"), 0644)
sc := New("localhost:8080", "1GB", 10, "1GB", 100, td, "", false)
sc.dirty = true
sc.LogStats()
if err := sc.vfs.Set("key", []byte("value")); err != nil {
t.Errorf("Set failed: %v", err)
}
if err := sc.vfs.Set("key1", []byte("value1")); err != nil {
t.Errorf("Set failed: %v", err)
}
sc.dirty = true
sc.LogStats()
if sc.diskgc.Size() != 17 {
t.Errorf("Size failed: got %d, want %d", sc.diskgc.Size(), 17)
}
if sc.vfs.Size() != 17 {
t.Errorf("Size failed: got %d, want %d", sc.vfs.Size(), 17)
}
if d, err := sc.vfs.Get("key"); err != nil {
t.Errorf("Get failed: %v", err)
} else if string(d) != "value" {
t.Errorf("Get failed: got %s, want %s", d, "value")
}
if d, err := sc.vfs.Get("key1"); err != nil {
t.Errorf("Get failed: %v", err)
} else if string(d) != "value1" {
t.Errorf("Get failed: got %s, want %s", d, "value1")
}
if d, err := sc.vfs.Get("key2"); err != nil {
t.Errorf("Get failed: %v", err)
} else if string(d) != "value2" {
t.Errorf("Get failed: got %s, want %s", d, "value2")
}
if sc.diskgc.Size() != 17 {
t.Errorf("Size failed: got %d, want %d", sc.diskgc.Size(), 17)
}
if sc.vfs.Size() != 17 {
t.Errorf("Size failed: got %d, want %d", sc.vfs.Size(), 17)
}
sc.memory.Delete("key2")
os.Remove(filepath.Join(td, "key2"))
if _, err := sc.vfs.Get("key2"); err == nil {
t.Errorf("Get failed: got nil, want error")
}
}

25
vfs/cache/cache.go vendored
View File

@@ -21,23 +21,24 @@ type CacheFS struct {
type CacheHandler func(*vfs.FileInfo, cachestate.CacheState) bool type CacheHandler func(*vfs.FileInfo, cachestate.CacheState) bool
// New creates a new CacheFS. fast is used for caching, and slow is used for storage. fast should obviously be faster than slow. // New creates a new CacheFS. fast is used for caching, and slow is used for storage. fast should obviously be faster than slow.
func New(fast, slow vfs.VFS, cacheHandler CacheHandler) *CacheFS { func New(cacheHandler CacheHandler) *CacheFS {
if slow == nil {
panic("slow is nil")
}
if fast == slow {
panic("fast and slow are the same")
}
return &CacheFS{ return &CacheFS{
fast: fast,
slow: slow,
cacheHandler: cacheHandler, cacheHandler: cacheHandler,
} }
} }
func (c *CacheFS) SetSlow(vfs vfs.VFS) {
if vfs == nil {
panic("vfs is nil") // panic if the vfs is nil
}
c.slow = vfs
}
func (c *CacheFS) SetFast(vfs vfs.VFS) {
c.fast = vfs
}
// cacheState returns the state of the file at key. // cacheState returns the state of the file at key.
func (c *CacheFS) cacheState(key string) cachestate.CacheState { func (c *CacheFS) cacheState(key string) cachestate.CacheState {
if c.fast != nil { if c.fast != nil {

View File

@@ -20,7 +20,9 @@ func TestNew(t *testing.T) {
fast := testMemory() fast := testMemory()
slow := testMemory() slow := testMemory()
cache := New(fast, slow, nil) cache := New(nil)
cache.SetFast(fast)
cache.SetSlow(slow)
if cache == nil { if cache == nil {
t.Fatal("expected cache to be non-nil") t.Fatal("expected cache to be non-nil")
} }
@@ -35,7 +37,9 @@ func TestNewPanics(t *testing.T) {
} }
}() }()
New(nil, nil, nil) cache := New(nil)
cache.SetFast(nil)
cache.SetSlow(nil)
} }
func TestSetAndGet(t *testing.T) { func TestSetAndGet(t *testing.T) {
@@ -43,7 +47,9 @@ func TestSetAndGet(t *testing.T) {
fast := testMemory() fast := testMemory()
slow := testMemory() slow := testMemory()
cache := New(fast, slow, nil) cache := New(nil)
cache.SetFast(fast)
cache.SetSlow(slow)
key := "test" key := "test"
value := []byte("value") value := []byte("value")
@@ -66,7 +72,8 @@ func TestSetAndGetNoFast(t *testing.T) {
t.Parallel() t.Parallel()
slow := testMemory() slow := testMemory()
cache := New(nil, slow, nil) cache := New(nil)
cache.SetSlow(slow)
key := "test" key := "test"
value := []byte("value") value := []byte("value")
@@ -89,9 +96,11 @@ func TestCaching(t *testing.T) {
fast := testMemory() fast := testMemory()
slow := testMemory() slow := testMemory()
cache := New(fast, slow, func(fi *vfs.FileInfo, cs cachestate.CacheState) bool { cache := New(func(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
return true return true
}) })
cache.SetFast(fast)
cache.SetSlow(slow)
key := "test" key := "test"
value := []byte("value") value := []byte("value")
@@ -148,7 +157,9 @@ func TestGetNotFound(t *testing.T) {
fast := testMemory() fast := testMemory()
slow := testMemory() slow := testMemory()
cache := New(fast, slow, nil) cache := New(nil)
cache.SetFast(fast)
cache.SetSlow(slow)
_, err := cache.Get("nonexistent") _, err := cache.Get("nonexistent")
if !errors.Is(err, vfserror.ErrNotFound) { if !errors.Is(err, vfserror.ErrNotFound) {
@@ -161,7 +172,9 @@ func TestDelete(t *testing.T) {
fast := testMemory() fast := testMemory()
slow := testMemory() slow := testMemory()
cache := New(fast, slow, nil) cache := New(nil)
cache.SetFast(fast)
cache.SetSlow(slow)
key := "test" key := "test"
value := []byte("value") value := []byte("value")
@@ -185,7 +198,9 @@ func TestStat(t *testing.T) {
fast := testMemory() fast := testMemory()
slow := testMemory() slow := testMemory()
cache := New(fast, slow, nil) cache := New(nil)
cache.SetFast(fast)
cache.SetSlow(slow)
key := "test" key := "test"
value := []byte("value") value := []byte("value")

View File

@@ -1,11 +1,13 @@
package disk package disk
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"s1d3sw1ped/SteamCache2/steamcache/logger" "s1d3sw1ped/SteamCache2/steamcache/logger"
"s1d3sw1ped/SteamCache2/vfs" "s1d3sw1ped/SteamCache2/vfs"
"s1d3sw1ped/SteamCache2/vfs/vfserror" "s1d3sw1ped/SteamCache2/vfs/vfserror"
"strings"
"sync" "sync"
"time" "time"
@@ -27,6 +29,24 @@ type DiskFS struct {
// New creates a new DiskFS. // New creates a new DiskFS.
func new(root string, capacity int64, skipinit bool) *DiskFS { func new(root string, capacity int64, skipinit bool) *DiskFS {
if capacity <= 0 {
panic("disk capacity must be greater than 0") // panic if the capacity is less than or equal to 0
}
if root == "" {
panic("disk root must not be empty") // panic if the root is empty
}
fi, err := os.Stat(root)
if err != nil {
if !os.IsNotExist(err) {
panic(err) // panic if the error is something other than not found
}
}
if !fi.IsDir() {
panic("disk root must be a directory") // panic if the root is not a directory
}
dfs := &DiskFS{ dfs := &DiskFS{
root: root, root: root,
info: make(map[string]*vfs.FileInfo), info: make(map[string]*vfs.FileInfo),
@@ -60,7 +80,14 @@ func (d *DiskFS) init() {
d.walk(d.root) d.walk(d.root)
d.sg.Wait() d.sg.Wait()
logger.Logger.Info().Str("name", d.Name()).Str("root", d.root).Str("capacity", units.HumanSize(float64(d.capacity))).Str("duration", time.Since(tstart).String()).Msg("init") logger.Logger.Info().
Str("name", d.Name()).
Str("root", d.root).
Str("capacity", units.HumanSize(float64(d.capacity))).
Str("size", units.HumanSize(float64(d.Size()))).
Str("files", fmt.Sprint(len(d.info))).
Str("duration", time.Since(tstart).String()).
Msg("init")
} }
func (d *DiskFS) walk(path string) { func (d *DiskFS) walk(path string) {
@@ -82,7 +109,8 @@ func (d *DiskFS) walk(path string) {
} }
d.mu.Lock() d.mu.Lock()
k := npath[len(d.root)+1:] k := strings.ReplaceAll(npath[len(d.root)+1:], "\\", "/")
logger.Logger.Debug().Str("name", k).Str("root", d.root).Msg("walk")
d.info[k] = vfs.NewFileInfoFromOS(info, k) d.info[k] = vfs.NewFileInfoFromOS(info, k)
d.mu.Unlock() d.mu.Unlock()
@@ -101,9 +129,10 @@ func (d *DiskFS) Name() string {
} }
func (d *DiskFS) Size() int64 { func (d *DiskFS) Size() int64 {
var size int64
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
var size int64
for _, v := range d.info { for _, v := range d.info {
size += v.Size() size += v.Size()
} }
@@ -111,24 +140,34 @@ func (d *DiskFS) Size() int64 {
} }
func (d *DiskFS) Set(key string, src []byte) error { func (d *DiskFS) Set(key string, src []byte) error {
if key == "" {
return vfserror.ErrInvalidKey
}
if key[0] == '/' {
return vfserror.ErrInvalidKey
}
if d.capacity > 0 { if d.capacity > 0 {
if size := d.Size() + int64(len(src)); size > d.capacity { if size := d.Size() + int64(len(src)); size > d.capacity {
return vfserror.ErrDiskFull return vfserror.ErrDiskFull
} }
} }
logger.Logger.Debug().Str("name", key).Str("root", d.root).Msg("set")
if _, err := d.Stat(key); err == nil { if _, err := d.Stat(key); err == nil {
logger.Logger.Debug().Str("name", key).Str("root", d.root).Msg("delete")
d.Delete(key) d.Delete(key)
} }
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
os.MkdirAll(filepath.Join(d.root, filepath.Dir(key)), 0755) os.MkdirAll(d.root+"/"+filepath.Dir(key), 0755)
if err := os.WriteFile(filepath.Join(d.root, key), src, 0644); err != nil { if err := os.WriteFile(d.root+"/"+key, src, 0644); err != nil {
return err return err
} }
fi, err := os.Stat(filepath.Join(d.root, key)) fi, err := os.Stat(d.root + "/" + key)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -140,6 +179,13 @@ func (d *DiskFS) Set(key string, src []byte) error {
// Delete deletes the value of key. // Delete deletes the value of key.
func (d *DiskFS) Delete(key string) error { func (d *DiskFS) Delete(key string) error {
if key == "" {
return vfserror.ErrInvalidKey
}
if key[0] == '/' {
return vfserror.ErrInvalidKey
}
_, err := d.Stat(key) _, err := d.Stat(key)
if err != nil { if err != nil {
return err return err
@@ -147,6 +193,7 @@ func (d *DiskFS) Delete(key string) error {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
delete(d.info, key) delete(d.info, key)
if err := os.Remove(filepath.Join(d.root, key)); err != nil { if err := os.Remove(filepath.Join(d.root, key)); err != nil {
return err return err
@@ -157,6 +204,13 @@ func (d *DiskFS) Delete(key string) error {
// Get gets the value of key and returns it. // Get gets the value of key and returns it.
func (d *DiskFS) Get(key string) ([]byte, error) { func (d *DiskFS) Get(key string) ([]byte, error) {
if key == "" {
return nil, vfserror.ErrInvalidKey
}
if key[0] == '/' {
return nil, vfserror.ErrInvalidKey
}
_, err := d.Stat(key) _, err := d.Stat(key)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -175,25 +229,23 @@ func (d *DiskFS) Get(key string) ([]byte, error) {
// Stat returns the FileInfo of key. If key is not found in the cache, it will stat the file on disk. If the file is not found on disk, it will return vfs.ErrNotFound. // Stat returns the FileInfo of key. If key is not found in the cache, it will stat the file on disk. If the file is not found on disk, it will return vfs.ErrNotFound.
func (d *DiskFS) Stat(key string) (*vfs.FileInfo, error) { func (d *DiskFS) Stat(key string) (*vfs.FileInfo, error) {
d.mu.Lock() if key == "" {
fi, ok := d.info[key] return nil, vfserror.ErrInvalidKey
d.mu.Unlock() // unlock before statting the file }
if key[0] == '/' {
if !ok { return nil, vfserror.ErrInvalidKey
fii, err := os.Stat(filepath.Join(d.root, key))
if err != nil {
return nil, vfserror.ErrNotFound
}
d.mu.Lock() // relock to update the info map
defer d.mu.Unlock() // nothing else needs to unlock before returning
d.info[key] = vfs.NewFileInfoFromOS(fii, key)
fi = d.info[key]
// fallthrough to return fi with shiny new info
} }
return fi, nil logger.Logger.Debug().Str("name", key).Str("root", d.root).Msg("stat")
d.mu.Lock()
defer d.mu.Unlock()
if fi, ok := d.info[key]; !ok {
return nil, vfserror.ErrNotFound
} else {
return fi, nil
}
} }
func (m *DiskFS) StatAll() []*vfs.FileInfo { func (m *DiskFS) StatAll() []*vfs.FileInfo {

View File

@@ -85,3 +85,62 @@ func TestInit(t *testing.T) {
t.Errorf("Stat failed: got %s, want %s", s.Name(), "key") t.Errorf("Stat failed: got %s, want %s", s.Name(), "key")
} }
} }
func TestDiskSizeDiscrepancy(t *testing.T) {
t.Parallel()
td := t.TempDir()
assumedSize := int64(6 + 5 + 6) // 6 + 5 + 6 bytes for key, key1, key2
os.WriteFile(filepath.Join(td, "key2"), []byte("value2"), 0644)
m := New(td, 1024)
if 6 != m.Size() {
t.Errorf("Size failed: got %d, want %d", m.Size(), 6)
}
if err := m.Set("key", []byte("value")); err != nil {
t.Errorf("Set failed: %v", err)
}
if err := m.Set("key1", []byte("value1")); err != nil {
t.Errorf("Set failed: %v", err)
}
if assumedSize != m.Size() {
t.Errorf("Size failed: got %d, want %d", m.Size(), assumedSize)
}
if d, err := m.Get("key"); err != nil {
t.Errorf("Get failed: %v", err)
} else if string(d) != "value" {
t.Errorf("Get failed: got %s, want %s", d, "value")
}
if d, err := m.Get("key1"); err != nil {
t.Errorf("Get failed: %v", err)
} else if string(d) != "value1" {
t.Errorf("Get failed: got %s, want %s", d, "value1")
}
m = New(td, 1024)
if assumedSize != m.Size() {
t.Errorf("Size failed: got %d, want %d", m.Size(), assumedSize)
}
if d, err := m.Get("key"); err != nil {
t.Errorf("Get failed: %v", err)
} else if string(d) != "value" {
t.Errorf("Get failed: got %s, want %s", d, "value")
}
if d, err := m.Get("key1"); err != nil {
t.Errorf("Get failed: %v", err)
} else if string(d) != "value1" {
t.Errorf("Get failed: got %s, want %s", d, "value1")
}
if assumedSize != m.Size() {
t.Errorf("Size failed: got %d, want %d", m.Size(), assumedSize)
}
}

View File

@@ -12,9 +12,9 @@ type FileInfo struct {
ATime time.Time ATime time.Time
} }
func NewFileInfo(name string, size int64, modTime time.Time) *FileInfo { func NewFileInfo(key string, size int64, modTime time.Time) *FileInfo {
return &FileInfo{ return &FileInfo{
name: name, name: key,
size: size, size: size,
MTime: modTime, MTime: modTime,
ATime: time.Now(), ATime: time.Now(),

View File

@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"s1d3sw1ped/SteamCache2/vfs" "s1d3sw1ped/SteamCache2/vfs"
"s1d3sw1ped/SteamCache2/vfs/vfserror" "s1d3sw1ped/SteamCache2/vfs/vfserror"
"sync"
"time"
) )
// Ensure GCFS implements VFS. // Ensure GCFS implements VFS.
@@ -12,14 +14,23 @@ var _ vfs.VFS = (*GCFS)(nil)
// GCFS is a virtual file system that calls a GC handler when the disk is full. The GC handler is responsible for freeing up space on the disk. The GCFS is a wrapper around another VFS. // GCFS is a virtual file system that calls a GC handler when the disk is full. The GC handler is responsible for freeing up space on the disk. The GCFS is a wrapper around another VFS.
type GCFS struct { type GCFS struct {
vfs.VFS vfs.VFS
multiplier int multiplier int
gcHanderFunc GCHandlerFunc
// protected by mu
gcHanderFunc GCHandlerFunc
lifetimeBytes, lifetimeFiles uint
reclaimedBytes, deletedFiles uint
gcTime time.Duration
mu sync.Mutex
} }
// GCHandlerFunc is a function that is called when the disk is full and the GCFS needs to free up space. It is passed the VFS and the size of the file that needs to be written. Its up to the implementation to free up space. How much space is freed is also up to the implementation. // GCHandlerFunc is a function that is called when the disk is full and the GCFS needs to free up space. It is passed the VFS and the size of the file that needs to be written. Its up to the implementation to free up space. How much space is freed is also up to the implementation.
type GCHandlerFunc func(vfs vfs.VFS, size int) type GCHandlerFunc func(vfs vfs.VFS, size uint) (reclaimedBytes uint, deletedFiles uint)
func New(vfs vfs.VFS, multiplier int, gcHandlerFunc GCHandlerFunc) *GCFS { func New(vfs vfs.VFS, multiplier int, gcHandlerFunc GCHandlerFunc) *GCFS {
if multiplier <= 0 {
multiplier = 1 // if the multiplier is less than or equal to 0 set it to 1 will be slow but the user can set it to a higher value if they want
}
return &GCFS{ return &GCFS{
VFS: vfs, VFS: vfs,
multiplier: multiplier, multiplier: multiplier,
@@ -27,13 +38,44 @@ func New(vfs vfs.VFS, multiplier int, gcHandlerFunc GCHandlerFunc) *GCFS {
} }
} }
// Stats returns the lifetime bytes, lifetime files, reclaimed bytes and deleted files.
// The lifetime bytes and lifetime files are the total bytes and files that have been freed up by the GC handler.
// The reclaimed bytes and deleted files are the bytes and files that have been freed up by the GC handler since last call to Stats.
// The gc time is the total time spent in the GC handler since last call to Stats.
// The reclaimed bytes and deleted files and gc time are reset to 0 after the call to Stats.
func (g *GCFS) Stats() (lifetimeBytes, lifetimeFiles, reclaimedBytes, deletedFiles uint, gcTime time.Duration) {
g.mu.Lock()
defer g.mu.Unlock()
g.lifetimeBytes += g.reclaimedBytes
g.lifetimeFiles += g.deletedFiles
lifetimeBytes = g.lifetimeBytes
lifetimeFiles = g.lifetimeFiles
reclaimedBytes = g.reclaimedBytes
deletedFiles = g.deletedFiles
gcTime = g.gcTime
g.reclaimedBytes = 0
g.deletedFiles = 0
g.gcTime = time.Duration(0)
return
}
// Set overrides the Set method of the VFS interface. It tries to set the key and src, if it fails due to disk full error, it calls the GC handler and tries again. If it still fails it returns the error. // Set overrides the Set method of the VFS interface. It tries to set the key and src, if it fails due to disk full error, it calls the GC handler and tries again. If it still fails it returns the error.
func (g *GCFS) Set(key string, src []byte) error { func (g *GCFS) Set(key string, src []byte) error {
g.mu.Lock()
defer g.mu.Unlock()
err := g.VFS.Set(key, src) // try to set the key and src err := g.VFS.Set(key, src) // try to set the key and src
if err == vfserror.ErrDiskFull && g.gcHanderFunc != nil { // if the error is disk full and there is a GC handler if err == vfserror.ErrDiskFull && g.gcHanderFunc != nil { // if the error is disk full and there is a GC handler
g.gcHanderFunc(g.VFS, len(src)*g.multiplier) // call the GC handler tstart := time.Now()
err = g.VFS.Set(key, src) // try again after GC if it still fails return the error reclaimedBytes, deletedFiles := g.gcHanderFunc(g.VFS, uint(len(src)*g.multiplier)) // call the GC handler
g.gcTime += time.Since(tstart)
g.reclaimedBytes += reclaimedBytes
g.deletedFiles += deletedFiles
err = g.VFS.Set(key, src) // try again after GC if it still fails return the error
} }
return err return err

View File

@@ -6,7 +6,6 @@ import (
"s1d3sw1ped/SteamCache2/vfs/memory" "s1d3sw1ped/SteamCache2/vfs/memory"
"sort" "sort"
"testing" "testing"
"time"
"golang.org/x/exp/rand" "golang.org/x/exp/rand"
) )
@@ -15,13 +14,11 @@ func TestGCSmallRandom(t *testing.T) {
t.Parallel() t.Parallel()
m := memory.New(1024 * 1024 * 16) m := memory.New(1024 * 1024 * 16)
gc := New(m, 10, func(vfs vfs.VFS, size int) { gc := New(m, 10, func(vfs vfs.VFS, size uint) (uint, uint) {
tstart := time.Now()
deletions := 0 deletions := 0
targetreclaim := int64(size) var reclaimed uint
var reclaimed int64
t.Logf("GC starting to reclaim %d bytes", targetreclaim) t.Logf("GC starting to reclaim %d bytes", size)
stats := vfs.StatAll() stats := vfs.StatAll()
sort.Slice(stats, func(i, j int) bool { sort.Slice(stats, func(i, j int) bool {
@@ -31,7 +28,7 @@ func TestGCSmallRandom(t *testing.T) {
// Delete the oldest files until we've reclaimed enough space. // Delete the oldest files until we've reclaimed enough space.
for _, s := range stats { for _, s := range stats {
sz := s.Size() // Get the size of the file sz := uint(s.Size()) // Get the size of the file
err := vfs.Delete(s.Name()) err := vfs.Delete(s.Name())
if err != nil { if err != nil {
panic(err) panic(err)
@@ -41,12 +38,11 @@ func TestGCSmallRandom(t *testing.T) {
// t.Logf("GC deleting %s, %v", s.Name(), s.AccessTime().Format(time.RFC3339Nano)) // t.Logf("GC deleting %s, %v", s.Name(), s.AccessTime().Format(time.RFC3339Nano))
if reclaimed >= targetreclaim { // We've reclaimed enough space if reclaimed >= size { // We've reclaimed enough space
break break
} }
} }
return uint(reclaimed), uint(deletions)
t.Logf("GC took %v to reclaim %d bytes by deleting %d files", time.Since(tstart), reclaimed, deletions)
}) })
for i := 0; i < 10000; i++ { for i := 0; i < 10000; i++ {
@@ -70,13 +66,11 @@ func TestGCLargeRandom(t *testing.T) {
t.Parallel() t.Parallel()
m := memory.New(1024 * 1024 * 16) // 16MB m := memory.New(1024 * 1024 * 16) // 16MB
gc := New(m, 10, func(vfs vfs.VFS, size int) { gc := New(m, 10, func(vfs vfs.VFS, size uint) (uint, uint) {
tstart := time.Now()
deletions := 0 deletions := 0
targetreclaim := int64(size) var reclaimed uint
var reclaimed int64
t.Logf("GC starting to reclaim %d bytes", targetreclaim) t.Logf("GC starting to reclaim %d bytes", size)
stats := vfs.StatAll() stats := vfs.StatAll()
sort.Slice(stats, func(i, j int) bool { sort.Slice(stats, func(i, j int) bool {
@@ -86,17 +80,17 @@ func TestGCLargeRandom(t *testing.T) {
// Delete the oldest files until we've reclaimed enough space. // Delete the oldest files until we've reclaimed enough space.
for _, s := range stats { for _, s := range stats {
sz := s.Size() // Get the size of the file sz := uint(s.Size()) // Get the size of the file
vfs.Delete(s.Name()) vfs.Delete(s.Name())
reclaimed += sz // Track how much space we've reclaimed reclaimed += sz // Track how much space we've reclaimed
deletions++ // Track how many files we've deleted deletions++ // Track how many files we've deleted
if reclaimed >= targetreclaim { // We've reclaimed enough space if reclaimed >= size { // We've reclaimed enough space
break break
} }
} }
t.Logf("GC took %v to reclaim %d bytes by deleting %d files", time.Since(tstart), reclaimed, deletions) return uint(reclaimed), uint(deletions)
}) })
for i := 0; i < 10000; i++ { for i := 0; i < 10000; i++ {

View File

@@ -25,6 +25,10 @@ type MemoryFS struct {
// New creates a new MemoryFS. // New creates a new MemoryFS.
func New(capacity int64) *MemoryFS { func New(capacity int64) *MemoryFS {
if capacity <= 0 {
panic("memory capacity must be greater than 0") // panic if the capacity is less than or equal to 0
}
return &MemoryFS{ return &MemoryFS{
files: make(map[string]*file), files: make(map[string]*file),
capacity: capacity, capacity: capacity,
@@ -69,9 +73,8 @@ func (m *MemoryFS) Set(key string, src []byte) error {
int64(len(src)), int64(len(src)),
time.Now(), time.Now(),
), ),
data: make([]byte, len(src)), data: src,
} }
copy(m.files[key].data, src)
return nil return nil
} }

View File

@@ -3,6 +3,9 @@ package vfserror
import "errors" import "errors"
var ( var (
// ErrInvalidKey is returned when a key is invalid.
ErrInvalidKey = errors.New("vfs: invalid key")
// ErrUnreachable is returned when a code path is unreachable. // ErrUnreachable is returned when a code path is unreachable.
ErrUnreachable = errors.New("unreachable") ErrUnreachable = errors.New("unreachable")