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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user