Files
steamcache2/steamcache/negative_cache_test.go
T
pike b7710de0ca
CI / vulncheck (pull_request) Successful in 20s
CI / check-and-test (pull_request) Failing after 21s
cache: Per-client fair-share bandwidth on table uplink
2026-09-09 20:22:23 +00:00

223 lines
6.7 KiB
Go

package steamcache
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
)
func steamGet(t *testing.T, sc *SteamCache, path string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("User-Agent", "Valve/Steam HTTP Client 1.0")
rec := httptest.NewRecorder()
sc.ServeHTTP(rec, req)
return rec
}
func TestNegativeCache404(t *testing.T) {
var upstreamHits atomic.Int64
f := func(w http.ResponseWriter, r *http.Request) {
upstreamHits.Add(1)
http.Error(w, "not found", http.StatusNotFound)
}
sc, _ := newTestCacheWithFakeUpstream(t, f, "1MB", "0")
sc.ResetMetrics()
rec1 := steamGet(t, sc, "/depot/gone/chunk")
if rec1.Code != http.StatusNotFound {
t.Fatalf("first request: expected 404, got %d body=%q", rec1.Code, rec1.Body.String())
}
if n := upstreamHits.Load(); n != 1 {
t.Fatalf("first request: expected 1 upstream hit, got %d", n)
}
rec2 := steamGet(t, sc, "/depot/gone/chunk")
if rec2.Code != http.StatusNotFound {
t.Fatalf("second request: expected 404, got %d", rec2.Code)
}
if n := upstreamHits.Load(); n != 1 {
t.Fatalf("repeated miss must not re-hit upstream within TTL, got %d", n)
}
stats := sc.GetMetrics()
if stats.NegativeCacheHits < 1 {
t.Errorf("expected NegativeCacheHits >= 1, got %d", stats.NegativeCacheHits)
}
if stats.CacheHits < 1 {
t.Errorf("negative hit should also count as cache_hits, got %d", stats.CacheHits)
}
if stats.UpstreamErrors != 1 {
t.Errorf("first 404 should count upstream error once, got %d", stats.UpstreamErrors)
}
mrec := httptest.NewRecorder()
sc.ServeHTTP(mrec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
if !bytes.Contains(mrec.Body.Bytes(), []byte("negative_cache_hits")) {
t.Errorf("/metrics missing negative_cache_hits:\n%s", mrec.Body.String())
}
if ct := mrec.Header().Get("Content-Type"); ct != "text/plain; version=0.0.4; charset=utf-8" {
t.Errorf("/metrics Content-Type=%q, want Prometheus text 0.0.4", ct)
}
}
func TestNegativeCache410(t *testing.T) {
var upstreamHits atomic.Int64
f := func(w http.ResponseWriter, r *http.Request) {
upstreamHits.Add(1)
w.WriteHeader(http.StatusGone)
}
sc, _ := newTestCacheWithFakeUpstream(t, f, "1MB", "0")
rec1 := steamGet(t, sc, "/depot/gone410/chunk")
if rec1.Code != http.StatusGone {
t.Fatalf("first request: expected 410, got %d", rec1.Code)
}
rec2 := steamGet(t, sc, "/depot/gone410/chunk")
if rec2.Code != http.StatusGone {
t.Fatalf("second request: expected 410, got %d", rec2.Code)
}
if n := upstreamHits.Load(); n != 1 {
t.Fatalf("410 negative cache should suppress second upstream hit, got %d", n)
}
if sc.GetMetrics().NegativeCacheHits < 1 {
t.Errorf("expected NegativeCacheHits >= 1 after cached 410, got %d", sc.GetMetrics().NegativeCacheHits)
}
}
func TestNegativeCacheExpiredRefetch(t *testing.T) {
var upstreamHits atomic.Int64
f := func(w http.ResponseWriter, r *http.Request) {
upstreamHits.Add(1)
http.Error(w, "not found", http.StatusNotFound)
}
s := httptest.NewServer(http.HandlerFunc(f))
t.Cleanup(s.Close)
sc, err := NewWithOptions(Options{
Address: "127.0.0.1:0",
MemorySize: "1MB",
DiskSize: "0",
DiskPath: t.TempDir(),
Upstream: s.URL,
MemoryGC: "lru",
DiskGC: "lru",
MaxConcurrentRequests: 10,
MaxRequestsPerClient: 5,
MaxObjectSize: "0",
NegativeTTL: "1s",
})
if err != nil {
t.Fatalf("NewWithOptions: %v", err)
}
t.Cleanup(func() { sc.Shutdown() })
if rec := steamGet(t, sc, "/depot/ttl/chunk"); rec.Code != http.StatusNotFound {
t.Fatalf("first request: expected 404, got %d", rec.Code)
}
if n := upstreamHits.Load(); n != 1 {
t.Fatalf("first request: expected 1 upstream hit, got %d", n)
}
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
time.Sleep(50 * time.Millisecond)
rec := steamGet(t, sc, "/depot/ttl/chunk")
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404 after expiry poll, got %d", rec.Code)
}
if upstreamHits.Load() >= 2 {
return
}
}
t.Fatalf("expired negative entry did not re-fetch upstream; hits=%d", upstreamHits.Load())
}
func TestSerializeNegativeHeader(t *testing.T) {
raw := []byte("HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n\r\ngone")
pos, err := serializeRawResponse(raw)
if err != nil {
t.Fatalf("serialize positive: %v", err)
}
posFile, err := deserializeCacheFile(pos)
if err != nil {
t.Fatalf("deserialize positive: %v", err)
}
if posFile.ExpiresUnix != 0 {
t.Errorf("positive entry ExpiresUnix=%d, want 0", posFile.ExpiresUnix)
}
expires := time.Now().Add(5 * time.Minute).Unix()
neg, err := serializeCacheFile(raw, expires)
if err != nil {
t.Fatalf("serialize negative: %v", err)
}
negFile, err := deserializeCacheFile(neg)
if err != nil {
t.Fatalf("deserialize negative: %v", err)
}
if negFile.ExpiresUnix != expires {
t.Errorf("ExpiresUnix=%d, want %d", negFile.ExpiresUnix, expires)
}
if !bytes.Equal(negFile.Response, raw) {
t.Error("negative raw response not preserved")
}
}
func TestNewInvalidNegativeTTL(t *testing.T) {
sc, err := New("127.0.0.1:0", "1MB", "0", t.TempDir(), "", "lru", "lru", 10, 5, "0", nil, "not-a-duration", "", 0)
if err == nil {
if sc != nil {
sc.Shutdown()
}
t.Fatal("expected error for invalid negative ttl")
}
if sc != nil {
t.Error("expected nil SteamCache on invalid negative ttl")
}
if !strings.Contains(err.Error(), "invalid negative ttl") {
t.Errorf("err %q missing invalid negative ttl", err)
}
}
func TestWriteTextNegativeCacheHits(t *testing.T) {
body := []byte("ok")
f := func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(body)
}
sc, _ := newTestCacheWithFakeUpstream(t, f, "1MB", "0")
srv := newCacheServer(t, sc)
c := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest(http.MethodGet, srv.URL+"/metrics", nil)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
resp, err := c.Do(req)
if err != nil {
t.Fatalf("GET /metrics: %v", err)
}
out, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
t.Fatalf("read /metrics: %v", err)
}
if !bytes.Contains(out, []byte("negative_cache_hits 0\n")) {
t.Errorf("/metrics missing negative_cache_hits 0:\n%s", out)
}
if ct := resp.Header.Get("Content-Type"); ct != "text/plain; version=0.0.4; charset=utf-8" {
t.Errorf("/metrics Content-Type=%q, want Prometheus text 0.0.4", ct)
}
if !bytes.Contains(out, []byte("# HELP negative_cache_hits ")) {
t.Errorf("/metrics missing # HELP negative_cache_hits:\n%s", out)
}
if !bytes.Contains(out, []byte("# TYPE negative_cache_hits counter")) {
t.Errorf("/metrics missing # TYPE negative_cache_hits counter:\n%s", out)
}
}