Judge the daily report by wall time, and keep one filesystem check at a time (issue 160)
mesh/merge-gate head c7a4bf5c11d6 merged into trunk d8e785bb4b4b — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head c7a4bf5c11d6 merged into trunk d8e785bb4b4b — pass: its merge-check.sh passed
mesh/delivery delivered

Go's monotonic clock stops while a laptop is suspended, so a day judged by it could pass a day and a half
of wall time before the machine said what it is. And a hung statfs left a goroutine and thread blocked per
apply; now one check runs at a time and reports after it say the sizes unknown until it ends.
This commit is contained in:
jochen
2026-10-12 17:42:18 +02:00
parent ec983bfc3d
commit c7a4bf5c11
4 changed files with 86 additions and 4 deletions
+12
View File
@@ -7,6 +7,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
@@ -362,6 +363,9 @@ var localTypes = map[string]bool{"ext2": true, "ext3": true, "ext4": true, "xfs"
"zfs": true, "f2fs": true, "vfat": true, "exfat": true, "ntfs": true, "ntfs3": true, "fuseblk": true,
"jfs": true, "reiserfs": true, "bcachefs": true}
// sizing is held while a check of the filesystems' sizes runs.
var sizing sync.Mutex
// filesystemsOf lists each local filesystem once — a device mounted at several places (a btrfs
// subvolume, a bind mount) under its shortest mount — with its size and what is free. Bounded by the
// context, so a filesystem whose statfs hangs cannot hang the apply that reports it.
@@ -385,12 +389,20 @@ func filesystemsOf(ctx context.Context, mounts string, stat func(string) (int64,
return nil, "/proc/self/mounts lists no local filesystem"
}
// One check at a time, across every report: a filesystem whose statfs hangs keeps the one check that
// asked it, and every report after it says the sizes unknown until it finishes, rather than leaving
// another goroutine and thread blocked behind it on every apply.
if !sizing.TryLock() {
return nil, "the last check of the filesystems' sizes has not finished yet"
}
type answer struct {
fs []Filesystem
why []string
}
done := make(chan answer, 1)
go func() {
defer sizing.Unlock()
var a answer
for _, fs := range byDevice {
size, free, err := stat(fs.Mount)
+26 -2
View File
@@ -7,6 +7,7 @@ import (
"os"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
)
@@ -112,14 +113,37 @@ func TestMachine_aFactItCannotReadIsSaidUnknownNeverZero(t *testing.T) {
func TestMachine_aHungFilesystemCannotHangTheReport(t *testing.T) {
src := given(files)
block := make(chan struct{})
defer close(block)
src.Statfs = func(string) (int64, int64, error) { <-block; return 0, 0, nil }
var asked atomic.Int32
src.Statfs = func(string) (int64, int64, error) { asked.Add(1); <-block; return 0, 0, nil }
defer func() {
close(block)
// The check that hung ends once its filesystem answers, and lets the next one run.
for !sizing.TryLock() {
time.Sleep(time.Millisecond)
}
sizing.Unlock()
}()
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
m := DescribeMachine(ctx, src)
if len(m.Filesystems) != 0 || !strings.Contains(m.Unknown[FactFilesystems], "did not finish") {
t.Errorf("filesystems %v, unknown %q", m.Filesystems, m.Unknown[FactFilesystems])
}
stuck := asked.Load()
// Every report while it hangs says so at once, and asks no filesystem again: no goroutine is left
// blocked behind the hung one per apply.
for i := 0; i < 3; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
m = DescribeMachine(ctx, src)
cancel()
if !strings.Contains(m.Unknown[FactFilesystems], "has not finished yet") {
t.Errorf("a report while the check hangs said %q", m.Unknown[FactFilesystems])
}
}
if asked.Load() != stuck {
t.Errorf("filesystems asked %d times while one check hung, was %d", asked.Load(), stuck)
}
}
func TestMachine_coresUnsaidAreUnknownNotGuessed(t *testing.T) {