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).
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: `<repository>@<number>` 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)) {
|
||||
|
||||
Reference in New Issue
Block a user