check / check (push) Failing after 2s
The default limit was 75% of the space free at startup. The cache's own files are not free space, so a fuller cache got a smaller limit after a restart and eviction then deleted most of it. The default is now 75% of the sum of the free space and what the cache already holds by its own size accounting, at least 500 MiB. The cache works it out when it opens, after the database is open, so the computation and its tests moved from internal/config to internal/imgcache; the config only records whether cache_max_bytes was set. Model: opus-5-5
179 lines
5.2 KiB
Go
179 lines
5.2 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// errTestStatfsFailed is returned by the failing free-space probe below.
|
|
var errTestStatfsFailed = errors.New("statfs failed")
|
|
|
|
// 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)
|
|
}
|
|
|
|
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, content_type)
|
|
VALUES (?, ?, ?)`,
|
|
string(testVariantKeyOne), 3221225472, testContentTypeWebP,
|
|
)
|
|
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)
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
// TestComputeDefaultMaxBytesProbesTheCacheDirectory verifies that the
|
|
// probe is pointed at <state_dir>/cache/ and that the directory exists
|
|
// by then, so statfs measures the filesystem that holds the cache.
|
|
func TestComputeDefaultMaxBytesProbesTheCacheDirectory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, stateDir := newEvictionTestCache(t, 1<<30)
|
|
wantCacheDir := filepath.Join(stateDir, "cache")
|
|
|
|
var probedPath string
|
|
|
|
_, err := cache.computeDefaultMaxBytes(t.Context(),
|
|
func(path string) (uint64, error) {
|
|
probedPath = path
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil || !info.IsDir() {
|
|
t.Errorf("cache directory %q missing when probed: info=%v err=%v",
|
|
path, info, err)
|
|
}
|
|
|
|
return 4294967296, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("computeDefaultMaxBytes returned error: %v", err)
|
|
}
|
|
|
|
if probedPath != wantCacheDir {
|
|
t.Errorf("free space probed at %q, want cache directory %q",
|
|
probedPath, wantCacheDir)
|
|
}
|
|
}
|
|
|
|
// TestNewCacheUsesDefaultMaxBytesOnlyWhenAsked verifies that NewCache
|
|
// replaces MaxBytes with the computed default when UseDefaultMaxBytes
|
|
// is set, and keeps MaxBytes as given otherwise.
|
|
func TestNewCacheUsesDefaultMaxBytesOnlyWhenAsked(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
withDefault, err := NewCache(evictionTestDB(t), CacheConfig{
|
|
StateDir: t.TempDir(),
|
|
MaxBytes: 1024,
|
|
UseDefaultMaxBytes: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewCache with UseDefaultMaxBytes failed: %v", err)
|
|
}
|
|
|
|
if withDefault.config.MaxBytes < DefaultCacheMaxBytesFloor {
|
|
t.Errorf("MaxBytes with UseDefaultMaxBytes = %d, want the default, "+
|
|
"at least %d", withDefault.config.MaxBytes, DefaultCacheMaxBytesFloor)
|
|
}
|
|
|
|
explicit, err := NewCache(evictionTestDB(t), CacheConfig{
|
|
StateDir: t.TempDir(),
|
|
MaxBytes: 1024,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewCache failed: %v", err)
|
|
}
|
|
|
|
if explicit.config.MaxBytes != 1024 {
|
|
t.Errorf("MaxBytes without UseDefaultMaxBytes = %d, want 1024",
|
|
explicit.config.MaxBytes)
|
|
}
|
|
}
|