Take the controller out of the judge, the checker's siblings and the assigner (issue 1126, B8)

The controller ended by its unassignment (ADR 0590) and mesh-sdk drops
its seat, the grants seat and placement's adopt and hand-back. The
indexer's judge still carried the controller's seat entry, the old
removal words and the two placement verbs, and counted 58 seats; the
checker still placed mesh-controller beside the node-engine's and the
catalogue's tests and failed a node-engine check without its trunk; the
assigner still asked a compose of the running controller, served adopt
and hand-back as stubs, and kept a policy-split finding that could no
longer arise.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 11:32:56 +01:00
parent d4aa1fe11b
commit f7f3518a04
15 changed files with 93 additions and 320 deletions
@@ -91,7 +91,7 @@ func (b *bed) hold(t *testing.T) *holder {
// 1622/AC1-AC3 and 1623 on the bus: started whole, the assigner composes a group holding a mesh-sdk pull request by
// asking the Go build seat for the composer at the member's head, and for the composer at the mesh-sdk the running
// controller pins; it waits naming the asks while they run, writes the verdict from their answers naming the mesh-sdk the
// node-engine pins; it waits naming the asks while they run, writes the verdict from their answers naming the mesh-sdk the
// head's composer was built with, and asks nothing more on a later pass of the same heads.
func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) {
b := startBed(t, "")
@@ -104,8 +104,8 @@ func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) {
b.putJSON(t, delivery.ModulesBucket, "web", mod)
b.place(t, "g14", "web")
running := strings.Repeat("3", 40)
b.place(t, "novox", "mesh-controller")
b.built(t, "mesh-controller", "novox/mesh-controller", running)
b.place(t, "novox", "mesh-host")
b.built(t, "mesh-host", "novox/mesh-host", running)
sdkHead := strings.Repeat("4", 40)
heads := []delivery.Head{{Repository: "novox/mesh-sdk", Number: 80, Head: sdkHead}, {Repository: "novox/web", Number: 7, Head: strings.Repeat("2", 40)}}
hash, _ := delivery.HeadsHash(heads)
@@ -121,8 +121,8 @@ func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) {
key, _ := delivery.CompositionKey("feat/x", hash)
// No holder takes the asks yet: the composition waits, naming them.
waits := b.verdict(t, key, func(v delivery.CompositionVerdict) bool { return v.State == delivery.CompositionWaits })
if !strings.Contains(waits.Words, "mesh-sdk "+sdkHead) || !strings.Contains(waits.Words, "mesh-controller at "+running) {
t.Fatalf("the waiting composition says %q, want the head's and the running controller's asks", waits.Words)
if !strings.Contains(waits.Words, "mesh-sdk "+sdkHead) || !strings.Contains(waits.Words, "mesh-host at "+running) {
t.Fatalf("the waiting composition says %q, want the head's and the running node-engine's asks", waits.Words)
}
h := b.hold(t)
v := b.verdict(t, key, func(v delivery.CompositionVerdict) bool { return v.State != delivery.CompositionWaits })
@@ -131,7 +131,7 @@ func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) {
}
asks := h.taken()
if len(asks) != 2 {
t.Fatalf("the seat was asked %d composes, want two (the head, the running controller)", len(asks))
t.Fatalf("the seat was asked %d composes, want two (the head, the running node-engine)", len(asks))
}
byRepo := map[string]delivery.StepAsk{}
for _, a := range asks {
@@ -143,8 +143,8 @@ func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) {
if a := byRepo["novox/mesh-sdk"]; a.Compose == nil || a.Compose.Fetch["commit"] != sdkHead || a.Compose.Dir != "go" {
t.Errorf("the head's ask = %+v", a.Compose)
}
if a := byRepo["novox/mesh-controller"]; a.Compose == nil || a.Compose.Fetch["commit"] != running || a.Compose.Dir != delivery.RootPath {
t.Errorf("the running controller's ask = %+v", a.Compose)
if a := byRepo["novox/mesh-host"]; a.Compose == nil || a.Compose.Fetch["commit"] != running || a.Compose.Dir != delivery.RootPath {
t.Errorf("the running node-engine's ask = %+v", a.Compose)
}
// A later pass of the same heads (woken by any change) asks nothing more.
on, _ := delivery.EncodePresence(delivery.Presence{Online: true})
@@ -30,10 +30,8 @@ type passer struct {
// pass says a condition once and a restart says it again (the router keeps one per key).
say func(asks.Condition) error
said map[string]bool
// findings is the assigner's findings bucket (its said conditions and policy-split, ADR 0564 §2, ADR 0565 §8); splits the
// modules whose policy-split finding stands, nil until the first pass, which deletes any a previous process left.
// findings is the assigner's findings bucket (its said conditions, ADR 0564 §2).
findings assign.Findings
splits map[string]bool
// failedProvisions are the failed provisions whose finding this process wrote (ADR 0570 §4).
failedProvisions map[string]bool
// missing are the required provisions with no key this process pulsed, and missingSaid those whose finding it wrote.
@@ -138,7 +136,6 @@ func (p *passer) wants(ctx context.Context) {
defer func() { p.provisionsMissing(ctx, missing) }()
defer p.unplacedFindings()
defer p.seatHolders(ctx)
defer p.policySplits(manifests, chosen)
p.clearStaleCalls()
placed := p.w.placedNodes()
puts := p.putsByModule()
@@ -159,7 +156,7 @@ func (p *passer) wants(ctx context.Context) {
stale := map[string]neitherOne{}
left := map[string]string{}
defer func() { p.sayNeither(neither, stale, left) }()
// Where grants are read from, once for the pass: the grants part's while the lease is handed away (issue 774).
// Where grants are read from, once for the pass: the grants part's (issue 774, ADR 0571 clause 9).
grants := p.w.grantsBucket()
for name := range names {
fp := trunk[name].FP2
@@ -925,55 +922,6 @@ func chosenOf(chosen map[string]catalogue.PersonPolicy, module string) *catalogu
return nil
}
// policySplits writes the finding policy-split for each module whose policy at the assigner differs from one the controller
// writes beside a composition of it, and deletes it once they agree (ADR 0565 §8): written when a split begins, never again
// while it stands. The first pass after a start deletes every one a previous process left that no longer splits.
func (p *passer) policySplits(manifests map[string]catalogue.Manifest, chosen map[string]catalogue.PersonPolicy) {
if p.findings.Put == nil {
return
}
now := time.Now
if p.now != nil {
now = p.now
}
split := map[string]bool{}
// The controller writes no policy any more (ADR 0571 §6): nothing is compared with.
for _, s := range assign.Splits(manifests, chosen, nil) {
split[s.Module] = true
if p.splits[s.Module] {
continue
}
value, err := assign.PolicySplitFinding(s, now())
if err == nil {
err = p.findings.Put(assign.PolicySplitKey(s.Module), value)
}
if err != nil {
p.logf("the policy split of %s is not written: %v", s.Module, err)
delete(split, s.Module)
}
}
gone := p.splits
if gone == nil && p.findings.Keys != nil {
gone = map[string]bool{}
if keys, err := p.findings.Keys(); err == nil {
for _, k := range keys {
if m, ok := strings.CutSuffix(strings.TrimPrefix(k, "module."), "."+assign.KindPolicySplit); ok && strings.HasPrefix(k, "module.") {
gone[m] = true
}
}
}
}
for m := range gone {
if !split[m] {
if err := p.findings.Delete(assign.PolicySplitKey(m)); err != nil {
p.logf("the policy split of %s is not deleted: %v", m, err)
split[m] = true // tried again at the next pass
}
}
}
p.splits = split
}
// provisionsOf answers, per module, the provisions it consumes on a node, by key (ADR 0570 §4), from the seat
// mesh-provisioner's state; nil while the state is not read (the seat not delivered yet), so no want waits on it. A failed
// provision is also said as the assigner's finding `provider.<key>.provision-failed` (a waiting one is the watchman's
@@ -312,17 +312,6 @@ func (s *seatTools) doAgain(args map[string]any) (any, error) {
return map[string]any{"proposal": p.ID, "again": p.Again, "follow": followProposal(p.ID)}, nil
}
// doAdopt is the verb adopt (§7): gone since the lease write. The seed was the controller's export proposed whole, and the
// mesh has no controller export to take over (ADR 0547 §11, ADR 0567 §11); nothing is written.
func (s *seatTools) doAdopt(args map[string]any, by string) (any, error) {
return nil, errors.New("adopt is gone: placement is the mesh's alone since the lease write, and there is no controller to take it over from (ADR 0547 §11, ADR 0567 §11)")
}
// doHandBack is the verb hand-back (§7): gone since the lease write, as adopt is; nothing is deleted.
func (s *seatTools) doHandBack(args map[string]any, by string) (any, error) {
return nil, errors.New("hand-back is gone: placement is the mesh's alone since the lease write, and there is no controller to hand it back to (ADR 0547 §11, ADR 0567 §11)")
}
// placementAnswer is what the verb placement answers.
type placementAnswer struct {
Keys map[string]keyed `json:"keys,omitempty"`
@@ -496,8 +485,6 @@ func (s *seatTools) tools() []stdio.Tool {
catalogue.PlacementVerbPropose: func(a map[string]any) (any, error) { return s.doPropose(a, stdio.CallerOf(a)) },
catalogue.PlacementVerbWithdraw: func(a map[string]any) (any, error) { return s.doWithdraw(a, stdio.CallerOf(a)) },
catalogue.PlacementVerbAgain: s.doAgain,
catalogue.PlacementVerbAdopt: func(a map[string]any) (any, error) { return s.doAdopt(a, stdio.CallerOf(a)) },
catalogue.PlacementVerbHandBack: func(a map[string]any) (any, error) { return s.doHandBack(a, stdio.CallerOf(a)) },
}
var out []stdio.Tool
for _, v := range seat.Serves {
@@ -35,9 +35,8 @@ func (s memStore) history(state, key string) ([]stdio.StateRevision, error) {
return []stdio.StateRevision{{Key: key, Value: v, Revision: uint64(len(v)), Op: "put"}}, nil
}
// ADR 0567 §2, §7, §10: the seat's seven verbs over the states. propose lays a proposal and writes it with its candidates,
// or refuses naming every finding and writes nothing; withdraw and again mark it for the holder; adopt and hand-back are
// answered as gone and change nothing (ADR 0547 §11, ADR 0567 §11); placement and
// ADR 0567 §2, §7, §10: the seat's five verbs over the states. propose lays a proposal and writes it with its candidates,
// or refuses naming every finding and writes nothing; withdraw and again mark it for the holder; placement and
// proposals read every state the seat keeps, and history names the stream for what the state no longer keeps.
func TestTheSeatsVerbs(t *testing.T) {
anchor := delivery.Placement{V: delivery.Version, Node: "anchor", Modules: []delivery.PlacedModule{{Module: "web"}}}
@@ -59,21 +58,8 @@ func TestTheSeatsVerbs(t *testing.T) {
})
s.now = func() time.Time { return time.Date(2026, 10, 28, 20, 0, 0, 0, time.UTC) }
// 774/S28/AC1: adopt and hand-back are answered as gone, naming their records, and change nothing and ask nobody.
raw, _ := delivery.EncodePlacement(anchor)
st[delivery.PlacementState].values["placement.anchor"] = raw
for verb, call := range map[string]func() (any, error){
"adopt": func() (any, error) { return s.doAdopt(map[string]any{"why": "take over"}, "operator@laptop") },
"hand-back": func() (any, error) { return s.doHandBack(map[string]any{"why": "back"}, "op") },
} {
if _, err := call(); err == nil || !strings.Contains(err.Error(), verb+" is gone") || !strings.Contains(err.Error(), "ADR 0567 §11") {
t.Errorf("%s: %v", verb, err)
}
}
if len(asked) != 0 || len(st[delivery.PlacementProposalsState].values) != 0 || len(st[delivery.PlacementCandidatesState].values) != 0 ||
len(st[delivery.PlacementState].values) != 1 {
t.Fatalf("adopt or hand-back changed something: asked %v", asked)
}
// withdraw and again mark the proposal; the holder ends or judges it.
got, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op")
@@ -194,10 +180,8 @@ func TestTheSeatsVerbs(t *testing.T) {
} else if rm := got.(proposed).Proposal; !slices.Equal(rm.Nodes(), []string{"joiner"}) || !slices.Equal(rm.RemovedNodes(), []string{"joiner"}) {
t.Errorf("the remove-node proposed %+v", rm)
}
if _, err := s.doHandBack(map[string]any{"why": "back"}, "op"); err == nil || !strings.Contains(err.Error(), "since the lease write") {
t.Errorf("a hand-back: %v", err)
}
if tools := s.tools(); len(tools) != 7 || tools[2].Name != "mesh-placement.propose" || tools[2].Run == nil || tools[2].Input == nil {
// 1126/B8: the seat serves its five verbs; adopt and hand-back went with the controller (ADR 0590).
if tools := s.tools(); len(tools) != 5 || tools[2].Name != "mesh-placement.propose" || tools[2].Run == nil || tools[2].Input == nil {
t.Errorf("the seat's verbs as served: %+v", tools)
}
t.Run("1953/D4-D6: the seat mesh-wants' upgrade and bus", wantsVerbs)
@@ -379,8 +363,7 @@ func TestTheSeatsVerbs(t *testing.T) {
// wantsVerbs (novox/hq ADR 0565 §7, §8, §9): upgrade answers each module's policy and its source; a person's choice is
// written with who and why, recorded as a hand act in the seat's log and said as policy-changed, and default takes it back;
// the bus is refused; compare lists the modules the controller writes another policy for; the process writes policy-split
// for a split and deletes it once they agree; bus names the node behind the indexer's newest.
// the bus is refused; compare lists each node's composition and the findings; bus names the node behind the indexer's newest.
func wantsVerbs(t *testing.T) {
fpOld, fpNew := delivery.SourcePrefix2+strings.Repeat("1", 64), delivery.SourcePrefix2+strings.Repeat("2", 64)
st := memStore{}
@@ -419,9 +402,10 @@ func wantsVerbs(t *testing.T) {
if p := policyOf("nats"); p.Policy != catalogue.PolicyRecord || p.From != catalogue.FromBus {
t.Errorf("nats = %+v", p)
}
// compare: the controller writes no policy, so no module splits; each node's own composition is the digest its have names.
// compare: each node's own composition is the digest its have names, and the assigner's findings; nothing splits, the
// controller being gone (1126/B8).
out, _ := w.doUpgrade(map[string]any{"compare": "true"}, "op")
if got := out.(map[string]any); len(got["splits"].([]assign.Split)) != 0 || got["empty"] != true || got["findings"] == nil ||
if got := out.(map[string]any); got["splits"] != nil || got["findings"] == nil ||
got["composed"].(map[string]string)["anchor"] != "sha256:"+strings.Repeat("c", 64) || len(got["composed"].(map[string]string)) != 1 {
t.Errorf("compare = %+v", got)
}
@@ -466,27 +450,7 @@ func wantsVerbs(t *testing.T) {
if watches[policiesState] != 0 {
t.Errorf("the seat's own small policies state was watched: %v", watches)
}
// The process says the split as its finding, and deletes it once they agree.
findings := map[string][]byte{}
p := &passer{w: newWorld(), logf: func(string, ...any) {}, findings: assign.Findings{
Put: func(k string, v []byte) error { findings[k] = v; return nil },
Delete: func(k string) error { delete(findings, k); return nil },
Keys: func() ([]string, error) { return []string{"module.gone.policy-split"}, nil }}}
man := map[string]catalogue.Manifest{}
for _, name := range []string{"web", "photos"} {
raw, _, _, _ := st.get(modulesState, name)
mod, _ := delivery.DecodeModule(raw)
mraw, _, _, _ := st.get(manifestsState, mod.Manifest)
kept, _ := delivery.DecodeManifest(mraw)
man[name], _ = catalogue.Decode([]byte(kept.JSON))
}
findings["module.gone.policy-split"] = []byte("{}")
// The controller writes no policy now (ADR 0571 §6): the process compares with nothing, and deletes a finding left.
p.policySplits(man, nil)
if len(findings) != 0 {
t.Errorf("findings = %v, want a stale one deleted", findings)
}
// a person's record for web: written, recorded, said; the split is gone.
// a person's record for web: written, recorded, said.
if _, err := w.doUpgrade(map[string]any{"module": "web", "policy": "record", "why": "the operator checks it"}, "person.jochen"); err != nil {
t.Fatal(err)
}
@@ -495,10 +459,6 @@ func wantsVerbs(t *testing.T) {
len(said) != 1 || !strings.HasPrefix(said[0], "policy-changed policy-changed.web.") {
t.Errorf("chosen %+v, asked %v, said %v", c, asked, said)
}
p.policySplits(man, chosen)
if len(findings) != 0 {
t.Errorf("findings after the choice = %v", findings)
}
if _, err := w.doUpgrade(map[string]any{"module": "nats", "policy": "roll", "why": "x"}, "op"); err == nil {
t.Error("the bus was given a policy")
}
@@ -133,7 +133,7 @@ type policyRow struct {
}
// doUpgrade is the verb upgrade: read every module's policy or one's; with policy and why set a person's choice, or take it
// back with default; with compare list the splits.
// back with default; with compare each node's composition and the assigner's findings.
func (s *wantsTools) doUpgrade(args map[string]any, by string) (any, error) {
manifests, _, err := s.manifests()
if err != nil {
@@ -157,12 +157,8 @@ func (s *wantsTools) doUpgrade(args map[string]any, by string) (any, error) {
}
}
}
splits := assign.Splits(manifests, chosen, nil) // the controller writes no policy to differ from (ADR 0571 §6)
if splits == nil {
splits = []assign.Split{}
}
out := map[string]any{"splits": splits, "empty": len(splits) == 0, "composed": composed}
// The assigner's findings, its said conditions and its policy splits as the condition keeper reads them (§8, §15).
out := map[string]any{"composed": composed}
// The assigner's findings, its said conditions as the condition keeper reads them (§15).
if found, err := s.values(health.FindingsState); err != nil {
out["findings-unread"] = err.Error()
} else {
@@ -8,7 +8,6 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-assigner/internal/assign"
@@ -128,27 +127,19 @@ func theWorldCountsTheValuesAKeyTakes(t *testing.T) {
if got := w.changed(delivery.HaveBucket, "gone"); got != 0 {
t.Errorf("changed = %d for a key never seen", got)
}
// ADR 0571 clause 9: grants are read from the grants part's bucket in every case, a lease held, handed away, absent or
// unreadable alike, and the controller's own grants count for nothing.
// ADR 0571 clause 9: grants are read from the grants part's bucket, and the controller's own grants count for nothing.
fp := delivery.SourcePrefix2 + strings.Repeat("d", 64)
gkey, _ := delivery.ControllerGrantKey("web", fp)
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{Accounts: []string{"ace.web"}})
for _, row := range []struct {
name, lease string
name string
inPart, inController bool
granted bool
}{
{"handed away, the part's grant", `{"handed-to":"the operator's hand-over"}`, true, false, true},
{"handed away, only the controller's grant", `{"handed-to":"x"}`, false, true, false},
{"a controller holds it, the part's grant", `{"instance":"novox/1","epoch":3}`, true, false, true},
{"a controller holds it, only its own grant", `{"instance":"novox/1"}`, false, true, false},
{"no lease read", ``, true, false, true},
{"an unreadable lease", `[1]`, true, false, true},
{"the part's grant", true, false, true},
{"only the controller's grant", false, true, false},
} {
p := newPasser(buckets{}, time.Now())
if row.lease != "" {
p.w.put(buscompose.LeaseBucket, buscompose.ControllerLeaseKey, []byte(row.lease))
}
if row.inPart {
p.w.put(delivery.PartGrantsBucket, gkey, grant)
}
@@ -23,9 +23,9 @@ const (
// composer is built at its repository's root, at the mesh-sdk its go.mod pins.
const SDKDir = "go"
// The modules whose mesh-sdk reads the manifests a plan sends (ADR 0482 clause 5): the controller composes and sends
// them, the node-engine applies them. Each is judged at its repository.
var ReaderModules = map[string]string{"mesh-controller": "novox/mesh-controller", "mesh-host": "novox/mesh-host"}
// The modules whose mesh-sdk reads the manifests a plan sends (ADR 0482 clause 5): the node-engine composes and applies
// them, the controller being gone (novox/hq ADR 0590, issue 1126). Each is judged at its repository.
var ReaderModules = map[string]string{"mesh-host": "novox/mesh-host"}
// HeadJob is the compose job of a group with a mesh-sdk member (ADR 0482 clauses 3 and 4, issue 1622): the composer
// built at the member's head composes the machines the change touches, as the trunk holds them and as the change leaves
@@ -17,8 +17,6 @@ import (
// A person's upgrade policy is the assigner's (novox/hq ADR 0565 §7, §8, issue 1953 D4, D5): kept in its state `policies`, one
// key per module, set and taken back through `mesh-wants.upgrade`. Every reader takes one derivation, mesh-sdk's
// catalogue.Upgrade: a person's choice over the module's word, its irreplaceable data and the default, the bus never rolled.
// Until the lease write the controller still writes a policy beside each composition's digest; where it differs from the
// assigner's, the assigner says so as its finding `policy-split`, naming mesh-wants.upgrade as the remedy.
// PoliciesBucket is the assigner's state `policies`.
var PoliciesBucket = delivery.AssignerModule + "_" + delivery.AssignerPoliciesState
@@ -57,64 +55,6 @@ func contains(list []string, s string) bool {
return false
}
// Split is a module whose policy at the assigner differs from one the controller writes beside a composition of it.
type Split struct {
Module string `json:"module"`
Policy string `json:"policy"`
From string `json:"from"`
Controller []string `json:"controller"`
}
// Splits is every module of manifests the controller writes a policy for that differs from the assigner's, sorted.
func Splits(manifests map[string]catalogue.Manifest, chosen map[string]catalogue.PersonPolicy, controller map[string][]string) []Split {
var out []Split
for module, theirs := range controller {
m, ok := manifests[module]
if !ok {
continue
}
policy, from, _, _ := PolicyOf(m, chosen)
for _, p := range theirs {
if p != policy {
out = append(out, Split{Module: module, Policy: policy, From: from, Controller: theirs})
break
}
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Module < out[j].Module })
return out
}
// KindPolicySplit is the assigner's finding of a split policy (ADR 0565 §8).
const KindPolicySplit = "policy-split"
// PolicySplitKey is a module's policy-split finding's key.
func PolicySplitKey(module string) string {
return health.FindingKey(health.ScopeModule, module, KindPolicySplit)
}
// PolicySplitFinding is the finding of one split, in mesh-sdk's layout.
func PolicySplitFinding(s Split, at time.Time) ([]byte, error) {
summary := fmt.Sprintf("%s's upgrade policy is %s at the assigner (%s) and %s at the controller", s.Module, s.Policy, s.From,
strings.Join(s.Controller, " and "))
return health.EncodeFinding(health.Finding{Key: PolicySplitKey(s.Module), Kind: KindPolicySplit,
Subject: health.Subject{Scope: health.ScopeModule, ID: s.Module}, Severity: health.Warning, Summary: summary,
Headline: clipHeadline(s.Module + "'s upgrade policy is split"),
Needs: "set it with mesh-wants.upgrade, so the assigner and the controller agree",
Explanation: asks.NeedsYou + "set it with mesh-wants.upgrade, so the assigner and the controller agree." + " The controller still " +
"writes a policy beside each composition until it hands its lease away; a choice made at its upgrade verb after the copy " +
"is not the assigner's.",
Resolved: s.Module + "'s upgrade policy agrees again", Evidence: []health.Evidence{{At: at.UTC(), Said: summary}},
Source: "assigner." + KindPolicySplit, Observer: delivery.AssignerModule, At: at.UTC()})
}
func clipHeadline(s string) string {
if len([]rune(s)) <= 60 {
return s
}
return string([]rune(s)[:60])
}
// KindProvisionFailed is the assigner's finding of a provision its module's want waits for that failed (ADR 0570 §4).
const KindProvisionFailed = "provision-failed"
@@ -186,3 +126,10 @@ func UnplacedFinding(node string, at time.Time) ([]byte, error) {
Resolved: node + " is placed", Evidence: []health.Evidence{{At: at.UTC(), Said: summary}},
Source: "assigner." + KindUnplaced, Observer: delivery.AssignerModule, At: at.UTC()})
}
func clipHeadline(s string) string {
if len([]rune(s)) <= 60 {
return s
}
return string([]rune(s)[:60])
}
@@ -60,14 +60,14 @@ func TestManifest(t *testing.T) {
}
// ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the
// module), says each put's step as the event put, and calls the hand-act seat and the bus node's backup holder.
// ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its seven verbs; keeps the seat's events on its stream
// placement for 30 days; calls the controller's export, its four verbs that keep its assignments in step and its
// hand-act log; and reads mesh-config's resolved values to compose every node a proposal touches.
// ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its five verbs (adopt and hand-back went with the
// controller, ADR 0590); keeps the seat's events on its stream placement for 30 days; calls the hand-act log; and reads
// mesh-config's resolved values to compose every node a proposal touches.
// ADR 0565 §7, §9 (issue 1953 D4): and the seat mesh-wants, serving upgrade and bus.
if len(m.Claims) != 2 || m.Claims[0].Name != catalogue.PlacementSeat || m.Claims[0].At() != catalogue.ScopeMesh ||
!slices.Equal(m.Claims[0].Serves, []string{"placement", "proposals", "propose", "withdraw", "again", "adopt", "hand-back"}) ||
!slices.Equal(m.Claims[0].Serves, []string{"placement", "proposals", "propose", "withdraw", "again"}) ||
m.Claims[1].Name != catalogue.WantsSeat || m.Claims[1].At() != catalogue.ScopeMesh || !slices.Equal(m.Claims[1].Serves, []string{"upgrade", "bus"}) {
t.Errorf("the assigner claims %+v, want mesh-placement with its seven verbs and mesh-wants with its two", m.Claims)
t.Errorf("the assigner claims %+v, want mesh-placement with its five verbs and mesh-wants with its two", m.Claims)
}
if seat, _ := catalogue.SeatNamed(catalogue.WantsSeat); catalogue.CanHold(m, seat) != nil {
t.Errorf("the assigner cannot hold mesh-wants: %v", catalogue.CanHold(m, seat))
+1 -3
View File
@@ -11,9 +11,7 @@
"proposals",
"propose",
"withdraw",
"again",
"adopt",
"hand-back"
"again"
]
},
{
@@ -2,7 +2,6 @@ package check
import (
"context"
"fmt"
"strconv"
)
@@ -33,12 +32,13 @@ const (
// besideRules are the repositories each core repository's tests read, as the tests state it (mesh-sdk go/internal/beside
// beside.go): the catalogue, the node-engine, the SDK, and the delivery's pinned forge dependencies (mesh-catalog
// checks/deps). Which commit of each is besideCommit's rule.
// checks/deps). Which commit of each is besideCommit's rule. No repository's tests read the controller, which ended by its
// unassignment (novox/hq ADR 0590, issue 1126); its own repository keeps its siblings while it holds a module.
var besideRules = map[string][]string{
controllerRepo: {catalogueRepository, nodeEngineRepo, SDKRepository},
nodeEngineRepo: {catalogueRepository, SDKRepository, controllerRepo},
nodeEngineRepo: {catalogueRepository, SDKRepository},
SDKRepository: {catalogueRepository, nodeEngineRepo},
catalogueRepository: {SDKRepository, controllerRepo},
catalogueRepository: {SDKRepository},
"novox/mesh-delivery": {SDKRepository, catalogueRepository},
}
@@ -82,10 +82,6 @@ func (j *Examiner) besideFor(ctx context.Context, c *Case, p Pull) ([]BesideRef,
out = append(out, BesideRef{Repository: repo, Commit: member.Head, Ref: refOf(member.Number)})
} else if head := heads[repo]; head != "" {
out = append(out, BesideRef{Repository: repo, Commit: head, atTrunk: true})
} else if repo == controllerRepo && p.Repository == nodeEngineRepo {
// The node-engine's step-nine test judges the controller's own manifest and fails without it (issue 1295): a
// checker that cannot place it says so, where placing none left that test to skip.
return nil, fmt.Errorf("the trunk head of %s is not known, so the controller the tests of %s#%d read cannot be placed beside them", repo, p.Repository, p.Number)
}
}
}
@@ -2,6 +2,7 @@ package check
import (
"maps"
"slices"
"strings"
"testing"
@@ -106,34 +107,31 @@ func TestTheSiblingsAreWhereTheTestsStateThem(t *testing.T) { // review D2, ADR
}
}
// 1295/R4g: the node-engine's tests read the controller's manifest, so a mesh-host pull request has mesh-controller beside it:
// at the group member's head when the group holds one, else at its trunk.
func hostCase(t *testing.T, pulls ...delivery.CandidatePull) *Case {
t.Helper()
c, err := delivery.DecodeCandidate(candidateOf(t, "feat/x", pulls...))
if err != nil {
t.Fatal(err)
}
cs := CaseOf(c, map[string]string{"novox/mesh-host": trunkA, controllerRepo: trunkA2})
if cs == nil {
t.Fatal("no case")
}
return cs
}
func TestTheNodeEnginesTestsHaveTheControllerBesideThem(t *testing.T) {
// 1126/B8 (novox/hq ADR 0590): the controller is gone, so no repository's tests read it: a mesh-host pull request has
// the catalogue and mesh-sdk beside it and no mesh-controller, even where its trunk is known and the group holds one.
func TestNoRepositorysTestsHaveTheControllerBesideThem(t *testing.T) {
const host = "novox/mesh-host"
b := newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{host: trunkA, controllerRepo: trunkA2, catalogueRepository: trunkS, SDKRepository: trunkS})
b.judge(t, hostCase(t, pullOf(host, 5, head1)))
if c := besideOf(t, b, host)[controllerRepo]; c.Commit != trunkA2 || c.Ref != "" {
t.Errorf("the controller beside the node-engine alone = %+v, want its trunk %s", c, trunkA2)
c, err := delivery.DecodeCandidate(candidateOf(t, "feat/x", pullOf(host, 5, head1), pullOf(controllerRepo, 9, head2)))
if err != nil {
t.Fatal(err)
}
b = newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{host: trunkA, controllerRepo: trunkA2, catalogueRepository: trunkS, SDKRepository: trunkS})
b.judge(t, hostCase(t, pullOf(host, 5, head1), pullOf(controllerRepo, 9, head2)))
if c := besideOf(t, b, host)[controllerRepo]; c.Commit != head2 || c.Ref != "refs/pull/9/head" {
t.Errorf("the controller beside the node-engine in a group = %+v, want the member's head %s", c, head2)
if v := b.judge(t, CaseOf(c, map[string]string{host: trunkA, controllerRepo: trunkA2})).Verdict; v.State == delivery.VerdictFail &&
strings.Contains(v.Failed.Words, "mesh-controller") {
t.Fatalf("verdict = %+v", v)
}
got := besideOf(t, b, host)
if _, ok := got[controllerRepo]; ok {
t.Errorf("the controller is beside the node-engine: %+v", got)
}
if _, ok := got[catalogueRepository]; !ok {
t.Errorf("the catalogue is not beside the node-engine: %+v", got)
}
for repo, siblings := range besideRules {
if repo != controllerRepo && slices.Contains(siblings, controllerRepo) {
t.Errorf("the tests of %s read the controller", repo)
}
}
}
@@ -148,15 +146,15 @@ func TestTheChangesOwnRepositoryIsBesideItAtItsTrunk(t *testing.T) {
}
}
// 1887/A12: a catalogue change's siblings hold the catalogue itself at the trunk it is merged onto, beside mesh-sdk and
// mesh-controller, so the catalogue's shrink-only list tests read $MESH_CHECK_BESIDE/mesh-catalog.
// 1887/A12: a catalogue change's siblings hold the catalogue itself at the trunk it is merged onto, beside mesh-sdk, so
// the catalogue's shrink-only list tests read $MESH_CHECK_BESIDE/mesh-catalog.
func TestACatalogueChangeHasTheCatalogueBesideItAtItsTrunk(t *testing.T) {
b := newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{catalogueRepository: trunkA, SDKRepository: trunkS, controllerRepo: trunkA2})
out := b.judge(t, caseOf(t, pullOf(catalogueRepository, 12, head1)))
// ADR 0548 §6: the outcome names the siblings read at their trunk's head, never the repository it merges.
if out.Beside[SDKRepository] != trunkS || out.Beside[controllerRepo] != trunkA2 || out.Beside[catalogueRepository] != "" {
t.Errorf("the siblings named for the verdict = %v, want mesh-sdk and mesh-controller at their trunks and not the catalogue", out.Beside)
if out.Beside[SDKRepository] != trunkS || out.Beside[controllerRepo] != "" || out.Beside[catalogueRepository] != "" {
t.Errorf("the siblings named for the verdict = %v, want mesh-sdk at its trunk and neither the controller nor the catalogue", out.Beside)
}
got := besideOf(t, b, catalogueRepository)
if c := got[catalogueRepository]; c.Commit != trunkA || c.Ref != "" {
@@ -188,15 +186,3 @@ func TestAMeshLabHeadGetsACheck(t *testing.T) {
t.Fatalf("mesh-lab was not fetched: %+v", fetched)
}
}
// 1295: the node-engine's step-nine test fails without the controller beside it, so a checker that knows no trunk head of the
// controller fails the check at the placing, loudly, and does not place none.
func TestTheNodeEnginesCheckFailsWhereTheControllersTrunkIsNotKnown(t *testing.T) {
const host = "novox/mesh-host"
b := newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{host: trunkA, catalogueRepository: trunkS, SDKRepository: trunkS})
v := b.judge(t, hostCase(t, pullOf(host, 5, head1))).Verdict
if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "mesh-controller") || !strings.Contains(v.Failed.Words, "cannot be placed") {
t.Fatalf("verdict = %+v", v)
}
}
@@ -16,11 +16,10 @@
// check gets is chosen from the inventory: mesh-catalog by the source of the `nats` module, mesh-host
// by the source of `mesh-host`, and mesh-sdk as the controller's sibling at the commit the controller's
// go.mod pins. Each repository's tests read these siblings, and the checker places exactly these
// (mesh-delivery's checker, besideRules, follows this list): mesh-controller's read mesh-catalog,
// mesh-host and mesh-sdk; mesh-host's read mesh-catalog, mesh-sdk and mesh-controller (its step-nine
// test reads the controller's own manifest, novox/hq issue 1295); mesh-sdk's read mesh-catalog and
// mesh-host; mesh-catalog's read mesh-sdk and mesh-controller; mesh-delivery's read mesh-sdk and
// mesh-catalog. In a mesh where either module has no source repository nothing is
// (mesh-delivery's checker, besideRules, follows this list): mesh-host's read mesh-catalog and
// mesh-sdk; mesh-sdk's read mesh-catalog and mesh-host; mesh-catalog's read mesh-sdk; mesh-delivery's
// read mesh-sdk and mesh-catalog. None reads mesh-controller, which ended by its unassignment (novox/hq
// ADR 0590). In a mesh where either module has no source repository nothing is
// cloned, and these tests fail loudly with "not beside this check": a cause in the setup, not in the
// change. And in a delivery group that holds a mesh-catalog pull request, these tests read the
// catalogue's main, not the group's head.
+3 -40
View File
@@ -4005,7 +4005,9 @@ func toolsSecretProblems(m catalogue.Manifest) []string {
return problems
}
var BusRemovals = []string{catalogue.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs, catalogue.RemovesAssignmentsStream}
// BusRemovals are what a bus account may remove, the closed set of novox/hq ADR 0564 Limits 4 as ADR 0590 clause 6
// changes it: the node-engine's acts and the ended controller's leftovers.
var BusRemovals = []string{catalogue.RemovesEngineActs, catalogue.RemovesControllerLeftovers}
// ReadsHolderProblems is what a holder of node-reads is held to (novox/hq ADR 0310 §3): it reads as an
// account of its own, so the node's file modes bound what it sees. Carried by the machine's runtime it would
@@ -7251,37 +7253,6 @@ func namesOfAccessIDs(accesses map[string]string) []string {
var renderFuncs = template.FuncMap{"quote": quote}
var defaultSeats = append([]catalogue.Seat{
// The control plane states what it did under the seat it holds (novox/hq ADR 0134): a role's
// events belong to the role, so they keep their address while the holder is replaced. No accepts,
// so no work queue is raised for it — only what its holder may say.
// And it serves the mesh's own verbs as the seat's tools (novox/hq ADR 0154): `status`, `push`,
// `assign` and the rest are a role's interface, not a container's, and stay addressable while
// the control plane is replaced.
{Name: catalogue.ControllerSeatName, Scope: catalogue.ScopeMesh, Decision: "novox/hq ADR 0079",
// And what is wrong, as it changes, and the self-check's heartbeat (novox/hq to-be 45 §2, §4).
Emits: []string{"applied", "refused", "built-before",
"condition-raised", "condition-changed", "condition-cleared", "doctor-heartbeat",
// A value given by hand, replaced after its module's first good start (novox/hq ADR 0228).
"secret-replaced",
// Every act a healer takes (novox/hq to-be 45 §7).
"healer-acted",
// A build put back after its gate failed (novox/hq ADR 0236, to-be 45 §8).
"rolled-back",
// A pull request's merge check, judged (novox/hq to-be 45 §9).
"checked",
// A walk kept in a new state, and a tier sent to a machine: kept until the catalogue's main consumes them no
// more; nothing emits them (ADR 0539).
"plan-moved",
"sent",
// A key of a record mirror written or removed (novox/hq ADR 0289, design 54 §3; design 57 F3).
"assignment-changed", "declared-config-changed", "predefined-changed",
// A builder's key not taken in because the artifact store does not hold an artifact it names (novox/hq
// issue 1937): the builder builds it again.
"artifacts-gone",
// A bucket of a state this release deleted, deleted at the controller's first start (novox/hq ADR 0539
// clause 13): the merge window's, the silent sends' and the building plans' hold, each said once.
"bucket-deleted"},
Serves: catalogue.ControllerVerbs},
// The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable,
// served by whichever module holds the seat with tools of these names.
{Name: "mesh-store", Scope: catalogue.ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079",
@@ -7957,14 +7928,6 @@ func placementVerbs() []catalogue.Verb {
"stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. " +
"Nothing more: the recovery of a proposal whose judgement or ask was lost.",
Input: schema(map[string]string{"id": "the proposal's id"}, []string{"id"})},
{Name: catalogue.PlacementVerbAdopt, Optional: true, Description: "Take placement over from the controller: its " +
"placement-export is proposed whole as the seed, every node's key, which every node must answer as the same. " +
"Its Approve is the take-over; at the warrant the export is read again and a changed one is proposed anew.",
Input: schema(map[string]string{"why": "why, shown to the operator"}, []string{"why"})},
{Name: catalogue.PlacementVerbHandBack, Optional: true, Description: "The operator's alone, recorded as a hand act: " +
"delete every placement key, the history kept, so every node returns to the controller's compositions. " +
"Refused after the lease write.",
Input: schema(map[string]string{"why": "why, recorded with the hand act"}, []string{"why"})},
}
}
@@ -2588,7 +2588,7 @@ func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) {
for _, c := range []struct {
name, manifest, refused string
}{
{name: "the bus module's tools removing", manifest: tools(broker, `{"removes":["DEAD_LETTER_NOTICES.controller","mesh-host_acts","ASSIGNMENTS"]}`)},
{name: "the bus module's tools removing", manifest: tools(broker, `{"removes":["mesh-host_acts","controller-leftovers"]}`)},
{name: "a removal outside the set", manifest: tools(broker, `{"removes":["EVENTS.a_m"]}`), refused: "not one of"},
{name: "a removal beside a grant", manifest: tools(broker, `{"removes":["mesh-host_acts"],"invokes":["nats.nats_streams"]}`), refused: "with nothing else"},
{name: "another module's tools removing", manifest: tools(`[]`, `{"removes":["mesh-host_acts"]}`), refused: "only the bus module"},
@@ -2613,7 +2613,7 @@ func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) {
}
accounts := m.BusAccounts()
if len(accounts) != 1 || accounts[0].ID != "nats-tools" || !accounts[0].Tools ||
!strSliceEqual(accounts[0].Bus.Removes, []string{catalogue.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs, catalogue.RemovesAssignmentsStream}) {
!strSliceEqual(accounts[0].Bus.Removes, []string{catalogue.RemovesEngineActs, catalogue.RemovesControllerLeftovers}) {
t.Errorf("%s: the accounts are %+v", c.name, accounts)
}
// Resolved, the build goes and its bundles keep the bus (build.go).
@@ -4357,7 +4357,7 @@ func TestTheBussAccountSaysNothingOnTheBus(t *testing.T) {
"invokes":["seat:mesh-hand-acts.hand-act"],"consumes":[],
"tools":["nats_server","nats_bucket_expiry"],
"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/nats-tools",
"binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["DEAD_LETTER_NOTICES.controller","ASSIGNMENTS"]}}]}}`)
"binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["controller-leftovers"]}}]}}`)
nm, err := ParseManifest(nats)
if err != nil {
t.Fatalf("the bus module whose tools bundle invokes a seat verb was refused: %v", err)
@@ -4970,9 +4970,10 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
delivered[s.Delivers] = s.Name
}
}
// Forty-seven with mesh-placement (novox/hq ADR 0567), serving its seven verbs.
// Forty-seven with mesh-placement (novox/hq ADR 0567), serving its five verbs: adopt and hand-back went with the
// controller (novox/hq ADR 0590, issue 1126).
if s, ok := catalogue.SeatNamed(catalogue.PlacementSeat); !ok || s.Scope != catalogue.ScopeMesh || !reflect.DeepEqual(catalogue.VerbNames(s.Serves),
[]string{"placement", "proposals", "propose", "withdraw", "again", "adopt", "hand-back"}) {
[]string{"placement", "proposals", "propose", "withdraw", "again"}) {
t.Errorf("the placement seat is %v, %v", ok, s)
}
// The checker's seat serves the try, which replaces the shell's merge check (ADR 0547 §10, ADR 0245).
@@ -5049,9 +5050,10 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
// Forty-eight with mesh-delivery-checker (novox/hq ADR 0547 §10, issue 774); forty-nine with node-engine (novox/hq ADR 0563 §1);
// fifty-three with the four health seats (novox/hq ADR 0564 clause 1); fifty-five with mesh-wants and mesh-hand-acts
// (novox/hq ADR 0565 clause 1); fifty-six with mesh-artifact-sweep (novox/hq ADR 0571 clause 2);
// fifty-seven with mesh-grants (ADR 0582 clause 1); fifty-eight with mesh-enrolment (novox/hq ADR 0587 §1).
if len(catalogue.Seats()) != 58 {
t.Errorf("the mesh defines %d seats rather than 58; the set is closed, so a change here is "+
// fifty-seven with mesh-grants (ADR 0582 clause 1); fifty-eight with mesh-enrolment (novox/hq ADR 0587 §1); fifty-six
// without the controller's seat and mesh-grants, which served its lease write alone (novox/hq ADR 0590, issue 1126).
if len(catalogue.Seats()) != 56 {
t.Errorf("the mesh defines %d seats rather than 56; the set is closed, so a change here is "+
"a decision (novox/hq ADR 0110): %s", len(catalogue.Seats()), seatNames())
}
}
@@ -5286,10 +5288,10 @@ func TestUsingASeatNobodyDeclaresIsRefused(t *testing.T) {
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.status"}}, unclaimed, true},
{"a verb of a mesh seat whose holder lives in another repository", "",
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-watchdogs.signals"}}, telegram(), false},
{"a verb a mesh seat does not promise", `the seat mesh-controller serves no verb "root-freed"`,
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-controller.root-freed"}}, telegram(), false},
{"the controller's verb: the controller answers its seat", "",
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-controller.root-free"}}, telegram(), false},
{"a verb a mesh seat does not promise", `the seat mesh-placement serves no verb "adopt"`,
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-placement.adopt"}}, telegram(), false},
{"the controller's seat is gone (ADR 0590)", `a verb of the seat "mesh-controller", ` + catalogue.UndeclaredSeat,
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-controller.root-free"}}, telegram(), true},
{"the engine's verb: every node's engine holds its seat", "",
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-engine.root-free"}}, telegram(), false},
{"a verb the engine's seat does not promise", `the seat node-engine serves no verb "root-freed"`,