diff --git a/modules/nfs-server/cmd/nfs-server/machine.go b/modules/nfs-server/cmd/nfs-server/machine.go index 6025426bb..f73dfc90d 100644 --- a/modules/nfs-server/cmd/nfs-server/machine.go +++ b/modules/nfs-server/cmd/nfs-server/machine.go @@ -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 diff --git a/modules/nfs-server/cmd/nfs-server/review_test.go b/modules/nfs-server/cmd/nfs-server/review_test.go index e987bf463..925f36a83 100644 --- a/modules/nfs-server/cmd/nfs-server/review_test.go +++ b/modules/nfs-server/cmd/nfs-server/review_test.go @@ -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) + } +} diff --git a/modules/nfs-server/cmd/nfs-server/shares.go b/modules/nfs-server/cmd/nfs-server/shares.go index ca81ddf9e..8ef26637d 100644 --- a/modules/nfs-server/cmd/nfs-server/shares.go +++ b/modules/nfs-server/cmd/nfs-server/shares.go @@ -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