owpengram-server/internal/app/files/diskspace.go
2026-08-04 23:09:11 +03:00

99 lines
3.1 KiB
Go

package files
import "sync/atomic"
// SpaceGuard bounds how much more may be written to the permanent blob
// backend. LocalDiskSpaceGuard checks real OS free disk bytes;
// S3BudgetSpaceGuard compares a cached tracked-bytes total against a
// configured budget (S3 has no OS-level "free space" concept). Both are
// refreshed periodically by DiskUsageWorker rather than recomputed on every
// upload chunk.
type SpaceGuard interface {
// Allow reports whether writing `additional` more bytes is currently
// permitted. false should surface to the client as domain.ErrStorageFull.
Allow(additional int64) (bool, error)
// Usage returns the last-refreshed (used, total) byte snapshot for the
// admin panel. ok is false if no successful refresh has happened yet.
Usage() (used, total int64, ok bool)
}
// NoopSpaceGuard always allows writes; the default when the low-space guard
// is disabled by configuration.
type NoopSpaceGuard struct{}
func (NoopSpaceGuard) Allow(int64) (bool, error) { return true, nil }
func (NoopSpaceGuard) Usage() (int64, int64, bool) { return 0, 0, false }
// LocalDiskSpaceGuard rejects writes once cached free disk bytes fall below
// minFreeBytes (<=0 disables the check). The free-bytes figure is
// refreshed by DiskUsageWorker, not recomputed per call, to avoid a statfs
// syscall on every upload chunk -- reads are lock-free.
type LocalDiskSpaceGuard struct {
minFreeBytes int64
free atomic.Int64
total atomic.Int64
ready atomic.Bool
}
func NewLocalDiskSpaceGuard(minFreeBytes int64) *LocalDiskSpaceGuard {
return &LocalDiskSpaceGuard{minFreeBytes: minFreeBytes}
}
func (g *LocalDiskSpaceGuard) Allow(additional int64) (bool, error) {
// Before the first refresh completes, allow rather than reject: a
// startup race shouldn't turn into spurious upload failures.
if g.minFreeBytes <= 0 || !g.ready.Load() {
return true, nil
}
return g.free.Load()-additional >= g.minFreeBytes, nil
}
func (g *LocalDiskSpaceGuard) Usage() (used, total int64, ok bool) {
if !g.ready.Load() {
return 0, 0, false
}
total = g.total.Load()
used = total - g.free.Load()
if used < 0 {
used = 0
}
return used, total, true
}
func (g *LocalDiskSpaceGuard) setFree(free, total int64) {
g.free.Store(free)
g.total.Store(total)
g.ready.Store(true)
}
// S3BudgetSpaceGuard rejects writes once a cached tracked-bytes total
// (refreshed periodically from file_blobs) would exceed maxTotalBytes
// (<=0 disables the check).
type S3BudgetSpaceGuard struct {
maxTotalBytes int64
used atomic.Int64
ready atomic.Bool
}
func NewS3BudgetSpaceGuard(maxTotalBytes int64) *S3BudgetSpaceGuard {
return &S3BudgetSpaceGuard{maxTotalBytes: maxTotalBytes}
}
func (g *S3BudgetSpaceGuard) Allow(additional int64) (bool, error) {
if g.maxTotalBytes <= 0 || !g.ready.Load() {
return true, nil
}
return g.used.Load()+additional <= g.maxTotalBytes, nil
}
func (g *S3BudgetSpaceGuard) Usage() (used, total int64, ok bool) {
if !g.ready.Load() {
return 0, 0, false
}
return g.used.Load(), g.maxTotalBytes, true
}
func (g *S3BudgetSpaceGuard) setUsed(used int64) {
g.used.Store(used)
g.ready.Store(true)
}