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.
83 lines
3.3 KiB
Go
83 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// One machine that cannot be worked out is not a reason to leave the mesh unconverged.
|
|
//
|
|
// A whole-mesh push refused outright the moment any single node failed to resolve, so a module on
|
|
// the anchor requiring a provision nobody had assigned a provider for stopped every OTHER machine
|
|
// from being sent anything — machines with no relation to the fault, and nothing wrong with them.
|
|
// The failure and the punishment were on different machines.
|
|
//
|
|
// It is the same rule an un-hostable module already follows one level down (a92c11b: one module on
|
|
// the wrong machine no longer refuses the whole node), applied one level up.
|
|
func TestOneUnresolvableNodeStillLetsTheRestBeSent(t *testing.T) {
|
|
sending, refusals := composeEach(
|
|
[]string{"anchor", "home-server", "laptop"}, numbered(),
|
|
func(node string) (sendable, error) {
|
|
if node == "anchor" {
|
|
return sendable{}, errors.New(`nothing provides "acme-ca", wanted by route-proxy`)
|
|
}
|
|
return sendable{Resources: []map[string]any{{"id": node + ".thing"}}}, nil
|
|
})
|
|
|
|
var told []string
|
|
for _, s := range sending {
|
|
told = append(told, s.node)
|
|
}
|
|
if strings.Join(told, ",") != "home-server,laptop" {
|
|
t.Errorf("a machine with nothing wrong with it was not sent: %v", told)
|
|
}
|
|
if len(refusals) != 1 || !strings.Contains(refusals[0], "anchor") ||
|
|
!strings.Contains(refusals[0], "acme-ca") {
|
|
t.Errorf("the machine that could not be worked out was not named with its reason: %v",
|
|
refusals)
|
|
}
|
|
}
|
|
|
|
// A machine whose declaration composes to nothing is SENT the empty declaration, not skipped
|
|
// (novox/hq issue 127): it may have held something before, and only sending the empty
|
|
// declaration tells it to drop what the mesh owned. It is never a refusal.
|
|
func TestAnEmptyDeclarationIsSentSoTheNodeDropsWhatItHeld(t *testing.T) {
|
|
sending, refusals := composeEach([]string{"spare"}, numbered(),
|
|
func(string) (sendable, error) { return sendable{}, nil })
|
|
if len(sending) != 1 || len(refusals) != 0 {
|
|
t.Errorf("an empty declaration must be sent, not skipped or refused: %v / %v", sending, refusals)
|
|
}
|
|
}
|
|
|
|
// A push that skipped a machine still ends badly, and says what was sent.
|
|
//
|
|
// **Skipping is not succeeding.** The mesh is not in the state somebody asked for, so the command
|
|
// exits non-zero — but it says how many machines it did reach, because the old message ("nothing
|
|
// was sent") was the very claim that had become untrue.
|
|
func TestASkippedMachineIsStillAnError(t *testing.T) {
|
|
if err := couldNotBeResolved(nil, 3); err != nil {
|
|
t.Fatalf("a push that resolved every machine reported a problem: %v", err)
|
|
}
|
|
|
|
err := couldNotBeResolved([]string{"anchor:\nnothing provides \"acme-ca\""}, 2)
|
|
if err == nil {
|
|
t.Fatal("a push that could not work out a machine reported success")
|
|
}
|
|
said := err.Error()
|
|
if strings.Contains(said, "nothing was sent") {
|
|
t.Errorf("the push says nothing was sent, and it sent two machines: %q", said)
|
|
}
|
|
for _, want := range []string{"anchor", "acme-ca", "2 other node(s) were"} {
|
|
if !strings.Contains(said, want) {
|
|
t.Errorf("the refusal does not say %q: %q", want, said)
|
|
}
|
|
}
|
|
}
|
|
|
|
// numbered is an allotter for tests: one higher per call, as the inventory's is per machine.
|
|
func numbered() func(string) (order, error) {
|
|
var n int64
|
|
return func(string) (order, error) { n++; return order{sequence: n}, nil }
|
|
}
|