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, and the handlers turn the disk cache off only for an explicit 0. Model: opus-5-5
90 lines
2.8 KiB
Go
90 lines
2.8 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"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
|
|
|
|
// freeSpaceFractionNumerator and freeSpaceFractionDenominator express
|
|
// the 75% share used for the computed default limit as integer
|
|
// arithmetic (dividing before multiplying avoids overflow).
|
|
const (
|
|
freeSpaceFractionNumerator uint64 = 3
|
|
freeSpaceFractionDenominator uint64 = 4
|
|
)
|
|
|
|
var errNegativeBlockSize = errors.New("statfs reported negative block size")
|
|
|
|
// 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
|
|
}
|
|
|
|
// computeDefaultMaxBytes returns the default cache size limit: 75% of
|
|
// the sum of the free bytes probe reports for <state_dir>/cache/ and
|
|
// the bytes the cache already holds, with a floor of
|
|
// DefaultCacheMaxBytesFloor. Counting what the cache holds keeps the
|
|
// limit from shrinking as the cache fills.
|
|
func (c *Cache) computeDefaultMaxBytes(
|
|
ctx context.Context, probe FreeSpaceProbeFunc,
|
|
) (int64, error) {
|
|
cacheDir := filepath.Join(c.config.StateDir, "cache")
|
|
|
|
freeBytes, err := probe(cacheDir)
|
|
if err != nil {
|
|
return 0, fmt.Errorf(
|
|
"default cache_max_bytes: cannot determine free space for %q: %w",
|
|
cacheDir, err)
|
|
}
|
|
|
|
usedBytes, err := c.UsageBytes(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Both terms are at most math.MaxInt64, so the sum cannot overflow.
|
|
//nolint:gosec // G115: UsageBytes sums file sizes, never negative
|
|
spaceBytes := min(freeBytes, math.MaxInt64) + uint64(usedBytes)
|
|
|
|
computed := spaceBytes / 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
|
|
}
|