nfs-server: a share the export file carries stays exported while its first look runs after a restart (issue 1971, review round 1)

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-05 10:43:50 +01:00
parent b0bdcd02c9
commit 279ac41087
3 changed files with 49 additions and 10 deletions
+1 -1
View File
@@ -109,7 +109,7 @@ type Machine struct {
Beside func(func())
// looks is the last look below each real path (LookKept).
looks map[string]*lookBelow
cleanMu sync.Mutex
looksMu sync.Mutex
// walk is the running pass's context, which ends the look below a share when the process stops.
walk context.Context
walkMu sync.Mutex
@@ -483,3 +483,29 @@ func TestTheLookBelowRunsBesideThePassAndNeverHoldsItsStatus(t *testing.T) {
t.Errorf("the repository was not found by the next look: %+v", s.Refused)
}
}
// After a restart no look is remembered; a share the previous process's export file carries stays exported,
// and nothing is reloaded, while its first look runs (a delivery never unexports a mounted share).
func TestARestartKeepsTheExportedShareWhileItsFirstLookRuns(t *testing.T) {
f := newFake(t)
f.mountinfo("/", "/srv/clean")
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"}}})
f.kernelReads()
first := f.machine()
s := first.Pass(context.Background(), nil)
before := f.read(ExportFile)
if refusedWhy(s, "clean") != "" || !strings.Contains(before, "/srv/clean ") {
t.Fatalf("the share was not exported to begin with: %+v %q", s.Refused, before)
}
m := f.machine()
var held []func()
m.Beside = func(w func()) { held = append(held, w) }
f.ran = nil
s = m.Pass(context.Background(), &s)
if len(held) != 1 || refusedWhy(s, "clean") != "" || f.read(ExportFile) != before || countExportfs(f.ran) != 0 {
t.Errorf("a restart with an unfinished walk unexported the share: %+v, ran %v", s.Refused, f.ran)
}
}
+22 -9
View File
@@ -265,7 +265,7 @@ type lookBelow struct {
// kept clean look is being renewed the old result stands. complete is false when the look stopped at its
// bound or with the process.
func (m *Machine) repositoryBelow(dir string) (found string, complete, pending bool) {
m.cleanMu.Lock()
m.looksMu.Lock()
r := m.looks[dir]
if r == nil {
if m.looks == nil {
@@ -279,12 +279,12 @@ func (m *Machine) repositoryBelow(dir string) (found string, complete, pending b
if start {
r.running = true
}
m.cleanMu.Unlock()
m.looksMu.Unlock()
if start {
walk := func() {
f, c := m.walkBelow(dir)
m.cleanMu.Lock()
defer m.cleanMu.Unlock()
m.looksMu.Lock()
defer m.looksMu.Unlock()
r.running = false
if w := m.walking(); w != nil && w.Err() != nil {
return // ended by the process stopping: no result, the old one stands
@@ -297,8 +297,8 @@ func (m *Machine) repositoryBelow(dir string) (found string, complete, pending b
go walk()
}
}
m.cleanMu.Lock()
defer m.cleanMu.Unlock()
m.looksMu.Lock()
defer m.looksMu.Unlock()
if !r.done {
return "", false, true
}
@@ -381,7 +381,8 @@ func (m *Machine) Judge(c Config, roster map[string]string, wants map[string][]W
prefix, _ = netip.ParsePrefix(c.Range)
}
points := m.MountPoints()
backup, _ := m.Backup() // judged on every pass, never kept: see backup.go
onDisk, _ := m.Read(ExportFile) // what the previous process judged
backup, _ := m.Backup() // judged on every pass, never kept: see backup.go
backup.Paths = m.realPaths(backup.Paths)
shares := map[string]bool{}
for _, s := range c.Shares {
@@ -455,11 +456,11 @@ func (m *Machine) Judge(c Config, roster map[string]string, wants map[string][]W
no(fmt.Sprintf("The share %s's folder %s%s holds a backup repository (%s), and backups are never exported: share the folders beside it instead.", s.Name, real, via, holds))
continue
}
if pending {
if pending && !carries(onDisk, s.Name, real) {
no(fmt.Sprintf("The share %s's folder %s%s has not been looked through yet for a backup repository (the look runs beside the pass and takes a while on a large folder), so it is not exported yet.", s.Name, real, via))
continue
}
if !complete {
if !pending && !complete {
no(fmt.Sprintf("The share %s's folder %s%s holds more folders than the module looks through (%d levels, at most %d folders), so it could not make sure no backup repository is under it: share smaller folders.", s.Name, real, via, WalkDepth, m.limit()))
continue
}
@@ -530,6 +531,18 @@ func (m *Machine) Judge(c Config, roster map[string]string, wants map[string][]W
return out, refused, said
}
// carries says whether the export file already carries a share: its line, or the note that it goes to no
// node. While a share's first look is unfinished (after a restart) it stays as the previous process judged
// it, so a delivery never unexports a share clients have mounted.
func carries(file, name, real string) bool {
for _, l := range strings.Split(file, "\n") {
if strings.HasPrefix(l, real+" ") || l == "# share "+name || strings.HasPrefix(l, "# share "+name+":") {
return true
}
}
return false
}
func (m *Machine) limit() int {
if m.WalkLimit > 0 {
return m.WalkLimit