The backup module says on the bus when a backup asked for now has ended, so no one polls backed-up (issue 1944, ADR 0539 §16)
This commit is contained in:
@@ -184,6 +184,10 @@ type Backups struct {
|
||||
Run Runner
|
||||
Now func() time.Time
|
||||
Say func(format string, args ...any)
|
||||
// Emit says an event of this module on the bus; nil where the process has no runtime to say it (the night and the
|
||||
// measuring run as processes of their own). Node is the machine's name, said in the events.
|
||||
Emit func(event string, body any) error
|
||||
Node string
|
||||
}
|
||||
|
||||
// The locks, files in this module's state. The night runs in a process of its own and `now`, `restore` and a
|
||||
@@ -249,6 +253,28 @@ func (b *Backups) Nights() map[string]Night {
|
||||
return nights
|
||||
}
|
||||
|
||||
// EventBackedUp is what this module says when a backup asked for now (`node-backup.now`) has ended for a module, well or
|
||||
// not: {node, module, ok, snapshot, at, error}. A caller that asked waits for it on the bus instead of asking `backed-up`
|
||||
// again and again (novox/hq ADR 0539 §16); the nights run in a process with no runtime and say nothing.
|
||||
const EventBackedUp = "backed-up"
|
||||
|
||||
// saidBackedUp says EventBackedUp for one module's backup taken by hand. A failure to say it is logged, never the backup's.
|
||||
func (b *Backups) saidBackedUp(module string, n Night) {
|
||||
if b.Emit == nil {
|
||||
return
|
||||
}
|
||||
body := map[string]any{"node": b.Node, "module": module, "ok": n.OK, "at": n.At}
|
||||
if n.Snapshot != "" {
|
||||
body["snapshot"] = n.Snapshot
|
||||
}
|
||||
if n.Error != "" {
|
||||
body["error"] = n.Error
|
||||
}
|
||||
if err := b.Emit(EventBackedUp, body); err != nil {
|
||||
b.Say("%s's backup could not be said on the bus: %v", module, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Backups) record(module string, n Night) {
|
||||
nights := b.Nights()
|
||||
nights[module] = n
|
||||
@@ -379,6 +405,9 @@ func (b *Backups) backUp(ctx context.Context, only string, hand bool) (map[strin
|
||||
for _, d := range chosen {
|
||||
outcome[d.Module] = b.one(ctx, d, hand)
|
||||
b.record(d.Module, outcome[d.Module])
|
||||
if hand {
|
||||
b.saidBackedUp(d.Module, outcome[d.Module])
|
||||
}
|
||||
if n := outcome[d.Module]; !hand && n.OK && n.Snapshot != "" {
|
||||
b.recordGoodNight(d.Module, n.Snapshot)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ const HandTag = "hand"
|
||||
func (b *Backups) BackUpNow(ctx context.Context, only string) (map[string]Night, error) {
|
||||
out, err := b.backUp(ctx, only, true)
|
||||
if err != nil && only != "" {
|
||||
b.record(only, Night{OK: false, At: b.Now().UTC(), Error: err.Error()})
|
||||
n := Night{OK: false, At: b.Now().UTC(), Error: err.Error()}
|
||||
b.record(only, n)
|
||||
b.saidBackedUp(only, n)
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -351,3 +351,57 @@ func TestABackupAskedForNowThatCannotStartIsRecordedAsThatModulesLastNight(t *te
|
||||
t.Errorf("mail's last night is %+v, want a failure saying it declares nothing", n)
|
||||
}
|
||||
}
|
||||
|
||||
// R6 (novox/hq ADR 0539 §16): a backup asked for now says on the bus when it has ended for a module, well or not, with the node,
|
||||
// the module, the snapshot and the time, so the node agent waits for the event instead of asking `backed-up` every five seconds.
|
||||
func TestABackupAskedForNowSaysItEndedOnTheBusForEachModule(t *testing.T) {
|
||||
type said struct {
|
||||
event string
|
||||
body map[string]any
|
||||
}
|
||||
var events []said
|
||||
emit := func(event string, body any) error {
|
||||
raw, _ := json.Marshal(body)
|
||||
var m map[string]any
|
||||
_ = json.Unmarshal(raw, &m)
|
||||
events = append(events, said{event, m})
|
||||
return nil
|
||||
}
|
||||
at := time.Date(2026, 10, 24, 14, 0, 0, 0, time.UTC)
|
||||
h := &handRestic{made: "abcdef0123456789"}
|
||||
b := &Backups{Where: placed(t, "# pg\npath /\n# app\npath /\n"), Run: h.run, Now: func() time.Time { return at }, Say: quiet,
|
||||
Emit: emit, Node: "ace"}
|
||||
_, err := b.BackUpNow(context.Background(), "")
|
||||
must(t, err)
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("said %d events for two modules: %v", len(events), events)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, e := range events {
|
||||
m, _ := e.body["module"].(string)
|
||||
seen[m] = true
|
||||
if e.event != "backed-up" || e.body["node"] != "ace" || e.body["ok"] != true || e.body["snapshot"] == "" || e.body["snapshot"] == nil ||
|
||||
e.body["at"] != at.Format(time.RFC3339) {
|
||||
t.Errorf("%s: %s %v", m, e.event, e.body)
|
||||
}
|
||||
}
|
||||
if !seen["pg"] || !seen["app"] {
|
||||
t.Errorf("modules said: %v", seen)
|
||||
}
|
||||
// One that cannot start is said failed with why, so a caller waiting on it reads an answer and not silence.
|
||||
events = nil
|
||||
_, err = b.BackUpNow(context.Background(), "nobody")
|
||||
if err == nil {
|
||||
t.Fatal("a module that declares nothing was backed up")
|
||||
}
|
||||
if len(events) != 1 || events[0].body["module"] != "nobody" || events[0].body["ok"] != false || events[0].body["error"] == nil {
|
||||
t.Errorf("the failure was not said: %v", events)
|
||||
}
|
||||
// A night says nothing: it runs in a process with no runtime.
|
||||
events = nil
|
||||
_, err = b.BackUp(context.Background(), "")
|
||||
must(t, err)
|
||||
if len(events) != 0 {
|
||||
t.Errorf("a night said %v", events)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,14 @@ func main() {
|
||||
say("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
b := &Backups{Where: where, Run: execRunner, Now: time.Now, Say: say}
|
||||
b := &Backups{Where: where, Run: execRunner, Now: time.Now, Say: say, Node: os.Getenv("MESH_NODE")}
|
||||
way := ""
|
||||
if len(os.Args) > 1 {
|
||||
way = os.Args[1]
|
||||
}
|
||||
if way == "" {
|
||||
b.Emit = func(event string, body any) error { return stdio.Emit(event, body) }
|
||||
}
|
||||
switch way {
|
||||
case "night":
|
||||
nights(b)
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"emits": [
|
||||
"backed-up"
|
||||
],
|
||||
"tools": [
|
||||
"backup_health",
|
||||
"backup_delete_retired",
|
||||
|
||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user