fixes for storage managament

This commit is contained in:
onysd 2026-09-03 08:33:06 +03:00
parent e6bfe2d444
commit 8ef2b58bf9
29 changed files with 1768 additions and 63 deletions

View file

@ -7,6 +7,8 @@ package hoststats
import (
"context"
"os"
"path/filepath"
"sync"
"time"
)
@ -14,12 +16,18 @@ import (
// Snapshot is the last successfully sampled host-resource reading. Ready is
// false until the first sample completes, so callers can distinguish "0% CPU"
// from "no data yet" instead of rendering a misleading zero on startup.
// DiskReady is a separate flag: disk stats are sampled from a configured
// path (see NewPoller) that can fail independently of CPU/memory sampling
// (wrong working directory, path not created yet, etc) -- without it, a
// failed disk read looked identical to "this server's disk is completely
// full" (0 free bytes) instead of "we don't have a reading right now".
type Snapshot struct {
CPUPercent float64
MemUsedBytes int64
MemTotalBytes int64
DiskFreeBytes int64
DiskTotalBytes int64
DiskReady bool
Ready bool
}
@ -29,6 +37,10 @@ type Snapshot struct {
// for the local blob-storage free-space guard.
type Poller struct {
diskPath string
// diskFreeBytesFn defaults to the platform diskFreeBytes function;
// overridable in tests to simulate a failing/succeeding disk read
// without touching the real filesystem/OS call.
diskFreeBytesFn func(path string) (free, total int64, err error)
mu sync.RWMutex
snap Snapshot
@ -38,12 +50,31 @@ type Poller struct {
// NewPoller creates a poller that reports free/total disk space for the
// filesystem containing diskPath (pass the server's data/blob directory, or
// "." if it doesn't matter which volume).
// "." if it doesn't matter which volume). diskPath is resolved to an
// absolute path up front (a relative path depends on the process's current
// directory, which callers shouldn't have to reason about here) and, if it
// doesn't exist yet -- e.g. an S3-backend deployment whose local blob
// staging directory is only created on first upload -- walked up to the
// nearest existing ancestor, since GetDiskFreeSpaceEx/statfs need a real
// path and every ancestor is on the same volume anyway.
func NewPoller(diskPath string) *Poller {
if diskPath == "" {
diskPath = "."
}
return &Poller{diskPath: diskPath}
if abs, err := filepath.Abs(diskPath); err == nil {
diskPath = abs
}
for {
if _, err := os.Stat(diskPath); err == nil {
break
}
parent := filepath.Dir(diskPath)
if parent == diskPath {
break
}
diskPath = parent
}
return &Poller{diskPath: diskPath, diskFreeBytesFn: diskFreeBytes}
}
// Snapshot returns the last sample. Safe to call concurrently with Run.
@ -72,13 +103,27 @@ func (p *Poller) Run(ctx context.Context, interval time.Duration) {
}
func (p *Poller) sampleOnce() {
p.mu.RLock()
prevFree, prevTotal := p.snap.DiskFreeBytes, p.snap.DiskTotalBytes
p.mu.RUnlock()
var snap Snapshot
snap.CPUPercent = p.cpu.sample()
if used, total, err := memStats(); err == nil {
snap.MemUsedBytes, snap.MemTotalBytes = used, total
}
if free, total, err := diskFreeBytes(p.diskPath); err == nil {
if free, total, err := p.diskFreeBytesFn(p.diskPath); err == nil {
snap.DiskFreeBytes, snap.DiskTotalBytes = free, total
snap.DiskReady = true
} else {
// Keep the last known-good byte values stored (harmless, and a
// reasonable fallback for any future caller that wants "last known"
// over nothing) but DiskReady reflects THIS sample, not a stale one
// -- a failure must show as "no current reading", not silently keep
// claiming Ready while quietly reusing old numbers forever if the
// underlying path became permanently unreadable.
snap.DiskFreeBytes, snap.DiskTotalBytes = prevFree, prevTotal
snap.DiskReady = false
}
snap.Ready = true