A built-by edge orders and gates a plan; it never widens it — and plans stop
The first live plan took seventy-five modules along for a controller change: the builder packages the controller's source, everything is built by the builder, so everything was reachable. A module built by the build machine is not changed by a new build machine. Reachability now follows the code and build edges only; built-by still orders a tier after the build machine and gates it on the machine's roll-out. plans stop <id> ends a plan by hand: what was asked still builds and registers, nothing further is asked.
This commit is contained in:
@@ -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:
|
||||
// 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 {
|
||||
in := map[string]bool{}
|
||||
for _, m := range moved {
|
||||
@@ -120,6 +122,9 @@ func reachableFrom(moved []string, edges []inventory.Edge) []string {
|
||||
for grew := true; grew; {
|
||||
grew = false
|
||||
for _, e := range edges {
|
||||
if e.Kind == inventory.EdgeBuiltBy {
|
||||
continue
|
||||
}
|
||||
if in[e.To] && !in[e.From] {
|
||||
in[e.From] = true
|
||||
grew = true
|
||||
@@ -571,6 +576,23 @@ func plansCommand(ctx context.Context, args []string) error {
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -18,6 +18,7 @@ func TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency(t *testing.T) {
|
||||
{From: "shop-plugin", To: "shop", Kind: inventory.EdgeDeclared},
|
||||
// a code dependency: the proxy packages the controller's source — same tier
|
||||
{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
|
||||
{From: "shop", 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.
|
||||
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) {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
if pos["shop-plugin"] <= pos["shop"] {
|
||||
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) {
|
||||
t.Fatalf("no cycle here: %v", tiers)
|
||||
}
|
||||
|
||||
// The controller alone moved: the proxy with it, nothing else.
|
||||
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)
|
||||
}
|
||||
if tiers := tiersOf(small, edges); len(tiers) != 1 {
|
||||
t.Fatalf("both in one tier: %v", tiers)
|
||||
// The builder packages the controller's source (same tier by that edge) and the controller is
|
||||
// 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.
|
||||
|
||||
@@ -70,6 +70,9 @@ func argvFor(verb string, args map[string]any) ([]string, error) {
|
||||
}
|
||||
return []string{"builds"}, nil
|
||||
case "plans":
|
||||
if id := str("stop"); id != "" {
|
||||
return []string{"plans", "stop", id}, nil
|
||||
}
|
||||
if id := str("id"); id != "" {
|
||||
return []string{"plans", id}, nil
|
||||
}
|
||||
|
||||
@@ -93,7 +93,10 @@ var ControllerVerbs = []Verb{
|
||||
}, nil)},
|
||||
{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.",
|
||||
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.",
|
||||
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.",
|
||||
|
||||
Reference in New Issue
Block a user