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
150 lines
5.6 KiB
Go
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)
|
|
}
|
|
}
|