From 93f655dac9a305e4eb9ede4ae2b3b9e2698097c1 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 23:01:37 +0100 Subject: [PATCH] mesh-delivery-gate: a merge learnt from the open list seconds after it landed moves its trunk and source.main; delivery_status shows source.main mesh-delivery#51 merged 21:43:11 and the gate read the open list at 21:43:14, before the forge's merged event: taken as learnt late, it moved no trunk and its group's finish left it out of source.main, so the checker kept judging every mesh-delivery head on 73be3df. A late merge within replayLate of its landing is now taken as heard as it happened, and one a gate before this left is written at the catch-up of the start (never backwards). --- .../cmd/mesh-delivery-gate/holder.go | 7 ++++ .../cmd/mesh-delivery-gate/issue1275_test.go | 36 +++++++++++++++++++ .../cmd/mesh-delivery-gate/main.go | 9 +++-- .../cmd/mesh-delivery-gate/merges.go | 36 +++++++++++++++++-- 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/holder.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/holder.go index 505f36a5..3041be93 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/holder.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/holder.go @@ -726,6 +726,13 @@ func (h *Holder) pullMerged(p PullEvent, late bool) { d.MergedAs = p.MergeCommit if t, err := time.Parse(time.RFC3339, p.MergedAt); err == nil { d.MergedAt = t.UTC() + if late && now.Sub(d.MergedAt) < replayLate { + // Read from the forge's open pull requests seconds after the merge, before its event arrived (mesh-delivery#51, + // 2026-10-25 21:43: merged 21:43:11, learnt late 21:43:14): the trunk has not moved past it, so it is taken as a + // merge heard as it happened. Taken late, it moved no trunk and its group's finish left it out of source.main, + // which kept naming the trunk before it, and the checker judged every head on that. + late = false + } } else if !late { d.MergedAt = now } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1275_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1275_test.go index 83ecc256..73d88678 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1275_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1275_test.go @@ -639,6 +639,42 @@ func TestAnApprovedGroupIsHandedWholeInItsOrderAndAStopIsSaid(t *testing.T) { t.Fatalf("source.main moved for a merge learnt late: %+v", w.source.moves[moves:]) } }}, + // mesh-delivery#51 (2026-10-25 21:43): the open list was read seconds after the bridge merged it and before the forge's + // merged event, so it was learnt late, moved no trunk, and its group's finish left it out of source.main. A merge + // learnt within replayLate of its landing is taken as heard as it happened; one a gate before the fix took late is + // written at the catch-up of the start. + {"a merge learnt from the open list seconds after it landed moves source.main", func(t *testing.T) { + const repo, head, merge = "novox/mesh-delivery", "4628ba26b41a4628ba26b41a4628ba26b41a4628", "db53ec975f01db53ec975f01db53ec975f01db53" + for _, oldGate := range []bool{false, true} { + w := newWorld(t) + w.merges.manual = true + w.h.PullUpdated(pr(repo, 51, head, "fix/1945", "modules/a/a.go")) + w.h.Checked(judgedAt(repo, 51, head, "pass", "t1t1t1t1t1t1")) + w.settleAll() + w.source.main = &sdk.Source{V: sdk.Version, Group: "before", Heads: map[string]string{repo: "73be3df28cd4"}} + w.forge.pulls[repo+"#51"] = ForgePull{Number: 51, State: "closed", Merged: true, MergeCommit: merge, + MergedAt: w.h.Now().Add(-3 * time.Second).UTC().Format(time.RFC3339), HeadSHA: head, Base: "main"} + w.forge.open = []OpenPull{{Owner: "novox", Repo: "other", Number: 1, Base: "main", HeadSHA: "abab"}} + w.h.wantForge() + w.settleAll() + if oldGate { // what the gate before the fix left + w.h.mu.Lock() + kept := w.h.deliveries[IDOf(repo, head)] + kept.LearntLate, kept.InSource, kept.Ready.SourceMoved = true, true, true + w.h.mu.Unlock() + w.source.main.Heads[repo] = "73be3df28cd4" + w.h.SourceCatchUp() + w.settleAll() + } + d := w.delivery(IDOf(repo, head)) + if d.State != Merged || (!oldGate && d.LearntLate) || w.source.main.Heads[repo] != merge { + t.Fatalf("old gate %v: %s, learnt late %v, source.main %v", oldGate, d.State, d.LearntLate, w.source.main.Heads) + } + if heads, _ := w.h.SourceMain()["heads"].(map[string]string); heads[repo] != merge { + t.Fatalf("delivery_status's source-main says %v", w.h.SourceMain()) + } + } + }}, // Review round 1, blocker 2 (1275/AC6): an out-of-date refusal at t0 with no other event is said urgent at t0+10 // minutes: the gate asks itself for a look then. {"an out-of-date refusal turns urgent on the clock alone", func(t *testing.T) { diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go index 072ee780..e9e4c84a 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go @@ -360,8 +360,13 @@ func tools(h *Holder, l *listening) []stdio.Tool { {Name: "delivery_status", Description: "Whether this owner works: how many deliveries in each state, the groups, what is owed to " + "the forge and the bus and not yet done, how many owed effects were tried since it started (done, failed " + - "and tried again, given up) and when the last was, whether its events arrive, and the last refusals.", - Run: func(map[string]any) (any, error) { return h.Status(l.get()), nil }}, + "and tried again, given up) and when the last was, whether its events arrive, the last refusals, and " + + "source.main as the gate's bucket source holds it (each repository's trunk head the checker judges on).", + Run: func(map[string]any) (any, error) { + out := h.Status(l.get()) + out["source-main"] = h.SourceMain() + return out, nil + }}, } // A verb that changes a delivery asks for a look at what it makes possible (react.go). for i := range list { diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go index e7e5f4f9..b38e721f 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go @@ -105,6 +105,26 @@ func (kvSource) Move(update func(prev *sdk.Source) (sdk.Source, bool)) error { return fmt.Errorf("source.main was written by another writer first %d times: %w", changeAttempts, stdio.ErrChanged) } +// SourceMain is source.main as the bucket source holds it, read and never written: its group and each repository's trunk +// head, or why it cannot be read. Outside the lock. +func (h *Holder) SourceMain() map[string]any { + if h.Source == nil { + return map[string]any{"error": "no source bucket"} + } + var held *sdk.Source + err := h.Source.Move(func(prev *sdk.Source) (sdk.Source, bool) { + held = prev + return sdk.Source{}, false + }) + switch { + case err != nil: + return map[string]any{"error": err.Error()} + case held == nil: + return map[string]any{"heads": map[string]string{}, "says": "source.main is not written yet"} + } + return map[string]any{"group": held.Group, "heads": held.Heads} +} + // approvedOf are the heads an Approve covers as a merge carries them: `@` to the full head. func (h *Holder) approvedOf(heads map[string]string) map[string]string { out := map[string]string{} @@ -434,6 +454,18 @@ func gateMerges(d *Delivery) bool { return d.Ready != nil && d.Ready.Approved != nil && d.Ready.AskID != "" && len(d.Ready.Heads) > 0 } +// leftOutOfForgeSource is whether settleForgeMerges never writes a merge to source.main: one the gate handed on an Approve +// (finish writes it), and one learnt after the fact, long past or with no time the forge said. One learnt late within +// replayLate of its merge, as a gate before the fix took mesh-delivery#51 (2026-10-25, its group's finish left it out and +// source.main kept the trunk before it), is weighed like any merge heard from the forge at a catch-up, and never moves +// source.main back (olderHead). +func (d *Delivery) leftOutOfForgeSource() bool { + if d.LearntLate { + return d.MergedAt.IsZero() || d.Since.Sub(d.MergedAt) >= replayLate + } + return gateMerges(d) +} + // settleForgeMerges moves source.main for each merge into the trunk heard only from the forge (issue 888): a pull request // merged at the forge directly, not handed by the gate on an Approve. Every other delivery's trunk moved with it (trunkMoved), // and the checker judges on the trunk source.main names, so source.main moves too. A merge replayed as history, or older than @@ -447,7 +479,7 @@ func (h *Holder) settleForgeMerges() []func() { again = h.newestInSource() } for _, d := range h.deliveries { - if d.MergedAs == "" || (d.InSource && !again[d]) || gateMerges(d) || d.LearntLate { + if d.MergedAs == "" || (d.InSource && !again[d]) || d.leftOutOfForgeSource() { continue } if d.Base != MergeBranch || d.replayed() || h.newerMergeOf(d) { @@ -564,7 +596,7 @@ func (h *Holder) SourceCatchUp() { func (h *Holder) newestInSource() map[*Delivery]bool { newest := map[string]*Delivery{} for _, d := range h.deliveries { - if d.MergedAs == "" || !d.InSource || gateMerges(d) || d.LearntLate || d.Base != MergeBranch || d.replayed() { + if d.MergedAs == "" || !d.InSource || d.leftOutOfForgeSource() || d.Base != MergeBranch || d.replayed() { continue } if o := newest[d.Repository]; o == nil || mergedWhen(d).After(mergedWhen(o)) {