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 if err := syscall.Statfs(path, &stat); err != nil { return 0, err } if stat.Bsize < 0 { return 0, fmt.Errorf("statfs reported negative block size %d for %q", stat.Bsize, path) } blockSize := uint64(stat.Bsize) //nolint:gosec // G115: negative Bsize rejected above 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 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 /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") 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.Info("effective cache size limit", "cache_max_bytes", c.CacheMaxBytes, "cache_disabled", c.CacheMaxBytes == 0, ) return nil }