40 Commits

Author SHA1 Message Date
s1d3sw1ped 6fc83063da chore: remove outdated copyright notice from main.go
Release / Build versioned release (push) Successful in 11s
2025-01-22 17:34:56 -06:00
s1d3sw1ped cd4b5e0598 chore: remove outdated copyright notice from root.go 2025-01-22 17:34:41 -06:00
s1d3sw1ped e583780b9c feat: import version package to access global version variable 2025-01-22 17:34:21 -06:00
s1d3sw1ped c0ed8a590e feat: include application version in logging output 2025-01-22 17:32:45 -06:00
s1d3sw1ped 09d3450738 feat: add version command to display application version 2025-01-22 17:32:30 -06:00
s1d3sw1ped f01b15c8c6 docs: clarify license coverage in README.md 2025-01-22 17:32:04 -06:00
s1d3sw1ped 27ac30d28a chore: add 'dist/' to .gitignore to exclude distribution files 2025-01-22 17:07:06 -06:00
s1d3sw1ped 0424de1fca chore: format launch.json arguments for improved readability 2025-01-22 17:06:35 -06:00
s1d3sw1ped 32c0d85bad chore: remove unused extra files from goreleaser configuration 2025-01-22 10:03:14 -06:00
s1d3sw1ped 6b51e61a87 chore: remove unused goreleaser checksum script 2025-01-22 10:02:19 -06:00
s1d3sw1ped 1094505f20 fix: increase hits buffer size in AvgCacheState initialization
Release / Build versioned release (push) Successful in 8s
2025-01-22 00:02:11 -06:00
s1d3sw1ped 93d04410bf feat: integrate zerolog for structured logging and replace fmt with logger
Release / Build versioned release (push) Successful in 20s
2025-01-21 23:33:14 -06:00
s1d3sw1ped 62f7a38295 refactor: replace log statements with fmt for console output
Release / Build versioned release (push) Successful in 16s
2025-01-21 20:56:58 -06:00
s1d3sw1ped c370e80888 fix: initialize the avgcachestatus
Release / Build versioned release (push) Successful in 14s
2025-01-21 20:20:48 -06:00
s1d3sw1ped 9d0f4894e1 refactor: comment out logging for request duration in ServeHTTP method
Release / Build versioned release (push) Successful in 16s
2025-01-21 20:03:11 -06:00
s1d3sw1ped 745ee37184 feat: add AvgCacheState to track average cache hits and misses 2025-01-21 20:02:34 -06:00
s1d3sw1ped 5a9b07f609 refactor: streamline release and test workflows by removing unnecessary steps 2025-01-21 15:49:50 -06:00
s1d3sw1ped d88db3d9c1 refactor: remove old CI workflows and consolidate into a new test workflow
Release / Build versioned release (push) Successful in 14s
2025-01-21 15:37:31 -06:00
s1d3sw1ped 91075232cd refactor: remove unnecessary go generate step from goreleaser configuration
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 57s
2025-01-21 15:26:30 -06:00
s1d3sw1ped 1f588fa20a fix: handle error from http.ListenAndServe in SteamCache
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 59s
2025-01-21 15:24:08 -06:00
s1d3sw1ped d39a2ad4fa refactor: remove address flag and set default value in command configuration 2025-01-21 15:24:01 -06:00
s1d3sw1ped 80cc75f372 docs: update README with garbage collection details and Windows hosts file override instructions 2025-01-21 15:23:24 -06:00
s1d3sw1ped 53f5d1577c refactor: simplify tag release workflow by removing unnecessary steps
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 17s
CI / Test (push) Successful in 59s
2025-01-21 13:38:23 -06:00
s1d3sw1ped 1186e14c56 fix: update Gitea URLs in goreleaser configuration forehead
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 1m1s
Release versioned tag / Build versioned release (push) Successful in 1m11s
2025-01-21 13:37:12 -06:00
s1d3sw1ped 3bb57b7741 refactor: update goreleaser configuration for improved build and release process
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 1m0s
Release versioned tag / Build versioned release (push) Failing after 1m7s
2025-01-21 13:34:51 -06:00
s1d3sw1ped 8f2d1eff9b refactor: remove commented build steps and add checksum script for binaries
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 14s
CI / Test (push) Successful in 59s
Release versioned tag / Build versioned release (push) Failing after 1m5s
2025-01-21 13:18:21 -06:00
s1d3sw1ped 27242fdf05 test
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 14s
CI / Test (push) Successful in 1m0s
Release versioned tag / Build versioned release (push) Failing after 1m25s
2025-01-21 13:07:42 -06:00
s1d3sw1ped 55330ebd3c get the entire repo depth for changelogs
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 58s
Release versioned tag / Build versioned release (push) Failing after 1m6s
2025-01-21 13:03:23 -06:00
s1d3sw1ped e37dc930dd test goreleaser
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 13s
CI / Test (push) Successful in 59s
Release versioned tag / Build versioned release (push) Failing after 1m3s
2025-01-21 12:51:17 -06:00
s1d3sw1ped 79f0814745 refactor CI workflows to streamline linting and release processes
CI / Lint (push) Successful in 6s
CI / Build (push) Successful in 14s
CI / Test (push) Successful in 57s
Release versioned tag / Build versioned release (push) Failing after 1m5s
2025-01-21 12:36:31 -06:00
s1d3sw1ped 1feee2758c fixed bad character
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 14s
CI / Test (push) Successful in 59s
Release versioned tag / Lint (push) Failing after 6s
Release versioned tag / Build versioned release (push) Failing after 20s
Release versioned tag / Test (push) Successful in 58s
2025-01-21 12:32:20 -06:00
s1d3sw1ped 22f9196960 enable parallel execution for cache tests
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 15s
CI / Test (push) Successful in 58s
2025-01-21 12:27:49 -06:00
s1d3sw1ped 9d0a09d8be minor fixes to gc_test to lower the log spam
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 16s
CI / Test (push) Successful in 58s
2025-01-21 12:25:44 -06:00
s1d3sw1ped f6f007bf47 d for disable
CI / Lint (push) Successful in 7s
CI / Build (push) Successful in 15s
CI / Test (push) Successful in 59s
2025-01-21 12:22:20 -06:00
s1d3sw1ped 232b290a44 disable errcheck
CI / Lint (push) Failing after 7s
CI / Build (push) Successful in 20s
CI / Test (push) Successful in 59s
2025-01-21 12:21:05 -06:00
s1d3sw1ped b55b95f5d8 fixed a invisible character in yaml
CI / Lint (push) Failing after 27s
CI / Build (push) Successful in 27s
CI / Test (push) Successful in 1m11s
2025-01-21 12:07:31 -06:00
s1d3sw1ped ca6c9a5861 added CI
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
2025-01-21 12:06:25 -06:00
s1d3sw1ped 16dce1f0c2 it works 2025-01-21 11:52:04 -06:00
s1d3sw1ped 2be7b117ea lottsa stuff 2025-01-19 20:17:57 -06:00
s1d3sw1ped 88ecb1bc24 initial commit 2025-01-18 20:03:40 -06:00
40 changed files with 1347 additions and 5886 deletions
+5 -4
View File
@@ -1,4 +1,4 @@
name: Release Tag name: Release
on: on:
push: push:
tags: tags:
@@ -6,16 +6,17 @@ on:
jobs: jobs:
release: release:
name: Build versioned release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@main - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: git fetch --force --tags - run: git fetch --force --tags
- uses: actions/setup-go@main - uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
- uses: goreleaser/goreleaser-action@master - uses: goreleaser/goreleaser-action@v6
with: with:
distribution: goreleaser distribution: goreleaser
version: 'latest' version: 'latest'
+7 -3
View File
@@ -1,4 +1,4 @@
name: PR Check name: CI
on: on:
- pull_request - pull_request
@@ -6,10 +6,14 @@ jobs:
check-and-test: check-and-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@main - uses: actions/checkout@v4
- uses: actions/setup-go@main - uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
- run: go mod tidy - run: go mod tidy
- uses: golangci/golangci-lint-action@v3
with:
args: -D errcheck
version: latest
- run: go build ./... - run: go build ./...
- run: go test -race -v -shuffle=on ./... - run: go test -race -v -shuffle=on ./...
+3 -15
View File
@@ -1,15 +1,3 @@
#build artifacts dist/
/dist/ tmp/
__*.exe
#disk cache
/disk/
#config file
/config.yaml
#windows executables
*.exe
#test cache
/steamcache/test_cache/*
!/steamcache/test_cache/.gitkeep
+16 -23
View File
@@ -2,17 +2,11 @@ version: 2
before: before:
hooks: hooks:
- go mod tidy -v - go mod tidy
builds: builds:
- id: default - ldflags:
binary: steamcache2
ldflags:
- -s
- -w
- -extldflags "-static"
- -X s1d3sw1ped/SteamCache2/version.Version={{.Version}} - -X s1d3sw1ped/SteamCache2/version.Version={{.Version}}
- -X s1d3sw1ped/SteamCache2/version.Date={{.Date}}
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
goos: goos:
@@ -20,24 +14,19 @@ builds:
- windows - windows
goarch: goarch:
- amd64 - amd64
- arm64
ignore:
- goos: windows
goarch: arm64
checksum:
name_template: "checksums.txt"
archives: archives:
- id: default - format: tar.gz
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" name_template: >-
formats: tar.gz {{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides: format_overrides:
- goos: windows - goos: windows
formats: zip format: zip
files:
- README.md
- LICENSE
changelog: changelog:
sort: asc sort: asc
@@ -47,7 +36,11 @@ changelog:
- "^test:" - "^test:"
release: release:
name_template: "{{ .ProjectName }}-{{ .Version }}" footer: >-
---
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
gitea_urls: gitea_urls:
api: https://git.s1d3sw1ped.com/api/v1 api: https://git.s1d3sw1ped.com/api/v1
+23
View File
@@ -0,0 +1,23 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"args": [
"--memory",
"1G",
"--disk",
"10G",
"--disk-path",
"tmp/disk",
],
}
]
}
-21
View File
@@ -1,21 +0,0 @@
run: build-snapshot-single ## Run the application
@dist/default_windows_amd64_v1/steamcache2.exe
run-debug: build-snapshot-single ## Run the application with debug logging
@dist/default_windows_amd64_v1/steamcache2.exe --log-level debug
test: deps ## Run all tests
@go test -v ./...
deps: ## Download dependencies
@go mod tidy
build-snapshot-single: deps test ## Build a snapshot of the application for the current platform
@goreleaser build --single-target --snapshot --clean
help: ## Show this help message
@echo SteamCache2 Makefile
@echo Available targets:
@echo run Run the application
@echo run-debug Run the application with debug logging
@echo test Run all tests
@echo deps Download dependencies
+6 -216
View File
@@ -10,154 +10,15 @@ SteamCache2 is a blazing fast download cache for Steam, designed to reduce bandw
- Reduces bandwidth usage - Reduces bandwidth usage
- Easy to set up and configure aside from dns stuff to trick Steam into using it - Easy to set up and configure aside from dns stuff to trick Steam into using it
- Supports multiple clients - Supports multiple clients
- **NEW:** YAML configuration system with automatic config generation
- **NEW:** Simple Makefile for development workflow
- Cross-platform builds (Linux, macOS, Windows)
## Quick Start ## Usage
### First Time Setup 1. Start the cache server:
```sh
1. **Clone and build:** ./SteamCache2 --memory 1G --disk 10G --disk-path tmp/disk
```bash
git clone <repository-url>
cd SteamCache2
make # This will run tests and build the application
``` ```
2. Configure your DNS:
2. **Run the application** (it will create a default config): - If your on Windows and don't want a whole network implementation (THIS)[#windows-hosts-file-override]
```bash
./steamcache2
# or on Windows:
steamcache2.exe
```
The application will automatically create a `config.yaml` file with default settings and exit, allowing you to customize it.
3. **Edit the configuration** (`config.yaml`):
```yaml
listen_address: :80
cache:
memory:
size: 1GB
gc_algorithm: lru
disk:
size: 10GB
path: ./disk
gc_algorithm: hybrid
upstream: "https://steam.cdn.com" # Set your upstream server
```
4. **Run the application again:**
```bash
make run # or ./steamcache2
```
### Development Workflow
```bash
# Run all tests and start the application (default target)
make
# Run only tests
make test
# Run with debug logging
make run-debug
# Download dependencies
make deps
# Show available commands
make help
```
### Command Line Flags
While most configuration is done via the YAML file, some runtime options are still available as command-line flags:
```bash
# Use a custom config file
./steamcache2 --config /path/to/my-config.yaml
# Set logging level
./steamcache2 --log-level debug --log-format json
# Set number of worker threads
./steamcache2 --threads 8
# Show help
./steamcache2 --help
```
### Configuration
SteamCache2 uses a YAML configuration file (`config.yaml`) for all settings. Here's a complete configuration example:
```yaml
# Server configuration
listen_address: :80
# Cache configuration
cache:
# Memory cache settings
memory:
# Size of memory cache (e.g., "512MB", "1GB", "0" to disable)
size: 1GB
# Garbage collection algorithm
gc_algorithm: lru
# Disk cache settings
disk:
# Size of disk cache (e.g., "10GB", "50GB", "0" to disable)
size: 10GB
# Path to disk cache directory
path: ./disk
# Garbage collection algorithm
gc_algorithm: hybrid
# Upstream server configuration
# The upstream server to proxy requests to
upstream: "https://steam.cdn.com"
```
#### Garbage Collection Algorithms
SteamCache2 supports different garbage collection algorithms for memory and disk caches, allowing you to optimize performance for each storage tier:
**Available GC Algorithms:**
- **`lru`** (default): Least Recently Used - evicts oldest accessed files
- **`lfu`**: Least Frequently Used - evicts least accessed files (good for popular content)
- **`fifo`**: First In, First Out - evicts oldest created files (predictable)
- **`largest`**: Size-based - evicts largest files first (maximizes file count)
- **`smallest`**: Size-based - evicts smallest files first (maximizes cache hit rate)
- **`hybrid`**: Combines access time and file size for optimal eviction
**Recommended Algorithms by Cache Type:**
**For Memory Cache (Fast, Limited Size):**
- **`lru`** - Best overall performance, good balance of speed and hit rate
- **`lfu`** - Excellent for gaming cafes where popular games stay cached
- **`hybrid`** - Optimal for mixed workloads with varying file sizes
**For Disk Cache (Slow, Large Size):**
- **`hybrid`** - Recommended for optimal performance, balances speed and storage efficiency
- **`largest`** - Good for maximizing number of cached files
- **`lru`** - Reliable default with good performance
**Use Cases:**
- **Gaming Cafes**: Use `lfu` for memory, `hybrid` for disk
- **LAN Events**: Use `lfu` for memory, `hybrid` for disk
- **Home Use**: Use `lru` for memory, `hybrid` for disk
- **Testing**: Use `fifo` for predictable behavior
- **Large File Storage**: Use `largest` for disk to maximize file count
### DNS Configuration
Configure your DNS to direct Steam traffic to your SteamCache2 server:
- If you're on Windows and don't want a whole network implementation, see the [Windows Hosts File Override](#windows-hosts-file-override) section below.
### Windows Hosts File Override ### Windows Hosts File Override
@@ -192,77 +53,6 @@ Configure your DNS to direct Steam traffic to your SteamCache2 server:
This will direct any requests to `lancache.steamcontent.com` to your SteamCache2 server. This will direct any requests to `lancache.steamcontent.com` to your SteamCache2 server.
## Building from Source
### Prerequisites
- Go 1.19 or later
- Make (optional, but recommended)
### Build Commands
```bash
# Clone the repository
git clone <repository-url>
cd SteamCache2
# Download dependencies
make deps
# Run tests
make test
# Build for current platform
go build -o steamcache2 .
# Build for specific platforms
GOOS=linux GOARCH=amd64 go build -o steamcache2-linux-amd64 .
GOOS=windows GOARCH=amd64 go build -o steamcache2-windows-amd64.exe .
```
### Development
```bash
# Run in development mode with debug logging
make run-debug
# Run all tests and start the application
make
```
## Troubleshooting
### Common Issues
1. **"Config file not found" on first run**
- This is expected! SteamCache2 will automatically create a default `config.yaml` file
- Edit the generated config file with your desired settings
- Run the application again
2. **Permission denied when creating config**
- Make sure you have write permissions in the current directory
- Try running with elevated privileges if necessary
3. **Port already in use**
- Change the `listen_address` in `config.yaml` to a different port (e.g., `:8080`)
- Or stop the service using the current port
4. **High memory usage**
- Reduce the memory cache size in `config.yaml`
- Consider using disk-only caching by setting `memory.size: "0"`
5. **Slow disk performance**
- Use SSD storage for the disk cache
- Consider using a different GC algorithm like `hybrid`
- Adjust the disk cache size to match available storage
### Getting Help
- Check the logs for detailed error messages
- Run with `--log-level debug` for more verbose output
- Ensure your upstream server is accessible
- Verify DNS configuration is working correctly
## License ## License
See the [LICENSE](LICENSE) file for details. See the [LICENSE](LICENSE) file for details.
+16 -112
View File
@@ -1,27 +1,18 @@
// cmd/root.go
package cmd package cmd
import ( import (
"fmt"
"os" "os"
"s1d3sw1ped/SteamCache2/config"
"s1d3sw1ped/SteamCache2/steamcache" "s1d3sw1ped/SteamCache2/steamcache"
"s1d3sw1ped/SteamCache2/steamcache/logger"
"s1d3sw1ped/SteamCache2/version"
"strings"
"github.com/rs/zerolog"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( var (
configPath string memory string
memorymultiplier int
logLevel string disk string
logFormat string diskmultiplier int
diskpath string
maxConcurrentRequests int64
maxRequestsPerClient int64
) )
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -33,100 +24,15 @@ 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) {
// Configure logging
switch logLevel {
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
default:
zerolog.SetGlobalLevel(zerolog.InfoLevel) // Default to info level if not specified
}
var writer zerolog.ConsoleWriter
if logFormat == "json" {
writer = zerolog.ConsoleWriter{Out: os.Stderr, NoColor: true}
} else {
writer = zerolog.ConsoleWriter{Out: os.Stderr}
}
logger.Logger = zerolog.New(writer).With().Timestamp().Logger()
logger.Logger.Info().
Msg("SteamCache2 " + version.Version + " " + version.Date + " starting...")
// Load configuration
cfg, err := config.LoadConfig(configPath)
if err != nil {
// Check if the error is because the config file doesn't exist
// The error is wrapped, so we check the error message
if strings.Contains(err.Error(), "no such file") ||
strings.Contains(err.Error(), "cannot find the file") ||
strings.Contains(err.Error(), "The system cannot find the file") {
logger.Logger.Info().
Str("config_path", configPath).
Msg("Config file not found, creating default configuration")
if err := config.SaveDefaultConfig(configPath); err != nil {
logger.Logger.Error().
Err(err).
Str("config_path", configPath).
Msg("Failed to create default configuration")
fmt.Fprintf(os.Stderr, "Error: Failed to create default config at %s: %v\n", configPath, err)
os.Exit(1)
}
logger.Logger.Info().
Str("config_path", configPath).
Msg("Default configuration created successfully. Please edit the file and run again.")
fmt.Printf("Default configuration created at %s\n", configPath)
fmt.Println("Please edit the configuration file as needed and run the application again.")
os.Exit(0)
} else {
logger.Logger.Error().
Err(err).
Str("config_path", configPath).
Msg("Failed to load configuration")
fmt.Fprintf(os.Stderr, "Error: Failed to load configuration from %s: %v\n", configPath, err)
os.Exit(1)
}
}
logger.Logger.Info().
Str("config_path", configPath).
Msg("Configuration loaded successfully")
// Use command-line flags if provided, otherwise use config values
finalMaxConcurrentRequests := cfg.MaxConcurrentRequests
if maxConcurrentRequests > 0 {
finalMaxConcurrentRequests = maxConcurrentRequests
}
finalMaxRequestsPerClient := cfg.MaxRequestsPerClient
if maxRequestsPerClient > 0 {
finalMaxRequestsPerClient = maxRequestsPerClient
}
sc := steamcache.New( sc := steamcache.New(
cfg.ListenAddress, ":80",
cfg.Cache.Memory.Size, memory,
cfg.Cache.Disk.Size, memorymultiplier,
cfg.Cache.Disk.Path, disk,
cfg.Upstream, diskmultiplier,
cfg.Cache.Memory.GCAlgorithm, diskpath,
cfg.Cache.Disk.GCAlgorithm,
finalMaxConcurrentRequests,
finalMaxRequestsPerClient,
) )
logger.Logger.Info().
Msg("SteamCache2 " + version.Version + " started on " + cfg.ListenAddress)
sc.Run() sc.Run()
logger.Logger.Info().Msg("SteamCache2 stopped")
os.Exit(0)
}, },
} }
@@ -140,11 +46,9 @@ func Execute() {
} }
func init() { func init() {
rootCmd.Flags().StringVarP(&configPath, "config", "c", "config.yaml", "Path to configuration file") rootCmd.Flags().StringVarP(&memory, "memory", "m", "100MB", "The size of the memory cache")
rootCmd.Flags().IntVarP(&memorymultiplier, "memory-multiplier", "M", 10, "The multiplier for the memory cache")
rootCmd.Flags().StringVarP(&logLevel, "log-level", "l", "info", "Logging level: debug, info, error") rootCmd.Flags().StringVarP(&disk, "disk", "d", "10GB", "The size of the disk cache")
rootCmd.Flags().StringVarP(&logFormat, "log-format", "f", "console", "Logging format: json, console") rootCmd.Flags().IntVarP(&diskmultiplier, "disk-multiplier", "D", 10, "The multiplier for the disk cache")
rootCmd.Flags().StringVarP(&diskpath, "disk-path", "p", "tmp/steamcache2-disk", "The path to the disk cache")
rootCmd.Flags().Int64Var(&maxConcurrentRequests, "max-concurrent-requests", 0, "Maximum concurrent requests (0 = use config file value)")
rootCmd.Flags().Int64Var(&maxRequestsPerClient, "max-requests-per-client", 0, "Maximum concurrent requests per client IP (0 = use config file value)")
} }
+1 -2
View File
@@ -1,4 +1,3 @@
// cmd/version.go
package cmd package cmd
import ( import (
@@ -15,7 +14,7 @@ var versionCmd = &cobra.Command{
Short: "prints the version of SteamCache2", Short: "prints the version of SteamCache2",
Long: `Prints the version of SteamCache2. This command is useful for checking the version of the application.`, Long: `Prints the version of SteamCache2. This command is useful for checking the version of the application.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, "SteamCache2", version.Version, version.Date) fmt.Fprintln(os.Stderr, "SteamCache2", version.Version)
}, },
} }
-128
View File
@@ -1,128 +0,0 @@
package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
// Server configuration
ListenAddress string `yaml:"listen_address" default:":80"`
// Concurrency limits
MaxConcurrentRequests int64 `yaml:"max_concurrent_requests" default:"200"`
MaxRequestsPerClient int64 `yaml:"max_requests_per_client" default:"5"`
// Cache configuration
Cache CacheConfig `yaml:"cache"`
// Upstream configuration
Upstream string `yaml:"upstream"`
}
type CacheConfig struct {
// Memory cache settings
Memory MemoryConfig `yaml:"memory"`
// Disk cache settings
Disk DiskConfig `yaml:"disk"`
}
type MemoryConfig struct {
// Size of memory cache (e.g., "512MB", "1GB")
Size string `yaml:"size" default:"0"`
// Garbage collection algorithm: lru, lfu, fifo, largest, smallest, hybrid
GCAlgorithm string `yaml:"gc_algorithm" default:"lru"`
}
type DiskConfig struct {
// Size of disk cache (e.g., "10GB", "50GB")
Size string `yaml:"size" default:"0"`
// Path to disk cache directory
Path string `yaml:"path" default:""`
// Garbage collection algorithm: lru, lfu, fifo, largest, smallest, hybrid
GCAlgorithm string `yaml:"gc_algorithm" default:"lru"`
}
// LoadConfig loads configuration from a YAML file
func LoadConfig(configPath string) (*Config, error) {
if configPath == "" {
configPath = "config.yaml"
}
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err)
}
// Set defaults for empty values
if config.ListenAddress == "" {
config.ListenAddress = ":80"
}
if config.MaxConcurrentRequests == 0 {
config.MaxConcurrentRequests = 50
}
if config.MaxRequestsPerClient == 0 {
config.MaxRequestsPerClient = 3
}
if config.Cache.Memory.Size == "" {
config.Cache.Memory.Size = "0"
}
if config.Cache.Memory.GCAlgorithm == "" {
config.Cache.Memory.GCAlgorithm = "lru"
}
if config.Cache.Disk.Size == "" {
config.Cache.Disk.Size = "0"
}
if config.Cache.Disk.GCAlgorithm == "" {
config.Cache.Disk.GCAlgorithm = "lru"
}
return &config, nil
}
// SaveDefaultConfig creates a default configuration file
func SaveDefaultConfig(configPath string) error {
if configPath == "" {
configPath = "config.yaml"
}
defaultConfig := Config{
ListenAddress: ":80",
MaxConcurrentRequests: 50, // Reduced for home user (less concurrent load)
MaxRequestsPerClient: 3, // Reduced for home user (more conservative per client)
Cache: CacheConfig{
Memory: MemoryConfig{
Size: "1GB", // Recommended for systems that can spare 1GB RAM for caching
GCAlgorithm: "lru",
},
Disk: DiskConfig{
Size: "1TB", // Large HDD cache for home user
Path: "./disk",
GCAlgorithm: "lru", // Better for gaming patterns (keeps recently played games)
},
},
Upstream: "",
}
data, err := yaml.Marshal(&defaultConfig)
if err != nil {
return fmt.Errorf("failed to marshal default config: %w", err)
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("failed to write default config file: %w", err)
}
return nil
}
+1 -3
View File
@@ -4,11 +4,9 @@ go 1.23.0
require ( require (
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/edsrzf/mmap-go v1.1.0
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
golang.org/x/sync v0.16.0 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
+2 -6
View File
@@ -2,8 +2,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@@ -21,13 +19,11 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-1
View File
@@ -1,4 +1,3 @@
// main.go
package main package main
import ( import (
+60
View File
@@ -0,0 +1,60 @@
package avgcachestate
import (
"s1d3sw1ped/SteamCache2/vfs/cachestate"
"sync"
)
// AvgCacheState is a cache state that averages the last N cache states.
type AvgCacheState struct {
size int
avgs []cachestate.CacheState
mu sync.Mutex
}
// New creates a new average cache state with the given size.
func New(size int) *AvgCacheState {
return &AvgCacheState{
size: size,
avgs: make([]cachestate.CacheState, size),
mu: sync.Mutex{},
}
}
// Clear resets the average cache state to zero.
func (a *AvgCacheState) Clear() {
a.mu.Lock()
defer a.mu.Unlock()
a.avgs = make([]cachestate.CacheState, a.size) // zeroed
}
// Add adds a cache state to the average cache state.
func (a *AvgCacheState) Add(cs cachestate.CacheState) {
a.mu.Lock()
defer a.mu.Unlock()
a.avgs = append(a.avgs, cs)
if len(a.avgs) > a.size {
a.avgs = a.avgs[1:]
}
}
// Avg returns the average cache state.
func (a *AvgCacheState) Avg() float64 {
a.mu.Lock()
defer a.mu.Unlock()
var hits, misses int
for _, cs := range a.avgs {
switch cs {
case cachestate.CacheStateHit:
hits++
case cachestate.CacheStateMiss:
misses++
}
}
total := hits + misses
return float64(hits) / float64(total)
}
+75
View File
@@ -0,0 +1,75 @@
package steamcache
import (
"s1d3sw1ped/SteamCache2/steamcache/logger"
"s1d3sw1ped/SteamCache2/vfs"
"s1d3sw1ped/SteamCache2/vfs/cachestate"
"time"
"github.com/docker/go-units"
"golang.org/x/exp/rand"
)
func randomgc(vfss vfs.VFS, stats []*vfs.FileInfo) int64 {
// Pick a random file to delete
randfile := stats[rand.Intn(len(stats))]
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) {
tstart := time.Now()
deletions := 0
targetreclaim := int64(size)
var reclaimed int64
stats := vfss.StatAll()
for {
reclaimed += randomgc(vfss, stats)
deletions++
if reclaimed >= targetreclaim {
break
}
}
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++
if reclaimed >= targetreclaim {
break
}
}
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 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
}
-279
View File
@@ -1,279 +0,0 @@
package steamcache
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
)
const SteamHostname = "cache2-den-iwst.steamcontent.com"
func TestSteamIntegration(t *testing.T) {
// Skip this test if we don't have internet access or want to avoid hitting Steam servers
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Test URLs from real Steam usage - these should be cached when requested by Steam clients
testURLs := []string{
"/depot/516751/patch/288061881745926019/4378193572994177373",
"/depot/516751/chunk/42e7c13eb4b4e426ec5cf6d1010abfd528e5065a",
"/depot/516751/chunk/f949f71e102d77ed6e364e2054d06429d54bebb1",
"/depot/516751/chunk/6790f5105833556d37797657be72c1c8dd2e7074",
}
for _, testURL := range testURLs {
t.Run(fmt.Sprintf("URL_%s", testURL), func(t *testing.T) {
testSteamURL(t, testURL)
})
}
}
func testSteamURL(t *testing.T, urlPath string) {
// Create a unique temporary directory for this test to avoid cache persistence issues
tempDir, err := os.MkdirTemp("", "steamcache_test_*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir) // Clean up after test
// Create SteamCache instance with unique temp directory
sc := New(":0", "100MB", "1GB", tempDir, "", "LRU", "LRU", 10, 5)
// Use real Steam server
steamURL := "https://" + SteamHostname + urlPath
// Test direct download from Steam server
directResp, directBody := downloadDirectly(t, steamURL)
// Test download through SteamCache
cacheResp, cacheBody := downloadThroughCache(t, sc, urlPath)
// Compare responses
compareResponses(t, directResp, directBody, cacheResp, cacheBody, urlPath)
}
func downloadDirectly(t *testing.T, url string) (*http.Response, []byte) {
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
// Add Steam user agent
req.Header.Set("User-Agent", "Valve/Steam HTTP Client 1.0")
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to download directly from Steam: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read direct response body: %v", err)
}
return resp, body
}
func downloadThroughCache(t *testing.T, sc *SteamCache, urlPath string) (*http.Response, []byte) {
// Create a test server for SteamCache
cacheServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// For real Steam URLs, we need to set the upstream to the Steam hostname
// and let SteamCache handle the full URL construction
sc.upstream = "https://" + SteamHostname
sc.ServeHTTP(w, r)
}))
defer cacheServer.Close()
// First request - should be a MISS and cache the file
client := &http.Client{Timeout: 30 * time.Second}
req1, err := http.NewRequest("GET", cacheServer.URL+urlPath, nil)
if err != nil {
t.Fatalf("Failed to create first request: %v", err)
}
req1.Header.Set("User-Agent", "Valve/Steam HTTP Client 1.0")
resp1, err := client.Do(req1)
if err != nil {
t.Fatalf("Failed to download through cache (first request): %v", err)
}
defer resp1.Body.Close()
body1, err := io.ReadAll(resp1.Body)
if err != nil {
t.Fatalf("Failed to read cache response body (first request): %v", err)
}
// Verify first request was a MISS
if resp1.Header.Get("X-LanCache-Status") != "MISS" {
t.Errorf("Expected first request to be MISS, got %s", resp1.Header.Get("X-LanCache-Status"))
}
// Second request - should be a HIT from cache
req2, err := http.NewRequest("GET", cacheServer.URL+urlPath, nil)
if err != nil {
t.Fatalf("Failed to create second request: %v", err)
}
req2.Header.Set("User-Agent", "Valve/Steam HTTP Client 1.0")
resp2, err := client.Do(req2)
if err != nil {
t.Fatalf("Failed to download through cache (second request): %v", err)
}
defer resp2.Body.Close()
body2, err := io.ReadAll(resp2.Body)
if err != nil {
t.Fatalf("Failed to read cache response body (second request): %v", err)
}
// Verify second request was a HIT (unless hash verification failed)
status2 := resp2.Header.Get("X-LanCache-Status")
if status2 != "HIT" && status2 != "MISS" {
t.Errorf("Expected second request to be HIT or MISS, got %s", status2)
}
// If it's a MISS, it means hash verification failed and content wasn't cached
// This is correct behavior - we shouldn't cache content that doesn't match the expected hash
if status2 == "MISS" {
t.Logf("Second request was MISS (hash verification failed) - this is correct behavior")
}
// Verify both cache responses are identical
if !bytes.Equal(body1, body2) {
t.Error("First and second cache responses should be identical")
}
// Return the second response (from cache)
return resp2, body2
}
func compareResponses(t *testing.T, directResp *http.Response, directBody []byte, cacheResp *http.Response, cacheBody []byte, urlPath string) {
// Compare status codes
if directResp.StatusCode != cacheResp.StatusCode {
t.Errorf("Status code mismatch: direct=%d, cache=%d", directResp.StatusCode, cacheResp.StatusCode)
}
// Compare response bodies (this is the most important test)
if !bytes.Equal(directBody, cacheBody) {
t.Errorf("Response body mismatch for URL %s", urlPath)
t.Errorf("Direct body length: %d, Cache body length: %d", len(directBody), len(cacheBody))
// Find first difference
minLen := len(directBody)
if len(cacheBody) < minLen {
minLen = len(cacheBody)
}
for i := 0; i < minLen; i++ {
if directBody[i] != cacheBody[i] {
t.Errorf("First difference at byte %d: direct=0x%02x, cache=0x%02x", i, directBody[i], cacheBody[i])
break
}
}
}
// Compare important headers (excluding cache-specific ones)
importantHeaders := []string{
"Content-Type",
"Content-Length",
"X-Sha1",
"Cache-Control",
}
for _, header := range importantHeaders {
directValue := directResp.Header.Get(header)
cacheValue := cacheResp.Header.Get(header)
if directValue != cacheValue {
t.Errorf("Header %s mismatch: direct=%s, cache=%s", header, directValue, cacheValue)
}
}
// Verify cache-specific headers are present
if cacheResp.Header.Get("X-LanCache-Status") == "" {
t.Error("Cache response should have X-LanCache-Status header")
}
if cacheResp.Header.Get("X-LanCache-Processed-By") != "SteamCache2" {
t.Error("Cache response should have X-LanCache-Processed-By header set to SteamCache2")
}
t.Logf("✅ URL %s: Direct and cache responses are identical", urlPath)
}
// TestCacheFileFormat tests the cache file format directly
func TestCacheFileFormat(t *testing.T) {
// Create test data
bodyData := []byte("test steam content")
contentHash := calculateSHA256(bodyData)
// Create mock response
resp := &http.Response{
StatusCode: 200,
Status: "200 OK",
Header: make(http.Header),
Body: http.NoBody,
}
resp.Header.Set("Content-Type", "application/x-steam-chunk")
resp.Header.Set("Content-Length", "18")
resp.Header.Set("X-Sha1", contentHash)
// Create SteamCache instance
sc := &SteamCache{}
// Reconstruct raw response
rawResponse := sc.reconstructRawResponse(resp, bodyData)
// Serialize to cache format
cacheData, err := serializeRawResponse(rawResponse)
if err != nil {
t.Fatalf("Failed to serialize cache file: %v", err)
}
// Deserialize from cache format
cacheFile, err := deserializeCacheFile(cacheData)
if err != nil {
t.Fatalf("Failed to deserialize cache file: %v", err)
}
// Verify cache file structure
if cacheFile.ContentHash != contentHash {
t.Errorf("ContentHash mismatch: expected %s, got %s", contentHash, cacheFile.ContentHash)
}
if cacheFile.ResponseSize != int64(len(rawResponse)) {
t.Errorf("ResponseSize mismatch: expected %d, got %d", len(rawResponse), cacheFile.ResponseSize)
}
// Verify raw response is preserved
if !bytes.Equal(cacheFile.Response, rawResponse) {
t.Error("Raw response not preserved in cache file")
}
// Test streaming the cached response
recorder := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/test/format", nil)
sc.streamCachedResponse(recorder, req, cacheFile, "test-key", "127.0.0.1", time.Now())
// Verify streamed response
if recorder.Code != 200 {
t.Errorf("Expected status code 200, got %d", recorder.Code)
}
if !bytes.Equal(recorder.Body.Bytes(), bodyData) {
t.Error("Streamed response body does not match original")
}
t.Log("✅ Cache file format test passed")
}
+3 -2
View File
@@ -1,8 +1,9 @@
// steamcache/logger/logger.go
package logger package logger
import ( import (
"os"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
var Logger zerolog.Logger var Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger()
+122 -1420
View File
File diff suppressed because it is too large Load Diff
-356
View File
@@ -1,356 +0,0 @@
// steamcache/steamcache_test.go
package steamcache
import (
"io"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCaching(t *testing.T) {
td := t.TempDir()
os.WriteFile(filepath.Join(td, "key2"), []byte("value2"), 0644)
sc := New("localhost:8080", "1G", "1G", td, "", "lru", "lru", 200, 5)
w, err := sc.vfs.Create("key", 5)
if err != nil {
t.Errorf("Create failed: %v", err)
}
w.Write([]byte("value"))
w.Close()
w, err = sc.vfs.Create("key1", 6)
if err != nil {
t.Errorf("Create failed: %v", err)
}
w.Write([]byte("value1"))
w.Close()
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)
}
rc, err := sc.vfs.Open("key")
if err != nil {
t.Errorf("Open failed: %v", err)
}
d, _ := io.ReadAll(rc)
rc.Close()
if string(d) != "value" {
t.Errorf("Get failed: got %s, want %s", d, "value")
}
rc, err = sc.vfs.Open("key1")
if err != nil {
t.Errorf("Open failed: %v", err)
}
d, _ = io.ReadAll(rc)
rc.Close()
if string(d) != "value1" {
t.Errorf("Get failed: got %s, want %s", d, "value1")
}
rc, err = sc.vfs.Open("key2")
if err != nil {
t.Errorf("Open failed: %v", err)
}
d, _ = io.ReadAll(rc)
rc.Close()
if string(d) != "value2" {
t.Errorf("Get failed: got %s, want %s", d, "value2")
}
// With size-based promotion filtering, not all files may be promoted
// The total size should be at least the disk size (17 bytes) but may be less than 34 bytes
// if some files are filtered out due to size constraints
if sc.diskgc.Size() != 17 {
t.Errorf("Disk size failed: got %d, want %d", sc.diskgc.Size(), 17)
}
if sc.vfs.Size() < 17 {
t.Errorf("Total size too small: got %d, want at least 17", sc.vfs.Size())
}
if sc.vfs.Size() > 34 {
t.Errorf("Total size too large: got %d, want at most 34", sc.vfs.Size())
}
sc.memory.Delete("key2")
sc.disk.Delete("key2") // Also delete from disk cache
os.Remove(filepath.Join(td, "key2"))
if _, err := sc.vfs.Open("key2"); err == nil {
t.Errorf("Open failed: got nil, want error")
}
}
func TestCacheMissAndHit(t *testing.T) {
sc := New("localhost:8080", "0", "1G", t.TempDir(), "", "lru", "lru", 200, 5)
key := "testkey"
value := []byte("testvalue")
// Simulate miss: but since no upstream, skip full ServeHTTP, test VFS
w, err := sc.vfs.Create(key, int64(len(value)))
if err != nil {
t.Fatal(err)
}
w.Write(value)
w.Close()
rc, err := sc.vfs.Open(key)
if err != nil {
t.Fatal(err)
}
got, _ := io.ReadAll(rc)
rc.Close()
if string(got) != string(value) {
t.Errorf("expected %s, got %s", value, got)
}
}
func TestURLHashing(t *testing.T) {
// Test the SHA256-based cache key generation for Steam client requests
// The "steam/" prefix indicates the request came from a Steam client (User-Agent based)
testCases := []struct {
input string
desc string
shouldCache bool
}{
{
input: "/depot/1684171/chunk/abcdef1234567890",
desc: "chunk file URL",
shouldCache: true,
},
{
input: "/depot/1684171/manifest/944076726177422892/5/abcdef1234567890",
desc: "manifest file URL",
shouldCache: true,
},
{
input: "/appinfo/123456",
desc: "app info URL",
shouldCache: true,
},
{
input: "/some/other/path",
desc: "any URL from Steam client",
shouldCache: true, // All URLs from Steam clients (detected via User-Agent) are cached
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
result := generateServiceCacheKey(tc.input, "steam")
if tc.shouldCache {
// Should return a cache key with "steam/" prefix
if !strings.HasPrefix(result, "steam/") {
t.Errorf("generateServiceCacheKey(%s, \"steam\") = %s, expected steam/ prefix", tc.input, result)
}
// Should be exactly 70 characters (6 for "steam/" + 64 for SHA256 hex)
if len(result) != 70 {
t.Errorf("generateServiceCacheKey(%s, \"steam\") length = %d, expected 70", tc.input, len(result))
}
} else {
// Should return empty string for non-Steam URLs
if result != "" {
t.Errorf("generateServiceCacheKey(%s, \"steam\") = %s, expected empty string", tc.input, result)
}
}
})
}
}
func TestServiceDetection(t *testing.T) {
// Create a service manager for testing
sm := NewServiceManager()
testCases := []struct {
userAgent string
expectedName string
expectedFound bool
desc string
}{
{
userAgent: "Valve/Steam HTTP Client 1.0",
expectedName: "steam",
expectedFound: true,
desc: "Valve Steam HTTP Client",
},
{
userAgent: "Steam",
expectedName: "steam",
expectedFound: true,
desc: "Simple Steam user agent",
},
{
userAgent: "SteamClient/1.0",
expectedName: "steam",
expectedFound: true,
desc: "SteamClient with version",
},
{
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
expectedName: "",
expectedFound: false,
desc: "Browser user agent",
},
{
userAgent: "",
expectedName: "",
expectedFound: false,
desc: "Empty user agent",
},
{
userAgent: "curl/7.68.0",
expectedName: "",
expectedFound: false,
desc: "curl user agent",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
service, found := sm.DetectService(tc.userAgent)
if found != tc.expectedFound {
t.Errorf("DetectService(%s) found = %v, expected %v", tc.userAgent, found, tc.expectedFound)
}
if found && service.Name != tc.expectedName {
t.Errorf("DetectService(%s) service name = %s, expected %s", tc.userAgent, service.Name, tc.expectedName)
}
})
}
}
func TestServiceManagerExpandability(t *testing.T) {
// Create a service manager for testing
sm := NewServiceManager()
// Test adding a new service (Epic Games)
epicConfig := &ServiceConfig{
Name: "epic",
Prefix: "epic",
UserAgents: []string{
`EpicGamesLauncher`,
`EpicGames`,
`Epic.*Launcher`,
},
}
err := sm.AddService(epicConfig)
if err != nil {
t.Fatalf("Failed to add Epic service: %v", err)
}
// Test Epic Games detection
epicTestCases := []struct {
userAgent string
expectedName string
expectedFound bool
desc string
}{
{
userAgent: "EpicGamesLauncher/1.0",
expectedName: "epic",
expectedFound: true,
desc: "Epic Games Launcher",
},
{
userAgent: "EpicGames/2.0",
expectedName: "epic",
expectedFound: true,
desc: "Epic Games client",
},
{
userAgent: "Epic Launcher 1.5",
expectedName: "epic",
expectedFound: true,
desc: "Epic Launcher with regex match",
},
{
userAgent: "Steam",
expectedName: "steam",
expectedFound: true,
desc: "Steam should still work",
},
{
userAgent: "Mozilla/5.0",
expectedName: "",
expectedFound: false,
desc: "Browser should not match any service",
},
}
for _, tc := range epicTestCases {
t.Run(tc.desc, func(t *testing.T) {
service, found := sm.DetectService(tc.userAgent)
if found != tc.expectedFound {
t.Errorf("DetectService(%s) found = %v, expected %v", tc.userAgent, found, tc.expectedFound)
}
if found && service.Name != tc.expectedName {
t.Errorf("DetectService(%s) service name = %s, expected %s", tc.userAgent, service.Name, tc.expectedName)
}
})
}
// Test cache key generation for different services
steamKey := generateServiceCacheKey("/depot/123/chunk/abc", "steam")
epicKey := generateServiceCacheKey("/epic/123/chunk/abc", "epic")
if !strings.HasPrefix(steamKey, "steam/") {
t.Errorf("Steam cache key should start with 'steam/', got: %s", steamKey)
}
if !strings.HasPrefix(epicKey, "epic/") {
t.Errorf("Epic cache key should start with 'epic/', got: %s", epicKey)
}
}
// Removed hash calculation tests since we switched to lightweight validation
func TestSteamKeySharding(t *testing.T) {
sc := New("localhost:8080", "0", "1G", t.TempDir(), "", "lru", "lru", 200, 5)
// Test with a Steam-style key that should trigger sharding
steamKey := "steam/0016cfc5019b8baa6026aa1cce93e685d6e06c6e"
testData := []byte("test steam cache data")
// Create a file with the steam key
w, err := sc.vfs.Create(steamKey, int64(len(testData)))
if err != nil {
t.Fatalf("Failed to create file with steam key: %v", err)
}
w.Write(testData)
w.Close()
// Verify we can read it back
rc, err := sc.vfs.Open(steamKey)
if err != nil {
t.Fatalf("Failed to open file with steam key: %v", err)
}
got, _ := io.ReadAll(rc)
rc.Close()
if string(got) != string(testData) {
t.Errorf("Data mismatch: expected %s, got %s", testData, got)
}
// Verify that the file was created (sharding is working if no error occurred)
// The key difference is that with sharding, the file should be created successfully
// and be readable, whereas without sharding it might not work correctly
}
// Removed old TestKeyGeneration - replaced with TestURLHashing that uses SHA256
View File
-13
View File
@@ -1,16 +1,3 @@
// version/version.go
package version package version
import "time"
var Version string var Version string
var Date string
func init() {
if Version == "" {
Version = "0.0.0-dev"
}
if Date == "" {
Date = time.Now().Format("2006-01-02 15:04:05")
}
}
-273
View File
@@ -1,273 +0,0 @@
package adaptive
import (
"context"
"sync"
"sync/atomic"
"time"
)
// WorkloadPattern represents different types of workload patterns
type WorkloadPattern int
const (
PatternUnknown WorkloadPattern = iota
PatternSequential // Sequential file access (e.g., game installation)
PatternRandom // Random file access (e.g., game updates)
PatternBurst // Burst access (e.g., multiple users downloading same game)
PatternSteady // Steady access (e.g., popular games being accessed regularly)
)
// CacheStrategy represents different caching strategies
type CacheStrategy int
const (
StrategyLRU CacheStrategy = iota
StrategyLFU
StrategySizeBased
StrategyHybrid
StrategyPredictive
)
// WorkloadAnalyzer analyzes access patterns to determine optimal caching strategies
type WorkloadAnalyzer struct {
accessHistory map[string]*AccessInfo
patternCounts map[WorkloadPattern]int64
mu sync.RWMutex
analysisInterval time.Duration
ctx context.Context
cancel context.CancelFunc
}
// AccessInfo tracks access patterns for individual files
type AccessInfo struct {
Key string
AccessCount int64
LastAccess time.Time
FirstAccess time.Time
AccessTimes []time.Time
Size int64
AccessPattern WorkloadPattern
mu sync.RWMutex
}
// AdaptiveCacheManager manages adaptive caching strategies
type AdaptiveCacheManager struct {
analyzer *WorkloadAnalyzer
currentStrategy CacheStrategy
adaptationCount int64
mu sync.RWMutex
}
// NewWorkloadAnalyzer creates a new workload analyzer
func NewWorkloadAnalyzer(analysisInterval time.Duration) *WorkloadAnalyzer {
ctx, cancel := context.WithCancel(context.Background())
analyzer := &WorkloadAnalyzer{
accessHistory: make(map[string]*AccessInfo),
patternCounts: make(map[WorkloadPattern]int64),
analysisInterval: analysisInterval,
ctx: ctx,
cancel: cancel,
}
// Start background analysis with much longer interval to reduce overhead
go analyzer.analyzePatterns()
return analyzer
}
// RecordAccess records a file access for pattern analysis (lightweight version)
func (wa *WorkloadAnalyzer) RecordAccess(key string, size int64) {
// Use read lock first for better performance
wa.mu.RLock()
info, exists := wa.accessHistory[key]
wa.mu.RUnlock()
if !exists {
// Only acquire write lock when creating new entry
wa.mu.Lock()
// Double-check after acquiring write lock
if _, exists = wa.accessHistory[key]; !exists {
info = &AccessInfo{
Key: key,
AccessCount: 1,
LastAccess: time.Now(),
FirstAccess: time.Now(),
AccessTimes: []time.Time{time.Now()},
Size: size,
}
wa.accessHistory[key] = info
}
wa.mu.Unlock()
} else {
// Lightweight update - just increment counter and update timestamp
info.mu.Lock()
info.AccessCount++
info.LastAccess = time.Now()
// Only keep last 10 access times to reduce memory overhead
if len(info.AccessTimes) > 10 {
info.AccessTimes = info.AccessTimes[len(info.AccessTimes)-10:]
} else {
info.AccessTimes = append(info.AccessTimes, time.Now())
}
info.mu.Unlock()
}
}
// analyzePatterns analyzes access patterns in the background
func (wa *WorkloadAnalyzer) analyzePatterns() {
ticker := time.NewTicker(wa.analysisInterval)
defer ticker.Stop()
for {
select {
case <-wa.ctx.Done():
return
case <-ticker.C:
wa.performAnalysis()
}
}
}
// performAnalysis analyzes current access patterns
func (wa *WorkloadAnalyzer) performAnalysis() {
wa.mu.Lock()
defer wa.mu.Unlock()
// Reset pattern counts
wa.patternCounts = make(map[WorkloadPattern]int64)
now := time.Now()
cutoff := now.Add(-wa.analysisInterval * 2) // Analyze last 2 intervals
for _, info := range wa.accessHistory {
info.mu.RLock()
if info.LastAccess.After(cutoff) {
pattern := wa.determinePattern(info)
info.AccessPattern = pattern
wa.patternCounts[pattern]++
}
info.mu.RUnlock()
}
}
// determinePattern determines the access pattern for a file
func (wa *WorkloadAnalyzer) determinePattern(info *AccessInfo) WorkloadPattern {
if len(info.AccessTimes) < 3 {
return PatternUnknown
}
// Analyze access timing patterns
intervals := make([]time.Duration, len(info.AccessTimes)-1)
for i := 1; i < len(info.AccessTimes); i++ {
intervals[i-1] = info.AccessTimes[i].Sub(info.AccessTimes[i-1])
}
// Calculate variance in access intervals
var sum, sumSquares time.Duration
for _, interval := range intervals {
sum += interval
sumSquares += interval * interval
}
avg := sum / time.Duration(len(intervals))
variance := (sumSquares / time.Duration(len(intervals))) - (avg * avg)
// Determine pattern based on variance and access count
if info.AccessCount > 10 && variance < time.Minute {
return PatternBurst
} else if info.AccessCount > 5 && variance < time.Hour {
return PatternSteady
} else if variance < time.Minute*5 {
return PatternSequential
} else {
return PatternRandom
}
}
// GetDominantPattern returns the most common access pattern
func (wa *WorkloadAnalyzer) GetDominantPattern() WorkloadPattern {
wa.mu.RLock()
defer wa.mu.RUnlock()
var maxCount int64
var dominantPattern WorkloadPattern
for pattern, count := range wa.patternCounts {
if count > maxCount {
maxCount = count
dominantPattern = pattern
}
}
return dominantPattern
}
// GetAccessInfo returns access information for a key
func (wa *WorkloadAnalyzer) GetAccessInfo(key string) *AccessInfo {
wa.mu.RLock()
defer wa.mu.RUnlock()
return wa.accessHistory[key]
}
// Stop stops the workload analyzer
func (wa *WorkloadAnalyzer) Stop() {
wa.cancel()
}
// NewAdaptiveCacheManager creates a new adaptive cache manager
func NewAdaptiveCacheManager(analysisInterval time.Duration) *AdaptiveCacheManager {
return &AdaptiveCacheManager{
analyzer: NewWorkloadAnalyzer(analysisInterval),
currentStrategy: StrategyLRU, // Start with LRU
}
}
// AdaptStrategy adapts the caching strategy based on workload patterns
func (acm *AdaptiveCacheManager) AdaptStrategy() CacheStrategy {
acm.mu.Lock()
defer acm.mu.Unlock()
dominantPattern := acm.analyzer.GetDominantPattern()
// Adapt strategy based on dominant pattern
switch dominantPattern {
case PatternBurst:
acm.currentStrategy = StrategyLFU // LFU is good for burst patterns
case PatternSteady:
acm.currentStrategy = StrategyHybrid // Hybrid for steady patterns
case PatternSequential:
acm.currentStrategy = StrategySizeBased // Size-based for sequential
case PatternRandom:
acm.currentStrategy = StrategyLRU // LRU for random patterns
default:
acm.currentStrategy = StrategyLRU // Default to LRU
}
atomic.AddInt64(&acm.adaptationCount, 1)
return acm.currentStrategy
}
// GetCurrentStrategy returns the current caching strategy
func (acm *AdaptiveCacheManager) GetCurrentStrategy() CacheStrategy {
acm.mu.RLock()
defer acm.mu.RUnlock()
return acm.currentStrategy
}
// RecordAccess records a file access for analysis
func (acm *AdaptiveCacheManager) RecordAccess(key string, size int64) {
acm.analyzer.RecordAccess(key, size)
}
// GetAdaptationCount returns the number of strategy adaptations
func (acm *AdaptiveCacheManager) GetAdaptationCount() int64 {
return atomic.LoadInt64(&acm.adaptationCount)
}
// Stop stops the adaptive cache manager
func (acm *AdaptiveCacheManager) Stop() {
acm.analyzer.Stop()
}
+118 -403
View File
@@ -1,436 +1,151 @@
// vfs/cache/cache.go
package cache package cache
import ( import (
"io" "fmt"
"s1d3sw1ped/SteamCache2/vfs" "s1d3sw1ped/SteamCache2/vfs"
"s1d3sw1ped/SteamCache2/vfs/cachestate"
"s1d3sw1ped/SteamCache2/vfs/vfserror" "s1d3sw1ped/SteamCache2/vfs/vfserror"
"sync"
"sync/atomic"
) )
// TieredCache implements a two-tier cache with fast (memory) and slow (disk) storage // Ensure CacheFS implements VFS.
type TieredCache struct { var _ vfs.VFS = (*CacheFS)(nil)
fast vfs.VFS // Memory cache (fast)
slow vfs.VFS // Disk cache (slow)
mu sync.RWMutex // CacheFS is a virtual file system that caches files in memory and on disk.
type CacheFS struct {
fast vfs.VFS
slow vfs.VFS
cacheHandler CacheHandler
} }
// LockFreeTieredCache implements a lock-free two-tier cache for better concurrency type CacheHandler func(*vfs.FileInfo, cachestate.CacheState) bool
type LockFreeTieredCache struct {
fast *atomic.Value // Memory cache (fast) - atomic.Value for lock-free access
slow *atomic.Value // Disk cache (slow) - atomic.Value for lock-free access
}
// New creates a new tiered cache // 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() *TieredCache { func New(fast, slow vfs.VFS, cacheHandler CacheHandler) *CacheFS {
return &TieredCache{} if slow == nil {
} panic("slow is nil")
// SetFast sets the fast (memory) tier
func (tc *TieredCache) SetFast(vfs vfs.VFS) {
tc.mu.Lock()
defer tc.mu.Unlock()
tc.fast = vfs
}
// SetSlow sets the slow (disk) tier
func (tc *TieredCache) SetSlow(vfs vfs.VFS) {
tc.mu.Lock()
defer tc.mu.Unlock()
tc.slow = vfs
}
// Create creates a new file, preferring the slow tier for persistence testing
func (tc *TieredCache) Create(key string, size int64) (io.WriteCloser, error) {
tc.mu.RLock()
defer tc.mu.RUnlock()
// Try slow tier first (disk) for better testability
if tc.slow != nil {
return tc.slow.Create(key, size)
} }
// Fall back to fast tier (memory) if fast == slow {
if tc.fast != nil { panic("fast and slow are the same")
return tc.fast.Create(key, size)
} }
return nil, vfserror.ErrNotFound return &CacheFS{
fast: fast,
slow: slow,
cacheHandler: cacheHandler,
}
} }
// Open opens a file, checking fast tier first, then slow tier with promotion // cacheState returns the state of the file at key.
func (tc *TieredCache) Open(key string) (io.ReadCloser, error) { func (c *CacheFS) cacheState(key string) cachestate.CacheState {
tc.mu.RLock() if c.fast != nil {
defer tc.mu.RUnlock() if _, err := c.fast.Stat(key); err == nil {
return cachestate.CacheStateHit
// Try fast tier first (memory)
if tc.fast != nil {
if reader, err := tc.fast.Open(key); err == nil {
return reader, nil
} }
} }
// Fall back to slow tier (disk) and promote to fast tier if _, err := c.slow.Stat(key); err == nil {
if tc.slow != nil { return cachestate.CacheStateMiss
reader, err := tc.slow.Open(key) }
return cachestate.CacheStateNotFound
}
func (c *CacheFS) Name() string {
return fmt.Sprintf("CacheFS(%s, %s)", c.fast.Name(), c.slow.Name())
}
// Size returns the total size of the cache.
func (c *CacheFS) Size() int64 {
return c.slow.Size()
}
// Set sets the file at key to src. If the file is already in the cache, it is replaced.
func (c *CacheFS) Set(key string, src []byte) error {
state := c.cacheState(key)
switch state {
case cachestate.CacheStateHit:
if c.fast != nil {
c.fast.Delete(key)
}
return c.slow.Set(key, src)
case cachestate.CacheStateMiss, cachestate.CacheStateNotFound:
return c.slow.Set(key, src)
}
panic(vfserror.ErrUnreachable)
}
// Delete deletes the file at key from the cache.
func (c *CacheFS) Delete(key string) error {
if c.fast != nil {
c.fast.Delete(key)
}
return c.slow.Delete(key)
}
// Get returns the file at key. If the file is not in the cache, it is fetched from the storage.
func (c *CacheFS) Get(key string) ([]byte, error) {
src, _, err := c.GetS(key)
return src, err
}
// GetS returns the file at key. If the file is not in the cache, it is fetched from the storage. It also returns the cache state.
func (c *CacheFS) GetS(key string) ([]byte, cachestate.CacheState, error) {
state := c.cacheState(key)
switch state {
case cachestate.CacheStateHit:
// if c.fast == nil then cacheState cannot be CacheStateHit so we can safely ignore the check
src, err := c.fast.Get(key)
return src, state, err
case cachestate.CacheStateMiss:
src, err := c.slow.Get(key)
if err != nil { if err != nil {
return nil, err return nil, state, err
} }
// If we have both tiers, check if we should promote the file to fast tier sstat, _ := c.slow.Stat(key)
if tc.fast != nil { if sstat != nil && c.fast != nil { // file found in slow storage and fast storage is available
// Check file size before promoting - don't promote if larger than available memory cache space // We are accessing the file from the slow storage, and the file has been accessed less then a minute ago so it popular, so we should update the fast storage with the latest file.
if info, err := tc.slow.Stat(key); err == nil { if c.cacheHandler != nil && c.cacheHandler(sstat, state) {
availableSpace := tc.fast.Capacity() - tc.fast.Size() if err := c.fast.Set(key, src); err != nil {
// Only promote if file fits in available space (with 10% buffer for safety) return nil, state, err
if info.Size <= int64(float64(availableSpace)*0.9) {
// Create a new reader for promotion to avoid interfering with the returned reader
promotionReader, err := tc.slow.Open(key)
if err == nil {
go tc.promoteToFast(key, promotionReader)
}
} }
} }
} }
return reader, nil return src, state, nil
case cachestate.CacheStateNotFound:
return nil, state, vfserror.ErrNotFound
} }
panic(vfserror.ErrUnreachable)
}
// Stat returns information about the file at key.
// Warning: This will return information about the file in the fastest storage its in.
func (c *CacheFS) Stat(key string) (*vfs.FileInfo, error) {
state := c.cacheState(key)
switch state {
case cachestate.CacheStateHit:
// if c.fast == nil then cacheState cannot be CacheStateHit so we can safely ignore the check
return c.fast.Stat(key)
case cachestate.CacheStateMiss:
return c.slow.Stat(key)
case cachestate.CacheStateNotFound:
return nil, vfserror.ErrNotFound return nil, vfserror.ErrNotFound
}
panic(vfserror.ErrUnreachable)
} }
// Delete removes a file from all tiers // StatAll returns information about all files in the cache.
func (tc *TieredCache) Delete(key string) error { // Warning: This only returns information about the files in the slow storage.
tc.mu.RLock() func (c *CacheFS) StatAll() []*vfs.FileInfo {
defer tc.mu.RUnlock() return c.slow.StatAll()
var lastErr error
// Delete from fast tier
if tc.fast != nil {
if err := tc.fast.Delete(key); err != nil {
lastErr = err
}
}
// Delete from slow tier
if tc.slow != nil {
if err := tc.slow.Delete(key); err != nil {
lastErr = err
}
}
return lastErr
}
// Stat returns file information, checking fast tier first
func (tc *TieredCache) Stat(key string) (*vfs.FileInfo, error) {
tc.mu.RLock()
defer tc.mu.RUnlock()
// Try fast tier first (memory)
if tc.fast != nil {
if info, err := tc.fast.Stat(key); err == nil {
return info, nil
}
}
// Fall back to slow tier (disk)
if tc.slow != nil {
return tc.slow.Stat(key)
}
return nil, vfserror.ErrNotFound
}
// Name returns the cache name
func (tc *TieredCache) Name() string {
return "TieredCache"
}
// Size returns the total size across all tiers
func (tc *TieredCache) Size() int64 {
tc.mu.RLock()
defer tc.mu.RUnlock()
var total int64
if tc.fast != nil {
total += tc.fast.Size()
}
if tc.slow != nil {
total += tc.slow.Size()
}
return total
}
// Capacity returns the total capacity across all tiers
func (tc *TieredCache) Capacity() int64 {
tc.mu.RLock()
defer tc.mu.RUnlock()
var total int64
if tc.fast != nil {
total += tc.fast.Capacity()
}
if tc.slow != nil {
total += tc.slow.Capacity()
}
return total
}
// promoteToFast promotes a file from slow tier to fast tier
func (tc *TieredCache) promoteToFast(key string, reader io.ReadCloser) {
defer reader.Close()
// Get file info from slow tier to determine size
tc.mu.RLock()
var size int64
if tc.slow != nil {
if info, err := tc.slow.Stat(key); err == nil {
size = info.Size
} else {
tc.mu.RUnlock()
return // Skip promotion if we can't get file info
}
}
tc.mu.RUnlock()
// Check if file fits in available memory cache space
tc.mu.RLock()
if tc.fast != nil {
availableSpace := tc.fast.Capacity() - tc.fast.Size()
// Only promote if file fits in available space (with 10% buffer for safety)
if size > int64(float64(availableSpace)*0.9) {
tc.mu.RUnlock()
return // Skip promotion if file is too large
}
}
tc.mu.RUnlock()
// Read the entire file content
content, err := io.ReadAll(reader)
if err != nil {
return // Skip promotion if read fails
}
// Create the file in fast tier
tc.mu.RLock()
if tc.fast != nil {
writer, err := tc.fast.Create(key, size)
if err == nil {
// Write content to fast tier
writer.Write(content)
writer.Close()
}
}
tc.mu.RUnlock()
}
// NewLockFree creates a new lock-free tiered cache
func NewLockFree() *LockFreeTieredCache {
return &LockFreeTieredCache{
fast: &atomic.Value{},
slow: &atomic.Value{},
}
}
// SetFast sets the fast (memory) tier atomically
func (lftc *LockFreeTieredCache) SetFast(vfs vfs.VFS) {
lftc.fast.Store(vfs)
}
// SetSlow sets the slow (disk) tier atomically
func (lftc *LockFreeTieredCache) SetSlow(vfs vfs.VFS) {
lftc.slow.Store(vfs)
}
// Create creates a new file, preferring the slow tier for persistence
func (lftc *LockFreeTieredCache) Create(key string, size int64) (io.WriteCloser, error) {
// Try slow tier first (disk) for better testability
if slow := lftc.slow.Load(); slow != nil {
if vfs, ok := slow.(vfs.VFS); ok {
return vfs.Create(key, size)
}
}
// Fall back to fast tier (memory)
if fast := lftc.fast.Load(); fast != nil {
if vfs, ok := fast.(vfs.VFS); ok {
return vfs.Create(key, size)
}
}
return nil, vfserror.ErrNotFound
}
// Open opens a file, checking fast tier first, then slow tier with promotion
func (lftc *LockFreeTieredCache) Open(key string) (io.ReadCloser, error) {
// Try fast tier first (memory)
if fast := lftc.fast.Load(); fast != nil {
if vfs, ok := fast.(vfs.VFS); ok {
if reader, err := vfs.Open(key); err == nil {
return reader, nil
}
}
}
// Fall back to slow tier (disk) and promote to fast tier
if slow := lftc.slow.Load(); slow != nil {
if vfs, ok := slow.(vfs.VFS); ok {
reader, err := vfs.Open(key)
if err != nil {
return nil, err
}
// If we have both tiers, promote the file to fast tier
if fast := lftc.fast.Load(); fast != nil {
// Create a new reader for promotion to avoid interfering with the returned reader
promotionReader, err := vfs.Open(key)
if err == nil {
go lftc.promoteToFast(key, promotionReader)
}
}
return reader, nil
}
}
return nil, vfserror.ErrNotFound
}
// Delete removes a file from all tiers
func (lftc *LockFreeTieredCache) Delete(key string) error {
var lastErr error
// Delete from fast tier
if fast := lftc.fast.Load(); fast != nil {
if vfs, ok := fast.(vfs.VFS); ok {
if err := vfs.Delete(key); err != nil {
lastErr = err
}
}
}
// Delete from slow tier
if slow := lftc.slow.Load(); slow != nil {
if vfs, ok := slow.(vfs.VFS); ok {
if err := vfs.Delete(key); err != nil {
lastErr = err
}
}
}
return lastErr
}
// Stat returns file information, checking fast tier first
func (lftc *LockFreeTieredCache) Stat(key string) (*vfs.FileInfo, error) {
// Try fast tier first (memory)
if fast := lftc.fast.Load(); fast != nil {
if vfs, ok := fast.(vfs.VFS); ok {
if info, err := vfs.Stat(key); err == nil {
return info, nil
}
}
}
// Fall back to slow tier (disk)
if slow := lftc.slow.Load(); slow != nil {
if vfs, ok := slow.(vfs.VFS); ok {
return vfs.Stat(key)
}
}
return nil, vfserror.ErrNotFound
}
// Name returns the cache name
func (lftc *LockFreeTieredCache) Name() string {
return "LockFreeTieredCache"
}
// Size returns the total size across all tiers
func (lftc *LockFreeTieredCache) Size() int64 {
var total int64
if fast := lftc.fast.Load(); fast != nil {
if vfs, ok := fast.(vfs.VFS); ok {
total += vfs.Size()
}
}
if slow := lftc.slow.Load(); slow != nil {
if vfs, ok := slow.(vfs.VFS); ok {
total += vfs.Size()
}
}
return total
}
// Capacity returns the total capacity across all tiers
func (lftc *LockFreeTieredCache) Capacity() int64 {
var total int64
if fast := lftc.fast.Load(); fast != nil {
if vfs, ok := fast.(vfs.VFS); ok {
total += vfs.Capacity()
}
}
if slow := lftc.slow.Load(); slow != nil {
if vfs, ok := slow.(vfs.VFS); ok {
total += vfs.Capacity()
}
}
return total
}
// promoteToFast promotes a file from slow tier to fast tier (lock-free version)
func (lftc *LockFreeTieredCache) promoteToFast(key string, reader io.ReadCloser) {
defer reader.Close()
// Get file info from slow tier to determine size
var size int64
if slow := lftc.slow.Load(); slow != nil {
if vfs, ok := slow.(vfs.VFS); ok {
if info, err := vfs.Stat(key); err == nil {
size = info.Size
} else {
return // Skip promotion if we can't get file info
}
}
}
// Check if file fits in available memory cache space
if fast := lftc.fast.Load(); fast != nil {
if vfs, ok := fast.(vfs.VFS); ok {
availableSpace := vfs.Capacity() - vfs.Size()
// Only promote if file fits in available space (with 10% buffer for safety)
if size > int64(float64(availableSpace)*0.9) {
return // Skip promotion if file is too large
}
}
}
// Read the entire file content
content, err := io.ReadAll(reader)
if err != nil {
return // Skip promotion if read fails
}
// Create the file in fast tier
if fast := lftc.fast.Load(); fast != nil {
if vfs, ok := fast.(vfs.VFS); ok {
writer, err := vfs.Create(key, size)
if err == nil {
// Write content to fast tier
writer.Write(content)
writer.Close()
}
}
}
} }
+205
View File
@@ -0,0 +1,205 @@
package cache
import (
"errors"
"testing"
"s1d3sw1ped/SteamCache2/vfs"
"s1d3sw1ped/SteamCache2/vfs/cachestate"
"s1d3sw1ped/SteamCache2/vfs/memory"
"s1d3sw1ped/SteamCache2/vfs/vfserror"
)
func testMemory() vfs.VFS {
return memory.New(1024)
}
func TestNew(t *testing.T) {
t.Parallel()
fast := testMemory()
slow := testMemory()
cache := New(fast, slow, nil)
if cache == nil {
t.Fatal("expected cache to be non-nil")
}
}
func TestNewPanics(t *testing.T) {
t.Parallel()
defer func() {
if r := recover(); r == nil {
t.Fatal("expected panic but did not get one")
}
}()
New(nil, nil, nil)
}
func TestSetAndGet(t *testing.T) {
t.Parallel()
fast := testMemory()
slow := testMemory()
cache := New(fast, slow, nil)
key := "test"
value := []byte("value")
if err := cache.Set(key, value); err != nil {
t.Fatalf("unexpected error: %v", err)
}
got, err := cache.Get(key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(got) != string(value) {
t.Fatalf("expected %s, got %s", value, got)
}
}
func TestSetAndGetNoFast(t *testing.T) {
t.Parallel()
slow := testMemory()
cache := New(nil, slow, nil)
key := "test"
value := []byte("value")
if err := cache.Set(key, value); err != nil {
t.Fatalf("unexpected error: %v", err)
}
got, err := cache.Get(key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(got) != string(value) {
t.Fatalf("expected %s, got %s", value, got)
}
}
func TestCaching(t *testing.T) {
t.Parallel()
fast := testMemory()
slow := testMemory()
cache := New(fast, slow, func(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
return true
})
key := "test"
value := []byte("value")
if err := fast.Set(key, value); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := slow.Set(key, value); err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, state, err := cache.GetS(key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if state != cachestate.CacheStateHit {
t.Fatalf("expected %v, got %v", cachestate.CacheStateHit, state)
}
err = fast.Delete(key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got, state, err := cache.GetS(key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if state != cachestate.CacheStateMiss {
t.Fatalf("expected %v, got %v", cachestate.CacheStateMiss, state)
}
if string(got) != string(value) {
t.Fatalf("expected %s, got %s", value, got)
}
err = cache.Delete(key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, state, err = cache.GetS(key)
if !errors.Is(err, vfserror.ErrNotFound) {
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
}
if state != cachestate.CacheStateNotFound {
t.Fatalf("expected %v, got %v", cachestate.CacheStateNotFound, state)
}
}
func TestGetNotFound(t *testing.T) {
t.Parallel()
fast := testMemory()
slow := testMemory()
cache := New(fast, slow, nil)
_, err := cache.Get("nonexistent")
if !errors.Is(err, vfserror.ErrNotFound) {
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
}
}
func TestDelete(t *testing.T) {
t.Parallel()
fast := testMemory()
slow := testMemory()
cache := New(fast, slow, nil)
key := "test"
value := []byte("value")
if err := cache.Set(key, value); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := cache.Delete(key); err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err := cache.Get(key)
if !errors.Is(err, vfserror.ErrNotFound) {
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
}
}
func TestStat(t *testing.T) {
t.Parallel()
fast := testMemory()
slow := testMemory()
cache := New(fast, slow, nil)
key := "test"
value := []byte("value")
if err := cache.Set(key, value); err != nil {
t.Fatalf("unexpected error: %v", err)
}
info, err := cache.Stat(key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if info == nil {
t.Fatal("expected file info to be non-nil")
}
}
+22 -3
View File
@@ -1,5 +1,24 @@
// vfs/cachestate/cachestate.go
package cachestate package cachestate
// This is a placeholder for cache state management import "s1d3sw1ped/SteamCache2/vfs/vfserror"
// Currently not used but referenced in imports
type CacheState int
const (
CacheStateHit CacheState = iota
CacheStateMiss
CacheStateNotFound
)
func (c CacheState) String() string {
switch c {
case CacheStateHit:
return "hit"
case CacheStateMiss:
return "miss"
case CacheStateNotFound:
return "not found"
}
panic(vfserror.ErrUnreachable)
}
+117 -650
View File
@@ -1,22 +1,15 @@
// vfs/disk/disk.go
package disk package disk
import ( import (
"container/list"
"fmt"
"io"
"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"
"sort"
"strings"
"sync" "sync"
"time" "time"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/edsrzf/mmap-go"
) )
// Ensure DiskFS implements VFS. // Ensure DiskFS implements VFS.
@@ -28,717 +21,191 @@ type DiskFS struct {
info map[string]*vfs.FileInfo info map[string]*vfs.FileInfo
capacity int64 capacity int64
size int64 mu sync.Mutex
mu sync.RWMutex sg sync.WaitGroup
keyLocks []sync.Map // Sharded lock pools for better concurrency
LRU *lruList
timeUpdater *vfs.BatchedTimeUpdate // Batched time updates for better performance
}
// Number of lock shards for reducing contention
const numLockShards = 32
// lruList for time-decayed LRU eviction
type lruList struct {
list *list.List
elem map[string]*list.Element
}
func newLruList() *lruList {
return &lruList{
list: list.New(),
elem: make(map[string]*list.Element),
}
}
func (l *lruList) Add(key string, fi *vfs.FileInfo) {
elem := l.list.PushFront(fi)
l.elem[key] = elem
}
func (l *lruList) MoveToFront(key string, timeUpdater *vfs.BatchedTimeUpdate) {
if elem, exists := l.elem[key]; exists {
l.list.MoveToFront(elem)
// Update the FileInfo in the element with new access time
if fi := elem.Value.(*vfs.FileInfo); fi != nil {
fi.UpdateAccessBatched(timeUpdater)
}
}
}
func (l *lruList) Remove(key string) *vfs.FileInfo {
if elem, exists := l.elem[key]; exists {
delete(l.elem, key)
if fi := l.list.Remove(elem).(*vfs.FileInfo); fi != nil {
return fi
}
}
return nil
}
func (l *lruList) Len() int {
return l.list.Len()
}
// 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)
}
// extractKeyFromPath reverses the sharding logic to get the original key from a sharded path
func (d *DiskFS) extractKeyFromPath(path string) string {
// Fast path: if no slashes, it's not a sharded path
if !strings.Contains(path, "/") {
return path
}
parts := strings.SplitN(path, "/", 5)
numParts := len(parts)
if numParts >= 4 && parts[0] == "steam" {
lastThree := parts[numParts-3:]
shard1 := lastThree[0]
shard2 := lastThree[1]
filename := lastThree[2]
// Verify sharding is correct
if len(filename) >= 4 && filename[:2] == shard1 && filename[2:4] == shard2 {
return "steam/" + filename
}
}
// Handle single-level sharding for short hashes: steam/shard1/filename
if numParts >= 3 && parts[0] == "steam" {
lastTwo := parts[numParts-2:]
shard1 := lastTwo[0]
filename := lastTwo[1]
if len(filename) >= 2 && filename[:2] == shard1 {
return "steam/" + filename
}
}
// Fallback: return as-is for any unrecognized format
return path
} }
// New creates a new DiskFS. // New creates a new DiskFS.
func New(root string, capacity int64) *DiskFS { func new(root string, capacity int64, skipinit bool) *DiskFS {
if capacity <= 0 { dfs := &DiskFS{
panic("disk capacity must be greater than 0")
}
// Create root directory if it doesn't exist
os.MkdirAll(root, 0755)
// Initialize sharded locks
keyLocks := make([]sync.Map, numLockShards)
d := &DiskFS{
root: root, root: root,
info: make(map[string]*vfs.FileInfo), info: make(map[string]*vfs.FileInfo),
capacity: capacity, capacity: capacity,
size: 0, mu: sync.Mutex{},
keyLocks: keyLocks, sg: sync.WaitGroup{},
LRU: newLruList(),
timeUpdater: vfs.NewBatchedTimeUpdate(100 * time.Millisecond), // Update time every 100ms
} }
d.init() os.MkdirAll(dfs.root, 0755)
return d
if !skipinit {
dfs.init()
}
return dfs
}
func New(root string, capacity int64) *DiskFS {
return new(root, capacity, false)
}
func NewSkipInit(root string, capacity int64) *DiskFS {
return new(root, capacity, true)
} }
// init loads existing files from disk and migrates legacy depot files to sharded structure
func (d *DiskFS) init() { func (d *DiskFS) init() {
// logger.Logger.Info().Str("name", d.Name()).Str("root", d.root).Str("capacity", units.HumanSize(float64(d.capacity))).Msg("init")
tstart := time.Now() tstart := time.Now()
var depotFiles []string // Track depot files that need migration d.walk(d.root)
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")
}
func (d *DiskFS) walk(path string) {
d.sg.Add(1)
go func() {
defer d.sg.Done()
filepath.Walk(path, func(npath string, info os.FileInfo, err error) error {
if path == npath {
return nil
}
err := filepath.Walk(d.root, func(npath string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
if info.IsDir() { if info.IsDir() {
return nil d.walk(npath)
return filepath.SkipDir
} }
d.mu.Lock() d.mu.Lock()
// Extract key from sharded path: remove root and convert sharding back k := npath[len(d.root)+1:]
// Handle both "./disk" and "disk" root paths d.info[k] = vfs.NewFileInfoFromOS(info, k)
rootPath := d.root
if strings.HasPrefix(rootPath, "./") {
rootPath = rootPath[2:] // Remove "./" prefix
}
relPath := strings.ReplaceAll(npath[len(rootPath)+1:], "\\", "/")
// Extract the original key from the sharded path
k := d.extractKeyFromPath(relPath)
fi := vfs.NewFileInfoFromOS(info, k)
d.info[k] = fi
d.LRU.Add(k, fi)
// Initialize access time with file modification time
fi.UpdateAccessBatched(d.timeUpdater)
d.size += info.Size()
// Track depot files for potential migration
if strings.HasPrefix(relPath, "depot/") {
depotFiles = append(depotFiles, relPath)
}
d.mu.Unlock() d.mu.Unlock()
// logger.Logger.Debug().Str("name", d.Name()).Str("root", d.root).Str("capacity", units.HumanSize(float64(d.capacity))).Str("path", npath).Msg("init")
return nil return nil
}) })
if err != nil { }()
logger.Logger.Error().Err(err).Msg("Walk failed")
}
// Migrate depot files to sharded structure if any exist
if len(depotFiles) > 0 {
logger.Logger.Info().Int("count", len(depotFiles)).Msg("Found legacy depot files, starting migration")
d.migrateDepotFiles(depotFiles)
}
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")
} }
// migrateDepotFiles moves legacy depot files to the sharded steam structure
func (d *DiskFS) migrateDepotFiles(depotFiles []string) {
migratedCount := 0
errorCount := 0
for _, relPath := range depotFiles {
// Extract the steam key from the depot path
steamKey := d.extractKeyFromPath(relPath)
if !strings.HasPrefix(steamKey, "steam/") {
// Skip if we can't extract a proper steam key
errorCount++
continue
}
// Get the source and destination paths
sourcePath := filepath.Join(d.root, relPath)
shardedPath := d.shardPath(steamKey)
destPath := filepath.Join(d.root, shardedPath)
// Create destination directory
destDir := filepath.Dir(destPath)
if err := os.MkdirAll(destDir, 0755); err != nil {
logger.Logger.Error().Err(err).Str("path", destDir).Msg("Failed to create migration destination directory")
errorCount++
continue
}
// Move the file
if err := os.Rename(sourcePath, destPath); err != nil {
logger.Logger.Error().Err(err).Str("from", sourcePath).Str("to", destPath).Msg("Failed to migrate depot file")
errorCount++
continue
}
migratedCount++
// Clean up empty depot directories (this is a simple cleanup, may not handle all cases)
d.cleanupEmptyDepotDirs(filepath.Dir(sourcePath))
}
logger.Logger.Info().
Int("migrated", migratedCount).
Int("errors", errorCount).
Msg("Depot file migration completed")
}
// cleanupEmptyDepotDirs removes empty depot directories after migration
func (d *DiskFS) cleanupEmptyDepotDirs(dirPath string) {
for dirPath != d.root && strings.HasPrefix(dirPath, filepath.Join(d.root, "depot")) {
entries, err := os.ReadDir(dirPath)
if err != nil || len(entries) > 0 {
break
}
// Directory is empty, remove it
if err := os.Remove(dirPath); err != nil {
logger.Logger.Error().Err(err).Str("dir", dirPath).Msg("Failed to remove empty depot directory")
break
}
// Move up to parent directory
dirPath = filepath.Dir(dirPath)
}
}
// Name returns the name of this VFS
func (d *DiskFS) Name() string {
return "DiskFS"
}
// Size returns the current size
func (d *DiskFS) Size() int64 {
d.mu.RLock()
defer d.mu.RUnlock()
return d.size
}
// Capacity returns the maximum capacity
func (d *DiskFS) Capacity() int64 { func (d *DiskFS) Capacity() int64 {
return d.capacity return d.capacity
} }
// getShardIndex returns the shard index for a given key func (d *DiskFS) Name() string {
func getShardIndex(key string) int { return "DiskFS"
// Use FNV-1a hash for good distribution
var h uint32 = 2166136261 // FNV offset basis
for i := 0; i < len(key); i++ {
h ^= uint32(key[i])
h *= 16777619 // FNV prime
}
return int(h % numLockShards)
} }
// getKeyLock returns a lock for the given key using sharding func (d *DiskFS) Size() int64 {
func (d *DiskFS) getKeyLock(key string) *sync.RWMutex { var size int64
shardIndex := getShardIndex(key) d.mu.Lock()
shard := &d.keyLocks[shardIndex] defer d.mu.Unlock()
for _, v := range d.info {
keyLock, _ := shard.LoadOrStore(key, &sync.RWMutex{}) size += v.Size()
return keyLock.(*sync.RWMutex) }
return size
} }
// Create creates a new file func (d *DiskFS) Set(key string, src []byte) error {
func (d *DiskFS) Create(key string, size int64) (io.WriteCloser, error) { if d.capacity > 0 {
if key == "" { if size := d.Size() + int64(len(src)); size > d.capacity {
return nil, vfserror.ErrInvalidKey return vfserror.ErrDiskFull
} }
if key[0] == '/' {
return nil, vfserror.ErrInvalidKey
} }
// Sanitize key to prevent path traversal if _, err := d.Stat(key); err == nil {
key = filepath.Clean(key) d.Delete(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() d.mu.Lock()
// Check if file already exists and handle overwrite defer d.mu.Unlock()
if fi, exists := d.info[key]; exists { os.MkdirAll(filepath.Join(d.root, filepath.Dir(key)), 0755)
d.size -= fi.Size if err := os.WriteFile(filepath.Join(d.root, key), src, 0644); err != nil {
d.LRU.Remove(key)
delete(d.info, key)
}
shardedPath := d.shardPath(key)
path := filepath.Join(d.root, shardedPath)
d.mu.Unlock()
path = strings.ReplaceAll(path, "\\", "/")
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, err
}
file, err := os.Create(path)
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)
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()
return err return err
} }
actualSize := stat.Size() fi, err := os.Stat(filepath.Join(d.root, key))
// 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
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
}
keyMu := d.getKeyLock(key)
keyMu.RLock()
defer keyMu.RUnlock()
d.mu.Lock()
fi, exists := d.info[key]
if !exists {
d.mu.Unlock()
return nil, vfserror.ErrNotFound
}
fi.UpdateAccessBatched(d.timeUpdater)
d.LRU.MoveToFront(key, d.timeUpdater)
d.mu.Unlock()
shardedPath := d.shardPath(key)
path := filepath.Join(d.root, shardedPath)
path = strings.ReplaceAll(path, "\\", "/")
file, err := os.Open(path)
if err != nil { if err != nil {
return nil, err panic(err)
} }
// Use memory mapping for large files (>1MB) to improve performance d.info[key] = vfs.NewFileInfoFromOS(fi, key)
const mmapThreshold = 1024 * 1024 // 1MB
if fi.Size > mmapThreshold {
// Close the regular file handle
file.Close()
// Try memory mapping return nil
mmapFile, err := os.Open(path)
if err != nil {
return nil, err
}
mapped, err := mmap.Map(mmapFile, mmap.RDONLY, 0)
if err != nil {
mmapFile.Close()
// Fallback to regular file reading
return os.Open(path)
}
return &mmapReadCloser{
data: mapped,
file: mmapFile,
offset: 0,
}, nil
}
return file, nil
} }
// mmapReadCloser implements io.ReadCloser for memory-mapped files // Delete deletes the value of key.
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()
return m.file.Close()
}
// Delete removes a file
func (d *DiskFS) Delete(key string) error { func (d *DiskFS) Delete(key string) error {
if key == "" { _, err := d.Stat(key)
return vfserror.ErrInvalidKey if err != nil {
return err
} }
if key[0] == '/' {
return vfserror.ErrInvalidKey
}
keyMu := d.getKeyLock(key)
keyMu.Lock()
defer keyMu.Unlock()
d.mu.Lock() d.mu.Lock()
fi, exists := d.info[key] defer d.mu.Unlock()
if !exists {
d.mu.Unlock()
return vfserror.ErrNotFound
}
d.size -= fi.Size
d.LRU.Remove(key)
delete(d.info, key) delete(d.info, key)
d.mu.Unlock() if err := os.Remove(filepath.Join(d.root, key)); err != nil {
shardedPath := d.shardPath(key)
path := filepath.Join(d.root, shardedPath)
path = strings.ReplaceAll(path, "\\", "/")
err := os.Remove(path)
if err != nil {
return err return err
} }
return nil return nil
} }
// Stat returns file information // Get gets the value of key and returns it.
func (d *DiskFS) Get(key string) ([]byte, error) {
_, err := d.Stat(key)
if err != nil {
return nil, err
}
d.mu.Lock()
defer d.mu.Unlock()
data, err := os.ReadFile(filepath.Join(d.root, key))
if err != nil {
return nil, err
}
return data, nil
}
// 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) {
if key == "" { d.mu.Lock()
return nil, vfserror.ErrInvalidKey fi, ok := d.info[key]
} d.mu.Unlock() // unlock before statting the file
if key[0] == '/' {
return nil, vfserror.ErrInvalidKey
}
keyMu := d.getKeyLock(key)
keyMu.RLock()
defer keyMu.RUnlock()
d.mu.RLock()
defer d.mu.RUnlock()
if fi, ok := d.info[key]; ok {
return fi, nil
}
// Check if file exists on disk but wasn't indexed (for migration)
shardedPath := d.shardPath(key)
path := filepath.Join(d.root, shardedPath)
path = strings.ReplaceAll(path, "\\", "/")
if info, err := os.Stat(path); err == nil {
// File exists in sharded location but not indexed, re-index it
fi := vfs.NewFileInfoFromOS(info, key)
// We can't modify the map here because we're in a read lock
// This is a simplified version - in production you'd need to handle this properly
return fi, nil
}
if !ok {
fii, err := os.Stat(filepath.Join(d.root, key))
if err != nil {
return nil, vfserror.ErrNotFound 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
} }
// EvictLRU evicts the least recently used files to free up space func (m *DiskFS) StatAll() []*vfs.FileInfo {
func (d *DiskFS) EvictLRU(bytesNeeded uint) uint { m.mu.Lock()
d.mu.Lock() defer m.mu.Unlock()
defer d.mu.Unlock()
var evicted uint // hard copy the file info to prevent modification of the original file info or the other way around
files := make([]*vfs.FileInfo, 0, len(m.info))
// Evict from LRU list until we free enough space for _, v := range m.info {
for d.size > d.capacity-int64(bytesNeeded) && d.LRU.Len() > 0 { fi := *v
// Get the least recently used item files = append(files, &fi)
elem := d.LRU.list.Back()
if elem == nil {
break
} }
fi := elem.Value.(*vfs.FileInfo) return files
key := fi.Key
// Remove from LRU
d.LRU.Remove(key)
// Remove from map
delete(d.info, key)
// Remove file from disk
shardedPath := d.shardPath(key)
path := filepath.Join(d.root, shardedPath)
path = strings.ReplaceAll(path, "\\", "/")
if err := os.Remove(path); err != nil {
// Log error but continue
continue
}
// Update size
d.size -= fi.Size
evicted += uint(fi.Size)
// Clean up key lock
shardIndex := getShardIndex(key)
d.keyLocks[shardIndex].Delete(key)
}
return evicted
}
// EvictBySize evicts files by size (ascending = smallest first, descending = largest first)
func (d *DiskFS) EvictBySize(bytesNeeded uint, ascending bool) uint {
d.mu.Lock()
defer d.mu.Unlock()
var evicted uint
var candidates []*vfs.FileInfo
// Collect all files
for _, fi := range d.info {
candidates = append(candidates, fi)
}
// Sort by size
sort.Slice(candidates, func(i, j int) bool {
if ascending {
return candidates[i].Size < candidates[j].Size
}
return candidates[i].Size > candidates[j].Size
})
// Evict files until we free enough space
for _, fi := range candidates {
if d.size <= d.capacity-int64(bytesNeeded) {
break
}
key := fi.Key
// Remove from LRU
d.LRU.Remove(key)
// Remove from map
delete(d.info, key)
// Remove file from disk
shardedPath := d.shardPath(key)
path := filepath.Join(d.root, shardedPath)
path = strings.ReplaceAll(path, "\\", "/")
if err := os.Remove(path); err != nil {
continue
}
// Update size
d.size -= fi.Size
evicted += uint(fi.Size)
// Clean up key lock
shardIndex := getShardIndex(key)
d.keyLocks[shardIndex].Delete(key)
}
return evicted
}
// EvictFIFO evicts files using FIFO (oldest creation time first)
func (d *DiskFS) EvictFIFO(bytesNeeded uint) uint {
d.mu.Lock()
defer d.mu.Unlock()
var evicted uint
var candidates []*vfs.FileInfo
// Collect all files
for _, fi := range d.info {
candidates = append(candidates, fi)
}
// Sort by creation time (oldest first)
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].CTime.Before(candidates[j].CTime)
})
// Evict oldest files until we free enough space
for _, fi := range candidates {
if d.size <= d.capacity-int64(bytesNeeded) {
break
}
key := fi.Key
// Remove from LRU
d.LRU.Remove(key)
// Remove from map
delete(d.info, key)
// Remove file from disk
shardedPath := d.shardPath(key)
path := filepath.Join(d.root, shardedPath)
path = strings.ReplaceAll(path, "\\", "/")
if err := os.Remove(path); err != nil {
continue
}
// Update size
d.size -= fi.Size
evicted += uint(fi.Size)
// Clean up key lock
shardIndex := getShardIndex(key)
d.keyLocks[shardIndex].Delete(key)
}
return evicted
} }
+87
View File
@@ -0,0 +1,87 @@
package disk
import (
"fmt"
"os"
"path/filepath"
"s1d3sw1ped/SteamCache2/vfs/vfserror"
"testing"
)
func TestAllDisk(t *testing.T) {
t.Parallel()
m := NewSkipInit(t.TempDir(), 1024)
if err := m.Set("key", []byte("value")); err != nil {
t.Errorf("Set failed: %v", err)
}
if err := m.Set("key", []byte("value1")); err != nil {
t.Errorf("Set failed: %v", err)
}
if d, err := m.Get("key"); err != nil {
t.Errorf("Get failed: %v", err)
} else if string(d) != "value1" {
t.Errorf("Get failed: got %s, want %s", d, "value1")
}
if err := m.Delete("key"); err != nil {
t.Errorf("Delete failed: %v", err)
}
if _, err := m.Get("key"); err == nil {
t.Errorf("Get failed: got nil, want %v", vfserror.ErrNotFound)
}
if err := m.Delete("key"); err == nil {
t.Errorf("Delete failed: got nil, want %v", vfserror.ErrNotFound)
}
if _, err := m.Stat("key"); err == nil {
t.Errorf("Stat failed: got nil, want %v", vfserror.ErrNotFound)
}
if err := m.Set("key", []byte("value")); err != nil {
t.Errorf("Set failed: %v", err)
}
if _, err := m.Stat("key"); err != nil {
t.Errorf("Stat failed: %v", err)
}
}
func TestLimited(t *testing.T) {
t.Parallel()
m := NewSkipInit(t.TempDir(), 10)
for i := 0; i < 11; i++ {
if err := m.Set(fmt.Sprintf("key%d", i), []byte("1")); err != nil && i < 10 {
t.Errorf("Set failed: %v", err)
} else if i == 10 && err == nil {
t.Errorf("Set succeeded: got nil, want %v", vfserror.ErrDiskFull)
}
}
}
func TestInit(t *testing.T) {
t.Parallel()
td := t.TempDir()
path := filepath.Join(td, "test", "key")
os.MkdirAll(filepath.Dir(path), 0755)
os.WriteFile(path, []byte("value"), 0644)
m := New(td, 10)
if _, err := m.Get("test/key"); err != nil {
t.Errorf("Get failed: %v", err)
}
s, _ := m.Stat("test/key")
if s.Name() != "test/key" {
t.Errorf("Stat failed: got %s, want %s", s.Name(), "key")
}
}
+47
View File
@@ -0,0 +1,47 @@
package vfs
import (
"os"
"time"
)
type FileInfo struct {
name string
size int64
MTime time.Time
ATime time.Time
}
func NewFileInfo(name string, size int64, modTime time.Time) *FileInfo {
return &FileInfo{
name: name,
size: size,
MTime: modTime,
ATime: time.Now(),
}
}
func NewFileInfoFromOS(f os.FileInfo, key string) *FileInfo {
return &FileInfo{
name: key,
size: f.Size(),
MTime: f.ModTime(),
ATime: time.Now(),
}
}
func (f FileInfo) Name() string {
return f.name
}
func (f FileInfo) Size() int64 {
return f.size
}
func (f FileInfo) ModTime() time.Time {
return f.MTime
}
func (f FileInfo) AccessTime() time.Time {
return f.ATime
}
+25 -383
View File
@@ -1,402 +1,44 @@
// vfs/gc/gc.go
package gc package gc
import ( import (
"context" "fmt"
"io"
"s1d3sw1ped/SteamCache2/vfs" "s1d3sw1ped/SteamCache2/vfs"
"s1d3sw1ped/SteamCache2/vfs/disk" "s1d3sw1ped/SteamCache2/vfs/vfserror"
"s1d3sw1ped/SteamCache2/vfs/memory"
"sync"
"sync/atomic"
"time"
) )
// GCAlgorithm represents different garbage collection strategies // Ensure GCFS implements VFS.
type GCAlgorithm string var _ vfs.VFS = (*GCFS)(nil)
const ( // 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.
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 { type GCFS struct {
vfs vfs.VFS vfs.VFS
algorithm GCAlgorithm multiplier int
gcFunc func(vfs.VFS, uint) uint gcHanderFunc GCHandlerFunc
} }
// New creates a new GCFS with the specified algorithm // 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.
func New(wrappedVFS vfs.VFS, algorithm GCAlgorithm) *GCFS { type GCHandlerFunc func(vfs vfs.VFS, size int)
gcfs := &GCFS{
vfs: wrappedVFS,
algorithm: algorithm,
}
switch algorithm { func New(vfs vfs.VFS, multiplier int, gcHandlerFunc GCHandlerFunc) *GCFS {
case LRU: return &GCFS{
gcfs.gcFunc = gcLRU VFS: vfs,
case LFU: multiplier: multiplier,
gcfs.gcFunc = gcLFU gcHanderFunc: gcHandlerFunc,
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 // 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 (gc *GCFS) Create(key string, size int64) (io.WriteCloser, error) { func (g *GCFS) Set(key string, src []byte) error {
// Check if we need to GC before creating err := g.VFS.Set(key, src) // try to set the key and src
if gc.vfs.Size()+size > gc.vfs.Capacity() {
needed := uint((gc.vfs.Size() + size) - gc.vfs.Capacity()) if err == vfserror.ErrDiskFull && g.gcHanderFunc != nil { // if the error is disk full and there is a GC handler
gc.gcFunc(gc.vfs, needed) g.gcHanderFunc(g.VFS, len(src)*g.multiplier) // call the GC handler
err = g.VFS.Set(key, src) // try again after GC if it still fails return the error
} }
return gc.vfs.Create(key, size) return err
} }
// Open wraps the underlying Open method func (g *GCFS) Name() string {
func (gc *GCFS) Open(key string) (io.ReadCloser, error) { return fmt.Sprintf("GCFS(%s)", g.VFS.Name()) // wrap the name of the VFS with GCFS so we can see that its a GCFS
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)
} }
+111
View File
@@ -0,0 +1,111 @@
package gc
import (
"fmt"
"s1d3sw1ped/SteamCache2/vfs"
"s1d3sw1ped/SteamCache2/vfs/memory"
"sort"
"testing"
"time"
"golang.org/x/exp/rand"
)
func TestGCSmallRandom(t *testing.T) {
t.Parallel()
m := memory.New(1024 * 1024 * 16)
gc := New(m, 10, func(vfs vfs.VFS, size int) {
tstart := time.Now()
deletions := 0
targetreclaim := int64(size)
var reclaimed int64
t.Logf("GC starting to reclaim %d bytes", targetreclaim)
stats := vfs.StatAll()
sort.Slice(stats, func(i, j int) bool {
// Sort by access time so we can remove the oldest files first.
return stats[i].AccessTime().Before(stats[j].AccessTime())
})
// Delete the oldest files until we've reclaimed enough space.
for _, s := range stats {
sz := s.Size() // Get the size of the file
err := vfs.Delete(s.Name())
if err != nil {
panic(err)
}
reclaimed += sz // Track how much space we've reclaimed
deletions++ // Track how many files we've deleted
// t.Logf("GC deleting %s, %v", s.Name(), s.AccessTime().Format(time.RFC3339Nano))
if reclaimed >= targetreclaim { // We've reclaimed enough space
break
}
}
t.Logf("GC took %v to reclaim %d bytes by deleting %d files", time.Since(tstart), reclaimed, deletions)
})
for i := 0; i < 10000; i++ {
if err := gc.Set(fmt.Sprintf("key:%d", i), genRandomData(1024*1, 1024*4)); err != nil {
t.Errorf("Set failed: %v", err)
}
}
if gc.Size() > 1024*1024*16 {
t.Errorf("MemoryFS size is %d, want <= 1024", m.Size())
}
}
func genRandomData(min int, max int) []byte {
data := make([]byte, rand.Intn(max-min)+min)
rand.Read(data)
return data
}
func TestGCLargeRandom(t *testing.T) {
t.Parallel()
m := memory.New(1024 * 1024 * 16) // 16MB
gc := New(m, 10, func(vfs vfs.VFS, size int) {
tstart := time.Now()
deletions := 0
targetreclaim := int64(size)
var reclaimed int64
t.Logf("GC starting to reclaim %d bytes", targetreclaim)
stats := vfs.StatAll()
sort.Slice(stats, func(i, j int) bool {
// Sort by access time so we can remove the oldest files first.
return stats[i].AccessTime().Before(stats[j].AccessTime())
})
// Delete the oldest files until we've reclaimed enough space.
for _, s := range stats {
sz := s.Size() // Get the size of the file
vfs.Delete(s.Name())
reclaimed += sz // Track how much space we've reclaimed
deletions++ // Track how many files we've deleted
if reclaimed >= targetreclaim { // We've reclaimed enough space
break
}
}
t.Logf("GC took %v to reclaim %d bytes by deleting %d files", time.Since(tstart), reclaimed, deletions)
})
for i := 0; i < 10000; i++ {
if err := gc.Set(fmt.Sprintf("key:%d", i), genRandomData(1024, 1024*1024)); err != nil {
t.Errorf("Set failed: %v", err)
}
}
if gc.Size() > 1024*1024*16 {
t.Errorf("MemoryFS size is %d, want <= 1024", m.Size())
}
}
-130
View File
@@ -1,130 +0,0 @@
package memory
import (
"s1d3sw1ped/SteamCache2/vfs"
"sync"
"sync/atomic"
"time"
)
// DynamicCacheManager manages cache size adjustments based on system memory usage
type DynamicCacheManager struct {
originalCacheSize uint64
currentCacheSize uint64
memoryMonitor *MemoryMonitor
cache vfs.VFS
adjustmentInterval time.Duration
lastAdjustment time.Time
mu sync.RWMutex
adjustmentCount int64
isAdjusting int32
}
// NewDynamicCacheManager creates a new dynamic cache manager
func NewDynamicCacheManager(cache vfs.VFS, originalSize uint64, memoryMonitor *MemoryMonitor) *DynamicCacheManager {
return &DynamicCacheManager{
originalCacheSize: originalSize,
currentCacheSize: originalSize,
memoryMonitor: memoryMonitor,
cache: cache,
adjustmentInterval: 30 * time.Second, // Adjust every 30 seconds
}
}
// Start begins the dynamic cache size adjustment process
func (dcm *DynamicCacheManager) Start() {
go dcm.adjustmentLoop()
}
// GetCurrentCacheSize returns the current cache size
func (dcm *DynamicCacheManager) GetCurrentCacheSize() uint64 {
dcm.mu.RLock()
defer dcm.mu.RUnlock()
return atomic.LoadUint64(&dcm.currentCacheSize)
}
// GetOriginalCacheSize returns the original cache size
func (dcm *DynamicCacheManager) GetOriginalCacheSize() uint64 {
dcm.mu.RLock()
defer dcm.mu.RUnlock()
return dcm.originalCacheSize
}
// GetAdjustmentCount returns the number of adjustments made
func (dcm *DynamicCacheManager) GetAdjustmentCount() int64 {
return atomic.LoadInt64(&dcm.adjustmentCount)
}
// adjustmentLoop runs the cache size adjustment loop
func (dcm *DynamicCacheManager) adjustmentLoop() {
ticker := time.NewTicker(dcm.adjustmentInterval)
defer ticker.Stop()
for range ticker.C {
dcm.performAdjustment()
}
}
// performAdjustment performs a cache size adjustment if needed
func (dcm *DynamicCacheManager) performAdjustment() {
// Prevent concurrent adjustments
if !atomic.CompareAndSwapInt32(&dcm.isAdjusting, 0, 1) {
return
}
defer atomic.StoreInt32(&dcm.isAdjusting, 0)
// Check if enough time has passed since last adjustment
if time.Since(dcm.lastAdjustment) < dcm.adjustmentInterval {
return
}
// Get recommended cache size
recommendedSize := dcm.memoryMonitor.GetRecommendedCacheSize(dcm.originalCacheSize)
currentSize := atomic.LoadUint64(&dcm.currentCacheSize)
// Only adjust if there's a significant difference (more than 5%)
sizeDiff := float64(recommendedSize) / float64(currentSize)
if sizeDiff < 0.95 || sizeDiff > 1.05 {
dcm.adjustCacheSize(recommendedSize)
dcm.lastAdjustment = time.Now()
atomic.AddInt64(&dcm.adjustmentCount, 1)
}
}
// adjustCacheSize adjusts the cache size to the recommended size
func (dcm *DynamicCacheManager) adjustCacheSize(newSize uint64) {
dcm.mu.Lock()
defer dcm.mu.Unlock()
oldSize := atomic.LoadUint64(&dcm.currentCacheSize)
atomic.StoreUint64(&dcm.currentCacheSize, newSize)
// If we're reducing the cache size, trigger GC to free up memory
if newSize < oldSize {
// Calculate how much to free
bytesToFree := oldSize - newSize
// Trigger GC on the cache to free up the excess memory
// This is a simplified approach - in practice, you'd want to integrate
// with the actual GC system to free the right amount
if gcCache, ok := dcm.cache.(interface{ ForceGC(uint) }); ok {
gcCache.ForceGC(uint(bytesToFree))
}
}
}
// GetStats returns statistics about the dynamic cache manager
func (dcm *DynamicCacheManager) GetStats() map[string]interface{} {
dcm.mu.RLock()
defer dcm.mu.RUnlock()
return map[string]interface{}{
"original_cache_size": dcm.originalCacheSize,
"current_cache_size": atomic.LoadUint64(&dcm.currentCacheSize),
"adjustment_count": atomic.LoadInt64(&dcm.adjustmentCount),
"last_adjustment": dcm.lastAdjustment,
"memory_utilization": dcm.memoryMonitor.GetMemoryUtilization(),
"target_memory_usage": dcm.memoryMonitor.GetTargetMemoryUsage(),
"current_memory_usage": dcm.memoryMonitor.GetCurrentMemoryUsage(),
}
}
+74 -453
View File
@@ -1,512 +1,133 @@
// vfs/memory/memory.go
package memory package memory
import ( import (
"bytes" "s1d3sw1ped/SteamCache2/vfs"
"container/list"
"io"
"s1d3sw1ped/SteamCache2/vfs/types"
"s1d3sw1ped/SteamCache2/vfs/vfserror" "s1d3sw1ped/SteamCache2/vfs/vfserror"
"sort"
"strings"
"sync" "sync"
"time" "time"
) )
// VFS defines the interface for virtual file systems
type VFS interface {
// Create creates a new file at the given key
Create(key string, size int64) (io.WriteCloser, error)
// Open opens the file at the given key for reading
Open(key string) (io.ReadCloser, error)
// Delete removes the file at the given key
Delete(key string) error
// Stat returns information about the file at the given key
Stat(key string) (*types.FileInfo, error)
// Name returns the name of this VFS
Name() string
// Size returns the current size of the VFS
Size() int64
// Capacity returns the maximum capacity of the VFS
Capacity() int64
}
// Ensure MemoryFS implements VFS. // Ensure MemoryFS implements VFS.
var _ VFS = (*MemoryFS)(nil) var _ vfs.VFS = (*MemoryFS)(nil)
// MemoryFS is an in-memory virtual file system // file represents a file in memory.
type file struct {
fileinfo *vfs.FileInfo
data []byte
}
// MemoryFS is a virtual file system that stores files in memory.
type MemoryFS struct { type MemoryFS struct {
data map[string]*bytes.Buffer files map[string]*file
info map[string]*types.FileInfo
capacity int64 capacity int64
size int64 mu sync.Mutex
mu sync.RWMutex
keyLocks []sync.Map // Sharded lock pools for better concurrency
LRU *lruList
timeUpdater *types.BatchedTimeUpdate // Batched time updates for better performance
} }
// Number of lock shards for reducing contention // New creates a new MemoryFS.
const numLockShards = 32
// lruList for time-decayed LRU eviction
type lruList struct {
list *list.List
elem map[string]*list.Element
}
func newLruList() *lruList {
return &lruList{
list: list.New(),
elem: make(map[string]*list.Element),
}
}
func (l *lruList) Add(key string, fi *types.FileInfo) {
elem := l.list.PushFront(fi)
l.elem[key] = elem
}
func (l *lruList) MoveToFront(key string, timeUpdater *types.BatchedTimeUpdate) {
if elem, exists := l.elem[key]; exists {
l.list.MoveToFront(elem)
// Update the FileInfo in the element with new access time
if fi := elem.Value.(*types.FileInfo); fi != nil {
fi.UpdateAccessBatched(timeUpdater)
}
}
}
func (l *lruList) Remove(key string) *types.FileInfo {
if elem, exists := l.elem[key]; exists {
delete(l.elem, key)
if fi := l.list.Remove(elem).(*types.FileInfo); fi != nil {
return fi
}
}
return nil
}
func (l *lruList) Len() int {
return l.list.Len()
}
// 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")
}
// Initialize sharded locks
keyLocks := make([]sync.Map, numLockShards)
return &MemoryFS{ return &MemoryFS{
data: make(map[string]*bytes.Buffer), files: make(map[string]*file),
info: make(map[string]*types.FileInfo),
capacity: capacity, capacity: capacity,
size: 0, mu: sync.Mutex{},
keyLocks: keyLocks,
LRU: newLruList(),
timeUpdater: types.NewBatchedTimeUpdate(100 * time.Millisecond), // Update time every 100ms
} }
} }
// Name returns the name of this VFS
func (m *MemoryFS) Name() string {
return "MemoryFS"
}
// Size returns the current size
func (m *MemoryFS) Size() int64 {
m.mu.RLock()
defer m.mu.RUnlock()
return m.size
}
// Capacity returns the maximum capacity
func (m *MemoryFS) Capacity() int64 { func (m *MemoryFS) Capacity() int64 {
return m.capacity return m.capacity
} }
// GetFragmentationStats returns memory fragmentation statistics func (m *MemoryFS) Name() string {
func (m *MemoryFS) GetFragmentationStats() map[string]interface{} { return "MemoryFS"
m.mu.RLock()
defer m.mu.RUnlock()
var totalCapacity int64
var totalUsed int64
var bufferCount int
for _, buffer := range m.data {
totalCapacity += int64(buffer.Cap())
totalUsed += int64(buffer.Len())
bufferCount++
}
fragmentationRatio := float64(0)
if totalCapacity > 0 {
fragmentationRatio = float64(totalCapacity-totalUsed) / float64(totalCapacity)
}
return map[string]interface{}{
"buffer_count": bufferCount,
"total_capacity": totalCapacity,
"total_used": totalUsed,
"fragmentation_ratio": fragmentationRatio,
"average_buffer_size": float64(totalUsed) / float64(bufferCount),
}
} }
// getShardIndex returns the shard index for a given key func (m *MemoryFS) Size() int64 {
func getShardIndex(key string) int { var size int64
// Use FNV-1a hash for good distribution
var h uint32 = 2166136261 // FNV offset basis
for i := 0; i < len(key); i++ {
h ^= uint32(key[i])
h *= 16777619 // FNV prime
}
return int(h % numLockShards)
}
// getKeyLock returns a lock for the given key using sharding
func (m *MemoryFS) getKeyLock(key string) *sync.RWMutex {
shardIndex := getShardIndex(key)
shard := &m.keyLocks[shardIndex]
keyLock, _ := shard.LoadOrStore(key, &sync.RWMutex{})
return keyLock.(*sync.RWMutex)
}
// Create creates a new file
func (m *MemoryFS) 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
if strings.Contains(key, "..") {
return nil, vfserror.ErrInvalidKey
}
keyMu := m.getKeyLock(key)
keyMu.Lock()
defer keyMu.Unlock()
m.mu.Lock() m.mu.Lock()
// Check if file already exists and handle overwrite defer m.mu.Unlock()
if fi, exists := m.info[key]; exists {
m.size -= fi.Size for _, v := range m.files {
m.LRU.Remove(key) size += int64(len(v.data))
delete(m.info, key)
delete(m.data, key)
} }
buffer := &bytes.Buffer{} return size
m.data[key] = buffer
fi := types.NewFileInfo(key, size)
m.info[key] = fi
m.LRU.Add(key, fi)
// Initialize access time with current time
fi.UpdateAccessBatched(m.timeUpdater)
m.size += size
m.mu.Unlock()
return &memoryWriteCloser{
buffer: buffer,
memory: m,
key: key,
}, nil
} }
// memoryWriteCloser implements io.WriteCloser for memory files func (m *MemoryFS) Set(key string, src []byte) error {
type memoryWriteCloser struct { if m.capacity > 0 {
buffer *bytes.Buffer if size := m.Size() + int64(len(src)); size > m.capacity {
memory *MemoryFS return vfserror.ErrDiskFull
key string
}
func (mwc *memoryWriteCloser) Write(p []byte) (n int, err error) {
return mwc.buffer.Write(p)
}
func (mwc *memoryWriteCloser) Close() error {
// Update the actual size in FileInfo
mwc.memory.mu.Lock()
if fi, exists := mwc.memory.info[mwc.key]; exists {
actualSize := int64(mwc.buffer.Len())
sizeDiff := actualSize - fi.Size
fi.Size = actualSize
mwc.memory.size += sizeDiff
} }
mwc.memory.mu.Unlock() }
m.mu.Lock()
defer m.mu.Unlock()
m.files[key] = &file{
fileinfo: vfs.NewFileInfo(
key,
int64(len(src)),
time.Now(),
),
data: make([]byte, len(src)),
}
copy(m.files[key].data, src)
return nil return nil
} }
// Open opens a file for reading
func (m *MemoryFS) Open(key string) (io.ReadCloser, error) {
if key == "" {
return nil, vfserror.ErrInvalidKey
}
if key[0] == '/' {
return nil, vfserror.ErrInvalidKey
}
if strings.Contains(key, "..") {
return nil, vfserror.ErrInvalidKey
}
keyMu := m.getKeyLock(key)
keyMu.RLock()
defer keyMu.RUnlock()
m.mu.Lock()
fi, exists := m.info[key]
if !exists {
m.mu.Unlock()
return nil, vfserror.ErrNotFound
}
fi.UpdateAccessBatched(m.timeUpdater)
m.LRU.MoveToFront(key, m.timeUpdater)
buffer, exists := m.data[key]
if !exists {
m.mu.Unlock()
return nil, vfserror.ErrNotFound
}
// Use zero-copy approach - return reader that reads directly from buffer
m.mu.Unlock()
return &memoryReadCloser{
buffer: buffer,
offset: 0,
}, nil
}
// memoryReadCloser implements io.ReadCloser for memory files with zero-copy optimization
type memoryReadCloser struct {
buffer *bytes.Buffer
offset int64
}
func (mrc *memoryReadCloser) Read(p []byte) (n int, err error) {
if mrc.offset >= int64(mrc.buffer.Len()) {
return 0, io.EOF
}
// Zero-copy read directly from buffer
available := mrc.buffer.Len() - int(mrc.offset)
toRead := len(p)
if toRead > available {
toRead = available
}
// Read directly from buffer without copying
data := mrc.buffer.Bytes()
copy(p, data[mrc.offset:mrc.offset+int64(toRead)])
mrc.offset += int64(toRead)
return toRead, nil
}
func (mrc *memoryReadCloser) Close() error {
return nil
}
// Delete removes a file
func (m *MemoryFS) Delete(key string) error { func (m *MemoryFS) Delete(key string) error {
if key == "" { _, err := m.Stat(key)
return vfserror.ErrInvalidKey if err != nil {
return err
} }
if key[0] == '/' {
return vfserror.ErrInvalidKey
}
if strings.Contains(key, "..") {
return vfserror.ErrInvalidKey
}
keyMu := m.getKeyLock(key)
keyMu.Lock()
defer keyMu.Unlock()
m.mu.Lock() m.mu.Lock()
fi, exists := m.info[key] defer m.mu.Unlock()
if !exists {
m.mu.Unlock() delete(m.files, key)
return vfserror.ErrNotFound
}
m.size -= fi.Size
m.LRU.Remove(key)
delete(m.info, key)
delete(m.data, key)
m.mu.Unlock()
return nil return nil
} }
// Stat returns file information func (m *MemoryFS) Get(key string) ([]byte, error) {
func (m *MemoryFS) Stat(key string) (*types.FileInfo, error) { _, err := m.Stat(key)
if key == "" { if err != nil {
return nil, vfserror.ErrInvalidKey return nil, err
}
if key[0] == '/' {
return nil, vfserror.ErrInvalidKey
} }
if strings.Contains(key, "..") { m.mu.Lock()
return nil, vfserror.ErrInvalidKey defer m.mu.Unlock()
}
keyMu := m.getKeyLock(key) m.files[key].fileinfo.ATime = time.Now()
keyMu.RLock() dst := make([]byte, len(m.files[key].data))
defer keyMu.RUnlock() copy(dst, m.files[key].data)
m.mu.RLock() return dst, nil
defer m.mu.RUnlock() }
if fi, ok := m.info[key]; ok { func (m *MemoryFS) Stat(key string) (*vfs.FileInfo, error) {
return fi, nil m.mu.Lock()
} defer m.mu.Unlock()
f, ok := m.files[key]
if !ok {
return nil, vfserror.ErrNotFound return nil, vfserror.ErrNotFound
}
return f.fileinfo, nil
} }
// EvictLRU evicts the least recently used files to free up space func (m *MemoryFS) StatAll() []*vfs.FileInfo {
func (m *MemoryFS) EvictLRU(bytesNeeded uint) uint {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
var evicted uint // hard copy the file info to prevent modification of the original file info or the other way around
files := make([]*vfs.FileInfo, 0, len(m.files))
// Evict from LRU list until we free enough space for _, v := range m.files {
for m.size > m.capacity-int64(bytesNeeded) && m.LRU.Len() > 0 { fi := *v.fileinfo
// Get the least recently used item files = append(files, &fi)
elem := m.LRU.list.Back()
if elem == nil {
break
} }
fi := elem.Value.(*types.FileInfo) return files
key := fi.Key
// Remove from LRU
m.LRU.Remove(key)
// Remove from maps
delete(m.info, key)
delete(m.data, key)
// Update size
m.size -= fi.Size
evicted += uint(fi.Size)
// Clean up key lock
shardIndex := getShardIndex(key)
m.keyLocks[shardIndex].Delete(key)
}
return evicted
}
// EvictBySize evicts files by size (ascending = smallest first, descending = largest first)
func (m *MemoryFS) EvictBySize(bytesNeeded uint, ascending bool) uint {
m.mu.Lock()
defer m.mu.Unlock()
var evicted uint
var candidates []*types.FileInfo
// Collect all files
for _, fi := range m.info {
candidates = append(candidates, fi)
}
// Sort by size
sort.Slice(candidates, func(i, j int) bool {
if ascending {
return candidates[i].Size < candidates[j].Size
}
return candidates[i].Size > candidates[j].Size
})
// Evict files until we free enough space
for _, fi := range candidates {
if m.size <= m.capacity-int64(bytesNeeded) {
break
}
key := fi.Key
// Remove from LRU
m.LRU.Remove(key)
// Remove from maps
delete(m.info, key)
delete(m.data, key)
// Update size
m.size -= fi.Size
evicted += uint(fi.Size)
// Clean up key lock
shardIndex := getShardIndex(key)
m.keyLocks[shardIndex].Delete(key)
}
return evicted
}
// EvictFIFO evicts files using FIFO (oldest creation time first)
func (m *MemoryFS) EvictFIFO(bytesNeeded uint) uint {
m.mu.Lock()
defer m.mu.Unlock()
var evicted uint
var candidates []*types.FileInfo
// Collect all files
for _, fi := range m.info {
candidates = append(candidates, fi)
}
// Sort by creation time (oldest first)
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].CTime.Before(candidates[j].CTime)
})
// Evict oldest files until we free enough space
for _, fi := range candidates {
if m.size <= m.capacity-int64(bytesNeeded) {
break
}
key := fi.Key
// Remove from LRU
m.LRU.Remove(key)
// Remove from maps
delete(m.info, key)
delete(m.data, key)
// Update size
m.size -= fi.Size
evicted += uint(fi.Size)
// Clean up key lock
shardIndex := getShardIndex(key)
m.keyLocks[shardIndex].Delete(key)
}
return evicted
} }
+63
View File
@@ -0,0 +1,63 @@
package memory
import (
"fmt"
"s1d3sw1ped/SteamCache2/vfs/vfserror"
"testing"
)
func TestAllMemory(t *testing.T) {
t.Parallel()
m := New(1024)
if err := m.Set("key", []byte("value")); err != nil {
t.Errorf("Set failed: %v", err)
}
if err := m.Set("key", []byte("value1")); err != nil {
t.Errorf("Set failed: %v", err)
}
if d, err := m.Get("key"); err != nil {
t.Errorf("Get failed: %v", err)
} else if string(d) != "value1" {
t.Errorf("Get failed: got %s, want %s", d, "value1")
}
if err := m.Delete("key"); err != nil {
t.Errorf("Delete failed: %v", err)
}
if _, err := m.Get("key"); err == nil {
t.Errorf("Get failed: got nil, want %v", vfserror.ErrNotFound)
}
if err := m.Delete("key"); err == nil {
t.Errorf("Delete failed: got nil, want %v", vfserror.ErrNotFound)
}
if _, err := m.Stat("key"); err == nil {
t.Errorf("Stat failed: got nil, want %v", vfserror.ErrNotFound)
}
if err := m.Set("key", []byte("value")); err != nil {
t.Errorf("Set failed: %v", err)
}
if _, err := m.Stat("key"); err != nil {
t.Errorf("Stat failed: %v", err)
}
}
func TestLimited(t *testing.T) {
t.Parallel()
m := New(10)
for i := 0; i < 11; i++ {
if err := m.Set(fmt.Sprintf("key%d", i), []byte("1")); err != nil && i < 10 {
t.Errorf("Set failed: %v", err)
} else if i == 10 && err == nil {
t.Errorf("Set succeeded: got nil, want %v", vfserror.ErrDiskFull)
}
}
}
-153
View File
@@ -1,153 +0,0 @@
package memory
import (
"runtime"
"sync"
"sync/atomic"
"time"
)
// MemoryMonitor tracks system memory usage and provides dynamic sizing recommendations
type MemoryMonitor struct {
targetMemoryUsage uint64 // Target total memory usage in bytes
currentMemoryUsage uint64 // Current total memory usage in bytes
monitoringInterval time.Duration
adjustmentThreshold float64 // Threshold for cache size adjustments (e.g., 0.1 = 10%)
mu sync.RWMutex
ctx chan struct{}
stopChan chan struct{}
isMonitoring int32
}
// NewMemoryMonitor creates a new memory monitor
func NewMemoryMonitor(targetMemoryUsage uint64, monitoringInterval time.Duration, adjustmentThreshold float64) *MemoryMonitor {
return &MemoryMonitor{
targetMemoryUsage: targetMemoryUsage,
monitoringInterval: monitoringInterval,
adjustmentThreshold: adjustmentThreshold,
ctx: make(chan struct{}),
stopChan: make(chan struct{}),
}
}
// Start begins monitoring memory usage
func (mm *MemoryMonitor) Start() {
if atomic.CompareAndSwapInt32(&mm.isMonitoring, 0, 1) {
go mm.monitor()
}
}
// Stop stops monitoring memory usage
func (mm *MemoryMonitor) Stop() {
if atomic.CompareAndSwapInt32(&mm.isMonitoring, 1, 0) {
close(mm.stopChan)
}
}
// GetCurrentMemoryUsage returns the current total memory usage
func (mm *MemoryMonitor) GetCurrentMemoryUsage() uint64 {
mm.mu.RLock()
defer mm.mu.RUnlock()
return atomic.LoadUint64(&mm.currentMemoryUsage)
}
// GetTargetMemoryUsage returns the target memory usage
func (mm *MemoryMonitor) GetTargetMemoryUsage() uint64 {
mm.mu.RLock()
defer mm.mu.RUnlock()
return mm.targetMemoryUsage
}
// GetMemoryUtilization returns the current memory utilization as a percentage
func (mm *MemoryMonitor) GetMemoryUtilization() float64 {
mm.mu.RLock()
defer mm.mu.RUnlock()
current := atomic.LoadUint64(&mm.currentMemoryUsage)
return float64(current) / float64(mm.targetMemoryUsage)
}
// GetRecommendedCacheSize calculates the recommended cache size based on current memory usage
func (mm *MemoryMonitor) GetRecommendedCacheSize(originalCacheSize uint64) uint64 {
mm.mu.RLock()
defer mm.mu.RUnlock()
current := atomic.LoadUint64(&mm.currentMemoryUsage)
target := mm.targetMemoryUsage
// If we're under target, we can use the full cache size
if current <= target {
return originalCacheSize
}
// Calculate how much we're over target
overage := current - target
// If overage is significant, reduce cache size
if overage > uint64(float64(target)*mm.adjustmentThreshold) {
// Reduce cache size by the overage amount, but don't go below 10% of original
minCacheSize := uint64(float64(originalCacheSize) * 0.1)
recommendedSize := originalCacheSize - overage
if recommendedSize < minCacheSize {
recommendedSize = minCacheSize
}
return recommendedSize
}
return originalCacheSize
}
// monitor runs the memory monitoring loop
func (mm *MemoryMonitor) monitor() {
ticker := time.NewTicker(mm.monitoringInterval)
defer ticker.Stop()
for {
select {
case <-mm.stopChan:
return
case <-ticker.C:
mm.updateMemoryUsage()
}
}
}
// updateMemoryUsage updates the current memory usage
func (mm *MemoryMonitor) updateMemoryUsage() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// Use Alloc (currently allocated memory) as our metric
atomic.StoreUint64(&mm.currentMemoryUsage, m.Alloc)
}
// SetTargetMemoryUsage updates the target memory usage
func (mm *MemoryMonitor) SetTargetMemoryUsage(target uint64) {
mm.mu.Lock()
defer mm.mu.Unlock()
mm.targetMemoryUsage = target
}
// GetMemoryStats returns detailed memory statistics
func (mm *MemoryMonitor) GetMemoryStats() map[string]interface{} {
var m runtime.MemStats
runtime.ReadMemStats(&m)
mm.mu.RLock()
defer mm.mu.RUnlock()
return map[string]interface{}{
"current_usage": atomic.LoadUint64(&mm.currentMemoryUsage),
"target_usage": mm.targetMemoryUsage,
"utilization": mm.GetMemoryUtilization(),
"heap_alloc": m.HeapAlloc,
"heap_sys": m.HeapSys,
"heap_idle": m.HeapIdle,
"heap_inuse": m.HeapInuse,
"stack_inuse": m.StackInuse,
"stack_sys": m.StackSys,
"gc_cycles": m.NumGC,
"gc_pause_total": m.PauseTotalNs,
}
}
-367
View File
@@ -1,367 +0,0 @@
package predictive
import (
"context"
"sync"
"sync/atomic"
"time"
)
// PredictiveCacheManager implements predictive caching strategies
type PredictiveCacheManager struct {
accessPredictor *AccessPredictor
cacheWarmer *CacheWarmer
prefetchQueue chan PrefetchRequest
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
stats *PredictiveStats
}
// PrefetchRequest represents a request to prefetch content
type PrefetchRequest struct {
Key string
Priority int
Reason string
RequestedAt time.Time
}
// PredictiveStats tracks predictive caching statistics
type PredictiveStats struct {
PrefetchHits int64
PrefetchMisses int64
PrefetchRequests int64
CacheWarmHits int64
CacheWarmMisses int64
mu sync.RWMutex
}
// AccessPredictor predicts which files are likely to be accessed next
type AccessPredictor struct {
accessHistory map[string]*AccessSequence
patterns map[string][]string // Key -> likely next keys
mu sync.RWMutex
}
// AccessSequence tracks access sequences for prediction
type AccessSequence struct {
Key string
NextKeys []string
Frequency map[string]int64
LastSeen time.Time
mu sync.RWMutex
}
// CacheWarmer preloads popular content into cache
type CacheWarmer struct {
popularContent map[string]*PopularContent
warmerQueue chan WarmRequest
mu sync.RWMutex
}
// PopularContent tracks popular content for warming
type PopularContent struct {
Key string
AccessCount int64
LastAccess time.Time
Size int64
Priority int
}
// WarmRequest represents a cache warming request
type WarmRequest struct {
Key string
Priority int
Reason string
}
// NewPredictiveCacheManager creates a new predictive cache manager
func NewPredictiveCacheManager() *PredictiveCacheManager {
ctx, cancel := context.WithCancel(context.Background())
pcm := &PredictiveCacheManager{
accessPredictor: NewAccessPredictor(),
cacheWarmer: NewCacheWarmer(),
prefetchQueue: make(chan PrefetchRequest, 1000),
ctx: ctx,
cancel: cancel,
stats: &PredictiveStats{},
}
// Start background workers
pcm.wg.Add(1)
go pcm.prefetchWorker()
pcm.wg.Add(1)
go pcm.analysisWorker()
return pcm
}
// NewAccessPredictor creates a new access predictor
func NewAccessPredictor() *AccessPredictor {
return &AccessPredictor{
accessHistory: make(map[string]*AccessSequence),
patterns: make(map[string][]string),
}
}
// NewCacheWarmer creates a new cache warmer
func NewCacheWarmer() *CacheWarmer {
return &CacheWarmer{
popularContent: make(map[string]*PopularContent),
warmerQueue: make(chan WarmRequest, 100),
}
}
// RecordAccess records a file access for prediction analysis (lightweight version)
func (pcm *PredictiveCacheManager) RecordAccess(key string, previousKey string, size int64) {
// Only record if we have a previous key to avoid overhead
if previousKey != "" {
pcm.accessPredictor.RecordSequence(previousKey, key)
}
// Lightweight popular content tracking - only for large files
if size > 1024*1024 { // Only track files > 1MB
pcm.cacheWarmer.RecordAccess(key, size)
}
// Skip expensive prediction checks on every access
// Only check occasionally to reduce overhead
}
// PredictNextAccess predicts the next likely file to be accessed
func (pcm *PredictiveCacheManager) PredictNextAccess(currentKey string) []string {
return pcm.accessPredictor.PredictNext(currentKey)
}
// RequestPrefetch requests prefetching of predicted content
func (pcm *PredictiveCacheManager) RequestPrefetch(key string, priority int, reason string) {
select {
case pcm.prefetchQueue <- PrefetchRequest{
Key: key,
Priority: priority,
Reason: reason,
RequestedAt: time.Now(),
}:
atomic.AddInt64(&pcm.stats.PrefetchRequests, 1)
default:
// Queue full, skip prefetch
}
}
// RecordSequence records an access sequence for prediction
func (ap *AccessPredictor) RecordSequence(previousKey, currentKey string) {
if previousKey == "" || currentKey == "" {
return
}
ap.mu.Lock()
defer ap.mu.Unlock()
seq, exists := ap.accessHistory[previousKey]
if !exists {
seq = &AccessSequence{
Key: previousKey,
NextKeys: []string{},
Frequency: make(map[string]int64),
LastSeen: time.Now(),
}
ap.accessHistory[previousKey] = seq
}
seq.mu.Lock()
seq.Frequency[currentKey]++
seq.LastSeen = time.Now()
// Update next keys list (keep top 5)
nextKeys := make([]string, 0, 5)
for key, _ := range seq.Frequency {
nextKeys = append(nextKeys, key)
if len(nextKeys) >= 5 {
break
}
}
seq.NextKeys = nextKeys
seq.mu.Unlock()
}
// PredictNext predicts the next likely files to be accessed
func (ap *AccessPredictor) PredictNext(currentKey string) []string {
ap.mu.RLock()
defer ap.mu.RUnlock()
seq, exists := ap.accessHistory[currentKey]
if !exists {
return []string{}
}
seq.mu.RLock()
defer seq.mu.RUnlock()
// Return top predicted keys
predictions := make([]string, len(seq.NextKeys))
copy(predictions, seq.NextKeys)
return predictions
}
// IsPredictedAccess checks if an access was predicted
func (ap *AccessPredictor) IsPredictedAccess(key string) bool {
ap.mu.RLock()
defer ap.mu.RUnlock()
// Check if this key appears in any prediction lists
for _, seq := range ap.accessHistory {
seq.mu.RLock()
for _, predictedKey := range seq.NextKeys {
if predictedKey == key {
seq.mu.RUnlock()
return true
}
}
seq.mu.RUnlock()
}
return false
}
// RecordAccess records a file access for cache warming (lightweight version)
func (cw *CacheWarmer) RecordAccess(key string, size int64) {
// Use read lock first for better performance
cw.mu.RLock()
content, exists := cw.popularContent[key]
cw.mu.RUnlock()
if !exists {
// Only acquire write lock when creating new entry
cw.mu.Lock()
// Double-check after acquiring write lock
if content, exists = cw.popularContent[key]; !exists {
content = &PopularContent{
Key: key,
AccessCount: 1,
LastAccess: time.Now(),
Size: size,
Priority: 1,
}
cw.popularContent[key] = content
}
cw.mu.Unlock()
} else {
// Lightweight update - just increment counter
content.AccessCount++
content.LastAccess = time.Now()
// Only update priority occasionally to reduce overhead
if content.AccessCount%5 == 0 {
if content.AccessCount > 10 {
content.Priority = 3
} else if content.AccessCount > 5 {
content.Priority = 2
}
}
}
}
// GetPopularContent returns the most popular content for warming
func (cw *CacheWarmer) GetPopularContent(limit int) []*PopularContent {
cw.mu.RLock()
defer cw.mu.RUnlock()
// Sort by access count and return top items
popular := make([]*PopularContent, 0, len(cw.popularContent))
for _, content := range cw.popularContent {
popular = append(popular, content)
}
// Simple sort by access count (in production, use proper sorting)
// For now, just return the first 'limit' items
if len(popular) > limit {
popular = popular[:limit]
}
return popular
}
// prefetchWorker processes prefetch requests
func (pcm *PredictiveCacheManager) prefetchWorker() {
defer pcm.wg.Done()
for {
select {
case <-pcm.ctx.Done():
return
case req := <-pcm.prefetchQueue:
// Process prefetch request
pcm.processPrefetchRequest(req)
}
}
}
// analysisWorker performs periodic analysis and cache warming
func (pcm *PredictiveCacheManager) analysisWorker() {
defer pcm.wg.Done()
ticker := time.NewTicker(30 * time.Second) // Analyze every 30 seconds
defer ticker.Stop()
for {
select {
case <-pcm.ctx.Done():
return
case <-ticker.C:
pcm.performAnalysis()
}
}
}
// processPrefetchRequest processes a prefetch request
func (pcm *PredictiveCacheManager) processPrefetchRequest(req PrefetchRequest) {
// In a real implementation, this would:
// 1. Check if content is already cached
// 2. If not, fetch and cache it
// 3. Update statistics
// For now, just log the prefetch request
// In production, integrate with the actual cache system
}
// performAnalysis performs periodic analysis and cache warming
func (pcm *PredictiveCacheManager) performAnalysis() {
// Get popular content for warming
popular := pcm.cacheWarmer.GetPopularContent(10)
// Request warming for popular content
for _, content := range popular {
if content.AccessCount > 5 { // Only warm frequently accessed content
select {
case pcm.cacheWarmer.warmerQueue <- WarmRequest{
Key: content.Key,
Priority: content.Priority,
Reason: "popular_content",
}:
default:
// Queue full, skip
}
}
}
}
// GetStats returns predictive caching statistics
func (pcm *PredictiveCacheManager) GetStats() *PredictiveStats {
pcm.stats.mu.RLock()
defer pcm.stats.mu.RUnlock()
return &PredictiveStats{
PrefetchHits: atomic.LoadInt64(&pcm.stats.PrefetchHits),
PrefetchMisses: atomic.LoadInt64(&pcm.stats.PrefetchMisses),
PrefetchRequests: atomic.LoadInt64(&pcm.stats.PrefetchRequests),
CacheWarmHits: atomic.LoadInt64(&pcm.stats.CacheWarmHits),
CacheWarmMisses: atomic.LoadInt64(&pcm.stats.CacheWarmMisses),
}
}
// Stop stops the predictive cache manager
func (pcm *PredictiveCacheManager) Stop() {
pcm.cancel()
pcm.wg.Wait()
}
+76
View File
@@ -0,0 +1,76 @@
package sync
import (
"fmt"
"s1d3sw1ped/SteamCache2/vfs"
"sync"
)
// Ensure SyncFS implements VFS.
var _ vfs.VFS = (*SyncFS)(nil)
type SyncFS struct {
vfs vfs.VFS
mu sync.RWMutex
}
func New(vfs vfs.VFS) *SyncFS {
return &SyncFS{
vfs: vfs,
mu: sync.RWMutex{},
}
}
// Name returns the name of the file system.
func (sfs *SyncFS) Name() string {
return fmt.Sprintf("SyncFS(%s)", sfs.vfs.Name())
}
// Size returns the total size of all files in the file system.
func (sfs *SyncFS) Size() int64 {
sfs.mu.RLock()
defer sfs.mu.RUnlock()
return sfs.vfs.Size()
}
// Set sets the value of key as src.
// Setting the same key multiple times, the last set call takes effect.
func (sfs *SyncFS) Set(key string, src []byte) error {
sfs.mu.Lock()
defer sfs.mu.Unlock()
return sfs.vfs.Set(key, src)
}
// Delete deletes the value of key.
func (sfs *SyncFS) Delete(key string) error {
sfs.mu.Lock()
defer sfs.mu.Unlock()
return sfs.vfs.Delete(key)
}
// Get gets the value of key to dst, and returns dst no matter whether or not there is an error.
func (sfs *SyncFS) Get(key string) ([]byte, error) {
sfs.mu.RLock()
defer sfs.mu.RUnlock()
return sfs.vfs.Get(key)
}
// Stat returns the FileInfo of key.
func (sfs *SyncFS) Stat(key string) (*vfs.FileInfo, error) {
sfs.mu.RLock()
defer sfs.mu.RUnlock()
return sfs.vfs.Stat(key)
}
// StatAll returns the FileInfo of all keys.
func (sfs *SyncFS) StatAll() []*vfs.FileInfo {
sfs.mu.RLock()
defer sfs.mu.RUnlock()
return sfs.vfs.StatAll()
}
-87
View File
@@ -1,87 +0,0 @@
// vfs/types/types.go
package types
import (
"os"
"time"
)
// FileInfo contains metadata about a cached file
type FileInfo struct {
Key string `json:"key"`
Size int64 `json:"size"`
ATime time.Time `json:"atime"` // Last access time
CTime time.Time `json:"ctime"` // Creation time
AccessCount int `json:"access_count"`
}
// NewFileInfo creates a new FileInfo with the given key and current timestamp
func NewFileInfo(key string, size int64) *FileInfo {
now := time.Now()
return &FileInfo{
Key: key,
Size: size,
ATime: now,
CTime: now,
AccessCount: 1,
}
}
// NewFileInfoFromOS creates a FileInfo from os.FileInfo
func NewFileInfoFromOS(info os.FileInfo, key string) *FileInfo {
return &FileInfo{
Key: key,
Size: info.Size(),
ATime: time.Now(), // We don't have access time from os.FileInfo
CTime: info.ModTime(),
AccessCount: 1,
}
}
// UpdateAccess updates the access time and increments the access count
func (fi *FileInfo) UpdateAccess() {
fi.ATime = time.Now()
fi.AccessCount++
}
// BatchedTimeUpdate provides a way to batch time updates for better performance
type BatchedTimeUpdate struct {
currentTime time.Time
lastUpdate time.Time
updateInterval time.Duration
}
// NewBatchedTimeUpdate creates a new batched time updater
func NewBatchedTimeUpdate(interval time.Duration) *BatchedTimeUpdate {
now := time.Now()
return &BatchedTimeUpdate{
currentTime: now,
lastUpdate: now,
updateInterval: interval,
}
}
// GetTime returns the current cached time, updating it if necessary
func (btu *BatchedTimeUpdate) GetTime() time.Time {
now := time.Now()
if now.Sub(btu.lastUpdate) >= btu.updateInterval {
btu.currentTime = now
btu.lastUpdate = now
}
return btu.currentTime
}
// UpdateAccessBatched updates the access time using batched time updates
func (fi *FileInfo) UpdateAccessBatched(btu *BatchedTimeUpdate) {
fi.ATime = btu.GetTime()
fi.AccessCount++
}
// GetTimeDecayedScore calculates a score based on access time and frequency
// More recent and frequent accesses get higher scores
func (fi *FileInfo) GetTimeDecayedScore() float64 {
timeSinceAccess := time.Since(fi.ATime).Hours()
decayFactor := 1.0 / (1.0 + timeSinceAccess/24.0) // Decay over days
frequencyBonus := float64(fi.AccessCount) * 0.1
return decayFactor + frequencyBonus
}
+18 -38
View File
@@ -1,46 +1,26 @@
// vfs/vfs.go
package vfs package vfs
import ( // VFS is the interface that wraps the basic methods of a virtual file system.
"io"
"s1d3sw1ped/SteamCache2/vfs/types"
)
// VFS defines the interface for virtual file systems
type VFS interface { type VFS interface {
// Create creates a new file at the given key // Name returns the name of the file system.
Create(key string, size int64) (io.WriteCloser, error)
// Open opens the file at the given key for reading
Open(key string) (io.ReadCloser, error)
// Delete removes the file at the given key
Delete(key string) error
// Stat returns information about the file at the given key
Stat(key string) (*types.FileInfo, error)
// Name returns the name of this VFS
Name() string Name() string
// Size returns the current size of the VFS // Size returns the total size of all files in the file system.
Size() int64 Size() int64
// Capacity returns the maximum capacity of the VFS // Set sets the value of key as src.
Capacity() int64 // Setting the same key multiple times, the last set call takes effect.
Set(key string, src []byte) error
// Delete deletes the value of key.
Delete(key string) error
// Get gets the value of key to dst, and returns dst no matter whether or not there is an error.
Get(key string) ([]byte, error)
// Stat returns the FileInfo of key.
Stat(key string) (*FileInfo, error)
// StatAll returns the FileInfo of all keys.
StatAll() []*FileInfo
} }
// FileInfo is an alias for types.FileInfo for backward compatibility
type FileInfo = types.FileInfo
// NewFileInfo is an alias for types.NewFileInfo for backward compatibility
var NewFileInfo = types.NewFileInfo
// NewFileInfoFromOS is an alias for types.NewFileInfoFromOS for backward compatibility
var NewFileInfoFromOS = types.NewFileInfoFromOS
// BatchedTimeUpdate is an alias for types.BatchedTimeUpdate for backward compatibility
type BatchedTimeUpdate = types.BatchedTimeUpdate
// NewBatchedTimeUpdate is an alias for types.NewBatchedTimeUpdate for backward compatibility
var NewBatchedTimeUpdate = types.NewBatchedTimeUpdate
+7 -5
View File
@@ -1,12 +1,14 @@
// vfs/vfserror/vfserror.go
package vfserror package vfserror
import "errors" import "errors"
// Common VFS errors
var ( var (
// ErrUnreachable is returned when a code path is unreachable.
ErrUnreachable = errors.New("unreachable")
// ErrNotFound is returned when a key is not found.
ErrNotFound = errors.New("vfs: key not found") ErrNotFound = errors.New("vfs: key not found")
ErrInvalidKey = errors.New("vfs: invalid key")
ErrAlreadyExists = errors.New("vfs: key already exists") // ErrDiskFull is returned when the disk is full.
ErrCapacityExceeded = errors.New("vfs: capacity exceeded") ErrDiskFull = errors.New("vfs: disk full")
) )
-300
View File
@@ -1,300 +0,0 @@
package warming
import (
"context"
"s1d3sw1ped/SteamCache2/vfs"
"sync"
"sync/atomic"
"time"
)
// CacheWarmer implements intelligent cache warming strategies
type CacheWarmer struct {
vfs vfs.VFS
warmingQueue chan WarmRequest
activeWarmers map[string]*ActiveWarmer
stats *WarmingStats
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
mu sync.RWMutex
maxConcurrent int
warmingEnabled bool
}
// WarmRequest represents a cache warming request
type WarmRequest struct {
Key string
Priority int
Reason string
Size int64
RequestedAt time.Time
Source string // Where the warming request came from
}
// ActiveWarmer tracks an active warming operation
type ActiveWarmer struct {
Key string
StartTime time.Time
Priority int
Reason string
mu sync.RWMutex
}
// WarmingStats tracks cache warming statistics
type WarmingStats struct {
WarmRequests int64
WarmSuccesses int64
WarmFailures int64
WarmBytes int64
WarmDuration time.Duration
ActiveWarmers int64
mu sync.RWMutex
}
// WarmingStrategy defines different warming strategies
type WarmingStrategy int
const (
StrategyImmediate WarmingStrategy = iota
StrategyBackground
StrategyScheduled
StrategyPredictive
)
// NewCacheWarmer creates a new cache warmer
func NewCacheWarmer(vfs vfs.VFS, maxConcurrent int) *CacheWarmer {
ctx, cancel := context.WithCancel(context.Background())
cw := &CacheWarmer{
vfs: vfs,
warmingQueue: make(chan WarmRequest, 1000),
activeWarmers: make(map[string]*ActiveWarmer),
stats: &WarmingStats{},
ctx: ctx,
cancel: cancel,
maxConcurrent: maxConcurrent,
warmingEnabled: true,
}
// Start warming workers
for i := 0; i < maxConcurrent; i++ {
cw.wg.Add(1)
go cw.warmingWorker(i)
}
// Start cleanup worker
cw.wg.Add(1)
go cw.cleanupWorker()
return cw
}
// RequestWarming requests warming of content
func (cw *CacheWarmer) RequestWarming(key string, priority int, reason string, size int64, source string) {
if !cw.warmingEnabled {
return
}
// Check if already warming
cw.mu.RLock()
if _, exists := cw.activeWarmers[key]; exists {
cw.mu.RUnlock()
return // Already warming
}
cw.mu.RUnlock()
// Check if already cached
if _, err := cw.vfs.Stat(key); err == nil {
return // Already cached
}
select {
case cw.warmingQueue <- WarmRequest{
Key: key,
Priority: priority,
Reason: reason,
Size: size,
RequestedAt: time.Now(),
Source: source,
}:
atomic.AddInt64(&cw.stats.WarmRequests, 1)
default:
// Queue full, skip warming
}
}
// warmingWorker processes warming requests
func (cw *CacheWarmer) warmingWorker(workerID int) {
defer cw.wg.Done()
for {
select {
case <-cw.ctx.Done():
return
case req := <-cw.warmingQueue:
cw.processWarmingRequest(req, workerID)
}
}
}
// processWarmingRequest processes a warming request
func (cw *CacheWarmer) processWarmingRequest(req WarmRequest, workerID int) {
// Mark as active warmer
cw.mu.Lock()
cw.activeWarmers[req.Key] = &ActiveWarmer{
Key: req.Key,
StartTime: time.Now(),
Priority: req.Priority,
Reason: req.Reason,
}
cw.mu.Unlock()
atomic.AddInt64(&cw.stats.ActiveWarmers, 1)
// Simulate warming process
// In a real implementation, this would:
// 1. Fetch content from upstream
// 2. Store in cache
// 3. Update statistics
startTime := time.Now()
// Simulate warming delay based on priority
warmingDelay := time.Duration(100-req.Priority*10) * time.Millisecond
if warmingDelay < 10*time.Millisecond {
warmingDelay = 10 * time.Millisecond
}
select {
case <-time.After(warmingDelay):
// Warming completed successfully
atomic.AddInt64(&cw.stats.WarmSuccesses, 1)
atomic.AddInt64(&cw.stats.WarmBytes, req.Size)
case <-cw.ctx.Done():
// Context cancelled
atomic.AddInt64(&cw.stats.WarmFailures, 1)
}
duration := time.Since(startTime)
cw.stats.mu.Lock()
cw.stats.WarmDuration += duration
cw.stats.mu.Unlock()
// Remove from active warmers
cw.mu.Lock()
delete(cw.activeWarmers, req.Key)
cw.mu.Unlock()
atomic.AddInt64(&cw.stats.ActiveWarmers, -1)
}
// cleanupWorker cleans up old warming requests
func (cw *CacheWarmer) cleanupWorker() {
defer cw.wg.Done()
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-cw.ctx.Done():
return
case <-ticker.C:
cw.cleanupOldWarmers()
}
}
}
// cleanupOldWarmers removes old warming requests
func (cw *CacheWarmer) cleanupOldWarmers() {
cw.mu.Lock()
defer cw.mu.Unlock()
now := time.Now()
cutoff := now.Add(-5 * time.Minute) // Remove warmers older than 5 minutes
for key, warmer := range cw.activeWarmers {
warmer.mu.RLock()
if warmer.StartTime.Before(cutoff) {
warmer.mu.RUnlock()
delete(cw.activeWarmers, key)
atomic.AddInt64(&cw.stats.WarmFailures, 1)
} else {
warmer.mu.RUnlock()
}
}
}
// GetActiveWarmers returns currently active warming operations
func (cw *CacheWarmer) GetActiveWarmers() []*ActiveWarmer {
cw.mu.RLock()
defer cw.mu.RUnlock()
warmers := make([]*ActiveWarmer, 0, len(cw.activeWarmers))
for _, warmer := range cw.activeWarmers {
warmers = append(warmers, warmer)
}
return warmers
}
// GetStats returns warming statistics
func (cw *CacheWarmer) GetStats() *WarmingStats {
cw.stats.mu.RLock()
defer cw.stats.mu.RUnlock()
return &WarmingStats{
WarmRequests: atomic.LoadInt64(&cw.stats.WarmRequests),
WarmSuccesses: atomic.LoadInt64(&cw.stats.WarmSuccesses),
WarmFailures: atomic.LoadInt64(&cw.stats.WarmFailures),
WarmBytes: atomic.LoadInt64(&cw.stats.WarmBytes),
WarmDuration: cw.stats.WarmDuration,
ActiveWarmers: atomic.LoadInt64(&cw.stats.ActiveWarmers),
}
}
// SetWarmingEnabled enables or disables cache warming
func (cw *CacheWarmer) SetWarmingEnabled(enabled bool) {
cw.mu.Lock()
defer cw.mu.Unlock()
cw.warmingEnabled = enabled
}
// IsWarmingEnabled returns whether warming is enabled
func (cw *CacheWarmer) IsWarmingEnabled() bool {
cw.mu.RLock()
defer cw.mu.RUnlock()
return cw.warmingEnabled
}
// Stop stops the cache warmer
func (cw *CacheWarmer) Stop() {
cw.cancel()
cw.wg.Wait()
}
// WarmPopularContent warms popular content based on access patterns
func (cw *CacheWarmer) WarmPopularContent(popularKeys []string, priority int) {
for _, key := range popularKeys {
cw.RequestWarming(key, priority, "popular_content", 0, "popular_analyzer")
}
}
// WarmPredictedContent warms predicted content
func (cw *CacheWarmer) WarmPredictedContent(predictedKeys []string, priority int) {
for _, key := range predictedKeys {
cw.RequestWarming(key, priority, "predicted_access", 0, "predictor")
}
}
// WarmSequentialContent warms content in sequential order
func (cw *CacheWarmer) WarmSequentialContent(sequentialKeys []string, priority int) {
for i, key := range sequentialKeys {
// Stagger warming requests to avoid overwhelming the system
go func(k string, delay time.Duration) {
time.Sleep(delay)
cw.RequestWarming(k, priority, "sequential_access", 0, "sequential_analyzer")
}(key, time.Duration(i)*100*time.Millisecond)
}
}