A bare alive moves last_seen and nothing else (convergence race fix) #7
@@ -151,6 +151,18 @@ func (e Enrolment) Heard(ctx context.Context, report Report) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A bare word that a node is there is not an account of what the machine did or holds: it
|
||||||
|
// moves last_seen and touches nothing else. This arrives every minute (link.AliveEvery),
|
||||||
|
// while a real report is rare, so recording it as one would overwrite the node's last real
|
||||||
|
// apply with an empty one — wiping the declaration digest that decides whether the node is
|
||||||
|
// current, the carried ports a push assigns around, and the clean-or-failed outcome — and a
|
||||||
|
// node that had just caught up would read as behind within the minute. The alive path calls
|
||||||
|
// this with only a node name; a real report always carries an account (something applied, or
|
||||||
|
// a refusal, or a failure), so those are the reports that get written down.
|
||||||
|
if report.Applied == nil && report.Refused == "" && len(report.Failed) == 0 {
|
||||||
|
return e.Inventory.Seen(ctx, node.ID)
|
||||||
|
}
|
||||||
// What it did is kept whichever way it went. Until this, a refusal or a failure moved
|
// What it did is kept whichever way it went. Until this, a refusal or a failure moved
|
||||||
// last_seen and the reason went to a log line, so "which machine is not doing what it was
|
// last_seen and the reason went to a log line, so "which machine is not doing what it was
|
||||||
// told" had no answer the next morning — which is the question a mesh exists to answer.
|
// told" had no answer the next morning — which is the question a mesh exists to answer.
|
||||||
|
|||||||
@@ -86,6 +86,69 @@ func TestACleanApplyIsRecordedAsOne(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestABareAliveDoesNotWipeTheDeclarationThatSaysANodeIsCurrent(t *testing.T) {
|
||||||
|
// A node reports it is alive every minute and describes what it applied rarely. If a bare
|
||||||
|
// alive were written down as a report it would replace the last real apply with an empty one
|
||||||
|
// — clearing the declaration digest current is measured against — so a node that had just
|
||||||
|
// caught up would read as behind within the minute, and never converge. The lab saw exactly
|
||||||
|
// this: a heavy wave whose apply outran the first heartbeat never reached `current`.
|
||||||
|
inv := inventory.ForTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
node, err := inv.AddNode(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// The mesh sent this node a declaration, and the node applied it and named which by digest.
|
||||||
|
const digest = "d640d1b6a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071829304152"
|
||||||
|
if err := inv.RecordSent(ctx, node.ID, digest); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{
|
||||||
|
Node: "anchor", Applied: []string{"a", "b"}, Declared: digest, Carried: []int{5432},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
currentOf := func(name string) bool {
|
||||||
|
reports, err := inv.LastReports(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, r := range reports {
|
||||||
|
if r.Node == name {
|
||||||
|
return r.Current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("no report for %s", name)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !currentOf("anchor") {
|
||||||
|
t.Fatal("a node that applied exactly what it was sent does not read as current")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now the node says only that it is there, as it does every minute.
|
||||||
|
if err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{Node: "anchor"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !currentOf("anchor") {
|
||||||
|
t.Fatal("a bare alive wiped the declaration digest, so a current node now reads as behind")
|
||||||
|
}
|
||||||
|
// And the last real account of what it did and holds is untouched.
|
||||||
|
doing, said, err := inv.DoingOf(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !said || doing.Outcome != inventory.OutcomeApplied || doing.Applied != 2 {
|
||||||
|
t.Fatalf("a bare alive overwrote the last real report: said=%v %+v", said, doing)
|
||||||
|
}
|
||||||
|
owned, _, err := inv.Owned(ctx, node.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(owned) != 2 {
|
||||||
|
t.Fatalf("a bare alive replaced the account of what the machine holds: %v", owned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAFailureDoesNotBecomeTheAccountOfWhatTheMachineHolds(t *testing.T) {
|
func TestAFailureDoesNotBecomeTheAccountOfWhatTheMachineHolds(t *testing.T) {
|
||||||
// A partial list is not an account of what the machine holds. Recording one as though it
|
// A partial list is not an account of what the machine holds. Recording one as though it
|
||||||
// were would tell a rebuilding node to remove what it still has — which is the fault that
|
// were would tell a rebuilding node to remove what it still has — which is the fault that
|
||||||
|
|||||||
Reference in New Issue
Block a user