ops: Signal disk-tier attach pending vs ready
CI / vulncheck (pull_request) Successful in 14s
CI / check-and-test (pull_request) Successful in 40s

Large disk caches can look memory-only/broken during async DiskFS attach.
Expose disk_tier_ready in /metrics, pending/ready logs for disk-only and
mixed modes, and X-SteamCache-Disk-Tier on /lancache-heartbeat. GetMetrics
skips blocking disk.Size() while attach is pending so /metrics stays usable.

Closes #33
This commit is contained in:
2026-09-07 16:58:13 +00:00
parent c43bfba568
commit 12ea3ee4f6
5 changed files with 134 additions and 4 deletions
+91
View File
@@ -1057,6 +1057,12 @@ func TestDiskOnlyDelayedAttach(t *testing.T) {
t.Errorf("during init window, expected ErrNotFound from disk-only tiered Create (no slow), got %v", err)
}
// Disk tier is pending while the attach goroutine is in the Size barrier.
// GetMetrics must return quickly (it skips disk.Size() while pending) and report 0.
if got := sc.GetMetrics().DiskTierReady; got != 0 {
t.Errorf("during pending attach, DiskTierReady=%d, want 0", got)
}
// Wait the barrier (exercises the attach go's Size wait)
_ = sc.disk.Size()
@@ -1083,6 +1089,91 @@ func TestDiskOnlyDelayedAttach(t *testing.T) {
} else {
rc.Close()
}
// After attach, the disk tier must be marked ready (1)
if got := sc.GetMetrics().DiskTierReady; got != 1 {
t.Errorf("post-attach DiskTierReady=%d, want 1 (ready)", got)
}
// /metrics text output includes the disk_tier_ready line
rec := httptest.NewRecorder()
metrics.WriteText(rec, sc.GetMetrics())
if !bytes.Contains(rec.Body.Bytes(), []byte("disk_tier_ready 1")) {
t.Errorf("WriteText output missing \"disk_tier_ready 1\": %q", rec.Body.String())
}
}
// TestDiskTierSignalMemoryOnly covers memory-only mode: DiskTierReady=1 (N/A, not
// waiting on disk attach) and heartbeat header X-SteamCache-Disk-Tier: disabled.
func TestDiskTierSignalMemoryOnly(t *testing.T) {
sc, err := New("127.0.0.1:0", "1MB", "0", t.TempDir(), "", "lru", "lru", 10, 5, "0", nil)
if err != nil {
t.Fatalf("New memory-only: %v", err)
}
t.Cleanup(func() { sc.Shutdown() })
if got := sc.GetMetrics().DiskTierReady; got != 1 {
t.Errorf("DiskTierReady=%d, want 1 (memory-only = N/A/not pending)", got)
}
req := httptest.NewRequest("GET", "/lancache-heartbeat", nil)
rec := httptest.NewRecorder()
sc.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Errorf("heartbeat status=%d, want 204", rec.Code)
}
if got := rec.Header().Get("X-SteamCache-Disk-Tier"); got != "disabled" {
t.Errorf("X-SteamCache-Disk-Tier=%q, want disabled", got)
}
if got := rec.Header().Get("X-LanCache-Processed-By"); got != "SteamCache2" {
t.Errorf("X-LanCache-Processed-By=%q, want SteamCache2", got)
}
}
// TestDiskTierSignalMixedPendingReady covers mixed mode: DiskTierReady=0 (header
// pending) while the disk attach is in the Size barrier, then DiskTierReady=1
// (header ready) after the barrier opens and the attach goroutine sets SetSlow.
func TestDiskTierSignalMixedPendingReady(t *testing.T) {
td := t.TempDir()
diskPath := filepath.Join(td, "disk")
if err := os.MkdirAll(diskPath, 0755); err != nil {
t.Fatal(err)
}
sc, err := New("127.0.0.1:0", "1MB", "10MB", diskPath, "", "lru", "lru", 10, 1, "0", nil)
if err != nil {
t.Fatalf("New mixed: %v", err)
}
t.Cleanup(func() { sc.Shutdown() })
// Immediately in the pending window
if got := sc.GetMetrics().DiskTierReady; got != 0 {
t.Errorf("immediate DiskTierReady=%d, want 0 (pending)", got)
}
req := httptest.NewRequest("GET", "/lancache-heartbeat", nil)
rec := httptest.NewRecorder()
sc.ServeHTTP(rec, req)
if got := rec.Header().Get("X-SteamCache-Disk-Tier"); got != "pending" {
t.Errorf("heartbeat header=%q, want pending", got)
}
// Wait the barrier, then retry until the attach goroutine flips the flag
_ = sc.disk.Size()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if sc.GetMetrics().DiskTierReady == 1 {
break
}
time.Sleep(1 * time.Millisecond)
}
if got := sc.GetMetrics().DiskTierReady; got != 1 {
t.Fatalf("DiskTierReady=%d after barrier, want 1 (ready)", got)
}
rec2 := httptest.NewRecorder()
sc.ServeHTTP(rec2, httptest.NewRequest("GET", "/lancache-heartbeat", nil))
if got := rec2.Header().Get("X-SteamCache-Disk-Tier"); got != "ready" {
t.Errorf("heartbeat header=%q, want ready", got)
}
}
// --- Phase 2: narrow black-box tests for the new wrapper types ---