cache: Short TTL negative cache for 404/410 depot objects
CI / vulncheck (pull_request) Successful in 14s
CI / check-and-test (pull_request) Successful in 41s

Stop re-fetching gone depot objects on every miss: store 404/410 in the
existing VFS cache under the same key with a short TTL (default 5m).
This commit is contained in:
2026-09-08 16:40:03 +00:00
parent 8eb31143e5
commit 036ea1ea7f
10 changed files with 550 additions and 89 deletions
+35 -2
View File
@@ -59,6 +59,10 @@ type SteamCache struct {
maxObjectSize int64
trustedProxies []string
// Negative TTL for 404/410 depot objects stored in the same VFS cache.
// Zero disables storing negatives (client still receives the upstream status).
negativeTTL time.Duration
// Service management
serviceManager *ServiceManager
@@ -71,14 +75,17 @@ type SteamCache struct {
processor *requestProcessor
}
// DefaultNegativeTTL is used when cache.negative_ttl / Options.NegativeTTL is empty.
const DefaultNegativeTTL = 5 * time.Minute
// New creates a new SteamCache instance.
// Returns an error (instead of panicking) on invalid memorySize or diskSize strings.
// Also validates maxObjectSize and accepts trustedProxies for X-Forwarded-For handling.
// Empty maxObjectSize or nil trustedProxies are normalized to safe defaults before parsing.
// negativeTTL is a Go duration string for 404/410 negative cache entries; empty means 5m.
// Callers must check the returned error.
// The two new positional parameters are a breaking change for direct importers of the simple constructor.
// Prefer NewWithOptions (or config file) for forward compatibility. See README migration notes.
func New(address string, memorySize string, diskSize string, diskPath, upstream, memoryGC, diskGC string, maxConcurrentRequests int64, maxRequestsPerClient int64, maxObjectSize string, trustedProxies []string) (*SteamCache, error) {
func New(address string, memorySize string, diskSize string, diskPath, upstream, memoryGC, diskGC string, maxConcurrentRequests int64, maxRequestsPerClient int64, maxObjectSize string, trustedProxies []string, negativeTTL string) (*SteamCache, error) {
memorysize, err := units.FromHumanSize(memorySize)
if err != nil {
return nil, fmt.Errorf("invalid memory size: %w", err)
@@ -102,6 +109,11 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
return nil, fmt.Errorf("invalid max object size: %w", err)
}
negTTL, err := parseNegativeTTL(negativeTTL)
if err != nil {
return nil, err
}
c := cache.New()
var m *memory.MemoryFS
@@ -171,6 +183,7 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
// Hardening config plumbed
maxObjectSize: maxObjBytes,
trustedProxies: trustedProxies,
negativeTTL: negTTL,
// Initialize service management
serviceManager: NewServiceManager(),
@@ -352,6 +365,26 @@ func (sc *SteamCache) ResetMetrics() {
sc.metrics.Reset()
}
// parseNegativeTTL parses a Go duration string for 404/410 negative cache entries.
// Empty means DefaultNegativeTTL. Zero disables storing negatives.
func parseNegativeTTL(s string) (time.Duration, error) {
if s == "" {
return DefaultNegativeTTL, nil
}
d, err := time.ParseDuration(s)
if err != nil {
return 0, fmt.Errorf("invalid negative ttl: %w", err)
}
if d < 0 {
return 0, fmt.Errorf("invalid negative ttl: negative duration")
}
return d, nil
}
func isDefinitiveGone(statusCode int) bool {
return statusCode == http.StatusNotFound || statusCode == http.StatusGone
}
// newHTTPTransport returns a tuned http.Transport for upstream fetches.
// Extracted to shrink New (Phase 3).
func newHTTPTransport() *http.Transport {