Merge pull request 'The rest of the mesh resolves each machine with its pins and names what it leaves out; a built-by edge never widens a plan; plans stop' (#199) from fix/a-machine-not-on-the-network-is-said into main
This commit was merged in pull request #199.
This commit is contained in:
@@ -567,6 +567,9 @@ func onTheNetwork(ctx context.Context, inv *inventory.Inventory,
|
|||||||
catalogue.Node{Name: p.Name, Site: p.Site, Capabilities: caps},
|
catalogue.Node{Name: p.Name, Site: p.Site, Capabilities: caps},
|
||||||
catalogue.World{Unchecked: true, Holdings: holdings})
|
catalogue.World{Unchecked: true, Holdings: holdings})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Said, not skipped in silence: a machine dropped here loses its address, and every
|
||||||
|
// plan that names it fails in another module's words (novox/hq issue 188).
|
||||||
|
fmt.Fprintf(os.Stderr, "%s is not counted as on the network: it does not resolve: %v\n", p.Name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, m := range got.Modules {
|
for _, m := range got.Modules {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -281,8 +282,9 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
|||||||
for _, o := range others {
|
for _, o := range others {
|
||||||
got, err := catalogue.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings})
|
got, err := catalogue.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Their set does not resolve for some other reason. Not this node's problem to
|
// Said, not skipped: a machine dropped here offers nothing and holds nothing as far
|
||||||
// report, and nothing of theirs is running, so it offers nothing.
|
// as every other machine's plan can tell (novox/hq issue 188).
|
||||||
|
fmt.Fprintf(os.Stderr, "%s is left out of the rest of the mesh: it does not resolve: %v\n", o.node.Name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
firstHeld = append(firstHeld, got.Claims...)
|
firstHeld = append(firstHeld, got.Claims...)
|
||||||
@@ -313,8 +315,17 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
|||||||
world := catalogue.World{Offered: offered, Held: firstHeld, Holdings: holdings}
|
world := catalogue.World{Offered: offered, Held: firstHeld, Holdings: holdings}
|
||||||
var held []catalogue.Held
|
var held []catalogue.Held
|
||||||
for _, o := range others {
|
for _, o := range others {
|
||||||
got, err := catalogue.Resolve(shelf, o.assigned, o.node, world)
|
// Each machine is resolved with its own pins, as its plan is: a machine that needs one to
|
||||||
|
// settle two providers would otherwise be refused here and vanish from the mesh — every
|
||||||
|
// seat it holds unheld, every build that needs one refused (2026-10-01, the control node;
|
||||||
|
// novox/hq issue 188).
|
||||||
|
theirs := world
|
||||||
|
if pins, err := inv.PinsFor(ctx, o.node.Name); err == nil {
|
||||||
|
theirs.Pinned = pins
|
||||||
|
}
|
||||||
|
got, err := catalogue.Resolve(shelf, o.assigned, o.node, theirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s is left out of the rest of the mesh: it does not resolve: %v\n", o.node.Name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
held = append(held, got.Claims...)
|
held = append(held, got.Claims...)
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ func isBaseOf(base, to string, edges []inventory.Edge, in map[string]bool) bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reachableFrom is the moved modules plus everything that depends on them, through every layer:
|
// reachableFrom is the moved modules plus everything that depends on them, through every layer:
|
||||||
// what a merge rebuilds.
|
// what a merge rebuilds. Along the code and build edges only: a module *built by* the build machine
|
||||||
|
// is not changed by a new build machine, so a built-by edge orders and gates a plan and never
|
||||||
|
// widens it — the first plan of 2026-10-01 took the whole catalogue along for a controller change.
|
||||||
func reachableFrom(moved []string, edges []inventory.Edge) []string {
|
func reachableFrom(moved []string, edges []inventory.Edge) []string {
|
||||||
in := map[string]bool{}
|
in := map[string]bool{}
|
||||||
for _, m := range moved {
|
for _, m := range moved {
|
||||||
@@ -120,6 +122,9 @@ func reachableFrom(moved []string, edges []inventory.Edge) []string {
|
|||||||
for grew := true; grew; {
|
for grew := true; grew; {
|
||||||
grew = false
|
grew = false
|
||||||
for _, e := range edges {
|
for _, e := range edges {
|
||||||
|
if e.Kind == inventory.EdgeBuiltBy {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if in[e.To] && !in[e.From] {
|
if in[e.To] && !in[e.From] {
|
||||||
in[e.From] = true
|
in[e.From] = true
|
||||||
grew = true
|
grew = true
|
||||||
@@ -571,6 +576,23 @@ func plansCommand(ctx context.Context, args []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if len(positionals) == 2 && positionals[0] == "stop" {
|
||||||
|
p, err := inv.PlanByID(ctx, positionals[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !p.Open() {
|
||||||
|
return fmt.Errorf("%s is already %s", p.ID, p.State)
|
||||||
|
}
|
||||||
|
p.State = inventory.PlanFailed
|
||||||
|
p.Note = "stopped by hand at tier " + fmt.Sprint(p.Tier)
|
||||||
|
if err := inv.SavePlan(ctx, p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%s stopped at tier %d of %d; what was asked still builds and registers, nothing further is asked\n",
|
||||||
|
p.ID, p.Tier, len(p.Tiers))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
plans, err := inv.RecentPlans(ctx, *limit)
|
plans, err := inv.RecentPlans(ctx, *limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency(t *testing.T) {
|
|||||||
{From: "shop-plugin", To: "shop", Kind: inventory.EdgeDeclared},
|
{From: "shop-plugin", To: "shop", Kind: inventory.EdgeDeclared},
|
||||||
// a code dependency: the proxy packages the controller's source — same tier
|
// a code dependency: the proxy packages the controller's source — same tier
|
||||||
{From: "route-proxy", To: "mesh-controller", Kind: inventory.EdgePackages},
|
{From: "route-proxy", To: "mesh-controller", Kind: inventory.EdgePackages},
|
||||||
|
{From: "builder", To: "mesh-controller", Kind: inventory.EdgePackages},
|
||||||
// runtime dependencies: everything source-built is built by the builder
|
// runtime dependencies: everything source-built is built by the builder
|
||||||
{From: "shop", To: "builder", Kind: inventory.EdgeBuiltBy},
|
{From: "shop", To: "builder", Kind: inventory.EdgeBuiltBy},
|
||||||
{From: "postgres", To: "builder", Kind: inventory.EdgeBuiltBy},
|
{From: "postgres", To: "builder", Kind: inventory.EdgeBuiltBy},
|
||||||
@@ -30,7 +31,9 @@ func TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// The runtime image moved: everything on it, and what is built by what is on it.
|
// The runtime image moved: everything on it, and what is built by what is on it.
|
||||||
set := reachableFrom([]string{"mesh-tools"}, edges)
|
set := reachableFrom([]string{"mesh-tools"}, edges)
|
||||||
want := []string{"builder", "mesh-controller", "mesh-tools", "postgres", "route-proxy", "shop", "shop-plugin"}
|
// What stands on the runtime, and the builder that stands on it; not the controller, which the
|
||||||
|
// builder merely builds, nor the proxy that packages the controller.
|
||||||
|
want := []string{"builder", "mesh-tools", "postgres", "shop", "shop-plugin"}
|
||||||
if len(set) != len(want) {
|
if len(set) != len(want) {
|
||||||
t.Fatalf("reachable from the runtime: %v, want %v", set, want)
|
t.Fatalf("reachable from the runtime: %v, want %v", set, want)
|
||||||
}
|
}
|
||||||
@@ -44,26 +47,31 @@ func TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency(t *testing.T) {
|
|||||||
if pos["mesh-tools"] != 0 || pos["builder"] != 1 {
|
if pos["mesh-tools"] != 0 || pos["builder"] != 1 {
|
||||||
t.Fatalf("the runtime then the builder: %v", tiers)
|
t.Fatalf("the runtime then the builder: %v", tiers)
|
||||||
}
|
}
|
||||||
if !(pos["shop"] > pos["builder"] && pos["postgres"] > pos["builder"] && pos["mesh-controller"] > pos["builder"]) {
|
if !(pos["shop"] > pos["builder"] && pos["postgres"] > pos["builder"]) {
|
||||||
t.Fatalf("what the builder builds comes after the builder: %v", tiers)
|
t.Fatalf("what the builder builds comes after the builder: %v", tiers)
|
||||||
}
|
}
|
||||||
if pos["shop-plugin"] <= pos["shop"] {
|
if pos["shop-plugin"] <= pos["shop"] {
|
||||||
t.Fatalf("a plugin after what it is declared on: %v", tiers)
|
t.Fatalf("a plugin after what it is declared on: %v", tiers)
|
||||||
}
|
}
|
||||||
if pos["route-proxy"] != pos["mesh-controller"] {
|
|
||||||
t.Fatalf("a code dependency is rebuilt in the same tier as its source: %v", tiers)
|
|
||||||
}
|
|
||||||
if hasCycle(tiers, edges) {
|
if hasCycle(tiers, edges) {
|
||||||
t.Fatalf("no cycle here: %v", tiers)
|
t.Fatalf("no cycle here: %v", tiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The controller alone moved: the proxy with it, nothing else.
|
// The controller alone moved: the proxy with it, nothing else.
|
||||||
small := reachableFrom([]string{"mesh-controller"}, edges)
|
small := reachableFrom([]string{"mesh-controller"}, edges)
|
||||||
if len(small) != 2 {
|
if len(small) != 3 {
|
||||||
t.Fatalf("a controller merge rebuilds the controller and what packages it: %v", small)
|
t.Fatalf("a controller merge rebuilds the controller and what packages it: %v", small)
|
||||||
}
|
}
|
||||||
if tiers := tiersOf(small, edges); len(tiers) != 1 {
|
// The builder packages the controller's source (same tier by that edge) and the controller is
|
||||||
t.Fatalf("both in one tier: %v", tiers)
|
// built by the builder (next tier by that one): the builder first, then the controller and the
|
||||||
|
// proxy together — a code dependency in one tier, a runtime dependency across tiers.
|
||||||
|
smallTiers := tiersOf(small, edges)
|
||||||
|
if len(smallTiers) != 2 || smallTiers[0][0] != "builder" || len(smallTiers[1]) != 2 {
|
||||||
|
t.Fatalf("the builder, then the controller and the proxy together: %v", smallTiers)
|
||||||
|
}
|
||||||
|
// The builder alone moved: the builder, and nothing it builds.
|
||||||
|
if only := reachableFrom([]string{"builder"}, edges); len(only) != 1 {
|
||||||
|
t.Fatalf("a build machine change rebuilds the build machine alone: %v", only)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only a runtime dependency gates on deployment, and only when the module rolls out.
|
// Only a runtime dependency gates on deployment, and only when the module rolls out.
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ func argvFor(verb string, args map[string]any) ([]string, error) {
|
|||||||
}
|
}
|
||||||
return []string{"builds"}, nil
|
return []string{"builds"}, nil
|
||||||
case "plans":
|
case "plans":
|
||||||
|
if id := str("stop"); id != "" {
|
||||||
|
return []string{"plans", "stop", id}, nil
|
||||||
|
}
|
||||||
if id := str("id"); id != "" {
|
if id := str("id"); id != "" {
|
||||||
return []string{"plans", id}, nil
|
return []string{"plans", id}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ var ControllerVerbs = []Verb{
|
|||||||
}, nil)},
|
}, nil)},
|
||||||
{Name: "plans", Description: "What the last merges produced and where each stands (novox/hq ADR 0162): " +
|
{Name: "plans", Description: "What the last merges produced and where each stands (novox/hq ADR 0162): " +
|
||||||
"the tiers, the tier a plan is at, what it waits for and since when; one plan whole, given its id.",
|
"the tiers, the tier a plan is at, what it waits for and since when; one plan whole, given its id.",
|
||||||
Input: schema(map[string]string{"id": "a plan's id (as `plans` lists them): that plan, tier by tier"}, nil)},
|
Input: schema(map[string]string{
|
||||||
|
"id": "a plan's id (as `plans` lists them): that plan, tier by tier",
|
||||||
|
"stop": "a plan's id: stop it — what was asked still builds, nothing further is asked",
|
||||||
|
}, nil)},
|
||||||
{Name: "plan", Description: "What one machine would run, and why: the declaration the mesh would send it.",
|
{Name: "plan", Description: "What one machine would run, and why: the declaration the mesh would send it.",
|
||||||
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})},
|
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})},
|
||||||
{Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there.",
|
{Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there.",
|
||||||
|
|||||||
Reference in New Issue
Block a user