Two controllers could both act (issue 204), a reconcile's report could overtake the apply after it and the digest decided (issue 267), and a grant could make a second writer of a machine's report. - The lease (internal/lease, ADR 0229): mesh-controller_lease key `holder`, 15 s age, renewed every 5 s by compare-and-set; the epoch is the revision it was taken at. The gate is the clock (stops 3 s before expiry); a refused renewal is a loss and the process exits; a holder that stops gives it back. serve takes it before asserting the bus. Epochs kept in the store (migration 0068 controller_epoch) as a floor: a bucket raised from nothing is compacted past it. Unleased (no epoch, S12 urgent) only when nobody holds it and the bus will not let it be written. A shell command acts under the holder's epoch, or its own lease when none. - Declarations carry `epoch` inside the signed envelope, only to a machine whose latest account carried a report_sequence (mesh-host #35); would-send is composed with the epoch last sent. Allot and the send both pass the gate. - Reports: contract in internal/link/order.go (epoch, sequence, report_sequence, older_than, refused_older). Accounts kept by epoch, then sequence, then report sequence; older refused, counted; unordered reports keep the digest rule. Plans by compare-and-set on a revision, with epoch. Conditions and calls carry the epoch and are not written off the lease. - S12 and S13 (naming the writer by epoch) watched, D5 run; reset of the bucket said. Writers table compiled in and enforced in PermissionsFor; the controller no longer publishes mesh.control.>. A contract per consumed kind, and the empty-on-error lint over the repository. - mesh-host pinned to its main with the epoch in the validator (D1 validates the envelope as sent). Needs mesh-host's genesis lock with the lease grant (mesh-host PR) for TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose.
114 lines
5.0 KiB
Go
114 lines
5.0 KiB
Go
package link
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// The contract of order.go, case by case: what a node-engine refuses of a declaration (the rule
|
|
// mesh-host states, restated so the controller's tests say what the machines do with what it sends),
|
|
// what the controller refuses of a report, and the bytes on the wire both ways.
|
|
|
|
func TestADeclarationIsOlderOnlyWhenBothClaimAnEpoch(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name string
|
|
arriving Order
|
|
held Order
|
|
older bool
|
|
}{
|
|
{"a newer sequence of the same epoch", Order{57, 12}, Order{57, 11}, false},
|
|
{"the same declaration again (a reconcile)", Order{57, 12}, Order{57, 12}, false},
|
|
{"a lower sequence of the same epoch", Order{57, 11}, Order{57, 12}, true},
|
|
// Issue 204: a controller that lost its lease goes on sending.
|
|
{"an older epoch, whatever its sequence", Order{41, 99}, Order{57, 12}, true},
|
|
{"a newer epoch, whatever its sequence", Order{57, 3}, Order{41, 99}, false},
|
|
{"no epoch arriving: a rolled-back controller is not stranded", Order{0, 11}, Order{57, 12}, false},
|
|
{"no epoch held: what the machine held before any lease", Order{57, 11}, Order{0, 12}, false},
|
|
{"no order at all", Order{}, Order{57, 12}, false},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if got := c.arriving.Older(c.held); got != c.older {
|
|
t.Fatalf("%s against %s: older %v, want %v", c.arriving.Words(), c.held.Words(), got, c.older)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAnAccountIsOlderByEpochThenSequenceThenReportSequence(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name string
|
|
arriving Account
|
|
kept Account
|
|
older bool
|
|
}{
|
|
// Issue 267: a reconcile's account of declaration 11 arrives after the apply's of 12.
|
|
{"an account of an older declaration", Account{Order{57, 11}, 40}, Account{Order{57, 12}, 41}, true},
|
|
{"an older report of the same declaration", Account{Order{57, 12}, 40}, Account{Order{57, 12}, 41}, true},
|
|
{"a newer report of the same declaration (a reconcile after the apply)",
|
|
Account{Order{57, 12}, 42}, Account{Order{57, 12}, 41}, false},
|
|
{"the same report again (redelivered)", Account{Order{57, 12}, 41}, Account{Order{57, 12}, 41}, false},
|
|
{"an account of a newer declaration, whatever its report sequence",
|
|
Account{Order{57, 13}, 1}, Account{Order{57, 12}, 41}, false},
|
|
{"an account of an older epoch", Account{Order{41, 99}, 50}, Account{Order{57, 12}, 41}, true},
|
|
{"an account of a newer epoch", Account{Order{58, 1}, 2}, Account{Order{57, 12}, 41}, false},
|
|
{"no epoch either side: the sequences", Account{Order{0, 11}, 50}, Account{Order{0, 12}, 41}, true},
|
|
{"no report sequence: the declaration's alone", Account{Order{57, 12}, 0}, Account{Order{57, 12}, 41}, false},
|
|
{"an unordered declaration: the report sequences", Account{Order{}, 40}, Account{Order{}, 41}, true},
|
|
{"nothing kept yet", Account{Order{57, 3}, 1}, Account{}, false},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if got := c.arriving.OlderThan(c.kept); got != c.older {
|
|
t.Fatalf("%+v against %+v: older %v, want %v", c.arriving, c.kept, got, c.older)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The bytes, as mesh-host's report writes them: every key optional, absent when zero.
|
|
func TestTheOrderOnTheWire(t *testing.T) {
|
|
// A stale refusal, as the node-engine says it: the refused declaration's order at the top, the one
|
|
// it holds in older_than, and how many it has refused ever.
|
|
raw := []byte(`{"node":"anchor","refused":"older","declared":"d1","epoch":41,"sequence":11,` +
|
|
`"report_sequence":7,"older_than":{"epoch":57,"sequence":12},"refused_older":3}`)
|
|
var r Report
|
|
if err := json.Unmarshal(raw, &r); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.Order != (Order{41, 11}) || r.ReportSequence != 7 || r.OlderThan == nil || *r.OlderThan != (Order{57, 12}) ||
|
|
r.RefusedOlder != 3 {
|
|
t.Fatalf("a node-engine's stale refusal reads as %+v", r)
|
|
}
|
|
if !r.Ordered() || !r.ReadsEpoch() || !r.StaleRefusalOf() {
|
|
t.Fatalf("a node-engine that orders its reports reads as one that does not: %+v", r)
|
|
}
|
|
|
|
// A report from a node-engine older than the contract says none of it.
|
|
var older Report
|
|
if err := json.Unmarshal([]byte(`{"node":"anchor","applied":["a"],"declared":"d1"}`), &older); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if older.Ordered() || older.ReadsEpoch() || older.StaleRefusalOf() {
|
|
t.Fatalf("an older node-engine's report reads as ordered: %+v", older)
|
|
}
|
|
// Its stale refusal, in the words it has always used, is still one.
|
|
older.Refused = "this declaration " + StaleRefusal + ": it is sequence 3"
|
|
if !older.StaleRefusalOf() {
|
|
t.Fatal("an older node-engine's refusal of an older sequence does not read as stale")
|
|
}
|
|
|
|
// And a report with no order puts no order key on the wire.
|
|
raw, err := json.Marshal(Report{Node: "anchor", Declared: "d1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var keys map[string]any
|
|
if err := json.Unmarshal(raw, &keys); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, key := range []string{"epoch", "sequence", "report_sequence", "older_than", "refused_older"} {
|
|
if _, there := keys[key]; there {
|
|
t.Fatalf("an unordered report carries %s: %s", key, raw)
|
|
}
|
|
}
|
|
}
|