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..accept..`, 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:`. 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(keyAsker(msg), 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) } }