Files
mesh-delivery/modules/mesh-delivery-sdk/steprun/askers.go
T
jschoubben 7125a9f92f C1c-2a: the step runner says a run's answer in step-ended, inline or as an object, and serves it
The puller's end of a run carries the run's program's last line of standard output as `output` up to StepEndedInlineMost, else keeps
it with KeepOutput and names the object in `output-object`; a program that exits other than 0 ends failed. OutputTool serves the
seat's verb `output` from the kept object, as the whole file the tool runner sends in chunks; OutputsStore names the store.

Builds: design 47 G A-254, A-257, §602

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 09:37:51 +01:00

150 lines
5.6 KiB
Go

package steprun
import (
"encoding/json"
"fmt"
"slices"
"strings"
"sync/atomic"
buildrun "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildrun"
sdkstep "git.novox.be/novox/mesh-delivery/modules/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"
)
// **A holder refuses a kind its asker may not ask** (design 47 G §543, A-163, A-170). An ask rides
// `mesh.seat.<seat>.accept.<queue>.<asker>`, the asker the asking seat's own name, and its kind stays in its body. The
// holder takes the asker from the subject and checks the kind against the callers the seat's definition lists for it (read
// from the seat list); on main's subject, which names no asker, it takes only a build and a run.
// KindOf is the word the seat definition gives an ask's kind: run for a run or a compose, else the ask's own kind.
func KindOf(ask sdkstep.StepAsk) string {
if ask.Run != nil || ask.Compose != nil {
return "run"
}
return ask.Kind
}
// CallersFromDefinitions is a Puller.Callers: the seats the definition of language's build seat lists as callers of an
// accepted kind, from the seat list's definitions (seatlist.Reader.Definitions). found is false when the definition or the
// kind is not held yet. A caller entry is `seat:<name>`.
func CallersFromDefinitions(language string, definitions func() map[string]seatdef.Definition) func(kind string) (callers []string, found bool) {
return func(kind string) ([]string, bool) {
seat, err := sdkstep.BuildSeat(language)
if err != nil {
return nil, false
}
def, ok := definitions()[seat]
if !ok {
return nil, false
}
var out []string
found := false
for _, m := range def.Accepts {
if m.Name != kind {
continue
}
found = true
if m.Callers != nil {
for _, e := range m.Callers.List {
if name, ok := strings.CutPrefix(e, "seat:"); ok {
out = append(out, name)
}
}
}
}
return out, found
}
}
// askerRefusal is why an ask must not be taken from its subject's asker: "" when it may be, and judged false when the seat
// list does not say yet (the ask is given back, not refused).
func (p *Puller) askerRefusal(msg *Pulled, ask sdkstep.StepAsk) (why string, judged bool) {
if msg.Subject == "" {
return "", true // a source that gives no subject (a test's)
}
_, _, asker, err := sdkstep.ParseAskSubjectBy(msg.Subject)
if err != nil {
return fmt.Sprintf("the ask came on %s, which is no accept subject of this seat", msg.Subject), true
}
kind := KindOf(ask)
if asker == "" {
if (ask.Build != nil && ask.Kind == sdkstep.AskDelivery) || kind == "run" {
return "", true
}
return fmt.Sprintf("the kind %s is not taken on main's subject, which takes only a build and a run (asker: none named)", kind), true
}
if p.Callers == nil {
return "", true
}
callers, found := p.Callers(kind)
if !found {
return "", false
}
if !slices.Contains(callers, asker) {
return fmt.Sprintf("the kind %s is not one %s may ask: its callers are %s", kind, asker, strings.Join(callers, ", ")), true
}
return "", true
}
// refuseAsker ends an ask whose asker may not ask its kind: its step key says failed with the reason, naming the kind and the
// asker, step-ended says failed, and the message is dropped.
func (p *Puller) refuseAsker(msg *Pulled, ask sdkstep.StepAsk, why string) {
key, err := sdkstep.AskStepKey(p.Node, ask.ID)
if err == nil {
at := now().UTC()
step := sdkstep.Step{ID: ask.ID, Tool: ask.Kind + " ask", Node: p.Node, State: sdkstep.StepFailed, Started: at, Ended: at,
Reason: "error: " + why, Owner: buildrun.Owner, Run: int(msg.Delivered)}
err = writeEnd(p.Put, key, step)
}
if err != nil {
p.logf("the ask %s of the wrong asker cannot be answered on %s, so it is given back: %v", ask.ID, p.Node, err)
_ = msg.Nak()
return
}
p.logf("the ask %s is refused on %s: %s", ask.ID, p.Node, why)
if ask.Step == nil {
p.sayEnded(msg, ask, sdkstep.OutcomeFailed, why, nil)
} else if end, err := sdkstep.EndOf(ask, p.Node, sdkstep.OutcomeFailed); err == nil {
// A pipeline step's end names its pipeline, step, commit and fingerprint (its own form, §602).
end.Said, end.At, end.Asker = why, now().UTC(), askerOf(msg, ask)
if body, err := sdkstep.EncodeStepEnded(end); err == nil {
p.say("step-ended", json.RawMessage(body), "step-ended."+ask.ID)
}
}
_ = msg.Term()
}
// giveBack hands an ask back to be judged again a little later, once the seat list says more.
func (p *Puller) giveBack(msg *Pulled, ask sdkstep.StepAsk) {
p.logf("the ask %s is given back on %s: the seat list holds no definition of its kind yet", ask.ID, p.Node)
if msg.NakAfter != nil && msg.NakAfter(DefaultPullWait) == nil {
return
}
_ = msg.Nak()
}
// SeatListCallers is a Puller.Callers that follows the seat list: it starts the reader once the runtime serves
// (seatlist.Start through the runtime's own way in) and answers from its definitions, found false until it has read them.
// A reader that cannot start is said and the asks are given back until it can (design 47 G §543, A-163).
func SeatListCallers(language string, logf func(format string, args ...any)) func(kind string) ([]string, bool) {
var reader atomic.Pointer[seatlist.Reader]
go func() {
<-stdio.Served()
r, err := seatlist.Start(seatlist.Config{Bus: seatlist.StdioBus(), Bootstrap: seatlist.Bootstrapped()})
if err != nil && logf != nil {
logf("following the seat list: %v", err)
}
reader.Store(r)
}()
return func(kind string) ([]string, bool) {
r := reader.Load()
if r == nil {
return nil, false
}
return CallersFromDefinitions(language, r.Definitions)(kind)
}
}