All checks were successful
check / check (push) Successful in 4s
Canonical v2-schema `.golangci.yml`, golangci-lint pins bumped to v2.12.2 in `Dockerfile` and `script/bootstrap`, and the tree brought to `0 issues.` under it. Three behaviour deltas: `Cache.StoreVariant` takes a context (cancelled requests skip the accounting row, recovered by reconciliation); `MetadataStorage.Store` no longer leaks `.tmp-*.json` on Write/Close/Rename failure (dead-defer bug fix); the `signing_key` too-short error text gained a `value too short:` prefix. Eviction-loop context cancellation deferred to #102.
117 lines
3.6 KiB
Go
117 lines
3.6 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
)
|
|
|
|
// DefaultCacheMaxBytesFloor is the minimum computed default for the
|
|
// cache_max_bytes setting: 500 MiB. The floor applies only to the
|
|
// computed default (when the key is omitted from the configuration),
|
|
// 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.
|
|
type FreeSpaceProbeFunc func(path string) (uint64, error)
|
|
|
|
// defaultFreeSpaceProbe reports free filesystem bytes via statfs on
|
|
// the given path, as available to unprivileged processes.
|
|
func defaultFreeSpaceProbe(path string) (uint64, error) {
|
|
var stat syscall.Statfs_t
|
|
|
|
err := syscall.Statfs(path, &stat)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if stat.Bsize < 0 {
|
|
return 0, fmt.Errorf("%w %d for %q", errNegativeBlockSize, stat.Bsize, path)
|
|
}
|
|
|
|
blockSize := uint64(stat.Bsize)
|
|
|
|
return stat.Bavail * blockSize, nil
|
|
}
|
|
|
|
// 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(
|
|
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
|
|
computed = min(computed, math.MaxInt64)
|
|
|
|
// gosec cannot see that min() above bounds computed, so it reads
|
|
// this conversion as potentially overflowing. It cannot: computed is
|
|
// at most math.MaxInt64 on every path here.
|
|
//nolint:gosec // G115: clamped to MaxInt64 by min above
|
|
limit := int64(computed)
|
|
limit = max(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 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 {
|
|
if !c.cacheMaxBytesExplicit {
|
|
cacheDir := filepath.Join(c.StateDir, "cache")
|
|
|
|
err := os.MkdirAll(cacheDir, cacheDirPerms)
|
|
if err != nil {
|
|
return fmt.Errorf("config key %q: cannot create cache directory %q: %w",
|
|
keyCacheMaxBytes, 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.Info("effective cache size limit",
|
|
"cache_max_bytes", c.CacheMaxBytes,
|
|
"cache_disabled", c.CacheMaxBytes == 0,
|
|
)
|
|
|
|
return nil
|
|
}
|