Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

119 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"os"
"path/filepath"
"time"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/platform"
)
// Guarded one-time store compaction at daemon boot.
//
// Row-shedding maintenance (orphan-repo purges, the duplicate-collapse
// migration, resolver cleanups) returns pages to SQLite's freelist, where new
// writes reuse them — but only VACUUM returns them to the filesystem, and
// nothing ran it: a live store carried 4.4 GB of freelist inside a 6.8 GB
// file. This is not a correctness problem (freelist pages are fully
// reusable), so the trigger is deliberately conservative: compaction runs
// only when the dead fraction dominates the file AND is large in absolute
// terms AND the filesystem can absorb VACUUM's temporary full copy. Anything
// smaller is left to organic reuse.
// storeCompactor is the optional store capability the boot-time compaction
// probes for — implemented by the on-disk backend only, so a memory-mode
// daemon skips the whole feature via the failed type assertion.
type storeCompactor interface {
CompactStats() (freeBytes, totalBytes int64)
Compact() error
Path() string
}
const (
// compactMinFreeBytes is the absolute floor: below 1 GiB of reclaimable
// space a VACUUM (minutes of exclusive I/O on a store this size) costs
// more than the disk it returns.
compactMinFreeBytes = int64(1) << 30
)
// shouldCompactStore is the pure trigger predicate: compact only when the
// freelist is BOTH the majority of the file (> 50% of pages — organic reuse
// would take a long time to grow back into that much dead space) AND large in
// absolute terms (> 1 GiB — a small file's majority is still cheap to leave
// alone), AND the filesystem has room for VACUUM's transient full copy
// (available > 1.5 × the current file, headroom over the worst case of the
// rewrite temporarily doubling the footprint). Pure so the policy is
// table-testable without a store or a filesystem.
func shouldCompactStore(freeBytes, totalBytes int64, diskAvailBytes uint64) bool {
if totalBytes <= 0 || freeBytes <= 0 {
return false
}
if freeBytes*2 <= totalBytes { // freelist must exceed half the pages
return false
}
if freeBytes <= compactMinFreeBytes {
return false
}
need := uint64(totalBytes) + uint64(totalBytes)/2 // total × 1.5
return diskAvailBytes > need
}
// maybeCompactStore probes the store for the compaction capability and runs
// a one-time VACUUM when shouldCompactStore says the file is mostly dead
// space. Called from warmupDaemonState right after the orphan-prefix purge
// (so the purge's freshly-freed pages are measured and reclaimed in the same
// pass) and before the warmup re-index loop (whose writes would start
// reusing freelist pages and whose readers would contend with VACUUM's
// exclusive lock). Every failure path degrades to "skip": freelist pages
// stay reusable, so a missed compaction costs disk, never correctness.
func maybeCompactStore(g graph.Store, logger *zap.Logger) {
if os.Getenv("GORTEX_SKIP_STORE_COMPACT") == "1" {
logger.Debug("daemon: store compaction disabled (GORTEX_SKIP_STORE_COMPACT=1)")
return
}
c, ok := g.(storeCompactor)
if !ok {
return // memory-mode store: nothing on disk to compact
}
path := c.Path()
if path == "" {
return // no file backing — no filesystem to reason about
}
free, total := c.CompactStats()
avail, err := platform.DiskAvailBytes(filepath.Dir(path))
if err != nil {
// Unknown headroom means the ×1.5 guard cannot hold; VACUUM without it
// risks filling the volume mid-rewrite, so skip.
logger.Debug("daemon: store compaction skipped — disk headroom unknown",
zap.String("path", path), zap.Error(err))
return
}
if !shouldCompactStore(free, total, avail) {
logger.Debug("daemon: store compaction not warranted",
zap.Int64("reclaimable_bytes", free),
zap.Int64("store_bytes", total),
zap.Uint64("disk_avail_bytes", avail))
return
}
logger.Info("daemon: one-time store compaction starting — VACUUM may take minutes on a multi-GB store",
zap.Int64("reclaimable_bytes", free),
zap.Int64("store_bytes", total),
zap.Uint64("disk_avail_bytes", avail))
start := time.Now()
if err := c.Compact(); err != nil {
// Non-fatal by design: a failed VACUUM leaves the store exactly as it
// was (the freelist remains reusable), so boot continues.
logger.Warn("daemon: store compaction failed — continuing boot",
zap.Duration("elapsed", time.Since(start)), zap.Error(err))
return
}
_, after := c.CompactStats()
logger.Info("daemon: store compaction finished",
zap.Duration("elapsed", time.Since(start)),
zap.Int64("store_bytes", after),
zap.Int64("reclaimed_bytes", total-after))
}