Begin the record of merges at the earliest merge kept on any trunk, not per trunk (ADR 0463 section 2, issue 1336)
mesh/merge-gate head 9f0d9098b735 merged into trunk d163304e3ff1 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 9f0d9098b735 merged into trunk d163304e3ff1 — pass: its merge-check.sh passed
mesh/delivery delivered

This commit is contained in:
2026-10-20 14:49:10 +00:00
parent 3045af4722
commit 9f0d9098b7
6 changed files with 116 additions and 39 deletions
+20 -18
View File
@@ -114,8 +114,10 @@ func readTrunkMerges(ctx context.Context, owner, repo, branch string) ([]trunkMe
return lastMerges(merges), nil return lastMerges(merges), nil
} }
// sinceTheRecord are the merges that can be gaps: those made since the controller began keeping merges of the trunk (first, less // sinceTheRecord are the merges that can be gaps (novox/hq ADR 0463 §2): those made since the controller's record of merges begins on
// mergeGrace, as owedLate judges). With no merge kept on the trunk yet there is no record to have a gap in, and none is. // the mesh (first, the earliest merge it holds of any repository and branch, less mergeGrace as owedLate judges). A merge before it is
// history on every trunk, and a trunk with no merge kept of its own still has the merges made since as gaps. With no merge kept
// anywhere there is no record to have a gap in.
func sinceTheRecord(merges []trunkMerge, kept bool, first time.Time) []trunkMerge { func sinceTheRecord(merges []trunkMerge, kept bool, first time.Time) []trunkMerge {
if !kept { if !kept {
return nil return nil
@@ -469,6 +471,17 @@ func (f following) catchUp(ctx context.Context, only *caughtTrunk) (later bool)
fmt.Printf("catch-up: the catalogue cannot be read, so no merge is caught up now: %v\n", err) fmt.Printf("catch-up: the catalogue cannot be read, so no merge is caught up now: %v\n", err)
return true return true
} }
kept, first, err := inv.EarliestKept(ctx)
if err != nil {
fmt.Printf("catch-up: what the controller kept cannot be read, so no merge is caught up now: %v\n", err)
return true
}
if kept {
fmt.Printf("catch-up: the controller's record of merges begins %s; a merge made before it, less %s, is history on every trunk\n",
first.UTC().Format(time.RFC3339), mergeGrace)
} else {
fmt.Println("catch-up: the controller holds no merge of any trunk yet, so none can have been missed")
}
trunks := trunksOfCatalogue(newSourceForms(ctx, inv), entries) trunks := trunksOfCatalogue(newSourceForms(ctx, inv), entries)
if only != nil { if only != nil {
trunks = slices.DeleteFunc(trunks, func(t caughtTrunk) bool { trunks = slices.DeleteFunc(trunks, func(t caughtTrunk) bool {
@@ -476,14 +489,14 @@ func (f following) catchUp(ctx context.Context, only *caughtTrunk) (later bool)
}) })
} }
for _, t := range trunks { for _, t := range trunks {
if f.catchUpTrunk(ctx, t) { if f.catchUpTrunk(ctx, t, kept, first) {
later = true later = true
} }
} }
return later return later
} }
func (f following) catchUpTrunk(ctx context.Context, t caughtTrunk) (later bool) { func (f following) catchUpTrunk(ctx context.Context, t caughtTrunk, kept bool, first time.Time) (later bool) {
inv := f.open.inventory inv := f.open.inventory
repository := t.repository() repository := t.repository()
merges, err := readTrunkMerges(ctx, t.Owner, t.Repo, t.Branch) merges, err := readTrunkMerges(ctx, t.Owner, t.Repo, t.Branch)
@@ -493,22 +506,11 @@ func (f following) catchUpTrunk(ctx context.Context, t caughtTrunk) (later bool)
"or the next merge heard there does it: %v\n", repository, t.Branch, err) "or the next merge heard there does it: %v\n", repository, t.Branch, err)
return true return true
} }
// **A merge from before the controller kept merges of this trunk is history, never a gap**: it held no record then, so it cannot // **A merge made before the controller's record of merges begins is history, never a gap** (novox/hq ADR 0463 §2).
// have missed it (ADR 0457, as corrected). The first it kept, less the grace owedLate uses, is where the record begins.
kept, first, err := inv.KeptOnBranch(ctx, repository, t.Branch)
if err != nil {
fmt.Printf("catch-up: what the controller kept of %s %s cannot be read, so it is not caught up now: %v\n", repository, t.Branch, err)
return true
}
read := len(merges) read := len(merges)
merges = sinceTheRecord(merges, kept, first) merges = sinceTheRecord(merges, kept, first)
if kept { fmt.Printf("catch-up: %s %s: %d of the last %d pull request merges were made since the record began\n", repository, t.Branch,
fmt.Printf("catch-up: %s %s: the record begins %s; %d of the last %d pull request merges were made since\n", repository, t.Branch, len(merges), read)
first.UTC().Format(time.RFC3339), len(merges), read)
} else {
fmt.Printf("catch-up: %s %s: the controller kept no merge of it yet, so none of its %d pull request merges can have been missed\n",
repository, t.Branch, read)
}
unwarranted := map[string]bool{} unwarranted := map[string]bool{}
for _, tm := range merges { for _, tm := range merges {
id := fmt.Sprintf("%s_%.8s", repository, tm.Commit) id := fmt.Sprintf("%s_%.8s", repository, tm.Commit)
@@ -78,8 +78,8 @@ func aTrunkOf(t *testing.T, repo string, modules ...string) *aTrunk {
return tr return tr
} }
// aKeptMergeBefore is a merge the controller heard on the catalogue's trunk before the merges of a test: where its record begins, // aKeptMergeBefore is a merge the controller heard before the merges of a test: where its record begins, so what a trunk holds from
// so what the trunk holds from then on can be a gap (ADR 0457: a merge from before it cannot have been missed). // then on can be a gap (novox/hq ADR 0463 §2).
func aKeptMergeBefore(t *testing.T, open *stores) { func aKeptMergeBefore(t *testing.T, open *stores) {
t.Helper() t.Helper()
hear(t, open, catalogueMerge("mesh-catalog-seed", "app", t0.Add(-time.Hour)), t0.Add(-time.Hour)) hear(t, open, catalogueMerge("mesh-catalog-seed", "app", t0.Add(-time.Hour)), t0.Add(-time.Hour))
@@ -331,3 +331,22 @@ func TestAMergeFromBeforeTheRecordBeganIsHistoryAndNoCondition(t *testing.T) {
t.Fatalf("%d condition(s) raised: only the merge since the record began (the third) is a gap", raised) t.Fatalf("%d condition(s) raised: only the merge since the record began (the third) is a gap", raised)
} }
} }
// novox/hq ADR 0463 §2: the record begins at the earliest merge kept on ANY trunk. A trunk the controller kept no merge of has the
// merges made since as gaps (mesh-delivery #1 on novox), and the merges made before as history.
func TestATrunkWithNoKeptMergeHasItsLaterMergesAsGapsAndItsEarlierOnesAsHistory(t *testing.T) {
open := windowed(t)
everyHeadWaits(t)
asksRecorded(t)
// The record begins on another trunk, at t0 less an hour.
hear(t, open, repoMerge("one", "c-one", t0.Add(-time.Hour)), t0.Add(-time.Hour))
tr := aTrunkOf(t, "mesh-catalog", "notes", "notes")
tr.merges[0].At = t0.Add(-72 * time.Hour) // before the record: history
(following{open: open}).catchUp(t.Context(), nil)
if mergeKnown(t, open, "mesh-catalog-c01") {
t.Fatal("a merge from before the record began was heard")
}
if !mergeKnown(t, open, "mesh-catalog-c02") {
t.Fatal("a merge since the record began, on a trunk with no merge kept, was not caught up")
}
}
+22 -19
View File
@@ -318,26 +318,29 @@ func TestAMergeMadeInsideABranchIsNoMergeOfTheTrunk(t *testing.T) {
} }
} }
// A merge from before the controller kept merges of the trunk is history: it held no record then, so it cannot have missed it. On // novox/hq ADR 0463 §2: the record of merges begins at the earliest merge the controller holds on any trunk. A merge before it is
// novox (2026-10-20) about 95 of the 110 named gaps were merges of 2026-09-30 to 2026-10-14, before the record began. // history on every trunk; a trunk with no merge kept of its own still has the merges made since as gaps (mesh-delivery #1 on novox).
func TestOnlyTheMergesSinceTheControllerBeganKeepingMergesAreGaps(t *testing.T) { func TestOnlyTheMergesSinceTheRecordBeganAreGapsOnEveryTrunk(t *testing.T) {
first := time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC) // the first merge it heard on this trunk first := time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC) // the first merge it heard, on some trunk of the mesh
merges := []trunkMerge{ trunk := func(prefix string) []trunkMerge {
{Commit: "old-1", At: first.Add(-30 * 24 * time.Hour)}, return []trunkMerge{
{Commit: "old-2", At: first.Add(-2 * time.Hour)}, {Commit: prefix + "-old-1", At: first.Add(-30 * 24 * time.Hour)},
{Commit: "within-grace", At: first.Add(-mergeGrace / 2)}, {Commit: prefix + "-old-2", At: first.Add(-2 * time.Hour)},
{Commit: "the-first", At: first}, {Commit: prefix + "-within-grace", At: first.Add(-mergeGrace / 2)},
{Commit: "later", At: first.Add(48 * time.Hour)}, {Commit: prefix + "-since", At: first.Add(24 * time.Hour)},
}
} }
got := sinceTheRecord(merges, true, first) // Two trunks, one of which the controller has kept no merge of: both read by the one starting point.
var commits []string for _, prefix := range []string{"kept-trunk", "trunk-with-no-kept-merge"} {
for _, m := range got { var commits []string
commits = append(commits, m.Commit) for _, m := range sinceTheRecord(trunk(prefix), true, first) {
commits = append(commits, m.Commit)
}
if want := prefix + "-within-grace " + prefix + "-since"; strings.Join(commits, " ") != want {
t.Errorf("%s: the gaps are %v, want %q: earlier ones are history on every trunk, later ones are gaps", prefix, commits, want)
}
} }
if want := "within-grace the-first later"; strings.Join(commits, " ") != want { if none := sinceTheRecord(trunk("x"), false, time.Time{}); len(none) != 0 {
t.Fatalf("the gaps are %v, want %q: nothing before the record can have been missed", commits, want) t.Errorf("with no merge kept anywhere there are %d gap(s), but no record to have one in", len(none))
}
if none := sinceTheRecord(merges, false, time.Time{}); len(none) != 0 {
t.Fatalf("a trunk the controller kept no merge of has %d gap(s), but no record to have one in", len(none))
} }
} }
+10
View File
@@ -75,6 +75,16 @@ func (i *Inventory) KeptOnBranch(ctx context.Context, repository, branch string)
return i.chainKeptOnBranch(ctx, c, repository, branch) return i.chainKeptOnBranch(ctx, c, repository, branch)
} }
// EarliestKept says the controller keeps a merge of some repository's branch, and when it heard the first of all: where its record of
// merges begins on the whole mesh (novox/hq ADR 0463). A merge made before then is history on every trunk.
func (i *Inventory) EarliestKept(ctx context.Context) (bool, time.Time, error) {
c, err := i.theChain(ctx)
if err != nil {
return false, time.Time{}, err
}
return i.chainEarliestKept(ctx, c)
}
// AloneMerges is every merge waiting to be walked on its own commit, the newest first (ADR 0276 decision 3). // AloneMerges is every merge waiting to be walked on its own commit, the newest first (ADR 0276 decision 3).
func (i *Inventory) AloneMerges(ctx context.Context) ([]BatchedMerge, error) { func (i *Inventory) AloneMerges(ctx context.Context) ([]BatchedMerge, error) {
c, err := i.theChain(ctx) c, err := i.theChain(ctx)
+18
View File
@@ -437,6 +437,24 @@ func (i *Inventory) chainKeptOnBranch(ctx context.Context, c *chain.Chain, repos
return true, first.UTC(), nil return true, first.UTC(), nil
} }
// chainEarliestKept is when the controller heard the first merge it holds a record of, on any repository and branch.
func (i *Inventory) chainEarliestKept(ctx context.Context, c *chain.Chain) (bool, time.Time, error) {
w, err := i.chainWinners(ctx, c)
if err != nil {
return false, time.Time{}, err
}
var first time.Time
for _, h := range w {
if first.IsZero() || h.m.Heard.Before(first) {
first = h.m.Heard
}
}
if first.IsZero() {
return false, time.Time{}, nil
}
return true, first.UTC(), nil
}
func (i *Inventory) chainAloneMerges(ctx context.Context, c *chain.Chain) ([]BatchedMerge, error) { func (i *Inventory) chainAloneMerges(ctx context.Context, c *chain.Chain) ([]BatchedMerge, error) {
w, err := i.chainWinners(ctx, c) w, err := i.chainWinners(ctx, c)
if err != nil { if err != nil {
+25
View File
@@ -347,3 +347,28 @@ func TestThePlansAreReadFromAViewKeptByOneWatch(t *testing.T) {
return len(open) == 3 return len(open) == 3
}) })
} }
// Where the record of merges begins is the earliest merge kept on any repository and branch (novox/hq ADR 0463 §2).
func TestTheRecordOfMergesBeginsAtTheEarliestMergeKeptOnAnyTrunk(t *testing.T) {
inv, c := anInventoryOnTheChain(t)
ctx := context.Background()
if kept, _, err := inv.chainEarliestKept(ctx, c); err != nil || kept {
t.Fatalf("an inventory with no merge kept says it keeps one: %v %v", kept, err)
}
if err := inv.chainSavePlans(ctx, c, []*Plan{aBatch("plan-1", plansAt), aBatch("plan-2", plansAt)}, nil); err != nil {
t.Fatal(err)
}
later := aMerge("m-late", "plan-1")
later.Repository, later.Branch, later.Heard = "novox/mesh-sdk", "main", plansAt.Add(48*time.Hour)
earlier := aMerge("m-early", "plan-2")
earlier.Repository, earlier.Branch, earlier.Heard = "novox/photos", "nox-mesh", plansAt.Add(time.Hour)
for _, m := range []BatchedMerge{later, earlier} {
if added, err := inv.chainAddMerge(ctx, c, m); err != nil || !added {
t.Fatalf("%s: %v %v", m.Commit, added, err)
}
}
kept, first, err := inv.chainEarliestKept(ctx, c)
if err != nil || !kept || !first.Equal(plansAt.Add(time.Hour)) {
t.Fatalf("the record begins at %v (%v, %v), want %v", first, kept, err, plansAt.Add(time.Hour))
}
}