Ask the backup seats before the first tier (issue 1065)

This commit is contained in:
2026-10-17 22:45:09 +02:00
parent 7c9c187ca6
commit 9a20547dbd
6 changed files with 600 additions and 1 deletions
+2 -1
View File
@@ -245,7 +245,8 @@ var VerbsTheSelfCheckAsks = []SeatVerb{{Seat: "node-intrusion-prevention", Verb:
// VerbsTheBusStepAsks are the seat verbs the bus's planned step calls (novox/hq to-be 45 §8, ADR 0236):
// the bus machine's backup holder takes the bus's snapshot now, before the bus is replaced (ADR 0235's
// dump), and says when it is taken (`backed-up`, granted with the self-check's). The one verb of
// the controller's grant that acts, and only through the step a person starts.
// the controller's grant that acts: through the step a person starts, and through the backups a delivery plan
// takes before it asks its first tier (novox/hq ADR 0404).
var VerbsTheBusStepAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}}
// VerbsTheControllerAsksForASecret are the seat verbs `give` calls (novox/hq ADR 0259 §10): the operator's
+35
View File
@@ -0,0 +1,35 @@
package inventory
import (
"encoding/json"
"reflect"
"strings"
"testing"
"time"
)
// The plan's backups are the contract mesh-delivery reads from `plan-moved` (issue 1065): their JSON field names
// are fixed here, and a plan keeps them through a round trip.
func TestAPlanKeepsItsBackupsThroughItsJSONUnderTheNamesTheDeliveryReads(t *testing.T) {
at := time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC)
p := Plan{ID: "p", Backups: &PlanBackups{State: BackupFailed, NoHolder: []string{"bare"},
Asked: []PlanBackup{{Machine: "ace", Module: "mail", State: BackupFailed, AskedAt: &at, Words: "no space left"}}}}
raw, err := json.Marshal(p)
if err != nil {
t.Fatal(err)
}
for _, name := range []string{`"backups":{"state":"failed","asked":[{"machine":"ace","module":"mail","state":"failed",` +
`"asked_at":"2026-10-17T12:00:00Z","words":"no space left"}],"no_holder":["bare"]}`} {
if !strings.Contains(string(raw), name) {
t.Fatalf("plan JSON %s lacks %s", raw, name)
}
}
var back Plan
if err := json.Unmarshal(raw, &back); err != nil || !reflect.DeepEqual(back.Backups, p.Backups) {
t.Fatalf("round trip: %v %+v", err, back.Backups)
}
raw, _ = json.Marshal(Plan{ID: "q"})
if strings.Contains(string(raw), "backups") {
t.Fatalf("a plan with no backups says none: %s", raw)
}
}
+36
View File
@@ -62,12 +62,48 @@ type Plan struct {
// Phases is the walk's time from its merge, phase by phase (ADR 0282 decision 6): never kept, worked out
// from the walk's own moments by whoever says the walk (`delivery walks`, `plan-moved`).
Phases *WalkPhases `json:"phases,omitempty"`
// Backups is the backups the walk takes before it asks its first tier (novox/hq ADR 0404 decisions 2 and 3):
// per machine and module, what was asked and how it went, and the machines with no backup holder. Nil for a
// walk that moves no module with an item protected by a backup, and for a walk kept before it was.
Backups *PlanBackups `json:"backups,omitempty"`
// WalkSaid is, on the save that started or ended the walk, which of the two it was (novox/hq ADR 0308
// decision 1): set by the save, never kept, read by whoever says the save (`plan-moved`). Empty on every
// other save.
WalkSaid string `json:"-"`
}
// The states of a plan's backups, as a whole and each.
const (
BackupPending = "pending"
BackupDone = "done"
BackupFailed = "failed"
)
// PlanBackups is a walk's record of the backups it takes before it asks anything (novox/hq ADR 0404): read by
// mesh-delivery from `plan-moved`, so its JSON field names are a contract between the two repositories.
type PlanBackups struct {
// State is the whole: pending while any backup is asked or unasked and none failed, failed as soon as one
// failed, done when every one is.
State string `json:"state"`
// Asked is one entry per machine and module, sorted by machine then module.
Asked []PlanBackup `json:"asked"`
// NoHolder are the machines that run a module with an item protected by a backup and hold no node-backup:
// nothing is asked of them, and the plan says so.
NoHolder []string `json:"no_holder,omitempty"`
}
// PlanBackup is one backup asked of one machine's backup holder for one module.
type PlanBackup struct {
Machine string `json:"machine"`
Module string `json:"module"`
// State is pending, done or failed.
State string `json:"state"`
// AskedAt is when the holder was asked; nil while it is not asked yet.
AskedAt *time.Time `json:"asked_at,omitempty"`
// Words are the backup's own: the holder's error for a failed one, "did not answer in ten minutes" for a silent one.
Words string `json:"words,omitempty"`
}
// PlanTimes are a walk's own moments beside its modules' (novox/hq ADR 0282 decision 6).
type PlanTimes struct {
// WindowClosed is when its batch's merge window closed: no merge for the window's length, or its maximum.