check / check (push) Canceled after 0s
The five counts the cache_stats table held are now Prometheus counters in memory, as fields of the cache. NewCache makes them and registers them on the registry it is given. pixad gives it Prometheus' default registry, which /metrics serves; each test gives it a new one. The Increment methods add to their cache's counters, so counting makes no database call. cache_stats, Cache.Stats, Service.Stats, CacheStats and the pending counts are gone, with their tests. The background goroutine now writes only access times, and its names say so: StartPendingAccessTimeWrites, StopPendingAccessTimeWrites and pendingaccesstimes.go. The two stop tests whose write under way was a count now use access times. Model: opus-5-5
190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
// Static errors returned by the stub free-space probes below.
|
|
var (
|
|
errTestStatfsFailed = errors.New("statfs failed")
|
|
errTestProbeNotExpected = errors.New("probe must not be called")
|
|
)
|
|
|
|
// TestComputeDefaultMaxBytesCountsWhatTheCacheHolds verifies that the
|
|
// default limit is 75% of the free space plus what the cache already
|
|
// holds, so a cache filled to its limit keeps that limit across a
|
|
// restart instead of shrinking to 75% of the space left free.
|
|
func TestComputeDefaultMaxBytesCountsWhatTheCacheHolds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
|
|
// Empty cache, 4 GiB free -> 3 GiB default.
|
|
got, err := cache.computeDefaultMaxBytes(t.Context(),
|
|
func(string) (uint64, error) { return 4294967296, nil })
|
|
if err != nil {
|
|
t.Fatalf("computeDefaultMaxBytes returned error: %v", err)
|
|
}
|
|
|
|
t.Logf("default for an empty cache with 4 GiB free: %d", got)
|
|
|
|
if got != 3221225472 {
|
|
t.Errorf("default for an empty cache = %d, want 3221225472 (75%% of 4 GiB)",
|
|
got)
|
|
}
|
|
|
|
// The cache now holds those 3 GiB, which leaves 1 GiB free.
|
|
_, err = cache.db.ExecContext(t.Context(),
|
|
`INSERT INTO variant_content (cache_key, size_bytes) VALUES (?, ?)`,
|
|
string(testVariantKeyOne), 3221225472,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert variant accounting row: %v", err)
|
|
}
|
|
|
|
got, err = cache.computeDefaultMaxBytes(t.Context(),
|
|
func(string) (uint64, error) { return 1073741824, nil })
|
|
if err != nil {
|
|
t.Fatalf("computeDefaultMaxBytes returned error: %v", err)
|
|
}
|
|
|
|
t.Logf("default for a cache holding 3 GiB with 1 GiB free: %d", got)
|
|
|
|
if got != 3221225472 {
|
|
t.Errorf("default for a cache holding 3 GiB with 1 GiB free = %d, "+
|
|
"want 3221225472 (75%% of 1 GiB + 3 GiB)", got)
|
|
}
|
|
}
|
|
|
|
// TestComputeDefaultMaxBytesAppliesFloor verifies that when 75% of the
|
|
// free space plus what the cache holds is below 500 MiB, the default
|
|
// is floored at DefaultCacheMaxBytesFloor.
|
|
func TestComputeDefaultMaxBytesAppliesFloor(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
freeBytes uint64
|
|
}{
|
|
{name: "100 MiB free", freeBytes: 104857600},
|
|
{name: "zero free", freeBytes: 0},
|
|
{name: "just below floor threshold", freeBytes: 699050665},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
|
|
got, err := cache.computeDefaultMaxBytes(t.Context(),
|
|
func(string) (uint64, error) { return tc.freeBytes, nil })
|
|
if err != nil {
|
|
t.Fatalf("computeDefaultMaxBytes returned error: %v", err)
|
|
}
|
|
|
|
if got != DefaultCacheMaxBytesFloor {
|
|
t.Errorf("computeDefaultMaxBytes = %d, want floor %d",
|
|
got, DefaultCacheMaxBytesFloor)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestComputeDefaultMaxBytesPropagatesProbeError verifies that a
|
|
// failing free-space probe produces an error naming cache_max_bytes,
|
|
// instead of a silently wrong default.
|
|
func TestComputeDefaultMaxBytesPropagatesProbeError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
|
|
_, err := cache.computeDefaultMaxBytes(t.Context(),
|
|
func(string) (uint64, error) { return 0, errTestStatfsFailed })
|
|
if err == nil {
|
|
t.Fatal("probe failure must produce an error, got nil")
|
|
}
|
|
|
|
t.Logf("got expected error: %v", err)
|
|
|
|
if !strings.Contains(err.Error(), "cache_max_bytes") {
|
|
t.Errorf("error %q does not name cache_max_bytes", err.Error())
|
|
}
|
|
}
|
|
|
|
// TestNewCacheComputesDefaultMaxBytesWhenAsked verifies that with
|
|
// UseDefaultMaxBytes set, the cache's limit becomes the computed
|
|
// default, and that the probe is pointed at <state_dir>/cache/, which
|
|
// must be created first so statfs measures the right filesystem.
|
|
func TestNewCacheComputesDefaultMaxBytesWhenAsked(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
stateDir := t.TempDir()
|
|
wantCacheDir := filepath.Join(stateDir, "cache")
|
|
|
|
var probedPath string
|
|
|
|
// 4 GiB free -> 3 GiB default.
|
|
probe := func(path string) (uint64, error) {
|
|
probedPath = path
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil || !info.IsDir() {
|
|
t.Errorf("cache directory %q was not created before probing: info=%v err=%v",
|
|
path, info, err)
|
|
}
|
|
|
|
return 4294967296, nil
|
|
}
|
|
|
|
cache, err := newCache(evictionTestDB(t), CacheConfig{
|
|
StateDir: stateDir,
|
|
UseDefaultMaxBytes: true,
|
|
}, prometheus.NewRegistry(), probe)
|
|
if err != nil {
|
|
t.Fatalf("newCache returned error: %v", err)
|
|
}
|
|
|
|
if cache.config.MaxBytes != 3221225472 {
|
|
t.Errorf("MaxBytes = %d, want computed default 3221225472",
|
|
cache.config.MaxBytes)
|
|
}
|
|
|
|
if probedPath != wantCacheDir {
|
|
t.Errorf("free space probed at %q, want cache directory %q",
|
|
probedPath, wantCacheDir)
|
|
}
|
|
}
|
|
|
|
// TestNewCacheKeepsExplicitMaxBytes verifies that without
|
|
// UseDefaultMaxBytes the cache keeps MaxBytes exactly as given and
|
|
// never consults the free-space probe.
|
|
func TestNewCacheKeepsExplicitMaxBytes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
probe := func(string) (uint64, error) {
|
|
t.Error("free-space probe must not be consulted for explicit values")
|
|
|
|
return 0, errTestProbeNotExpected
|
|
}
|
|
|
|
cache, err := newCache(evictionTestDB(t), CacheConfig{
|
|
StateDir: t.TempDir(),
|
|
MaxBytes: 1024,
|
|
}, prometheus.NewRegistry(), probe)
|
|
if err != nil {
|
|
t.Fatalf("newCache returned error: %v", err)
|
|
}
|
|
|
|
if cache.config.MaxBytes != 1024 {
|
|
t.Errorf("MaxBytes = %d, want explicit 1024 (no floor, no recompute)",
|
|
cache.config.MaxBytes)
|
|
}
|
|
}
|