diff --git a/go/catalogue/holdings_test.go b/go/catalogue/holdings_test.go index 0159490..c8a15b5 100644 --- a/go/catalogue/holdings_test.go +++ b/go/catalogue/holdings_test.go @@ -170,10 +170,10 @@ func TestAMembershipForTheNewBusIsComposedAsASealedFile(t *testing.T) { func TestAStoreRowWithoutAProtocolKeepsTheCompiledOne(t *testing.T) { was := Seats() t.Cleanup(func() { UseSeats(was) }) - UseSeats([]Seat{{Name: "mesh-build-machine", Scope: ScopeMesh, Decision: "row"}}) - got, ok := SeatNamed("mesh-build-machine") - if !ok || len(got.Accepts) == 0 { - t.Fatalf("the build machine's seat lost what it accepts when loaded from the store: %+v", got) + UseSeats([]Seat{{Name: "node-build-go", Scope: ScopeNode, Decision: "row"}}) + got, ok := SeatNamed("node-build-go") + if !ok || len(got.Accepts) == 0 || !got.PulledByKind { + t.Fatalf("the Go build seat lost what it accepts when loaded from the store: %+v", got) } if got.Decision != "row" { t.Fatalf("the store's own columns were not kept: %+v", got) diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index 31269df..05d4961 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -45,6 +45,10 @@ type Seat struct { // today — which the bus refuses, because a namespace belongs to who it is named for. Accepts []string Emits []string + // PulledByKind says each accepted kind is a worker of its own on the seat's stream, which its holders pull in the + // order Accepts lists them (novox/hq ADR 0525 §2, ADR 0536 §4): the build seats'. Compiled, never stored, like + // Receives. + PulledByKind bool // Serves carries each verb in full — name, description, schema — because a role's tools are the // mesh's to define and an agent's to call (novox/hq ADR 0132, design 33 §2). Serves []Verb @@ -238,31 +242,19 @@ var defaultSeats = append([]Seat{ // They keep their names until that migration is done deliberately, apart from the node-* pass. {Name: "npm-package-registry", Scope: ScopeMesh, Delivers: "npm-package-registry", Decision: "novox/hq ADR 0109"}, {Name: "git", Scope: ScopeMesh, Delivers: "git", Decision: "novox/hq ADR 0111"}, - // A build is work submitted to this role and its outcome is the role's own event (ADR 0129). - // One publish reaches whoever asked, the controller that records it, and the catalogue that - // places it in the graph — what the old bus's shared exchange did for free. - // A build says what it does as it does it (novox/hq ADR 0157): `started` when work is taken, - // `log.` for every line, `built` for the outcome. The log's tail token is the build's - // id, so a reader follows one build by subject alone. - // **Node-scoped, and every holder takes from one queue** (novox/hq ADR 0190): a build is asked of - // the role, and whichever machine holding the seat is idle pulls it. One holder per machine is - // what the scope says; sharing the work is what a seat's queue has always done. - // - // **And its holder answers for the build it is running** (novox/hq ADR 0219): what it is - // building, kill it, take nothing new, take again. The queue as a whole is the controller's to - // show and change (`queue`, `cancel`, `clear`); what one machine does with an ask it already - // took only that machine can do. `paused.` is each holder saying whether it takes work, so - // the controller can tell a plan waiting on a paused seat from one that is late. - {Name: "node-build-agent", Scope: ScopeNode, - Accepts: []string{"build"}, Emits: []string{"started", "built", "log.*", "paused.*"}, - Serves: buildAgentVerbs(), - Decision: "novox/hq ADR 0190, ADR 0219"}, - // **Retired by ADR 0190, kept while a manifest still claims it.** The one build machine's seat. - // A claim to a seat the mesh no longer defines is refused, and the module holding this one is - // assigned on a live machine until build-agent replaces it — removing the row first would make - // that machine unresolvable in the meantime. Deleted once no registered manifest claims it. - {Name: "mesh-build-machine", Scope: ScopeMesh, - Accepts: []string{"build"}, Emits: []string{"started", "built", "log.*"}, Decision: "novox/hq ADR 0190"}, + // **A build node's work, one seat per build language** (novox/hq ADR 0525 §2, §10; ADR 0536 §4; issue 1887). The + // build module of each language claims its seat on every node it runs on; the builder and the checker put their + // asks on the seat's accepted kinds, which the broker derives as one work queue with a shared worker per kind, and + // each holder pulls delivery first, then check, then recheck, at most its node's build capacity at once. Its verbs + // say what the node runs and pause or resume it there: every holder pulls from the same worker, so a node's pause is + // its holder taking nothing, kept on the module's own pause key (issue 1887's design limit 2). They replace + // node-build-agent, retired with build-agent (ADR 0524 §5). + {Name: "node-build-go", Scope: ScopeNode, Accepts: buildAskKinds(), PulledByKind: true, Serves: buildSeatVerbs("Go"), + Decision: "novox/hq ADR 0525, ADR 0536"}, + {Name: "node-build-node", Scope: ScopeNode, Accepts: buildAskKinds(), PulledByKind: true, Serves: buildSeatVerbs("Node"), + Decision: "novox/hq ADR 0525, ADR 0536"}, + {Name: "node-build-image", Scope: ScopeNode, Accepts: buildAskKinds(), PulledByKind: true, Serves: buildSeatVerbs("image"), + Decision: "novox/hq ADR 0525, ADR 0536"}, // **The mesh's resolvers** (novox/hq ADR 0194, 0196, 0223): every node's internal domain, and // every node and container asks them and nothing else. Replicated since ADR 0223: held on more // than one machine, each answering the same names from the same roster, and every machine's @@ -498,6 +490,7 @@ func UseSeats(s []Seat) { row.Receives = d.Receives row.Paths = d.Paths row.Replicated = d.Replicated + row.PulledByKind = d.PulledByKind row.Serves = optionalAsCompiled(row.Serves, d.Serves) } merged = append(merged, row) @@ -623,6 +616,10 @@ func claimProblems(m Manifest) []string { // after UseSeats has replaced the set with the store's. continue } + if replaced, retired := RetiredSeats[c.Name]; retired { + problems = append(problems, fmt.Sprintf("%s claims the seat %q, which is retired: its work is %s", m.Module, c.Name, replaced)) + continue + } if isSystemSeatName(c.Name) { problems = append(problems, fmt.Sprintf( "%s claims %q, which is a seat in the mesh's own namespace (mesh-*/node-*) that it "+ @@ -903,22 +900,28 @@ func loginShellVerbs() []Verb { } } -// buildAgentVerbs are what a holder of node-build-agent answers on its own machine (novox/hq ADR -// 0219). One build at a time per holder (ADR 0190), so "the build running here" is one or none. -func buildAgentVerbs() []Verb { +// buildAskKinds are a build seat's accepted kinds, in the order a holder pulls them (ADR 0536 §4; mesh-sdk +// delivery.AskKinds). +func buildAskKinds() []string { return []string{"delivery", "check", "recheck"} } + +// RetiredSeats are seats the mesh no longer defines, each with what replaced it: a claim or a use of one is refused +// naming its replacement, not as a seat nobody declares. +var RetiredSeats = map[string]string{ + "node-build-agent": "node-build-go, node-build-node and node-build-image, held by the build modules (novox/hq ADR 0525 §10)", + "mesh-build-machine": "node-build-go, node-build-node and node-build-image, held by the build modules (novox/hq ADR 0190, ADR 0525 §10)", +} + +// buildSeatVerbs are what a build module answers on its own node for its seat (ADR 0525 §10). +func buildSeatVerbs(language string) []Verb { return []Verb{ - {Name: "current", Description: "The build this machine is running — its id, repository, path, " + - "the step it is at, when it started and for how long — or none; and whether this machine is " + - "paused, taking no new build.", + {Name: "current", Description: "The " + language + " asks this node runs now (each ask's id, kind, module and when it " + + "started), its build capacity, and whether it is paused, taking no new ask.", Input: schema(map[string]string{}, nil)}, - {Name: "kill", Description: "End the build with this id, running here: its commands and the " + - "containers it started are stopped, and its outcome is announced as failed, killed by hand — " + - "settled, so it is not handed to another machine.", - Input: schema(map[string]string{"id": "the build's id, as `queue` or `current` says it"}, []string{"id"})}, - {Name: "pause", Description: "Take no new build on this machine until resumed; a build running " + - "here finishes. Kept across a restart of the holder.", + {Name: "pause", Description: "Take no new " + language + " ask on this node until resumed; an ask running here " + + "ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the " + + "lasting way to keep a node from building.", Input: schema(map[string]string{}, nil)}, - {Name: "resume", Description: "Take builds again on this machine.", + {Name: "resume", Description: "Take " + language + " asks again on this node.", Input: schema(map[string]string{}, nil)}, } } diff --git a/go/catalogue/seats_declared.go b/go/catalogue/seats_declared.go index 457f87e..9165103 100644 --- a/go/catalogue/seats_declared.go +++ b/go/catalogue/seats_declared.go @@ -364,6 +364,10 @@ func CatalogueProblems(shelf Shelf) []string { // A `uses` naming nothing is where ADR 0110's guarantee lands under a derived set: the // same refusal, at the same moment, from a set nobody maintains by hand. for _, u := range m.Uses { + if replaced, retired := RetiredSeats[u]; retired && !exists(u) { + problems = append(problems, fmt.Sprintf("%s uses the seat %q, which is retired: its work is %s", module, u, replaced)) + continue + } if !exists(u) { problems = append(problems, fmt.Sprintf( "%s uses the seat %q, "+UndeclaredSeat, @@ -372,6 +376,10 @@ func CatalogueProblems(shelf Shelf) []string { } for _, c := range m.Claims { + if replaced, retired := RetiredSeats[c.Name]; retired && !exists(c.Name) { + problems = append(problems, fmt.Sprintf("%s claims the seat %q, which is retired: its work is %s", module, c.Name, replaced)) + continue + } if !exists(c.Name) { problems = append(problems, fmt.Sprintf( "%s claims the seat %q, "+UndeclaredSeat, diff --git a/go/catalogue/seats_test.go b/go/catalogue/seats_test.go index 2b01748..caa5a25 100644 --- a/go/catalogue/seats_test.go +++ b/go/catalogue/seats_test.go @@ -43,7 +43,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { delivered[s.Delivers] = s.Name } } - // Forty-five with node-files (novox/hq ADR 0394); forty-four again once node-reads goes. + // Forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq + // ADR 0525 §2, §10); forty-five with node-files (novox/hq ADR 0394); forty-four again once node-reads goes. // Forty-four with node-provisioner (novox/hq ADR 0349 clause 3); forty-three with mesh-provisioner (novox/hq ADR 0349 clause 3); forty-two with mesh-config (novox/hq ADR 0348 §9); forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired // into node-uplink (novox/hq ADR 0223); thirty-seven // since the retired node-dns-resolver went (novox/hq ADR 0220); thirty-eight with @@ -55,8 +56,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { // node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203, // ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired // mesh-build-machine row goes, when no registered manifest claims it. - if len(Seats()) != 45 { - t.Errorf("the mesh defines %d seats rather than 45; the set is closed, so a change here is "+ + if len(Seats()) != 46 { + t.Errorf("the mesh defines %d seats rather than 46; the set is closed, so a change here is "+ "a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames()) } } @@ -300,3 +301,35 @@ func TestAFormerNameResolvesAfterARename(t *testing.T) { t.Fatalf("the holder recorded under the former name was not matched: %+v found=%v", holder, found) } } + +// 1887/A9a: the catalogue defines no seat node-build-agent, and a manifest claiming it is refused naming its +// replacement; the build modules' seats accept the three kinds in their pull order and serve current, pause, resume. +func TestNodeBuildAgentIsRetiredForTheBuildSeats(t *testing.T) { + for _, gone := range []string{"node-build-agent", "mesh-build-machine"} { + if _, ok := SeatNamed(gone); ok { + t.Errorf("the mesh still defines %s", gone) + } + } + _, err := ParseManifest([]byte(`{"module":"build-agent","version":"1","claims":[{"name":"node-build-agent","scope":"node"}]}`)) + if err == nil || !strings.Contains(err.Error(), `"node-build-agent", which is retired`) || !strings.Contains(err.Error(), "node-build-go") { + t.Fatalf("the claim is not refused naming its replacement: %v", err) + } + // And over a catalogue whose store still holds no row for it, a use of it is refused the same way. + user := Manifest{Module: "watcher", Uses: []string{"node-build-agent"}} + if joined := strings.Join(CatalogueProblems(Shelf{user.Module: user}), "; "); !strings.Contains(joined, "which is retired: its work is node-build-go") { + t.Fatalf("a use is not refused naming the replacement: %s", joined) + } + for _, name := range []string{"node-build-go", "node-build-node", "node-build-image"} { + s, ok := SeatNamed(name) + if !ok || s.Scope != ScopeNode || !s.PulledByKind || strings.Join(s.Accepts, ",") != "delivery,check,recheck" { + t.Fatalf("%s = %+v", name, s) + } + var verbs []string + for _, v := range s.Serves { + verbs = append(verbs, v.Name) + } + if strings.Join(verbs, ",") != "current,pause,resume" { + t.Errorf("%s serves %v", name, verbs) + } + } +}