Files
mesh-catalog/modules/nfs-server/cmd/nfs-server/backup.go
T
jschoubben 35ff67bda0
mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
Design 47's last milestones: the keeper, self-check, healers, hand-acts, provisioner, vault, artifact sweep, disk readings, credential forms (issues 774, 1952, 1953, 1954, 1955)
One commit with exactly the tree of the reviewed head 35b4509c0a, on main 12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
2026-10-30 02:21:51 +01:00

176 lines
6.6 KiB
Go

package main
// Where this machine's backup seat keeps its data, from the bus (follow-up to the review of
// mesh-catalog #136, point 4).
//
// The look below a share for a backup repository walks the folders and is believed for LookKept, so a
// repository that the backup seat starts keeping under a share inside that hour would be exported until the
// look is taken again. So the module also learns, over the bus, which module holds `node-backup` on this
// machine and every path that module keeps data in (its repository among them), and every pass judges
// every share against those paths with nothing kept: a share at, under or above one of them is refused.
//
// The tools ask — the root process does not reach the bus — every BackupEvery, and leave what they learnt
// in BackupFile for the root process. A failed ask keeps the last paths and says why. Until a list has
// 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.
//
import (
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"os"
"path"
"sort"
"time"
)
// BackupFile is what the tools learnt about the backup seat on this machine.
const BackupFile = WantsDir + "/backup.json"
// BackupEvery is how often the tools ask.
const BackupEvery = 5 * time.Minute
// BackupSeat is the seat whose holder keeps this machine's backups.
const BackupSeat = "node-backup"
// BackupFromBus is the backup seat's holder on this machine and the paths it keeps data in.
type BackupFromBus struct {
Holder string `json:"holder"`
Paths []string `json:"paths"`
Looked string `json:"looked,omitempty"`
// Learnt is when an ask last succeeded: how old the holder and paths are.
Learnt string `json:"learnt,omitempty"`
// Error is why the last ask got no answer; the holder and paths are then the last answer's.
Error string `json:"error,omitempty"`
}
// 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 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
}
holder := ""
for _, h := range holders {
if h.Node == machine {
holder = h.Module
}
}
if holder == "" {
return "", nil, fmt.Errorf("no module holds %s on %s", BackupSeat, machine)
}
if len(readings) == 0 {
return "", nil, fmt.Errorf("disk-load keeps no reading of this machine's data yet")
}
var paths []string
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 reads the bus, as the module, through the runtime.
func (m *Machine) TakeBackup(machine string) {
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, readings func(node string) ([]health.Reading, error),
holderKeys func() (map[string][]byte, error)) {
var last BackupFromBus
if text, err := m.Read(BackupFile); err == nil {
_ = json.Unmarshal([]byte(text), &last)
}
next := last
next.Looked = m.Now().UTC().Format(time.RFC3339)
next.Error = ""
seats, err := holderKeys()
var data []health.Reading
if err == nil {
data, err = readings(machine)
}
if err == nil {
var holder string
var paths []string
holder, paths, err = BackupFrom(machine, seats, data)
if err == nil {
next.Holder, next.Paths, next.Learnt = holder, paths, next.Looked
}
}
if err != nil {
next.Error = err.Error()
fmt.Fprintln(os.Stderr, "nfs-server: where the backup seat keeps its data could not be learnt:", err)
}
raw, _ := json.MarshalIndent(next, "", " ")
if werr := m.Write(BackupFile, string(raw)+"\n"); werr != nil {
fmt.Fprintln(os.Stderr, "nfs-server: the backup seat's paths could not be written:", werr)
}
}
// Backup is what the tools last learnt, and a sentence when it cannot be relied on as it stands: nothing
// usable learnt yet, or a later ask failed and what is used is the last answer, with its age.
func (m *Machine) Backup() (BackupFromBus, string) {
text, err := m.Read(BackupFile)
if err != nil {
return BackupFromBus{}, "where this machine's backup seat keeps its repository has not been learnt from the bus yet; the shares are judged by the look below them alone until it is"
}
var b BackupFromBus
if err := json.Unmarshal([]byte(text), &b); err != nil {
return BackupFromBus{}, fmt.Sprintf("what was learnt about the backup seat (%s) could not be read: %v", BackupFile, err)
}
if len(b.Paths) == 0 {
why := "no path has been learnt"
if b.Error != "" {
why = b.Error
}
return b, "where this machine's backup seat keeps its repository is not known (" + why + "); the shares are judged by the look below them alone until it is"
}
if b.Error != "" {
age := "of unknown age"
if at, err := time.Parse(time.RFC3339, b.Learnt); err == nil {
age = fmt.Sprintf("learnt %s ago, at %s", m.Now().Sub(at).Round(time.Second), at.Local().Format("15:04"))
}
return b, fmt.Sprintf("where this machine's backup seat (%s) keeps its data is the last answer, %s: the "+
"latest ask failed (%s)", b.Holder, age, b.Error)
}
return b, ""
}
// realPaths are paths as the kernel resolves them, so a backup path named through a symbolic link is
// compared where it really is; one that cannot be resolved is kept as named.
func (m *Machine) realPaths(paths []string) []string {
out := make([]string, 0, len(paths))
for _, p := range paths {
if real, err := m.Real(p); err == nil {
out = append(out, real)
} else {
out = append(out, p)
}
}
return out
}
// nearBackup is the backup seat's path a real path is at, under or above, or "".
func nearBackup(real string, b BackupFromBus) (string, string) {
for _, p := range b.Paths {
switch {
case real == p:
return p, "is"
case len(real) > len(p) && real[:len(p)] == p && (p == "/" || real[len(p)] == '/'):
return p, "is inside"
case len(p) > len(real) && p[:len(real)] == real && (real == "/" || p[len(real)] == '/'):
return p, "holds"
}
}
return "", ""
}