WM13 (builder half): a key queued behind a package says wait-held and wait-ended in the one wait form

The builder says wait-held for a key queued behind a package, the package as its part and on (package:<ref>), on its
stream waits, and wait-ended published once the key is claimed; a toolchain wait never ends by that, and a package wait
never by a toolchain's end. Files: internal/build/waits.go (Package, On, WaitPublished), internal/build/run.go (Hold at
the queued result, End at the claim), cmd/mesh-delivery-builder/waits.go (Part in the body, the End rule).

Builds: design 47 G A-192, §499

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:52:10 +01:00
parent 62de8d0bb8
commit 524fbb02ef
6 changed files with 72 additions and 8 deletions
@@ -18,8 +18,10 @@ import (
// waitBody is the body of wait-held and wait-ended on the builder's stream waits (design 47 G §672): the fields of §629's
// wait with `step` building, `on` toolchain:<language>@<version>, `module` and `fingerprint`, and, on wait-ended, `how`.
type waitBody struct {
Step string `json:"step"`
On string `json:"on"`
Step string `json:"step"`
On string `json:"on"`
// Part is the package a key queued behind it waits for (design 47 G A-192); none for a toolchain wait.
Part string `json:"part,omitempty"`
Module string `json:"module"`
Fingerprint string `json:"fingerprint"`
Since time.Time `json:"since"`
@@ -46,7 +48,7 @@ func newWaitsKept(kv *process.AwaitedBucket, say func(context.Context, string, s
}
func (w *waitsKept) body(t build.ToolchainWait, how string) waitBody {
return waitBody{Step: waitStepBuilding, On: t.On(), Module: t.Module, Fingerprint: t.Fingerprint, Since: t.Since, Bound: w.bound.String(), How: how}
return waitBody{Step: waitStepBuilding, On: t.On(), Part: t.Package, Module: t.Module, Fingerprint: t.Fingerprint, Since: t.Since, Bound: w.bound.String(), How: how}
}
// start reads the state whole and says wait-held again for each key.
@@ -110,7 +112,13 @@ func (w *waitsKept) End(ctx context.Context, module, fp, how string) bool {
}
w.mu.Lock()
t, had := w.held[key]
delete(w.held, key)
// A key queued behind a package ends only when it is published, and a toolchain wait never by that (design 47 G A-192).
if had && (t.Package != "") != (how == build.WaitPublished) {
had = false
}
if had {
delete(w.held, key)
}
w.mu.Unlock()
if !had {
return false
@@ -0,0 +1,21 @@
package main
import (
"testing"
"time"
"mesh-delivery-builder/internal/build"
)
// Design 47 G A-192: a key queued behind a package says its wait in the one wait form, the package as its `part` and `on`; a toolchain
// wait names none.
func TestAWaitBodyNamesItsPart(t *testing.T) {
w := &waitsKept{bound: time.Hour, held: map[string]build.ToolchainWait{}}
pkg := w.body(build.ToolchainWait{Module: "m", Fingerprint: "src2:f", Package: "@novox/sdk@0.1.15"}, "")
if pkg.Part != "@novox/sdk@0.1.15" || pkg.On != "package:@novox/sdk@0.1.15" || pkg.Step != waitStepBuilding {
t.Errorf("a package wait says %+v", pkg)
}
if tc := w.body(build.ToolchainWait{Module: "m", Fingerprint: "src2:f", Language: "go", Version: "go1.26.0"}, ""); tc.Part != "" || tc.On != "toolchain:go@go1.26.0" {
t.Errorf("a toolchain wait says %+v", tc)
}
}
@@ -173,6 +173,8 @@ func sdkModule(t *testing.T, r *rig, ref string) []byte {
func TestAModuleWhoseLockNamesAnUnpublishedPackageWaitsQueuedAndIsBuiltOncePublished(t *testing.T) { // B7, ADR 0500 clause 6
const ref = "@novox/mesh-sdk@0.1.15"
r := newRig(t, tsBundle)
w := &memWaits{}
r.b.Waits = w
for _, n := range []string{"ace", "novox"} {
r.bm.fails[n+"/build_node_fetch"] = waitsFor + ref + ": the lock names a version the registry does not hold"
}
@@ -180,6 +182,15 @@ func TestAModuleWhoseLockNamesAnUnpublishedPackageWaitsQueuedAndIsBuiltOncePubli
if v.State != builds.BuildQueued || v.Waits != ref || v.Error != "" {
t.Fatalf("build = %+v, want queued, waiting for %s, not failed", v, ref)
}
// Design 47 G A-192: the queued key is a wait said in the one wait form, with its package as the part, and it stands while the package is unpublished.
if len(w.held) != 1 {
t.Fatalf("waits held %v, want the one of the package", w.held)
}
for _, h := range w.held {
if h.Package != ref || h.On() != "package:"+ref {
t.Errorf("the wait is %+v on %s, want the package %s", h, h.On(), ref)
}
}
delete(r.bm.fails, "ace/build_node_fetch")
delete(r.bm.fails, "novox/build_node_fetch")
writes := r.st.writes
@@ -188,11 +199,17 @@ func TestAModuleWhoseLockNamesAnUnpublishedPackageWaitsQueuedAndIsBuiltOncePubli
if r.st.writes != writes {
t.Fatalf("a key waiting for an unpublished package was claimed (%d writes, was %d)", r.st.writes, writes)
}
if len(w.held) != 1 {
t.Fatalf("the wait ended while the package is unpublished: %v", w.held)
}
r.b.Module(context.Background(), "sdk", sdkModule(t, r, ref)) // the package's trunk key is built and published
r.b.Wait()
if v = valueOf(t, r.st, r.key); v.State != builds.BuildDone || v.Waits != "" {
t.Errorf("build = %+v, want built once the package it waits for is published", v)
}
if len(w.held) != 0 || len(w.ended) != 1 || w.ended[0] != WaitPublished {
t.Errorf("held %v, ended %v: want the wait ended published", w.held, w.ended)
}
}
func TestAKeyWaitingForAPackageAlreadyPublishedIsBuiltWhenTheBuilderStarts(t *testing.T) { // B7: the scan at start
@@ -617,6 +617,9 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB
return
}
b.handClaimed(key, cl.Rev)
if b.Waits != nil {
b.Waits.End(ctx, module, m.Fp2, WaitPublished) // a key queued behind a package is claimed once it is published
}
b.logBuild(LogStarted(module, m, "", b.Node, b.now()))
if cl.Unasked != nil {
b.logf("%v", cl.Unasked)
@@ -717,6 +720,10 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB
}
if result.State == builds.BuildQueued {
b.logf("%s: waits for the package %s to be published", cl.Key, result.Waits)
if b.Waits != nil {
// Said as a wait in the one wait form, with its part, for the watchman to time (design 47 G A-192; ADR 0603 §8).
b.Waits.Hold(ctx, ToolchainWait{Module: module, Fingerprint: m.Fp2, Package: result.Waits, Since: b.now()})
}
}
if b.notePublished(result) {
// A key may wait for the package this build published (ADR 0500 clause 6).
@@ -26,8 +26,10 @@ const (
// 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.
WaitRan = "ran"
WaitReleased = "released"
WaitRan = "ran"
// WaitPublished ends the wait of a key queued behind a package, when the key is claimed (the package is published).
WaitPublished = "published"
WaitReleased = "released"
)
// ToolchainWait is one wait: the entry's module and fingerprint, and the toolchain it waits for.
@@ -37,10 +39,18 @@ type ToolchainWait struct {
Language string `json:"language"`
Version string `json:"version"`
Since time.Time `json:"since"`
// Package is the package version a key queued behind it waits for, `<name>@<version>` (ADR 0603 §8; design 47 G A-192): the wait is the
// package's, not a toolchain's, and Language and Version are empty.
Package string `json:"package,omitempty"`
}
// On is what the wait waits for, as the wait's `on` says it.
func (w ToolchainWait) On() string { return "toolchain:" + w.Language + "@" + w.Version }
func (w ToolchainWait) On() string {
if w.Package != "" {
return "package:" + w.Package
}
return "toolchain:" + w.Language + "@" + w.Version
}
// Waits keeps the builder's waits: Hold writes one in the state waits and says wait-held, once per wait; End deletes it and
// says wait-ended with how, and answers whether one was held.
@@ -28,7 +28,8 @@ func (w *memWaits) End(_ context.Context, module, fp, how string) bool {
w.mu.Lock()
defer w.mu.Unlock()
k := module + "." + fp
if _, ok := w.held[k]; !ok {
t, ok := w.held[k]
if !ok || (t.Package != "") != (how == WaitPublished) { // as the builder's waitsKept ends them: a package wait by its publishing alone
return false
}
delete(w.held, k)