D1 (builder): the builder names its own module and buckets from its credential's module

Its states' buckets, its finding observer, its build log entries and its step asks' asker take the credential's module (tools: the module the runtime serves them as) instead of constants of its name; builds.StateBucket/BuildsBucket replace the Bucket constants. The readers of the builds bucket (checker, view, grants) still name the builder: the next D1 subtask finds it by the seat mesh-builds.

Builds: design 47 G A-296, §428

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 22:34:08 +01:00
parent 2271b805bb
commit 39db267630
24 changed files with 91 additions and 83 deletions
@@ -48,12 +48,17 @@ func checkAskLanguage(language string) error {
const (
// BuilderModule is the module of the builder part.
BuilderModule = "mesh-delivery-builder"
// BuilderBuildsBucket is the builder's bucket of builds: `<owner module>_<name>`.
BuilderBuildsBucket = BuilderModule + "_builds"
// BuilderBuildsHistory is the revisions the bucket keeps per key.
BuilderBuildsHistory = 5
)
// BuildsBucket is the builder's bucket of builds: `<module>_builds`, of the module the builder runs as (its credential's), never
// a constant of its name (design 47 G A-296).
func BuildsBucket(module string) string { return StateBucket(module, "builds") }
// StateBucket is the bus bucket of a module's state: `<module>_<state>`.
func StateBucket(module, state string) string { return module + "_" + state }
// The states of a build.
const (
BuildQueued = "queued"
@@ -203,8 +203,8 @@ func TestBuilderBuildVersion(t *testing.T) {
// 1358/AC6: the builder's bucket and module do not collide with the controller's build layout (buildrecord.go), which
// this change leaves as it was.
func TestBuilderLayoutDoesNotCollideWithTheControllers(t *testing.T) {
if BuilderBuildsBucket != "mesh-delivery-builder_builds" || BuilderBuildsHistory != 5 || BuilderModule != "mesh-delivery-builder" {
t.Errorf("bucket %s history %d module %s", BuilderBuildsBucket, BuilderBuildsHistory, BuilderModule)
if BuildsBucket(BuilderModule) != "mesh-delivery-builder_builds" || BuilderBuildsHistory != 5 || BuilderModule != "mesh-delivery-builder" {
t.Errorf("bucket %s history %d module %s", BuildsBucket(BuilderModule), BuilderBuildsHistory, BuilderModule)
}
if buildrecord.BuildKey("m", "7") != "m.build.7" || buildrecord.NewestKey("m") != "m.newest" {
t.Error("the controller's layout changed")
@@ -125,7 +125,7 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q
return nil, err
}
if err := askAgain(key, value); err != nil {
return nil, fmt.Errorf("the ask could not be written to %s: %w", build.ReAsksBucket, err)
return nil, fmt.Errorf("the ask could not be written to %s: %w", build.ReAsksState, err)
}
return map[string]any{"key": key, "asked": true,
"next": "the builder answers in " + build.ReAsksState + "." + key + "; " + build.StuckTool + " shows the answer and the new ask"}, nil
@@ -241,7 +241,7 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q
// askQueue writes the queues value at key and waits, on a watch of the key, at most bound for the builder's answer.
func askQueue(s stdio.KeptState, bound time.Duration) queueAsker {
return askKept(s, build.QueuesState, build.QueuesBucket, "the read", bound, func(v []byte) bool {
return askKept(s, build.QueuesState, builds.StateBucket(os.Getenv(stdio.ServedAs), build.QueuesState), "the read", bound, func(v []byte) bool {
a, err := build.DecodeQueueAsk(v)
return err == nil && !a.Answered.IsZero()
})
@@ -353,7 +353,7 @@ func main() {
_, err := reasks.Put(key, json.RawMessage(value))
return err
}
buildAgain := askKept(reasks, build.ReAsksState, build.ReAsksBucket, "the request", 25*time.Second, func(v []byte) bool {
buildAgain := askKept(reasks, build.ReAsksState, builds.StateBucket(os.Getenv(stdio.ServedAs), build.ReAsksState), "the request", 25*time.Second, func(v []byte) bool {
a, err := build.DecodeReAsk(v)
return err == nil && !a.Answered.IsZero()
})
@@ -398,7 +398,7 @@ func main() {
return e.Value, true, nil
}}
// 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...) },
pk := &views.Reader{Module: os.Getenv(stdio.ServedAs), 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 {
@@ -457,7 +457,7 @@ func main() {
tools = append(tools, enginereads.Tool(EngineReadsTool, "the builder", enginereads.KeptState(stdio.State("engine-reads")), true, time.Now))
// 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, views.StatusTools(os.Getenv(stdio.ServedAs), "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)
@@ -17,10 +17,6 @@ import (
"mesh-delivery-builder/internal/versions"
)
// findingsBucket is the builder's findings (novox/hq ADR 0564 §2): what it sees wrong now, written at its raise and
// deleted at its clear, which the condition keeper raises and clears.
var findingsBucket = builds.BuilderModule + "_" + mcseat.StateFindings
// heldWriter is the builder's finding writer: its findings are raised as they happen, so the writer holds them (design 47 G
// A-29); its `findings-asked` subject is the seat's, from the generated contract (A-17).
type heldWriter = views.HeldWriter[mcseat.Finding, mcseat.FindingCleared, mcseat.FindingsStated, mcseat.FindingsAskedEvent]
@@ -34,9 +30,11 @@ type findingWriter interface {
// findings writes the builder's findings through its held finding writer (design 47 G §689, A-29): a finding written before the
// writer starts is held and stated at its start.
type findings struct {
w findingWriter
logf func(string, ...any)
now func() time.Time
// module is the module the builder runs as, its credential's: the observer of every finding.
module string
w findingWriter
logf func(string, ...any)
now func() time.Time
}
// finding is one of the builder's findings: its kind, what it is about, and its words.
@@ -48,7 +46,7 @@ type finding struct {
}
// seatFinding is fd as the seat's generated finding at at, its id made safe as a key's part.
func (fd finding) seatFinding(at time.Time) mcseat.Finding {
func (fd finding) seatFinding(module string, at time.Time) mcseat.Finding {
at = at.UTC()
id := keyID(fd.id)
sev := fd.severity
@@ -63,7 +61,7 @@ func (fd finding) seatFinding(at time.Time) mcseat.Finding {
source = "builder." + fd.kind
}
return mcseat.Finding{Scope: fd.scope, ID: id, Kind: fd.kind, Severity: sev, Summary: fd.summary, Headline: fd.headline,
Explanation: fd.explanation, Needs: fd.needs, Source: source, Observer: builds.BuilderModule, At: at,
Explanation: fd.explanation, Needs: fd.needs, Source: source, Observer: module, At: at,
Evidence: []mcseat.Evidence{{At: at, Said: said}},
Subject: mcseat.Subject{Scope: fd.scope, ID: id, Machine: fd.machine}}
}
@@ -107,7 +105,7 @@ func (f *findings) buildEnded(ctx context.Context, e builds.BuildLogEntry) {
// raise writes fd through the writer.
func (f *findings) raise(ctx context.Context, fd finding) {
x := fd.seatFinding(f.now())
x := fd.seatFinding(f.module, f.now())
if f.w == nil {
f.logf("finding %s (not written, no finding writer): %s", views.FindingKey(x.Scope, x.ID, x.Kind), x.Summary)
return
@@ -130,7 +128,7 @@ func (f *findings) clear(ctx context.Context, kind, scope, id string) {
// notHeard is the writer of the builder's finding of a family it does not hear (heard.Writer, ADR 0615 §5): written through the
// finding writer, said in the log until it starts.
func (f *findings) notHeard(ctx context.Context) heard.Writer {
return heard.Writer{Observer: builds.BuilderModule, Logf: f.logf, Now: f.now,
return heard.Writer{Observer: f.module, Logf: f.logf, Now: f.now,
Put: func(_ string, value []byte) error {
var x mcseat.Finding
if err := json.Unmarshal(value, &x); err != nil {
@@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"maps"
"os"
"path/filepath"
@@ -292,7 +293,7 @@ func startBedWith(t *testing.T, closed bool) *bed {
t.Fatal(err)
}
viewstest.Serve(t, ctx, js, ctl, testAssigner, "assignments", testAssigner+".assignments", readers...)
b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory)
b.builds = mk(builds.BuildsBucket(builds.BuilderModule), builds.BuilderBuildsHistory)
b.instances = mk(instancesBucket, 1)
// The state bounds, whose keys have each its own time to live: an ask's step bounds (design 47 G A-77, A-118) are kept in it.
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: versions.BoundsBucket, History: 1, LimitMarkerTTL: time.Second}); err != nil {
@@ -1120,8 +1121,8 @@ func (b *bed) registry(t *testing.T, node string, seatVersions ...string) *regis
}
return kv
}
r.versions, r.findings, r.reasks = mk(versions.Bucket), mk(findingsBucket), mk(build.ReAsksBucket)
mk(versions.FloorWaitsBucket)
r.versions, r.findings, r.reasks = mk(versions.Bucket), mk(builds.StateBucket(builds.BuilderModule, mcseat.StateFindings)), mk(builds.StateBucket(builds.BuilderModule, build.ReAsksState))
mk(builds.StateBucket(builds.BuilderModule, versions.FloorWaitsState))
// EVENTS with two seats' outcomes, and the forge bridge's stream (a branch's end): each with the builder's durable consumer.
for stream, subjects := range map[string][]string{
eventsStream: {"mesh.mod.*.event.>", "mesh.seat.*.event.>"},
@@ -75,7 +75,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return err
}
defer conn.Close()
buildsKV, err := process.EssentialBucket(ctx, js, "builder", builds.BuilderBuildsBucket, "claim or write a build key")
buildsKV, err := process.EssentialBucket(ctx, js, "builder", builds.BuildsBucket(cred.Module), "claim or write a build key")
if err != nil {
return err
}
@@ -91,8 +91,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The module has one durable consumer of EVENTS: one hub pulls it and hands each message to the part that hears it (the
// views reader and the builder's own events), started once every part is registered.
hub := process.NewEventHub(logger.Printf)
reader := &views.ProcessReader{Module: builds.BuilderModule, Call: calls.Call, Logf: logger.Printf, Hub: hub}
findingsKV := process.AwaitBucket(ctx, js, conn, findingsBucket, "the builder's findings, a first view unanswered past its bound among them, are said in its log only", logger.Printf)
reader := &views.ProcessReader{Module: cred.Module, Call: calls.Call, Logf: logger.Printf, Hub: hub}
findingsKV := process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, mcseat.StateFindings), "the builder's findings, a first view unanswered past its bound among them, are said in its log only", logger.Printf)
// The builder's findings are raised as they happen, so its one finding writer holds them (design 47 G A-29, §689): it
// is started once the state is bound, and what is written before is stated at its start.
fw, err := views.NewHeldWriter(views.WriterConfig[mcseat.Finding, mcseat.FindingCleared, mcseat.FindingsStated, mcseat.FindingsAskedEvent]{
@@ -144,7 +144,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
dead := &buildrun.DeadLetters{}
// The state may not be on the bus yet when a delivery started the builder (the grants part creates it, issue 1790): it
// is bound when the bus says it was created, and the latest hearing is written then.
hearingKV := process.AwaitBucket(ctx, js, conn, builds.BuilderModule+"_"+buildrun.HearingState, "builder_stuck cannot say whether DEAD_LETTERS is heard", logger.Printf)
hearingKV := process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, buildrun.HearingState), "builder_stuck cannot say whether DEAD_LETTERS is heard", logger.Printf)
hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, buildstep.AskLanguages, dead, logger.Printf,
buildrun.SayHearing(ctx, hearingKV, logger.Printf))
defer hearing.Stop()
@@ -157,11 +157,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The builder numbers each trunk build at its claim (novox/hq design 51 *The versions*): its state versions, bound once
// 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}
found := &findings{module: cred.Module, w: fw, logf: logger.Printf, now: time.Now}
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).
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, versions.FloorWaitsState), "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))
})
@@ -183,8 +183,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
}
b = &build.Builder{
Claimer: build.Claimer{Store: kvStore{buildsKV}, By: cred.Node + "." + cred.Module},
Seats: seats,
OwnModule: cred.Module,
Claimer: build.Claimer{Store: kvStore{buildsKV}, By: cred.Node + "." + cred.Module},
Seats: seats,
Asks: buildrun.Asker{Name: meshbuilds.Seat, Seats: seats, Publisher: asks, Waiter: buildrun.LostAskWaiter{Inner: heard, Dead: dead, Hearing: hearing}, Bound: bound, Dropper: buildrun.SeatDrops{Caller: calls},
Bounds: &stepBounds{bounds: pubBoundsKV.KV, js: js, now: time.Now, logf: logger.Printf}, Expiry: stepExpiry},
Held: heldOnTheBus{js: js, seats: seats},
@@ -209,7 +210,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if err != nil {
return err
}
waitsBucket := process.AwaitBucket(ctx, js, conn, build.WaitsBucket, "the toolchain waits are kept in memory only, and builder_waits shows none", logger.Printf)
waitsBucket := process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, build.WaitsState), "the toolchain waits are kept in memory only, and builder_waits shows none", logger.Printf)
b.Waits = newWaitsKept(waitsBucket, func(ctx context.Context, event, id string, body any) error {
return emitEvent(context.WithoutCancel(ctx), js, cred, event, id, body)
}, waitBound, logger.Printf)
@@ -318,11 +319,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// What each counted node's engine reads (ADR 0618 §2, ADR 0624 §1): asked through the node-engine seat's verb as the
// builder's own account, for every node the assignments names, and kept in the state engine-reads once it is bound.
reads := newEngineReads(calls.CallKeptOn, gate, logger.Printf)
reads.module = cred.Module
reads.keeper.Once = calls.CallOn // its fault keeps the last statement at once while the kept call asks on (ADR 0668 §3)
// The old reference form's window over them (DLV-B9), kept in the state old-form once it is bound; builder_old_form
// reads it and drops a retired node by hand.
reads.ctx = context.WithoutCancel(ctx)
reads.window = &oldForm{forms: forms.Current, findings: found, logf: logger.Printf, now: time.Now, log: func(e builds.BuildLogEntry) { b.Log(e) },
reads.window = &oldForm{module: cred.Module, forms: forms.Current, findings: found, logf: logger.Printf, now: time.Now, log: func(e builds.BuildLogEntry) { b.Log(e) },
bounds: pubBoundsKV.KV,
said: func(body versions.OldFormClosedBody) {
if err := emitEvent(context.WithoutCancel(ctx), js, cred, versions.OldFormClosedEvent, "old-form-closed-"+body.At.Format(time.RFC3339Nano), body); err != nil {
@@ -336,7 +338,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
return answer, err
}
process.AwaitBucket(ctx, js, conn, versions.OldFormBucket, "the old form's window is kept in memory only, and builder_old_form shows none", logger.Printf).
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, versions.OldFormState), "the old form's window is kept in memory only, and builder_old_form shows none", logger.Printf).
Ready(func(kv jetstream.KeyValue) {
reads.window.bind(context.WithoutCancel(ctx), kv)
reads.stepWindow()
@@ -435,7 +437,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
(&floorWake{b: b, waits: floorWaits, logf: logger.Printf}).wake(ctx, versions.WakeRevision, "", "")
}
// All of the catalog half's wiring is set before the reader starts: its views are heard on the reader's goroutines from then.
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "builder_first_views reads nothing and builder_first_view_again is not heard", logger.Printf))
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, views.StatusState), "builder_first_views reads nothing and builder_first_view_again is not heard", logger.Printf))
// Every part has registered: the hub pulls the module's durable. Where the consumer is not there yet the builder says so
// and binds it at a later change (heard.Writer; ADR 0615 §5).
_ = notHeard.Bind(eventsStream, sweep.EventArtifactsGone+", what wakes a floor, the registry seats' words, the views and manifest-reads-changed are not heard until the builder starts again", func() error {
@@ -445,10 +447,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// 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).
process.AwaitBucket(ctx, js, conn, build.ReAsksBucket, "builder_ask_again is not heard; an ask that can no longer be answered is still asked again by itself", logger.Printf).Ready(func(kv jetstream.KeyValue) {
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, build.ReAsksState), "builder_ask_again is not heard; an ask that can no longer be answered is still asked again by itself", logger.Printf).Ready(func(kv jetstream.KeyValue) {
eachReAsk := func(e jetstream.KeyValueEntry) { reAsked(ctx, b, acts, kv, e, logger.Printf) }
if rw, err := settle(ctx, kv, eachReAsk); err != nil {
process.NotWatched(logger.Printf, build.ReAsksBucket, err, "builder_ask_again is not heard until the builder starts again")
process.NotWatched(logger.Printf, builds.StateBucket(cred.Module, build.ReAsksState), err, "builder_ask_again is not heard until the builder starts again")
} else {
go func() { errs <- follow(ctx, rw, eachReAsk) }()
}
@@ -490,19 +492,19 @@ 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) {
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, build.QueuesState), "builder_seat_queue is not answered", logger.Printf).Ready(func(kv jetstream.KeyValue) {
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")
process.NotWatched(logger.Printf, builds.StateBucket(cred.Module, build.QueuesState), err, "builder_seat_queue is not answered until the builder starts again")
} else {
go func() { errs <- follow(ctx, qw, eachRead) }()
}
})
// The state engine-reads, new with ADR 0618 (issue 1790's kind): until it is on the bus each statement is kept in memory
// only, and builder_engine_reads answers nothing.
process.AwaitBucket(ctx, js, conn, engineReadsBucket, "what each node's engine reads is kept in memory only", logger.Printf).Ready(func(kv jetstream.KeyValue) {
process.AwaitBucket(ctx, js, conn, builds.StateBucket(cred.Module, EngineReadsState), "what each node's engine reads is kept in memory only", logger.Printf).Ready(func(kv jetstream.KeyValue) {
reads.bind(ctx, kv, errs, logger.Printf)
})
logger.Printf("started on %s", cred.Node)
@@ -517,10 +519,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
}
// The builder's own state of its first views (views.StatusState); its findings (findingsBucket) are where the condition
// keeper raises a first view unanswered past its bound (ADR 0615 §6).
var firstViewsBucket = builds.BuilderModule + "_" + views.StatusState
// 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.
@@ -25,6 +25,8 @@ import (
// build key written, never on a timer. A node holding it seven days is the finding old-form-held; its close is said once
// as version.old-form-closed and in the builder's log.
type oldForm struct {
// module is the module the builder runs as, its credential's: the module of its build log entries.
module string
forms forms.ArtifactForms
findings *findings
// said emits version.old-form-closed.
@@ -119,7 +121,7 @@ func (o *oldForm) step(ctx context.Context, named []string, counted map[string][
// In the build log too (design 51 *The node-engine's own build*), under the builder's own key, the window's key
// standing for the fingerprint: the close is no one build's.
if o.log != nil {
o.log(builds.BuildLogEntry{Event: builds.BuildSaidEvent, Module: builds.BuilderModule, FP: versions.OldFormKey,
o.log(builds.BuildLogEntry{Event: builds.BuildSaidEvent, Module: o.module, FP: versions.OldFormKey,
At: body.At.UTC(), Line: line, Why: versions.SaidOldFormClosed})
}
if o.said != nil {
@@ -22,11 +22,9 @@ import (
// with no key reads as stating the baseline: its first forms. No node's record is read in its place.
const EngineReadsState = "engine-reads"
// engineReadsBucket is that state's bucket.
const engineReadsBucket = builds.BuilderModule + "_" + EngineReadsState
// engineReads keeps the statements and the counted nodes for the builder's forms (build.Builder.Forms).
type engineReads struct {
module string
keeper *enginereads.Keeper
gate *buildrun.PlacedNodes
mu sync.Mutex
@@ -101,7 +99,7 @@ func (r *engineReads) bind(ctx context.Context, kv jetstream.KeyValue, errs chan
}
w, err := settle(ctx, kv, each)
if err != nil {
process.NotWatched(logf, engineReadsBucket, err, "builder_engine_reads is not answered until the builder starts again")
process.NotWatched(logf, builds.StateBucket(r.module, EngineReadsState), err, "builder_engine_reads is not answered until the builder starts again")
return
}
go func() { errs <- follow(ctx, w, each) }()
@@ -140,7 +140,7 @@ func theCountedNodesEngineReads(t *testing.T) {
asked = append(asked, key, body)
return json.Marshal(meshwatchdogs.PresenceAnswer{Machine: "n2", Online: online})
})
calls := r2.callsOf(&findings{w: st, logf: t.Logf, now: clock}, func() jetstream.KeyValue { return nil }, heard, t.Logf)
calls := r2.callsOf(&findings{module: builds.BuilderModule, w: st, logf: t.Logf, now: clock}, func() jetstream.KeyValue { return nil }, heard, t.Logf)
calls.Arm = func(_ context.Context, node string, ends time.Time) error { armed[node] = ends; return nil }
key := meshconditionsclient.Key(meshconditionsclient.ScopeCall, "manifest-reads.n2", meshconditionsclient.KindCallUnanswered)
r2.placed() // n2 asked, unanswered at 1000
@@ -303,7 +303,7 @@ func (failingVersions) KeysOf(context.Context, string) ([]string, error) {
func aFailedTrunkBuildIsAFinding(t *testing.T) {
st := &memFindings{vals: map[string][]byte{}}
f := &findings{w: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
f := &findings{module: builds.BuilderModule, w: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
ended := func(outcome, ref string) builds.BuildLogEntry {
return builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: "m", FP: "f1", Commit: "c", Ref: ref, Outcome: outcome}
}
@@ -440,10 +440,10 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) {
var closings []versions.OldFormClosedBody
var logged []builds.BuildLogEntry
marks := &memBounds{keys: map[string]bool{}}
window := &oldForm{forms: fs, findings: f, logf: t.Logf, now: func() time.Time { return now }, bounds: func() jetstream.KeyValue { return marks },
window := &oldForm{module: builds.BuilderModule, forms: fs, findings: f, logf: t.Logf, now: func() time.Time { return now }, bounds: func() jetstream.KeyValue { return marks },
said: func(b versions.OldFormClosedBody) { closings = append(closings, b) },
log: func(e builds.BuildLogEntry) { logged = append(logged, e) }}
reads := &engineReads{keeper: &enginereads.Keeper{}, window: window, counted: []string{"a", "b"}}
reads := &engineReads{module: builds.BuilderModule, keeper: &enginereads.Keeper{}, window: window, counted: []string{"a", "b"}}
// Issue 1966 note 155, ADR 0668 §4: at the consumer's bind every counted node is called once again.
var boundCalls []string
reads.keeper.Sync = true
@@ -575,3 +575,14 @@ func decodeFinding(v []byte) (mcseat.Finding, error) {
var f mcseat.Finding
return f, json.Unmarshal(v, &f)
}
// Design 47 G A-296: the builder's findings are observed by the module its credential names, never a constant of its name.
func TestTheBuildersFindingsAreObservedByItsCredentialsModule(t *testing.T) {
st := &memFindings{vals: map[string][]byte{}}
f := &findings{module: "acme-builder", w: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
f.buildEnded(context.Background(), builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: "m", FP: "f1", Commit: "c", Outcome: builds.BuildFailed})
got, err := decodeFinding(st.vals[meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m", BuildFailed)])
if err != nil || got.Observer != "acme-builder" {
t.Fatalf("the finding = %+v, %v; want the observer acme-builder", got, err)
}
}
@@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"strings"
"testing"
"time"
@@ -76,7 +77,7 @@ func (b *boundsKV) Delete(_ context.Context, key string, _ ...jetstream.KVDelete
func TestTheCatalogsWordsMoveTheReservations(t *testing.T) {
ctx := context.Background()
st := &memFindings{vals: map[string][]byte{}}
found := &findings{w: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
found := &findings{module: builds.BuilderModule, w: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
bounds := &boundsKV{keys: map[string]bool{}}
woke := 0
call := func(addr string, body any) (json.RawMessage, error) {
@@ -155,7 +156,7 @@ func TestAReleasedVersionIsTaggedAtTheCommitItWasBuiltFrom(t *testing.T) {
tags = append(tags, body.(git.TagInput))
return json.Marshal(git.TagAnswer{Performed: refuse == "", Error: refuse})
}
found := &findings{w: &memFindings{vals: map[string][]byte{}}, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
found := &findings{module: builds.BuilderModule, w: &memFindings{vals: map[string][]byte{}}, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
rv := newReservations(&memReserve{vals: map[string][]byte{}, revs: map[string]uint64{}}, call, func() jetstream.KeyValue { return &boundsKV{keys: map[string]bool{}} }, found, nil, t.Logf)
rv.filled.Store(true)
goPkg := versions.Source{Repository: "o/r", Commit: "c0ffee", GoPackage: true}
@@ -3,6 +3,7 @@ package main
import (
"context"
"errors"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"strings"
"testing"
"time"
@@ -18,7 +19,7 @@ import (
// while the read is unanswered raises call-unanswered about the ask; the answer deletes the bound and clears the finding.
func TestTheStepsReadIsCallUnansweredAfterItsBound(t *testing.T) {
st := &memFindings{vals: map[string][]byte{}}
f := &findings{w: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
f := &findings{module: builds.BuilderModule, w: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
marks := &memBounds{keys: map[string]bool{}}
s := &stepsRead{bounds: func() jetstream.KeyValue { return marks }, findings: f, bound: meshconditionsclient.CallUnansweredAfter,
now: func() time.Time { return time.Unix(600, 0) }, logf: t.Logf}
@@ -6,7 +6,6 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
)
@@ -19,8 +18,7 @@ import (
const (
SeatQueueTool = "builder_seat_queue"
// QueuesState is the builder's state of queue reads: the bucket mesh-delivery-builder_queues, keyed by the language.
QueuesState = "queues"
QueuesBucket = builds.BuilderModule + "_" + QueuesState
QueuesState = "queues"
// DefaultQueueLimit and MaxQueueLimit bound the messages read of each kind.
DefaultQueueLimit = 50
MaxQueueLimit = 500
@@ -40,7 +40,9 @@ type ToolchainEntries interface {
// pulled by whichever build node has a free slot: ADR 0525), and writes the key done or failed. It holds no timer; a build starts from a modules value or a
// change of the toolchains.
type Builder struct {
Claimer Claimer
// OwnModule is the module the builder runs as, its credential's: the asker of every step ask.
OwnModule string
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).
@@ -1065,7 +1067,7 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes
if needsFetch(l.tasks) {
job.Fetch = &req
}
ask := sdkstep.StepAsk{ID: sdkstep.NewID(), Kind: kind, Language: language, Asker: builds.BuilderModule, At: time.Now(), Version: l.version, Build: job}
ask := sdkstep.StepAsk{ID: sdkstep.NewID(), Kind: kind, Language: language, Asker: b.OwnModule, At: time.Now(), Version: l.version, Build: job}
if language == sdkstep.AskGo && job.Fetch != nil && len(m.Packages) > 0 {
// 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.
@@ -185,7 +185,7 @@ func newRig(t *testing.T, artifacts ...catalogue.Artifact) *rig {
r.tools.Set(n, tcOf("typescript", "tsc5"))
}
r.asks = &buildrun.FakeAsks{Answer: r.answer}
r.b = &Builder{
r.b = &Builder{OwnModule: builds.BuilderModule,
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,
}
@@ -594,7 +594,7 @@ func TestAKeyTheBuilderLeftBuildingIsBuiltAtStartAndDoneKeysAreNotBuiltTwice(t *
// whose compare-and-set comes first, and the other's write is refused, never overwriting it.
func TestTwoInstancesStartingTogetherEndWithOneDoneKey(t *testing.T) { // 1607/AC7, review round 4
r := newRig(t, goBundle)
other := &Builder{Claimer: Claimer{Store: r.st, By: "ace.builder"}, Asks: r.b.Asks, Toolchains: r.tools, Manifests: r.mf, Logf: t.Logf}
other := &Builder{OwnModule: builds.BuilderModule, Claimer: Claimer{Store: r.st, By: "ace.builder"}, Asks: r.b.Asks, Toolchains: r.tools, Manifests: r.mf, Logf: t.Logf}
value := moduleValue(t, "modules/m")
var wg sync.WaitGroup
for _, b := range []*Builder{r.b, other} {
@@ -31,8 +31,7 @@ const (
// of Mode ModeAnnounce.
AnnounceAgainTool = "builder_announce_again"
// ReAsksState is the builder's state of asks to ask again: the bucket mesh-delivery-builder_reasks, keyed as the build key.
ReAsksState = "reasks"
ReAsksBucket = builds.BuilderModule + "_" + ReAsksState
ReAsksState = "reasks"
)
// ReAsk is a value of the reasks state: why a person asked the key's build again, who and when, and, once the builder
@@ -21,8 +21,7 @@ const (
WaitsTool = "builder_waits"
ReleaseTool = "builder_release"
// WaitsState is the builder's old state section of the waits, one key <module>.<fingerprint>.
WaitsState = "waits"
WaitsBucket = builds.BuilderModule + "_" + WaitsState
WaitsState = "waits"
// ModeRelease is the Mode of a reasks value of builder_release.
ModeRelease = "release"
// WaitRan ends a wait when a toolchain-agreed names the version; WaitReleased when builder_release ended it.
@@ -110,9 +110,8 @@ func (w FloorWait) Finding() string {
// The builder's state floor-waits (DLV-B4): each name waiting on its floor, keyed by its escaped name, so a restarted
// builder still knows which names its silent sources' events wake; builder_floor_waits reads it.
const (
FloorWaitsState = "floor-waits"
FloorWaitsBucket = "mesh-delivery-builder_floor-waits"
FloorWaitsTool = "builder_floor_waits"
FloorWaitsState = "floor-waits"
FloorWaitsTool = "builder_floor_waits"
)
// FloorWaitKey is a wait's key in floor-waits.
@@ -9,7 +9,6 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/window"
)
@@ -28,9 +27,6 @@ const (
OldFormKey = "window"
)
// OldFormBucket is that state's bucket.
const OldFormBucket = builds.BuilderModule + "_" + OldFormState
// OldFormHeld is the finding of a node holding the window open for FindingAfter, about that node, cleared when the
// window closes or the node no longer holds it.
const OldFormHeld = "old-form-held"
@@ -607,7 +607,7 @@ func startBed(t *testing.T) *bed {
check.CheckerOwedBucket: 5, check.PreviewsBucket: 5, check.CandidatesBucket: 10,
check.ModulesBucket: 20, check.ManifestsBucket: 1,
check.PresenceBucket: 20, check.HaveBucket: 20, check.TriesBucket: 1,
builds.BuilderBuildsBucket: 1, check.InputsBucket: 1} {
builds.BuildsBucket(builds.BuilderModule): 1, check.InputsBucket: 1} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: uint8(history)})
if err != nil {
t.Fatal(err)
@@ -850,7 +850,7 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300
}
bkey, _ := builds.BuilderBuildKey("m", fp)
building, _ := json.Marshal(builds.BuilderBuild{V: 1, State: builds.BuildBuilding})
if _, err := b.buckets[builds.BuilderBuildsBucket].Put(b.ctx, bkey, building); err != nil {
if _, err := b.buckets[builds.BuildsBucket(builds.BuilderModule)].Put(b.ctx, bkey, building); err != nil {
t.Fatal(err)
}
if v := b.verdict(t, key, func(meshdeliverychecker.VerdictsByGroupHeadsValue) bool { return true }); v.State != meshdeliverycheckerclient.VerdictRunning || len(asked()) != 0 {
@@ -859,14 +859,14 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300
// m's build fails: the check fails naming it. Built again (builder_build_again writes the key done), the check asks
// itself again for the same heads, with no recheck, and passes.
failedBuild, _ := json.Marshal(builds.BuilderBuild{V: 1, State: builds.BuildFailed, Error: "go build: undefined: x"})
if _, err := b.buckets[builds.BuilderBuildsBucket].Put(b.ctx, bkey, failedBuild); err != nil {
if _, err := b.buckets[builds.BuildsBucket(builds.BuilderModule)].Put(b.ctx, bkey, failedBuild); err != nil {
t.Fatal(err)
}
if v := b.verdict(t, key, ended); v.State != meshdeliverycheckerclient.VerdictFail || v.Failed.Test != check.BuildFailedTest+"m" {
t.Fatalf("verdict %+v, want it failed on m's build", v)
}
built, _ := json.Marshal(builds.BuilderBuild{V: 1, State: builds.BuildDone, Manifest: digest})
if _, err := b.buckets[builds.BuilderBuildsBucket].Put(b.ctx, bkey, built); err != nil {
if _, err := b.buckets[builds.BuildsBucket(builds.BuilderModule)].Put(b.ctx, bkey, built); err != nil {
t.Fatal(err)
}
b.said(t, "the builds of m it failed on are done again, so it is checked again")
@@ -276,7 +276,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// says nothing of the builds, until the checker starts again.
var buildStates check.BuildStates
var ends check.BuildEnds
if kv := process.OptionalBucket(ctx, js, builds.BuilderBuildsBucket, "the line says nothing of the builds, and a check composes without waiting on them, until the checker starts again", logger.Printf); kv != nil {
if kv := process.OptionalBucket(ctx, js, builds.BuildsBucket(builds.BuilderModule), "the line says nothing of the builds, and a check composes without waiting on them, until the checker starts again", logger.Printf); kv != nil {
buildStates, ends = buildReader{kvStore{kv}}, buildEnds{kv}
}
if ends != nil {
@@ -177,7 +177,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The watchman's stuck keys name the wants' real bucket at their holder (A-303 (corrected)).
model.SetWantsHolder(func() string { return reader.HolderBucket(meshwants.Seat, meshwants.StateWants) })
open := buckets{js: js, reader: reader}
for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.WantsBucket, view.AsksBucket, own.Answers, view.TakenBucket, own.Reach} {
for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.AnswersBucket, builds.BuildsBucket(builds.BuilderModule), view.WantsBucket, view.AsksBucket, own.Answers, view.TakenBucket, own.Reach} {
go follow(ctx, open, bucket, false, model, logger)
}
// The view's own state reach, as it stood at the start, is what the derivation lays its history over (design 47 G A-225);
@@ -24,7 +24,7 @@ func builderKey(t *testing.T, module, fp2 string, b builds.BuilderBuild) Entry {
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: builds.BuilderBuildsBucket, Key: key, Value: data, At: t0}
return Entry{Bucket: builds.BuildsBucket(builds.BuilderModule), Key: key, Value: data, At: t0}
}
func TestAFingerprintOfTheNewFormIsShownAgainstItsBuilderKey(t *testing.T) { // I5
@@ -689,7 +689,7 @@ func (m *Model) apply(e Entry) {
delete(m.answerOf, e.Key)
}
m.answers[e.Key] = id
case builds.BuilderBuildsBucket:
case builds.BuildsBucket(builds.BuilderModule):
if e.Deleted {
delete(m.keys, e.Key)
return
@@ -358,7 +358,7 @@ const ResolvedBucket = "mesh-config_resolved"
func NodeComposeReads(node string) []string {
var out []string
for _, bucket := range []string{AssignmentsBucket, AssignmentsCandidatesBucket, ModulesBucket,
ManifestsBucket, builds.BuilderBuildsBucket} {
ManifestsBucket, builds.BuildsBucket(builds.BuilderModule)} {
out = append(out, readGrants(bucket, "")...)
}
return append(out, readGrants(ResolvedBucket, node+".>")...)