BL16: the builder puts its asks on accept.<queue>.builder, and a step key neither form parses is named in builder_stuck's journal

The delivery SDK's buildstep gains AskerBuilder, AskSubjectBy and ParseAskSubjectBy (an ask's subject names its
asker as the last token, main's form names none; ParseAskSubject reads both). The builder's put and its held-ask
reads use the builder's subject. Not built here: §543's refusal (the build modules'), the grants (grants part).

Builds: design 47 G §481, §484, §543, §600
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 22:47:40 +01:00
parent 52241e7a17
commit 28cb212030
7 changed files with 109 additions and 14 deletions
@@ -102,7 +102,8 @@ func (k Asker) dropped(a buildstep.StepAsk, why string) {
// PutTry is how long one publish of an ask waits for its stream's answer before it is made again (Put).
var PutTry = 10 * time.Second
// Put puts a on its stream: the seat's accept subject of its language and kind. The put is a kept call (novox/hq ADR 0668
// Put puts a on its stream: the seat's accept subject of its language and kind, under the builder's name as asker (design 47 G
// §481, §600: `accept.<queue>.builder`). The put is a kept call (novox/hq ADR 0668
// §1, §2; issue 1966, issue 1748 note 27): it carries the ask's id as its message id, so the stream takes it once, and it
// is made again through every transport fault (a deadline, no responders) within busretry.Bound, each try bounded by
// PutTry, until ctx ends. A put that still meets one is an error opening with buildstep.TransportFaultWords, so the key it
@@ -112,7 +113,7 @@ func (k Asker) Put(ctx context.Context, a buildstep.StepAsk) error {
if err != nil {
return err
}
subject, err := buildstep.AskSubject(a.Language, a.Kind)
subject, err := buildstep.AskSubjectBy(a.Language, a.Kind, buildstep.AskerBuilder)
if err != nil {
return err
}
@@ -30,7 +30,8 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
f := &FakeAsks{Answer: func(a buildstep.StepAsk) buildstep.Step {
return buildstep.Step{ID: a.ID, Node: "ace", State: buildstep.StepPassed, Result: []byte(`{"artifacts":[]}`)}
}}
k := Asker{Publisher: f, Waiter: f}
subjects := &subjectsOf{inner: f}
k := Asker{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) }),
@@ -39,6 +40,10 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) {
if err != nil || step.Node != "ace" || len(f.Asks()) != 1 || len(seen) != 1 || seen[0].Node != "ace" || len(also) != 1 {
t.Fatalf("step %+v, %v, asks %d, seen %+v and %+v", step, err, len(f.Asks()), seen, also)
}
// Design 47 G §481, §600: the ask is put under the builder's name, accept.<queue>.builder.
if len(subjects.seen) != 1 || subjects.seen[0] != "mesh.seat.node-build-go.accept."+goAsk("x").Kind+".builder" {
t.Fatalf("the ask was put on %v", subjects.seen)
}
f.Answer = func(a buildstep.StepAsk) buildstep.Step {
return buildstep.Step{ID: a.ID, Node: "shanks", State: buildstep.StepRefused, Reason: "go 1.27 installed"}
}
@@ -381,3 +386,14 @@ func (p *faultyPuts) PublishAsk(ctx context.Context, subject, id string, data []
}
return p.inner.PublishAsk(ctx, subject, id, data)
}
// subjectsOf records the subject each ask is published on.
type subjectsOf struct {
inner AskPublisher
seen []string
}
func (p *subjectsOf) PublishAsk(ctx context.Context, subject, id string, data []byte) error {
p.seen = append(p.seen, subject)
return p.inner.PublishAsk(ctx, subject, id, data)
}
@@ -115,7 +115,7 @@ func (h heldOnTheBus) seen(ctx context.Context, a buildstep.StepAsk, later bool)
// stream is the build seat's stream of language, its name and the subject of kind on it.
func (h heldOnTheBus) stream(ctx context.Context, language, kind string) (jetstream.Stream, string, string, error) {
subject, err := buildstep.AskSubject(language, kind)
subject, err := buildstep.AskSubjectBy(language, kind, buildstep.AskerBuilder)
if err != nil {
return nil, "", "", err
}
@@ -286,7 +286,7 @@ func (h heldOnTheBus) Queue(ctx context.Context, language string, limit int) (bu
return q, err
}
for _, kind := range buildstep.AskKinds {
subject, _ := buildstep.AskSubject(language, kind)
subject, _ := buildstep.AskSubjectBy(language, kind, buildstep.AskerBuilder)
var ahead []build.Queued
if err := onSubject(ctx, stream, subject, func(m build.Queued) bool {
ahead = append(ahead, m)
@@ -1328,6 +1328,27 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
if maxFlight.Load() < 2 {
t.Errorf("the values were read one at a time")
}
// Design 47 G §484: a step key neither form parses is written in the journal naming it.
var said []string
steps := StateRead{Keys: func() ([]string, error) {
return []string{"ace.mvl0frv18d1957", "builder.ace.mvl0frv18d1958", "a.b.c.d"}, nil
}}
if _, err := (StuckReader{Builds: slow, ReAsks: state(map[string]string{}), Steps: map[string]StateRead{"mesh-build-go": steps}, Now: func() time.Time { return at },
Logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }}).Stuck("mod10"); err != nil {
t.Fatal(err)
}
n := 0
for _, l := range said {
if strings.Contains(l, `"a.b.c.d"`) {
n++
}
if strings.Contains(l, "ace.mvl0frv18d1957") || strings.Contains(l, "builder.ace.mvl0frv18d1958") {
t.Errorf("a key of one of the two forms is said not to parse: %s", l)
}
}
if n != 1 {
t.Errorf("the key of neither form is named %d times in %q", n, said)
}
})
}
@@ -325,9 +325,15 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) {
}
byID := make(map[string]string, len(ks))
for _, k := range ks {
if _, _, id, err := buildstep.ParseStepKey(k); err == nil {
byID[id] = k
_, _, id, err := buildstep.ParseStepKey(k)
if err != nil {
// A key neither form parses is named, never passed over in silence (design 47 G §484).
if r.Logf != nil {
r.Logf("builder_stuck: the key %q of the steps of %s is a step key of neither form, <asker>.<node>.<id> or <node>.<id>: %v", k, bm, err)
}
continue
}
byID[id] = k
}
stepKeys[bm] = byID
}
+32 -7
View File
@@ -137,7 +137,8 @@ func BuildModuleOf(language string) (string, error) {
return "mesh-build-" + language, nil
}
// AskSubject is the subject a build seat accepts a kind of ask on: the seat's accept subject.
// AskSubject is the subject a build seat accepts a kind of ask on: the seat's accept subject of main's form, which names no
// asker (design 47 G §600, §483).
func AskSubject(language, kind string) (string, error) {
seat, err := BuildSeat(language)
if err != nil {
@@ -149,20 +150,44 @@ func AskSubject(language, kind string) (string, error) {
return "mesh.seat." + seat + ".accept." + kind, nil
}
// ParseAskSubject is the inverse of AskSubject.
// AskerBuilder is the builder's name as an asker: the last token of the subject its asks are put on and the first of its
// step keys (design 47 G §600, §481).
const AskerBuilder = "builder"
// AskSubjectBy is the subject a build seat accepts a queue's ask of asker on: `mesh.seat.<seat>.accept.<queue>.<asker>`, the
// asker's name the last token (design 47 G §481, §600). An empty asker is main's subject (AskSubject).
func AskSubjectBy(language, kind, asker string) (string, error) {
subject, err := AskSubject(language, kind)
if err != nil || asker == "" {
return subject, err
}
if !askerOK.MatchString(asker) {
return "", fmt.Errorf("buildstep: %q is not an asker's name", asker)
}
return subject + "." + asker, nil
}
// ParseAskSubject is the inverse of AskSubject and of AskSubjectBy: the language and the queue of either form.
func ParseAskSubject(subject string) (language, kind string, err error) {
language, kind, _, err = ParseAskSubjectBy(subject)
return language, kind, err
}
// ParseAskSubjectBy is the inverse of AskSubjectBy: the language, the queue and the asker, which is empty for main's form.
func ParseAskSubjectBy(subject string) (language, kind, asker string, err error) {
rest, ok := strings.CutPrefix(subject, "mesh.seat.node-build-")
if !ok {
return "", "", fmt.Errorf("buildstep: %q is not a build seat's accept subject", subject)
return "", "", "", fmt.Errorf("buildstep: %q is not a build seat's accept subject", subject)
}
language, kind, ok = strings.Cut(rest, ".accept.")
if !ok {
return "", "", fmt.Errorf("buildstep: %q is not a build seat's accept subject", subject)
return "", "", "", fmt.Errorf("buildstep: %q is not a build seat's accept subject", subject)
}
if want, err := AskSubject(language, kind); err != nil || want != subject {
return "", "", fmt.Errorf("buildstep: %q is not a build seat's accept subject: %v", subject, err)
kind, asker, _ = strings.Cut(kind, ".")
if want, err := AskSubjectBy(language, kind, asker); err != nil || want != subject {
return "", "", "", fmt.Errorf("buildstep: %q is not a build seat's accept subject: %v", subject, err)
}
return language, kind, nil
return language, kind, asker, nil
}
// AskStream is the work-queue stream of a language's build seat.
+26
View File
@@ -594,3 +594,29 @@ func TestFetchBuildsResolvedAndToolchainDigestAreRead(t *testing.T) {
t.Errorf("toolchain digest: %+v", tc)
}
}
// §481, §600: a queue's ask of an asker is put on accept.<queue>.<asker>, and both forms parse.
func TestAskSubjectBy(t *testing.T) {
got, err := AskSubjectBy(AskGo, AskDelivery, AskerBuilder)
if err != nil || got != "mesh.seat.node-build-go.accept.delivery.builder" {
t.Fatalf("%q %v", got, err)
}
for _, c := range []struct{ subject, language, kind, asker string }{
{got, "go", "delivery", "builder"},
{"mesh.seat.node-build-node.accept.check", "node", "check", ""},
} {
l, k, a, err := ParseAskSubjectBy(c.subject)
if err != nil || l != c.language || k != c.kind || a != c.asker {
t.Errorf("%s: %q %q %q %v", c.subject, l, k, a, err)
}
if l, k, err := ParseAskSubject(c.subject); err != nil || l != c.language || k != c.kind {
t.Errorf("%s: ParseAskSubject %q %q %v", c.subject, l, k, err)
}
}
if _, err := AskSubjectBy(AskGo, AskDelivery, "Bad.Name"); err == nil {
t.Error("a bad asker has a subject")
}
if _, _, _, err := ParseAskSubjectBy("mesh.seat.node-build-go.accept.delivery.builder.extra"); err == nil {
t.Error("a subject of four tokens after accept parses")
}
}