feat: add cache_max_bytes config key with statfs-derived default

Strict int64 parsing via the startup validation framework: a SET but
invalid value (negative, float, null, non-numeric) aborts startup
naming the key and value. An omitted key resolves after state_dir
validation to max(75% of free bytes on the filesystem containing
<state_dir>/cache/, 500 MiB), measured via an injectable statfs probe;
the floor never applies to explicit values. Zero is valid and means
the disk cache is disabled. The effective limit is logged at startup.
This commit is contained in:
2026-08-07 21:02:08 +00:00
parent 3963ec31c1
commit 8cb09b6aaf
2 changed files with 145 additions and 10 deletions

View File

@@ -3,6 +3,8 @@ package config
import (
"fmt"
"log/slog"
"math"
"os"
"path/filepath"
"syscall"
)
@@ -13,6 +15,18 @@ import (
// never to explicitly configured values.
const DefaultCacheMaxBytesFloor int64 = 524288000
// cacheDirPerms is the permission mode for the cache directory created
// before probing free space, matching the state directory permissions.
const cacheDirPerms = 0o750
// freeSpaceFractionNumerator and freeSpaceFractionDenominator express
// the 75% share of free space used for the computed default limit as
// integer arithmetic (dividing before multiplying avoids overflow).
const (
freeSpaceFractionNumerator uint64 = 3
freeSpaceFractionDenominator uint64 = 4
)
// FreeSpaceProbeFunc reports the number of free bytes available on the
// filesystem containing path. It is a function type so tests can
// inject a fake probe instead of depending on the host disk.
@@ -38,23 +52,59 @@ func defaultFreeSpaceProbe(path string) (uint64, error) {
// ComputeDefaultCacheMaxBytes returns the default cache size limit for
// the filesystem containing cacheDir: 75% of the free bytes reported
// by probe, with a floor of DefaultCacheMaxBytesFloor.
func ComputeDefaultCacheMaxBytes(_ string, _ FreeSpaceProbeFunc) (int64, error) {
// Red phase: implementation follows the failing tests.
return 0, nil
func ComputeDefaultCacheMaxBytes(cacheDir string, probe FreeSpaceProbeFunc) (int64, error) {
freeBytes, err := probe(cacheDir)
if err != nil {
return 0, fmt.Errorf("config key %q: cannot determine free space for %q: %w",
"cache_max_bytes", cacheDir, err)
}
computed := freeBytes / freeSpaceFractionDenominator * freeSpaceFractionNumerator
if computed > math.MaxInt64 {
computed = math.MaxInt64
}
limit := int64(computed) //nolint:gosec // G115: clamped to MaxInt64 above
if limit < DefaultCacheMaxBytesFloor {
limit = DefaultCacheMaxBytesFloor
}
return limit, nil
}
// resolveCacheMaxBytes finalizes CacheMaxBytes after state_dir
// validation: an explicitly configured value is kept as-is (no floor
// applies), while an omitted key receives the computed default based
// on free space in <state_dir>/cache/. The effective limit is logged.
// on free space in <state_dir>/cache/. The cache directory is created
// first so statfs measures the filesystem that will actually hold the
// cache. The effective limit is logged either way.
func (c *Config) resolveCacheMaxBytes(log *slog.Logger, probe FreeSpaceProbeFunc) error {
// Red phase: implementation follows the failing tests.
limit, err := ComputeDefaultCacheMaxBytes(filepath.Join(c.StateDir, "cache"), probe)
if err != nil {
return err
if !c.cacheMaxBytesExplicit {
cacheDir := filepath.Join(c.StateDir, "cache")
if err := os.MkdirAll(cacheDir, cacheDirPerms); err != nil {
return fmt.Errorf("config key %q: cannot create cache directory %q: %w",
"cache_max_bytes", cacheDir, err)
}
limit, err := ComputeDefaultCacheMaxBytes(cacheDir, probe)
if err != nil {
return err
}
c.CacheMaxBytes = limit
log.Info("computed default cache size limit from free space",
"cache_max_bytes", limit,
"cache_dir", cacheDir,
)
}
log.Debug("cache size limit resolution not implemented", "computed", limit)
log.Info("effective cache size limit",
"cache_max_bytes", c.CacheMaxBytes,
"cache_disabled", c.CacheMaxBytes == 0,
)
return nil
}