From 4ddf7021454f4338ffbde8648dd0c052501a8148 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 23:26:36 +0100 Subject: [PATCH] nfs-server: learn the backup holder's data paths from disk-load's readings, not the controller's data (issue 1955, ADR 0571 clause 3) --- modules/nfs-server/cmd/nfs-server/backup.go | 40 +++++++------------ .../nfs-server/cmd/nfs-server/backup_test.go | 28 +++++++++---- .../nfs-server/cmd/nfs-server/guards_test.go | 10 ++--- modules/nfs-server/cmd/nfs-server/main.go | 29 ++++++++++++-- .../cmd/nfs-server/manifest_test.go | 3 +- modules/nfs-server/go.mod | 2 +- modules/nfs-server/go.sum | 4 +- modules/nfs-server/module.json | 4 +- 8 files changed, 71 insertions(+), 49 deletions(-) diff --git a/modules/nfs-server/cmd/nfs-server/backup.go b/modules/nfs-server/cmd/nfs-server/backup.go index e8d3dfde..99c7ba59 100644 --- a/modules/nfs-server/cmd/nfs-server/backup.go +++ b/modules/nfs-server/cmd/nfs-server/backup.go @@ -14,13 +14,12 @@ package main // been read, what is exported stays exported and the status says the list is not known: the bus being // slow is no reason to take the library away, and the look below still guards against a repository. // -// The parsing of mesh-controller.data's answer is a small copy of the mounts module's (DataFrom). Shared -// code belongs in mesh-sdk: moving both there is mesh-sdk issue 13. import ( "encoding/json" "fmt" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/health" "os" "path" "sort" @@ -48,10 +47,10 @@ type BackupFromBus struct { } // BackupFrom reads the holder of node-backup on one machine from the placement seat's state of who holds which seat -// (novox/hq ADR 0571 clause 4), and the paths that module keeps data in on that machine from mesh-controller.data's -// answer. A machine whose backup seat nobody holds, and an answer with no data list, are errors: never an empty list -// read as "nothing to fear". -func BackupFrom(machine string, keys map[string][]byte, data json.RawMessage) (string, []string, error) { +// (novox/hq ADR 0571 clause 4), and the paths that module keeps data in on that machine from disk-load's readings +// (clause 3). A machine whose backup seat nobody holds, and no readings at all, are errors: never an empty list read as +// "nothing to fear". +func BackupFrom(machine string, keys map[string][]byte, readings []health.Reading) (string, []string, error) { holders, err := delivery.HoldersOf(keys, BackupSeat, machine) if err != nil { return "", nil, err @@ -65,36 +64,27 @@ func BackupFrom(machine string, keys map[string][]byte, data json.RawMessage) (s if holder == "" { return "", nil, fmt.Errorf("no module holds %s on %s", BackupSeat, machine) } - var d struct { - Answer *struct { - Data *[]struct { - Machine string `json:"machine"` - Module string `json:"module"` - Path string `json:"path"` - } `json:"data"` - } `json:"answer"` - } - if err := json.Unmarshal(data, &d); err != nil || d.Answer == nil || d.Answer.Data == nil { - return "", nil, fmt.Errorf("the controller's data answer holds no data list (%v)", err) + if len(readings) == 0 { + return "", nil, fmt.Errorf("disk-load keeps no reading of this machine's data yet") } var paths []string - for _, item := range *d.Answer.Data { - if item.Module == holder && (item.Machine == "" || item.Machine == machine) && path.IsAbs(item.Path) { - paths = append(paths, path.Clean(item.Path)) + for _, r := range readings { + if r.Module == holder && (r.Node == "" || r.Node == machine) && path.IsAbs(r.Path) { + paths = append(paths, path.Clean(r.Path)) } } sort.Strings(paths) return holder, paths, nil } -// TakeBackup asks the controller over the bus, as the module, through the runtime. +// TakeBackup reads the bus, as the module, through the runtime. func (m *Machine) TakeBackup(machine string) { - m.TakeBackupWith(machine, askBus, seatHolderKeys) + m.TakeBackupWith(machine, readingsOf, seatHolderKeys) } // TakeBackupWith is TakeBackup with the ask and the reading of the seat-holders state given, for tests. A failed ask keeps the last holder and // paths and adds why. -func (m *Machine) TakeBackupWith(machine string, ask func(key string, body any) (json.RawMessage, error), +func (m *Machine) TakeBackupWith(machine string, readings func(node string) ([]health.Reading, error), holderKeys func() (map[string][]byte, error)) { var last BackupFromBus if text, err := m.Read(BackupFile); err == nil { @@ -104,9 +94,9 @@ func (m *Machine) TakeBackupWith(machine string, ask func(key string, body any) next.Looked = m.Now().UTC().Format(time.RFC3339) next.Error = "" seats, err := holderKeys() - var data json.RawMessage + var data []health.Reading if err == nil { - data, err = ask("seat:mesh-controller.data", map[string]any{"machine": machine}) + data, err = readings(machine) } if err == nil { var holder string diff --git a/modules/nfs-server/cmd/nfs-server/backup_test.go b/modules/nfs-server/cmd/nfs-server/backup_test.go index 4efe26a5..dd4da159 100644 --- a/modules/nfs-server/cmd/nfs-server/backup_test.go +++ b/modules/nfs-server/cmd/nfs-server/backup_test.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "errors" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/health" "strings" "testing" "time" @@ -82,7 +84,7 @@ func TestWithoutTheBackupListTheExportsStayAndItIsSaid(t *testing.T) { func TestBackupFromTheSeatsAndTheData(t *testing.T) { seats := backupHolders("ace", "restic") - data := json.RawMessage(`{"answer":{"data":[{"machine":"ace","module":"restic","item":"repository","path":"/storage/restic"},{"machine":"ace","module":"restic","item":"state","path":"/var/lib/restic"},{"machine":"ace","module":"plex","item":"data","path":"/mnt/plex/data"}]},"ok":true}`) + data := readingsOfPaths("ace", map[string]string{"restic/repository": "/storage/restic", "restic/state": "/var/lib/restic", "plex/data": "/mnt/plex/data"}) holder, paths, err := BackupFrom("ace", seats, data) if err != nil || holder != "restic" || strings.Join(paths, " ") != "/storage/restic /var/lib/restic" { t.Errorf("got %q %v %v", holder, paths, err) @@ -90,8 +92,8 @@ func TestBackupFromTheSeatsAndTheData(t *testing.T) { if _, _, err := BackupFrom("g14", seats, data); err == nil { t.Errorf("a machine whose backup seat nobody holds is not said") } - if _, _, err := BackupFrom("ace", seats, json.RawMessage(`{"ok":true}`)); err == nil { - t.Errorf("an answer with no data list reads as an empty list") + if _, _, err := BackupFrom("ace", seats, nil); err == nil { + t.Errorf("no readings read as an empty list") } } @@ -99,11 +101,11 @@ func TestBackupFromTheSeatsAndTheData(t *testing.T) { func TestAFailedAskKeepsTheLastBackupPaths(t *testing.T) { f := newFake(t) m := f.machine() - m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) { + m.TakeBackupWith("ace", func(string) ([]health.Reading, error) { return nil, errors.New("no responders") }, noHolders) f.backup("restic", "/storage/restic") - m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) { + m.TakeBackupWith("ace", func(string) ([]health.Reading, error) { return nil, errors.New("timeout") }, func() (map[string][]byte, error) { return backupHolders("ace", "restic"), nil }) var b BackupFromBus @@ -152,12 +154,12 @@ func TestAStaleOrUnknownBackupListIsSaidWithItsAge(t *testing.T) { t.Errorf("an unknown backup list is not a warning in the health: %+v", h) } - ok := func(key string, body any) (json.RawMessage, error) { - return json.RawMessage(`{"answer":{"data":[{"machine":"hs","module":"restic","path":"/srv/restic"}]}}`), nil + ok := func(string) ([]health.Reading, error) { + return readingsOfPaths("hs", map[string]string{"restic/repository": "/srv/restic"}), nil } m.TakeBackupWith("hs", ok, func() (map[string][]byte, error) { return backupHolders("hs", "restic"), nil }) now = now.Add(2 * time.Hour) - m.TakeBackupWith("hs", func(string, any) (json.RawMessage, error) { return nil, errors.New("the bus did not answer") }, + m.TakeBackupWith("hs", func(string) ([]health.Reading, error) { return nil, errors.New("the bus did not answer") }, func() (map[string][]byte, error) { return backupHolders("hs", "restic"), nil }) s := m.Pass(context.Background(), nil) said := strings.Join(append(append([]string{}, s.Said...), s.Warnings...), "\n") @@ -177,3 +179,13 @@ func backupHolders(node, module string) map[string][]byte { } func noHolders() (map[string][]byte, error) { return nil, errors.New("the state is not read yet") } + +// readingsOfPaths is disk-load's readings of one node's items, by "/", each at its path. +func readingsOfPaths(node string, items map[string]string) []health.Reading { + var out []health.Reading + for k, p := range items { + module, item, _ := strings.Cut(k, "/") + out = append(out, health.Reading{V: 1, Node: node, PlacedDataItem: catalogue.PlacedDataItem{Module: module, ID: item, Path: p}}) + } + return out +} diff --git a/modules/nfs-server/cmd/nfs-server/guards_test.go b/modules/nfs-server/cmd/nfs-server/guards_test.go index 5ea2b715..457af6f6 100644 --- a/modules/nfs-server/cmd/nfs-server/guards_test.go +++ b/modules/nfs-server/cmd/nfs-server/guards_test.go @@ -4,7 +4,7 @@ package main import ( "context" - "encoding/json" + "git.novox.be/novox/mesh-sdk/go/health" "strings" "testing" ) @@ -63,12 +63,10 @@ func TestAFailedReloadIsTriedAgainWhenNothingElseAsks(t *testing.T) { } } -func TestBackupFromRefusesAnAnswerWithoutADataList(t *testing.T) { +func TestBackupFromRefusesNoReadingsAtAll(t *testing.T) { seats := backupHolders("ace", "restic") - for _, data := range []string{`{"answer":{}}`, `{"answer":{"data":null}}`} { - if _, paths, err := BackupFrom("ace", seats, json.RawMessage(data)); err == nil { - t.Errorf("%s was read as the empty list %v: never \"nothing to fear\"", data, paths) - } + if _, paths, err := BackupFrom("ace", seats, []health.Reading{}); err == nil { + t.Errorf("no readings were read as the empty list %v: never \"nothing to fear\"", paths) } } diff --git a/modules/nfs-server/cmd/nfs-server/main.go b/modules/nfs-server/cmd/nfs-server/main.go index 8a9db1a1..6edda7de 100644 --- a/modules/nfs-server/cmd/nfs-server/main.go +++ b/modules/nfs-server/cmd/nfs-server/main.go @@ -16,11 +16,13 @@ import ( "os" "os/signal" "sort" + "strings" "syscall" "time" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/health" ) // ExportsState is the state the exports are kept in on the bus; MountsState the `mounts` modules' state, @@ -89,9 +91,30 @@ func ShareExports(m *Machine) { } } -// askBus calls a seat's verb over the bus as this module, through the runtime. -func askBus(key string, body any) (json.RawMessage, error) { - return stdio.Ask(key, body) +// readingsOf reads disk-load's readings of one machine's data items, every key `..`. +func readingsOf(node string) ([]health.Reading, error) { + state := stdio.State(health.ReadingsRead) + names, err := state.Keys() + if err != nil { + return nil, err + } + var out []health.Reading + for _, k := range names { + if !strings.HasPrefix(k, node+".") { + continue + } + e, err := state.Get(k) + if err != nil { + return nil, err + } + if e == nil { + continue + } + if r, err := health.DecodeReading(e.Value); err == nil { + out = append(out, r) + } + } + return out, nil } // seatHolderKeys reads the placement seat's state of who holds which seat, every key. diff --git a/modules/nfs-server/cmd/nfs-server/manifest_test.go b/modules/nfs-server/cmd/nfs-server/manifest_test.go index c4bbedfb..ae048eb2 100644 --- a/modules/nfs-server/cmd/nfs-server/manifest_test.go +++ b/modules/nfs-server/cmd/nfs-server/manifest_test.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/health" "os" "regexp" "sort" @@ -138,7 +139,7 @@ func TestTheManifestCarriesGrantsTheRosterTheWantsAndTheRemovalOrder(t *testing. if err := json.Unmarshal([]byte(raw), &full); err != nil { t.Fatal(err) } - if len(full.Reads) != 2 || full.Reads[0] != delivery.SeatHoldersRead || full.Reads[1] != MountsState { + if len(full.Reads) != 3 || full.Reads[0] != health.ReadingsRead || full.Reads[1] != delivery.SeatHoldersRead || full.Reads[2] != MountsState { t.Errorf("reads: %v", full.Reads) } roster := full.Facts["machines"] diff --git a/modules/nfs-server/go.mod b/modules/nfs-server/go.mod index 833e932c..47fb5666 100644 --- a/modules/nfs-server/go.mod +++ b/modules/nfs-server/go.mod @@ -2,4 +2,4 @@ module nfsserver go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029221656-196c5b6af277 diff --git a/modules/nfs-server/go.sum b/modules/nfs-server/go.sum index 05839926..c0f109eb 100644 --- a/modules/nfs-server/go.sum +++ b/modules/nfs-server/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121 h1:OsA1v6n736a7RqEV8Nkip8pf5ILMbzj0PWuKqcBOhK0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029221656-196c5b6af277 h1:ajjsH4MWcx45MGYASjUSRQKhLzEringuk3TA0oxZRCU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029221656-196c5b6af277/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/modules/nfs-server/module.json b/modules/nfs-server/module.json index a33fb88f..c7082176 100644 --- a/modules/nfs-server/module.json +++ b/modules/nfs-server/module.json @@ -48,12 +48,10 @@ } ], "reads": [ + "disk-load.readings", "mesh-delivery-assigner.seat-holders", "mounts.shares" ], - "invokes": [ - "seat:mesh-controller.data" - ], "config": { "shares": { "type": "string",