Merge branch 'feat/47-G-builder6y' into feat/47-G-join8
# Conflicts: # modules/mesh-delivery-builder/buildrun/asks_test.go # modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go # modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go # modules/mesh-delivery-builder/internal/build/numbers.go # modules/mesh-delivery-builder/internal/build/run.go # modules/mesh-delivery-builder/internal/build/stuck.go # modules/mesh-delivery-builder/internal/versions/publish.go # modules/mesh-delivery-builder/internal/versions/reserve.go # modules/mesh-delivery-builder/manifest_test.go # modules/mesh-delivery-builder/module.json
This commit is contained in:
@@ -35,6 +35,8 @@ type AskWaiter interface {
|
||||
type Asker struct {
|
||||
Publisher AskPublisher
|
||||
Waiter AskWaiter
|
||||
// Seats finds the build module of an ask's language (design 47 G A-82).
|
||||
Seats *BuildSeats
|
||||
// Bound is how long a running step (a build node took the ask, its key written running) may stand still, with no new phase,
|
||||
// before the asker stops waiting; a step that keeps moving is waited for up to 3 Bounds from its start (issue 1930); zero waits as long as ctx. An ask waiting in its stream is not bounded: the stream's backlog is no fault
|
||||
// (ADR 0525 §6, §9), and the bound starts again when another node takes the ask.
|
||||
@@ -142,10 +144,11 @@ func (k Asker) Put(ctx context.Context, a buildstep.StepAsk) error {
|
||||
// putting them twice) and answers it when it ended, whatever its state. The bound runs from the step's start on the node
|
||||
// that took the ask, read from its running key, so an ask that waits in the stream for a free slot waits as long as ctx.
|
||||
func (k Asker) Wait(ctx context.Context, a buildstep.StepAsk) (buildstep.Step, error) {
|
||||
module, err := buildstep.BuildModuleOf(a.Language)
|
||||
bm, err := k.Seats.OfAskLanguage(a.Language)
|
||||
if err != nil {
|
||||
return buildstep.Step{}, err
|
||||
}
|
||||
module := bm.Module
|
||||
watch, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
steps, err := k.Waiter.WatchAsk(watch, module, a.ID)
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestAnAskIsPutOnceAndItsStepIsRead(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bus := JetStreamAsks{JS: js}
|
||||
k := Asker{Publisher: bus, Waiter: bus, Bound: 5 * time.Second}
|
||||
k := Asker{Seats: testSeats(), Publisher: bus, Waiter: bus, Bound: 5 * time.Second}
|
||||
a := goAsk("mvl0frv18d1960")
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := k.Put(ctx, a); err != nil {
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
return buildstep.Step{ID: a.ID, Node: "ace", State: buildstep.StepPassed, Result: []byte(`{"artifacts":[]}`)}
|
||||
}}
|
||||
subjects := &subjectsOf{inner: f}
|
||||
k := Asker{Publisher: subjects, Waiter: f}
|
||||
k := Asker{Seats: testSeats(), Publisher: subjects, Waiter: f}
|
||||
var seen, also []buildstep.Step // issue 1890: a waiter that asks to see each value of the step sees it
|
||||
// Issue 1921: a second seen on the same context (a check's begin bound inside a try) is added, never in place of the first.
|
||||
both := WithStepSeen(WithStepSeen(context.Background(), func(s buildstep.Step) { seen = append(seen, s) }),
|
||||
@@ -68,7 +68,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
if c.transport {
|
||||
restore = busretry.Waits(20*time.Millisecond, time.Millisecond, time.Millisecond) // a bound this row passes
|
||||
}
|
||||
err := (Asker{Publisher: p, Waiter: p.inner}).Put(context.Background(), goAsk("mvl0frv18d1960"))
|
||||
err := (Asker{Seats: testSeats(), Publisher: p, Waiter: p.inner}).Put(context.Background(), goAsk("mvl0frv18d1960"))
|
||||
restore()
|
||||
switch {
|
||||
case c.transport:
|
||||
@@ -94,7 +94,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
for _, st := range order {
|
||||
h.Write(st)
|
||||
}
|
||||
if got, err := (Asker{Publisher: h, Waiter: h, Bound: time.Minute}).Wait(context.Background(), goAsk("mvl0frv18d1971")); err != nil || got.State != buildstep.StepPassed {
|
||||
if got, err := (Asker{Seats: testSeats(), Publisher: h, Waiter: h, Bound: time.Minute}).Wait(context.Background(), goAsk("mvl0frv18d1971")); err != nil || got.State != buildstep.StepPassed {
|
||||
t.Errorf("%s: %+v, %v", name, got, err)
|
||||
}
|
||||
}
|
||||
@@ -106,12 +106,12 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
}
|
||||
h.Write(st)
|
||||
}
|
||||
if _, err := (Asker{Publisher: h, Waiter: h, Bound: time.Minute}).Ask(context.Background(), goAsk("mvl0frv18d1972")); err == nil || !strings.Contains(err.Error(), "transport fault: name lookup, node-build-go shanks, lookup proxy: i/o timeout") {
|
||||
if _, err := (Asker{Seats: testSeats(), Publisher: h, Waiter: h, Bound: time.Minute}).Ask(context.Background(), goAsk("mvl0frv18d1972")); err == nil || !strings.Contains(err.Error(), "transport fault: name lookup, node-build-go shanks, lookup proxy: i/o timeout") {
|
||||
t.Errorf("three runs then failed: %v", err)
|
||||
}
|
||||
h = &FakeAsks{}
|
||||
h.Write(buildstep.Step{ID: "mvl0frv18d1973", Node: "ace", State: buildstep.StepAgain, Run: 1, AgainAt: time.Now(), Started: time.Now()})
|
||||
if _, err := (Asker{Publisher: h, Waiter: h, Bound: 20 * time.Millisecond}).Wait(context.Background(), goAsk("mvl0frv18d1973")); err == nil || !strings.Contains(err.Error(), "given again after a transport fault") {
|
||||
if _, err := (Asker{Seats: testSeats(), Publisher: h, Waiter: h, Bound: 20 * time.Millisecond}).Wait(context.Background(), goAsk("mvl0frv18d1973")); err == nil || !strings.Contains(err.Error(), "given again after a transport fault") {
|
||||
t.Errorf("an ask never given again: %v", err)
|
||||
}
|
||||
// ADR 0664 §5: that ask is dropped; a wait ended by a named cause is dropped too, an ask no node took included (it waits in
|
||||
@@ -119,19 +119,19 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
drops := &seatDrops{}
|
||||
h = &FakeAsks{}
|
||||
h.Write(buildstep.Step{ID: "mvl0frv18d1974", Node: "ace", State: buildstep.StepAgain, Run: 1, AgainAt: time.Now(), Started: time.Now()})
|
||||
(Asker{Publisher: h, Waiter: h, Bound: 20 * time.Millisecond, Dropper: drops}).Wait(context.Background(), goAsk("mvl0frv18d1974"))
|
||||
(Asker{Seats: testSeats(), Publisher: h, Waiter: h, Bound: 20 * time.Millisecond, Dropper: drops}).Wait(context.Background(), goAsk("mvl0frv18d1974"))
|
||||
named, stop := context.WithCancelCause(context.Background())
|
||||
h.Write(buildstep.Step{ID: "mvl0frv18d1975", Node: "g14", State: buildstep.StepRunning, Started: time.Now()})
|
||||
seenStep := WithStepSeen(named, func(buildstep.Step) { stop(Withdrawn{Why: "superseded by a newer fingerprint"}) })
|
||||
(Asker{Publisher: h, Waiter: h, Bound: time.Hour, Dropper: drops}).Wait(seenStep, goAsk("mvl0frv18d1975"))
|
||||
(Asker{Seats: testSeats(), Publisher: h, Waiter: h, Bound: time.Hour, Dropper: drops}).Wait(seenStep, goAsk("mvl0frv18d1975"))
|
||||
shut, off := context.WithCancel(context.Background())
|
||||
off()
|
||||
(Asker{Publisher: h, Waiter: h, Bound: time.Hour, Dropper: drops}).Wait(shut, goAsk("mvl0frv18d1977"))
|
||||
(Asker{Publisher: &FakeAsks{}, Waiter: &FakeAsks{}, Bound: time.Hour, Dropper: drops}).Wait(named, goAsk("mvl0frv18d1976"))
|
||||
(Asker{Seats: testSeats(), Publisher: h, Waiter: h, Bound: time.Hour, Dropper: drops}).Wait(shut, goAsk("mvl0frv18d1977"))
|
||||
(Asker{Seats: testSeats(), Publisher: &FakeAsks{}, Waiter: &FakeAsks{}, Bound: time.Hour, Dropper: drops}).Wait(named, goAsk("mvl0frv18d1976"))
|
||||
// Review 1 (finding 1): a cause that is no withdrawal (the builder's check bound passing, which waits on) drops nothing.
|
||||
bounded, passed := context.WithCancelCause(context.Background())
|
||||
passed(errors.New("the check bound passed"))
|
||||
(Asker{Publisher: &FakeAsks{}, Waiter: &FakeAsks{}, Bound: time.Hour, Dropper: drops}).Wait(bounded, goAsk("mvl0frv18d1978"))
|
||||
(Asker{Seats: testSeats(), Publisher: &FakeAsks{}, Waiter: &FakeAsks{}, Bound: time.Hour, Dropper: drops}).Wait(bounded, goAsk("mvl0frv18d1978"))
|
||||
if got := strings.Join(drops.got, "|"); !strings.Contains(got, "mvl0frv18d1974") || !strings.Contains(got, "mvl0frv18d1975 superseded by a newer fingerprint") || !strings.Contains(got, "mvl0frv18d1976 superseded by a newer fingerprint") || len(drops.got) != 3 {
|
||||
t.Errorf("drops %q, want the ask never given again, the superseded one and the one no node took, and no other", got)
|
||||
}
|
||||
@@ -139,20 +139,20 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
// how many runs; a drop that failed is said, not discarded.
|
||||
late := &FakeAsks{}
|
||||
late.Write(buildstep.Step{ID: "mvl0frv18d1979", Node: "shanks", State: buildstep.StepFailed, Passing: true, Run: 3, Reason: "lookup proxy: i/o timeout"})
|
||||
if st, err := (Asker{Publisher: late, Waiter: late, Bound: time.Minute}).Wait(context.Background(), goAsk("mvl0frv18d1979")); err != nil || !strings.HasPrefix(st.Reason, "transport fault: name lookup, node-build-go shanks") {
|
||||
if st, err := (Asker{Seats: testSeats(), Publisher: late, Waiter: late, Bound: time.Minute}).Wait(context.Background(), goAsk("mvl0frv18d1979")); err != nil || !strings.HasPrefix(st.Reason, "transport fault: name lookup, node-build-go shanks") {
|
||||
t.Errorf("a failed step of run 3 read alone says %q (%v), want it names 3 runs", st.Reason, err)
|
||||
}
|
||||
var said []string
|
||||
bad, off2 := context.WithCancelCause(context.Background())
|
||||
off2(Withdrawn{Why: "withdrawn"})
|
||||
(Asker{Publisher: &FakeAsks{}, Waiter: &FakeAsks{}, Bound: time.Hour, Dropper: failingDrops{}, Logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }}).Wait(bad, goAsk("mvl0frv18d1980"))
|
||||
(Asker{Seats: testSeats(), Publisher: &FakeAsks{}, Waiter: &FakeAsks{}, Bound: time.Hour, Dropper: failingDrops{}, Logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }}).Wait(bad, goAsk("mvl0frv18d1980"))
|
||||
if len(said) != 1 || !strings.Contains(said[0], "mvl0frv18d1980") || !strings.Contains(said[0], "no holder") {
|
||||
t.Errorf("a drop that failed said %q, want it named with its cause", said)
|
||||
}
|
||||
// review round 2 item 1: an ask that waits in its stream for a free slot is not bounded; once a node writes its step
|
||||
// running, the step is bounded from its start, and the error names the node.
|
||||
g := &FakeAsks{}
|
||||
k = Asker{Publisher: g, Waiter: g, Bound: 20 * time.Millisecond}
|
||||
k = Asker{Seats: testSeats(), Publisher: g, Waiter: g, Bound: 20 * time.Millisecond}
|
||||
waiting, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
if _, err := k.Wait(waiting, goAsk("mvl0frv18d1959")); err != context.DeadlineExceeded {
|
||||
@@ -170,7 +170,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
// the phase; one three bounds from its start ends, said as kept moving. The steps' own times stand for the clock, so the
|
||||
// bound is an hour no loaded machine reaches and no case waits on a pause (the check of 2026-11-02 22:35 tripped one
|
||||
// of 60 ms).
|
||||
k = Asker{Publisher: g, Waiter: g, Bound: time.Hour}
|
||||
k = Asker{Seats: testSeats(), Publisher: g, Waiter: g, Bound: time.Hour}
|
||||
now := time.Now()
|
||||
g.Write(buildstep.Step{ID: "mvl0frv18d1980", Node: "ace", State: buildstep.StepRunning, Started: now.Add(-90 * time.Minute), Phase: "pb", PhaseAt: now})
|
||||
g.Write(buildstep.Step{ID: "mvl0frv18d1980", Node: "ace", State: buildstep.StepPassed})
|
||||
@@ -190,7 +190,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
// first is answered as the node wrote it.
|
||||
dead := &DeadLetters{}
|
||||
lw := LostAskWaiter{Inner: g, Dead: dead}
|
||||
k = Asker{Publisher: g, Waiter: lw, Bound: time.Minute}
|
||||
k = Asker{Seats: testSeats(), Publisher: g, Waiter: lw, Bound: time.Minute}
|
||||
g.Write(buildstep.Step{ID: "mvl0frv18d1970", Node: "ace", State: buildstep.StepRunning, Started: time.Now()})
|
||||
given, ok := GivenUpOf(map[string][]string{"Mesh-Dead-Msg-Id": {"mvl0frv18d1970"}, "Mesh-Dead-Deliveries": {"5"},
|
||||
"Mesh-Dead-Stream": {"SEAT_NODE_BUILD_GO"}, "Mesh-Dead-Gave-Up": {"2026-10-26T12:26:40Z"}})
|
||||
@@ -225,7 +225,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
if why := hearing.Deaf(); !strings.Contains(why, "SEAT_NODE_BUILD_GO: permissions violation") {
|
||||
t.Fatalf("a refused hearing says %q", why)
|
||||
}
|
||||
k = Asker{Publisher: g, Waiter: LostAskWaiter{Inner: g, Dead: dead, Hearing: hearing}, Bound: time.Minute}
|
||||
k = Asker{Seats: testSeats(), Publisher: g, Waiter: LostAskWaiter{Inner: g, Dead: dead, Hearing: hearing}, Bound: time.Minute}
|
||||
g.Write(buildstep.Step{ID: "mvl0frv18d1973", Node: "g14", State: buildstep.StepPassed})
|
||||
if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1973")); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -245,25 +245,25 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
// ended) and answered; a nudge of another build module reads nothing; a watch that closes is opened again.
|
||||
d := &FakeAsks{}
|
||||
heard := &HeardEnds{Inner: deafWatch{}, Read: d}
|
||||
k = Asker{Publisher: d, Waiter: heard, Bound: time.Minute}
|
||||
k = Asker{Seats: testSeats(), Publisher: d, Waiter: heard, Bound: time.Minute}
|
||||
d.Write(buildstep.Step{ID: "mvl0frv18d1980", Node: "shanks", State: buildstep.StepPassed})
|
||||
got := make(chan buildstep.Step, 1)
|
||||
go func() { s, _ := k.Wait(context.Background(), goAsk("mvl0frv18d1980")); got <- s }()
|
||||
for heard.Waiting() == 0 {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
heard.Nudge(NodeBuildModule)
|
||||
heard.Nudge(testNode)
|
||||
select {
|
||||
case s := <-got:
|
||||
t.Fatalf("a nudge of another build module answered %+v", s)
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
heard.Nudge(GoBuildModule)
|
||||
heard.Nudge(testGo)
|
||||
if s := <-got; s.State != buildstep.StepPassed || s.Node != "shanks" {
|
||||
t.Fatalf("a step its watch never handed over: %+v", s)
|
||||
}
|
||||
closing := &HeardEnds{Inner: &closingWatch{inner: d}, Read: d}
|
||||
if s, err := (Asker{Publisher: d, Waiter: closing}).Wait(context.Background(), goAsk("mvl0frv18d1980")); err != nil || s.State != buildstep.StepPassed {
|
||||
if s, err := (Asker{Seats: testSeats(), Publisher: d, Waiter: closing}).Wait(context.Background(), goAsk("mvl0frv18d1980")); err != nil || s.State != buildstep.StepPassed {
|
||||
t.Fatalf("a watch that closed once: %+v, %v", s, err)
|
||||
}
|
||||
for _, row := range []struct {
|
||||
@@ -287,7 +287,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
|
||||
// bound is reached only by a wait that kept the first run's start.
|
||||
func TestARedeliveryToTheSameNodeRestartsTheBound(t *testing.T) {
|
||||
f := &FakeAsks{}
|
||||
k := Asker{Publisher: f, Waiter: f, Bound: time.Hour}
|
||||
k := Asker{Seats: testSeats(), Publisher: f, Waiter: f, Bound: time.Hour}
|
||||
const id = "mvl0frv18d1962"
|
||||
now := time.Now()
|
||||
f.Write(buildstep.Step{ID: id, Node: "ace", State: buildstep.StepRunning, Started: now})
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package buildrun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// **The builder names no build module** (design 47 G §7, §428, §704; A-82): a build module is the holder of the build seat
|
||||
// whose acting rule `build` has an `on` matching an artifact's language or `kind:image`, found on the seat list (the seat
|
||||
// definitions) and the catalog half (the manifest that claims the seat), as the indexer's seed finds it. Every task's module and
|
||||
// every lane's key is that holder's name as the seat list and the catalog half give it.
|
||||
|
||||
// BuildModule is one build seat and the module that holds it.
|
||||
type BuildModule struct {
|
||||
Seat string // the seat whose acting rule builds
|
||||
Module string // the module holding the seat, empty while the catalog half names none
|
||||
// Toolchain is the language of the toolchain the rule's `on` names, empty for a rule on `kind:image` (no toolchain).
|
||||
Toolchain string
|
||||
// ByKind is whether the rule is on `kind:image`: its artifacts have no language and its nodes come from the node-engine's have.
|
||||
ByKind bool
|
||||
}
|
||||
|
||||
// AskLanguage is the language of the seat's asks (sdkstep.AskSubject): the seat is named node-build-<language>.
|
||||
func (m BuildModule) AskLanguage() string { return strings.TrimPrefix(m.Seat, "node-build-") }
|
||||
|
||||
// BuildSeats finds the build modules from the seat definitions and the holders of seats, both read at each call.
|
||||
type BuildSeats struct {
|
||||
// Defs are the seat definitions of the seat list; Holder is a seat's holder on the catalog half.
|
||||
defs func() map[string]seatdef.Definition
|
||||
holder func(seat string) (string, bool)
|
||||
}
|
||||
|
||||
// NewBuildSeats is the finder over defs (the seat list's definitions, nil before it is read) and holder.
|
||||
func NewBuildSeats(defs func() map[string]seatdef.Definition, holder func(seat string) (string, bool)) *BuildSeats {
|
||||
return &BuildSeats{defs: defs, holder: holder}
|
||||
}
|
||||
|
||||
func (s *BuildSeats) all() []BuildModule {
|
||||
if s == nil || s.defs == nil {
|
||||
return nil
|
||||
}
|
||||
defs := s.defs()
|
||||
var out []BuildModule
|
||||
for _, seat := range slices.Sorted(maps.Keys(defs)) {
|
||||
for _, r := range defs[seat].Acts {
|
||||
if r.Step != seatdef.StepBuild || r.Acting() == seatdef.OnEvery {
|
||||
continue
|
||||
}
|
||||
m := BuildModule{Seat: seat, ByKind: r.On == seatdef.OnKindImage}
|
||||
if !m.ByKind {
|
||||
m.Toolchain = r.Acting()
|
||||
}
|
||||
if s.holder != nil {
|
||||
m.Module, _ = s.holder(seat)
|
||||
}
|
||||
out = append(out, m)
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// All are the build seats with the module holding each, by seat name; a seat no catalog entry claims has no module.
|
||||
func (s *BuildSeats) All() []BuildModule { return s.all() }
|
||||
|
||||
// Modules are the holders of the build seats, sorted, none that is unnamed.
|
||||
func (s *BuildSeats) Modules() []string {
|
||||
var out []string
|
||||
for _, m := range s.all() {
|
||||
if m.Module != "" {
|
||||
out = append(out, m.Module)
|
||||
}
|
||||
}
|
||||
slices.Sort(out)
|
||||
return slices.Compact(out)
|
||||
}
|
||||
|
||||
// OfModule is the build seat held by module.
|
||||
func (s *BuildSeats) OfModule(module string) (BuildModule, bool) {
|
||||
for _, m := range s.all() {
|
||||
if m.Module == module && module != "" {
|
||||
return m, true
|
||||
}
|
||||
}
|
||||
return BuildModule{}, false
|
||||
}
|
||||
|
||||
// OfSeat is the build seat named seat.
|
||||
func (s *BuildSeats) OfSeat(seat string) (BuildModule, bool) {
|
||||
for _, m := range s.all() {
|
||||
if m.Seat == seat {
|
||||
return m, true
|
||||
}
|
||||
}
|
||||
return BuildModule{}, false
|
||||
}
|
||||
|
||||
// For is the build module of an artifact: the build seat whose rule's `on` is language, or `kind:image` when the artifact's
|
||||
// kind is image. None, or more than one, or a seat no entry claims, is the error.
|
||||
func (s *BuildSeats) For(language, kind string) (BuildModule, error) {
|
||||
var found []BuildModule
|
||||
for _, m := range s.all() {
|
||||
if (kind == "image" && m.ByKind) || (!m.ByKind && language != "" && strings.EqualFold(m.Toolchain, strings.TrimSpace(language))) {
|
||||
found = append(found, m)
|
||||
}
|
||||
}
|
||||
switch len(found) {
|
||||
case 0:
|
||||
return BuildModule{}, fmt.Errorf("no seat builds an artifact of language %q and kind %q", language, kind)
|
||||
case 1:
|
||||
if found[0].Module == "" {
|
||||
return BuildModule{}, fmt.Errorf("no module claims %s, which builds an artifact of language %q and kind %q", found[0].Seat, language, kind)
|
||||
}
|
||||
return found[0], nil
|
||||
}
|
||||
return BuildModule{}, fmt.Errorf("the seats %v each build an artifact of language %q and kind %q", seatsOf(found), language, kind)
|
||||
}
|
||||
|
||||
func seatsOf(ms []BuildModule) []string {
|
||||
var out []string
|
||||
for _, m := range ms {
|
||||
out = append(out, m.Seat)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// LanguageOf is the language of the toolchain the build module builds with, false for a module that is no build seat's holder
|
||||
// or builds with none.
|
||||
func (s *BuildSeats) LanguageOf(module string) (string, bool) {
|
||||
m, ok := s.OfModule(module)
|
||||
return m.Toolchain, ok && m.Toolchain != ""
|
||||
}
|
||||
|
||||
// Languages are the languages of the toolchains the build seats' rules name, sorted.
|
||||
func (s *BuildSeats) Languages() []string {
|
||||
var out []string
|
||||
for _, m := range s.all() {
|
||||
if m.Toolchain != "" {
|
||||
out = append(out, m.Toolchain)
|
||||
}
|
||||
}
|
||||
slices.Sort(out)
|
||||
return slices.Compact(out)
|
||||
}
|
||||
|
||||
// OfAskLanguage is the build module whose seat takes the asks of an ask language (go, node or image): the seat the sdk names
|
||||
// for it (buildstep.BuildSeat), held by the module the catalog half names.
|
||||
func (s *BuildSeats) OfAskLanguage(language string) (BuildModule, error) {
|
||||
seat, err := buildstep.BuildSeat(language)
|
||||
if err != nil {
|
||||
return BuildModule{}, err
|
||||
}
|
||||
m, ok := s.OfSeat(seat)
|
||||
if !ok {
|
||||
return BuildModule{}, fmt.Errorf("the seat list holds no build seat %s", seat)
|
||||
}
|
||||
if m.Module == "" {
|
||||
return BuildModule{}, fmt.Errorf("no module claims %s", seat)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package buildrun
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// The build module of an artifact is the holder of the build seat whose rule `on` names its language, or kind:image, as the seat
|
||||
// list and the catalog half name it (design 47 G A-82): no module name is in the code.
|
||||
func TestTheBuildModuleIsTheHolderOfTheBuildSeat(t *testing.T) {
|
||||
rule := func(step, on string) seatdef.Definition {
|
||||
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: step, On: on}}}
|
||||
}
|
||||
defs := map[string]seatdef.Definition{
|
||||
"node-build-go": rule(seatdef.StepBuild, "go"), "node-build-node": rule(seatdef.StepBuild, "typescript"),
|
||||
"node-build-image": rule(seatdef.StepBuild, seatdef.OnKindImage), "node-build-rust": rule(seatdef.StepBuild, "go"),
|
||||
"mesh-checks": rule(seatdef.StepCheck, "go"), "node-builder-every": rule(seatdef.StepBuild, "every"),
|
||||
}
|
||||
holders := map[string]string{"node-build-go": "acme-go", "node-build-node": "acme-ts", "node-build-image": "acme-img"}
|
||||
seats := NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(s string) (string, bool) { h, ok := holders[s]; return h, ok })
|
||||
for _, c := range []struct{ language, kind, module, fail string }{
|
||||
{"typescript", "bundle", "acme-ts", ""},
|
||||
{"", "image", "acme-img", ""},
|
||||
{"python", "bundle", "", "no seat builds"},
|
||||
{"go", "bundle", "", "each build"}, // node-build-go and node-build-rust both build go
|
||||
} {
|
||||
got, err := seats.For(c.language, c.kind)
|
||||
switch {
|
||||
case c.fail != "" && (err == nil || !strings.Contains(err.Error(), c.fail)):
|
||||
t.Errorf("For(%q, %q) = %+v, %v; want the error %q", c.language, c.kind, got, err, c.fail)
|
||||
case c.fail == "" && (err != nil || got.Module != c.module):
|
||||
t.Errorf("For(%q, %q) = %+v, %v; want %s", c.language, c.kind, got, err, c.module)
|
||||
}
|
||||
}
|
||||
delete(defs, "node-build-rust")
|
||||
if m, err := seats.For("go", "bundle"); err != nil || m.Module != "acme-go" || m.Toolchain != "go" || m.AskLanguage() != "go" {
|
||||
t.Errorf("For(go) = %+v, %v", m, err)
|
||||
}
|
||||
if l, ok := seats.LanguageOf("acme-ts"); !ok || l != "typescript" {
|
||||
t.Errorf("LanguageOf(acme-ts) = %q, %v", l, ok)
|
||||
}
|
||||
if _, ok := seats.LanguageOf("acme-img"); ok {
|
||||
t.Error("an image build module has a toolchain language")
|
||||
}
|
||||
if got := strings.Join(seats.Modules(), ","); got != "acme-go,acme-img,acme-ts" {
|
||||
t.Errorf("Modules = %s", got)
|
||||
}
|
||||
delete(holders, "node-build-go") // no catalog entry claims the seat: no build module, said by name
|
||||
if _, err := seats.For("go", "bundle"); err == nil || !strings.Contains(err.Error(), "no module claims node-build-go") {
|
||||
t.Errorf("a seat with no holder: %v", err)
|
||||
}
|
||||
if m, err := seats.OfAskLanguage("image"); err != nil || m.Module != "acme-img" {
|
||||
t.Errorf("OfAskLanguage(image) = %+v, %v", m, err)
|
||||
}
|
||||
if got := NewBuildSeats(func() map[string]seatdef.Definition { return nil }, nil).Modules(); len(got) != 0 {
|
||||
t.Errorf("before the seat list is read: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,9 @@ type HeardEnds struct {
|
||||
|
||||
mu sync.Mutex
|
||||
waiting map[string]*waitedAsk // by ask id
|
||||
watched map[string]bool // the modules whose steps are watched
|
||||
asks JetStreamAsks
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
type waitedAsk struct {
|
||||
@@ -180,16 +183,31 @@ func HearEnds(ctx context.Context, asks JetStreamAsks, inner AskWaiter, modules
|
||||
if logf == nil {
|
||||
logf = func(string, ...any) {}
|
||||
}
|
||||
h := &HeardEnds{Inner: inner, Read: asks, Logf: logf}
|
||||
h := &HeardEnds{Inner: inner, Read: asks, Logf: logf, asks: asks, ctx: ctx, watched: map[string]bool{}}
|
||||
h.Watch(modules)
|
||||
return h
|
||||
}
|
||||
|
||||
// Watch starts the watch of the steps of each module not watched yet: the build modules are the holders of the build seats the
|
||||
// seat list and the catalog half name (design 47 G A-82), known only once both are read, and again at each change of them.
|
||||
func (h *HeardEnds) Watch(modules []string) {
|
||||
for _, module := range modules {
|
||||
kv, err := asks.JS.KeyValue(ctx, StepsBucket(module))
|
||||
if err != nil {
|
||||
logf("the steps of %s cannot be read (%v): an ask whose own watch goes deaf is read only at a reconnect", module, err)
|
||||
h.mu.Lock()
|
||||
done := h.watched[module]
|
||||
h.mu.Unlock()
|
||||
if done {
|
||||
continue
|
||||
}
|
||||
go h.follow(ctx, kv, module)
|
||||
kv, err := h.asks.JS.KeyValue(h.ctx, StepsBucket(module))
|
||||
if err != nil {
|
||||
h.logf("the steps of %s cannot be read (%v): an ask whose own watch goes deaf is read only at a reconnect", module, err)
|
||||
continue
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.watched[module] = true
|
||||
h.mu.Unlock()
|
||||
go h.follow(h.ctx, kv, module)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *HeardEnds) follow(ctx context.Context, kv jetstream.KeyValue, module string) {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package buildrun
|
||||
|
||||
// The build modules the mesh runs, by the language each builds (ADR 0469).
|
||||
const (
|
||||
GoBuildModule = "mesh-build-go"
|
||||
NodeBuildModule = "mesh-build-node"
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
package buildrun
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// The build modules of the fixture seat list: the holders of the three build seats (design 47 G A-82).
|
||||
const (
|
||||
testGo = "mesh-build-go"
|
||||
testNode = "mesh-build-node"
|
||||
testImage = "mesh-build-image"
|
||||
)
|
||||
|
||||
// testSeats is the finder over a seat list whose build seats act on go, typescript and kind:image, each held by its module.
|
||||
func testSeats() *BuildSeats {
|
||||
rule := func(on string) seatdef.Definition {
|
||||
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
|
||||
}
|
||||
defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript"), "node-build-image": rule(seatdef.OnKindImage)}
|
||||
holders := map[string]string{"node-build-go": testGo, "node-build-node": testNode, "node-build-image": testImage}
|
||||
return NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func TestPlacedNodes(t *testing.T) {
|
||||
t.Run("the assignments names the counted nodes once it is settled", func(t *testing.T) {
|
||||
gate := NewPlacedNodes(func(string, ...any) {})
|
||||
for _, node := range three {
|
||||
k, v := placing(t, node, NodeBuildModule)
|
||||
k, v := placing(t, node, testNode)
|
||||
if gate.Assignments(k, v, false, old) {
|
||||
t.Fatal("an assignments before it is settled changed the counted nodes")
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func TestPlacedNodes(t *testing.T) {
|
||||
if got := strings.Join(gate.Nodes(), ","); got != "g14,novox,shanks" {
|
||||
t.Fatalf("nodes = %q", got)
|
||||
}
|
||||
k, v := placing(t, "ace", GoBuildModule)
|
||||
k, v := placing(t, "ace", testGo)
|
||||
if !gate.Assignments(k, v, false, old) || len(gate.Nodes()) != 4 {
|
||||
t.Fatalf("ace placed: nodes %v", gate.Nodes())
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func TestPlacedNodes(t *testing.T) {
|
||||
at := old.Add(time.Duration(n) * time.Hour)
|
||||
r := meshwants.AssignmentRevision{Revision: n, Kind: "trunk", At: &at}
|
||||
for _, node := range nodes {
|
||||
r.Entries = append(r.Entries, meshwants.NodeEntryRef{Node: node, Module: NodeBuildModule, Digest: "d-" + node})
|
||||
r.Entries = append(r.Entries, meshwants.NodeEntryRef{Node: node, Module: testNode, Digest: "d-" + node})
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -42,16 +42,16 @@ func TestToolchainGate(t *testing.T) {
|
||||
placed := time.Date(2026, 10, 25, 19, 56, 45, 0, time.UTC) // mesh-build-node placed on ace again
|
||||
three := []string{"g14", "novox", "shanks"}
|
||||
start := func(t *testing.T, seeded bool) (*ToolchainGate, *ToolchainSet, *[]string) {
|
||||
set := &ToolchainSet{}
|
||||
set := &ToolchainSet{Seats: testSeats()}
|
||||
said := &[]string{}
|
||||
gate := NewToolchainGate([]string{GoBuildModule, NodeBuildModule}, set.Put, func(f string, a ...any) { *said = append(*said, fmt.Sprintf(f, a...)) })
|
||||
gate := NewToolchainGate([]string{testGo, testNode}, set.Put, func(f string, a ...any) { *said = append(*said, fmt.Sprintf(f, a...)) })
|
||||
for _, node := range three {
|
||||
if seeded {
|
||||
key, value := placing(t, node, NodeBuildModule)
|
||||
key, value := placing(t, node, testNode)
|
||||
gate.Assignments(key, value, false, old.Add(-time.Hour))
|
||||
}
|
||||
key, value := gateReport(t, node, "v26.12.1", placed.Add(time.Minute))
|
||||
gate.Toolchain(NodeBuildModule, key, value, false)
|
||||
gate.Toolchain(testNode, key, value, false)
|
||||
}
|
||||
return gate, set, said
|
||||
}
|
||||
@@ -59,11 +59,11 @@ func TestToolchainGate(t *testing.T) {
|
||||
t.Run("not placed on ace: its report is no builder's", func(t *testing.T) {
|
||||
gate, set, said := start(t, true)
|
||||
key, value := gateReport(t, "ace", "v26.12.0", old)
|
||||
gate.Toolchain(NodeBuildModule, key, value, false)
|
||||
gate.Toolchain(testNode, key, value, false)
|
||||
if a := buildstep.ToolchainAgreement(set.Entries("typescript")); len(a.SittingOut) != 1 || a.SittingOut[0].Node != "ace" { // ADR 0584: the majority is agreed, ace's report counts as the one that sits out
|
||||
t.Fatalf("before the assignments is settled every report counts; got %+v", a)
|
||||
}
|
||||
k, v := placing(t, "ace", GoBuildModule) // another build module on ace: no matter
|
||||
k, v := placing(t, "ace", testGo) // another build module on ace: no matter
|
||||
gate.Assignments(k, v, false, old)
|
||||
if !gate.Settled() {
|
||||
t.Fatal("settling left ace's report out, so what was passed on changed")
|
||||
@@ -74,11 +74,11 @@ func TestToolchainGate(t *testing.T) {
|
||||
if len(*said) != 1 || !strings.Contains((*said)[0], "does not place mesh-build-node on ace") {
|
||||
t.Fatalf("ace's report is said left out once; said %q", *said)
|
||||
}
|
||||
if gate.Toolchain(NodeBuildModule, key, value, false) {
|
||||
if gate.Toolchain(testNode, key, value, false) {
|
||||
t.Fatal("a report of a node not placed changed what was passed on")
|
||||
}
|
||||
// Placed on ace with a report written since: it counts, and the builders really disagree.
|
||||
k, v = placing(t, "ace", GoBuildModule, NodeBuildModule)
|
||||
k, v = placing(t, "ace", testGo, testNode)
|
||||
gate.Assignments(k, v, false, old.Add(-time.Minute))
|
||||
if a := buildstep.ToolchainAgreement(set.Entries("typescript")); a.Toolchain.Version != "v26.12.1" || len(a.SittingOut) != 1 || a.SittingOut[0].Version != "v26.12.0" {
|
||||
t.Fatalf("builders that run different versions: the majority is agreed and ace sits out; got %+v", a)
|
||||
@@ -92,24 +92,24 @@ func TestToolchainGate(t *testing.T) {
|
||||
}
|
||||
// A report deleted from the bucket goes whether its node is placed or not.
|
||||
key, _ = gateReport(t, "g14", "v26.12.1", placed)
|
||||
if !gate.Toolchain(NodeBuildModule, key, nil, true) || len(set.Entries("typescript")) != 2 {
|
||||
if !gate.Toolchain(testNode, key, nil, true) || len(set.Entries("typescript")) != 2 {
|
||||
t.Fatalf("deleting g14's report: %d entries left, want novox and shanks", len(set.Entries("typescript")))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("build modules given later pass their reports on when the assignments moves", func(t *testing.T) {
|
||||
set := &ToolchainSet{}
|
||||
set := &ToolchainSet{Seats: testSeats()}
|
||||
gate := NewToolchainGate(nil, set.Put, func(string, ...any) {})
|
||||
gate.Settled()
|
||||
k, v := placing(t, "novox", GoBuildModule)
|
||||
k, v := placing(t, "novox", testGo)
|
||||
gate.Assignments(k, v, false, old)
|
||||
key, value := gateReport(t, "ace", "v26.12.1", placed.Add(time.Minute))
|
||||
gate.Toolchain(NodeBuildModule, key, value, false) // ace is not placed: left out
|
||||
k, v = placing(t, "ace", NodeBuildModule)
|
||||
gate.Toolchain(testNode, key, value, false) // ace is not placed: left out
|
||||
k, v = placing(t, "ace", testNode)
|
||||
if gate.Assignments(k, v, false, placed) || len(set.Entries("typescript")) != 0 {
|
||||
t.Fatalf("a module the gate was not given passed on at the assignments: %v", set.Entries("typescript"))
|
||||
}
|
||||
if !gate.UseModules([]string{NodeBuildModule}) || len(set.Entries("typescript")) != 1 {
|
||||
if !gate.UseModules([]string{testNode}) || len(set.Entries("typescript")) != 1 {
|
||||
t.Fatalf("given mesh-build-node, ace's report placed since is passed on; entries %v", set.Entries("typescript"))
|
||||
}
|
||||
})
|
||||
@@ -127,16 +127,16 @@ func TestToolchainGate(t *testing.T) {
|
||||
k, v := placing(t, "ace") // ace's key, first heard placing nothing
|
||||
gate.Assignments(k, v, false, old.Add(-time.Hour))
|
||||
gate.Settled()
|
||||
k, v = placing(t, "ace", NodeBuildModule)
|
||||
k, v = placing(t, "ace", testNode)
|
||||
if gate.Assignments(k, v, false, placed) {
|
||||
t.Fatal("assignments with no report passed something on")
|
||||
}
|
||||
key, value := gateReport(t, "ace", "v26.12.0", c.at)
|
||||
if got := gate.Toolchain(NodeBuildModule, key, value, false); got != c.counts {
|
||||
if got := gate.Toolchain(testNode, key, value, false); got != c.counts {
|
||||
t.Fatalf("ace's report passed on: %v, want %v", got, c.counts)
|
||||
}
|
||||
// ace's assignments written again with another module beside: its standing does not move, nothing is said again.
|
||||
k, v = placing(t, "ace", GoBuildModule, NodeBuildModule)
|
||||
k, v = placing(t, "ace", testGo, testNode)
|
||||
if gate.Assignments(k, v, false, placed.Add(time.Minute)) {
|
||||
t.Fatal("the same assignments of mesh-build-node changed what was passed on")
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func TestToolchainGate(t *testing.T) {
|
||||
t.Fatalf("said %q, want once why ace's report is left out", *said)
|
||||
}
|
||||
key, value = gateReport(t, "ace", "v26.12.1", placed.Add(time.Minute))
|
||||
if !gate.Toolchain(NodeBuildModule, key, value, false) || len(set.Entries("typescript")) != 4 {
|
||||
if !gate.Toolchain(testNode, key, value, false) || len(set.Entries("typescript")) != 4 {
|
||||
t.Fatal("ace's new report was not passed on")
|
||||
}
|
||||
})
|
||||
@@ -159,12 +159,12 @@ func TestToolchainGate(t *testing.T) {
|
||||
t.Run("no assignments held: every report counts", func(t *testing.T) {
|
||||
gate, set, _ := start(t, false)
|
||||
key, value := gateReport(t, "ace", "v26.12.0", old)
|
||||
gate.Toolchain(NodeBuildModule, key, value, false)
|
||||
gate.Toolchain(testNode, key, value, false)
|
||||
gate.Settled()
|
||||
if n := len(set.Entries("typescript")); n != 4 {
|
||||
t.Fatalf("before the first assignments file every report counts; got %d", n)
|
||||
}
|
||||
k, v := placing(t, "g14", NodeBuildModule)
|
||||
k, v := placing(t, "g14", testNode)
|
||||
if !gate.Assignments(k, v, false, old.Add(-time.Hour)) || len(set.Entries("typescript")) != 1 {
|
||||
t.Fatalf("the first assignments leaves every node it does not place out; got %d", len(set.Entries("typescript")))
|
||||
}
|
||||
@@ -177,20 +177,20 @@ func TestToolchainGate(t *testing.T) {
|
||||
// leaves out a node no revision places the build module on, and counts a node placed in a later revision only by a
|
||||
// report written since that revision's time.
|
||||
t.Run("fed by the assignment half", func(t *testing.T) {
|
||||
set := &ToolchainSet{}
|
||||
set := &ToolchainSet{Seats: testSeats()}
|
||||
said := &[]string{}
|
||||
gate := NewToolchainGate([]string{GoBuildModule, NodeBuildModule}, set.Put, func(f string, a ...any) { *said = append(*said, fmt.Sprintf(f, a...)) })
|
||||
gate := NewToolchainGate([]string{testGo, testNode}, set.Put, func(f string, a ...any) { *said = append(*said, fmt.Sprintf(f, a...)) })
|
||||
for _, node := range append([]string{"ace"}, three...) {
|
||||
key, value := gateReport(t, node, "v26.12.1", placed.Add(time.Minute))
|
||||
if node == "ace" {
|
||||
key, value = gateReport(t, node, "v26.12.0", old)
|
||||
}
|
||||
gate.Toolchain(NodeBuildModule, key, value, false)
|
||||
gate.Toolchain(testNode, key, value, false)
|
||||
}
|
||||
revision := func(n int64, at time.Time, nodes ...string) meshwants.AssignmentRevision {
|
||||
r := meshwants.AssignmentRevision{Revision: n, Kind: "trunk", At: &at}
|
||||
for _, node := range nodes {
|
||||
r.Entries = append(r.Entries, meshwants.NodeEntryRef{Node: node, Module: NodeBuildModule, Digest: "d-" + node})
|
||||
r.Entries = append(r.Entries, meshwants.NodeEntryRef{Node: node, Module: testNode, Digest: "d-" + node})
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -207,7 +207,7 @@ func TestToolchainGate(t *testing.T) {
|
||||
t.Fatalf("said %q, want why ace's old report is left out", *said)
|
||||
}
|
||||
key, value := gateReport(t, "ace", "v26.12.1", placed.Add(time.Minute))
|
||||
if !gate.Toolchain(NodeBuildModule, key, value, false) || len(set.Entries("typescript")) != 4 {
|
||||
if !gate.Toolchain(testNode, key, value, false) || len(set.Entries("typescript")) != 4 {
|
||||
t.Fatal("ace's report written since revision 8 is not passed on")
|
||||
}
|
||||
if !gate.Revision(revision(9, placed.Add(time.Hour), "ace", "novox", "shanks")) || len(set.Entries("typescript")) != 3 || len(gate.Nodes()) != 3 {
|
||||
@@ -218,27 +218,27 @@ func TestToolchainGate(t *testing.T) {
|
||||
t.Run("the history: a module in the first revision heard of a key stands since before it", func(t *testing.T) {
|
||||
p := meshmoduleassignerclient.NewPlaced()
|
||||
// ace's node key is the first revision heard of it: its module stands since before.
|
||||
k, v := placing(t, "ace", NodeBuildModule)
|
||||
k, v := placing(t, "ace", testNode)
|
||||
p.Put(k, v, false, old)
|
||||
if _, changed, _ := p.Put("ace."+NodeBuildModule, []byte("{}"), false, placed); len(changed) != 0 {
|
||||
if _, changed, _ := p.Put("ace."+testNode, []byte("{}"), false, placed); len(changed) != 0 {
|
||||
t.Errorf("a module key says which modules stand: %v", changed)
|
||||
}
|
||||
k2, v2 := placing(t, "g14")
|
||||
p.Put(k2, v2, false, old)
|
||||
k2, v2 = placing(t, "g14", NodeBuildModule)
|
||||
if _, changed, _ := p.Put(k2, v2, false, placed); strings.Join(changed, ",") != NodeBuildModule {
|
||||
k2, v2 = placing(t, "g14", testNode)
|
||||
if _, changed, _ := p.Put(k2, v2, false, placed); strings.Join(changed, ",") != testNode {
|
||||
t.Errorf("a module placed says changed %v", changed)
|
||||
}
|
||||
if _, _, err := p.Put("novox.node", []byte("not json"), false, old); err == nil {
|
||||
t.Error("a value that does not read was not said")
|
||||
}
|
||||
if s, ok := p.Since("ace", NodeBuildModule); !ok || !s.IsZero() {
|
||||
if s, ok := p.Since("ace", testNode); !ok || !s.IsZero() {
|
||||
t.Errorf("ace's module stands since %v, %v; want the zero time", s, ok)
|
||||
}
|
||||
if s, ok := p.Since("g14", NodeBuildModule); !ok || !s.Equal(placed) {
|
||||
if s, ok := p.Since("g14", testNode); !ok || !s.Equal(placed) {
|
||||
t.Errorf("g14's module stands since %v, %v; want the revision that placed it", s, ok)
|
||||
}
|
||||
if got := p.PlacedNodes(NodeBuildModule); strings.Join(got, ",") != "ace,g14" || len(p.ByNode()) != 2 || !p.Seeded() {
|
||||
if got := p.PlacedNodes(testNode); strings.Join(got, ",") != "ace,g14" || len(p.ByNode()) != 2 || !p.Seeded() {
|
||||
t.Errorf("PlacedNodes = %v, ByNode = %v", got, p.ByNode())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,9 +7,6 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
|
||||
// LanguageOf is the language each build module's toolchain reports hold.
|
||||
var LanguageOf = map[string]string{GoBuildModule: "go", NodeBuildModule: "typescript"}
|
||||
|
||||
// ToolchainsHeardState is the builder's old state section of the toolchains it heard (design 47 G §604, §546): one key
|
||||
// <node>.<language>, the toolchain as the build seat said it.
|
||||
const (
|
||||
@@ -20,8 +17,10 @@ const (
|
||||
// ToolchainSet holds each node's toolchain of a language, as the builder heard it (design 47 G §604). Safe for use from
|
||||
// the consumer and from a build at once.
|
||||
type ToolchainSet struct {
|
||||
mu sync.Mutex
|
||||
by map[string]map[string]buildstep.Toolchain // language -> node -> entry
|
||||
// Seats finds the build module of a language: a report of a module that holds no build seat is left out (design 47 G A-82).
|
||||
Seats *BuildSeats
|
||||
mu sync.Mutex
|
||||
by map[string]map[string]buildstep.Toolchain // language -> node -> entry
|
||||
}
|
||||
|
||||
// Set takes a node's toolchain in and answers whether the language's entries changed (a node's version, recipe or
|
||||
@@ -96,7 +95,7 @@ const (
|
||||
// delete. It answers whether the language's entries changed (a node's version or recipe, or a node appearing or going).
|
||||
// A key or a value that is not an entry of the module's language is left out.
|
||||
func (s *ToolchainSet) Put(module, key string, value []byte, deleted bool) bool {
|
||||
language, ok := LanguageOf[module]
|
||||
language, ok := s.Seats.LanguageOf(module)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -15,10 +15,15 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
||||
@@ -35,8 +40,11 @@ type queueAsker func(key string, value []byte) (json.RawMessage, error)
|
||||
|
||||
// Tools are the builder's tools: stuck reads, askAgain writes the reasks value, queue writes a queues value and waits for
|
||||
// the builder's read, buildAgain writes a reasks value of mode build and waits for the builder's answer.
|
||||
//
|
||||
// builder_versions, asked by a name, answers also what the verb reserved answers for it (reserve; design 47 G §572): the
|
||||
// bundle calls the seat mesh-builds through its invokes, as any caller does. Nil answers none.
|
||||
func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, queue, buildAgain queueAsker, since build.LogSince, now func() time.Time, vs versions.Reader,
|
||||
fw, of versions.Reader) []stdio.Tool {
|
||||
fw, of versions.Reader, reserve func(name, version string) (meshbuilds.ReservedAnswer, error)) []stdio.Tool {
|
||||
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
|
||||
// handAct writes a reasks value of a versions hand act and answers the builder's answer, or its refusal as an error.
|
||||
handAct := func(key string, value []byte, err error) (any, error) {
|
||||
@@ -83,7 +91,16 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q
|
||||
"version": map[string]any{"type": "string", "description": "one version, to ask whether it is recorded"},
|
||||
"digest": map[string]any{"type": "string", "description": "with version: the digest the holder received"}}},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
return versions.AnswerVersions(context.Background(), vs, str(args, "name"), str(args, "version"), str(args, "digest"))
|
||||
out, err := versions.AnswerVersions(context.Background(), vs, str(args, "name"), str(args, "version"), str(args, "digest"))
|
||||
if err != nil || reserve == nil {
|
||||
return out, err
|
||||
}
|
||||
ra, err := reserve(out.Name, str(args, "version"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the seat mesh-builds' reserved did not answer for %s: %w", out.Name, err)
|
||||
}
|
||||
out.Reserved = &ra
|
||||
return out, nil
|
||||
}},
|
||||
{Name: build.StuckTool,
|
||||
Description: "Every build key that waits (building or queued), oldest first: its module, fingerprint, step words and since when, and each ask it " +
|
||||
@@ -307,13 +324,15 @@ func written(s stdio.KeptState, key string, revision uint64) time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// The seat verbs the bundle calls (the manifest's invokes).
|
||||
const (
|
||||
catalogVersionsVerb = "seat:mesh-catalog.versions"
|
||||
reservedVerb = "seat:mesh-builds.reserved"
|
||||
)
|
||||
|
||||
func main() {
|
||||
steps := map[string]build.StateRead{}
|
||||
for _, bm := range build.StuckBuildModules {
|
||||
steps[bm] = read(stdio.State(bm + ".steps"))
|
||||
}
|
||||
reasks := stdio.State(build.ReAsksState)
|
||||
r := build.StuckReader{Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] "+f+"\n", a...) }, Builds: read(stdio.State("builds")), ReAsks: read(reasks), Steps: steps,
|
||||
r := build.StuckReader{Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] "+f+"\n", a...) }, Builds: read(stdio.State("builds")), ReAsks: read(reasks), StepsOf: func(seat string) build.StateRead { return read(stdio.State(seat + ".steps")) },
|
||||
Hearing: read(stdio.State(buildrun.HearingState))}
|
||||
put := func(key string, value []byte) error {
|
||||
_, err := reasks.Put(key, json.RawMessage(value))
|
||||
@@ -363,10 +382,52 @@ func main() {
|
||||
}
|
||||
return e.Value, true, nil
|
||||
}}
|
||||
tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(builds.BuilderLog).Since, time.Now, vr, fr, ofr)
|
||||
// The catalog half's view of the packages the bundle follows itself (design 47 G §572, A-70), as other bundles follow views.
|
||||
pk := &views.Reader{Module: builds.BuilderModule, Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] "+f+"\n", a...) },
|
||||
Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
|
||||
branches, err := meshcatalogclient.FollowBranches(pk, stdio.Ask)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "mesh-delivery-builder-tools:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
pk.Start(context.Background())
|
||||
// The build modules are no names (design 47 G A-82): the build seats on the seat list, each held by the module the catalog
|
||||
// half's manifests say; the seat list and its two tools wired once by mesh-sdk (§204).
|
||||
gate := seatlist.NewPart("builder", []string{stdio.AnyPerson, "node-tools", "mesh-healers"})
|
||||
go func() {
|
||||
<-stdio.Served()
|
||||
gate.Follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] "+f+"\n", a...) })
|
||||
}()
|
||||
r.Seats = buildrun.NewBuildSeats(func() map[string]seatdef.Definition {
|
||||
if rd := gate.Reader(); rd != nil {
|
||||
return rd.Definitions()
|
||||
}
|
||||
return nil
|
||||
}, versions.HolderOf(branches))
|
||||
carriers := versions.CarriersFrom(versions.EntriesOf(branches))
|
||||
released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) {
|
||||
raw, err := stdio.Ask(catalogVersionsVerb, meshcatalog.VersionsInput{Module: module})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var a meshcatalog.VersionsAnswer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return nil, fmt.Errorf("the seat mesh-catalog's versions answered what does not read: %w", err)
|
||||
}
|
||||
return a.Versions, nil
|
||||
}
|
||||
reserve := func(name, version string) (meshbuilds.ReservedAnswer, error) {
|
||||
raw, err := stdio.Ask(reservedVerb, meshbuilds.ReservedInput{Name: name, Version: version})
|
||||
if err != nil {
|
||||
return meshbuilds.ReservedAnswer{}, err
|
||||
}
|
||||
var a meshbuilds.ReservedAnswer
|
||||
return a, json.Unmarshal(raw, &a)
|
||||
}
|
||||
tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(builds.BuilderLog).Since, time.Now, vr, fr, ofr, reserve)
|
||||
tools = append(tools, buildTool(&build.BuildReader{Builds: read(stdio.State("builds")), Entry: entryOf(stdio.State("builds")), Asked: read(stdio.State("asked")),
|
||||
Newest: newestVia(stdio.Ask), Steps: r}))
|
||||
tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...)
|
||||
tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask}, carriers, released)...)
|
||||
qs, as := stdio.State(versions.PublishQueueState), stdio.State(versions.ActedState)
|
||||
tools = append(tools, republishTools(readerOf(qs), readerOf(as), buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...)
|
||||
ws := stdio.State(build.WaitsState)
|
||||
@@ -392,6 +453,7 @@ func main() {
|
||||
// builder_first_views and builder_first_view_again: where the builder's first views of the indexer's and the assigner's
|
||||
// states stand, and asking one again by hand (ADR 0615, DLV-M1).
|
||||
tools = append(tools, views.StatusTools(builds.BuilderModule, "builder_first_views", "builder_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
|
||||
tools = append(tools, gate.Tools()...)
|
||||
if err := stdio.Serve("", tools); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -16,7 +17,10 @@ import (
|
||||
// reservedTools are builder_reserved, builder_reserved_drop and the seat mesh-builds' verb reserved (design 47 G section
|
||||
// 369), served as tools of the bundle. The drop is a hand act: it is recorded in the hand-act log first, then written to
|
||||
// reasks for the builder, which owns the reservations, to answer.
|
||||
func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, acts meshhandactsclient.Recorder) []stdio.Tool {
|
||||
//
|
||||
// The verb reserved given a name finds the module from the catalog half's view the bundle follows (carriers) and, for a version
|
||||
// no reservation holds, asks the seat mesh-catalog's verb versions (released); the builder's process serves no call (A-70).
|
||||
func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, acts meshhandactsclient.Recorder, carriers func(context.Context, string) ([]versions.Built, error), released versions.CatalogVersions) []stdio.Tool {
|
||||
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
|
||||
return []stdio.Tool{
|
||||
{Name: versions.ReservedTool, Description: versions.ReservedDescription,
|
||||
@@ -25,10 +29,14 @@ func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, act
|
||||
Run: func(args map[string]any) (any, error) { return versions.AnswerReserved(rr, str(args, "module")) }},
|
||||
{Name: versions.ReservedVerb, Description: "The seat mesh-builds' verb reserved: a module's reserved version with each artifact's digest once built " +
|
||||
"(design 47 G section 369), for a registry holder deciding whether to publish. Read-only.",
|
||||
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
||||
"module": map[string]any{"type": "string", "description": "the module"},
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"module": map[string]any{"type": "string", "description": "the module; give this or name"},
|
||||
"name": map[string]any{"type": "string", "description": "a package's or an image's name in its ecosystem, in place of module"},
|
||||
"version": map[string]any{"type": "string", "description": "the version, when asked for a particular one"}}},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
if name := str(args, "name"); name != "" {
|
||||
return versions.AnswerReservedByName(context.Background(), rr, carriers, released, name, str(args, "version"))
|
||||
}
|
||||
return versions.AnswerReservedVerb(rr, str(args, "module"), str(args, "version"))
|
||||
}},
|
||||
{Name: versions.ReservedDropTool, Description: versions.ReservedDropDescription,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
|
||||
"mesh-delivery-builder/internal/build"
|
||||
"mesh-delivery-builder/internal/versions"
|
||||
)
|
||||
|
||||
// Design 47 G §572, §582, A-70: the bundle's verb reserved takes a name and answers from the view it follows and the catalog's
|
||||
// versions, and builder_versions, asked by a name, calls reserved and answers what it answers.
|
||||
func TestReservedByNameIsTheBundlesAndBuilderVersionsCallsIt(t *testing.T) {
|
||||
none := versions.Reader{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, bool, error) { return nil, false, nil }}
|
||||
carriers := func(_ context.Context, name string) ([]versions.Built, error) {
|
||||
if name == "@novox/mesh-sdk" {
|
||||
return []versions.Built{{Module: "mesh-sdk", Artifact: "ts"}}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) {
|
||||
return []meshcatalog.VersionEntry{{Module: module, Version: "0.1.2", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}}}, nil
|
||||
}
|
||||
acts := meshhandactsclient.Recorder{Ask: func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil }}
|
||||
byName := map[string]func(map[string]any) (any, error){}
|
||||
for _, tl := range reservedTools(none, nil, time.Now, acts, carriers, released) {
|
||||
byName[tl.Name] = tl.Run
|
||||
}
|
||||
got, err := byName[versions.ReservedVerb](map[string]any{"name": "@novox/mesh-sdk", "version": "0.1.2"})
|
||||
want := meshbuilds.ReservedAnswer{Module: "mesh-sdk", Version: "0.1.2", Released: true, Artifacts: []meshbuilds.ReservedAnswerArtifactsItem{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}}
|
||||
if err != nil || got.(meshbuilds.ReservedAnswer).Released != want.Released || got.(meshbuilds.ReservedAnswer).Artifacts[0] != want.Artifacts[0] {
|
||||
t.Fatalf("reserved by name = %+v (%v), want %+v", got, err, want)
|
||||
}
|
||||
var called []string
|
||||
reserve := func(name, version string) (meshbuilds.ReservedAnswer, error) {
|
||||
called = append(called, name+"@"+version)
|
||||
return want, nil
|
||||
}
|
||||
tools := Tools(build.StuckReader{}, nil, nil, nil, nil, time.Now, none, none, none, reserve)
|
||||
for _, tl := range tools {
|
||||
if tl.Name == versions.VersionsTool {
|
||||
out, err := tl.Run(map[string]any{"name": "@novox/mesh-sdk", "version": "0.1.2"})
|
||||
ans, ok := out.(versions.VersionsAnswer)
|
||||
if err != nil || !ok || ans.Reserved == nil || ans.Reserved.Module != "mesh-sdk" || !ans.Reserved.Released {
|
||||
t.Fatalf("builder_versions by name = %+v (%v), want reserved's answer in it", out, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(called) != 1 || called[0] != "@novox/mesh-sdk@0.1.2" {
|
||||
t.Errorf("reserved was called %v, want once, by the name and version", called)
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
|
||||
"mesh-delivery-builder/internal/build"
|
||||
)
|
||||
|
||||
@@ -106,23 +102,3 @@ func follow(ctx context.Context, w jetstream.KeyWatcher, each func(jetstream.Key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// manifestReader reads manifests.<digest> of the indexer and parses the manifest it holds.
|
||||
type manifestReader struct{ kv jetstream.KeyValue }
|
||||
|
||||
func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue.Manifest, error) {
|
||||
key, err := meshcatalogclient.ManifestsKey(digest)
|
||||
if err != nil {
|
||||
return catalogue.Manifest{}, err
|
||||
}
|
||||
e, err := m.kv.Get(ctx, key)
|
||||
if err != nil {
|
||||
return catalogue.Manifest{}, fmt.Errorf("manifests.%s: %w", key, err)
|
||||
}
|
||||
kept, err := meshcatalogclient.DecodeManifest(e.Value())
|
||||
if err != nil {
|
||||
return catalogue.Manifest{}, err
|
||||
}
|
||||
// Decoded, never judged (novox/hq ADR 0578 §3): what a build needs of it is read, a field it does not know set aside.
|
||||
return catalogue.Decode([]byte(kept.Manifest))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
"mesh-delivery-builder/internal/build"
|
||||
)
|
||||
|
||||
// **The builder reads the catalog half per watched branch** (design 47 G §348, §479, §614; A-5, A-15): the shared branch follower
|
||||
// of meshcatalogclient, adopted on the builder's views reader, gives each watched branch's entries; the manifests, the packages
|
||||
// and each package's name in its ecosystem are read from the entries, never the indexer's states.
|
||||
|
||||
// entryManifests keeps the manifest of each entry heard, by the entry's manifest digest (the entry holds the manifest whole).
|
||||
type entryManifests struct {
|
||||
mu sync.Mutex
|
||||
byFp map[string]catalogue.Manifest
|
||||
}
|
||||
|
||||
// keep takes the manifest of e in; an entry with no manifest or digest keeps none.
|
||||
func (m *entryManifests) keep(e meshcatalog.CatalogEntry) error {
|
||||
if e.ManifestDigest == "" || len(e.Manifest) == 0 {
|
||||
return nil
|
||||
}
|
||||
raw, err := json.Marshal(e.Manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Decoded, never judged (novox/hq ADR 0578 §3): what a build needs of it is read, a field it does not know set aside.
|
||||
mf, err := catalogue.Decode(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the manifest of the entry of %s does not decode: %w", e.Module, err)
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.byFp == nil {
|
||||
m.byFp = map[string]catalogue.Manifest{}
|
||||
}
|
||||
m.byFp[e.ManifestDigest] = mf
|
||||
return nil
|
||||
}
|
||||
|
||||
// Manifest is the manifest the entry heard with that digest holds.
|
||||
func (m *entryManifests) Manifest(_ context.Context, digest string) (catalogue.Manifest, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
mf, ok := m.byFp[digest]
|
||||
if !ok {
|
||||
return catalogue.Manifest{}, fmt.Errorf("no entry of the catalog half holds the manifest %s", digest)
|
||||
}
|
||||
return mf, nil
|
||||
}
|
||||
|
||||
// catalogHalf is the builder's part over the branch follower: what changes in a branch's half goes to the builder, and the
|
||||
// entries of every followed half are what the package names are read from.
|
||||
type catalogHalf struct {
|
||||
branches *meshcatalogclient.Branches
|
||||
b *build.Builder
|
||||
manifests *entryManifests
|
||||
logf func(string, ...any)
|
||||
// named is, by module, the package names of its first entry last heard, so a name newly heard looks at its module again.
|
||||
mu sync.Mutex
|
||||
named map[string]string
|
||||
// ready is told when every followed branch's first view is in, once; halves are the views adopted (sweepWiring).
|
||||
ready func()
|
||||
halves map[string]*views.Follower
|
||||
once sync.Once
|
||||
ctx context.Context
|
||||
// holders is told each entry change: a claim of a build seat may have moved (design 47 G A-82).
|
||||
holders func()
|
||||
}
|
||||
|
||||
// first says whether branch is the first watched branch of module's registration (design 47 G §371); a module with no
|
||||
// registration heard is taken as watched on this branch, the first one.
|
||||
func (c *catalogHalf) first(branch, module string) bool {
|
||||
reg, ok := c.branches.Registrations.Registration(module)
|
||||
return !ok || len(reg.Branches) == 0 || reg.Branches[0] == branch
|
||||
}
|
||||
|
||||
// Changed is the follower's Changed: one module's entry of a branch came, or went.
|
||||
func (c *catalogHalf) Changed(ctx context.Context) func(branch, module string, entry json.RawMessage, deleted bool, at time.Time) {
|
||||
return func(branch, module string, entry json.RawMessage, deleted bool, _ time.Time) {
|
||||
if c.holders != nil {
|
||||
defer c.holders()
|
||||
}
|
||||
first := c.first(branch, module)
|
||||
if deleted {
|
||||
c.b.EntryGone(branch, first, module)
|
||||
return
|
||||
}
|
||||
origin := build.Origin{Branch: branch, First: first}
|
||||
if cat, ok := c.branches.Catalog(branch); ok {
|
||||
if half, ok := cat.View(); ok {
|
||||
origin.Source = half.Revision.Source
|
||||
}
|
||||
}
|
||||
var e meshcatalog.CatalogEntry
|
||||
if err := json.Unmarshal(entry, &e); err != nil || e.Module != module {
|
||||
c.logf("the catalog half of the branch %q holds what is not the entry of %s, left: %v", branch, module, err)
|
||||
return
|
||||
}
|
||||
if err := c.manifests.keep(e); err != nil {
|
||||
c.logf("%v", err)
|
||||
}
|
||||
c.b.Entry(ctx, origin, e)
|
||||
c.mu.Lock()
|
||||
was := c.named[module]
|
||||
now := namesOf(e)
|
||||
if c.named == nil {
|
||||
c.named = map[string]string{}
|
||||
}
|
||||
c.named[module] = now
|
||||
c.mu.Unlock()
|
||||
if first && now != "" && now != was {
|
||||
c.logf("the catalog half's entry names a package of %s: its build is looked at again", module)
|
||||
c.b.PackageNamed(ctx, module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// namesOf are the package names an entry's artifacts carry, in one line.
|
||||
func namesOf(e meshcatalog.CatalogEntry) string {
|
||||
var names []string
|
||||
for _, a := range e.Artifacts {
|
||||
if a.Package != nil && a.Package.Name != "" {
|
||||
names = append(names, a.Ref+"="+a.Package.Name)
|
||||
}
|
||||
}
|
||||
slices.Sort(names)
|
||||
return fmt.Sprint(names)
|
||||
}
|
||||
|
||||
// sweepWiring is the follower's wiring on the builder's reader that learns each catalog half view adopted, so ready is told once
|
||||
// when the first view of every branch followed is in (a branch with no revision yet is ready and empty at once).
|
||||
type sweepWiring struct {
|
||||
meshcatalogclient.Wiring
|
||||
half *catalogHalf
|
||||
}
|
||||
|
||||
func (w sweepWiring) Adopt(f *views.Follower, seatName, event string, hear func(context.Context, []byte) error) error {
|
||||
if err := w.Wiring.Adopt(f, seatName, event, hear); err != nil {
|
||||
return err
|
||||
}
|
||||
if seatName == meshcatalog.Seat && event == meshcatalog.EventCatalogRevision {
|
||||
w.half.adopted(f)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w sweepWiring) Drop(owner, state string) error {
|
||||
err := w.Wiring.Drop(owner, state)
|
||||
w.half.dropped(state)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *catalogHalf) adopted(f *views.Follower) {
|
||||
c.mu.Lock()
|
||||
if c.halves == nil {
|
||||
c.halves = map[string]*views.Follower{}
|
||||
}
|
||||
c.halves[f.State] = f
|
||||
c.mu.Unlock()
|
||||
go func() {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
case <-f.Ready():
|
||||
c.checkReady()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *catalogHalf) dropped(state string) {
|
||||
c.mu.Lock()
|
||||
delete(c.halves, state)
|
||||
c.mu.Unlock()
|
||||
c.checkReady()
|
||||
}
|
||||
|
||||
// checkReady calls ready once every adopted half's first view is in.
|
||||
func (c *catalogHalf) checkReady() {
|
||||
c.mu.Lock()
|
||||
all := len(c.halves) > 0
|
||||
for _, f := range c.halves {
|
||||
select {
|
||||
case <-f.Ready():
|
||||
default:
|
||||
all = false
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if all && c.ready != nil {
|
||||
c.once.Do(c.ready)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
)
|
||||
|
||||
// TestTheManifestOfAnEntryIsReadByItsDigest: the builder reads a manifest from the entry that holds it, by the entry's manifest
|
||||
// digest (design 47 G §348), and an entry with no manifest keeps none.
|
||||
func TestTheManifestOfAnEntryIsReadByItsDigest(t *testing.T) {
|
||||
m := &entryManifests{}
|
||||
e := meshcatalog.CatalogEntry{Module: "netcheck", ManifestDigest: "sha256:aa", Manifest: map[string]any{"module": "netcheck", "version": "1"}}
|
||||
if err := m.keep(e); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mf, err := m.Manifest(context.Background(), "sha256:aa"); err != nil || mf.Module != "netcheck" {
|
||||
t.Errorf("Manifest(sha256:aa) = %+v, %v", mf, err)
|
||||
}
|
||||
if _, err := m.Manifest(context.Background(), "sha256:bb"); err == nil {
|
||||
t.Error("Manifest of a digest no entry holds: want an error")
|
||||
}
|
||||
if err := m.keep(meshcatalog.CatalogEntry{Module: "bare"}); err != nil || len(m.byFp) != 1 {
|
||||
t.Errorf("an entry with no manifest kept one: %v, %v", err, m.byFp)
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
|
||||
)
|
||||
|
||||
// catalogHolder is the holder of the seat mesh-catalog as the bed needs it (design 47 G §348, §614): it answers `revision` for the
|
||||
// branch main with the entries put so far, `registrations` with none (so the follower follows the branches the gate's `source`
|
||||
// answers) and `entry`, and says `catalog-revision` at each put, as the follower and the builder hear it.
|
||||
type catalogHolder struct {
|
||||
ctx context.Context
|
||||
js jetstream.JetStream
|
||||
|
||||
mu sync.Mutex
|
||||
entries map[string]meshcatalog.CatalogEntry
|
||||
revision int64
|
||||
}
|
||||
|
||||
func newCatalogHolder(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) *catalogHolder {
|
||||
t.Helper()
|
||||
h := &catalogHolder{ctx: ctx, js: js, entries: map[string]meshcatalog.CatalogEntry{}}
|
||||
answer := func(subject string, with func(in []byte) any) {
|
||||
s, err := nc.Subscribe(subject, func(m *nats.Msg) {
|
||||
reply, _ := json.Marshal(map[string]any{"result": with(m.Data), "node": "ctl"})
|
||||
_ = m.Respond(reply)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = s.Unsubscribe() })
|
||||
}
|
||||
answer("mesh.seat."+meshcatalog.Seat+".call."+meshcatalog.VerbRevision+".>", func([]byte) any { return h.whole() })
|
||||
answer("mesh.seat."+meshcatalog.Seat+".call."+meshcatalog.VerbRegistrations+".>", func([]byte) any {
|
||||
return meshcatalog.RegistrationsAnswer{Registrations: []meshcatalog.Registration{}}
|
||||
})
|
||||
answer("mesh.seat."+meshcatalog.Seat+".call."+meshcatalog.VerbEntry+".>", func(in []byte) any {
|
||||
var q meshcatalog.EntryInput
|
||||
_ = json.Unmarshal(in, &q)
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for _, e := range h.entries {
|
||||
if e.Fp == q.Fingerprint {
|
||||
return meshcatalog.EntryAnswer{Entry: e}
|
||||
}
|
||||
}
|
||||
return map[string]any{"error": "no entry of that fingerprint"}
|
||||
})
|
||||
answer("mesh.seat."+meshdelivery.Seat+".call."+meshdelivery.VerbSource+".>", func([]byte) any {
|
||||
return meshdelivery.SourceAnswer{Branches: []string{"main"}}
|
||||
})
|
||||
return h
|
||||
}
|
||||
|
||||
// whole is the newest trunk revision of main, every entry in one page; the branch has none yet before the first put.
|
||||
func (h *catalogHolder) whole() meshcatalog.RevisionAnswer {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.revision == 0 {
|
||||
return meshcatalog.RevisionAnswer{NoneYet: true}
|
||||
}
|
||||
var refs []meshcatalog.EntryRef
|
||||
var all []meshcatalog.CatalogEntry
|
||||
for m, e := range h.entries {
|
||||
refs = append(refs, meshcatalog.EntryRef{Module: m, Digest: "sha256:" + e.Fp})
|
||||
all = append(all, e)
|
||||
}
|
||||
at := time.Now()
|
||||
return meshcatalog.RevisionAnswer{Revision: &meshcatalog.CatalogRevision{Revision: h.revision, Kind: "trunk", Branch: "main", Entries: refs, Source: 1, At: &at}, Entries: all}
|
||||
}
|
||||
|
||||
// put sets one module's entry in a new trunk revision of main and says catalog-revision.
|
||||
func (h *catalogHolder) put(t *testing.T, e meshcatalog.CatalogEntry) { h.putAll(t, e) }
|
||||
|
||||
// putAll sets several modules' entries in one new trunk revision of main.
|
||||
func (h *catalogHolder) putAll(t *testing.T, es ...meshcatalog.CatalogEntry) {
|
||||
t.Helper()
|
||||
h.mu.Lock()
|
||||
h.revision++
|
||||
var changed []string
|
||||
for _, e := range es {
|
||||
h.entries[e.Module] = e
|
||||
changed = append(changed, e.Module)
|
||||
}
|
||||
ev, _ := json.Marshal(meshcatalog.CatalogRevisionEvent{Branch: "main", Kind: "trunk", Revision: h.revision, Source: 1, Entries: int64(len(h.entries)), Changed: changed, V: 1})
|
||||
h.mu.Unlock()
|
||||
if _, err := h.js.Publish(h.ctx, meshcatalog.SubjectCatalogRevision("main"), ev); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// buildSeats is the seat list and the catalog half as the builder finds its build modules from them (design 47 G A-82): the seat
|
||||
// mesh-catalog's verb seat-definitions answers the three build seats' definitions, each acting on its language (the image seat on
|
||||
// kind:image), and an entry of each holder's manifest claims its seat.
|
||||
func (h *catalogHolder) buildSeats(t *testing.T, nc *nats.Conn, holders map[string]string) {
|
||||
t.Helper()
|
||||
defs := map[string]seatdef.Definition{}
|
||||
for _, d := range seatlisttest.Definitions() {
|
||||
if _, ok := holders[d.Seat]; ok {
|
||||
defs[d.Seat] = d
|
||||
}
|
||||
}
|
||||
for seat, on := range map[string]string{"node-build-go": "go", "node-build-node": "typescript", "node-build-image": seatdef.OnKindImage} {
|
||||
d := defs[seat]
|
||||
d.Acts = []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}
|
||||
defs[seat] = d
|
||||
}
|
||||
s, err := nc.Subscribe("mesh.seat."+meshcatalog.Seat+".call."+seatlist.Verb+".>", func(m *nats.Msg) {
|
||||
var in struct {
|
||||
Seat string `json:"seat"`
|
||||
}
|
||||
_ = json.Unmarshal(m.Data, &in)
|
||||
var result any
|
||||
if in.Seat == "" {
|
||||
l := seatlist.Listing{Revision: 1}
|
||||
for seat := range defs {
|
||||
l.Seats = append(l.Seats, seatlist.Entry{Seat: seat, Source: seatlist.SourceTrunk, Digest: "sha256:" + seat})
|
||||
}
|
||||
result = l
|
||||
} else {
|
||||
raw, _ := json.Marshal(defs[in.Seat])
|
||||
result = seatlist.Answer{Seat: in.Seat, Found: true, Revision: 1, Entry: &seatlist.Entry{Seat: in.Seat, Source: seatlist.SourceTrunk, Digest: "sha256:" + in.Seat, Definition: raw}}
|
||||
}
|
||||
reply, _ := json.Marshal(map[string]any{"result": result, "node": "ctl"})
|
||||
_ = m.Respond(reply)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = s.Unsubscribe() })
|
||||
var entries []meshcatalog.CatalogEntry
|
||||
for seat, module := range holders {
|
||||
entries = append(entries, meshcatalog.CatalogEntry{Module: module, Repository: "novox/mesh-catalog", Commit: "c0", Fp: "src2:" + strings.Repeat(fmt.Sprintf("%x", 10+len(seat)%6), 64), ManifestDigest: "sha256:" + fmt.Sprintf("%064x", len(module)*7919+int(module[len(module)-1])),
|
||||
Manifest: map[string]any{"module": module, "claims": []any{map[string]any{"name": seat}}}})
|
||||
}
|
||||
h.putAll(t, entries...)
|
||||
}
|
||||
@@ -19,7 +19,10 @@ import (
|
||||
// heldOnTheBus says whether an ask a build key names is on its build seat's stream, or a build node wrote a step for it
|
||||
// (issue 1910), read once at the take-over of a key at start; and, at the builder's check bound, whether an ask can still
|
||||
// be answered (issue 1945). Never polled.
|
||||
type heldOnTheBus struct{ js jetstream.JetStream }
|
||||
type heldOnTheBus struct {
|
||||
js jetstream.JetStream
|
||||
seats *buildrun.BuildSeats // the build modules (design 47 G A-82)
|
||||
}
|
||||
|
||||
// Held is build.Builder's Held: a step key `<node>.<ask id>` in the build module's steps, or a message of the stream under
|
||||
// the ask's subject whose Nats-Msg-Id is the ask's id.
|
||||
@@ -51,10 +54,11 @@ const (
|
||||
// the ask is on its stream.
|
||||
func (h heldOnTheBus) seen(ctx context.Context, a buildstep.StepAsk, later bool) (build.AskSeen, error) {
|
||||
seen := build.AskSeen{Kind: a.Kind, At: build.AskedAt(a.ID)}
|
||||
module, err := buildstep.BuildModuleOf(a.Language)
|
||||
bm, err := h.seats.OfAskLanguage(a.Language)
|
||||
if err != nil {
|
||||
return seen, err
|
||||
}
|
||||
module := bm.Module
|
||||
steps, err := h.js.KeyValue(ctx, buildrun.StepsBucket(module))
|
||||
if err != nil {
|
||||
return seen, fmt.Errorf("the bucket %s: %w", buildrun.StepsBucket(module), err)
|
||||
@@ -259,10 +263,11 @@ func (h heldOnTheBus) Drop(ctx context.Context, a buildstep.StepAsk) error {
|
||||
// every kind, oldest first and at most limit of each kind, each with its step's state and whether it still waits.
|
||||
func (h heldOnTheBus) Queue(ctx context.Context, language string, limit int) (build.SeatQueue, error) {
|
||||
q := build.SeatQueue{Language: language, Note: build.QueueNote}
|
||||
module, err := buildstep.BuildModuleOf(language)
|
||||
bm, err := h.seats.OfAskLanguage(language)
|
||||
if err != nil {
|
||||
return q, err
|
||||
}
|
||||
module := bm.Module
|
||||
stream, name, _, err := h.stream(ctx, language, buildstep.AskDelivery)
|
||||
q.Stream = name
|
||||
if errors.Is(err, jetstream.ErrStreamNotFound) {
|
||||
|
||||
@@ -28,15 +28,12 @@ import (
|
||||
runner "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
|
||||
|
||||
@@ -201,38 +198,22 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, nodes ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// trunkHead is the trunk head of novox/mesh-catalog the bed's source revision names: newer than the module values' commit.
|
||||
// trunkHead is a trunk head of novox/mesh-catalog.
|
||||
const trunkHead = "3333333333333333333333333333333333333333"
|
||||
|
||||
// trunk writes the gate's source.main naming head as novox/mesh-catalog's trunk head, and answers its revision.
|
||||
func (b *bed) trunk(t *testing.T, head string) uint64 {
|
||||
t.Helper()
|
||||
data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByBranchValue{V: keys.Version, Group: "g", Heads: map[string]string{"novox/mesh-catalog": head}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rev, err := b.source.Put(b.ctx, meshdeliveryclient.SourceKey("main"), data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return rev
|
||||
}
|
||||
|
||||
type bed struct {
|
||||
ctx context.Context
|
||||
js jetstream.JetStream
|
||||
modules jetstream.KeyValue
|
||||
candidates jetstream.KeyValue
|
||||
source jetstream.KeyValue
|
||||
builds jetstream.KeyValue
|
||||
heard jetstream.KeyValue // the state toolchains-heard
|
||||
instances jetstream.KeyValue
|
||||
packages jetstream.KeyValue // the indexer's packages, served by its read tool and views family
|
||||
out *saidLog // the builder's process log, every instance's
|
||||
stop func() // stops the builder started last
|
||||
fake *fakeBuildModules
|
||||
srvURL string
|
||||
dir string
|
||||
ctx context.Context
|
||||
js jetstream.JetStream
|
||||
catalog *catalogHolder // the seat mesh-catalog, as its holder answers the branch follower
|
||||
source jetstream.KeyValue
|
||||
builds jetstream.KeyValue
|
||||
heard jetstream.KeyValue // the state toolchains-heard
|
||||
instances jetstream.KeyValue
|
||||
out *saidLog // the builder's process log, every instance's
|
||||
stop func() // stops the builder started last
|
||||
fake *fakeBuildModules
|
||||
srvURL string
|
||||
dir string
|
||||
}
|
||||
|
||||
// startBed starts the bus, the buckets the controller makes from the builder's and the build modules' manifests, the
|
||||
@@ -300,17 +281,13 @@ func startBedWith(t *testing.T, closed bool) *bed {
|
||||
}
|
||||
return kv
|
||||
}
|
||||
b.modules = mk(indexerBucket(modulesState), modulesHistory)
|
||||
b.candidates = mk(indexerBucket(candidatesState), candidatesHistory)
|
||||
b.source = mk(sourceBucket, sourceHistory)
|
||||
mk(indexerBucket(manifestsState), manifestsHistory)
|
||||
// The indexer and the assigner as the owners of the states the builder follows: their read tools and views families
|
||||
// (ADR 0615, DLV-M1); n1 and n2 each have their durable consumer.
|
||||
// The seat mesh-catalog's holder and the assigner as the owners of what the builder follows: the catalog half through the seat's
|
||||
// verbs and events (design 47 G §348, §614), the assignments by their read tool and views family (ADR 0615, DLV-M1); n1 and n2
|
||||
// each have their durable consumer.
|
||||
b.catalog = newCatalogHolder(t, ctx, js, ctl)
|
||||
b.catalog.buildSeats(t, ctl, map[string]string{"node-build-go": testGo, "node-build-node": testNode, "node-build-image": testImage,
|
||||
"git": "mesh-delivery-forge-bridge", "mesh-catalog": "mesh-delivery-indexer"}) // the floor's sources are the holders of git and mesh-catalog
|
||||
readers := []string{"n1_" + builds.BuilderModule, "n2_" + builds.BuilderModule}
|
||||
for _, st := range []string{modulesState, manifestsState, candidatesState} {
|
||||
viewstest.Serve(t, ctx, js, ctl, indexerModule, st, indexerModule+".indexer_read_"+st, readers...)
|
||||
}
|
||||
b.packages = viewstest.Serve(t, ctx, js, ctl, indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages", readers...)
|
||||
viewstest.Serve(t, ctx, js, ctl, assignerModule, "assignments", assignerModule+".assigner_read_assignments", readers...)
|
||||
b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory)
|
||||
b.instances = mk(instancesBucket, 1)
|
||||
@@ -404,28 +381,34 @@ func (b *bed) moduleFP(t *testing.T, name, fixture, path string, toolchains map[
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := meshcatalogclient.ManifestDigest(raw)
|
||||
manifests, err := b.js.KeyValue(b.ctx, indexerBucket(manifestsState))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mv, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: name, Manifest: string(raw)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := manifests.Put(b.ctx, digest, mv); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: at, Fp: "src1:" + strings.Repeat("f", 64), Fp2: fp2,
|
||||
Manifest: digest, Path: path, Toolchains: build.CatalogToolchains(toolchains), Src: int64(src)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := b.modules.Put(b.ctx, name, data); err != nil {
|
||||
var manifest map[string]any
|
||||
if err := json.Unmarshal(raw, &manifest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.catalog.put(t, b.entry(name, path, at, fp2, digest, manifest, toolchains, src))
|
||||
return digest
|
||||
}
|
||||
|
||||
// manifestOf is a fixture manifest as the entry holds it.
|
||||
func (b *bed) manifestOf(t *testing.T, fixture string) map[string]any {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(filepath.Join("testdata", fixture))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// entry is the catalog graph entry of a module as the holder answers it.
|
||||
func (b *bed) entry(name, path, at, fp2, digest string, manifest map[string]any, toolchains map[string]builds.ToolchainUsed, src uint64) meshcatalog.CatalogEntry {
|
||||
return meshcatalog.CatalogEntry{Module: name, Repository: "novox/mesh-catalog", Commit: at, Fp: fp2, Path: path, ManifestDigest: digest, Manifest: manifest,
|
||||
Inputs: &meshcatalog.EntryInputs{Toolchains: build.CatalogToolchains(toolchains), Src: int64(src)}}
|
||||
}
|
||||
|
||||
// archive writes modules.<name> as the real indexer does for a module of an archive only: its fingerprint names no
|
||||
// toolchain (mesh-delivery-indexer internal/index inputs.go languagesOf gives an archive no language), so Toolchains is empty.
|
||||
func (b *bed) archive(t *testing.T, name, path, at string) string {
|
||||
@@ -459,6 +442,12 @@ type saidLog struct {
|
||||
buf strings.Builder
|
||||
}
|
||||
|
||||
func (l *saidLog) String() string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.buf.String()
|
||||
}
|
||||
|
||||
func (l *saidLog) Write(p []byte) (int, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
@@ -588,11 +577,12 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7
|
||||
t.Errorf("%s called %d times, want %d (on one node, ADR 0478 clause 4)", tool, got, want)
|
||||
}
|
||||
}
|
||||
// Design 32, ADR 0615 §1: a package's name the indexer writes later reaches the builder by its packages' views family
|
||||
// (never their bucket), and looks at the module again; the done bundles are kept, not numbered twice.
|
||||
if _, err := b.packages.Put(b.ctx, "netcheck.0.1", []byte(`{"v":1,"name":"netcheck","package":"netcheck/lib","series":"0.1"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Design 32, design 47 G §348: a package's name the catalog half's entry of the module gives later reaches the builder by the
|
||||
// branch follower's view (never the indexer's state), and looks at the module again; the done bundles are kept, not numbered twice.
|
||||
named := b.entry("netcheck", "modules/netcheck", commit, fp, digest, nil, bothToolchains, 0)
|
||||
named.Manifest = b.manifestOf(t, "catalogue-a67f0196-netcheck.module.json")
|
||||
named.Artifacts = []meshcatalog.Artifact{{Ref: "netcheck:lib", Language: "go", Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: "netcheck"}}}
|
||||
b.catalog.put(t, named)
|
||||
b.awaitSaid(t, "names a package of netcheck")
|
||||
// Design 47 G §349: version.numbered is no longer said, for a number or for a late name.
|
||||
if got := reg.numberedOf("netcheck/tools-go"); len(got) != 0 {
|
||||
@@ -647,7 +637,7 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7
|
||||
reg.awaitFinding(t, failed, true)
|
||||
b.fake.hang.Store(nil)
|
||||
close(release)
|
||||
b.moduleFP(t, "netcheck", netcheck, "modules/netcheck", bothToolchains, commit, third, 3) // the indexer writes it again
|
||||
b.moduleFP(t, "netcheck", netcheck, "modules/netcheck", bothToolchains, "89abcdef0123456789abcdef0123456789abcdef", third, 3) // the entry moves to a later commit, same fingerprint
|
||||
b.awaitFP(t, "netcheck", third, isState(builds.BuildDone))
|
||||
b.awaitSaid(t, "kept: fingerprint "+third+" is numbered under it")
|
||||
if !slices.ContainsFunc(b.logged(t, "netcheck"), func(e builds.BuildLogEntry) bool {
|
||||
@@ -747,7 +737,7 @@ func TestAToolchainChangeDoesNotBuildTheOldFingerprint(t *testing.T) { // 1615/A
|
||||
map[string]builds.ToolchainUsed{"go": {Version: "go1.27.2", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}}, commit)
|
||||
b.await(t, "netcheck2", isState(builds.BuildDone))
|
||||
if _, err := b.builds.Get(b.ctx, mustKey(t, "netcheck")); err == nil {
|
||||
t.Error("the old fingerprint, naming go1.27.1, was built with go1.27.2")
|
||||
t.Error("the old fingerprint, naming go1.27.1, was built with go1.27.2; the log: " + b.out.String())
|
||||
}
|
||||
if n := b.fake.count("mesh-build-go.build_go_build"); n != 1 {
|
||||
t.Errorf("build_go_build was called %d times, want 1 (the new fingerprint on one node only)", n)
|
||||
@@ -786,77 +776,14 @@ func TestAKeyLeftBuildingForAModuleThatIsGoneIsClosedAtStart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuilderBuildsACandidatesFingerprintFromTheHeadMergedOntoTheTrunk: the trunk's record of netcheck carries a fingerprint of its
|
||||
// own; a candidate the indexer wrote gives netcheck the fingerprint fp at a pull request's head; the program writes
|
||||
// builds.netcheck.<fp> done, its fetch given the head, the ref that carries it and the trunk head of the candidate's source
|
||||
// revision, newer than the module value's commit (issue 1684; the operator 2026-10-25: a value names the commit its
|
||||
// fingerprint first appeared at).
|
||||
func TestBuilderBuildsACandidatesFingerprintFromTheHeadMergedOntoTheTrunk(t *testing.T) { // N7/AC1
|
||||
// TestAnImageIsBuiltOnOneNodeAndEveryFingerprintOfTheCatalogHalfPublishes: the catalog half's entry of imaged (one image
|
||||
// artifact) is built on n1, where mesh-build-image runs: its key is done with the digest and the reference, its build call carrying
|
||||
// publish; the entry's next fingerprint is built again with publish and ends with a reference of its own (ADR 0487 clauses 5 and
|
||||
// 12; a run's candidate is no input of the builder: design 47 G A-84).
|
||||
func TestAnImageIsBuiltOnOneNodeAndEveryFingerprintOfTheCatalogHalfPublishes(t *testing.T) { // 1705
|
||||
b := startBed(t)
|
||||
other := "src2:" + strings.Repeat("e", 64)
|
||||
digest := b.module(t, "netcheck", "catalogue-a67f0196-netcheck.module.json", "modules/netcheck")
|
||||
data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: commit, Fp: "src1:" + strings.Repeat("f", 64), Fp2: other,
|
||||
Manifest: digest, Path: "modules/netcheck", Toolchains: build.CatalogToolchains(map[string]builds.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}})})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := b.modules.Put(b.ctx, "netcheck", data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const head = "1111111111111111111111111111111111111111"
|
||||
pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: head, Modules: []string{"netcheck"},
|
||||
Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"netcheck": {Fp2: fp, Manifest: digest}}}}
|
||||
hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}})
|
||||
cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Base: int64(b.trunk(t, trunkHead)), Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ckey, err := keys.EscapeName("feat/x"), error(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := b.candidates.Put(b.ctx, ckey, cand); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.start(t, "n1", nil)
|
||||
v := b.await(t, "netcheck", isState(builds.BuildDone))
|
||||
if len(v.Artifacts) != 2 {
|
||||
t.Fatalf("build = %+v", v)
|
||||
}
|
||||
b.fake.mu.Lock()
|
||||
defer b.fake.mu.Unlock()
|
||||
found := false
|
||||
for _, f := range b.fake.fetches {
|
||||
if f["commit"] == head && f["ref"] == "refs/pull/7/head" && f["base"] == trunkHead {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("fetches = %v, want one of the candidate's head with its ref and the trunk head it was previewed onto, not the module value's commit", b.fake.fetches)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes: modules.imaged (one image artifact) is built on n1, where
|
||||
// mesh-build-image runs: a trunk record's key is done with the digest and the reference, its build call carrying publish; a
|
||||
// candidate's key for the same module is built first with no publish and no reference; when the pull request is merged and the
|
||||
// trunk's record names the candidate's fingerprint, the key is built again with publish and ends with a reference
|
||||
// (ADR 0487 clauses 5 and 12).
|
||||
func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 1705
|
||||
b := startBed(t)
|
||||
other := "src2:" + strings.Repeat("e", 64)
|
||||
digest := b.moduleCommit(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit)
|
||||
// The trunk carries `other`; its key is built and published.
|
||||
trunk := func(fp2 string) {
|
||||
data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: commit, Fp: "src1:" + strings.Repeat("f", 64), Fp2: fp2,
|
||||
Manifest: digest, Path: "modules/imaged"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := b.modules.Put(b.ctx, "imaged", data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
trunk(other)
|
||||
digest := b.moduleFP(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit, other, 0)
|
||||
b.start(t, "n1", nil)
|
||||
okey, _ := builds.BuilderBuildKey("imaged", other)
|
||||
awaitKey := func(key string, pred func(builds.BuilderBuild) bool) builds.BuilderBuild {
|
||||
@@ -868,42 +795,23 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatalf("builds.%s = %+v within 20 s", key, last)
|
||||
t.Fatalf("builds.%s = %+v within 20 s; the log: %s", key, last, b.out.String())
|
||||
return last
|
||||
}
|
||||
v := awaitKey(okey, isState(builds.BuildDone))
|
||||
if len(v.Artifacts) != 1 || v.Artifacts[0].Digest != digestOf("server") || !strings.HasSuffix(v.Artifacts[0].Reference, "@"+digestOf("server")) {
|
||||
t.Fatalf("trunk build = %+v, want the digest and the reference", v)
|
||||
t.Fatalf("build = %+v, want the digest and the reference", v)
|
||||
}
|
||||
// The candidate carries fp: built, not published.
|
||||
const head = "1111111111111111111111111111111111111111"
|
||||
pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: head, Modules: []string{"imaged"},
|
||||
Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"imaged": {Fp2: fp, Manifest: digest}}}}
|
||||
hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}})
|
||||
cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Base: int64(b.trunk(t, trunkHead)), Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
// The module's next fingerprint: built once more, published.
|
||||
if got := b.moduleFP(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit, fp, 0); got != digest {
|
||||
t.Fatalf("manifest digest = %s, want %s", got, digest)
|
||||
}
|
||||
ckey := keys.EscapeName("feat/x")
|
||||
if _, err := b.candidates.Put(b.ctx, ckey, cand); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v = b.await(t, "imaged", isState(builds.BuildDone))
|
||||
if len(v.Artifacts) != 1 || v.Artifacts[0].Reference != "" {
|
||||
t.Fatalf("candidate build = %+v, want done with no reference", v)
|
||||
}
|
||||
b.fake.mu.Lock()
|
||||
if len(b.fake.builds) != 2 || b.fake.builds[0]["publish"] != true || b.fake.builds[1]["publish"] != nil {
|
||||
t.Errorf("builds = %v, want the trunk's with publish, then the candidate's with none", b.fake.builds)
|
||||
}
|
||||
b.fake.mu.Unlock()
|
||||
// The pull request is merged: the trunk's record names fp, and its key ends published, built once more with publish.
|
||||
trunk(fp)
|
||||
v = b.await(t, "imaged", func(v builds.BuilderBuild) bool {
|
||||
fkey, _ := builds.BuilderBuildKey("imaged", fp)
|
||||
v = awaitKey(fkey, func(v builds.BuilderBuild) bool {
|
||||
return v.State == builds.BuildDone && len(v.Artifacts) == 1 && v.Artifacts[0].Reference != ""
|
||||
})
|
||||
if v.Artifacts[0].Digest != digestOf("server") {
|
||||
t.Errorf("merged build = %+v", v)
|
||||
t.Errorf("second build = %+v", v)
|
||||
}
|
||||
b.fake.mu.Lock()
|
||||
defer b.fake.mu.Unlock()
|
||||
@@ -913,8 +821,8 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170
|
||||
published++
|
||||
}
|
||||
}
|
||||
if len(b.fake.builds) != 3 || published != 2 {
|
||||
t.Errorf("builds = %v, want three, two of them with publish (the trunk's two fingerprints)", b.fake.builds)
|
||||
if len(b.fake.builds) != 2 || published != 2 {
|
||||
t.Errorf("builds = %v, want two, both with publish", b.fake.builds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1085,7 +993,7 @@ func TestARestartedBuilderPutsNoSecondAsk(t *testing.T) {
|
||||
}
|
||||
// 1945 note 62, on the real bus: the stream read as builder_seat_queue reads it shows the Go ask waiting; the ask can
|
||||
// still be answered (nothing was taken since); and a message dropped as an ask asked again is gone from the stream.
|
||||
held := heldOnTheBus{b.js}
|
||||
held := heldOnTheBus{js: b.js, seats: testSeats()}
|
||||
goAsk := sdkstep.StepAsk{ID: v.Asks[sdkstep.AskGo], Language: sdkstep.AskGo, Kind: sdkstep.AskDelivery}
|
||||
stale := sdkstep.StepAsk{ID: "mvo53jcm70b5f7", Language: sdkstep.AskGo, Kind: sdkstep.AskDelivery}
|
||||
subject, _ := sdkstep.AskSubjectBy(sdkstep.AskGo, sdkstep.AskDelivery, sdkstep.AskerBuilder)
|
||||
|
||||
@@ -12,13 +12,10 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"maps"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -34,14 +31,13 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/trunk"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"mesh-delivery-builder/internal/build"
|
||||
@@ -82,10 +78,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The indexer's modules, manifests, candidates and packages and the assigner's assignments are their owners' states, read by the
|
||||
// first view of their read tools and then their views families, never their buckets (ADR 0615 §1, DLV-M1). An owner that
|
||||
// is down never holds the start: each is read on a goroutine once its first view is answered, and until then the
|
||||
// unanswered first view is the builder's finding.
|
||||
// The assigner's assignments are its state, read by the first view of its read tool and then its views family, never its
|
||||
// bucket (ADR 0615 §1, DLV-M1); the catalog half is read through the seat mesh-catalog. An owner that is down never holds the
|
||||
// start: each is read on a goroutine once its first view is answered, and until then the unanswered first view is the
|
||||
// builder's finding.
|
||||
calls, err := process.Connect(cred, nats.Name(cred.Node+"."+cred.Module+".calls"), nats.RetryOnFailedConnect(true))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -109,24 +105,20 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
}
|
||||
})
|
||||
reader.Findings = func() views.WireWriter { return fw }
|
||||
modules, err := reader.Follow(indexerModule, modulesState, indexerModule+".indexer_read_modules")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifests, err := reader.Follow(indexerModule, manifestsState, indexerModule+".indexer_read_manifests")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The candidates' fingerprints are built too (issue 1684).
|
||||
candidates, err := reader.Follow(indexerModule, candidatesState, indexerModule+".indexer_read_candidates")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The indexer's packages, whose names a package is numbered under (design 32).
|
||||
packages, err := reader.Follow(indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages")
|
||||
// The catalog half per watched branch (design 47 G §348, §479, §614; A-5, A-15): the shared branch follower of
|
||||
// meshcatalogclient on the builder's reader, so the modules, their manifests, their packages and each package's name in its
|
||||
// ecosystem are read from the entries, never the indexer's states.
|
||||
half := &catalogHalf{manifests: &entryManifests{}, logf: logger.Printf, ctx: ctx}
|
||||
branches, err := meshcatalogclient.NewBranches(sweepWiring{meshcatalogclient.ProcessWiring(reader), half}, calls.Call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
branches.Logf = logger.Printf
|
||||
half.branches = branches
|
||||
// The build modules are no names (design 47 G A-82): the holders of the build seats, the seat list's definitions and the
|
||||
// catalog half's claims, found at each use.
|
||||
seatGate := newSeatGate()
|
||||
seats := buildrun.NewBuildSeats(seatGate.definitions, versions.HolderOf(branches))
|
||||
assignments, err := reader.Follow(assignerModule, "assignments", assignerModule+".assigner_read_assignments")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -137,13 +129,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return fmt.Errorf("MESH_BUILDER_STEP_BOUND: %w", err)
|
||||
}
|
||||
}
|
||||
tools := &buildrun.ToolchainSet{}
|
||||
tools := &buildrun.ToolchainSet{Seats: seats}
|
||||
// Each build module's part of a build is an ask on its build seat's stream, pulled by a build node with a free slot
|
||||
// (novox/hq ADR 0525): the builder chooses no node, so it reads neither presence nor have and holds no capacity.
|
||||
// The read of an ask's steps is a kept call (design 47 G §568): a fault ends no key, and past five minutes unanswered it is a
|
||||
// finding; its bound is a key of the state bounds, bound once the bus holds it (set below, with the findings).
|
||||
stepReads := &stepsRead{bounds: func() jetstream.KeyValue { return nil }, bound: meshconditionsclient.CallUnansweredAfter, now: time.Now, logf: logger.Printf}
|
||||
asks := buildrun.JetStreamAsks{JS: js, Unanswered: stepReads.unanswered, Answered: stepReads.answered}
|
||||
// The steps of each build module are watched once the seat list and the catalog half name it (seatsChanged).
|
||||
// The asks the build seats gave up on (issue 734): heard from DEAD_LETTERS, each ends the wait for its step at once,
|
||||
// and the lane asks its part again (Builder.waitOnce) instead of waiting for the check bound.
|
||||
// Heard in the background, and again at a reconnect or an ask while it is not; whether it is heard is kept in the
|
||||
@@ -157,13 +150,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
defer hearing.Stop()
|
||||
// Each ask's step is heard by its own watch and, at every step end of its build module and every reconnect, read
|
||||
// (buildrun.HearEnds): a watch that goes deaf loses no end (the lost check of 2026-10-28).
|
||||
var buildModules []string
|
||||
for _, l := range buildstep.AskLanguages {
|
||||
if m, err := buildstep.BuildModuleOf(l); err == nil {
|
||||
buildModules = append(buildModules, m)
|
||||
}
|
||||
}
|
||||
heard := buildrun.HearEnds(ctx, asks, asks, buildModules, logger.Printf)
|
||||
heard := buildrun.HearEnds(ctx, asks, asks, seats.Modules(), logger.Printf)
|
||||
conn.SetReconnectHandler(func(*nats.Conn) { hearing.Again(); heard.Nudge("") })
|
||||
// The builder asks the controller, as its own account, whether the artifact store still holds a done key's blobs
|
||||
// (the seat verb held, novox/hq issue 1937) before it leaves the key done, on its calls connection.
|
||||
@@ -171,31 +158,39 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// the bus holds it; the floor's three sources asked as its own account.
|
||||
versionsKV := process.AwaitBucket(ctx, js, conn, versions.Bucket, "no trunk build is numbered: each is built unnumbered and published by its digest", logger.Printf)
|
||||
found := &findings{w: fw, logf: logger.Printf, now: time.Now}
|
||||
floorWaits := &versions.FloorWaits{Logf: logger.Printf, Said: found.floorSaid(context.WithoutCancel(ctx))}
|
||||
floorWaits := &versions.FloorWaits{Holder: versions.HolderOf(branches), Logf: logger.Printf, Said: found.floorSaid(context.WithoutCancel(ctx))}
|
||||
// The names waiting on their floor are kept in the state floor-waits (DLV-B4): a restarted builder takes them in, so
|
||||
// their silent sources' events still wake them; until the state is bound they are kept here only.
|
||||
process.AwaitBucket(ctx, js, conn, versions.FloorWaitsBucket, "the names waiting on their floor are kept in memory only, and builder_floor_waits shows none", logger.Printf).
|
||||
Ready(func(kv jetstream.KeyValue) {
|
||||
floorWaits.Keep(kvKeeper{ctx: ctx, kv: kv}, heldFloorWaits(ctx, kv, logger.Printf))
|
||||
})
|
||||
// A package is numbered under its name in its ecosystem, read from the indexer's packages state (design 32).
|
||||
numberer := &versions.Numberer{Versions: versions.Versions{Store: awaitedVersions{b: versionsKV}}, Floor: floorVia(calls.Call), Waits: floorWaits,
|
||||
Names: versions.NamesIn(indexerPackages{packages})}
|
||||
// A package is numbered under its name in its ecosystem, read from the catalog half's entries (design 32, design 47 G §348).
|
||||
numberer := &versions.Numberer{Versions: versions.Versions{Store: awaitedVersions{b: versionsKV}}, Floor: floorVia(calls.Call, versions.HolderOf(branches)), Waits: floorWaits,
|
||||
Names: versions.NamesFrom(versions.EntriesOf(branches))}
|
||||
// A publish that ends badly is a finding (design 51 *Health and conditions*): a holder's refused, a version no holder
|
||||
// answered within its bound (a key of the state bounds with its own time to live), a seat's outcome that does not read.
|
||||
pubBoundsKV := process.AwaitBucket(ctx, js, conn, versions.BoundsBucket, "a publish no holder answers, and a node holding the old form seven days, are not raised", logger.Printf)
|
||||
stepReads.bounds, stepReads.findings = func() jetstream.KeyValue { return pubBoundsKV.KV() }, found
|
||||
pubs := &publishes{bounds: pubBoundsKV.KV, versions: numberer.Versions, findings: found, bound: versions.PublishBound, logf: logger.Printf}
|
||||
numberer.Publishing = pubs.arm
|
||||
b := &build.Builder{
|
||||
var b *build.Builder
|
||||
seatsChanged := func(ctx context.Context) {
|
||||
heard.Watch(seats.Modules())
|
||||
if b != nil {
|
||||
b.Reconsider(ctx)
|
||||
}
|
||||
}
|
||||
b = &build.Builder{
|
||||
Claimer: build.Claimer{Store: kvStore{buildsKV}, By: cred.Node + "." + cred.Module},
|
||||
Asks: buildrun.Asker{Publisher: asks, Waiter: buildrun.LostAskWaiter{Inner: heard, Dead: dead, Hearing: hearing}, Bound: bound, Dropper: buildrun.SeatDrops{Caller: calls}},
|
||||
Held: heldOnTheBus{js},
|
||||
Seats: seats,
|
||||
Asks: buildrun.Asker{Seats: seats, Publisher: asks, Waiter: buildrun.LostAskWaiter{Inner: heard, Dead: dead, Hearing: hearing}, Bound: bound, Dropper: buildrun.SeatDrops{Caller: calls}},
|
||||
Held: heldOnTheBus{js: js, seats: seats},
|
||||
// An ask that can no longer be answered is asked again (issue 1945), read at the step bound: a context's bound.
|
||||
Gone: heldOnTheBus{js},
|
||||
Gone: heldOnTheBus{js: js, seats: seats},
|
||||
CheckAfter: bound,
|
||||
Toolchains: tools,
|
||||
Manifests: manifestReader{manifests},
|
||||
Manifests: half.manifests,
|
||||
Logf: logger.Printf,
|
||||
Stored: storedVia(calls.Call),
|
||||
Node: cred.Node,
|
||||
@@ -221,6 +216,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// and version-released heard; nothing is reserved before the fill.
|
||||
reservedKV := process.AwaitBucket(ctx, js, conn, versions.ReservedBucket, "no version is reserved: builds are made unreserved", logger.Printf)
|
||||
reserved := newReservations(awaitedReserved{awaitedVersions{b: reservedKV, bucket: versions.ReservedBucket}}, calls.Call, pubBoundsKV.KV, found, b.Reconsider, logger.Printf)
|
||||
// Each artifact the holder says published at a reserved version is said as the seat mesh-builds' package-published (design 47 G §52).
|
||||
reserved.say = func(ctx context.Context, p meshbuilds.PackagePublished) error {
|
||||
return emitSeatEvent(context.WithoutCancel(ctx), js, cred, meshbuilds.SubjectPackagePublished, "package-published-"+p.Module+"-"+p.Version+"-"+p.Artifact, p)
|
||||
}
|
||||
b.Reserved = reserved
|
||||
// A state bound later numbers the builds looked at without it.
|
||||
versionsKV.Ready(func(jetstream.KeyValue) { b.Reconsider(ctx) })
|
||||
@@ -251,18 +250,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
retake := &retaking{rt: &build.Retaker{Builder: b, Acted: retakeActed, Entry: entryVia(calls.Call), Ask: operator.Ask, Logf: logger.Printf}}
|
||||
queueKV.Ready(again.bound(ctx, js, logger.Printf))
|
||||
actedKV.Ready(again.bound(ctx, js, logger.Printf))
|
||||
// A candidate's preview is merged onto the trunk head the indexer previewed it onto: source.main at the candidate's base
|
||||
// revision (the gate's source state). The source's revisions are watched with their history (a direct get of one revision
|
||||
// is not what a read grants): a revision heard after the candidate that names it builds that candidate then. Where it
|
||||
// cannot be read the trunk's keys are built and the candidates wait (issue 1945).
|
||||
errs := make(chan error, 10)
|
||||
trunks := &trunkSource{ctx: ctx, js: js, heads: &sourceHeads{}, moved: func(ctx context.Context) {
|
||||
// A source revision heard is the forge's merge: it wakes a floor whose bridge_tags was silent (design 51 *The floor*).
|
||||
(&floorWake{b: b, waits: floorWaits, logf: logger.Printf}).wake(ctx, versions.WakeForge, "", "")
|
||||
b.TrunkMoved(ctx)
|
||||
}, errs: errs, logf: logger.Printf}
|
||||
trunks.start()
|
||||
b.Trunk = trunks.head
|
||||
b.MemberBounds = &memberBounds{bounds: pubBoundsKV.KV, now: time.Now, logf: logger.Printf}
|
||||
b.Members = versions.ModuleFrom(versions.EntriesOf(branches)) // the build of a group's package member (design 47 G §373, §465)
|
||||
// The builder's log (ADR 0571 §1): each build's start and end in its module log `build-log`, read by the view, the
|
||||
// indexer and the assigner through their reads, and by the operator's command nox builds through the tool builder_builds_since.
|
||||
b.Log = func(e builds.BuildLogEntry) {
|
||||
@@ -290,7 +280,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
switch kv := process.OptionalBucket(ctx, js, instancesBucket, "the controller is not told that this builder writes references until it starts again", logger.Printf); {
|
||||
case kv == nil:
|
||||
default:
|
||||
says := newSayer(ctx, kv, cred.Node, []string{buildrun.GoBuildModule, buildrun.NodeBuildModule, build.ImageBuildModule}, logger.Printf)
|
||||
says := newSayer(ctx, kv, cred.Node, seats.Modules, logger.Printf)
|
||||
b.Answered = says.Answered
|
||||
defer func() {
|
||||
stopping, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
||||
@@ -393,7 +383,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// condition keeper's condition-cleared.
|
||||
floor := floorWake{b: b, waits: floorWaits, logf: logger.Printf}
|
||||
// And the four registry seats' published, refused and removed (DLV-B4), which move a numbered version.
|
||||
seats := seatHearer{n: numberer, b: b, logf: logger.Printf, pub: pubs, res: reserved}
|
||||
seatHeard := seatHearer{n: numberer, b: b, logf: logger.Printf, pub: pubs, res: reserved}
|
||||
events := func(m jetstream.Msg) {
|
||||
if again.heard(ctx, m, logger.Printf) { // the publish again's own subjects, left for the bus to give again when its state cannot take them
|
||||
return
|
||||
@@ -404,17 +394,29 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
if retake.heard(ctx, m, logger.Printf) { // a key failed on a transport fault, built again when its cause passed (§568)
|
||||
return
|
||||
}
|
||||
if reads.heard(m, logger.Printf) || heardTools.Reported(ctx, m) || heardTools.Running(ctx, m) || floor.heard(ctx, m) || reserved.heard(ctx, m) || seats.heard(ctx, m) { // reads: the node-engine seat's manifest-reads-changed (ADR 0618 §4)
|
||||
if reads.heard(m, logger.Printf) || heardTools.Reported(ctx, m) || heardTools.Running(ctx, m) || floor.heard(ctx, m) || reserved.heard(ctx, m) || seatHeard.heard(ctx, m) { // reads: the node-engine seat's manifest-reads-changed (ADR 0618 §4)
|
||||
return
|
||||
}
|
||||
heardGone(ctx, b, m, logger.Printf)
|
||||
}
|
||||
// The seats' events (artifacts-gone, the registry seats' words, manifest-accepted and manifest-reads-changed and the
|
||||
// seat mesh-conditions' condition-cleared, which this pattern covers: a consumer takes no two overlapping filters); the
|
||||
// owners' views events are the views reader's.
|
||||
if err := hub.Handle("mesh.seat.*.event.>", events); err != nil {
|
||||
// The seats' events (artifacts-gone, the registry seats' words, the build seats' toolchain-reported, manifest-accepted and
|
||||
// manifest-reads-changed and the seat mesh-conditions' condition-cleared: seatEventPatterns); the owners' views events and the
|
||||
// catalog half's catalog-revision and registration-changed and the gate's source-moved are the views reader's (a message has
|
||||
// one taker, so no pattern here covers them).
|
||||
// The seat list over the connection and heard on the hub: a change of it, as of the catalog half's holders, names the build
|
||||
// modules again (design 47 G A-82, §204).
|
||||
// The runner's build step ask, the builder's one entry point for a candidate (design 47 G A-97).
|
||||
b.StepEntry = newEntryWaits(branches, half.manifests).entry
|
||||
if err := serveSteps(ctx, conn, js, cred, b, logger.Printf); err != nil {
|
||||
return err
|
||||
}
|
||||
seatGate.Follow(seatlist.ProcessBus(calls.Call, hub), func() { seatsChanged(ctx) }, logger.Printf)
|
||||
half.holders = func() { seatsChanged(ctx) }
|
||||
for _, pattern := range seatEventPatterns {
|
||||
if err := hub.Handle(pattern, events); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
hub.OnBound(reads.atBind) // what a node's engine said before the consumer was bound (ADR 0668 §4)
|
||||
if stopped != nil {
|
||||
return stopped
|
||||
@@ -426,10 +428,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return hub.Start(ctx, js, cred)
|
||||
})
|
||||
go func() { <-ctx.Done(); hub.Stop() }()
|
||||
// The modules, once their first views and the manifests' are answered: what a module builds is read from the manifests.
|
||||
go func() { errs <- buildFromModules(ctx, b, modules, manifests, candidates) }()
|
||||
// A package's name heard in the indexer's packages looks at its module again, so a done build is numbered then.
|
||||
go func() { errs <- followPackages(ctx, packages, b, logger.Printf) }()
|
||||
// The catalog half's entries as they come: a module's entry of a branch goes to the builder, and a key left building for a
|
||||
// fingerprint no branch carries is closed (issue 1660) once the first view of every followed branch is in (never at the start:
|
||||
// a holder of the seat that is down holds no other part of it).
|
||||
half.b = b
|
||||
half.ready = func() {
|
||||
if err := b.Sweep(ctx); err != nil {
|
||||
errs <- err
|
||||
}
|
||||
}
|
||||
branches.Changed = half.Changed(ctx)
|
||||
// A person asks a key's build again with builder_ask_again, which writes the reasks state (issue 1945): watched, as
|
||||
// every other input. The builder asks again by itself without it.
|
||||
// A state not on the bus yet at this start is watched once it appears (issue 1790).
|
||||
@@ -454,6 +462,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
stepReads.expired(ctx, e)
|
||||
return
|
||||
}
|
||||
if key, ok := versions.MemberBoundOf(e.Key()); ok {
|
||||
if e.Operation() == jetstream.KeyValuePurge { // its 60 minutes ended: the dependent's step fails naming the member
|
||||
b.MemberBoundEnded(ctx, key)
|
||||
}
|
||||
return
|
||||
}
|
||||
if node, ok := enginereads.BoundOf(e.Key()); ok {
|
||||
if e.Operation() == jetstream.KeyValuePurge { // its five minutes ended
|
||||
reads.expiredCall(node)
|
||||
@@ -473,7 +487,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// A person reads a build seat's stream with builder_seat_queue, which writes the queues state (issue 1945 note 62): the
|
||||
// builder alone may read the stream, so it answers there.
|
||||
process.AwaitBucket(ctx, js, conn, build.QueuesBucket, "builder_seat_queue is not answered", logger.Printf).Ready(func(kv jetstream.KeyValue) {
|
||||
eachRead := func(e jetstream.KeyValueEntry) { queueRead(ctx, heldOnTheBus{js}, kv, e, logger.Printf) }
|
||||
eachRead := func(e jetstream.KeyValueEntry) {
|
||||
queueRead(ctx, heldOnTheBus{js: js, seats: seats}, kv, e, logger.Printf)
|
||||
}
|
||||
if qw, err := settle(ctx, kv, eachRead); err != nil {
|
||||
process.NotWatched(logger.Printf, build.QueuesBucket, err, "builder_seat_queue is not answered until the builder starts again")
|
||||
} else {
|
||||
@@ -509,102 +525,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// keeper raises a first view unanswered past its bound (ADR 0615 §6).
|
||||
var firstViewsBucket = builds.BuilderModule + "_" + views.StatusState
|
||||
|
||||
// The indexer's states the builder follows (indexerBucket(modulesState), ManifestsBucket, CandidatesBucket), and the owners'
|
||||
// views families as their manifests' `streams` name them.
|
||||
const (
|
||||
modulesState = "modules"
|
||||
manifestsState = "manifests"
|
||||
candidatesState = "candidates"
|
||||
)
|
||||
|
||||
// buildFromModules builds from the indexer's modules and candidates once the first views of the modules, the manifests and
|
||||
// the candidates are answered (never at the start: an indexer that is down holds no other part of it), and follows them.
|
||||
func buildFromModules(ctx context.Context, b *build.Builder, modules, manifests, candidates *views.Picture) error {
|
||||
for _, p := range []*views.Picture{modules, manifests, candidates} {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-p.Follower().Ready():
|
||||
}
|
||||
}
|
||||
each := func(e jetstream.KeyValueEntry) {
|
||||
if opOf(e) {
|
||||
b.Module(ctx, e.Key(), e.Value())
|
||||
} else {
|
||||
b.Forget(e.Key())
|
||||
}
|
||||
}
|
||||
w, err := settle(ctx, modules, each)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// A key left building for a fingerprint its module no longer has is closed (issue 1660); the modules are all known now.
|
||||
if err := b.Sweep(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// The fingerprints the indexer gives the candidates' modules are built too (issue 1684), once the trunk's modules are known.
|
||||
eachCandidate := func(e jetstream.KeyValueEntry) {
|
||||
if opOf(e) {
|
||||
b.Candidate(ctx, e.Key(), e.Value())
|
||||
} else {
|
||||
b.ForgetCandidate(e.Key())
|
||||
}
|
||||
}
|
||||
cw, err := settle(ctx, candidates, eachCandidate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() { _ = follow(ctx, cw, eachCandidate) }()
|
||||
return follow(ctx, w, each)
|
||||
}
|
||||
|
||||
// sourceHeads are the trunk heads of each revision of the gate's source.main the watch delivered, the newest
|
||||
// sourceHistory of them: what a candidate's base revision names.
|
||||
type sourceHeads struct {
|
||||
mu sync.Mutex
|
||||
byRev map[uint64]map[string]string
|
||||
}
|
||||
|
||||
// put takes one revision in and answers whether it is new.
|
||||
func (s *sourceHeads) put(e jetstream.KeyValueEntry) bool {
|
||||
if !opOf(e) {
|
||||
return false
|
||||
}
|
||||
src, err := meshdeliveryclient.DecodeSource(e.Value())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.byRev == nil {
|
||||
s.byRev = map[uint64]map[string]string{}
|
||||
}
|
||||
if _, had := s.byRev[e.Revision()]; had {
|
||||
return false
|
||||
}
|
||||
s.byRev[e.Revision()] = src.Heads
|
||||
for len(s.byRev) > sourceHistory {
|
||||
delete(s.byRev, slices.Min(slices.Collect(maps.Keys(s.byRev))))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// head is the head of repository in revision rev; an error when the revision is not heard (yet, or any more).
|
||||
func (s *sourceHeads) head(_ context.Context, rev uint64, repository string) (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
heads, ok := s.byRev[rev]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("source revision %d is not heard (it is built once it is)", rev)
|
||||
}
|
||||
for r, head := range heads {
|
||||
if trunk.SameRepository(r, repository) {
|
||||
return head, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// toolchainWaitOf reads the toolchain wait's setting, toolchain-wait-minutes, from the file the manifest declares with the
|
||||
// content ${config:toolchain-wait-minutes} (design 47 G §672, A-71), at the builder's start: no file, or an empty one, is
|
||||
// the default; a content that is no number of minutes is an error naming the file.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"mesh-delivery-builder/internal/versions"
|
||||
)
|
||||
|
||||
// memberBounds keeps the bound of a dependent's wait for a package member of a group's run (design 47 G §373, A-77): a key of
|
||||
// the state bounds with its own time to live, whose expiry the watch of the state bounds hears (Builder.MemberBoundEnded).
|
||||
// No timer.
|
||||
type memberBounds struct {
|
||||
bounds func() jetstream.KeyValue
|
||||
now func() time.Time
|
||||
logf func(string, ...any)
|
||||
}
|
||||
|
||||
// Arm is build.MemberBounds' Arm.
|
||||
func (m *memberBounds) Arm(ctx context.Context, key, why string, bound time.Duration) error {
|
||||
kv := m.bounds()
|
||||
if kv == nil {
|
||||
return fmt.Errorf("%s is not on the bus yet", versions.BoundsBucket)
|
||||
}
|
||||
if _, err := kv.Create(ctx, versions.MemberBoundKey(key), versions.EncodeBound(why, m.now().Add(bound)), jetstream.KeyTTL(bound)); err != nil && !errors.Is(err, jetstream.ErrKeyExists) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// End is build.MemberBounds' End.
|
||||
func (m *memberBounds) End(ctx context.Context, key string) {
|
||||
if kv := m.bounds(); kv != nil {
|
||||
if err := kv.Delete(ctx, versions.MemberBoundKey(key)); err != nil && !errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
m.logf("the member bound of %s is not deleted: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,20 +18,24 @@ const (
|
||||
indexerModule = "mesh-delivery-indexer"
|
||||
// assignerModule is the holder of the state assignments.
|
||||
assignerModule = buildrun.AssignerModule
|
||||
|
||||
// sourceBucket is the gate's state source (reads: mesh-delivery-gate.source), the heads a candidate's base names.
|
||||
sourceBucket = gateModule + "_source"
|
||||
// packagesBucket is the indexer's state packages.
|
||||
packagesBucket = indexerModule + "_" + meshcatalog.StatePackages
|
||||
|
||||
// The revisions each keeps per key (the beds of the integration test create them alike).
|
||||
sourceHistory = 64
|
||||
candidatesHistory = 10
|
||||
modulesHistory = 20
|
||||
manifestsHistory = 1
|
||||
)
|
||||
|
||||
func indexerBucket(state string) string { return indexerModule + "_" + state }
|
||||
|
||||
// namedTreePrefix opens the tree input of a module whose every artifact names its paths.
|
||||
const namedTreePrefix = "named:"
|
||||
|
||||
// seatEventPatterns are the seats' events the builder's own handler takes off its durable consumer (its manifest's `consumes` of
|
||||
// them): the registry seats' words, artifacts-gone, the build seats' toolchain-reported, condition-cleared, the node-engine
|
||||
// seat's events and, of the seat mesh-catalog, manifest-accepted, version-released and versions-filled. The seat mesh-catalog's
|
||||
// catalog-revision and registration-changed and the seat mesh-delivery's source-moved are the views reader's (the branch
|
||||
// follower's), so no pattern here covers them: a message has one taker.
|
||||
var seatEventPatterns = func() []string {
|
||||
var out []string
|
||||
for _, seat := range []string{"container-image-registry", "go-package-registry", "npm-package-registry", "oci-artifact-registry",
|
||||
"mesh-artifact-sweep", "mesh-conditions", "node-engine", "node-build-go", "node-build-image", "node-build-node"} {
|
||||
out = append(out, "mesh.seat."+seat+".event.>")
|
||||
}
|
||||
for _, event := range []string{meshcatalog.EventManifestAccepted, meshcatalog.EventVersionReleased, meshcatalog.EventVersionsFilled} {
|
||||
out = append(out, "mesh.seat."+meshcatalog.Seat+".event."+event, "mesh.seat."+meshcatalog.Seat+".event."+event+".>")
|
||||
}
|
||||
return out
|
||||
}()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
@@ -25,22 +26,26 @@ func TestTheNamesAreTheManifests(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, read := range []string{
|
||||
gateModule + ".source",
|
||||
"mesh-build-go.steps", "mesh-build-image.steps", "mesh-build-node.steps",
|
||||
"node-build-go.steps", "node-build-image.steps", "node-build-node.steps",
|
||||
} {
|
||||
if !slices.Contains(m.Reads, read) {
|
||||
t.Errorf("the builder reads %s, its manifest's reads are %v", read, m.Reads)
|
||||
}
|
||||
}
|
||||
for _, view := range []string{indexerModule + ".views.modules", indexerModule + ".views.manifests", indexerModule + ".views.candidates",
|
||||
indexerModule + ".views.packages", assignerModule + ".views.assignments"} {
|
||||
// The catalog half (design 47 G §348, §614) is followed through the seat, and no state of the indexer is.
|
||||
for _, c := range m.Consumes {
|
||||
if strings.HasPrefix(c, indexerModule+".") {
|
||||
t.Errorf("the builder follows %s, an indexer state, its manifest consumes it", c)
|
||||
}
|
||||
}
|
||||
for _, view := range []string{"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", assignerModule + ".views.assignments"} {
|
||||
if !slices.Contains(m.Consumes, view) {
|
||||
t.Errorf("the builder follows %s, its manifest consumes %v", view, m.Consumes)
|
||||
}
|
||||
}
|
||||
for want, got := range map[string]string{
|
||||
"mesh-delivery-gate_source": sourceBucket, "mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"),
|
||||
buildrun.ToolchainsHeardBucket: "mesh-delivery-builder_toolchains-heard", "mesh-delivery-indexer_packages": packagesBucket,
|
||||
"mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"),
|
||||
buildrun.ToolchainsHeardBucket: "mesh-delivery-builder_toolchains-heard",
|
||||
"mesh-delivery-assigner_assignments": buildrun.AssignmentsBucket,
|
||||
} {
|
||||
if want != got {
|
||||
|
||||
@@ -1,76 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
// **A part does not stop for a bucket it can work without** (mesh-sdk process.EssentialBucket, OptionalBucket) (novox/hq issue 1945, met twice on 2026-10-25: the builder of
|
||||
// mesh-delivery #48 exited on a refused read of the gate's source, and its reasks bucket would have done the same). A grant
|
||||
// reaches the bus with a delivery, so a bucket the bus refuses, or times out on, at start is often only not granted yet:
|
||||
// exiting for it stops every build until a person's hand. A bucket the builder cannot build at all without (its builds,
|
||||
// the indexer's modules and manifests) still stops the start, naming the grant to look at; every other is said and left,
|
||||
// and opened again at the next event that needs it where there is one.
|
||||
|
||||
// trunkSource is the gate's source history the candidates' previews are merged onto (sourceHeads), opened at start and,
|
||||
// while it cannot be, again at the next candidate that asks a trunk head: a builder without it builds the trunk's keys,
|
||||
// and a candidate's preview waits, saying why.
|
||||
type trunkSource struct {
|
||||
ctx context.Context
|
||||
js bucketOpener
|
||||
heads *sourceHeads
|
||||
moved func(context.Context)
|
||||
errs chan<- error
|
||||
logf func(string, ...any)
|
||||
|
||||
mu sync.Mutex
|
||||
opened bool
|
||||
}
|
||||
|
||||
// ensure opens and settles the source's watch once, and follows it; an error leaves it closed for the next ask.
|
||||
func (t *trunkSource) ensure() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.opened {
|
||||
return nil
|
||||
}
|
||||
kv, err := t.js.KeyValue(t.ctx, sourceBucket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the bucket %s cannot be read (%v)", sourceBucket, err)
|
||||
}
|
||||
w, err := kv.WatchAll(t.ctx, jetstream.IncludeHistory()) // every watched branch's key: a revision names its head whichever branch it is of (design 47 G §15)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the bucket %s cannot be watched (%v)", sourceBucket, err)
|
||||
}
|
||||
sw, err := settled(t.ctx, w, func(e jetstream.KeyValueEntry) { t.heads.put(e) })
|
||||
if err != nil {
|
||||
return fmt.Errorf("the bucket %s: %w", sourceBucket, err)
|
||||
}
|
||||
t.opened = true
|
||||
go func() {
|
||||
t.errs <- follow(t.ctx, sw, func(e jetstream.KeyValueEntry) {
|
||||
if t.heads.put(e) && t.moved != nil {
|
||||
t.moved(t.ctx)
|
||||
}
|
||||
})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// start opens the source at start; one that cannot be is said, and the builder goes on.
|
||||
func (t *trunkSource) start() {
|
||||
if err := t.ensure(); err != nil {
|
||||
t.logf("%v: the trunk's keys are built, and a candidate's preview waits until the source can be read, tried again at its next candidate", err)
|
||||
}
|
||||
}
|
||||
|
||||
// head is Builder.Trunk: the head of repository at source revision rev, opening the source first if it is not yet.
|
||||
func (t *trunkSource) head(ctx context.Context, rev uint64, repository string) (string, error) {
|
||||
if err := t.ensure(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return t.heads.head(ctx, rev, repository)
|
||||
}
|
||||
|
||||
@@ -43,23 +43,11 @@ func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
|
||||
t.Run("a floor's wake is heard on the events consumer (design 51 The floor)", aFloorsWakeIsHeard)
|
||||
t.Run("a failed trunk build is a finding until a later done one (DLV-B7)", aFailedTrunkBuildIsAFinding)
|
||||
t.Run("a branch's end is acknowledged once its tagged versions are removed (design 51 Tagged versions)", aBranchsEndIsHeard)
|
||||
var logs bytes.Buffer
|
||||
logger := log.New(&logs, "", 0)
|
||||
refused := refusingBus{err: errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules")}
|
||||
if _, err := process.EssentialBucket(context.Background(), refusingBus{refuseOpen: true, err: refused.err}, "builder", "mesh-delivery-indexer_modules", "work"); err == nil ||
|
||||
!strings.Contains(err.Error(), "grant of mesh-delivery-indexer_modules") {
|
||||
t.Errorf("a refused bucket the part needs: %v, want an error naming its grant", err)
|
||||
}
|
||||
// The gate's source (the previews' trunk heads): refused at start, the builder goes on, and a candidate asking a trunk
|
||||
// head is told why and opens it again then.
|
||||
src := &trunkSource{ctx: context.Background(), js: refusingBus{refuseOpen: true, err: refused.err}, heads: &sourceHeads{}, errs: make(chan error, 1), logf: logger.Printf}
|
||||
src.start()
|
||||
if _, err := src.head(context.Background(), 7, "novox/mesh-catalog"); err == nil || !strings.Contains(err.Error(), sourceBucket) {
|
||||
t.Errorf("a trunk head with the source refused: %v, want why", err)
|
||||
}
|
||||
if src.opened {
|
||||
t.Error("a refused source counts as opened, so it is never opened again")
|
||||
}
|
||||
t.Run("ADR 0624: the counted nodes' engine reads", theCountedNodesEngineReads)
|
||||
}
|
||||
|
||||
@@ -258,7 +246,8 @@ func (m *wakeMsg) Headers() nats.Header {
|
||||
|
||||
func aFloorsWakeIsHeard(t *testing.T) {
|
||||
var logs bytes.Buffer
|
||||
waits := &versions.FloorWaits{}
|
||||
holders := map[string]string{"git": "mesh-delivery-forge-bridge", "mesh-catalog": "mesh-delivery-indexer"}
|
||||
waits := &versions.FloorWaits{Holder: func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok }}
|
||||
waits.Wait("m/lib", versions.SeatGoPackages, "", versions.Floor{Silent: []string{versions.SourceRequirements}}, nil, time.Unix(0, 0))
|
||||
waits.Wait("m/img", versions.SeatImages, "", versions.Floor{Silent: []string{versions.SourceTags}}, nil, time.Unix(0, 0))
|
||||
f := &floorWake{b: &build.Builder{}, waits: waits, logf: log.New(&logs, "", 0).Printf}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -48,6 +49,8 @@ type reservations struct {
|
||||
logf func(string, ...any)
|
||||
// call asks a seat's verb: the git seat's tag (design 47 G §370).
|
||||
call toolCaller
|
||||
// say says package-published on the seat mesh-builds; nil says none.
|
||||
say func(context.Context, meshbuilds.PackagePublished) error
|
||||
}
|
||||
|
||||
// newReservations makes the reserving over store; the released versions of a module are asked of the seat mesh-catalog's
|
||||
@@ -82,9 +85,9 @@ func (rv *reservations) Reserve(ctx context.Context, module, series string, arti
|
||||
|
||||
// Built records the digests; once every artifact has one, the publish's bound starts (a key of the state bounds with its
|
||||
// own time to live): its end with the reservation standing is version-unreleased.
|
||||
func (rv *reservations) Built(ctx context.Context, module, version string, digests map[string]string) error {
|
||||
func (rv *reservations) Built(ctx context.Context, module, version string, digests map[string]string, deps map[string][]meshbuilds.Dependency) error {
|
||||
for ref, d := range digests {
|
||||
if _, err := rv.r.Built(ctx, module, version, ref, d); err != nil {
|
||||
if _, err := rv.r.Built(ctx, module, version, ref, d, deps[ref]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -203,9 +206,19 @@ func (rv *reservations) heard(ctx context.Context, m jetstream.Msg) bool {
|
||||
|
||||
// published marks the artifacts of the reservation a registry seat's published names (its version and digest).
|
||||
func (rv *reservations) published(ctx context.Context, o versions.Outcome) {
|
||||
if _, err := rv.r.Published(ctx, o.Version, o.Digest); err != nil {
|
||||
marked, err := rv.r.Published(ctx, o.Version, o.Digest)
|
||||
if err != nil {
|
||||
rv.logf("a published of %s@%s is not recorded in the reservations: %v", o.Name, o.Version, err)
|
||||
}
|
||||
// Each artifact the holder's published newly marked is said as the seat mesh-builds' package-published (design 47 G §52, §334).
|
||||
for _, m := range marked {
|
||||
if rv.say == nil {
|
||||
continue
|
||||
}
|
||||
if err := rv.say(ctx, m.PackagePublished(o.Digest, time.Now())); err != nil {
|
||||
rv.logf("the package-published of %s at %s is not said: %v", m.Artifact, m.Reserved.Version, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropped is builder_reserved_drop: the reservation is deleted and its bound ended.
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
|
||||
"mesh-delivery-builder/internal/versions"
|
||||
@@ -93,11 +94,13 @@ func TestTheCatalogsWordsMoveTheReservations(t *testing.T) {
|
||||
if !rv.heard(ctx, filled) || !filled.acked || !rv.Filled() || woke != 1 {
|
||||
t.Fatalf("versions-filled: acked %v, filled %v, woke %d", filled.acked, rv.Filled(), woke)
|
||||
}
|
||||
res, err := rv.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, versions.Source{})
|
||||
var said []meshbuilds.PackagePublished
|
||||
rv.say = func(_ context.Context, p meshbuilds.PackagePublished) error { said = append(said, p); return nil }
|
||||
res, err := rv.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, versions.Source{Branch: "main", Revision: 41})
|
||||
if err != nil || res.Version != "1.4.3" {
|
||||
t.Fatalf("reserved %q (%v), want 1.4.3 after the catalog's 1.4.2", res.Version, err)
|
||||
}
|
||||
if err := rv.Built(ctx, "m", "1.4.3", map[string]string{"m:a": "sha256:a", "m:b": "sha256:b"}); err != nil {
|
||||
if err := rv.Built(ctx, "m", "1.4.3", map[string]string{"m:a": "sha256:a", "m:b": "sha256:b"}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
boundKey := versions.ReservedBoundKey("m", "1.4")
|
||||
@@ -105,6 +108,12 @@ func TestTheCatalogsWordsMoveTheReservations(t *testing.T) {
|
||||
t.Fatalf("the publish's bound is not armed: %v", bounds.keys)
|
||||
}
|
||||
rv.published(ctx, versions.Outcome{Name: "m-a", Version: "1.4.3", Digest: "sha256:a"})
|
||||
// Design 47 G §52, A-57: the holder's published is said once, per artifact, as the seat mesh-builds' package-published with the
|
||||
// branch and source revision of the reservation; a second published of the same artifact says nothing.
|
||||
rv.published(ctx, versions.Outcome{Name: "m-a", Version: "1.4.3", Digest: "sha256:a"})
|
||||
if len(said) != 1 || said[0].Artifact != "m:a" || said[0].Version != "1.4.3" || said[0].Digest != "sha256:a" || said[0].Branch != "main" || said[0].Source != 41 {
|
||||
t.Fatalf("package-published said %+v, want one of m:a at 1.4.3 for main at source 41", said)
|
||||
}
|
||||
if !rv.ended(ctx, purged{key: boundKey}) {
|
||||
t.Fatal("a reservation's bound was not taken in")
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ type sayer struct {
|
||||
ctx context.Context
|
||||
store sayStore
|
||||
node string
|
||||
need []string
|
||||
need func() []string
|
||||
logf func(string, ...any)
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -39,7 +39,7 @@ type sayer struct {
|
||||
said bool
|
||||
}
|
||||
|
||||
func newSayer(ctx context.Context, store sayStore, node string, need []string, logf func(string, ...any)) *sayer {
|
||||
func newSayer(ctx context.Context, store sayStore, node string, need func() []string, logf func(string, ...any)) *sayer {
|
||||
return &sayer{ctx: ctx, store: store, node: node, need: need, logf: logf, answered: map[string]bool{}}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (s *sayer) Answered(module string) {
|
||||
if s.said {
|
||||
return
|
||||
}
|
||||
for _, m := range s.need {
|
||||
for _, m := range s.need() {
|
||||
if !s.answered[m] {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (s saysOf) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt
|
||||
// one; stopped, it says nothing any more.
|
||||
func TestTheBuilderSaysItWritesReferencesOnceEveryBuildModuleAnsweredOne(t *testing.T) {
|
||||
s := saysOf{}
|
||||
say := newSayer(t.Context(), s, "shanks", []string{"mesh-build-go", "mesh-build-node", "mesh-build-image"}, t.Logf)
|
||||
say := newSayer(t.Context(), s, "shanks", func() []string { return []string{"mesh-build-go", "mesh-build-node", "mesh-build-image"} }, t.Logf)
|
||||
say.Answered("mesh-build-go")
|
||||
say.Answered("mesh-build-node")
|
||||
if v, ok := s["shanks"]; ok {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// The builder reads the seat list as every part does, through mesh-sdk's one wiring (go/seatlist.Part; design 47 G §204): its
|
||||
// definitions name the build seats (the seats whose acting rule is the step build), its tools `builder_seat_list` and
|
||||
// `builder_seat_list_again`. A call through the mesh MCP server is proved only as its machine's tool runner (issue 627).
|
||||
var seatListAgainCallers = []string{stdio.AnyPerson, "node-tools", "mesh-healers"}
|
||||
|
||||
// seatGate is the builder's wiring of the seat list.
|
||||
type seatGate struct{ *seatlist.Part }
|
||||
|
||||
func newSeatGate() *seatGate { return &seatGate{seatlist.NewPart("builder", seatListAgainCallers)} }
|
||||
|
||||
// definitions are the seat definitions the list holds, none before it is read.
|
||||
func (g *seatGate) definitions() map[string]seatdef.Definition {
|
||||
if r := g.Reader(); r != nil {
|
||||
return r.Definitions()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// The build modules of the fixture seat list: the holders of the three build seats (design 47 G A-82).
|
||||
const (
|
||||
testGo = "mesh-build-go"
|
||||
testNode = "mesh-build-node"
|
||||
testImage = "mesh-build-image"
|
||||
)
|
||||
|
||||
// testSeats is the finder over a seat list whose build seats act on go, typescript and kind:image, each held by its module.
|
||||
func testSeats() *buildrun.BuildSeats {
|
||||
rule := func(on string) seatdef.Definition {
|
||||
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
|
||||
}
|
||||
defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript"), "node-build-image": rule(seatdef.OnKindImage)}
|
||||
holders := map[string]string{"node-build-go": testGo, "node-build-node": testNode, "node-build-image": testImage}
|
||||
return buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"mesh-delivery-builder/internal/build"
|
||||
)
|
||||
|
||||
// **The runner's build step ask is the builder's one entry point for a candidate** (design 47 G A-97, A-84, A-99): the seat
|
||||
// mesh-builds' accepted kind `step`, heard on its subject, names the entry of one module by fingerprint; the builder reads it through
|
||||
// the seat mesh-catalog's `entry`, builds it (build.Builder.BuildStep) and says the end as the seat's event step-ended.<node>, the
|
||||
// ask's id as the event's id, which the runner holds the end to.
|
||||
|
||||
// entryWaits answers an entry by fingerprint from the catalog half or the seat's `entry`, waiting for the answer the entries
|
||||
// keeper is told of (meshcatalogclient.Entries.Changed), never a timer.
|
||||
type entryWaits struct {
|
||||
branches *meshcatalogclient.Branches
|
||||
manifest *entryManifests
|
||||
mu sync.Mutex
|
||||
waiting map[string][]chan meshcatalogclient.EntryRow
|
||||
}
|
||||
|
||||
func newEntryWaits(branches *meshcatalogclient.Branches, manifests *entryManifests) *entryWaits {
|
||||
w := &entryWaits{branches: branches, manifest: manifests, waiting: map[string][]chan meshcatalogclient.EntryRow{}}
|
||||
prior := branches.Entries.Changed
|
||||
branches.Entries.Changed = func(row meshcatalogclient.EntryRow) {
|
||||
w.mu.Lock()
|
||||
chans := w.waiting[row.Fingerprint]
|
||||
delete(w.waiting, row.Fingerprint)
|
||||
w.mu.Unlock()
|
||||
for _, ch := range chans {
|
||||
ch <- row
|
||||
}
|
||||
if prior != nil {
|
||||
prior(row)
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// entry is build.Builder.StepEntry.
|
||||
func (w *entryWaits) entry(ctx context.Context, fingerprint string) (meshcatalog.CatalogEntry, error) {
|
||||
ch := make(chan meshcatalogclient.EntryRow, 1)
|
||||
w.mu.Lock()
|
||||
w.waiting[fingerprint] = append(w.waiting[fingerprint], ch)
|
||||
w.mu.Unlock()
|
||||
e, state := w.branches.Resolve(ctx, fingerprint)
|
||||
if state == meshcatalogclient.EntryAsked {
|
||||
select {
|
||||
case row := <-ch:
|
||||
if row.Entry == nil {
|
||||
return meshcatalog.CatalogEntry{}, fmt.Errorf("the seat mesh-catalog holds no entry of it (%s)", row.State)
|
||||
}
|
||||
e, state = *row.Entry, row.State
|
||||
case <-ctx.Done():
|
||||
return meshcatalog.CatalogEntry{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
if state != meshcatalogclient.EntryAnswered {
|
||||
return meshcatalog.CatalogEntry{}, fmt.Errorf("the seat mesh-catalog's entry of it is %s", state)
|
||||
}
|
||||
return e, w.manifest.keep(e) // the manifest the build is planned from
|
||||
}
|
||||
|
||||
// serveSteps hears the step asks on the seat's accept subject and answers each one's end, in its own goroutine.
|
||||
func serveSteps(ctx context.Context, conn *nats.Conn, js jetstream.JetStream, cred process.Account, b *build.Builder, logf func(string, ...any)) error {
|
||||
sub, err := conn.Subscribe(meshbuilds.AcceptSubjectStep, func(m *nats.Msg) {
|
||||
id := m.Header.Get("x-event-id")
|
||||
if id == "" {
|
||||
id = m.Header.Get(nats.MsgIdHdr)
|
||||
}
|
||||
var run meshbuilds.StepRun
|
||||
if err := json.Unmarshal(m.Data, &run); err != nil || id == "" || run.Step != meshbuilds.StepRunStepBuild {
|
||||
logf("a step ask on %s that is no build step with an id is left: %v", m.Subject, err)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
end := b.BuildStep(ctx, run)
|
||||
end.ID = id
|
||||
if err := emitSeatEvent(context.WithoutCancel(ctx), js, cred, meshbuilds.SubjectStepEnded(cred.Node), id, end); err != nil {
|
||||
logf("saying the end of the build step %s: %v", id, err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() { <-ctx.Done(); _ = sub.Drain() }()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
)
|
||||
|
||||
// The runner's build step ask on the seat mesh-builds' accept subject, naming an entry by its fingerprint, is built through the
|
||||
// module's one claim and answered as the seat's step-ended.<node> under the ask's id (design 47 G A-97, A-99).
|
||||
func TestTheRunnersBuildStepAskIsAnsweredAsStepEnded(t *testing.T) {
|
||||
b := startBed(t)
|
||||
b.module(t, "netcheck", "catalogue-a67f0196-netcheck.module.json", "modules/netcheck")
|
||||
sub, err := nats.Connect(b.srvURL, nats.UserInfo("ctl", "pw"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sub.Close()
|
||||
ended := make(chan *nats.Msg, 4)
|
||||
s, err := sub.ChanSubscribe(meshbuilds.SubjectStepEndedAll, ended)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Unsubscribe()
|
||||
b.start(t, "n1", nil)
|
||||
b.await(t, "netcheck", isState(builds.BuildDone))
|
||||
run := meshbuilds.StepRun{Pipeline: "main.1.netcheck", Step: meshbuilds.StepRunStepBuild, Branch: "main", Revision: 1,
|
||||
Entries: []meshbuilds.BuildEntry{{Module: "netcheck", Fingerprint: fp}}}
|
||||
data, _ := json.Marshal(run)
|
||||
msg := nats.NewMsg(meshbuilds.AcceptSubjectStep)
|
||||
msg.Data = data
|
||||
msg.Header.Set("x-event-id", "ask-1")
|
||||
if err := sub.PublishMsg(msg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case m := <-ended:
|
||||
var end meshbuilds.StepEnded
|
||||
if err := json.Unmarshal(m.Data, &end); err != nil || end.ID != "ask-1" || end.Outcome != meshbuilds.StepEndedOutcomePassed || end.Pipeline != "main.1.netcheck" || m.Subject != meshbuilds.SubjectStepEnded("n1") {
|
||||
t.Fatalf("step-ended on %s = %+v, %v; want the ask ask-1 passed on n1", m.Subject, end, err)
|
||||
}
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Fatal("no step-ended within 20 s")
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (h *toolchainsHeard) bind(ctx context.Context, state heardState) {
|
||||
h.mu.Lock()
|
||||
h.kept = state
|
||||
h.mu.Unlock()
|
||||
for _, language := range buildrun.LanguageOf {
|
||||
for _, language := range h.set.Seats.Languages() {
|
||||
for _, e := range h.set.Entries(language) {
|
||||
h.keep(e.Node, e.Toolchain)
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func (h *toolchainsHeard) running(ctx context.Context, data []byte) {
|
||||
h.runAt[id] = r.At
|
||||
kept := h.kept
|
||||
h.mu.Unlock()
|
||||
language, build := buildrun.LanguageOf[r.Module]
|
||||
language, build := h.set.Seats.LanguageOf(r.Module)
|
||||
if !build || r.Fingerprint != "" {
|
||||
return
|
||||
}
|
||||
@@ -224,7 +224,7 @@ func (h *toolchainsHeard) Dropped(ctx context.Context, key string) {
|
||||
// agree says toolchain-agreed for each language whose agreed version is not the one last said (design 47 G §472: said
|
||||
// only when the agreed version changes, ADR 0584 §1).
|
||||
func (h *toolchainsHeard) agree(ctx context.Context) {
|
||||
for _, language := range buildrun.LanguageOf {
|
||||
for _, language := range h.set.Seats.Languages() {
|
||||
entries := h.set.Entries(language)
|
||||
tc, none := buildstep.AgreedToolchain(entries)
|
||||
if none != "" {
|
||||
|
||||
@@ -75,7 +75,7 @@ type heardRig struct {
|
||||
func newHeardRig(t *testing.T, state *memState) *heardRig {
|
||||
t.Helper()
|
||||
r := &heardRig{state: state}
|
||||
r.h = newToolchainsHeard(&buildrun.ToolchainSet{}, func(context.Context) { r.changed++ },
|
||||
r.h = newToolchainsHeard(&buildrun.ToolchainSet{Seats: testSeats()}, func(context.Context) { r.changed++ },
|
||||
func(_ context.Context, e meshbuilds.ToolchainAgreed) error { r.said = append(r.said, e); return nil }, log.New(io.Discard, "", 0))
|
||||
r.h.bind(context.Background(), state)
|
||||
return r
|
||||
@@ -130,18 +130,18 @@ func TestTheBuilderKeepsTheToolchainsItHeardAndForgetsThemOnStopped(t *testing.T
|
||||
t.Fatalf("a changed agreed version was not said: %+v", again.said)
|
||||
}
|
||||
at := time.Now()
|
||||
again.running(t, "ace", buildrun.GoBuildModule, "src2:x", at) // running: nothing is forgotten
|
||||
again.running(t, "ace", testGo, "src2:x", at) // running: nothing is forgotten
|
||||
if k, _ := state.Keys(); len(k) != 3 {
|
||||
t.Fatalf("a running forgot a key: %v", k)
|
||||
}
|
||||
again.running(t, "ace", buildrun.GoBuildModule, "", at.Add(time.Second)) // stopped: no fingerprint
|
||||
again.running(t, "ace", testGo, "", at.Add(time.Second)) // stopped: no fingerprint
|
||||
if k, _ := state.Keys(); len(k) != 2 || k[0] != "g14.go" {
|
||||
t.Fatalf("stopped on ace left %v, want g14.go and novox.go", k)
|
||||
}
|
||||
// An older running read after (the start's read of the stream kept last per subject) changes nothing.
|
||||
before := again.changed
|
||||
again.running(t, "g14", buildrun.GoBuildModule, "", at)
|
||||
again.running(t, "g14", buildrun.GoBuildModule, "src2:y", at.Add(-time.Hour))
|
||||
again.running(t, "g14", testGo, "", at)
|
||||
again.running(t, "g14", testGo, "src2:y", at.Add(-time.Hour))
|
||||
if k, _ := state.Keys(); len(k) != 1 || k[0] != "novox.go" || again.changed != before+1 {
|
||||
t.Fatalf("after g14 stopped and an older running: %v", k)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
|
||||
// The floor's two module tools (novox/hq design 51 *The floor*); the seat's is `seat:<registry>.versions`.
|
||||
const (
|
||||
bridgeTagsTool = "mesh-delivery-forge-bridge.bridge_tags"
|
||||
requirementsTool = "mesh-delivery-indexer.indexer_requirements"
|
||||
bridgeTagsTool = "bridge_tags"
|
||||
requirementsTool = "indexer_requirements"
|
||||
)
|
||||
|
||||
// awaitedVersions is the state versions as versions.Store: bound once the bus holds it (the grants part makes it, issue
|
||||
@@ -60,55 +60,6 @@ func (s awaitedVersions) Get(ctx context.Context, key string) ([]byte, uint64, b
|
||||
return e.Value(), e.Revision(), true, nil
|
||||
}
|
||||
|
||||
// indexerPackages is the indexer's packages state as the builder reads a package's name in its ecosystem from it (design
|
||||
// 32): the picture of its read tool's first view and its views family (ADR 0615 §1), never its bucket. Until the first
|
||||
// view is answered a package is built unnumbered and said (versions.Numberer.Names), and numbered once its name is heard
|
||||
// (followPackages).
|
||||
type indexerPackages struct{ kv jetstream.KeyValue }
|
||||
|
||||
func (s indexerPackages) Keys(ctx context.Context) ([]string, error) {
|
||||
keys, err := s.kv.Keys(ctx)
|
||||
if errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return keys, err
|
||||
}
|
||||
|
||||
func (s indexerPackages) Get(ctx context.Context, key string) ([]byte, bool, error) {
|
||||
e, err := s.kv.Get(ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) || errors.Is(err, jetstream.ErrKeyDeleted) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return e.Value(), true, nil
|
||||
}
|
||||
|
||||
// followPackages watches the picture of the indexer's packages state: each series key heard that names a package's name
|
||||
// (versions.NamedModule), its first view's included, looks at its module again (build.Builder.PackageNamed), so a done
|
||||
// build that left the package unnumbered is numbered then with its digests. Woken by the views family, never a timer; a
|
||||
// key heard again naming the name and module it named (the indexer writes a series at each publish) looks at nothing.
|
||||
func followPackages(ctx context.Context, kv jetstream.KeyValue, b *build.Builder, logf func(string, ...any)) error {
|
||||
w, err := kv.WatchAll(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
heard := map[string]string{}
|
||||
return follow(ctx, w, func(e jetstream.KeyValueEntry) {
|
||||
if e.Operation() != jetstream.KeyValuePut {
|
||||
delete(heard, e.Key())
|
||||
return
|
||||
}
|
||||
module, name, named := versions.NamedModule(e.Key(), e.Value())
|
||||
if named && heard[e.Key()] != module+" "+name {
|
||||
heard[e.Key()] = module + " " + name
|
||||
logf("%s.%s names a package of %s: its build is looked at again", packagesBucket, e.Key(), module)
|
||||
b.PackageNamed(ctx, module)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func versionsConflict(err error) error {
|
||||
if errors.Is(err, jetstream.ErrKeyExists) || errors.Is(err, jetstream.ErrKeyRevisionMismatch) {
|
||||
return versions.ErrConflict
|
||||
@@ -154,7 +105,15 @@ func (s awaitedVersions) KeysOf(ctx context.Context, filter string) ([]string, e
|
||||
// floorVia is the floor's three sources as calls of the builder's own account (design 51 *The floor*): the seat's verb
|
||||
// versions, the forge bridge's bridge_tags and the indexer's indexer_requirements. A call that fails is that source
|
||||
// silent.
|
||||
func floorVia(call toolCaller) versions.FloorSources {
|
||||
func floorVia(call toolCaller, holder func(seat string) (string, bool)) versions.FloorSources {
|
||||
sourceTool := func(source, tool string) (string, error) {
|
||||
seat := versions.SourceSeats[source]
|
||||
module, ok := holder(seat)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("no module is known to hold the seat %s, the source of %s", seat, tool)
|
||||
}
|
||||
return module + "." + tool, nil
|
||||
}
|
||||
return versions.FloorSources{
|
||||
Seat: func(_ context.Context, seat, name string) ([]string, error) {
|
||||
raw, err := call("seat:"+seat+".versions", map[string]any{"name": name})
|
||||
@@ -178,7 +137,11 @@ func floorVia(call toolCaller) versions.FloorSources {
|
||||
return out, nil
|
||||
},
|
||||
Tags: func(_ context.Context, repository, prefix string) ([]string, error) {
|
||||
raw, err := call(bridgeTagsTool, map[string]any{"repository": repository, "prefix": prefix})
|
||||
tool, err := sourceTool(versions.SourceTags, bridgeTagsTool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := call(tool, map[string]any{"repository": repository, "prefix": prefix})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -191,7 +154,11 @@ func floorVia(call toolCaller) versions.FloorSources {
|
||||
return a.Tags, nil
|
||||
},
|
||||
Requirements: func(_ context.Context, name string) ([]string, error) {
|
||||
raw, err := call(requirementsTool, map[string]any{"package": name})
|
||||
tool, err := sourceTool(versions.SourceRequirements, requirementsTool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := call(tool, map[string]any{"package": name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package build
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
@@ -20,7 +23,15 @@ import (
|
||||
|
||||
// via is how a build's fetch step reaches its source when it is a candidate's: the ref that carries the head and the trunk
|
||||
// commit the head is merged onto.
|
||||
type via struct{ Ref, Base string }
|
||||
type via struct {
|
||||
Ref, Base string
|
||||
// Members are the group's package members the module is built against by their builds (design 47 G §373, §465).
|
||||
Members []member
|
||||
}
|
||||
|
||||
// member is one package member of a group's run: the package the module's fingerprint was computed against at the member's
|
||||
// head, the member's module and artifact that build it, and the member's fingerprint in the same candidate.
|
||||
type member struct{ Package, Module, Artifact, Fp string }
|
||||
|
||||
// Candidate takes one value of the indexer's candidates bucket in (the key is the group) and builds the fingerprints it
|
||||
// carries. A value that is not a candidate is said and left.
|
||||
@@ -46,6 +57,14 @@ func (b *Builder) ForgetCandidate(key string) {
|
||||
}
|
||||
|
||||
func (b *Builder) considerCandidate(ctx context.Context, c meshcatalog.CandidatesByGroupValue) {
|
||||
group := map[string]string{} // the fingerprint of each module the group's pull requests change
|
||||
for _, p := range c.Pulls {
|
||||
for name, cm := range p.Fps {
|
||||
if cm.Fp2 != "" && cm.Manifest != "" {
|
||||
group[name] = cm.Fp2
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range c.Pulls {
|
||||
names := make([]string, 0, len(p.Fps))
|
||||
for name := range p.Fps {
|
||||
@@ -83,9 +102,14 @@ func (b *Builder) considerCandidate(ctx context.Context, c meshcatalog.Candidate
|
||||
b.logf("candidates.%s: the module %s's fingerprint %s is not built: %v", c.Group, name, cm.Fp2, err)
|
||||
continue
|
||||
}
|
||||
members, err := b.membersOf(ctx, name, cm, group)
|
||||
if err != nil {
|
||||
b.logf("candidates.%s: the module %s's fingerprint %s is not built: %v", c.Group, name, cm.Fp2, err)
|
||||
continue
|
||||
}
|
||||
b.mu.Lock()
|
||||
if _, onTrunk := b.trunkFP[key]; !onTrunk {
|
||||
b.via[key] = via{Ref: fmt.Sprintf("refs/pull/%d/head", p.Number), Base: base}
|
||||
b.via[key] = via{Ref: fmt.Sprintf("refs/pull/%d/head", p.Number), Base: base, Members: members}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
b.consider(ctx, name, m)
|
||||
@@ -106,7 +130,7 @@ func (b *Builder) agreedFor(ctx context.Context, manifest string) map[string]bui
|
||||
if !plan.NeedsToolchain(bm) {
|
||||
continue
|
||||
}
|
||||
language := buildrun.LanguageOf[bm]
|
||||
language, _ := b.Seats.LanguageOf(bm)
|
||||
if tc, none := buildstep.AgreedToolchain(b.Toolchains.Entries(language)); none == "" {
|
||||
out[language] = builds.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe}
|
||||
}
|
||||
@@ -131,3 +155,177 @@ func (b *Builder) trunkHead(ctx context.Context, rev uint64, repository string)
|
||||
}
|
||||
return head, nil
|
||||
}
|
||||
|
||||
// membersOf are the package members of a group's run that module name is built against (design 47 G §373, §465): each package
|
||||
// its fingerprint names at a full commit, the head of a group member, whose module and artifact the catalog half's view of the
|
||||
// packages names (Builder.Members) and which the group changes. A package at a version, or one no module of the group builds,
|
||||
// is no member: it is fetched as before.
|
||||
func (b *Builder) membersOf(ctx context.Context, name string, cm meshcatalog.CandidatesByGroupValuePullsItemFpsValue, group map[string]string) ([]member, error) {
|
||||
if b.Members == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var out []member
|
||||
for _, pkg := range slices.Sorted(maps.Keys(cm.Packages)) {
|
||||
if len(cm.Packages[pkg]) != 40 {
|
||||
continue
|
||||
}
|
||||
module, artifact, found, err := b.Members(ctx, pkg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the module and artifact of the package %s cannot be read: %w", pkg, err)
|
||||
}
|
||||
if !found || module == name || group[module] == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, member{Package: pkg, Module: module, Artifact: artifact, Fp: group[module]})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MemberBound is how long a dependent of a group's run waits for a package member's build (design 47 G §373: building's
|
||||
// 60-minute bound).
|
||||
const MemberBound = 60 * time.Minute
|
||||
|
||||
// MemberBounds keeps the bound of a member wait, a key of the builder's state with its own time to live (design 47 G §373, A-77):
|
||||
// Arm writes it for key, the dependent's build key; End deletes it.
|
||||
type MemberBounds interface {
|
||||
Arm(ctx context.Context, key, why string, bound time.Duration) error
|
||||
End(ctx context.Context, key string)
|
||||
}
|
||||
|
||||
// MemberBoundEnded is told that key's bound ended (the bus's marker of the expiry): the dependent's wait ends then, and the
|
||||
// candidates are looked at again, which fails its step naming the member still not built.
|
||||
func (b *Builder) MemberBoundEnded(ctx context.Context, key string) {
|
||||
b.init()
|
||||
b.mu.Lock()
|
||||
waiting := b.memberWaits[key]
|
||||
if waiting {
|
||||
b.memberExpired[key] = true
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if waiting {
|
||||
b.TrunkMoved(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// endMemberWait forgets key's wait and deletes its bound.
|
||||
func (b *Builder) endMemberWait(ctx context.Context, key string) {
|
||||
b.mu.Lock()
|
||||
had := b.memberWaits[key]
|
||||
delete(b.memberWaits, key)
|
||||
delete(b.memberExpired, key)
|
||||
b.mu.Unlock()
|
||||
if had && b.MemberBounds != nil {
|
||||
b.MemberBounds.End(ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
// memberBuilds reads the builds of the package members key's module is built against, from the builder's own keys: the
|
||||
// build each is fetched by, why the step fails (a member's build failed, holds no digest, or is missing past the bound), or
|
||||
// that it waits (a member not built yet), saying wait-held naming the member once (design 47 G §373). The bound is read at each look: the
|
||||
// builder holds no timer.
|
||||
func (b *Builder) memberBuilds(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, key string) (got []buildstep.FetchBuild, fail string, waiting bool) {
|
||||
b.mu.Lock()
|
||||
v := b.via[key]
|
||||
b.mu.Unlock()
|
||||
if len(v.Members) == 0 {
|
||||
return nil, "", false
|
||||
}
|
||||
defer func() {
|
||||
if !waiting {
|
||||
b.endMemberWait(ctx, key) // a wait that ends, by a build or a failure, deletes its bound
|
||||
}
|
||||
}()
|
||||
var pending []string
|
||||
for _, mem := range v.Members {
|
||||
ref := mem.Module + "/" + mem.Artifact
|
||||
mk, err := builds.BuilderBuildKey(mem.Module, mem.Fp)
|
||||
if err != nil {
|
||||
return nil, fmt.Sprintf("the package member %s (%s) has no build key: %v", mem.Package, ref, err), false
|
||||
}
|
||||
raw, _, found, err := b.Claimer.Store.Get(ctx, mk)
|
||||
if err != nil {
|
||||
return nil, fmt.Sprintf("the build of the package member %s (%s) cannot be read: %v", mem.Package, ref, err), false
|
||||
}
|
||||
if !found {
|
||||
pending = append(pending, ref)
|
||||
continue
|
||||
}
|
||||
cur, err := builds.DecodeBuilderBuild(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Sprintf("the build of the package member %s (%s) is not read: %v", mem.Package, ref, err), false
|
||||
}
|
||||
switch cur.State {
|
||||
case builds.BuildDone:
|
||||
digest := ""
|
||||
for _, a := range cur.Artifacts {
|
||||
if a.Name == mem.Artifact {
|
||||
digest = a.Digest
|
||||
}
|
||||
}
|
||||
if digest == "" {
|
||||
return nil, fmt.Sprintf("the build of the package member %s (%s) names no digest of it", mem.Package, ref), false
|
||||
}
|
||||
got = append(got, buildstep.FetchBuild{Package: mem.Package, Module: mem.Module, Artifact: mem.Artifact, Digest: digest})
|
||||
case builds.BuildFailed:
|
||||
return nil, fmt.Sprintf("the build of the package member %s (%s) failed: %s", mem.Package, ref, cur.Error), false
|
||||
default:
|
||||
pending = append(pending, ref)
|
||||
}
|
||||
}
|
||||
if len(pending) == 0 {
|
||||
b.mu.Lock()
|
||||
b.fetched[key] = got
|
||||
b.mu.Unlock()
|
||||
return got, "", false
|
||||
}
|
||||
b.mu.Lock()
|
||||
said := b.memberWaits[key]
|
||||
expired := b.memberExpired[key]
|
||||
b.mu.Unlock()
|
||||
if expired {
|
||||
return nil, fmt.Sprintf("the package member %s was not built within %s", strings.Join(pending, ", "), MemberBound), false
|
||||
}
|
||||
if !said {
|
||||
line := module + " at " + m.Fp2 + " is not built yet: wait-held, the package member " + strings.Join(pending, ", ") + " of the group is not built, within " + MemberBound.String()
|
||||
if b.MemberBounds != nil {
|
||||
// The bound is a key with its own time to live, whose expiry is heard (MemberBoundEnded); one that cannot be written
|
||||
// would never end, so the step fails at once, saying so.
|
||||
if err := b.MemberBounds.Arm(ctx, key, line, MemberBound); err != nil {
|
||||
return nil, fmt.Sprintf("the package member %s is not built, and the bound of the wait cannot be written: %v", strings.Join(pending, ", "), err), false
|
||||
}
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.memberWaits[key] = true
|
||||
b.mu.Unlock()
|
||||
b.logf("%s", line)
|
||||
if b.Log != nil {
|
||||
b.Log(builds.BuildLogEntry{Event: builds.BuildSaidEvent, Module: module, FP: m.Fp2, Commit: m.Commit, Node: b.Node, At: b.now().UTC(), Line: line, Why: "wait-held"})
|
||||
}
|
||||
}
|
||||
return nil, "", true
|
||||
}
|
||||
|
||||
// memberEnded looks at the candidates again once the build of key ended, when it is a package member of a dependent of a group's
|
||||
// run: a dependent that read it as not built just before this end is not waiting yet, so the look cannot rest on the waits.
|
||||
func (b *Builder) memberEnded(ctx context.Context, key string) {
|
||||
b.mu.Lock()
|
||||
member := false
|
||||
for _, v := range b.via {
|
||||
for _, mem := range v.Members {
|
||||
if mk, err := builds.BuilderBuildKey(mem.Module, mem.Fp); err == nil && mk == key {
|
||||
member = true
|
||||
}
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if member {
|
||||
b.TrunkMoved(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// fetchedBuilds are the builds key's module is fetched against (memberBuilds), nil for none.
|
||||
func (b *Builder) fetchedBuilds(key string) []buildstep.FetchBuild {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.fetched[key]
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ type ClaimOptions struct {
|
||||
// ImageBuilder says whether a node runs a healthy image build module now: a key failed for NoImageBuilderPrefix is taken
|
||||
// again only then. Nil means none does.
|
||||
ImageBuilder func() bool
|
||||
// Seats finds the build modules of the artifacts a done key holds unpublished (design 47 G A-82).
|
||||
Seats *buildrun.BuildSeats
|
||||
// Trunk is whether the trunk's record names this fingerprint. A done key whose image or package has no reference was
|
||||
// built as a candidate's, which never pushes or publishes (ADR 0487 clause 12, ADR 0498 clauses 5 and 6); on the trunk it
|
||||
// is taken again to publish them, an image only when an image builder runs (ImageBuilder).
|
||||
@@ -96,11 +98,13 @@ func GoneStep(gone []string) string {
|
||||
|
||||
// Unpublished is whether a done build holds an image built without a push or a package built without a publish: it has
|
||||
// no reference (a candidate's, ADR 0487 clause 12, ADR 0498 clauses 5 and 6).
|
||||
func Unpublished(b builds.BuilderBuild) bool { return len(UnpublishedModules(b)) > 0 }
|
||||
func Unpublished(b builds.BuilderBuild, seats *buildrun.BuildSeats) bool {
|
||||
return len(UnpublishedModules(b, seats)) > 0
|
||||
}
|
||||
|
||||
// UnpublishedModules are the build modules that publish what a done build holds unpublished: the image build module for
|
||||
// an image, the Node build module for a package; sorted, none when it holds nothing unpublished.
|
||||
func UnpublishedModules(b builds.BuilderBuild) []string {
|
||||
func UnpublishedModules(b builds.BuilderBuild, seats *buildrun.BuildSeats) []string {
|
||||
if b.State != builds.BuildDone {
|
||||
return nil
|
||||
}
|
||||
@@ -111,10 +115,14 @@ func UnpublishedModules(b builds.BuilderBuild) []string {
|
||||
}
|
||||
var out []string
|
||||
if image {
|
||||
out = append(out, ImageBuildModule)
|
||||
if bm, err := seats.For("", "image"); err == nil {
|
||||
out = append(out, bm.Module)
|
||||
}
|
||||
}
|
||||
if pkg {
|
||||
out = append(out, buildrun.NodeBuildModule)
|
||||
if bm, err := seats.For("typescript", catalogue.ArtifactPackage); err == nil {
|
||||
out = append(out, bm.Module)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
@@ -122,10 +130,10 @@ func UnpublishedModules(b builds.BuilderBuild) []string {
|
||||
|
||||
// publishable are the build modules of UnpublishedModules that can publish now: the image build module only while a node
|
||||
// runs it (imageBuilder).
|
||||
func publishable(b builds.BuilderBuild, imageBuilder func() bool) []string {
|
||||
func publishable(b builds.BuilderBuild, seats *buildrun.BuildSeats, imageBuilder func() bool) []string {
|
||||
var out []string
|
||||
for _, bm := range UnpublishedModules(b) {
|
||||
if bm != ImageBuildModule || (imageBuilder != nil && imageBuilder()) {
|
||||
for _, bm := range UnpublishedModules(b, seats) {
|
||||
if seat, _ := seats.OfModule(bm); !seat.ByKind || (imageBuilder != nil && imageBuilder()) {
|
||||
out = append(out, bm)
|
||||
}
|
||||
}
|
||||
@@ -194,7 +202,7 @@ func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s: whether the artifact store still holds %s cannot be asked, so the key is "+
|
||||
"left done (the controller's take-in refuses it if one is gone): %w", key, strings.Join(refs, ", "), err)
|
||||
if len(publishable(cur, opt.ImageBuilder)) > 0 {
|
||||
if len(publishable(cur, opt.Seats, opt.ImageBuilder)) > 0 {
|
||||
return &Claim{Key: key, Rev: rev, Build: cur, Unasked: err}, nil
|
||||
}
|
||||
return nil, err
|
||||
@@ -210,7 +218,7 @@ func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string
|
||||
}
|
||||
}
|
||||
}
|
||||
if opt.Trunk && len(publishable(cur, opt.ImageBuilder)) > 0 {
|
||||
if opt.Trunk && len(publishable(cur, opt.Seats, opt.ImageBuilder)) > 0 {
|
||||
return &Claim{Key: key, Rev: rev, Build: cur}, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
)
|
||||
|
||||
// TestAnEntryOfTheCatalogHalfIsTheFormTheBuilderBuildsBy: a catalog graph entry gives the builder its fingerprint, manifest digest,
|
||||
// source, the toolchains its fingerprint was computed with, its bases, packages, needs and the named paths of its artifacts by path
|
||||
// (design 47 G §348, §614).
|
||||
func TestAnEntryOfTheCatalogHalfIsTheFormTheBuilderBuildsBy(t *testing.T) {
|
||||
e := meshcatalog.CatalogEntry{Module: "netcheck", Repository: "novox/mesh-catalog", Path: "modules/netcheck", Commit: "abc", Fp: "src2:f",
|
||||
ManifestDigest: "sha256:m", Bases: []string{"base"}, Packages: map[string]string{"p": "1"}, Needs: []string{"n"},
|
||||
Inputs: &meshcatalog.EntryInputs{Src: 7, Toolchains: map[string]meshcatalog.ToolchainUsed{"go": {Version: "go1", Recipe: "1"}},
|
||||
Paths: map[string][]string{"lib": {"a/b", "c"}}}}
|
||||
got := ModuleOf(e)
|
||||
if got.Fp2 != "src2:f" || got.Manifest != "sha256:m" || got.Path != "modules/netcheck" || got.Commit != "abc" || got.Src != 7 ||
|
||||
got.Toolchains["go"].Version != "go1" || got.Packages["p"] != "1" || !reflect.DeepEqual(got.Needs, []string{"n"}) ||
|
||||
got.Inputs2 == nil || !reflect.DeepEqual(got.Inputs2.Bases, []string{"base"}) ||
|
||||
!reflect.DeepEqual(got.Inputs2.Paths, map[string]string{"a/b": "lib", "c": "lib"}) {
|
||||
t.Errorf("ModuleOf = %+v", got)
|
||||
}
|
||||
if bare := ModuleOf(meshcatalog.CatalogEntry{Module: "m"}); bare.Inputs2 != nil || bare.Fp2 != "" {
|
||||
t.Errorf("ModuleOf of an entry with none = %+v, want nothing set", bare)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
const memberPackage = "example.org/p"
|
||||
|
||||
var fpP = "src2:" + hex64('2')
|
||||
|
||||
// groupValue is a group's candidate: the module m, built against the head of the package member p, and p.
|
||||
func groupValue(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: headX, Modules: []string{"m", "p"},
|
||||
Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{
|
||||
"m": {Fp2: fp1, Manifest: mdigest('a'), Packages: map[string]string{memberPackage: headY, "example.org/q": "v1.2.3"}},
|
||||
"p": {Fp2: fpP, Manifest: mdigest('b')}}}}
|
||||
hash, err := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: 7, Head: headX}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// memberRig is a rig whose group changes m and its package member p (artifact lib); p has a trunk record when known.
|
||||
func memberRig(t *testing.T, known bool) *rig {
|
||||
t.Helper()
|
||||
r := newRig(t, goBundle)
|
||||
lib := catalogue.Artifact{Name: "lib", Kind: "bundle", Language: "go"}
|
||||
r.mf.byDigest[mdigest('b')] = catalogue.Manifest{Module: "p", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{lib}}}
|
||||
r.b.Members = func(_ context.Context, pkg string) (string, string, bool, error) {
|
||||
if pkg == memberPackage {
|
||||
return "p", "lib", true, nil
|
||||
}
|
||||
return "", "", false, nil
|
||||
}
|
||||
trunkModule(t, r)
|
||||
if known {
|
||||
data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: trunkHead, Fp2: "src2:" + hex64('d'),
|
||||
Manifest: mdigest('b'), Path: "modules/p", Toolchains: CatalogToolchains(agreedToolchains)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.b.Module(context.Background(), "p", data)
|
||||
r.b.Wait()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestAGroupRunBuildsADependentAgainstItsPackageMembersBuildByDigest(t *testing.T) { // design 47 G §373, §465; test 103
|
||||
t.Run("the member is fetched by its build's digest, and no package of it is fetched by version", func(t *testing.T) {
|
||||
r := memberRig(t, true)
|
||||
r.b.Candidate(context.Background(), "feat%2Fx", groupValue(t))
|
||||
r.b.Wait()
|
||||
if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone {
|
||||
t.Fatalf("m = %+v, want built", v)
|
||||
}
|
||||
var found bool
|
||||
for _, c := range r.bm.callsOf("build_go_fetch") {
|
||||
f := c.body.(map[string]any)
|
||||
bs, _ := f["builds"].([]any)
|
||||
if len(bs) == 0 {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
want := map[string]any{"package": memberPackage, "module": "p", "artifact": "lib", "digest": digest}
|
||||
if len(bs) != 1 || !jsonEqual(bs[0], want) {
|
||||
t.Errorf("builds = %v, want %v", bs, want)
|
||||
}
|
||||
pk, _ := f["packages"].(map[string]any)
|
||||
if _, named := pk[memberPackage]; named || pk["example.org/q"] != "v1.2.3" {
|
||||
t.Errorf("packages = %v, want the member out of it and the other package kept", pk)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("no fetch named the member's build")
|
||||
}
|
||||
})
|
||||
t.Run("a member not built holds the dependent with wait-held naming it, and past the bound fails it", func(t *testing.T) {
|
||||
r := memberRig(t, false)
|
||||
var mu sync.Mutex
|
||||
bounds := &fakeMemberBounds{}
|
||||
r.b.MemberBounds = bounds
|
||||
var said []builds.BuildLogEntry
|
||||
r.b.Log = func(e builds.BuildLogEntry) { mu.Lock(); said = append(said, e); mu.Unlock() }
|
||||
r.b.Candidate(context.Background(), "feat%2Fx", groupValue(t))
|
||||
r.b.Wait()
|
||||
if _, written := r.st.vals[r.key]; written || len(r.bm.callsOf("build_go_fetch")) != 0 {
|
||||
t.Fatal("the dependent was claimed or built while its package member is not built")
|
||||
}
|
||||
mu.Lock()
|
||||
if len(said) != 1 || said[0].Why != "wait-held" || !strings.Contains(said[0].Line, "p/lib") {
|
||||
t.Errorf("said = %+v, want one wait-held naming p/lib", said)
|
||||
}
|
||||
mu.Unlock()
|
||||
if got := bounds.armed(); len(got) != 1 || got[0] != r.key+" "+MemberBound.String() {
|
||||
t.Fatalf("bounds armed = %v, want one of 60 minutes on the dependent's key", got)
|
||||
}
|
||||
r.b.MemberBoundEnded(context.Background(), r.key) // the bus's marker of the bound's expiry
|
||||
r.b.Wait()
|
||||
if got := bounds.ended(); len(got) != 1 || got[0] != r.key {
|
||||
t.Errorf("bounds ended = %v, want the wait's bound deleted", got)
|
||||
}
|
||||
if v := valueOf(t, r.st, r.key); v.State != builds.BuildFailed || !strings.Contains(v.Error, "p/lib") || len(r.bm.callsOf("build_go_fetch")) != 0 {
|
||||
t.Errorf("m = %+v, want failed naming the member and nothing built", v)
|
||||
}
|
||||
})
|
||||
t.Run("a member whose build failed fails the dependent's step naming it", func(t *testing.T) {
|
||||
r := memberRig(t, false)
|
||||
mk, _ := builds.BuilderBuildKey("p", fpP)
|
||||
data, err := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildFailed, Error: "boom"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.st.put(mk, data)
|
||||
r.b.Candidate(context.Background(), "feat%2Fx", groupValue(t))
|
||||
r.b.Wait()
|
||||
if v := valueOf(t, r.st, r.key); v.State != builds.BuildFailed || !strings.Contains(v.Error, "p/lib") || !strings.Contains(v.Error, "boom") || len(r.bm.callsOf("build_go_fetch")) != 0 {
|
||||
t.Errorf("m = %+v, want failed naming the member and its error", v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// fakeMemberBounds keeps the bounds a builder arms and ends.
|
||||
type fakeMemberBounds struct {
|
||||
mu sync.Mutex
|
||||
arms, ends_ []string
|
||||
}
|
||||
|
||||
func (f *fakeMemberBounds) Arm(_ context.Context, key, _ string, bound time.Duration) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.arms = append(f.arms, key+" "+bound.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeMemberBounds) End(_ context.Context, key string) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.ends_ = append(f.ends_, key)
|
||||
}
|
||||
|
||||
func (f *fakeMemberBounds) armed() []string { f.mu.Lock(); defer f.mu.Unlock(); return f.arms }
|
||||
func (f *fakeMemberBounds) ended() []string { f.mu.Lock(); defer f.mu.Unlock(); return f.ends_ }
|
||||
|
||||
func jsonEqual(a, b any) bool {
|
||||
x, _ := json.Marshal(a)
|
||||
y, _ := json.Marshal(b)
|
||||
return string(x) == string(y)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package build
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -135,8 +136,9 @@ type Reservations interface {
|
||||
Filled() bool
|
||||
// Reserve reserves module's next version in series for the artifacts (their references, <module>:<artifact>).
|
||||
Reserve(ctx context.Context, module, series string, artifacts []string, src versions.Source) (versions.ReservedVersion, error)
|
||||
// Built records the digests of the artifacts built under the reserved version, by reference.
|
||||
Built(ctx context.Context, module, version string, digests map[string]string) error
|
||||
// Built records the digests of the artifacts built under the reserved version, by reference, and what each resolved and was
|
||||
// built with (design 47 G §466, §673).
|
||||
Built(ctx context.Context, module, version string, digests map[string]string, deps map[string][]meshbuilds.Dependency) error
|
||||
}
|
||||
|
||||
// reserves says whether a look at a trunk build is one that reserves: the build has a series of the form MAJOR.MINOR
|
||||
@@ -172,12 +174,12 @@ func (b *Builder) waitFill(module string, m meshcatalog.ModulesByModuleValue) bo
|
||||
|
||||
// reserve reserves the version of the build that starts; a reservation that cannot be made is said and the build goes on
|
||||
// unreserved, as an unnumbered build does.
|
||||
func (b *Builder) reserve(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan) (versions.ReservedVersion, bool) {
|
||||
func (b *Builder) reserve(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, o Origin) (versions.ReservedVersion, bool) {
|
||||
refs := make([]string, 0, len(plan.Tasks))
|
||||
for _, t := range plan.Tasks {
|
||||
refs = append(refs, module+":"+t.Artifact)
|
||||
}
|
||||
src := versions.Source{Repository: m.Repository, Commit: m.Commit, Fingerprint: m.Fp2}
|
||||
src := versions.Source{Repository: m.Repository, Commit: m.Commit, Fingerprint: m.Fp2, Branch: o.Branch, Revision: o.Source}
|
||||
for _, t := range plan.Tasks {
|
||||
src.GoPackage = src.GoPackage || (t.Kind == catalogue.ArtifactPackage && t.Language == "go")
|
||||
}
|
||||
|
||||
@@ -13,11 +13,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// ImageBuildModule is the build module that builds an image artifact (novox/hq ADR 0487): buildah, rootless, on the nodes that
|
||||
// run it. It has no toolchain language; the builder finds its nodes from the node-engine's have.
|
||||
const ImageBuildModule = "mesh-build-image"
|
||||
|
||||
// NoImageBuilderPrefix opens the error of a build key failed because no node runs ImageBuildModule (ADR 0487 clause 5): such a
|
||||
// NoImageBuilderPrefix opens the error of a build key failed because no node runs the image build module (ADR 0487 clause 5): such a
|
||||
// key is not built again as a new revision, only when a node reports the module (ClaimOptions.ImageBuilder) or the fingerprint
|
||||
// changes.
|
||||
const NoImageBuilderPrefix = "no image builder:"
|
||||
@@ -82,34 +78,55 @@ func (n *NotPinned) Error() string {
|
||||
return fmt.Sprintf("%s%s: %q names no digest (@sha256:), so what would be copied is whatever its tag means today", builds.NotPinned, n.Artifact, n.From)
|
||||
}
|
||||
|
||||
// NoBuildModule is the error of an artifact whose build seat or its holder the seat list and the catalog half do not name (yet):
|
||||
// the plan is not kept, so it is made again when they are read (design 47 G A-82).
|
||||
type NoBuildModule struct {
|
||||
Artifact string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (n *NoBuildModule) Error() string { return fmt.Sprintf("artifact %s: %v", n.Artifact, n.Err) }
|
||||
func (n *NoBuildModule) Unwrap() error { return n.Err }
|
||||
|
||||
// PlanOf is the plan of a manifest (ADR 0478 clauses 1 and 2, ADR 0498 clauses 4 and 5): a Go bundle and an archive are
|
||||
// built by the Go build module, an image by the image build module (ADR 0487), which also copies an upstream image pinned
|
||||
// by its digest, and a TypeScript bundle or package by the Node one, each in the order of the artifacts' names. A manifest
|
||||
// with an upstream artifact pinned by no digest, or a bundle or a package of another language, or a kind no build module
|
||||
// builds, is refused as a whole, naming the first such artifact by name. A manifest with no artifact has an empty plan.
|
||||
func PlanOf(m catalogue.Manifest) (Plan, error) {
|
||||
func PlanOf(m catalogue.Manifest, seats *buildrun.BuildSeats) (Plan, error) {
|
||||
if m.Build == nil {
|
||||
return Plan{}, nil
|
||||
}
|
||||
artifacts := append([]catalogue.Artifact(nil), m.Build.Artifacts...)
|
||||
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
|
||||
p := Plan{Series: m.Version}
|
||||
task := func(a catalogue.Artifact, language, kind string) error {
|
||||
bm, err := seats.For(language, kind)
|
||||
if err != nil {
|
||||
return &NoBuildModule{Artifact: a.Name, Err: err}
|
||||
}
|
||||
t := Task{Artifact: a.Name, Kind: a.Kind, Module: bm.Module, Language: bm.Toolchain, From: a.From}
|
||||
if a.Kind != catalogue.ArtifactUpstream {
|
||||
t.From = ""
|
||||
}
|
||||
p.Tasks = append(p.Tasks, t)
|
||||
return nil
|
||||
}
|
||||
for _, a := range artifacts {
|
||||
var err error
|
||||
switch {
|
||||
case a.Kind == catalogue.ArtifactArchive:
|
||||
p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: buildrun.GoBuildModule, Language: "go"})
|
||||
case a.Kind == catalogue.ArtifactBundle && a.Language == "go":
|
||||
p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: buildrun.GoBuildModule, Language: "go"})
|
||||
case a.Kind == catalogue.ArtifactBundle && a.Language == "typescript":
|
||||
p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: buildrun.NodeBuildModule, Language: "typescript"})
|
||||
err = task(a, "go", a.Kind) // an archive is packed by the Go build seat's holder, which the indexer gives no language
|
||||
case a.Kind == catalogue.ArtifactBundle && (a.Language == "go" || a.Language == "typescript"):
|
||||
err = task(a, a.Language, a.Kind)
|
||||
case a.Kind == catalogue.ArtifactImage:
|
||||
p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: ImageBuildModule})
|
||||
err = task(a, "", "image")
|
||||
case a.Kind == catalogue.ArtifactUpstream && !strings.Contains(a.From, "@sha256:"):
|
||||
return Plan{}, &NotPinned{Artifact: a.Name, From: a.From}
|
||||
case a.Kind == catalogue.ArtifactUpstream:
|
||||
p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: ImageBuildModule, From: a.From})
|
||||
err = task(a, "", "image")
|
||||
case a.Kind == catalogue.ArtifactPackage && a.Language == "typescript":
|
||||
p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: buildrun.NodeBuildModule, Language: "typescript"})
|
||||
err = task(a, a.Language, a.Kind)
|
||||
case a.Kind == catalogue.ArtifactBundle:
|
||||
return Plan{}, &Refused{Artifact: a.Name, What: fmt.Sprintf("bundle of language %q", a.Language)}
|
||||
case a.Kind == catalogue.ArtifactPackage:
|
||||
@@ -117,6 +134,9 @@ func PlanOf(m catalogue.Manifest) (Plan, error) {
|
||||
default:
|
||||
return Plan{}, &Refused{Artifact: a.Name, What: a.Kind}
|
||||
}
|
||||
if err != nil {
|
||||
return Plan{}, err
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// manifestOf is a manifest holding these artifacts: the plan reads only their name, kind and language.
|
||||
@@ -22,14 +23,14 @@ func TestPlanRoutesEachKindToItsBuildModule(t *testing.T) { // 1607/AC2
|
||||
catalogue.Artifact{Name: "tool", Kind: "bundle", Language: "go"},
|
||||
catalogue.Artifact{Name: "web", Kind: "bundle", Language: "typescript"},
|
||||
catalogue.Artifact{Name: "files", Kind: "archive"})
|
||||
p, err := PlanOf(m)
|
||||
p, err := PlanOf(m, testSeats())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []Task{
|
||||
{Artifact: "files", Kind: "archive", Module: buildrun.GoBuildModule, Language: "go"},
|
||||
{Artifact: "tool", Kind: "bundle", Module: buildrun.GoBuildModule, Language: "go"},
|
||||
{Artifact: "web", Kind: "bundle", Module: buildrun.NodeBuildModule, Language: "typescript"},
|
||||
{Artifact: "files", Kind: "archive", Module: testGo, Language: "go"},
|
||||
{Artifact: "tool", Kind: "bundle", Module: testGo, Language: "go"},
|
||||
{Artifact: "web", Kind: "bundle", Module: testNode, Language: "typescript"},
|
||||
}
|
||||
if len(p.Tasks) != len(want) {
|
||||
t.Fatalf("tasks = %+v, want %+v", p.Tasks, want)
|
||||
@@ -39,7 +40,7 @@ func TestPlanRoutesEachKindToItsBuildModule(t *testing.T) { // 1607/AC2
|
||||
t.Errorf("task %d = %+v, want %+v", i, p.Tasks[i], want[i])
|
||||
}
|
||||
}
|
||||
if modules := p.Modules(); len(modules) != 2 || modules[0] != buildrun.GoBuildModule || modules[1] != buildrun.NodeBuildModule {
|
||||
if modules := p.Modules(); len(modules) != 2 || modules[0] != testGo || modules[1] != testNode {
|
||||
t.Errorf("modules = %v", modules)
|
||||
}
|
||||
}
|
||||
@@ -47,24 +48,24 @@ func TestPlanRoutesEachKindToItsBuildModule(t *testing.T) { // 1607/AC2
|
||||
const pinned = "nats:2.11.17-alpine@sha256:3333333333333333333333333333333333333333333333333333333333333333"
|
||||
|
||||
func TestPlanSendsAPinnedUpstreamToTheImageBuildModulesCopyAndRefusesAnUnpinnedOne(t *testing.T) { // B3, ADR 0498 clause 4
|
||||
p, err := PlanOf(manifestOf(catalogue.Artifact{Name: "ok", Kind: "archive"}, catalogue.Artifact{Name: "up", Kind: "upstream", From: pinned}))
|
||||
p, err := PlanOf(manifestOf(catalogue.Artifact{Name: "ok", Kind: "archive"}, catalogue.Artifact{Name: "up", Kind: "upstream", From: pinned}), testSeats())
|
||||
if err != nil || len(p.Tasks) != 2 {
|
||||
t.Fatalf("plan %+v, error %v", p, err)
|
||||
}
|
||||
if up := p.Tasks[1]; up.Artifact != "up" || up.Kind != "upstream" || up.Module != ImageBuildModule || up.From != pinned || up.Language != "" {
|
||||
t.Errorf("task = %+v, want the upstream sent to %s with its pinned reference and no toolchain", up, ImageBuildModule)
|
||||
if up := p.Tasks[1]; up.Artifact != "up" || up.Kind != "upstream" || up.Module != testImage || up.From != pinned || up.Language != "" {
|
||||
t.Errorf("task = %+v, want the upstream sent to %s with its pinned reference and no toolchain", up, testImage)
|
||||
}
|
||||
if p.NeedsToolchain(ImageBuildModule) {
|
||||
if p.NeedsToolchain(testImage) {
|
||||
t.Error("a copy needs no toolchain")
|
||||
}
|
||||
_, err = PlanOf(manifestOf(catalogue.Artifact{Name: "up", Kind: "upstream", From: "nats:2.11.17-alpine"}))
|
||||
_, err = PlanOf(manifestOf(catalogue.Artifact{Name: "up", Kind: "upstream", From: "nats:2.11.17-alpine"}), testSeats())
|
||||
if err == nil || !strings.HasPrefix(err.Error(), builds.NotPinned+"up") || !builds.FailedUntilChanged(err.Error()) {
|
||||
t.Errorf("an upstream with no digest: error = %v, want it to open %q naming the artifact", err, builds.NotPinned)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanRefusesAPackageOfAnotherLanguageByName(t *testing.T) { // ADR 0498 clause 5
|
||||
_, err := PlanOf(manifestOf(catalogue.Artifact{Name: "ok", Kind: "archive"}, catalogue.Artifact{Name: "pkg", Kind: "package", Language: "python"}))
|
||||
_, err := PlanOf(manifestOf(catalogue.Artifact{Name: "ok", Kind: "archive"}, catalogue.Artifact{Name: "pkg", Kind: "package", Language: "python"}), testSeats())
|
||||
var r *Refused
|
||||
if err == nil || !asRefused(err, &r) || !strings.HasPrefix(err.Error(), builds.RefusedKind) || !strings.Contains(err.Error(), "python") || !strings.Contains(err.Error(), "pkg") {
|
||||
t.Errorf("error = %v, want it to open %q naming the artifact and its language", err, builds.RefusedKind)
|
||||
@@ -72,10 +73,10 @@ func TestPlanRefusesAPackageOfAnotherLanguageByName(t *testing.T) { // ADR 0498
|
||||
}
|
||||
|
||||
func TestPlanRefusesABundleOfAnotherLanguageAndAModuleWithNoArtifact(t *testing.T) {
|
||||
if _, err := PlanOf(manifestOf(catalogue.Artifact{Name: "x", Kind: "bundle", Language: "python"})); err == nil || !strings.HasPrefix(err.Error(), builds.RefusedKind) {
|
||||
if _, err := PlanOf(manifestOf(catalogue.Artifact{Name: "x", Kind: "bundle", Language: "python"}), testSeats()); err == nil || !strings.HasPrefix(err.Error(), builds.RefusedKind) {
|
||||
t.Errorf("a python bundle: error = %v, want a refusal", err)
|
||||
}
|
||||
p, err := PlanOf(manifestOf())
|
||||
p, err := PlanOf(manifestOf(), testSeats())
|
||||
if err != nil || len(p.Tasks) != 0 {
|
||||
t.Errorf("no artifact: plan %+v error %v, want an empty plan", p, err)
|
||||
}
|
||||
@@ -84,14 +85,36 @@ func TestPlanRefusesABundleOfAnotherLanguageAndAModuleWithNoArtifact(t *testing.
|
||||
func asRefused(err error, target **Refused) bool { return errors.As(err, target) }
|
||||
|
||||
func TestPlanSendsAnImageToMeshBuildImageWithNoToolchain(t *testing.T) { // 1705a, ADR 0487 clause 5
|
||||
p, err := PlanOf(manifestOf(catalogue.Artifact{Name: "img", Kind: "image"}, catalogue.Artifact{Name: "files", Kind: "archive"}))
|
||||
p, err := PlanOf(manifestOf(catalogue.Artifact{Name: "img", Kind: "image"}, catalogue.Artifact{Name: "files", Kind: "archive"}), testSeats())
|
||||
if err != nil || len(p.Tasks) != 2 {
|
||||
t.Fatalf("plan %+v, error %v", p, err)
|
||||
}
|
||||
if p.Tasks[1].Artifact != "img" || p.Tasks[1].Module != ImageBuildModule {
|
||||
t.Errorf("task = %+v, want the image on %s", p.Tasks[1], ImageBuildModule)
|
||||
if p.Tasks[1].Artifact != "img" || p.Tasks[1].Module != testImage {
|
||||
t.Errorf("task = %+v, want the image on %s", p.Tasks[1], testImage)
|
||||
}
|
||||
if p.NeedsToolchain(ImageBuildModule) {
|
||||
if p.NeedsToolchain(testImage) {
|
||||
t.Error("an image needs no toolchain")
|
||||
}
|
||||
}
|
||||
|
||||
// A plan's task names the holder of the build seat the seat list and the catalog half give, and a plan made before they name one
|
||||
// is no refusal and is not kept (design 47 G A-82).
|
||||
func TestAPlanNamesTheHoldersOfTheBuildSeats(t *testing.T) {
|
||||
m := manifestOf(catalogue.Artifact{Name: "tool", Kind: "bundle", Language: "go"}, catalogue.Artifact{Name: "img", Kind: "image"})
|
||||
defs := map[string]seatdef.Definition{
|
||||
"node-build-go": {Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: "go"}}},
|
||||
"node-build-image": {Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: seatdef.OnKindImage}}},
|
||||
}
|
||||
holders := map[string]string{}
|
||||
seats := buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(s string) (string, bool) { h, ok := holders[s]; return h, ok })
|
||||
_, err := PlanOf(m, seats)
|
||||
var none *NoBuildModule
|
||||
if !errors.As(err, &none) {
|
||||
t.Fatalf("with no holder: %v, want NoBuildModule", err)
|
||||
}
|
||||
holders["node-build-go"], holders["node-build-image"] = "acme-go", "acme-img"
|
||||
p, err := PlanOf(m, seats)
|
||||
if err != nil || len(p.Tasks) != 2 || p.Tasks[0].Module != "acme-img" || p.Tasks[1].Module != "acme-go" || p.Tasks[1].Language != "go" || p.Tasks[0].Language != "" {
|
||||
t.Fatalf("plan = %+v, %v", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@ package build
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
@@ -21,16 +25,19 @@ type fakeReservations struct {
|
||||
reserved [][]string
|
||||
series []string
|
||||
built map[string]string
|
||||
sources []versions.Source
|
||||
deps map[string][]meshbuilds.Dependency
|
||||
}
|
||||
|
||||
func (f *fakeReservations) Filled() bool { return f.filled }
|
||||
|
||||
func (f *fakeReservations) Reserve(_ context.Context, module, series string, artifacts []string, _ versions.Source) (versions.ReservedVersion, error) {
|
||||
f.reserved, f.series = append(f.reserved, artifacts), append(f.series, series)
|
||||
func (f *fakeReservations) Reserve(_ context.Context, module, series string, artifacts []string, src versions.Source) (versions.ReservedVersion, error) {
|
||||
f.reserved, f.series, f.sources = append(f.reserved, artifacts), append(f.series, series), append(f.sources, src)
|
||||
return versions.ReservedVersion{Module: module, Series: series, Version: series + ".5"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeReservations) Built(_ context.Context, _, version string, digests map[string]string) error {
|
||||
func (f *fakeReservations) Built(_ context.Context, _, version string, digests map[string]string, deps map[string][]meshbuilds.Dependency) error {
|
||||
f.deps = deps
|
||||
f.built = map[string]string{version: ""}
|
||||
for ref, d := range digests {
|
||||
f.built[ref] = d
|
||||
@@ -127,3 +134,73 @@ func TestADropOfAReservationIsAReasksValueOfItsOwnKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnlyABuildOfTheFirstWatchedBranchReservesAndItCarriesItsCatalogRevision: design 47 G §371, A-57 — a build of the registration's
|
||||
// first watched branch reserves a version and the reservation carries the branch and the source revision of the catalog revision its
|
||||
// entry came from; a build of another watched branch reserves none and is built and done.
|
||||
func TestOnlyABuildOfTheFirstWatchedBranchReservesAndItCarriesItsCatalogRevision(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
origin Origin
|
||||
want []versions.Source
|
||||
}{
|
||||
{"the first branch", Origin{Branch: "main", Source: 41, First: true},
|
||||
[]versions.Source{{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Branch: "main", Revision: 41}}},
|
||||
{"another branch", Origin{Branch: "beta", Source: 42}, nil},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
r := newRig(t, goBundle)
|
||||
r.mf.byDigest[mdigest('a')] = catalogue.Manifest{Module: "m", Version: "0.1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{goBundle}}}
|
||||
res := &fakeReservations{filled: true}
|
||||
r.b.Reserved = res
|
||||
r.b.Entry(context.Background(), c.origin, meshcatalog.CatalogEntry{Module: "m", Repository: "novox/mesh-catalog", Path: "modules/m",
|
||||
Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: fp1, ManifestDigest: mdigest('a'), Inputs: &meshcatalog.EntryInputs{Toolchains: CatalogToolchains(agreedToolchains)}})
|
||||
r.b.Wait()
|
||||
if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone {
|
||||
t.Fatalf("build = %+v, want done", v)
|
||||
}
|
||||
if !reflect.DeepEqual(res.sources, c.want) {
|
||||
t.Errorf("reserved from %+v, want %+v", res.sources, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAReservedBuildKeepsWhatItsStepResolvedAndWasBuiltWith: design 47 G §466, §673, A-57 — the `resolved` of each artifact and the
|
||||
// toolchain digest of the step's result are kept on the reservation as dependencies of kind 1 and 3, their parts the artifact
|
||||
// references the entry's edges name; a package no edge names is left out and said.
|
||||
func TestAReservedBuildKeepsWhatItsStepResolvedAndWasBuiltWith(t *testing.T) {
|
||||
r := newRig(t, goBundle)
|
||||
r.mf.byDigest[mdigest('a')] = catalogue.Manifest{Module: "m", Version: "0.1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{goBundle}}}
|
||||
res := &fakeReservations{filled: true}
|
||||
r.b.Reserved = res
|
||||
var journal []string
|
||||
r.b.Logf = func(f string, a ...any) { journal = append(journal, fmt.Sprintf(f, a...)) }
|
||||
r.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
|
||||
st := r.answer(a)
|
||||
var out buildstep.BuildAnswer
|
||||
if err := json.Unmarshal(st.Result, &out); err != nil || len(out.Artifacts) == 0 {
|
||||
return st
|
||||
}
|
||||
out.Artifacts[0].Resolved = []buildstep.AskResolved{{Package: "git.novox.be/novox/mesh-sdk/go", Version: "0.1.14", Digest: "sha256:11"},
|
||||
{Package: "unnamed/pkg", Version: "1.0.0", Digest: "sha256:22"}}
|
||||
out.Toolchain = &buildstep.ToolchainUsed{Language: "go", Version: "go1.27.1", Recipe: "1", Digest: "sha256:tc"}
|
||||
st.Result, _ = json.Marshal(out)
|
||||
return st
|
||||
}
|
||||
r.b.Entry(context.Background(), Origin{Branch: "main", Source: 41, First: true}, meshcatalog.CatalogEntry{Module: "m", Repository: "novox/mesh-catalog",
|
||||
Path: "modules/m", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: fp1, ManifestDigest: mdigest('a'),
|
||||
Inputs: &meshcatalog.EntryInputs{Toolchains: CatalogToolchains(agreedToolchains)},
|
||||
Edges: []meshcatalog.Edge{{Kind: 1, From: "mesh-sdk:go", To: "m:tool", Why: "git.novox.be/novox/mesh-sdk/go"}, {Kind: 3, From: "go-toolchain:program", To: "m:tool"}}})
|
||||
r.b.Wait()
|
||||
want := map[string][]meshbuilds.Dependency{"m:tool": {{Kind: 1, Part: "mesh-sdk:go", Version: "0.1.14", Digest: "sha256:11"},
|
||||
{Kind: 3, Part: "go-toolchain:program", Version: "go1.27.1", Digest: "sha256:tc"}}}
|
||||
if !reflect.DeepEqual(res.deps, want) {
|
||||
t.Errorf("dependencies = %+v, want %+v", res.deps, want)
|
||||
}
|
||||
if !slices.ContainsFunc(journal, func(l string) bool {
|
||||
return strings.Contains(l, "unnamed/pkg") && strings.Contains(l, "names no artifact")
|
||||
}) {
|
||||
t.Errorf("journal %q, want the package no edge names said", journal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,12 +42,12 @@ func TestAnArchiveOnlyModuleIsBuiltAlthoughItsFingerprintNamesNoToolchain(t *tes
|
||||
}
|
||||
|
||||
func TestABundleAndAnArchiveOfOneBuildModuleStillNeedTheAgreedToolchain(t *testing.T) { // review round 4, 1
|
||||
p, err := PlanOf(catalogue.Manifest{Module: "m", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{goBundle, archiveA}}})
|
||||
if err != nil || !p.NeedsToolchain(buildrun.GoBuildModule) {
|
||||
t.Errorf("a plan with a Go bundle: %v, %v", p.NeedsToolchain(buildrun.GoBuildModule), err)
|
||||
p, err := PlanOf(catalogue.Manifest{Module: "m", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{goBundle, archiveA}}}, testSeats())
|
||||
if err != nil || !p.NeedsToolchain(testGo) {
|
||||
t.Errorf("a plan with a Go bundle: %v, %v", p.NeedsToolchain(testGo), err)
|
||||
}
|
||||
p, _ = PlanOf(catalogue.Manifest{Module: "m", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{archiveA}}})
|
||||
if p.NeedsToolchain(buildrun.GoBuildModule) {
|
||||
p, _ = PlanOf(catalogue.Manifest{Module: "m", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{archiveA}}}, testSeats())
|
||||
if p.NeedsToolchain(testGo) {
|
||||
t.Error("a plan of an archive needs a toolchain")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
@@ -39,6 +41,10 @@ type ToolchainEntries interface {
|
||||
// change of the toolchains.
|
||||
type Builder struct {
|
||||
Claimer Claimer
|
||||
// StepEntry reads the entry of a fingerprint through the seat mesh-catalog's entry (design 47 G A-84, A-97).
|
||||
StepEntry func(ctx context.Context, fingerprint string) (meshcatalog.CatalogEntry, error)
|
||||
// Seats finds the build modules: the holders of the build seats on the seat list and the catalog half (design 47 G A-82).
|
||||
Seats *buildrun.BuildSeats
|
||||
// Asks puts each build module's part of a build as one ask on its build seat's stream and waits for its step (novox/hq
|
||||
// ADR 0525 §1, §3, §6; issue 1887 A4): the builder chooses no node, whichever build node pulls the ask builds it.
|
||||
Asks interface {
|
||||
@@ -74,6 +80,11 @@ type Builder struct {
|
||||
// Trunk answers the head of a repository in the gate's source revision rev (source.main at that revision): the trunk a
|
||||
// candidate's preview is merged onto (Candidate.Base). Nil builds no candidate's fingerprint, and says why.
|
||||
Trunk func(ctx context.Context, rev uint64, repository string) (string, error)
|
||||
// Members answers the module and artifact that build a package, from the catalog half's view of the packages (design 47 G
|
||||
// §373, §465): a package named at a group member's head is fetched by that member's build. Nil takes no package as a
|
||||
// member's. MemberBounds keeps the bound of a dependent's wait for a member's build (A-77); nil keeps none.
|
||||
Members func(ctx context.Context, pkg string) (module, artifact string, found bool, err error)
|
||||
MemberBounds MemberBounds
|
||||
// Gone says why an ask with no ended step can no longer be answered, or "" when it can (issue 1945): asked once the
|
||||
// ask has waited CheckAfter, and again each CheckAfter it goes on waiting. An ask that cannot be answered is asked
|
||||
// again under a new id, at most Askings times. Nil, or CheckAfter 0, never asks it.
|
||||
@@ -112,10 +123,15 @@ type Builder struct {
|
||||
mu sync.Mutex
|
||||
// fillWaits are the builds that said wait-held for the fill (module.fingerprint), so it is said once.
|
||||
fillWaits sync.Map
|
||||
known map[string]meshcatalog.ModulesByModuleValue // by modules key
|
||||
known map[string]meshcatalog.ModulesByModuleValue // by modules key: the first watched branch's
|
||||
heard map[string]meshcatalog.ModulesByModuleValue // by branch and module (take): what supersedes what
|
||||
origin map[string]Origin // by build key: the catalog revision its entry came from (take)
|
||||
edges map[string][]meshcatalog.Edge // by build key: the edges of its entry (take), whose parts a package-published names
|
||||
deps map[string]map[string][]meshbuilds.Dependency // by build key: what each artifact resolved and was built with (keepDependencies)
|
||||
inflight map[string]bool // by build key
|
||||
again map[string]meshcatalog.ModulesByModuleValue // by build key: the newest value that arrived while the key was being built
|
||||
plans map[string]planned // by manifest digest
|
||||
stepWaits map[string][]chan builds.BuildLogEntry // by build key: the step asks waiting for its end (step.go)
|
||||
cands map[string]meshcatalog.CandidatesByGroupValue // by candidates key (candidates.go)
|
||||
via map[string]via // by build key: a candidate's fetch (candidates.go)
|
||||
// outOfStep are, by build key, the out-of-step files a build's lanes answered, until its end entry takes them.
|
||||
@@ -125,6 +141,10 @@ type Builder struct {
|
||||
// published are the package versions `<name>@<version>` a done key of this bucket names as a package's reference: a key
|
||||
// waiting for one is built once it is here (ADR 0500 clause 6).
|
||||
published map[string]bool
|
||||
// memberWaits are the build keys of the dependents of a group's run that wait for a package member's build, and memberExpired
|
||||
// those whose bound ended; fetched are the member builds a key's module is fetched against (candidates.go).
|
||||
memberWaits, memberExpired map[string]bool
|
||||
fetched map[string][]sdkstep.FetchBuild
|
||||
// held are the done keys' references the store answered held, by `<key> <references>`, asked once per process: a
|
||||
// blob collected later is found by the take-in, whose artifacts-gone forgets the key here (issue 1937).
|
||||
held map[string]bool
|
||||
@@ -162,9 +182,12 @@ func (b *Builder) init() {
|
||||
}
|
||||
b.sem = make(chan struct{}, limit)
|
||||
b.cands, b.via, b.trunkFP = map[string]meshcatalog.CandidatesByGroupValue{}, map[string]via{}, map[string]bool{}
|
||||
b.known, b.inflight, b.again, b.plans, b.tried = map[string]meshcatalog.ModulesByModuleValue{}, map[string]bool{}, map[string]meshcatalog.ModulesByModuleValue{}, map[string]planned{}, map[string]bool{}
|
||||
b.heard, b.known = map[string]meshcatalog.ModulesByModuleValue{}, map[string]meshcatalog.ModulesByModuleValue{}
|
||||
b.origin, b.edges = map[string]Origin{}, map[string][]meshcatalog.Edge{}
|
||||
b.inflight, b.again, b.plans, b.tried = map[string]bool{}, map[string]meshcatalog.ModulesByModuleValue{}, map[string]planned{}, map[string]bool{}
|
||||
b.published, b.held, b.waiting = map[string]bool{}, map[string]bool{}, map[string]map[string]chan string{}
|
||||
b.hands, b.events = map[string]*handWatch{}, map[string]bool{}
|
||||
b.memberWaits, b.memberExpired, b.fetched = map[string]bool{}, map[string]bool{}, map[string][]sdkstep.FetchBuild{}
|
||||
b.builds = map[string]context.CancelCauseFunc{}
|
||||
})
|
||||
}
|
||||
@@ -178,8 +201,9 @@ func (b *Builder) logf(format string, args ...any) {
|
||||
// Wait returns when every build started so far has ended.
|
||||
func (b *Builder) Wait() { b.wg.Wait() }
|
||||
|
||||
// Module takes one value of the indexer's modules bucket in (the key is the module's name) and builds it if it is
|
||||
// to be built. A value that is not a module is said and left.
|
||||
// Module takes one value of a modules bucket in (the key is the module's name) and builds it if it is to be built. A value that
|
||||
// is not a module is said and left. The builder takes its modules from the catalog half (Entry); this is the same take of one
|
||||
// module's value, with no branch, the first one's.
|
||||
func (b *Builder) Module(ctx context.Context, key string, value []byte) {
|
||||
b.init()
|
||||
m, err := meshcatalogclient.DecodeModule(value)
|
||||
@@ -187,9 +211,49 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) {
|
||||
b.logf("modules.%s holds what is not a module, left: %v", key, err)
|
||||
return
|
||||
}
|
||||
b.take(ctx, Origin{First: true}, key, m, nil)
|
||||
}
|
||||
|
||||
// Origin is the catalog revision an entry came from (design 47 G §371, A-57): the watched branch it is of, as named, the source
|
||||
// revision the revision was taken in from, and whether the branch is the first of the module's registration, the only one whose
|
||||
// builds reserve a version and whose entry is the module's value for what the builder reads by name (a version, a build again).
|
||||
type Origin struct {
|
||||
Branch string
|
||||
Source int64
|
||||
First bool
|
||||
}
|
||||
|
||||
// Entry takes the catalog graph entry of one module on one watched branch in (design 47 G §348, §614) and builds its
|
||||
// fingerprint if it is to be built. An entry of a branch other than the first builds its fingerprint, reserves none, and
|
||||
// supersedes only that branch's older one. An entry naming no module is said and left.
|
||||
func (b *Builder) Entry(ctx context.Context, o Origin, e meshcatalog.CatalogEntry) {
|
||||
b.init()
|
||||
if e.Module == "" {
|
||||
b.logf("the catalog half of the branch %q holds an entry that names no module, left", o.Branch)
|
||||
return
|
||||
}
|
||||
b.take(ctx, o, e.Module, ModuleOf(e), e.Edges)
|
||||
}
|
||||
|
||||
// EntryGone takes a module's entry out of a branch's half (the module was dropped from it, or the branch is followed no more).
|
||||
func (b *Builder) EntryGone(branch string, first bool, module string) {
|
||||
b.init()
|
||||
b.mu.Lock()
|
||||
old, had := b.known[key]
|
||||
b.known[key] = m
|
||||
delete(b.heard, branch+"\x00"+module)
|
||||
b.mu.Unlock()
|
||||
if first {
|
||||
b.Forget(module)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) take(ctx context.Context, o Origin, key string, m meshcatalog.ModulesByModuleValue, e []meshcatalog.Edge) {
|
||||
b.mu.Lock()
|
||||
hk := o.Branch + "\x00" + key
|
||||
old, had := b.heard[hk]
|
||||
b.heard[hk] = m
|
||||
if o.First {
|
||||
b.known[key] = m
|
||||
}
|
||||
var superseded context.CancelCauseFunc
|
||||
if had && old.Fp2 != "" && old.Fp2 != m.Fp2 {
|
||||
if ok, err := builds.BuilderBuildKey(key, old.Fp2); err == nil {
|
||||
@@ -197,8 +261,10 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) {
|
||||
}
|
||||
}
|
||||
if bk, err := builds.BuilderBuildKey(key, m.Fp2); err == nil && m.Fp2 != "" {
|
||||
// A fingerprint the trunk carries is built from the trunk's commit, whoever asked for it first.
|
||||
// A fingerprint a watched branch carries is built from that branch's commit, whoever asked for it first.
|
||||
b.trunkFP[bk] = true
|
||||
b.origin[bk] = o
|
||||
b.edges[bk] = e
|
||||
delete(b.via, bk)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
@@ -210,6 +276,43 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) {
|
||||
b.consider(ctx, key, m)
|
||||
}
|
||||
|
||||
// ModuleOf is a catalog entry in the form the builder builds by: its fingerprint, manifest digest, source (repository,
|
||||
// directory, commit, the source revision it was taken in at), the toolchains its fingerprint was computed with, the bases it is
|
||||
// built against, its packages and needs, and the named paths of its artifacts.
|
||||
func ModuleOf(e meshcatalog.CatalogEntry) meshcatalog.ModulesByModuleValue {
|
||||
m := meshcatalog.ModulesByModuleValue{Repository: e.Repository, Path: e.Path, Commit: e.Commit, Fp2: e.Fp, None2: e.None,
|
||||
Manifest: e.ManifestDigest, Packages: e.Packages, Needs: e.Needs}
|
||||
if len(e.Bases) > 0 {
|
||||
m.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{Bases: e.Bases}
|
||||
}
|
||||
if in := e.Inputs; in != nil {
|
||||
m.Src, m.Toolchains, m.Problems = in.Src, in.Toolchains, in.Problems
|
||||
if len(in.Paths) > 0 {
|
||||
if m.Inputs2 == nil {
|
||||
m.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{}
|
||||
}
|
||||
m.Inputs2.Paths = map[string]string{}
|
||||
for artifact, paths := range in.Paths {
|
||||
for _, p := range paths {
|
||||
m.Inputs2.Paths[p] = artifact
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// originOf is the catalog revision the entry of a build key came from; a key no entry named is of the first branch, as a build
|
||||
// of the trunk's value was before branches were watched.
|
||||
func (b *Builder) originOf(key string) Origin {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if o, ok := b.origin[key]; ok {
|
||||
return o
|
||||
}
|
||||
return Origin{First: true}
|
||||
}
|
||||
|
||||
// NodesChanged is Reconsider after a toolchain moved, so a build that waited for the nodes to agree on one may go on.
|
||||
func (b *Builder) NodesChanged(ctx context.Context) { b.Reconsider(ctx) }
|
||||
|
||||
@@ -226,10 +329,14 @@ func (b *Builder) Sweep(ctx context.Context) error {
|
||||
}
|
||||
b.mu.Lock()
|
||||
current := map[string]bool{}
|
||||
for name, m := range b.known {
|
||||
if m.Fp2 != "" {
|
||||
if k, err := builds.BuilderBuildKey(name, m.Fp2); err == nil {
|
||||
current[k] = true
|
||||
// What every followed branch carries now is current, not only the first branch's (design 47 G §348).
|
||||
for _, held := range []map[string]meshcatalog.ModulesByModuleValue{b.known, b.heard} {
|
||||
for hk, m := range held {
|
||||
name := hk[strings.LastIndex(hk, "\x00")+1:]
|
||||
if m.Fp2 != "" {
|
||||
if k, err := builds.BuilderBuildKey(name, m.Fp2); err == nil {
|
||||
current[k] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,7 +506,11 @@ func (b *Builder) planOf(ctx context.Context, digest string) (Plan, error, bool)
|
||||
if err != nil {
|
||||
return Plan{}, fmt.Errorf("its manifest %s cannot be read: %w", digest, err), false
|
||||
}
|
||||
plan, err := PlanOf(m)
|
||||
plan, err := PlanOf(m, b.Seats)
|
||||
var none *NoBuildModule
|
||||
if errors.As(err, &none) {
|
||||
return plan, err, false // the seat list or the catalog half names no build module yet: made again when they do
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.plans[digest] = planned{plan, err}
|
||||
b.mu.Unlock()
|
||||
@@ -475,6 +586,13 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB
|
||||
defer b.handEnded(key)
|
||||
defer b.unrelease(key)
|
||||
plan, planErr, read := b.planOf(ctx, m.Manifest)
|
||||
var noModule *NoBuildModule
|
||||
if errors.As(planErr, &noModule) {
|
||||
// The seat list or the catalog half does not name the build module yet: nothing is claimed, and the look is made again
|
||||
// when they do (Reconsider, at each change of either; design 47 G A-82).
|
||||
b.logf("%s at %s is not built yet: %v", module, m.Fp2, planErr)
|
||||
return
|
||||
}
|
||||
nothing := read && planErr == nil && len(plan.Tasks) == 0
|
||||
if planErr == nil && !nothing && !b.isReleased(key) {
|
||||
if why := b.stale(m, plan); why != "" {
|
||||
@@ -505,13 +623,21 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB
|
||||
return
|
||||
}
|
||||
}
|
||||
reserving := trunk && planErr == nil && !nothing && b.reserves(ctx, module, m, plan)
|
||||
memberFail := ""
|
||||
if !trunk && planErr == nil && !nothing {
|
||||
var waiting bool
|
||||
if _, memberFail, waiting = b.memberBuilds(ctx, module, m, key); waiting {
|
||||
return // a package member of the group is not built (design 47 G §373): its end looks at this key again
|
||||
}
|
||||
}
|
||||
// Only a build of the registration's first watched branch reserves and releases a version (design 47 G §371).
|
||||
reserving := trunk && b.originOf(key).First && planErr == nil && !nothing && b.reserves(ctx, module, m, plan)
|
||||
if reserving && b.waitFill(module, m) {
|
||||
return
|
||||
}
|
||||
// A key built again by hand is taken over as it stands too: one left building by a look that ended without its write
|
||||
// (issue 1966) is the hand tool's to finish.
|
||||
cl, err := b.Claimer.Claim(ctx, module, m.Fp2, m.Manifest, ClaimOptions{TakeOver: first || byHand, ByHand: byHand, ByEvent: byEvent, ImageBuilder: imageSeat, Trunk: trunk, Published: b.isPublished,
|
||||
cl, err := b.Claimer.Claim(ctx, module, m.Fp2, m.Manifest, ClaimOptions{TakeOver: first || byHand, ByHand: byHand, ByEvent: byEvent, ImageBuilder: imageSeat, Seats: b.Seats, Trunk: trunk, Published: b.isPublished,
|
||||
Stored: b.storedOnce(key)})
|
||||
if err != nil {
|
||||
b.logf("%s: %v", module, err)
|
||||
@@ -537,7 +663,7 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB
|
||||
if cl.Build.State == builds.BuildDone {
|
||||
prior, only = &cl.Build, map[string]bool{}
|
||||
if trunk {
|
||||
for _, bm := range publishable(cl.Build, imageSeat) {
|
||||
for _, bm := range publishable(cl.Build, b.Seats, imageSeat) {
|
||||
only[bm] = true
|
||||
publishing = true
|
||||
}
|
||||
@@ -546,13 +672,15 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB
|
||||
var reserved versions.ReservedVersion
|
||||
hasReserved := false
|
||||
if reserving && cl.Build.State != builds.BuildDone {
|
||||
reserved, hasReserved = b.reserve(ctx, module, m, plan)
|
||||
reserved, hasReserved = b.reserve(ctx, module, m, plan, b.originOf(key))
|
||||
}
|
||||
var result builds.BuilderBuild
|
||||
if numbering.Refused != "" && cl.Build.State != builds.BuildDone {
|
||||
// Refused, naming the state it could not read, and built against no guess (ADR 0666 §15; design 47, test 70): the key
|
||||
// fails with why, and is taken again at the next look.
|
||||
result = failed("%s", numbering.Refused)
|
||||
} else if memberFail != "" && cl.Build.State != builds.BuildDone {
|
||||
result = failed("%s", memberFail) // a group's run: a package member's build failed or is missing (design 47 G §373)
|
||||
} else if nothing {
|
||||
// A manifest with no artifact is built by building nothing: its key is done with no artifact, claimed and written as
|
||||
// any other, so the controller has a done key to register it from (issue 1613, ADR 0478 clause 1, ADR 0501 clause 1).
|
||||
@@ -599,6 +727,7 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB
|
||||
if b.Ended != nil {
|
||||
b.Ended(ctx, cl.Key)
|
||||
}
|
||||
b.memberEnded(ctx, cl.Key)
|
||||
ended := LogEnded(module, m, b.refOf(module, m.Fp2), b.Node, b.now(), result)
|
||||
ended.OutOfStep = b.takeOutOfStep(key) // the generated files the build module's compare found out of step (design 26)
|
||||
b.logBuild(ended)
|
||||
@@ -612,7 +741,7 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB
|
||||
for _, a := range result.Artifacts {
|
||||
digests[module+":"+a.Name] = a.Digest
|
||||
}
|
||||
if err := b.Reserved.Built(ctx, module, reserved.Version, digests); err != nil {
|
||||
if err := b.Reserved.Built(ctx, module, reserved.Version, digests, b.takeDependencies(key)); err != nil {
|
||||
b.logf("%s: the digests of %s are not recorded: %v", cl.Key, reserved.Version, err)
|
||||
}
|
||||
}
|
||||
@@ -635,7 +764,7 @@ func (b *Builder) stale(m meshcatalog.ModulesByModuleValue, plan Plan) string {
|
||||
if !plan.NeedsToolchain(bm) {
|
||||
continue // an archive is packed with no toolchain
|
||||
}
|
||||
language := buildrun.LanguageOf[bm]
|
||||
language, _ := b.Seats.LanguageOf(bm)
|
||||
ag := sdkstep.ToolchainAgreement(b.Toolchains.Entries(language))
|
||||
if ag.Wait != "" {
|
||||
return fmt.Sprintf("the %s toolchain is not agreed: %s", language, ag.Wait)
|
||||
@@ -659,11 +788,14 @@ type lane struct {
|
||||
version string // the version the build reserved, MAJOR.MINOR.PATCH, empty for a build that reserves none (design 47 G §573)
|
||||
tasks []Task
|
||||
toolchain *builds.ToolchainUsed
|
||||
digests map[string]string // artifact -> digest
|
||||
references map[string]string // artifact -> the reference an image was pushed to (a trunk build only)
|
||||
imports []string // the mesh packages the lane's programs import, as its build module answered them
|
||||
kept string // the digest of the artifact the build node kept instead of storing it (BuildAnswer.Kept), else empty
|
||||
outOfStep []string // the generated files its build module's compare found out of step, passed or failed
|
||||
digests map[string]string // artifact -> digest
|
||||
references map[string]string // artifact -> the reference an image was pushed to (a trunk build only)
|
||||
imports []string // the mesh packages the lane's programs import, as its build module answered them
|
||||
resolved map[string][]sdkstep.AskResolved // artifact -> the mesh packages its build resolved (design 47 G §466)
|
||||
tcDigest string // the digest of the toolchain program the building node ran, kind 3's (design 47 G §466, §472)
|
||||
tcVersion string // and the version it ran
|
||||
kept string // the digest of the artifact the build node kept instead of storing it (BuildAnswer.Kept), else empty
|
||||
outOfStep []string // the generated files its build module's compare found out of step, passed or failed
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -686,9 +818,9 @@ func (b *Builder) build(ctx context.Context, module string, m meshcatalog.Module
|
||||
continue
|
||||
}
|
||||
var toolchain *builds.ToolchainUsed
|
||||
if bm != ImageBuildModule && plan.NeedsToolchain(bm) {
|
||||
if plan.NeedsToolchain(bm) {
|
||||
// The ask names the toolchain the fingerprint was computed with; a node building with another refuses it.
|
||||
language := buildrun.LanguageOf[bm]
|
||||
language, _ := b.Seats.LanguageOf(bm)
|
||||
ag := sdkstep.ToolchainAgreement(b.Toolchains.Entries(language))
|
||||
if ag.Wait != "" {
|
||||
return failed("the %s toolchain is not agreed: %s", language, ag.Wait)
|
||||
@@ -712,7 +844,7 @@ func (b *Builder) build(ctx context.Context, module string, m meshcatalog.Module
|
||||
}
|
||||
}
|
||||
// One ask builds each fingerprint's part of a build module, on whichever node pulls it (ADR 0525 §3).
|
||||
lanes = append(lanes, &lane{module: bm, version: version, tasks: tasks, toolchain: toolchain, digests: map[string]string{}, references: map[string]string{}})
|
||||
lanes = append(lanes, &lane{module: bm, version: version, tasks: tasks, toolchain: toolchain, digests: map[string]string{}, references: map[string]string{}, resolved: map[string][]sdkstep.AskResolved{}})
|
||||
}
|
||||
if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil {
|
||||
b.handLanes(key, len(lanes))
|
||||
@@ -725,6 +857,7 @@ func (b *Builder) build(ctx context.Context, module string, m meshcatalog.Module
|
||||
wg.Wait()
|
||||
if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil {
|
||||
b.keepOutOfStep(key, lanes)
|
||||
b.keepDependencies(key, module, lanes)
|
||||
}
|
||||
var errs []string
|
||||
for _, l := range lanes {
|
||||
@@ -797,8 +930,9 @@ func (b *Builder) build(ctx context.Context, module string, m meshcatalog.Module
|
||||
}
|
||||
for _, bm := range kept {
|
||||
steps = append(steps, bm+" kept")
|
||||
kept, _ := b.Seats.LanguageOf(bm)
|
||||
for lang, u := range prior.Toolchains {
|
||||
if _, ok := used[lang]; !ok && buildrun.LanguageOf[bm] == lang {
|
||||
if _, ok := used[lang]; !ok && kept == lang {
|
||||
used[lang] = u
|
||||
}
|
||||
}
|
||||
@@ -883,9 +1017,6 @@ func keyError(errs []string) string {
|
||||
// want of an image builder before R4g is taken again (ADR 0525 §1).
|
||||
func imageSeat() bool { return true }
|
||||
|
||||
// askLanguage is the build seat language of each build module (sdkstep.AskSubject).
|
||||
var askLanguage = map[string]string{buildrun.GoBuildModule: sdkstep.AskGo, buildrun.NodeBuildModule: sdkstep.AskNode, ImageBuildModule: sdkstep.AskImage}
|
||||
|
||||
// askNotes writes a claimed key's asks into it, building, before each is put (ADR 0525 §1; issue 1887 A4b), and holds
|
||||
// the asks a key taken over at start already names, so they are waited for and never put again.
|
||||
type askNotes struct {
|
||||
@@ -942,6 +1073,7 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes
|
||||
b.mu.Unlock()
|
||||
if candidate {
|
||||
req.Ref, req.Base = v.Ref, v.Base
|
||||
req.Builds = b.fetchedBuilds(key) // a group's run: each package member by its build's digest (design 47 G §465)
|
||||
}
|
||||
}
|
||||
if m.Inputs2 != nil && len(m.Inputs2.Paths) > 0 {
|
||||
@@ -954,7 +1086,8 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes
|
||||
sort.Strings(req.Paths)
|
||||
}
|
||||
candidate := req.Base != ""
|
||||
language := askLanguage[l.module]
|
||||
seat, _ := b.Seats.OfModule(l.module)
|
||||
language := seat.AskLanguage()
|
||||
kind := sdkstep.AskDelivery
|
||||
if candidate {
|
||||
kind = sdkstep.AskCheck
|
||||
@@ -978,6 +1111,9 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes
|
||||
// The versions of the mesh packages the fingerprint was computed with (novox/hq ADR 0573 §2): the build module writes
|
||||
// them into the workspace, whatever the repository's go.mod names.
|
||||
ask.Packages = maps.Clone(m.Packages)
|
||||
for _, fb := range req.Builds {
|
||||
delete(ask.Packages, fb.Package) // a package `builds` names is named in neither packages nor package (§465)
|
||||
}
|
||||
}
|
||||
if id := asked.resume(language); id != "" {
|
||||
ask.ID = id // put before this builder started again: the stream holds it, or a node already ran it
|
||||
@@ -1039,6 +1175,9 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes
|
||||
}
|
||||
l.imports = answer.Imports
|
||||
l.kept = answer.Kept
|
||||
if answer.Toolchain != nil {
|
||||
l.tcDigest, l.tcVersion = answer.Toolchain.Digest, answer.Toolchain.Version
|
||||
}
|
||||
built := map[string]sdkstep.AskBuilt{}
|
||||
for _, a := range answer.Artifacts {
|
||||
built[a.Artifact] = a
|
||||
@@ -1050,6 +1189,7 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes
|
||||
return
|
||||
}
|
||||
l.digests[t.Artifact] = a.Digest
|
||||
l.resolved[t.Artifact] = a.Resolved
|
||||
if a.Reference != "" && (stored(t.Kind) || !candidate) {
|
||||
l.references[t.Artifact] = a.Reference
|
||||
}
|
||||
@@ -1078,6 +1218,7 @@ func (b *Builder) now() time.Time {
|
||||
|
||||
// logBuild tells the log an entry, said in the journal when it cannot be one.
|
||||
func (b *Builder) logBuild(e builds.BuildLogEntry) {
|
||||
b.stepEnded(e)
|
||||
if b.Log == nil {
|
||||
return
|
||||
}
|
||||
@@ -1184,3 +1325,73 @@ func SittingOutWords(language string, a sdkstep.Agreement) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// keepDependencies keeps, for a build key, what its step results say each artifact resolved (kind 1) and was built with (kind 3) as
|
||||
// the dependencies `package-published` carries (design 47 G §466, §673, A-57): the part of a kind-1 item is the artifact reference of
|
||||
// the package, and of the kind-3 item the toolchain's part, each as the edges of the entry name it. A part no edge names is said and
|
||||
// its item left out.
|
||||
func (b *Builder) keepDependencies(key, module string, lanes []*lane) {
|
||||
b.mu.Lock()
|
||||
edges := b.edges[key]
|
||||
b.mu.Unlock()
|
||||
out := map[string][]meshbuilds.Dependency{}
|
||||
for _, l := range lanes {
|
||||
for _, t := range l.tasks {
|
||||
ref := module + ":" + t.Artifact
|
||||
var deps []meshbuilds.Dependency
|
||||
for _, r := range l.resolved[t.Artifact] {
|
||||
part := ""
|
||||
for _, e := range edges {
|
||||
if e.Kind == 1 && e.To == ref && e.Why == r.Package {
|
||||
part = cmp.Or(e.Part, e.From)
|
||||
}
|
||||
}
|
||||
if part == "" && b.Members != nil {
|
||||
if mod, art, ok, err := b.Members(context.Background(), r.Package); ok && err == nil {
|
||||
part = mod + ":" + art
|
||||
}
|
||||
}
|
||||
if part == "" {
|
||||
b.logf("%s: the package %s the build of %s resolved names no artifact: its dependency is not said", key, r.Package, ref)
|
||||
continue
|
||||
}
|
||||
deps = append(deps, meshbuilds.Dependency{Kind: 1, Part: part, Version: r.Version, Digest: r.Digest})
|
||||
}
|
||||
if l.tcDigest != "" {
|
||||
part := ""
|
||||
for _, e := range edges {
|
||||
if e.Kind == 3 && e.To == ref {
|
||||
part = cmp.Or(e.Part, e.From)
|
||||
}
|
||||
}
|
||||
if part == "" {
|
||||
b.logf("%s: no edge of kind 3 names the toolchain part the build of %s used: its dependency is not said", key, ref)
|
||||
} else {
|
||||
deps = append(deps, meshbuilds.Dependency{Kind: 3, Part: part, Version: l.tcVersion, Digest: l.tcDigest})
|
||||
}
|
||||
}
|
||||
if len(deps) > 0 {
|
||||
out[ref] = deps
|
||||
}
|
||||
}
|
||||
}
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.deps == nil {
|
||||
b.deps = map[string]map[string][]meshbuilds.Dependency{}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
delete(b.deps, key)
|
||||
return
|
||||
}
|
||||
b.deps[key] = out
|
||||
}
|
||||
|
||||
// takeDependencies answers the dependencies held for a build key and forgets them.
|
||||
func (b *Builder) takeDependencies(key string) map[string][]meshbuilds.Dependency {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
d := b.deps[key]
|
||||
delete(b.deps, key)
|
||||
return d
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ type rig struct {
|
||||
}
|
||||
|
||||
// toolPrefix is the start of each build module's tool names, as the fake build modules answer them.
|
||||
var toolPrefix = map[string]string{buildrun.GoBuildModule: "build_go_", buildrun.NodeBuildModule: "build_node_", ImageBuildModule: "build_image_"}
|
||||
var toolPrefix = map[string]string{testGo: "build_go_", testNode: "build_node_", testImage: "build_image_"}
|
||||
|
||||
// answer runs an ask as the build module that pulls it does (runAsk), with the fake build modules' tools on the
|
||||
// rig's pulling node, and answers the step that node writes.
|
||||
@@ -178,14 +178,14 @@ func (r *rig) answer(a buildstep.StepAsk) buildstep.Step {
|
||||
|
||||
func newRig(t *testing.T, artifacts ...catalogue.Artifact) *rig {
|
||||
t.Helper()
|
||||
r := &rig{st: newMem(), bm: newBuildModules(), tools: &buildrun.ToolchainSet{}, mf: &fakeManifests{byDigest: map[string]catalogue.Manifest{mdigest('a'): {Module: "m", Build: &catalogue.Build{Artifacts: artifacts}}}}}
|
||||
r := &rig{st: newMem(), bm: newBuildModules(), tools: &buildrun.ToolchainSet{Seats: testSeats()}, mf: &fakeManifests{byDigest: map[string]catalogue.Manifest{mdigest('a'): {Module: "m", Build: &catalogue.Build{Artifacts: artifacts}}}}}
|
||||
for _, n := range []string{"ace", "novox"} {
|
||||
r.tools.Set(n, tcOf("go", "go1.27.1"))
|
||||
r.tools.Set(n, tcOf("typescript", "tsc5"))
|
||||
}
|
||||
r.asks = &buildrun.FakeAsks{Answer: r.answer}
|
||||
r.b = &Builder{
|
||||
Claimer: Claimer{Store: r.st, By: "novox.builder"}, Asks: buildrun.Asker{Publisher: r.asks, Waiter: r.asks, Bound: 2 * time.Second},
|
||||
Seats: testSeats(), Claimer: Claimer{Store: r.st, By: "novox.builder"}, Asks: buildrun.Asker{Seats: testSeats(), Publisher: r.asks, Waiter: r.asks, Bound: 2 * time.Second},
|
||||
Toolchains: r.tools, Manifests: r.mf, Logf: t.Logf,
|
||||
Trunk: func(context.Context, uint64, string) (string, error) { return trunkHead, nil },
|
||||
}
|
||||
@@ -667,7 +667,7 @@ func aNewerFingerprintWithdrawsTheBuildOfTheOlderOne(t *testing.T) {
|
||||
}
|
||||
return r.answer(a)
|
||||
}
|
||||
r.b.Asks = buildrun.Asker{Publisher: r.asks, Waiter: r.asks, Bound: 2 * time.Second, Dropper: drops}
|
||||
r.b.Asks = buildrun.Asker{Seats: testSeats(), Publisher: r.asks, Waiter: r.asks, Bound: 2 * time.Second, Dropper: drops}
|
||||
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m"))
|
||||
<-running
|
||||
fp2 := "src2:" + hex64('2')
|
||||
@@ -915,7 +915,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
||||
t.Run("waits in its stream: never asked again", func(t *testing.T) {
|
||||
r := newRig(t, goBundle)
|
||||
r.asks.Answer = nil
|
||||
r.b.Asks = buildrun.Asker{Publisher: r.asks, Waiter: r.asks, Bound: 20 * time.Millisecond}
|
||||
r.b.Asks = buildrun.Asker{Seats: testSeats(), Publisher: r.asks, Waiter: r.asks, Bound: 20 * time.Millisecond}
|
||||
gone := &goneAsks{why: func(buildstep.StepAsk) string { return "" }}
|
||||
r.b.Gone, r.b.CheckAfter = gone, 5*time.Millisecond
|
||||
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m"))
|
||||
@@ -1122,12 +1122,12 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
||||
gone := &goneAsks{}
|
||||
dead := &buildrun.DeadLetters{}
|
||||
if c.dead {
|
||||
r.b.Asks = buildrun.Asker{Publisher: r.asks, Waiter: buildrun.LostAskWaiter{Inner: r.asks, Dead: dead}, Bound: 2 * time.Second}
|
||||
r.b.Asks = buildrun.Asker{Seats: testSeats(), Publisher: r.asks, Waiter: buildrun.LostAskWaiter{Inner: r.asks, Dead: dead}, Bound: 2 * time.Second}
|
||||
}
|
||||
var heard *buildrun.HeardEnds
|
||||
if c.deaf {
|
||||
heard = &buildrun.HeardEnds{Inner: silentWatch{}, Read: r.asks}
|
||||
r.b.Asks = buildrun.Asker{Publisher: r.asks, Waiter: heard, Bound: 2 * time.Second}
|
||||
r.b.Asks = buildrun.Asker{Seats: testSeats(), Publisher: r.asks, Waiter: heard, Bound: 2 * time.Second}
|
||||
}
|
||||
if !c.byHand && !c.dead && !c.deaf {
|
||||
gone.why = func(a buildstep.StepAsk) string {
|
||||
@@ -1158,7 +1158,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
||||
t.Fatal("no ask is waited on")
|
||||
}
|
||||
}
|
||||
heard.Nudge(buildrun.GoBuildModule) // a step of mesh-build-go ended
|
||||
heard.Nudge(testGo) // a step of mesh-build-go ended
|
||||
}
|
||||
if c.done && !c.deaf {
|
||||
asks := waitForAsks(t, r, c.puts)
|
||||
@@ -1269,7 +1269,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
||||
running, _ := json.Marshal(buildstep.Step{V: keys.Version, ID: "mvo53jcm70b5f8", Tool: "delivery ask", Node: "g14", State: buildstep.StepRunning, Started: at})
|
||||
_, reask, _ := NewReAsk("m", fp1, "stuck", "shanks.lead", at)
|
||||
r := StuckReader{Builds: state(map[string]string{key: string(building), done: string(finished)}), ReAsks: state(map[string]string{key: string(reask)}),
|
||||
Steps: map[string]StateRead{buildrun.GoBuildModule: state(map[string]string{}), ImageBuildModule: state(map[string]string{"g14.mvo53jcm70b5f8": string(running)})},
|
||||
Seats: testSeats(), Steps: map[string]StateRead{"node-build-go": state(map[string]string{}), "node-build-image": state(map[string]string{"g14.mvo53jcm70b5f8": string(running)})},
|
||||
Hearing: state(map[string]string{buildrun.HearingKey: `{"heard":false,"streams":["SEAT_NODE_BUILD_GO"],"why":"DEAD_LETTERS is not heard: SEAT_NODE_BUILD_GO: permissions violation","at":"2026-10-26T13:00:00Z"}`}),
|
||||
Now: func() time.Time { return at.Add(time.Hour) }}
|
||||
out, err := r.Stuck("")
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// The build modules of the fixture seat list: the holders of the three build seats (design 47 G A-82).
|
||||
const (
|
||||
testGo = "mesh-build-go"
|
||||
testNode = "mesh-build-node"
|
||||
testImage = "mesh-build-image"
|
||||
)
|
||||
|
||||
// testSeats is the finder over a seat list whose build seats act on go, typescript and kind:image, each held by its module.
|
||||
func testSeats() *buildrun.BuildSeats {
|
||||
rule := func(on string) seatdef.Definition {
|
||||
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
|
||||
}
|
||||
defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript"), "node-build-image": rule(seatdef.OnKindImage)}
|
||||
holders := map[string]string{"node-build-go": testGo, "node-build-node": testNode, "node-build-image": testImage}
|
||||
return buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
)
|
||||
|
||||
// **A candidate's build is the runner's step ask** (design 47 G A-97, A-84, A-99): the runner's asking half asks the seat
|
||||
// mesh-builds a `build` step naming each entry by fingerprint, and the builder, its one entry point for a candidate, reads each
|
||||
// entry through the seat mesh-catalog's `entry` (the repository and the commit are the member's head), builds it as it builds any
|
||||
// fingerprint (the existing claim, plan and lanes) and answers the step's end. A build is per fingerprint, so no pull request
|
||||
// number is needed, and the builder follows no candidate.
|
||||
|
||||
// BuildStep builds the entries of one step ask and answers its end: passed when every entry's build is done, failed when one
|
||||
// failed (said), could not run when an entry could not be read, nothing to run for an ask naming none. The ask's id and the node
|
||||
// are the caller's to set.
|
||||
func (b *Builder) BuildStep(ctx context.Context, run meshbuilds.StepRun) meshbuilds.StepEnded {
|
||||
b.init()
|
||||
started := b.now()
|
||||
end := meshbuilds.StepEnded{V: 1, Pipeline: run.Pipeline, Step: meshbuilds.StepEndedStepBuild, Rerun: run.Rerun, By: run.By, Entries: run.Entries, Node: b.Node, Started: &started}
|
||||
finish := func(outcome, said string) meshbuilds.StepEnded {
|
||||
at := b.now()
|
||||
end.Ended, end.Outcome, end.Said = &at, outcome, said
|
||||
return end
|
||||
}
|
||||
if len(run.Entries) == 0 {
|
||||
return finish(meshbuilds.StepEndedOutcomeNothingToRun, "the ask names no entry")
|
||||
}
|
||||
if b.StepEntry == nil {
|
||||
return finish(meshbuilds.StepEndedOutcomeCouldNotRun, "the builder reads no entry of the seat mesh-catalog")
|
||||
}
|
||||
var failed []string
|
||||
for _, e := range run.Entries {
|
||||
outcome, said := b.buildEntry(ctx, e)
|
||||
switch outcome {
|
||||
case meshbuilds.StepEndedOutcomeCouldNotRun:
|
||||
return finish(outcome, said)
|
||||
case meshbuilds.StepEndedOutcomeFailed:
|
||||
failed = append(failed, said)
|
||||
}
|
||||
}
|
||||
if len(failed) > 0 {
|
||||
return finish(meshbuilds.StepEndedOutcomeFailed, strings.Join(failed, "; "))
|
||||
}
|
||||
return finish(meshbuilds.StepEndedOutcomePassed, "")
|
||||
}
|
||||
|
||||
// buildEntry builds one entry at its fingerprint and waits for its end.
|
||||
func (b *Builder) buildEntry(ctx context.Context, e meshbuilds.BuildEntry) (outcome, said string) {
|
||||
entry, err := b.StepEntry(ctx, e.Fingerprint)
|
||||
if err != nil {
|
||||
return meshbuilds.StepEndedOutcomeCouldNotRun, fmt.Sprintf("the entry %s at %s cannot be read: %v", e.Module, e.Fingerprint, err)
|
||||
}
|
||||
if entry.Module != e.Module || (e.Commit != "" && entry.Commit != e.Commit) {
|
||||
return meshbuilds.StepEndedOutcomeCouldNotRun, fmt.Sprintf("the entry of %s at %s is of %s at %s", e.Module, e.Fingerprint, entry.Module, entry.Commit)
|
||||
}
|
||||
key, err := builds.BuilderBuildKey(entry.Module, entry.Fp)
|
||||
if err != nil {
|
||||
return meshbuilds.StepEndedOutcomeCouldNotRun, err.Error()
|
||||
}
|
||||
ended := b.awaitEnd(key)
|
||||
defer b.dropEnd(key, ended)
|
||||
if done, failedWords, found := b.keyEnd(ctx, key); found {
|
||||
if done {
|
||||
return meshbuilds.StepEndedOutcomePassed, ""
|
||||
}
|
||||
return meshbuilds.StepEndedOutcomeFailed, failedWords
|
||||
}
|
||||
m := meshcatalog.ModulesByModuleValue{Repository: entry.Repository, Commit: entry.Commit, Fp2: entry.Fp, Manifest: entry.ManifestDigest, Path: entry.Path,
|
||||
Toolchains: CatalogToolchains(b.agreedFor(ctx, entry.ManifestDigest)), Packages: entry.Packages}
|
||||
b.mu.Lock()
|
||||
if _, onTrunk := b.trunkFP[key]; !onTrunk {
|
||||
b.via[key] = via{}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
b.consider(ctx, entry.Module, m)
|
||||
select {
|
||||
case log := <-ended:
|
||||
if log.Outcome == builds.BuildDone {
|
||||
return meshbuilds.StepEndedOutcomePassed, ""
|
||||
}
|
||||
return meshbuilds.StepEndedOutcomeFailed, fmt.Sprintf("%s at %s: %s %s", log.Module, log.FP, log.Outcome, log.Error)
|
||||
case <-ctx.Done():
|
||||
return meshbuilds.StepEndedOutcomeCouldNotRun, fmt.Sprintf("%s at %s: the builder stopped before its build ended", entry.Module, entry.Fp)
|
||||
}
|
||||
}
|
||||
|
||||
// keyEnd reads the build key: whether it is done, and its error when it failed for good; found only for a key that has ended.
|
||||
func (b *Builder) keyEnd(ctx context.Context, key string) (done bool, failed string, found bool) {
|
||||
raw, _, ok, err := b.Claimer.Store.Get(ctx, key)
|
||||
if err != nil || !ok {
|
||||
return false, "", false
|
||||
}
|
||||
v, err := builds.DecodeBuilderBuild(raw)
|
||||
if err != nil {
|
||||
return false, "", false
|
||||
}
|
||||
switch {
|
||||
case v.State == builds.BuildDone && !Unpublished(v, b.Seats):
|
||||
return true, "", true
|
||||
case v.State == builds.BuildFailed && builds.FailedUntilChanged(v.Error):
|
||||
return false, key + ": " + v.Error, true
|
||||
}
|
||||
return false, "", false
|
||||
}
|
||||
|
||||
// awaitEnd registers a wait for the end of the build of key, heard at its log entry (logBuild).
|
||||
func (b *Builder) awaitEnd(key string) chan builds.BuildLogEntry {
|
||||
ch := make(chan builds.BuildLogEntry, 1)
|
||||
b.mu.Lock()
|
||||
if b.stepWaits == nil {
|
||||
b.stepWaits = map[string][]chan builds.BuildLogEntry{}
|
||||
}
|
||||
b.stepWaits[key] = append(b.stepWaits[key], ch)
|
||||
b.mu.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (b *Builder) dropEnd(key string, ch chan builds.BuildLogEntry) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for i, c := range b.stepWaits[key] {
|
||||
if c == ch {
|
||||
b.stepWaits[key] = append(b.stepWaits[key][:i], b.stepWaits[key][i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(b.stepWaits[key]) == 0 {
|
||||
delete(b.stepWaits, key)
|
||||
}
|
||||
}
|
||||
|
||||
// stepEnded tells the step asks waiting for a build that its end was logged.
|
||||
func (b *Builder) stepEnded(e builds.BuildLogEntry) {
|
||||
if e.Event != builds.BuildEndedEvent {
|
||||
return
|
||||
}
|
||||
key, err := builds.BuilderBuildKey(e.Module, e.FP)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b.mu.Lock()
|
||||
chans := append([]chan builds.BuildLogEntry(nil), b.stepWaits[key]...)
|
||||
b.mu.Unlock()
|
||||
for _, ch := range chans {
|
||||
select {
|
||||
case ch <- e:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// The runner's build step ask is the builder's one entry point for a candidate (design 47 G A-97): each entry is read by its
|
||||
// fingerprint, built through the one claim, and the step's end says passed only for a build that is done.
|
||||
func TestABuildStepAnswersItsEnd(t *testing.T) {
|
||||
r := newRig(t, catalogue.Artifact{Name: "files", Kind: "archive"})
|
||||
entryOf := func(fp, commit string) meshcatalog.CatalogEntry {
|
||||
return meshcatalog.CatalogEntry{Module: "m", Repository: "novox/m", Commit: commit, Fp: fp, ManifestDigest: mdigest('a'), Path: "."}
|
||||
}
|
||||
r.b.StepEntry = func(_ context.Context, fp string) (meshcatalog.CatalogEntry, error) {
|
||||
if fp == fp1 {
|
||||
return entryOf(fp1, "c1"), nil
|
||||
}
|
||||
return meshcatalog.CatalogEntry{}, errors.New("not found")
|
||||
}
|
||||
ask := func(entries ...meshbuilds.BuildEntry) meshbuilds.StepEnded {
|
||||
return r.b.BuildStep(t.Context(), meshbuilds.StepRun{Pipeline: "main.1.m", Step: meshbuilds.StepRunStepBuild, Entries: entries})
|
||||
}
|
||||
if end := ask(); end.Outcome != meshbuilds.StepEndedOutcomeNothingToRun {
|
||||
t.Errorf("an ask naming no entry: %+v", end)
|
||||
}
|
||||
if end := ask(meshbuilds.BuildEntry{Module: "m", Fingerprint: "src2:" + strings.Repeat("2", 64)}); end.Outcome != meshbuilds.StepEndedOutcomeCouldNotRun || !strings.Contains(end.Said, "cannot be read") {
|
||||
t.Errorf("an entry the seat does not hold: %+v", end)
|
||||
}
|
||||
if end := ask(meshbuilds.BuildEntry{Module: "m", Fingerprint: fp1, Commit: "other"}); end.Outcome != meshbuilds.StepEndedOutcomeCouldNotRun {
|
||||
t.Errorf("an entry at another commit: %+v", end)
|
||||
}
|
||||
// A build already done is passed at once, naming each entry as it was asked.
|
||||
done, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: mdigest('a')})
|
||||
if _, err := r.st.Create(t.Context(), r.key, done); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries := []meshbuilds.BuildEntry{{Module: "m", Fingerprint: fp1, Commit: "c1"}}
|
||||
if end := ask(entries...); end.Outcome != meshbuilds.StepEndedOutcomePassed || end.Step != meshbuilds.StepEndedStepBuild || len(end.Entries) != 1 || end.Entries[0].Commit != "c1" || end.Pipeline != "main.1.m" {
|
||||
t.Errorf("a done build: %+v", end)
|
||||
}
|
||||
}
|
||||
|
||||
// A step ask waits for the end of its build without a timer: the log entry of that end wakes it (design 47 G A-97).
|
||||
func TestAStepAskIsWokenByItsBuildsEnd(t *testing.T) {
|
||||
r := newRig(t)
|
||||
key := r.key
|
||||
ch := r.b.awaitEnd(key)
|
||||
r.b.stepEnded(builds.BuildLogEntry{Event: builds.BuildStartedEvent, Module: "m", FP: fp1}) // a start wakes nothing
|
||||
select {
|
||||
case <-ch:
|
||||
t.Fatal("a build's start woke the wait")
|
||||
default:
|
||||
}
|
||||
r.b.stepEnded(builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: "m", FP: fp1, Outcome: builds.BuildDone})
|
||||
select {
|
||||
case e := <-ch:
|
||||
if e.Outcome != builds.BuildDone {
|
||||
t.Errorf("end = %+v", e)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("the end of the build did not wake the wait")
|
||||
}
|
||||
r.b.dropEnd(key, ch)
|
||||
if len(r.b.stepWaits) != 0 {
|
||||
t.Errorf("waits left: %v", r.b.stepWaits)
|
||||
}
|
||||
}
|
||||
@@ -236,8 +236,13 @@ type Stuck struct {
|
||||
type StuckReader struct {
|
||||
Builds StateRead
|
||||
ReAsks StateRead
|
||||
// Steps are the build modules' steps states, by build module.
|
||||
// Steps are the build seats' steps states (`<seat>.steps`, design 47 G A-82), by build seat.
|
||||
Steps map[string]StateRead
|
||||
// StepsOf, when set, reads the steps state of a build seat the seat list names (`<seat>.steps`), instead of Steps: the seats
|
||||
// are known only once the list is read.
|
||||
StepsOf func(seat string) StateRead
|
||||
// Seats finds the build module of a build seat, whose name the key's step words give.
|
||||
Seats *buildrun.BuildSeats
|
||||
// Hearing is the builder's state hearing: whether it hears DEAD_LETTERS (issue 734); without it none is said.
|
||||
Hearing StateRead
|
||||
Now func() time.Time
|
||||
@@ -365,13 +370,14 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) {
|
||||
s.Age = now().Sub(at).Round(time.Second).String()
|
||||
}
|
||||
for language, id := range v.Asks {
|
||||
bm, _ := buildstep.BuildModuleOf(language)
|
||||
a := StuckAsk{Language: language, BuildModule: bm, ID: id, Asking: askingOf(v.Step, bm), Step: "none"}
|
||||
seat, _ := buildstep.BuildSeat(language)
|
||||
bm, _ := r.Seats.OfSeat(seat)
|
||||
a := StuckAsk{Language: language, BuildModule: bm.Module, ID: id, Asking: askingOf(v.Step, bm.Module), Step: "none"}
|
||||
if t, ok := idTime(id); ok {
|
||||
a.Asked = t
|
||||
}
|
||||
if st, ok := r.Steps[bm]; ok {
|
||||
if k, ok := stepKeys[bm][id]; ok {
|
||||
if st, ok := steps[seat]; ok {
|
||||
if k, ok := stepKeys[seat][id]; ok {
|
||||
reads++
|
||||
if raw, _, err := st.Get(k); err == nil && raw != nil {
|
||||
if step, err := buildstep.DecodeStep(raw); err == nil {
|
||||
@@ -416,7 +422,14 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) {
|
||||
// until an ask names it.
|
||||
func (r StuckReader) stepIndex() (map[string]map[string]string, error) {
|
||||
stepKeys := map[string]map[string]string{}
|
||||
for bm, s := range r.Steps {
|
||||
steps := r.Steps
|
||||
if r.StepsOf != nil {
|
||||
steps = map[string]StateRead{}
|
||||
for _, m := range r.Seats.All() {
|
||||
steps[m.Seat] = r.StepsOf(m.Seat)
|
||||
}
|
||||
}
|
||||
for bm, s := range steps {
|
||||
ks, err := s.Keys()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the steps of %s: %w", bm, err)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
@@ -57,7 +56,7 @@ func (b *Builder) toolchainWait(module string, m meshcatalog.ModulesByModuleValu
|
||||
if !plan.NeedsToolchain(bm) {
|
||||
continue
|
||||
}
|
||||
language := buildrun.LanguageOf[bm]
|
||||
language, _ := b.Seats.LanguageOf(bm)
|
||||
ag := sdkstep.ToolchainAgreement(b.Toolchains.Entries(language))
|
||||
if want, ok := m.Toolchains[language]; ag.Wait == "" && ok && want.Version != ag.Toolchain.Version {
|
||||
return ToolchainWait{Module: module, Fingerprint: m.Fp2, Language: language, Version: want.Version, Since: b.now()}, true
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// Entries are the catalog graph entries of the halves the builder follows, one per module and watched branch (design 47 G §348,
|
||||
// §614): where a package's name in its ecosystem is read from, each artifact's `package` (§676), never the indexer's state.
|
||||
type Entries func() []meshcatalog.CatalogEntry
|
||||
|
||||
// artifactOf is the artifact name of an artifact reference `<module>:<artifact>` of module.
|
||||
func artifactOf(module, ref string) (string, bool) {
|
||||
m, a, ok := strings.Cut(ref, ":")
|
||||
return a, ok && m == module && a != ""
|
||||
}
|
||||
|
||||
// NamesFrom answers a package's name in its ecosystem from the entries (design 47 G §348): the name the entry of module gives the
|
||||
// artifact; known is false when no entry names one for it.
|
||||
func NamesFrom(all Entries) PackageNames {
|
||||
return func(_ context.Context, module, artifact string) (string, bool, error) {
|
||||
for _, e := range all() {
|
||||
if e.Module != module {
|
||||
continue
|
||||
}
|
||||
for _, a := range e.Artifacts {
|
||||
if name, ok := artifactOf(module, a.Ref); ok && name == artifact && a.Package != nil && a.Package.Name != "" {
|
||||
return a.Package.Name, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CarriersFrom are the module artifacts a package's name in its ecosystem is the name of, as the entries hold them (design 47 G
|
||||
// §572, §478), each distinct module and artifact once, sorted; a name no entry carries answers none.
|
||||
func CarriersFrom(all Entries) func(ctx context.Context, name string) ([]Built, error) {
|
||||
return func(_ context.Context, name string) ([]Built, error) {
|
||||
seen := map[Built]bool{}
|
||||
var out []Built
|
||||
for _, e := range all() {
|
||||
for _, a := range e.Artifacts {
|
||||
artifact, ok := artifactOf(e.Module, a.Ref)
|
||||
if !ok || a.Package == nil || a.Package.Name != name || seen[Built{e.Module, artifact}] {
|
||||
continue
|
||||
}
|
||||
seen[Built{e.Module, artifact}] = true
|
||||
out = append(out, Built{e.Module, artifact})
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Module+"/"+out[i].Artifact < out[j].Module+"/"+out[j].Artifact })
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ModuleFrom is the module and artifact a package's name is built from, from the entries (CarriersFrom); more than one entry
|
||||
// carrying it is an error naming them.
|
||||
func ModuleFrom(all Entries) func(ctx context.Context, name string) (module, artifact string, found bool, err error) {
|
||||
return moduleOfCarriers(CarriersFrom(all))
|
||||
}
|
||||
|
||||
// EntriesOf are the entries of every half the branch follower holds, the followed branches in the order they came.
|
||||
func EntriesOf(branches *meshcatalogclient.Branches) Entries {
|
||||
return func() []meshcatalog.CatalogEntry {
|
||||
var out []meshcatalog.CatalogEntry
|
||||
for _, br := range branches.Followed() {
|
||||
if cat, ok := branches.Catalog(br); ok {
|
||||
if half, ok := cat.View(); ok {
|
||||
for _, e := range half.Entries {
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
// HolderOf finds the module that holds a seat: the one whose entry in the first followed branch's half (the trunk's) has a
|
||||
// manifest that claims it (design 47 G A-82; the seat list answers seat definitions alone, §527). More than one claimant, or
|
||||
// none, is no holder.
|
||||
func HolderOf(branches *meshcatalogclient.Branches) func(seat string) (string, bool) {
|
||||
var mu sync.Mutex
|
||||
claims := map[string][]string{} // manifest digest -> the seats it claims
|
||||
claimed := func(e meshcatalog.CatalogEntry) []string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if seats, ok := claims[e.ManifestDigest]; ok && e.ManifestDigest != "" {
|
||||
return seats
|
||||
}
|
||||
var seats []string
|
||||
if raw, err := json.Marshal(e.Manifest); err == nil {
|
||||
if mf, err := catalogue.Decode(raw); err == nil {
|
||||
for _, c := range mf.Claims {
|
||||
seats = append(seats, c.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.ManifestDigest != "" {
|
||||
claims[e.ManifestDigest] = seats
|
||||
}
|
||||
return seats
|
||||
}
|
||||
return func(seat string) (string, bool) {
|
||||
followed := branches.Followed()
|
||||
if len(followed) == 0 {
|
||||
return "", false
|
||||
}
|
||||
cat, ok := branches.Catalog(followed[0])
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
half, ok := cat.View()
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
var holders []string
|
||||
for _, e := range half.Entries {
|
||||
if slices.Contains(claimed(e), seat) {
|
||||
holders = append(holders, e.Module)
|
||||
}
|
||||
}
|
||||
if len(holders) != 1 {
|
||||
return "", false
|
||||
}
|
||||
return holders[0], true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
)
|
||||
|
||||
func named(module, artifact, name string) meshcatalog.CatalogEntry {
|
||||
return meshcatalog.CatalogEntry{Module: module, Artifacts: []meshcatalog.Artifact{
|
||||
{Ref: module + ":" + artifact, Language: "go", Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: name}}}}
|
||||
}
|
||||
|
||||
// TestPackageNamesAreReadFromTheCatalogHalfsEntries: a package's name in its ecosystem is the name the entry of its module gives the
|
||||
// artifact (design 47 G §348, §676), the module and artifact a name is built from are the entries carrying it, and a name two
|
||||
// entries carry is an error naming both (§491); no state of the indexer is read.
|
||||
func TestPackageNamesAreReadFromTheCatalogHalfsEntries(t *testing.T) {
|
||||
entries := func() []meshcatalog.CatalogEntry {
|
||||
return []meshcatalog.CatalogEntry{named("mesh-sdk", "go", "git.novox.be/novox/mesh-sdk/go"), named("netcheck", "lib", "netcheck"),
|
||||
named("twin-a", "lib", "twin"), named("twin-b", "lib", "twin"), {Module: "bare"}}
|
||||
}
|
||||
ctx := context.Background()
|
||||
for _, c := range []struct {
|
||||
module, artifact string
|
||||
name string
|
||||
known bool
|
||||
}{
|
||||
{"mesh-sdk", "go", "git.novox.be/novox/mesh-sdk/go", true},
|
||||
{"netcheck", "lib", "netcheck", true},
|
||||
{"netcheck", "other", "", false},
|
||||
{"bare", "lib", "", false},
|
||||
{"unknown", "lib", "", false},
|
||||
} {
|
||||
got, known, err := NamesFrom(entries)(ctx, c.module, c.artifact)
|
||||
if err != nil || got != c.name || known != c.known {
|
||||
t.Errorf("NamesFrom(%s/%s) = %q, %v, %v; want %q, %v", c.module, c.artifact, got, known, err, c.name, c.known)
|
||||
}
|
||||
}
|
||||
module, artifact, found, err := ModuleFrom(entries)(ctx, "netcheck")
|
||||
if module != "netcheck" || artifact != "lib" || !found || err != nil {
|
||||
t.Errorf("ModuleFrom(netcheck) = %q, %q, %v, %v", module, artifact, found, err)
|
||||
}
|
||||
if _, _, found, err := ModuleFrom(entries)(ctx, "nobody"); found || err != nil {
|
||||
t.Errorf("ModuleFrom(nobody) = %v, %v; want none", found, err)
|
||||
}
|
||||
if _, _, _, err := ModuleFrom(entries)(ctx, "twin"); err == nil {
|
||||
t.Error("ModuleFrom(twin): two entries carry it, want an error")
|
||||
}
|
||||
got, _ := CarriersFrom(entries)(ctx, "twin")
|
||||
if want := []Built{{"twin-a", "lib"}, {"twin-b", "lib"}}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("CarriersFrom(twin) = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -77,11 +77,9 @@ const (
|
||||
// sourceOfWake is the floor source each event says something of; a clear names the source by its module.
|
||||
var sourceOfWake = map[string]string{WakePublished: SourceSeat, WakeForge: SourceTags, WakeAccepted: SourceRequirements}
|
||||
|
||||
// ModuleOfSource is the module whose health condition's clear wakes a silent source.
|
||||
var ModuleOfSource = map[string]string{
|
||||
SourceTags: "mesh-delivery-forge-bridge",
|
||||
SourceRequirements: "mesh-delivery-indexer",
|
||||
}
|
||||
// SourceSeats are the seats whose holders are the floor's tag and requirement sources (design 47 G A-82): the seat git (tags)
|
||||
// and the seat mesh-catalog (requirements), the holders read from the seat list and the catalog half.
|
||||
var SourceSeats = map[string]string{SourceTags: "git", SourceRequirements: "mesh-catalog"}
|
||||
|
||||
// FloorWait is one name waiting on its floor: the sources still silent and why, since when; the finding the builder
|
||||
// writes names them, cleared once all three answer.
|
||||
@@ -135,6 +133,8 @@ type FloorWaits struct {
|
||||
keeper FloorKeeper
|
||||
// Logf says a wait that could not be kept; nil says nothing.
|
||||
Logf func(string, ...any)
|
||||
// Holder finds the module holding a seat (the catalog half's claim); a source's module is the holder of its SourceSeats seat.
|
||||
Holder func(seat string) (string, bool)
|
||||
// Said is told each wait recorded (raised) and each answered (not raised): the builder's finding floor-silent.
|
||||
Said func(w FloorWait, raised bool)
|
||||
}
|
||||
@@ -290,7 +290,7 @@ func (fw *FloorWaits) Wake(kind, seat, module, name string) []string {
|
||||
}
|
||||
case WakeCleared:
|
||||
for _, s := range w.Silent {
|
||||
if ModuleOfSource[s] == module || (s == SourceSeat && w.Holder != "" && w.Holder == module) {
|
||||
if fw.holds(SourceSeats[s], module) || (s == SourceSeat && w.Holder != "" && w.Holder == module) {
|
||||
out = append(out, n)
|
||||
break
|
||||
}
|
||||
@@ -317,3 +317,12 @@ func (fw *FloorWaits) Waiting() []FloorWait {
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// holds is whether module holds seat on the catalog half.
|
||||
func (fw *FloorWaits) holds(seat, module string) bool {
|
||||
if seat == "" || module == "" || fw.Holder == nil {
|
||||
return false
|
||||
}
|
||||
holder, ok := fw.Holder(seat)
|
||||
return ok && holder == module
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -70,3 +72,73 @@ func isSeriesKey(key string) bool {
|
||||
_, _, ok := meshcatalog.ParsePackagesByPackageSeriesKey(key)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Built is a module's artifact, which a package's name is the name of.
|
||||
type Built struct{ Module, Artifact string }
|
||||
|
||||
// Carriers are the module artifacts a package's name in its ecosystem is the name of, as the catalog half's packages view
|
||||
// holds them (the inverse of NamesIn; design 47 G §572, §478): each distinct module and artifact of a series key whose value
|
||||
// names that name, sorted. A name no entry carries answers none.
|
||||
func Carriers(s PackagesState) func(ctx context.Context, name string) ([]Built, error) {
|
||||
return func(ctx context.Context, name string) ([]Built, error) {
|
||||
keys, err := s.Keys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(keys)
|
||||
seen := map[Built]bool{}
|
||||
var out []Built
|
||||
for _, key := range keys {
|
||||
if !isSeriesKey(key) {
|
||||
continue
|
||||
}
|
||||
value, found, err := s.Get(ctx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
v, err := meshcatalog.DecodePackagesByPackageSeriesValue(value)
|
||||
if err != nil || v.Name != name {
|
||||
continue
|
||||
}
|
||||
if module, artifact, cut := strings.Cut(v.Package, "/"); cut && module != "" && artifact != "" && !seen[Built{module, artifact}] {
|
||||
seen[Built{module, artifact}] = true
|
||||
out = append(out, Built{module, artifact})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ModuleOf is the module and artifact a package's name is built from (Carriers), found false for a name no entry carries; more
|
||||
// than one entry carrying it is an error naming them.
|
||||
func ModuleOf(s PackagesState) func(ctx context.Context, name string) (module, artifact string, found bool, err error) {
|
||||
return moduleOfCarriers(Carriers(s))
|
||||
}
|
||||
|
||||
// moduleOfCarriers is ModuleOf over one way of finding the carriers.
|
||||
func moduleOfCarriers(carriers func(ctx context.Context, name string) ([]Built, error)) func(ctx context.Context, name string) (module, artifact string, found bool, err error) {
|
||||
return func(ctx context.Context, name string) (string, string, bool, error) {
|
||||
got, err := carriers(ctx, name)
|
||||
switch {
|
||||
case err != nil:
|
||||
return "", "", false, err
|
||||
case len(got) == 0:
|
||||
return "", "", false, nil
|
||||
case len(got) > 1:
|
||||
return "", "", false, errors.New(MoreThanOne(name, got))
|
||||
}
|
||||
return got[0].Module, got[0].Artifact, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// MoreThanOne is the words of a name that more than one entry carries.
|
||||
func MoreThanOne(name string, got []Built) string {
|
||||
var refs []string
|
||||
for _, b := range got {
|
||||
refs = append(refs, b.Module+"/"+b.Artifact)
|
||||
}
|
||||
return fmt.Sprintf("the name %s is carried by more than one entry: %s", name, strings.Join(refs, ", "))
|
||||
}
|
||||
|
||||
@@ -38,8 +38,16 @@ const (
|
||||
oldFormBoundPrefix = "old-form."
|
||||
unputBoundPrefix = "unput."
|
||||
stepsBoundPrefix = "steps."
|
||||
memberBoundPrefix = "member."
|
||||
)
|
||||
|
||||
// MemberBoundKey is the bound of a dependent's wait for a package member of a group's run (design 47 G §373, A-77):
|
||||
// `member.<build key>`. Its end while the member is not built fails the dependent's step naming the member.
|
||||
func MemberBoundKey(buildKey string) string { return memberBoundPrefix + buildKey }
|
||||
|
||||
// MemberBoundOf is the build key a member wait's bound names, and whether key is one.
|
||||
func MemberBoundOf(key string) (string, bool) { return strings.CutPrefix(key, memberBoundPrefix) }
|
||||
|
||||
// UnputBoundKey is the bound of a build key a look left building because its end could not be written (issue 1966,
|
||||
// novox/hq ADR 0668 §3): `unput.<build key>`. Its end while the key still stands so, with no look holding it, is the
|
||||
// finding call-unanswered about the key.
|
||||
@@ -84,7 +92,8 @@ const BoundsTool = "builder_bounds"
|
||||
const BoundsDescription = "The builder's expiring keys, its state " + BoundsState + " (design 51 *Health and conditions*): each key with what it " +
|
||||
"bounds (a version's publish, whose end unanswered is the finding " + PublishUnanswered + "; or a node's seven days holding the old " +
|
||||
"form's window, whose end is the finding " + OldFormHeld + "; or a build key left building because its end could not be written, " +
|
||||
"whose end is the finding call-unanswered), when it ends, and how long is left. The bus ends each key by its own time to live."
|
||||
"whose end is the finding call-unanswered; or a dependent's wait for a package member of a group's run, whose end fails its " +
|
||||
"step), when it ends, and how long is left. The bus ends each key by its own time to live."
|
||||
|
||||
// Bound is a key's value in BoundsState: what it bounds, in plain words, and when it ends.
|
||||
type Bound struct {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
)
|
||||
|
||||
// **The reservation is one key per module and MAJOR.MINOR** (design 47 G §369, §52): the builder keeps in its old state
|
||||
@@ -60,6 +61,13 @@ type ReservedVersion struct {
|
||||
// Unstored are the artifacts a holder refused as not stored, with its words: said once, and not published again on
|
||||
// events (design 47 G §588); builder_build_again builds the key again, which reserves anew.
|
||||
Unstored map[string]string `json:"unstored,omitempty"`
|
||||
// Branch and Source are the watched branch and the source revision of the catalog revision whose entry the build came from
|
||||
// (design 47 G §371, A-57): what `package-published` says of the publication.
|
||||
Branch string `json:"branch,omitempty"`
|
||||
Source int64 `json:"source,omitempty"`
|
||||
// Dependencies are, by artifact reference, what the step's result says each artifact resolved (kind 1) and was built with
|
||||
// (kind 3), kept for `package-published` (design 47 G §466, §673, A-57).
|
||||
Dependencies map[string][]meshbuilds.Dependency `json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
// Source is what a build reserving a version is made from (design 47 G §370): the repository and commit the git tag is
|
||||
@@ -69,6 +77,10 @@ type Source struct {
|
||||
// Fingerprint is the fingerprint of the catalog entry the build is made from (design 47 G A-76).
|
||||
Fingerprint string
|
||||
GoPackage bool
|
||||
// Branch is the watched branch of the catalog revision whose entry the build came from, and Revision the source revision of
|
||||
// that catalog revision (design 47 G §371, A-57), kept on the reservation for the publication.
|
||||
Branch string
|
||||
Revision int64
|
||||
}
|
||||
|
||||
// ReservedKey is the key of module's reservation of series (MAJOR.MINOR).
|
||||
@@ -143,7 +155,7 @@ func (r *Reserver) Reserve(ctx context.Context, module, series string, artifacts
|
||||
}
|
||||
}
|
||||
out := ReservedVersion{V: 1, Module: module, Series: series, Version: fmt.Sprintf("%s.%d", series, next+1), Artifacts: map[string]string{}, At: r.now(),
|
||||
Repository: src.Repository, Commit: src.Commit, Fingerprint: src.Fingerprint}
|
||||
Repository: src.Repository, Commit: src.Commit, Fingerprint: src.Fingerprint, Branch: src.Branch, Source: src.Revision}
|
||||
if src.GoPackage {
|
||||
out.Tag = GoTagOf(out.Version)
|
||||
}
|
||||
@@ -180,7 +192,7 @@ func (r *Reserver) get(ctx context.Context, key string) (ReservedVersion, uint64
|
||||
|
||||
// Built records artifact's digest under the reserved version of module's series; once every artifact has one the build is
|
||||
// ended (BuiltAt). A reservation moved on to another version (a newer build took the next) is left alone and answers false.
|
||||
func (r *Reserver) Built(ctx context.Context, module, version, artifact, digest string) (bool, error) {
|
||||
func (r *Reserver) Built(ctx context.Context, module, version, artifact, digest string, deps []meshbuilds.Dependency) (bool, error) {
|
||||
series, _, err := SplitVersion(version)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -192,6 +204,12 @@ func (r *Reserver) Built(ctx context.Context, module, version, artifact, digest
|
||||
return false, err
|
||||
}
|
||||
cur.Artifacts[artifact] = digest
|
||||
if len(deps) > 0 {
|
||||
if cur.Dependencies == nil {
|
||||
cur.Dependencies = map[string][]meshbuilds.Dependency{}
|
||||
}
|
||||
cur.Dependencies[artifact] = deps
|
||||
}
|
||||
if cur.BuiltAt.IsZero() && allSet(cur.Artifacts) {
|
||||
cur.BuiltAt = r.now()
|
||||
}
|
||||
@@ -312,9 +330,24 @@ func (r *Reserver) Drop(ctx context.Context, module, series string) (bool, error
|
||||
return true, r.Store.Delete(ctx, key)
|
||||
}
|
||||
|
||||
// Marked is an artifact of a reservation a registry seat's published newly marked: the reservation as it stands then.
|
||||
type Marked struct {
|
||||
Reserved ReservedVersion
|
||||
Artifact string
|
||||
}
|
||||
|
||||
// PackagePublished is the seat mesh-builds' package-published of one artifact the holder said published at the reservation's
|
||||
// version (design 47 G §52, §334, §673, A-57): the artifact reference, the version and the digest published, the watched branch and
|
||||
// the source revision the build was made from, and the dependencies its step result named.
|
||||
func (m Marked) PackagePublished(digest string, at time.Time) meshbuilds.PackagePublished {
|
||||
at = at.UTC()
|
||||
return meshbuilds.PackagePublished{V: 1, Artifact: m.Artifact, At: &at, Branch: m.Reserved.Branch, Source: m.Reserved.Source, Module: m.Reserved.Module,
|
||||
Version: m.Reserved.Version, Digest: digest, Dependencies: m.Reserved.Dependencies[m.Artifact]}
|
||||
}
|
||||
|
||||
// Published takes a registry seat's published in: the artifacts of a reservation at version (a leading v is the Go form)
|
||||
// whose built digest it names are marked published. It answers the keys it marked.
|
||||
func (r *Reserver) Published(ctx context.Context, version, digest string) ([]string, error) {
|
||||
// whose built digest it names are marked published. It answers each artifact it newly marked, with the reservation as marked.
|
||||
func (r *Reserver) Published(ctx context.Context, version, digest string) ([]Marked, error) {
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
if version == "" || digest == "" {
|
||||
return nil, nil
|
||||
@@ -324,7 +357,7 @@ func (r *Reserver) Published(ctx context.Context, version, digest string) ([]str
|
||||
return nil, fmt.Errorf("the reservations cannot be listed: %w", err)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var marked []string
|
||||
var marked []Marked
|
||||
for _, k := range keys {
|
||||
again:
|
||||
cur, rev, found, err := r.get(ctx, k)
|
||||
@@ -335,14 +368,17 @@ func (r *Reserver) Published(ctx context.Context, version, digest string) ([]str
|
||||
continue
|
||||
}
|
||||
changed := false
|
||||
var arts []string
|
||||
for a, d := range cur.Artifacts {
|
||||
if d == digest && !cur.Published[a] {
|
||||
if cur.Published == nil {
|
||||
cur.Published = map[string]bool{}
|
||||
}
|
||||
cur.Published[a], changed = true, true
|
||||
arts = append(arts, a)
|
||||
}
|
||||
}
|
||||
sort.Strings(arts)
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
@@ -352,7 +388,9 @@ func (r *Reserver) Published(ctx context.Context, version, digest string) ([]str
|
||||
} else if err != nil {
|
||||
return marked, fmt.Errorf("%s cannot be written: %w", k, err)
|
||||
}
|
||||
marked = append(marked, k)
|
||||
for _, a := range arts {
|
||||
marked = append(marked, Marked{Reserved: cur, Artifact: a})
|
||||
}
|
||||
}
|
||||
return marked, nil
|
||||
}
|
||||
@@ -444,6 +482,53 @@ func AnswerReservedVerb(r Reader, module, version string) (meshbuilds.ReservedAn
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CatalogVersions answers a module's released versions, the seat mesh-catalog's verb versions (design 47 G §582).
|
||||
type CatalogVersions func(ctx context.Context, module string) ([]meshcatalog.VersionEntry, error)
|
||||
|
||||
// AnswerReservedByName is the verb reserved given a package's name (design 47 G §572, §582, A-70): the module is found from the
|
||||
// name in the catalog half's view (carriers); that module's reserved version answers as AnswerReservedVerb does. A version
|
||||
// given with no reservation of it (a reservation goes once its version is released) is asked of the catalog, and answers
|
||||
// released with the artifacts and digests the release recorded. A name no entry carries, more than one entry carries, or a
|
||||
// version neither reserved nor released is answered error, naming which, set alone.
|
||||
func AnswerReservedByName(ctx context.Context, r Reader, carriers func(context.Context, string) ([]Built, error), released CatalogVersions, name, version string) (meshbuilds.ReservedAnswer, error) {
|
||||
name, version = strings.TrimSpace(name), strings.TrimSpace(version)
|
||||
got, err := carriers(ctx, name)
|
||||
if err != nil {
|
||||
return meshbuilds.ReservedAnswer{}, fmt.Errorf("the entries of %s cannot be read: %w", name, err)
|
||||
}
|
||||
switch {
|
||||
case len(got) == 0:
|
||||
return meshbuilds.ReservedAnswer{Error: "no entry carries the name " + name}, nil
|
||||
case len(got) > 1:
|
||||
return meshbuilds.ReservedAnswer{Error: MoreThanOne(name, got)}, nil
|
||||
}
|
||||
module := got[0].Module
|
||||
rs := &Reserver{Store: readOnlyReserve{readStore{r}}}
|
||||
if _, found, err := rs.Of(ctx, module, version); err != nil {
|
||||
return meshbuilds.ReservedAnswer{}, err
|
||||
} else if found {
|
||||
return AnswerReservedVerb(r, module, version)
|
||||
}
|
||||
if version != "" {
|
||||
entries, err := released(ctx, module)
|
||||
if err != nil {
|
||||
return meshbuilds.ReservedAnswer{}, fmt.Errorf("the released versions of %s cannot be read: %w", module, err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.Version != version {
|
||||
continue
|
||||
}
|
||||
out := meshbuilds.ReservedAnswer{Module: module, Version: version, Released: true, Artifacts: []meshbuilds.ReservedAnswerArtifactsItem{}}
|
||||
for _, a := range e.Artifacts {
|
||||
out.Artifacts = append(out.Artifacts, meshbuilds.ReservedAnswerArtifactsItem{Artifact: a.Artifact, Digest: a.Digest})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return meshbuilds.ReservedAnswer{Error: fmt.Sprintf("%s (%s) has no version %s reserved or released", name, module, version)}, nil
|
||||
}
|
||||
return meshbuilds.ReservedAnswer{Error: fmt.Sprintf("%s (%s) has no reserved version", name, module)}, nil
|
||||
}
|
||||
|
||||
// The descriptions of the reservation's tools.
|
||||
const (
|
||||
ReservedDescription = "The versions the builder reserved (its state reserved, design 47 G section 369): one per module and MAJOR.MINOR, " +
|
||||
|
||||
@@ -78,11 +78,11 @@ func TestReservation(t *testing.T) {
|
||||
|
||||
// The artifacts' digests, then the bound.
|
||||
for _, a := range arts {
|
||||
if ok, err := r.Built(ctx, "m", "0.1.23", a, "sha256:"+a); err != nil || !ok {
|
||||
if ok, err := r.Built(ctx, "m", "0.1.23", a, "sha256:"+a, nil); err != nil || !ok {
|
||||
t.Fatalf("built %s: %v %v", a, ok, err)
|
||||
}
|
||||
}
|
||||
if ok, _ := r.Built(ctx, "m", "0.1.22", "m:a", "x"); ok {
|
||||
if ok, _ := r.Built(ctx, "m", "0.1.22", "m:a", "x", nil); ok {
|
||||
t.Error("a digest was written under a version a newer build superseded")
|
||||
}
|
||||
published := func(_, _, a string) bool { return a == "m:a" }
|
||||
@@ -118,11 +118,12 @@ func TestReservationIsReadAndMarkedPublished(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := &reserveMem{mem: newMem()}
|
||||
r := &Reserver{Store: store, ReleasedOf: func(context.Context, string) ([]string, error) { return []string{"1.4.2"}, nil }}
|
||||
v, err := r.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, Source{})
|
||||
v, err := r.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, Source{Branch: "main", Revision: 41})
|
||||
if err != nil || v.Version != "1.4.3" {
|
||||
t.Fatalf("reserved %q (%v), want 1.4.3", v.Version, err)
|
||||
}
|
||||
if _, err := r.Built(ctx, "m", "1.4.3", "m:a", "sha256:aa"); err != nil {
|
||||
deps := []meshbuilds.Dependency{{Kind: 1, Part: "mesh-sdk:go", Version: "0.1.14", Digest: "sha256:dd"}, {Kind: 3, Part: "go-toolchain:program", Version: "go1.27", Digest: "sha256:tt"}}
|
||||
if _, err := r.Built(ctx, "m", "1.4.3", "m:a", "sha256:aa", deps); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rd := Reader{
|
||||
@@ -142,9 +143,18 @@ func TestReservationIsReadAndMarkedPublished(t *testing.T) {
|
||||
if got, err := AnswerReserved(rd, "other"); err != nil || len(got["reserved"].([]ReservedVersion)) != 0 {
|
||||
t.Errorf("builder_reserved of another module = %v (%v), want none", got, err)
|
||||
}
|
||||
if marked, err := r.Published(ctx, "v1.4.3", "sha256:aa"); err != nil || len(marked) != 1 {
|
||||
marked, err := r.Published(ctx, "v1.4.3", "sha256:aa")
|
||||
if err != nil || len(marked) != 1 {
|
||||
t.Fatalf("a published of 1.4.3 with a built digest marked %v (%v)", marked, err)
|
||||
}
|
||||
// Design 47 G §52, §673, A-57: the package-published of that artifact carries the digest published, the branch and the source
|
||||
// revision the reservation was made for, and the dependencies its step result named.
|
||||
at := time.Unix(100, 0)
|
||||
said := marked[0].PackagePublished("sha256:aa", at)
|
||||
atUTC := at.UTC()
|
||||
if wantSaid := (meshbuilds.PackagePublished{V: 1, Artifact: "m:a", At: &atUTC, Branch: "main", Source: 41, Module: "m", Version: "1.4.3", Digest: "sha256:aa", Dependencies: deps}); !reflect.DeepEqual(said, wantSaid) {
|
||||
t.Errorf("package-published = %+v, want %+v", said, wantSaid)
|
||||
}
|
||||
if marked, _ := r.Published(ctx, "1.4.3", "sha256:other"); len(marked) != 0 {
|
||||
t.Errorf("a published with another digest marked %v", marked)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
)
|
||||
|
||||
// TestReservedByNameAnswersFromTheViewAndTheCatalog: design 47 G §572, §582, A-70 — a package's name (an npm name or a Go
|
||||
// module path) is mapped to its module from the catalog half's view; the reserved version answers as for the module, a version
|
||||
// no reservation holds is asked of the catalog's versions and answers released with the release's digests, and a name no entry
|
||||
// carries, one more than one carries, or a version neither reserved nor released answers error, set alone.
|
||||
func TestReservedByNameAnswersFromTheViewAndTheCatalog(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
view := packagesMem{
|
||||
"git=2enovox=2ebe/novox/mesh-sdk/go.0.1": []byte(`{"v":1,"name":"git.novox.be/novox/mesh-sdk/go","package":"mesh-sdk/go","series":"0.1"}`),
|
||||
"git=2enovox=2ebe/novox/mesh-sdk/go.0.2": []byte(`{"v":1,"name":"git.novox.be/novox/mesh-sdk/go","package":"mesh-sdk/go","series":"0.2"}`),
|
||||
"@novox/mesh-sdk.0.1": []byte(`{"v":1,"name":"@novox/mesh-sdk","package":"mesh-sdk/ts","series":"0.1"}`),
|
||||
"twice.0.1": []byte(`{"v":1,"name":"twice","package":"a/x","series":"0.1"}`),
|
||||
"twice-b.0.1": []byte(`{"v":1,"name":"twice","package":"b/y","series":"0.1"}`),
|
||||
}
|
||||
store := &reserveMem{mem: newMem()}
|
||||
r := &Reserver{Store: store, ReleasedOf: func(context.Context, string) ([]string, error) { return []string{"0.1.3"}, nil }}
|
||||
if _, err := r.Reserve(ctx, "mesh-sdk", "0.1", []string{"mesh-sdk:ts"}, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rd := Reader{
|
||||
Keys: func() ([]string, error) { return store.KeysOf(ctx, AllFilter) },
|
||||
Get: func(key string) (json.RawMessage, bool, error) {
|
||||
raw, _, ok, err := store.Get(ctx, key)
|
||||
return raw, ok, err
|
||||
}}
|
||||
var asked []string
|
||||
released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) {
|
||||
asked = append(asked, module)
|
||||
return []meshcatalog.VersionEntry{{Module: module, Version: "0.1.2", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "mesh-sdk:go", Digest: "sha256:dd"}}}}, nil
|
||||
}
|
||||
carriers := Carriers(view)
|
||||
ask := func(name, version string) meshbuilds.ReservedAnswer {
|
||||
t.Helper()
|
||||
got, err := AnswerReservedByName(ctx, rd, carriers, released, name, version)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", name, version, err)
|
||||
}
|
||||
return got
|
||||
}
|
||||
if got := ask("@novox/mesh-sdk", ""); got.Module != "mesh-sdk" || got.Version != "0.1.4" || got.Released || got.Error != "" {
|
||||
t.Errorf("an npm name = %+v, want the module's reserved version", got)
|
||||
}
|
||||
if got := ask("git.novox.be/novox/mesh-sdk/go", "0.1.2"); !got.Released || got.Module != "mesh-sdk" || len(got.Artifacts) != 1 || got.Artifacts[0].Digest != "sha256:dd" {
|
||||
t.Errorf("a Go module path = %+v, want the release's artifacts and digests (two series of one entry are one entry)", got)
|
||||
}
|
||||
if len(asked) != 1 || asked[0] != "mesh-sdk" {
|
||||
t.Errorf("the catalog was asked for %v, want the module's versions once, for the version no reservation holds", asked)
|
||||
}
|
||||
for name, c := range map[string]struct{ name, version, want string }{
|
||||
"unknown name": {"nothing", "", "no entry carries"},
|
||||
"two entries": {"twice", "", "more than one entry"},
|
||||
"neither released": {"git.novox.be/novox/mesh-sdk/go", "9.9.9", "reserved or released"},
|
||||
} {
|
||||
if got := ask(c.name, c.version); !strings.Contains(got.Error, c.want) || got.Module != "" || len(got.Artifacts) != 0 {
|
||||
t.Errorf("%s = %+v, want error naming %q alone", name, got, c.want)
|
||||
}
|
||||
}
|
||||
if _, err := AnswerReservedByName(ctx, rd, Carriers(packagesMem(nil)), released, "x", ""); err == nil {
|
||||
t.Error("an unreadable view was answered as if no entry carried the name")
|
||||
}
|
||||
if _, err := AnswerReservedByName(ctx, rd, carriers, func(context.Context, string) ([]meshcatalog.VersionEntry, error) { return nil, errors.New("down") }, "git.novox.be/novox/mesh-sdk/go", "0.1.2"); err == nil {
|
||||
t.Error("an unreadable catalog was answered as if the version were not released")
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
)
|
||||
|
||||
// The builder's tools of its versions (design 51 *Events of the builder*, *Tools of the builder*).
|
||||
@@ -64,6 +66,9 @@ type VersionsAnswer struct {
|
||||
Asked string `json:"asked,omitempty"`
|
||||
Recorded *bool `json:"recorded,omitempty"`
|
||||
Why string `json:"why,omitempty"`
|
||||
// Reserved is what the seat mesh-builds' verb reserved answers for the name (and version): the reserved or released version
|
||||
// with each artifact's digest, or its error (design 47 G §572, §582).
|
||||
Reserved *meshbuilds.ReservedAnswer `json:"reserved,omitempty"`
|
||||
}
|
||||
|
||||
// AnswerVersions is builder_versions: a name's versions in order, in one call; with a version (and a digest), whether
|
||||
|
||||
@@ -258,7 +258,8 @@ func TestTheBuildersVersions(t *testing.T) {
|
||||
if f.Known() || !reflect.DeepEqual(f.Silent, []string{SourceTags}) || why[SourceTags] != "no answer" {
|
||||
t.Fatalf("Ask = %+v, %v; want bridge_tags silent", f, why)
|
||||
}
|
||||
var fw FloorWaits
|
||||
holders := map[string]string{SourceSeats[SourceTags]: "mesh-delivery-forge-bridge", SourceSeats[SourceRequirements]: "mesh-delivery-indexer"}
|
||||
fw := FloorWaits{Holder: func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok }}
|
||||
w := fw.Wait("m/program", "oci-artifact-registry", "distribution", f, why, time.Unix(0, 0))
|
||||
if !strings.Contains(w.Finding(), "bridge_tags (no answer)") {
|
||||
t.Errorf("finding = %q", w.Finding())
|
||||
|
||||
@@ -119,8 +119,8 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
// 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and
|
||||
// holds no capacity; it puts asks on the three build seats and reads their steps (ADR 0525 §1, §4, §5). Whether a
|
||||
// node's machine is heard it asks the watchdogs seat's presence (issue 1966, ADR 0596 §2, ADR 0668 §3).
|
||||
if want := []string{"mesh-build-go.steps", "mesh-build-image.steps", "mesh-build-node.steps",
|
||||
"mesh-delivery-assigner.assignments", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
||||
if want := []string{"mesh-delivery-assigner.assignments",
|
||||
"node-build-go.steps", "node-build-image.steps", "node-build-node.steps"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
||||
t.Errorf("reads = %v, want %v", m.Reads, want)
|
||||
}
|
||||
if want := []string{"node-build-go", "node-build-image", "node-build-node", "operator-channel"}; !reflect.DeepEqual(sorted(m.Uses), want) {
|
||||
@@ -129,9 +129,9 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
// 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no
|
||||
// other tool; it holds no capacity.
|
||||
// 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's `held` is no longer asked (issue 1955).
|
||||
// DLV-M1 (ADR 0615 §1): the indexer's modules, manifests and candidates and the assigner's assignments are asked by their
|
||||
// read tools and followed by their views families, never read; so are the indexer's packages, whose names a package is
|
||||
// numbered under (design 32).
|
||||
// DLV-M1 (ADR 0615 §1): the assigner's assignments are asked by their read tool and followed by their views family, never
|
||||
// read; the catalog half (the modules, their manifests, their packages and the package names a package is numbered under) is
|
||||
// read through the seat mesh-catalog's branch follower (design 47 G §348, §614), and the indexer's states not at all.
|
||||
for _, r := range m.Reads {
|
||||
// The toolchain gate follows the assigner's assignments through meshmoduleassignerclient (buildrun.AssignmentsBucket), so
|
||||
// it declares that one read, as the console and route-proxy do; everything else of theirs is asked by tools.
|
||||
@@ -141,11 +141,10 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
}
|
||||
// Design 51 *The floor*: the floor's three sources, the registry seats' versions, bridge_tags and indexer_requirements.
|
||||
// The watchdogs seat's presence says a node's machine is heard (issue 1966, ADR 0596 §2, ADR 0668 §3).
|
||||
if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.publish", "seat:container-image-registry.remove", "seat:container-image-registry.versions",
|
||||
if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_requirements", "seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.seat-definitions", "seat:mesh-catalog.revision", "seat:mesh-delivery.source", "seat:container-image-registry.publish", "seat:container-image-registry.remove", "seat:container-image-registry.versions",
|
||||
"seat:git.tag",
|
||||
"seat:go-package-registry.publish", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.publish", "seat:npm-package-registry.remove",
|
||||
"seat:npm-package-registry.versions", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
|
||||
"seat:go-package-registry.publish", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-builds.reserved", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.publish", "seat:npm-package-registry.remove",
|
||||
"seat:npm-package-registry.versions", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), sorted(want)) {
|
||||
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
||||
}
|
||||
// The builder hears the build seats' toolchain-reported and the node-engine seat's running (design 47 G §492, §546) and reads no
|
||||
@@ -156,12 +155,11 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
// DLV-M1: the indexer's and the assigner's views families of the states it follows.
|
||||
if want := []string{"container-image-registry.kept", "container-image-registry.published", "container-image-registry.refused", "container-image-registry.removed",
|
||||
"go-package-registry.kept", "go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed",
|
||||
"mesh-artifact-sweep.artifacts-gone", "mesh-catalog.manifest-accepted", "mesh-catalog.version-released", "mesh-catalog.versions-filled",
|
||||
"mesh-artifact-sweep.artifacts-gone", "mesh-catalog.manifest-accepted", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-catalog.version-released", "mesh-catalog.versions-filled",
|
||||
"mesh-conditions.condition-cleared", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments", "mesh-delivery-forge-bridge.forge.**",
|
||||
"mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.packages", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.manifest-reads-changed", "node-engine.running",
|
||||
"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.manifest-reads-changed", "node-engine.running",
|
||||
"npm-package-registry.kept", "npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed",
|
||||
"oci-artifact-registry.kept", "oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed", "operator-channel.decided"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
|
||||
"oci-artifact-registry.kept", "oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed", "operator-channel.decided"}; !reflect.DeepEqual(sorted(m.Consumes), sorted(want)) {
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
}
|
||||
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
|
||||
@@ -172,7 +170,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
}
|
||||
// 1945: the builder's tools (builder_seat_queue: note 62), served by its tools bundle, and the event it says when it asks an ask again.
|
||||
if want := []string{"builder_announce_again", "builder_ask_again", "builder_bounds", "builder_build", "builder_build_again", "builder_builds_since", "builder_engine_reads", "builder_first_view_again", "builder_first_views",
|
||||
"builder_floor_again", "builder_floor_waits", "builder_old_form", "builder_publish_again", "builder_publish_queue", "builder_release", "builder_reserved", "builder_reserved_drop", "builder_seat_queue", "builder_stuck", "builder_toolchains", "builder_toolchains_drop", "builder_versions", "builder_waits", "reserved"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
||||
"builder_floor_again", "builder_floor_waits", "builder_old_form", "builder_publish_again", "builder_publish_queue", "builder_release", "builder_reserved", "builder_reserved_drop", "builder_seat_list", "builder_seat_list_again", "builder_seat_queue", "builder_stuck", "builder_toolchains", "builder_toolchains_drop", "builder_versions", "builder_waits", "reserved"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
// Design 47 G §672: the toolchain waits' stream, kept 7 days within 16 MiB, and the bound's setting, 60 minutes.
|
||||
@@ -278,6 +276,10 @@ func TestServesNoRequestSubjectAndHoldsNoTicker(t *testing.T) { // 1607/AC6 (ADR
|
||||
return err
|
||||
}
|
||||
for _, f := range forbidden {
|
||||
// The one subscription is the seat mesh-builds' accepted kind `step`, the runner's build ask (design 47 G A-97).
|
||||
if f == "Subscribe(" && filepath.ToSlash(path) == "cmd/mesh-delivery-builder/step.go" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(string(src), f) {
|
||||
t.Errorf("%s holds %s: the builder serves no request subject and holds no timer", path, f)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
],
|
||||
"tools": [
|
||||
"builder_announce_again",
|
||||
"builder_seat_list",
|
||||
"builder_seat_list_again",
|
||||
"builder_ask_again",
|
||||
"builder_bounds",
|
||||
"builder_build",
|
||||
@@ -53,6 +55,8 @@
|
||||
"go-package-registry.removed",
|
||||
"mesh-artifact-sweep.artifacts-gone",
|
||||
"mesh-catalog.manifest-accepted",
|
||||
"mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed",
|
||||
"mesh-catalog.version-released",
|
||||
"mesh-catalog.versions-filled",
|
||||
"mesh-conditions.condition-cleared",
|
||||
@@ -71,10 +75,9 @@
|
||||
"oci-artifact-registry.refused",
|
||||
"oci-artifact-registry.removed",
|
||||
"mesh-delivery-assigner.views.assignments",
|
||||
"mesh-delivery-indexer.views.candidates",
|
||||
"mesh-delivery-indexer.views.manifests",
|
||||
"mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.packages"
|
||||
"mesh-catalog.catalog-revision",
|
||||
"mesh-catalog.registration-changed",
|
||||
"mesh-delivery.source-moved"
|
||||
],
|
||||
"invokes": [
|
||||
"mesh-delivery-forge-bridge.bridge_tags",
|
||||
@@ -85,9 +88,7 @@
|
||||
"seat:go-package-registry.remove",
|
||||
"seat:go-package-registry.versions",
|
||||
"seat:mesh-artifact-sweep.held",
|
||||
"seat:mesh-catalog.entry",
|
||||
"seat:mesh-catalog.registrations",
|
||||
"seat:mesh-catalog.revision",
|
||||
"seat:mesh-builds.reserved",
|
||||
"seat:mesh-catalog.versions",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:mesh-module-assigner.assignments",
|
||||
@@ -101,14 +102,15 @@
|
||||
"seat:oci-artifact-registry.remove",
|
||||
"seat:oci-artifact-registry.versions",
|
||||
"mesh-delivery-assigner.assigner_read_assignments",
|
||||
"mesh-delivery-indexer.indexer_read_candidates",
|
||||
"mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-indexer.indexer_read_modules",
|
||||
"mesh-delivery-indexer.indexer_read_packages",
|
||||
"seat:container-image-registry.publish",
|
||||
"seat:go-package-registry.publish",
|
||||
"seat:npm-package-registry.publish",
|
||||
"seat:oci-artifact-registry.publish"
|
||||
"seat:oci-artifact-registry.publish",
|
||||
"seat:mesh-catalog.entry",
|
||||
"seat:mesh-catalog.registrations",
|
||||
"seat:mesh-catalog.seat-definitions",
|
||||
"seat:mesh-catalog.revision",
|
||||
"seat:mesh-delivery.source"
|
||||
],
|
||||
"state": [
|
||||
{ "name": "builds", "history": 5 },
|
||||
@@ -157,11 +159,10 @@
|
||||
{ "name": "build-log", "max-age-days": 30, "max-mib": 3072 }
|
||||
],
|
||||
"reads": [
|
||||
"mesh-build-go.steps",
|
||||
"mesh-build-image.steps",
|
||||
"mesh-build-node.steps",
|
||||
"mesh-delivery-assigner.assignments",
|
||||
"mesh-delivery-gate.source"
|
||||
"node-build-go.steps",
|
||||
"node-build-image.steps",
|
||||
"node-build-node.steps",
|
||||
"mesh-delivery-assigner.assignments"
|
||||
],
|
||||
"uses": [
|
||||
"node-build-go",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// The checker's names of the build modules, kept until its part finds them on the seat list as the builder does (design 47 G
|
||||
// A-82 is the builder's change; the builder's package holds no name of them any more): the holders of the three build seats.
|
||||
const (
|
||||
goBuildModule = "mesh-build-go"
|
||||
nodeBuildModule = "mesh-build-node"
|
||||
)
|
||||
|
||||
// checkerBuildSeats is the finder over the build seats those names hold.
|
||||
var checkerBuildSeats = func() *buildrun.BuildSeats {
|
||||
rule := func(on string) seatdef.Definition {
|
||||
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
|
||||
}
|
||||
defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript")}
|
||||
holders := map[string]string{"node-build-go": goBuildModule, "node-build-node": nodeBuildModule}
|
||||
return buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })
|
||||
}()
|
||||
@@ -404,7 +404,7 @@ func (r *repoModules) PackagePaths(repository string) []string {
|
||||
// is judged by both, each finding its own directories in the tree (go.mod files; package.json files), so a repository
|
||||
// with tests in either language and no manifest is judged whole (issue 1887 A12).
|
||||
func (r *repoModules) BuildModules(repository string) []string {
|
||||
out := []string{buildrun.GoBuildModule}
|
||||
out := []string{goBuildModule}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
known := false
|
||||
@@ -414,12 +414,12 @@ func (r *repoModules) BuildModules(repository string) []string {
|
||||
}
|
||||
known = true
|
||||
if r.langs[r.typed[module]]["typescript"] {
|
||||
out = append(out, buildrun.NodeBuildModule)
|
||||
out = append(out, nodeBuildModule)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !known {
|
||||
out = append(out, buildrun.NodeBuildModule)
|
||||
out = append(out, nodeBuildModule)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
|
||||
@@ -70,10 +69,10 @@ func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *test
|
||||
r := &repoModules{manifests: store}
|
||||
putModule(t, r, "mesh-delivery-checker", "novox/mesh-delivery", goManifest)
|
||||
putModule(t, r, "mesh-ui", "novox/mesh-catalog", tsManifest)
|
||||
if got := r.BuildModules("novox/mesh-delivery"); strings.Join(got, ",") != buildrun.GoBuildModule {
|
||||
if got := r.BuildModules("novox/mesh-delivery"); strings.Join(got, ",") != goBuildModule {
|
||||
t.Errorf("mesh-delivery: %v, want the Go build module alone", got)
|
||||
}
|
||||
if got := r.BuildModules("novox/mesh-catalog"); strings.Join(got, ",") != buildrun.GoBuildModule+","+buildrun.NodeBuildModule {
|
||||
if got := r.BuildModules("novox/mesh-catalog"); strings.Join(got, ",") != goBuildModule+","+nodeBuildModule {
|
||||
t.Errorf("mesh-catalog: %v, want both", got)
|
||||
}
|
||||
if got := r.CommitOf("mesh-delivery-checker"); got != strings.Repeat("a", 40) || r.CommitOf("gone") != "" {
|
||||
@@ -81,7 +80,7 @@ func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *test
|
||||
}
|
||||
// 1887/A12: a repository with no module the mesh knows (mesh-lab: nested go.mod files and a root package.json) is
|
||||
// judged by both build modules.
|
||||
if got := r.BuildModules("novox/mesh-lab"); strings.Join(got, ",") != buildrun.GoBuildModule+","+buildrun.NodeBuildModule {
|
||||
if got := r.BuildModules("novox/mesh-lab"); strings.Join(got, ",") != goBuildModule+","+nodeBuildModule {
|
||||
t.Errorf("a repository with no module: %v, want both build modules", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
if link == "" {
|
||||
link = defaultLink
|
||||
}
|
||||
tools := &buildrun.ToolchainSet{}
|
||||
tools := &buildrun.ToolchainSet{Seats: checkerBuildSeats}
|
||||
presence := &buildrun.PresenceSet{}
|
||||
trunks := &check.Trunks{}
|
||||
// A check is one ask per pull request and build module on the build seats' check stream, pulled by a build node with a
|
||||
@@ -228,7 +228,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
defer hearing.Stop()
|
||||
// Each ask's step is heard by its own watch and, at every step end of its build module and every reconnect, read
|
||||
// (buildrun.HearEnds): a watch that goes deaf loses no end (the lost check of 2026-10-28).
|
||||
heard := buildrun.HearEnds(ctx, asks, asks, []string{buildrun.GoBuildModule, buildrun.NodeBuildModule}, logger.Printf)
|
||||
heard := buildrun.HearEnds(ctx, asks, asks, []string{goBuildModule, nodeBuildModule}, logger.Printf)
|
||||
conn.SetReconnectHandler(func(*nats.Conn) { hearing.Again(); heard.Nudge("") })
|
||||
waiter := buildrun.LostAskWaiter{Inner: heard, Dead: dead, Hearing: hearing}
|
||||
asksKV := process.AwaitBucket(ctx, js, conn, check.AsksBucket, "checker_ask_again refuses every check, as it cannot tell whether its ask waits in a stream", logger.Printf)
|
||||
@@ -285,7 +285,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
Asker: engineAsks{call: caller.CallOn},
|
||||
Placed: placedNodes{kv: func(context.Context) (jetstream.KeyValue, error) { return assignments, nil }}.Placed,
|
||||
Bound: answerBound,
|
||||
Sandbox: seatSandbox{asker: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}},
|
||||
Sandbox: seatSandbox{asker: buildrun.Asker{Seats: checkerBuildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}},
|
||||
Verdicts: verdicts.all,
|
||||
// The node-engine's trunk head: a group with no node-engine member composes with the composer built there (ADR 0656 §5).
|
||||
Composer: func() string { return trunks.Heads()[check.NodeEngineRepository] },
|
||||
@@ -314,7 +314,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
ch.Runs = repos // the node-engine a check reads beside at its running commit (ADR 0548 §6)
|
||||
ch.Says = verdictSays{js: js, node: cred.Node, now: time.Now} // a pull request's verdict event, on the stream asks (design 26 *Out of step*)
|
||||
ch.Judge = newJudge(judgeDeps{
|
||||
asks: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}, tools: tools, trunks: trunks,
|
||||
asks: buildrun.Asker{Seats: checkerBuildSeats, Publisher: asks, Waiter: waiter, Bound: stepBound, Dropper: buildrun.SeatDrops{Caller: caller}}, tools: tools, trunks: trunks,
|
||||
kept: kvStore{buckets[check.CheckerKeptBucket]}, previews: kvStore{buckets[check.PreviewsBucket]}, candidates: tryCandidates{candidates},
|
||||
repos: repos, published: published, builds: buildStates, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf,
|
||||
announce: announce, compositions: compositions, inputs: runInputs{awaitedKV{inputsKV}}, beginBound: beginBound,
|
||||
@@ -331,7 +331,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
watch := newToolchainWatch(ctx, js, tools, ch.NodesChanged, logger)
|
||||
// Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the
|
||||
// assignments is settled first, so the toolchains settle through the gate already knowing it.
|
||||
gate := buildrun.NewToolchainGate([]string{buildrun.GoBuildModule, buildrun.NodeBuildModule}, tools.Put, logger.Printf)
|
||||
gate := buildrun.NewToolchainGate([]string{goBuildModule, nodeBuildModule}, tools.Put, logger.Printf)
|
||||
watch.put = gate.Toolchain
|
||||
// Until the assignments's first view is answered the gate passes every report on, as before the assignments is settled.
|
||||
assignmentsErrs := make(chan error, 1)
|
||||
@@ -354,7 +354,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// A build module says each time it wrote its toolchains key: its stream, and the checker's durable consumer on it, are
|
||||
// made by the controller. Where the stream is not there yet the checker says so and is started again with the build
|
||||
// module.
|
||||
for _, module := range []string{buildrun.GoBuildModule, buildrun.NodeBuildModule} {
|
||||
for _, module := range []string{goBuildModule, nodeBuildModule} {
|
||||
stream := toolchainStreams[module]
|
||||
cons, err := cred.EventConsumer(ctx, js, stream)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// its grant. The gate over the seat's assignments, and its refusal, are mesh-sdk's (buildrun.ToolchainGate,
|
||||
// issue 774).
|
||||
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
|
||||
set := &buildrun.ToolchainSet{}
|
||||
set := &buildrun.ToolchainSet{Seats: checkerBuildSeats}
|
||||
var logs bytes.Buffer
|
||||
logger := log.New(&logs, "", 0)
|
||||
refused := refusingBus{err: errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-build-node_toolchains")}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
// toolchainStreams are the stream each build module carries its toolchain.reported event on, as the bus names it
|
||||
// (declared by the build modules, heard by this module through consumes).
|
||||
var toolchainStreams = map[string]string{buildrun.GoBuildModule: "TOOLCHAINS_GO", buildrun.NodeBuildModule: "TOOLCHAINS_NODE"}
|
||||
var toolchainStreams = map[string]string{goBuildModule: "TOOLCHAINS_GO", nodeBuildModule: "TOOLCHAINS_NODE"}
|
||||
|
||||
// toolchainWatch keeps the build modules' toolchains in a buildrun.ToolchainSet and tells the builder when one changed. A
|
||||
// build module's bucket is made when it is assigned, after the builder started, and the builder holds no timer (ADR 0437
|
||||
@@ -37,12 +37,12 @@ type toolchainWatch struct {
|
||||
}
|
||||
|
||||
func newToolchainWatch(ctx context.Context, js bucketOpener, set *buildrun.ToolchainSet, changed func(context.Context), logger *log.Logger) *toolchainWatch {
|
||||
return &toolchainWatch{ctx: ctx, js: js, set: set, put: set.Put, changed: changed, logger: logger, errs: make(chan error, len(buildrun.LanguageOf)), watched: map[string]bool{}}
|
||||
return &toolchainWatch{ctx: ctx, js: js, set: set, put: set.Put, changed: changed, logger: logger, errs: make(chan error, len(checkerBuildSeats.Modules())), watched: map[string]bool{}}
|
||||
}
|
||||
|
||||
// start watches the buckets that exist now; a missing one is said and waits for its build module's event.
|
||||
func (w *toolchainWatch) start() error {
|
||||
for module := range buildrun.LanguageOf {
|
||||
for _, module := range checkerBuildSeats.Modules() {
|
||||
if err := w.ensure(module); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,7 +96,7 @@ type delivered interface {
|
||||
// terminated; a failure stays unacknowledged, and the server delivers it again.
|
||||
func (w *toolchainWatch) Reported(m delivered) {
|
||||
module, _, _ := strings.Cut(strings.TrimPrefix(m.Subject(), "mesh.stream."), ".event.")
|
||||
if _, known := buildrun.LanguageOf[module]; !known || !strings.HasPrefix(m.Subject(), "mesh.stream.") {
|
||||
if _, known := checkerBuildSeats.LanguageOf(module); !known || !strings.HasPrefix(m.Subject(), "mesh.stream.") {
|
||||
w.logger.Printf("%s: not a build module's event, dropped", m.Subject())
|
||||
_ = m.Term()
|
||||
return
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
@@ -75,13 +74,13 @@ func (st *state) buildsOfTheCheck(ctx context.Context) (map[string]string, []str
|
||||
if !readable {
|
||||
return wait, words
|
||||
}
|
||||
for _, bm := range slices.Sorted(maps.Keys(buildrun.LanguageOf)) {
|
||||
for _, bm := range slices.Sorted(maps.Keys(languageOf)) {
|
||||
if _, alters := changed[bm]; !alters {
|
||||
continue
|
||||
}
|
||||
var built []string
|
||||
for module, m := range changed {
|
||||
if _, isBuildModule := buildrun.LanguageOf[module]; !isBuildModule && buildsWith(m, buildrun.LanguageOf[bm]) {
|
||||
if _, isBuildModule := languageOf[module]; !isBuildModule && buildsWith(m, languageOf[bm]) {
|
||||
built = append(built, module)
|
||||
delete(wait, module)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package check
|
||||
|
||||
// The checker's names of the build modules, kept until its part finds them on the seat list as the builder does (design 47 G
|
||||
// A-82 is the builder's change; the builder's package holds no name of them any more).
|
||||
const (
|
||||
goBuildModule = "mesh-build-go"
|
||||
nodeBuildModule = "mesh-build-node"
|
||||
)
|
||||
|
||||
// languageOf is the language each build module's toolchain reports hold.
|
||||
var languageOf = map[string]string{goBuildModule: "go", nodeBuildModule: "typescript"}
|
||||
@@ -0,0 +1,16 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// testSeats is the finder over the build seats the checker's names hold (buildmodules.go).
|
||||
func testSeats() *buildrun.BuildSeats {
|
||||
rule := func(on string) seatdef.Definition {
|
||||
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
|
||||
}
|
||||
defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript")}
|
||||
holders := map[string]string{"node-build-go": goBuildModule, "node-build-node": nodeBuildModule}
|
||||
return buildrun.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })
|
||||
}
|
||||
@@ -450,7 +450,7 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
(buildrun.Asker{Publisher: fake, Waiter: fake, Bound: time.Hour, Dropper: drops}).Wait(run, buildstep.StepAsk{ID: "mvl0frv18d2000", Language: buildstep.AskGo, Kind: buildstep.AskCheck})
|
||||
(buildrun.Asker{Seats: testSeats(), Publisher: fake, Waiter: fake, Bound: time.Hour, Dropper: drops}).Wait(run, buildstep.StepAsk{ID: "mvl0frv18d2000", Language: buildstep.AskGo, Kind: buildstep.AskCheck})
|
||||
}()
|
||||
ch.Forget("g")
|
||||
<-done
|
||||
|
||||
@@ -106,7 +106,7 @@ func (l langs) BuildModules(repository string) []string {
|
||||
if m, ok := l[repository]; ok {
|
||||
return m
|
||||
}
|
||||
return []string{buildrun.GoBuildModule}
|
||||
return []string{goBuildModule}
|
||||
}
|
||||
|
||||
func goEntries(nodes ...string) entries {
|
||||
@@ -207,7 +207,7 @@ func refsOf(v any) []BesideRef {
|
||||
}
|
||||
|
||||
// toolPrefix is the start of each build module's tool names, as the fake build modules answer them.
|
||||
var toolPrefix = map[string]string{buildrun.GoBuildModule: "build_go_", buildrun.NodeBuildModule: "build_node_"}
|
||||
var toolPrefix = map[string]string{goBuildModule: "build_go_", nodeBuildModule: "build_node_"}
|
||||
|
||||
// answer runs a check ask as the build module that pulls it does (gobuild.RunAsk), with the fake tools on shanks and the
|
||||
// bench's kept verdicts, and answers the step it writes.
|
||||
@@ -228,7 +228,7 @@ func (b *bench) answer(a buildstep.StepAsk) buildstep.Step {
|
||||
k, found, err := (Kept{Store: b.kept}).Lookup(ctx, level, digest)
|
||||
return int(k.Tests), found, err
|
||||
}}
|
||||
if module == buildrun.GoBuildModule {
|
||||
if module == goBuildModule {
|
||||
steps.Digest = tool("digest")
|
||||
}
|
||||
began := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
|
||||
@@ -278,7 +278,7 @@ func newBench(t testing.TB) *bench {
|
||||
}
|
||||
now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
|
||||
b.asks = &buildrun.FakeAsks{Answer: b.answer}
|
||||
b.ex = &Examiner{Asks: buildrun.Asker{Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 5 * time.Second}, Toolchains: goEntries("shanks"), Kept: Kept{Store: b.kept}, Previews: b.prev,
|
||||
b.ex = &Examiner{Asks: buildrun.Asker{Seats: testSeats(), Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 5 * time.Second}, Toolchains: goEntries("shanks"), Kept: Kept{Store: b.kept}, Previews: b.prev,
|
||||
Modules: langs{}, Trunks: &Trunks{heads: map[string]string{DeliveryRepository: trunkD}}, Inputs: memInputs{b.inputs}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }}
|
||||
return b
|
||||
}
|
||||
@@ -559,7 +559,7 @@ func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3
|
||||
|
||||
func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1292/AC1
|
||||
b := newBench(t)
|
||||
b.ex.Modules = langs{repoA: {buildrun.NodeBuildModule}}
|
||||
b.ex.Modules = langs{repoA: {nodeBuildModule}}
|
||||
b.fetch = captured[Fetched](t, "node-fetch-clean")
|
||||
b.test = func(c stepCall) (any, error) {
|
||||
return Tested{Level: c.args["level"].(string), Module: "modules/m", Passed: true, Elapsed: 0.4, Bound: 5}, nil
|
||||
@@ -991,7 +991,7 @@ func (p pkgPaths) PackagePaths(repository string) []string { return p[repository
|
||||
// groupPackageBench is a group of a TypeScript member (mesh-delivery) and the mesh-sdk member, whose merge changes sdkChanged.
|
||||
func groupPackageBench(t *testing.T, sdkChanged []string) *bench {
|
||||
b := newBench(t)
|
||||
b.ex.Modules = langs{repoA: {buildrun.GoBuildModule, buildrun.NodeBuildModule}, repoSDK: {buildrun.GoBuildModule, buildrun.NodeBuildModule}}
|
||||
b.ex.Modules = langs{repoA: {goBuildModule, nodeBuildModule}, repoSDK: {goBuildModule, nodeBuildModule}}
|
||||
b.ex.Packages = pkgPaths{repoSDK: {"src", "test", "package.json", "package-lock.json", "tsconfig.json"}}
|
||||
fetch := func(c stepCall) (any, error) {
|
||||
f := b.fetch
|
||||
@@ -1006,7 +1006,7 @@ func groupPackageBench(t *testing.T, sdkChanged []string) *bench {
|
||||
|
||||
func nodeFetchOf(b *bench, repo string) stepCall {
|
||||
for _, c := range b.steps.tool("fetch") {
|
||||
if c.module == buildrun.NodeBuildModule && c.args["repository"] == repo {
|
||||
if c.module == nodeBuildModule && c.args["repository"] == repo {
|
||||
return c
|
||||
}
|
||||
}
|
||||
@@ -1065,7 +1065,7 @@ func TestAnAskWhoseStepNeverEndsEndsTheCheckLoudly(t *testing.T) {
|
||||
b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
|
||||
return buildstep.Step{ID: a.ID, Tool: "check ask", Node: "shanks", State: buildstep.StepRunning, Started: time.Now()}
|
||||
}
|
||||
b.ex.Asks = buildrun.Asker{Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 20 * time.Millisecond}
|
||||
b.ex.Asks = buildrun.Asker{Seats: testSeats(), Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 20 * time.Millisecond}
|
||||
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
||||
if f := out.Verdict.Failed; out.Verdict.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(f.Words, "on shanks did not end within") {
|
||||
t.Fatalf("verdict = %+v", out.Verdict)
|
||||
@@ -1105,7 +1105,7 @@ func TestACheckAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
||||
b := newBench(t)
|
||||
answer := b.asks.Answer
|
||||
b.asks.Answer = nil
|
||||
b.ex.Asks = buildrun.Asker{Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 20 * time.Millisecond}
|
||||
b.ex.Asks = buildrun.Asker{Seats: testSeats(), Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 20 * time.Millisecond}
|
||||
done := make(chan Outcome, 1)
|
||||
go func() { done <- b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) }()
|
||||
for deadline := time.Now().Add(10 * time.Second); len(b.asks.Asks()) == 0; time.Sleep(time.Millisecond) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
// goModule is the build module the Go repositories are judged by.
|
||||
const goModule = buildrun.GoBuildModule
|
||||
const goModule = goBuildModule
|
||||
|
||||
// RunnerWorkspace is the runner the Go build module's digest and test steps are asked with when the merge changes the build
|
||||
// module itself: the installed module compiles the candidate's program from the merged tree and runs the step as it, shut
|
||||
@@ -69,7 +69,7 @@ type BuildModules interface {
|
||||
}
|
||||
|
||||
// askLanguage is the build seat language of each build module the checker asks.
|
||||
var askLanguage = map[string]string{buildrun.GoBuildModule: buildstep.AskGo, buildrun.NodeBuildModule: buildstep.AskNode}
|
||||
var askLanguage = map[string]string{goBuildModule: buildstep.AskGo, nodeBuildModule: buildstep.AskNode}
|
||||
|
||||
// Examiner judges a case through the build seats (ADR 0484 clauses 3, 4, 10, 13; ADR 0525 §1, §3): for each pull request
|
||||
// and build module it puts one check ask whose job fetches the head merged onto its trunk head, then runs the unit level
|
||||
@@ -545,17 +545,17 @@ func (st *state) judgeAll(ctx context.Context) {
|
||||
modules := st.j.Modules.BuildModules(p.Repository)
|
||||
for _, bm := range modules {
|
||||
t := &target{pull: p, module: bm}
|
||||
if tc, none := buildstep.AgreedToolchain(st.j.Toolchains.Entries(buildrun.LanguageOf[bm])); none == "" {
|
||||
if tc, none := buildstep.AgreedToolchain(st.j.Toolchains.Entries(languageOf[bm])); none == "" {
|
||||
t.toolchain = tc.Version
|
||||
t.agreed = &buildstep.ToolchainUsed{Language: buildrun.LanguageOf[bm], Version: tc.Version, Recipe: tc.Recipe}
|
||||
t.agreed = &buildstep.ToolchainUsed{Language: languageOf[bm], Version: tc.Version, Recipe: tc.Recipe}
|
||||
}
|
||||
args := buildstep.Fetch{Repository: p.Repository, Commit: p.Head, Ref: refOf(p.Number), Base: p.Trunk}
|
||||
if bm == buildrun.NodeBuildModule {
|
||||
if bm == nodeBuildModule {
|
||||
if groupPackage != nil && p.Repository != SDKRepository {
|
||||
args.Package = groupPackage
|
||||
}
|
||||
}
|
||||
if bm == buildrun.GoBuildModule {
|
||||
if bm == goBuildModule {
|
||||
beside, err := st.j.besideFor(ctx, st.c, p)
|
||||
if err != nil {
|
||||
st.fail(forgeMachine(err, bm), err.Error()+unheardWords(err))
|
||||
@@ -797,7 +797,7 @@ func unmetOf(levels []buildstep.LevelAnswer) string {
|
||||
// one that did not pass, so the verdict names every failed test of the level (issue 1921). It answers the level's
|
||||
// running time (its tests' and setup's).
|
||||
func (st *state) level(ctx context.Context, t *target, la buildstep.LevelAnswer) float64 {
|
||||
if t.runner != "" && t.module == buildrun.GoBuildModule && la.Digest != nil {
|
||||
if t.runner != "" && t.module == goBuildModule && la.Digest != nil {
|
||||
t.judgedBy(la.Digest.JudgedBy) // the installed module judged it: the runner is not used again
|
||||
}
|
||||
if t.runner != "" && la.Judged != "" {
|
||||
@@ -806,7 +806,7 @@ func (st *state) level(ctx context.Context, t *target, la buildstep.LevelAnswer)
|
||||
_, elapsed, results := st.levels.Level(t.pull.Repository, t.pull.Number, t.module, t.node, la)
|
||||
st.facts = append(st.facts, rules.Step{Repository: t.pull.Repository, Number: t.pull.Number, Module: t.module, Node: t.node, Level: rules.LevelFact(la)})
|
||||
var digests map[string]string
|
||||
if t.module == buildrun.GoBuildModule && la.Digest != nil {
|
||||
if t.module == goBuildModule && la.Digest != nil {
|
||||
// The packages come as a list, never keyed by a package path (issue 1930); an answer stored before reads too.
|
||||
digests = la.Digest.Map()
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (l *losingAsks) PublishAsk(ctx context.Context, subject, id string, data []
|
||||
|
||||
func losing(b *bench, lose int) *losingAsks {
|
||||
l := &losingAsks{FakeAsks: &buildrun.FakeAsks{}, answer: b.answer, dead: &buildrun.DeadLetters{}, lose: lose}
|
||||
b.ex.Asks = buildrun.Asker{Publisher: b.asker(l, l.FakeAsks.Write), Waiter: buildrun.LostAskWaiter{Inner: l.FakeAsks, Dead: l.dead}, Bound: 5 * time.Second}
|
||||
b.ex.Asks = buildrun.Asker{Seats: testSeats(), Publisher: b.asker(l, l.FakeAsks.Write), Waiter: buildrun.LostAskWaiter{Inner: l.FakeAsks, Dead: l.dead}, Bound: 5 * time.Second}
|
||||
return l
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
|
||||
tr = &troubled{FakeAsks: &buildrun.FakeAsks{}, answer: b.answer, restarts: row.restarts, deaf: row.deaf,
|
||||
deafIDs: map[string]bool{}, steps: map[string]buildstep.Step{}}
|
||||
heard = &buildrun.HeardEnds{Inner: tr, Read: tr}
|
||||
b.ex.Asks = buildrun.Asker{Publisher: b.asker(tr, tr.Write), Waiter: heard, Bound: 5 * time.Second}
|
||||
b.ex.Asks = buildrun.Asker{Seats: testSeats(), Publisher: b.asker(tr, tr.Write), Waiter: heard, Bound: 5 * time.Second}
|
||||
asked = func() int { return len(tr.Asks()) }
|
||||
}
|
||||
r := newRig(t, b.ex)
|
||||
@@ -307,23 +307,23 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
|
||||
}
|
||||
switch row.how {
|
||||
case "nudge":
|
||||
heard.Nudge(buildrun.NodeBuildModule) // another build module's step: not this one's to read
|
||||
heard.Nudge(buildrun.GoBuildModule)
|
||||
heard.Nudge(nodeBuildModule) // another build module's step: not this one's to read
|
||||
heard.Nudge(goBuildModule)
|
||||
case "hand":
|
||||
cands := newMemKV()
|
||||
ckey, _ := meshcatalogclient.CandidatesKey("feat/x")
|
||||
_, _ = cands.Create(ctx, ckey, cand)
|
||||
none := StateRead{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, error) { return nil, nil }}
|
||||
rd := Reader{CandidateState: kvRead(cands), VerdictState: kvRead(r.verdicts), AskState: kvRead(asksKept),
|
||||
StepStates: map[string]StateRead{buildrun.GoBuildModule: none}} // no node took its ask yet
|
||||
StepStates: map[string]StateRead{goBuildModule: none}} // no node took its ask yet
|
||||
if _, err := rd.AskAgain(asVerdicts{r.verdicts}, "feat/x", "its watch went deaf"); err == nil || !strings.Contains(err.Error(), "waits in its build seat's stream") {
|
||||
t.Errorf("asked again while its ask waits in the stream (issue 1791's kind): %v", err)
|
||||
}
|
||||
rd.StepStates[buildrun.GoBuildModule] = tr.Steps(true)
|
||||
rd.StepStates[goBuildModule] = tr.Steps(true)
|
||||
if _, err := rd.AskAgain(asVerdicts{r.verdicts}, "feat/x", "its watch went deaf"); err == nil || !strings.Contains(err.Error(), "a step of it runs") {
|
||||
t.Errorf("asked again while its step runs: %v", err)
|
||||
}
|
||||
rd.StepStates[buildrun.GoBuildModule] = tr.Steps(false)
|
||||
rd.StepStates[goBuildModule] = tr.Steps(false)
|
||||
if st, _ := rd.Status(); !strings.Contains(st.Lost["feat/x"], AskAgainTool) {
|
||||
t.Errorf("status lost = %v, want feat/x named with %s", st.Lost, AskAgainTool)
|
||||
}
|
||||
@@ -368,7 +368,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
|
||||
id := fmt.Sprint("mvl0frv18d19", 80+i)
|
||||
vals["ace."+id], _ = buildstep.EncodeStep(buildstep.Step{ID: id, Node: "ace", Tool: "check ask", State: buildstep.StepFailed, Passing: p, Ended: time.Now(), What: "check " + repoA + "@" + short12(head1)})
|
||||
}
|
||||
rd := Reader{CandidateState: kvRead(cands2(t, cand)), VerdictState: kvRead(failedKV), AskState: kvRead(newMemKV()), StepStates: map[string]StateRead{buildrun.GoBuildModule: {
|
||||
rd := Reader{CandidateState: kvRead(cands2(t, cand)), VerdictState: kvRead(failedKV), AskState: kvRead(newMemKV()), StepStates: map[string]StateRead{goBuildModule: {
|
||||
Keys: func() ([]string, error) { return slices.Collect(maps.Keys(vals)), nil }, Get: func(k string) (json.RawMessage, error) { return vals[k], nil }}}}
|
||||
if got, err := rd.AskAgain(asVerdicts{failedKV}, "feat/x", "the network"); (err == nil) != c.ok || (c.ok && !strings.HasPrefix(got.Said, AskedAgainWords)) {
|
||||
t.Errorf("%s: asked again = %+v, %v, want asked again %v", c.name, got, err, c.ok)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/grouppackages"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
@@ -119,7 +118,7 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
|
||||
b = newBench(t)
|
||||
b.fetch.Modules = []string{"modules/m"}
|
||||
b.ex.Read = fixedManifests{digest: sdk}
|
||||
b.ex.Modules = langs{repoA: {buildrun.GoBuildModule, buildrun.NodeBuildModule}}
|
||||
b.ex.Modules = langs{repoA: {goBuildModule, nodeBuildModule}}
|
||||
if v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), sdkPull)).Verdict; v.State != meshdeliverycheckerclient.VerdictPass {
|
||||
t.Errorf("a group's verdict = %+v", v)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package rules
|
||||
|
||||
// The checker's names of the build modules, kept until its part finds them on the seat list as the builder does (design 47 G
|
||||
// A-82 is the builder's change; the builder's package holds no name of them any more).
|
||||
const (
|
||||
goBuildModule = "mesh-build-go"
|
||||
nodeBuildModule = "mesh-build-node"
|
||||
)
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
|
||||
@@ -159,7 +158,7 @@ func (t Tally) Level(repository string, number int, module, node string, la buil
|
||||
}
|
||||
results = append(results, res)
|
||||
elapsed += res.Elapsed + res.Setup + res.Lint
|
||||
if module == buildrun.NodeBuildModule {
|
||||
if module == nodeBuildModule {
|
||||
if !res.NoTests {
|
||||
a.Run++ // a Node package is counted whole: its build module counts no tests (ADR 0484 clause 10)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
)
|
||||
|
||||
// The module graph's cycle rule (novox/hq ADR 0603 §2, ADR 0614 §3, §6), judged by the checking program (ADR 0656 §5):
|
||||
@@ -60,7 +58,7 @@ func JudgeGraph(g Graph) (*FailedFirst, []string) {
|
||||
return nil, []string{"the module graph has the cycle " + words + ", which the trunk already has among the same modules (ADR 0603 §2)"}
|
||||
}
|
||||
}
|
||||
return &FailedFirst{Machine: buildrun.GoBuildModule, Words: fmt.Sprintf("the change's module graph has the cycle %s: these modules would "+
|
||||
return &FailedFirst{Machine: goBuildModule, Words: fmt.Sprintf("the change's module graph has the cycle %s: these modules would "+
|
||||
"each wait for the other for good, through their bases, the provisions and bus state they await, or the verbs they invoke (ADR 0603 §2, ADR 0614 §6)", words)}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/composer"
|
||||
)
|
||||
@@ -21,7 +20,7 @@ import (
|
||||
// (milestone 47/B0, D1b): the running checker asks and records; these rules give the verdict.
|
||||
|
||||
// manifestsMachine is the machine a manifest's failure names: the build seat that ran the manifest validator.
|
||||
const manifestsMachine = buildrun.GoBuildModule
|
||||
const manifestsMachine = goBuildModule
|
||||
|
||||
// CatalogSeat is the seat the indexer holds: the module whose manifest claims it is the indexer, found by the claim and
|
||||
// never by its name (novox/hq ADR 0633 clause 6, ADR 0601 §4).
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
|
||||
@@ -183,7 +182,7 @@ func LevelFailure(repository, module, level string, results ...Tested) *meshdeli
|
||||
}
|
||||
for _, f := range res.Failed {
|
||||
name := FailedName(f)
|
||||
if module == buildrun.NodeBuildModule && f.Package == "" {
|
||||
if module == nodeBuildModule && f.Package == "" {
|
||||
name = res.Module
|
||||
}
|
||||
all = append(all, FailedFirst{Test: level + ": " + name, Words: FailureWordsAfter(f, HidWords(module, res.By, f))})
|
||||
|
||||
Reference in New Issue
Block a user