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 823c89d9..957d4be8 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/holder.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/holder.go @@ -57,6 +57,8 @@ type Holder struct { // catchUp asks the next look to weigh again the newest merge of each repository against source.main (SourceCatchUp). catchUp bool + // seeded are the repositories source.main was found to name, or was seeded with, since this owner started (seedTrunks). + seeded map[string]bool mu sync.Mutex deliveries map[string]*Delivery diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1726_trunk_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1726_trunk_test.go index 5c7bd1bc..532c1e63 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1726_trunk_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1726_trunk_test.go @@ -1,6 +1,7 @@ package main import ( + "strings" "testing" "time" @@ -48,6 +49,36 @@ func TestStartingWritesTheNewestMergeOfEachRepositoryNotYetInSourceMain(t *testi t.Fatalf("a second catch-up moved source.main again: %d moves, then %d", n, len(w.source.moves)) } + // Issue 1422: a repository never merged through the gate gets main's head as the forge announced its open pull request + // with, once, beside the others; a pull request into another base, or one announced with no base head (the forge could + // not read it), seeds nothing; a head source.main holds is never replaced. + const never, trunkN, trunkM = "novox/never-merged", "eeee000000000000000000000000000000000001", "eeee000000000000000000000000000000000002" + s := newWorld(t) + s.source.main = &sdk.Source{V: sdk.Version, Group: "before", Heads: map[string]string{i1726Repo: i1726Merge1}} + other := pr("novox/other-base", 6, "ffff000000000000000000000000000000000001", "fix/c", "a.go") + other.Base, other.BaseSHA = "nox-mesh", trunkM + s.h.PullUpdated(other) + unread := pr("novox/unread", 2, "ffff000000000000000000000000000000000002", "fix/d", "a.go") + s.h.PullUpdated(unread) + seed := pr(never, 4, "ffff000000000000000000000000000000000003", "fix/e", "a.go") + seed.BaseSHA = trunkN + s.h.PullUpdated(seed) + s.settleAll() + if h := s.source.main.Heads; h[never] != trunkN || h[i1726Repo] != i1726Merge1 || h["novox/other-base"] != "" || h["novox/unread"] != "" || len(s.source.moves) != 1 { + t.Fatalf("source.main is %v after %d moves; wanted %s seeded at %s, the catalogue kept and nothing else, in one move", h, len(s.source.moves), never, trunkN) + } + if !strings.Contains(s.logs(), "seeded with "+trunkN[:12]) { + t.Errorf("the seed was not said: %s", s.logs()) + } + again := pr(never, 5, "ffff000000000000000000000000000000000004", "fix/f", "a.go") + again.BaseSHA = trunkM + s.h.PullUpdated(again) + s.h.SourceCatchUp() + s.settleAll() + if got := s.source.main.Heads[never]; got != trunkN || len(s.source.moves) != 1 { + t.Fatalf("a seeded head was replaced by %s (%d moves)", got, len(s.source.moves)) + } + o := newWorld(t) o.source.main = &sdk.Source{V: sdk.Version, Group: "newer", Heads: map[string]string{i1726Repo: i1726Merge2}} o.h.PullUpdated(pr(i1726Repo, 471, head2, "fix/b", "modules/app/b.go")) 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 a3055a32..f6fec216 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "regexp" "sort" "strings" "time" @@ -524,6 +525,66 @@ func (h *Holder) settleForgeMerges() []func() { return out } +// trunkHead is a full commit id as the forge names a branch's head. +var trunkHead = regexp.MustCompile(`^[0-9a-f]{40}$`) + +// seedTrunks gives source.main the trunk head of a repository it names none of (issue 1422): one never merged through the +// gate, whose open pull requests the checker could otherwise never judge. The head is the base_sha the forge announced the +// newest open pull request into main with (gitea.pull.updated; ADR 0323), the trunk's head when it was announced. It is +// added in one compare-and-set write that never replaces a head source.main holds, so a merge always wins; the first merge +// through the gate moves it as any other. source.main is per repository and names main's head only (ADR 0484 §11, MergeBranch): +// a pull request into another base seeds nothing. A repository is weighed once per start after it was found or seeded. +// Under the lock; the writing happens outside it. +func (h *Holder) seedTrunks() []func() { + newest := map[string]*Delivery{} + for _, d := range h.deliveries { + if h.seeded[d.Repository] || d.Base != MergeBranch || !trunkHead.MatchString(d.BaseSHA) || d.State.Final() || d.MergedAs != "" { + continue + } + if o := newest[d.Repository]; o == nil || d.Created.After(o.Created) { + newest[d.Repository] = d + } + } + var out []func() + for repository, d := range newest { + id, head, group := d.ID, d.BaseSHA, firstOf(d.Branch, d.Repository) + out = append(out, func() { + seeded := false + err := h.Source.Move(func(prev *sdk.Source) (sdk.Source, bool) { + seeded = false + next := sdk.Source{Group: group, Heads: map[string]string{}} + if prev != nil { + if prev.Heads[repository] != "" { + return *prev, false // named already: never replaced + } + next.Group = prev.Group + for r, held := range prev.Heads { + next.Heads[r] = held + } + } + next.Heads[repository] = head + seeded = true + return next, true + }) + h.mu.Lock() + defer h.mu.Unlock() + if err != nil { + h.Logf("[mesh-delivery] %s: source.main names no trunk head of %s, and it cannot be seeded yet: %v; asked again in %s", id, repository, err, askAgainAfter) + h.again("source.main, seeded with the trunk head of a repository it names none of") + return + } + if h.seeded == nil { + h.seeded = map[string]bool{} + } + h.seeded[repository] = true + if seeded { + h.Logf("[mesh-delivery] source.main named no trunk head of %s: seeded with %s, main's head as the forge announced %s with, so its pull requests are judged (issue 1422)", repository, shortOf(head, 12), id) + } + }) + } + return out +} + // replayLate is how long after its merge a merge heard only from the forge's word is a replay: the forge announces a merge // within seconds, so one heard this late was replayed from the bus's history, and the source already holds what came after. const replayLate = 15 * time.Minute diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ready.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ready.go index c3ecbb17..d998ea80 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ready.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ready.go @@ -294,6 +294,7 @@ func (h *Holder) readyAsks(now time.Time) []func() { asks = append(asks, h.turnWrites()...) // the merge turns, after the holds of this look (ADR 0548 §3) asks = append(asks, h.settleMerges()...) asks = append(asks, h.settleForgeMerges()...) + asks = append(asks, h.seedTrunks()...) asks = append(asks, h.withdrawnKeys()...) asks = append(h.cancels, asks...) h.cancels = nil