From d0d00f922a91f0e90db77269559efdb1fff654f7 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 11:41:26 +0100 Subject: [PATCH] =?UTF-8?q?gate:=20an=20approved=20merge=20writes=20its=20?= =?UTF-8?q?turn=20on=20every=20repository=20it=20goes=20into=20before=20it?= =?UTF-8?q?=20is=20handed,=20kept=20for=20a=20group=20until=20its=20last?= =?UTF-8?q?=20member=20merged,=20read=20by=20the=20checker=20(ADR=200548?= =?UTF-8?q?=20=C2=A73;=20issue=20568=20G2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-gate/adr548_test.go | 78 +++++++++++++++++++ .../cmd/mesh-delivery-gate/keeps.go | 2 +- .../cmd/mesh-delivery-gate/keeps_test.go | 3 + .../cmd/mesh-delivery-gate/main.go | 1 + .../cmd/mesh-delivery-gate/manifest_test.go | 5 +- .../cmd/mesh-delivery-gate/ready.go | 1 + .../cmd/mesh-delivery-gate/turns.go | 5 +- modules/mesh-delivery-gate/module.json | 6 +- 8 files changed, 96 insertions(+), 5 deletions(-) diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr548_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr548_test.go index d17f9505..f9d28bcf 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr548_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr548_test.go @@ -1,8 +1,12 @@ package main import ( + "fmt" "strings" + "sync" "testing" + + sdk "git.novox.be/novox/mesh-sdk/go/delivery" ) // One check per delivery (novox/hq ADR 0548), the gate's part: nothing merges into a repository while a check of other heads @@ -22,6 +26,49 @@ func approvedLone(w *world) string { return IDOf(oneRepo, oneHead) } +// memTurns is the bucket turns in memory; it logs each write with the merges the fake bridge had made by then. +type memTurns struct { + mu sync.Mutex + m map[string]sdk.Turn + log []string + merges func() int +} + +func newMemTurns(w *world) *memTurns { + return &memTurns{m: map[string]sdk.Turn{}, merges: func() int { return len(w.forge.merges) }} +} + +func (f *memTurns) All() (map[string]sdk.Turn, error) { + f.mu.Lock() + defer f.mu.Unlock() + out := map[string]sdk.Turn{} + for k, v := range f.m { + out[k] = v + } + return out, nil +} + +func (f *memTurns) Put(t sdk.Turn) error { + if _, err := sdk.EncodeTurn(t); err != nil { + return err + } + f.mu.Lock() + defer f.mu.Unlock() + f.m[t.Repository] = t + f.log = append(f.log, fmt.Sprintf("put %s after %d merges", t.Repository, f.merges())) + return nil +} + +func (f *memTurns) Delete(repository string) error { + f.mu.Lock() + defer f.mu.Unlock() + delete(f.m, repository) + f.log = append(f.log, fmt.Sprintf("delete %s after %d merges", repository, f.merges())) + return nil +} + +func (f *memTurns) said() string { f.mu.Lock(); defer f.mu.Unlock(); return strings.Join(f.log, "; ") } + func TestOneCheckPerDeliveryAtTheGate(t *testing.T) { for name, run := range map[string]func(t *testing.T){ // §2: a merge into a repository a running verdict names is not written to merges, and is written on that verdict's pass. @@ -67,6 +114,37 @@ func TestOneCheckPerDeliveryAtTheGate(t *testing.T) { t.Fatalf("merges %v, run %+v: its own check held it", w.forge.merges, d.Ready) } }, + // §3: the turn is written before the verdicts are read for the merge, and deleted after the last merge. + "a lone merge's turn is written before it is handed and deleted once it merged": func(t *testing.T) { + w := newWorld(t) + turns := newMemTurns(w) + w.h.Turns = turns + approvedLone(w) + w.settleAll() + if len(w.forge.merges) != 1 || turns.said() != "put "+oneRepo+" after 0 merges; delete "+oneRepo+" after 1 merges" { + t.Fatalf("merges %v; the turns were %q", w.forge.merges, turns.said()) + } + }, + "a group's turns stand on every member's repository until its last member merged": func(t *testing.T) { + w := newWorld(t) + turns := newMemTurns(w) + w.h.Turns = turns + a, b := pr(oneRepo, 1, oneHead, "feat/two"), pr(oneRepo2, 2, oneHead2, "feat/two") + requiresCheck(w, a, b) + w.h.PullUpdated(a) + w.h.PullUpdated(b) + w.h.wantForge() + putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead), headOf(oneRepo2, 2, oneHead2)) + w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk)) + w.h.CommitStatus(checkStatus(oneRepo2, oneHead2, "success", oneTrunk)) + for range 3 { + w.settleAll() + } + want := "put " + oneRepo2 + " after 0 merges; put " + oneRepo + " after 0 merges; delete " + oneRepo2 + " after 2 merges; delete " + oneRepo + " after 2 merges" + if len(w.forge.merges) != 2 || turns.said() != want { + t.Fatalf("merges %v; the turns were %q, want %q", w.forge.merges, turns.said(), want) + } + }, } { t.Run(name, run) } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps.go index 0bd8150c..c610b4c3 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps.go @@ -6,7 +6,7 @@ import "git.novox.be/novox/mesh-sdk/go/announce" // issue 1790): the module says it itself, from its code, before it serves, and a reader is granted what it said // (clause 3). A state is readable when another module's manifest reads it (keeps_test.go names the readers). keeps_test.go holds the list to the // manifest's `state`, and keeps_integration_test.go that the module announces it on a bus. -var Kept = []announce.State{{Name: "asks", Readable: true}, {Name: "conditions"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "pulls", Readable: true}, {Name: "source", Readable: true}} +var Kept = []announce.State{{Name: "asks", Readable: true}, {Name: "conditions"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "pulls", Readable: true}, {Name: "source", Readable: true}, {Name: "turns", Readable: true}} // announceKept says Kept through keeps, which is stdio.Keeps. func announceKept(keeps func(...announce.State) error) error { return keeps(Kept...) } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_test.go index 687144b8..e3ef8b05 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_test.go @@ -17,6 +17,9 @@ var readers = map[string][]string{ "pulls": {"build-agent"}, // in mesh-controller "owed": {"mesh-delivery-forge-bridge"}, "source": {"mesh-delivery-indexer", "mesh-delivery-checker"}, + // ADR 0548 §3, §4: the checker reads the merge turns and claims no check of a repository one names; the gate alone writes + // them (a state is written by its owner's account alone, ADR 0495 §3: announcing it readable grants a read, never a write). + "turns": {"mesh-delivery-checker"}, } // 1790/S: the states the module announces are the states its manifest declares, no more, no fewer, and the readable 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 e9e4c84a..e2f35d20 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go @@ -65,6 +65,7 @@ func main() { Operator: kvOperator{}, Merges: kvMerges{}, Source: kvSource{}, + Turns: kvTurns{}, Owed: kvOwed{}, Pulls: kvPulls{}, // The gate says what stalled to the operator itself (issue 1275, ADR 0437 §9). diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go index eb9d8a66..4ca96530 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go @@ -91,9 +91,10 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) { if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged"}) { t.Fatalf("emits %v", m.Emits) } - // Eight buckets, and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3, and the conditions it raised (issue 1275). + // Nine buckets, and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3, the conditions it raised (issue 1275) and the + // merge turns (ADR 0548 §3). buckets := m.buckets() - if !reflect.DeepEqual(buckets, []string{"deliveries", "groups", "pulls", "asks", "merges", "owed", "source", "conditions"}) { + if !reflect.DeepEqual(buckets, []string{"deliveries", "groups", "pulls", "asks", "merges", "owed", "source", "conditions", "turns"}) { t.Fatalf("state %v", buckets) } // What it reads: the teller's answers and the checker's verdicts (issue 1302). The carry from today's module's buckets, the 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 d37f31aa..a141e803 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ready.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ready.go @@ -277,6 +277,7 @@ func (h *Holder) readyAsks(now time.Time) []func() { asks = append(asks, a) } } + 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.withdrawnKeys()...) diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/turns.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/turns.go index 55df4c20..600adc58 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/turns.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/turns.go @@ -282,7 +282,10 @@ func (h *Holder) turnWrites() []func() { h.Poke() }} } - want := h.wantedTurns() + want := map[string]sdk.Turn{} + if h.checkGates() { + want = h.wantedTurns() // while the check gates no merge, no merge waits for one, and no turn holds a check + } var out []func() repos := make([]string, 0, len(want)+len(h.turnsHeld)) for repo := range want { diff --git a/modules/mesh-delivery-gate/module.json b/modules/mesh-delivery-gate/module.json index f4166ce0..da631307 100644 --- a/modules/mesh-delivery-gate/module.json +++ b/modules/mesh-delivery-gate/module.json @@ -61,7 +61,11 @@ "name": "source", "history": 64 }, - "conditions" + "conditions", + { + "name": "turns", + "history": 5 + } ], "reads": [ "mesh-delivery-teller.answers",