check / check (push) Waiting to run
UsageBytes now reads the new cache_usage row, which triggers on source_content and variant_content keep up to date in the statement that adds, removes or resizes a row. The reconciliation pass reads both tables 1000 rows per query, sums them, and corrects the total when it differs, unless a row changed while it summed. Source rows now get last_accessed_at when added, so choosing source images to evict reads that column's index instead of sorting the whole table. Stats still sums the tables, now in pages: an existing test drops both tables and expects that sum to fail. 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 returns the total cache usage, 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
|
|
}
|