From d0c9b9b4bcd208dc37f938bdba6c9d2814b61660 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 10 Oct 2026 14:10:35 +0200 Subject: [PATCH] A merge on the controller's own path never shares a batch with one that waits for the delivery's word (hq ADR 0276, decided during the build) --- cmd/mesh-controller/batches_test.go | 89 +++++++++++++++++++++++++++++ internal/inventory/plans.go | 9 +++ 2 files changed, 98 insertions(+) diff --git a/cmd/mesh-controller/batches_test.go b/cmd/mesh-controller/batches_test.go index b2fa1802..38af562a 100644 --- a/cmd/mesh-controller/batches_test.go +++ b/cmd/mesh-controller/batches_test.go @@ -669,3 +669,92 @@ func TestTheCatchUpKeepsWhatACutMadeHistory(t *testing.T) { t.Fatalf("the merge made before the cut and heard after it was dropped: %v %v", kept, err) } } + +// A merge on the controller's own path never shares a batch with one that waits for mesh-delivery's word +// (decided during the build, 2026-10-10): the two are batches of their own, cut one after the other; a walk on +// the own path folds no waiting catalogue walk but batches its merges again behind it; the catalogue's walk +// still waits for the word, so no catalogue delivery skips its turn. +func TestAMergeOnTheControllersPathNeverSharesABatch(t *testing.T) { + open := windowed(t) + asked := asksWithPaths(t) + ctx := t.Context() + if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-delivery", Version: "1", + Claims: []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}}, + inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/mesh-delivery", Ref: "main", + BuiltFrom: "c0", Head: "c0"}); err != nil { + t.Fatal(err) + } + if _, err := open.inventory.Assign(ctx, "anchor", "mesh-delivery"); err != nil { + t.Fatal(err) + } + hear(t, open, catalogueMerge("aaaa0001", "app", t0), t0) + hear(t, open, repoMerge("mesh-controller", "k1", t0.Add(10*time.Second)), t0.Add(10*time.Second)) + hear(t, open, catalogueMerge("bbbb0002", "notes", t0.Add(20*time.Second)), t0.Add(20*time.Second)) + _, bs := walks(t, open) + var ownBatch, catalogueBatch inventory.Plan + for _, b := range bs { + if b.OwnPath() { + ownBatch = b + } else { + catalogueBatch = b + } + } + if len(bs) != 2 || ownBatch.ID == "" || catalogueBatch.ID == "" || len(catalogueBatch.Delivery.Merges) != 2 || + len(ownBatch.Delivery.Merges) != 1 { + t.Fatalf("a controller merge and two catalogue merges in one window: %+v", bs) + } + if !strings.Contains(batchWords(ownBatch, t0.Add(30*time.Second)), "own path") { + t.Fatalf("the own-path batch does not say so: %q", batchWords(ownBatch, t0.Add(30*time.Second))) + } + // The catalogue batch, the older, is cut first and waits for its word; the controller's batch is cut next: + // the waiting walk is batched again behind it, not folded into it. + cutAt(t, open, t0.Add(2*time.Minute)) + ws, _ := walks(t, open) + if len(ws) != 1 || !ws[0].Waiting() { + t.Fatalf("the catalogue batch was not cut into a waiting walk: %+v", ws) + } + catalogueWalk := ws[0] + cutAt(t, open, t0.Add(2*time.Minute+5*time.Second)) + ws, bs = walks(t, open) + var ownWalk inventory.Plan + for _, w := range ws { + if w.Open() { + ownWalk = w + } + } + if ownWalk.ID == "" || ownWalk.Waiting() || ownWalk.CommitOf("novox/mesh-controller") != "k1" { + t.Fatalf("the controller's batch was not cut into a started walk: %+v", ws) + } + for _, m := range []string{"app", "notes"} { + if _, in := ownWalk.Modules[m]; in { + t.Fatalf("the controller's walk builds %s, a catalogue module: it skipped its turn", m) + } + } + for _, a := range *asked { + if a[1] == "modules/app" || a[1] == "modules/notes" { + t.Fatalf("a catalogue module was asked without the word: %v", *asked) + } + } + folded, _ := open.inventory.PlanByID(ctx, catalogueWalk.ID) + if folded.State != inventory.PlanSuperseded || len(bs) != 1 || folded.Delivery.TakenOverBy != bs[0].ID || + bs[0].State != inventory.PlanQueued || bs[0].Delivery.Batch.Behind != ownWalk.ID || bs[0].OwnPath() || + len(bs[0].Delivery.Merges) != 2 { + t.Fatalf("the waiting catalogue walk is %s (taken over by %q); batches %+v", folded.State, + folded.Delivery.TakenOverBy, bs) + } + // The controller's walk done, the catalogue batch is cut and waits for its word with both merges. + ownWalk.State = inventory.PlanDone + if err := open.inventory.SavePlan(ctx, &ownWalk); err != nil { + t.Fatal(err) + } + cutAt(t, open, t0.Add(3*time.Minute)) + ws, bs = walks(t, open) + if len(bs) != 0 || ws[0].ID != folded.Delivery.TakenOverBy || !ws[0].Waiting() || len(ws[0].Delivery.Merges) != 2 { + t.Fatalf("the catalogue batch was not cut into a waiting walk once the controller's ended: %+v %+v", ws, bs) + } + for _, m := range []string{"app", "notes"} { + if _, in := ws[0].Modules[m]; !in { + t.Fatalf("the catalogue walk does not build %s: %v", m, ws[0].Modules) + } + } +} diff --git a/internal/inventory/plans.go b/internal/inventory/plans.go index 4dba027c..56045490 100644 --- a/internal/inventory/plans.go +++ b/internal/inventory/plans.go @@ -79,6 +79,15 @@ type PlanBatch struct { ClosesAt time.Time `json:"closes_at"` AtMost time.Time `json:"at_most"` Behind string `json:"behind,omitempty"` + // Own says the batch holds merges on the controller's own path, whose walk waits for nobody's word: such + // a merge never shares a batch with one that waits for mesh-delivery's (ADR 0276, decided during the + // build, 2026-10-10). + Own bool `json:"own,omitempty"` +} + +// OwnPath says the record is a batch of merges on the controller's own path. +func (p Plan) OwnPath() bool { + return p.Delivery != nil && p.Delivery.Batch != nil && p.Delivery.Batch.Own } // CommitOf is the commit of a repository the walk carries; empty when it carries none of it.