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
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:
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user