Merge pull request 'Keep the account of the sent declaration over an older one (hq issue 267)' (#74) from fix/stale-report-overwrites into main

This commit was merged in pull request #74.
This commit is contained in:
2026-10-05 23:47:10 +00:00
3 changed files with 85 additions and 2 deletions
+2 -2
View File
@@ -851,9 +851,9 @@ func (i *Inventory) DoingOf(ctx context.Context, name string) (Doing, bool, erro
var d Doing var d Doing
var failed []byte var failed []byte
err = i.store.Pool().QueryRow(ctx, err = i.store.Pool().QueryRow(ctx,
`select outcome, refused, failed, applied, at, failing_since, failures `select outcome, refused, failed, applied, at, failing_since, failures, coalesce(declared, '')
from node_report where node = $1`, node.ID). from node_report where node = $1`, node.ID).
Scan(&d.Outcome, &d.Refused, &failed, &d.Applied, &d.At, &d.Since, &d.Times) Scan(&d.Outcome, &d.Refused, &failed, &d.Applied, &d.At, &d.Since, &d.Times, &d.Declared)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return Doing{}, false, nil return Doing{}, false, nil
} }
+19
View File
@@ -410,6 +410,25 @@ func (e Enrolment) Heard(ctx context.Context, report Report) (news bool, err err
if err := e.Inventory.RecordCarried(ctx, report.Node, report.Carried); err != nil { if err := e.Inventory.RecordCarried(ctx, report.Node, report.Carried); err != nil {
return false, err return false, err
} }
// **An account of a declaration the mesh has moved past never replaces the account it keeps**
// (novox/hq issue 267). The machine-facts half of such a report is kept above, whenever it
// arrives; this half is the machine's word on what it did with what it was sent, and the release
// plan reads it as such. A reconcile that held a machine to the older declaration a moment before
// the newer one arrived reported *after* the newer apply's report, and stored last, it read as the
// machine never having applied what it was sent — the plan waited until somebody pushed by hand.
// One row per node means the last write wins, so the order of arrival must not decide it.
if report.Declared != "" {
sent, err := e.Inventory.Outstanding(ctx, report.Node)
if err != nil {
return false, err
}
if Superseded(report.Declared, sent) {
log.Printf("kept what %s says about the machine, and not its account of declaration %s: "+
"the mesh has sent it %s since", report.Node, short(report.Declared), short(sent))
return false, e.Inventory.Seen(ctx, node.ID)
}
}
// **Whether this is news** is the store's answer: it holds the previous report, and a machine // **Whether this is news** is the store's answer: it holds the previous report, and a machine
// that reconciles every minute says the same thing until something changes (novox/hq ADR 0134). // that reconciles every minute says the same thing until something changes (novox/hq ADR 0134).
news, err = e.Inventory.RecordDoing(ctx, node.ID, doing) news, err = e.Inventory.RecordDoing(ctx, node.ID, doing)
+64
View File
@@ -260,3 +260,67 @@ func TestWhatFiltersAMachineIsKeptFromItsReport(t *testing.T) {
t.Fatalf("the next report did not replace what filters the machine: %+v", again) t.Fatalf("the next report did not replace what filters the machine: %+v", again)
} }
} }
// Measured on the home server (novox/hq issue 267): the mesh sent d2; the machine applied it and
// reported, and a reconcile's report about d1 — made just before d2 arrived — reached the mesh after
// it. Stored last, it read as the machine never having applied d2, and the release plan waited on a
// report it had already been given.
func TestAnAccountOfAnOlderDeclarationDoesNotReplaceTheNewer(t *testing.T) {
inv := inventory.ForTest(t)
ctx := context.Background()
node, err := inv.AddNode(ctx, "home-server")
if err != nil {
t.Fatal(err)
}
if err := inv.RecordSent(ctx, node.ID, "d2", nil); err != nil {
t.Fatal(err)
}
heard := link.Enrolment{Inventory: inv}
if _, err := heard.Heard(ctx, link.Report{Node: "home-server", Declared: "d2",
Applied: []string{"a", "b"}, Outward: []string{"eth0"}}); err != nil {
t.Fatal(err)
}
if _, err := heard.Heard(ctx, link.Report{Node: "home-server", Declared: "d1",
Applied: []string{"a"}, Outward: []string{"eth1"}}); err != nil {
t.Fatal(err)
}
doing, said, err := inv.DoingOf(ctx, "home-server")
if err != nil || !said {
t.Fatalf("no account kept: %v", err)
}
if doing.Declared != "d2" || doing.Applied != 2 {
t.Fatalf("the older report replaced the account of what was sent: %+v", doing)
}
// What it says about the machine is kept whenever it arrives, as before.
if links, err := inv.OutwardLinksOf(ctx, "home-server"); err != nil || len(links) != 1 || links[0] != "eth1" {
t.Fatalf("what the older report said about the machine was not kept: %v %v", links, err)
}
}
// The account of the declaration that is outstanding replaces whatever was kept, and so does one
// from a machine nothing was ever recorded as sent to.
func TestAnAccountOfTheSentDeclarationReplacesTheKeptOne(t *testing.T) {
inv := inventory.ForTest(t)
ctx := context.Background()
node, err := inv.AddNode(ctx, "home-server")
if err != nil {
t.Fatal(err)
}
heard := link.Enrolment{Inventory: inv}
if _, err := heard.Heard(ctx, link.Report{Node: "home-server", Declared: "d1", Applied: []string{"a"}}); err != nil {
t.Fatal(err)
}
if err := inv.RecordSent(ctx, node.ID, "d2", nil); err != nil {
t.Fatal(err)
}
if _, err := heard.Heard(ctx, link.Report{Node: "home-server", Declared: "d2", Applied: []string{"a", "b"}}); err != nil {
t.Fatal(err)
}
doing, _, err := inv.DoingOf(ctx, "home-server")
if err != nil {
t.Fatal(err)
}
if doing.Declared != "d2" || doing.Applied != 2 {
t.Fatalf("the account of what was sent was not kept: %+v", doing)
}
}