cache: Serve Range GET from disk when present
CI / vulncheck (pull_request) Successful in 15s
CI / check-and-test (pull_request) Successful in 41s

Steam clients lean on Range requests. Hit/miss looked fine while downloads
still felt cold when a Range miss returned the full upstream body as 200.

Add range_cache / range_upstream metrics. HIT path already sliced 206 from
the cached full object; count range_cache there. On MISS, keep stripping
Range for the upstream fetch (full object still cached) but serve the
client's requested slice as 206 and count range_upstream.

Tests in steamcache/range_test.go cover Range HIT (local, no upstream),
Range MISS (206 + full object cached), and /metrics emission.
This commit is contained in:
2026-09-07 16:32:36 +00:00
parent c43bfba568
commit ac2d36f1ad
5 changed files with 456 additions and 7 deletions
+38 -6
View File
@@ -109,8 +109,14 @@ func (sc *SteamCache) handleCacheHit(w http.ResponseWriter, r *http.Request, cac
// Track cache hit metrics
sc.metrics.IncrementCacheHits()
sc.metrics.AddResponseTime(time.Since(tstart))
sc.metrics.AddBytesServed(int64(len(cachedData)))
sc.metrics.AddBytesSaved(int64(len(cachedData)))
if r.Header.Get("Range") == "" {
// Full-object HIT: count the cached blob served. Range HITs skip
// this here — streamCachedResponse counts the range bytes actually
// written to the client instead, so BytesServed/Saved reflect the
// partial body and are not double-counted.
sc.metrics.AddBytesServed(int64(len(cachedData)))
sc.metrics.AddBytesSaved(int64(len(cachedData)))
}
sc.metrics.IncrementServiceRequests(service.Name)
logger.Logger.Debug().
@@ -549,14 +555,40 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-LanCache-Status", "MISS")
w.Header().Set("X-LanCache-Processed-By", "SteamCache2")
// Stream the response body to client
w.WriteHeader(resp.StatusCode)
_, _ = w.Write(bodyData) // client write error ignored (disconnect during MISS body send is not actionable)
// Stream the response body to client.
// Range miss: the Range header was stripped for the upstream fetch (so the
// FULL object is cached below); serve the client's requested slice from the
// full body as 206, matching the HIT Range path.
if rangeHeader := r.Header.Get("Range"); rangeHeader != "" {
start, end, totalSize, rangeValid := parseRangeHeader(rangeHeader, int64(len(bodyData)))
if !rangeValid {
// Invalid range — 416 (consistent with the HIT Range path). Drop the
// upstream Content-Length: it describes the full body, which 416 does
// not send (a stale CL would hang clients waiting for a body).
w.Header().Del("Content-Length")
w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", len(bodyData)))
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
} else {
rangeData := bodyData[start : end+1]
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, totalSize))
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(rangeData)))
w.Header().Set("Accept-Ranges", "bytes")
w.WriteHeader(http.StatusPartialContent)
_, _ = w.Write(rangeData) // client write error ignored (disconnect during MISS range body send is not actionable)
// Range required an upstream fetch (full object) then served as 206
sc.metrics.IncrementRangeUpstream()
sc.metrics.AddBytesServed(int64(len(rangeData))) // range bytes only, not the full cached object
}
} else {
w.WriteHeader(resp.StatusCode)
_, _ = w.Write(bodyData) // client write error ignored (disconnect during MISS body send is not actionable)
sc.metrics.AddBytesServed(int64(len(bodyData)))
}
// Track cache miss metrics
sc.metrics.IncrementCacheMisses()
sc.metrics.AddResponseTime(time.Since(tstart))
sc.metrics.AddBytesServed(int64(len(bodyData)))
sc.metrics.IncrementServiceRequests(service.Name)
// Verify we received the complete file by checking Content-Length