diff --git a/modules/nfs-server/cmd/nfs-server/machine.go b/modules/nfs-server/cmd/nfs-server/machine.go index 08196cd0..47f70e4e 100644 --- a/modules/nfs-server/cmd/nfs-server/machine.go +++ b/modules/nfs-server/cmd/nfs-server/machine.go @@ -10,6 +10,7 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "syscall" "time" ) @@ -101,6 +102,9 @@ type Machine struct { // WalkLimit bounds the look for a backup repository below a share. WalkLimit int owners map[string]Owner + // clean is when a look below each real path last found no backup repository (LookKept). + clean map[string]time.Time + cleanMu sync.Mutex } // Here is the machine this process runs on. diff --git a/modules/nfs-server/cmd/nfs-server/review_test.go b/modules/nfs-server/cmd/nfs-server/review_test.go index 80a71ebf..266bf00b 100644 --- a/modules/nfs-server/cmd/nfs-server/review_test.go +++ b/modules/nfs-server/cmd/nfs-server/review_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "testing" + "time" ) // The review of 2026-10-08 (mesh-catalog #136): each finding, reproduced. @@ -311,3 +312,30 @@ func TestWantsFromTheMountsStateKeepOnlyWhatIsAskedOfThisMachine(t *testing.T) { t.Errorf("the tools may write the wants file: %v", err) } } + +// The look below a share for a backup repository walks up to WalkLimit folders; on a library of tens of +// thousands of folders that must not run on every 30 s pass. A clean result is kept for LookKept, and a +// repository put there later is found once that has passed. +func TestTheLookBelowIsKeptForAWhileAndThenTakenAgain(t *testing.T) { + f := newFake(t) + f.mountinfo("/") + f.dir("/srv/clean/films/one") + f.file(ConfigFile, grantsConfig("clean=/srv/clean", "clean=ws:rw")) + f.roster("ws 10.0.0.3") + f.wants(map[string][]Want{"ws": {{Share: "clean", Access: "rw"}}}) + m := f.machine() + now := time.Date(2026, 10, 8, 19, 0, 0, 0, time.UTC) + m.Now = func() time.Time { return now } + if s := m.Pass(context.Background(), nil); refusedWhy(s, "clean") != "" { + t.Fatalf("a clean folder is refused: %+v", s.Refused) + } + f.restic("/srv/clean/films/one/backup") + now = now.Add(time.Minute) + if s := m.Pass(context.Background(), nil); refusedWhy(s, "clean") != "" { + t.Errorf("the look was taken again within a minute: %+v", s.Refused) + } + now = now.Add(LookKept) + if s := m.Pass(context.Background(), nil); !strings.Contains(refusedWhy(s, "clean"), "/srv/clean/films/one/backup") { + t.Errorf("a repository put there later was not found once the kept look expired: %+v", s.Refused) + } +} diff --git a/modules/nfs-server/cmd/nfs-server/shares.go b/modules/nfs-server/cmd/nfs-server/shares.go index 60e64913..eb656442 100644 --- a/modules/nfs-server/cmd/nfs-server/shares.go +++ b/modules/nfs-server/cmd/nfs-server/shares.go @@ -6,6 +6,7 @@ import ( "path" "sort" "strings" + "time" ) // The shares, from the assignment, and the export lines made from them (novox/hq ADR 0263). @@ -242,9 +243,38 @@ const ( WalkLimit = 200000 ) +// LookKept is how long a look below a share that found no backup repository is believed. The look walks up +// to WalkLimit folders, which on a large library is too much for every 30 s pass; a repository put under a +// share is found within this long. A look that found one, or was cut short, is never kept: the share stays +// refused and is looked at again on the next pass. +const LookKept = time.Hour + // repositoryBelow finds a backup repository within WalkDepth levels below dir. complete is false when // the look stopped at its bound before it saw everything. func (m *Machine) repositoryBelow(dir string) (found string, complete bool) { + now := time.Now() + if m.Now != nil { + now = m.Now() + } + m.cleanMu.Lock() + defer m.cleanMu.Unlock() + if at, kept := m.clean[dir]; kept && now.Sub(at) < LookKept { + return "", true + } + found, complete = m.walkBelow(dir) + if found == "" && complete { + if m.clean == nil { + m.clean = map[string]time.Time{} + } + m.clean[dir] = now + } else { + delete(m.clean, dir) + } + return found, complete +} + +// walkBelow is the look itself, breadth first, bounded by WalkDepth and WalkLimit. +func (m *Machine) walkBelow(dir string) (found string, complete bool) { limit := m.WalkLimit if limit <= 0 { limit = WalkLimit