Since the build CPU cap (issue 1806) shanks' mesh-build-go kept capacity 3 inside a 12-core slice: three full checks got about 4 cores each and ran past the 30-minute step bound (issue 1890). The puller now judges by min(configured, floor(slice quota cores / cores-per-step)), the quota read live from its cgroup, says it on change and writes capacity.<node> to the pauses state. A run's phases (fetch and merge, digest, tests) are logged with the ask id and written to its running step key; Asker.Wait hands every step value to a WithStepSeen hook.
125 lines
4.2 KiB
Go
125 lines
4.2 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// StepsState is the name of the state a build module keeps its long steps in; its bucket is `<module>_steps`, built
|
|
// by StepsBucket (novox/hq ADR 0469 clause 2; issue 1471). A tool call is cut at 30 s, so a fetch, a test level or a
|
|
// build answers at once with a step id and writes its progress and its end to the key `<node>.<step id>`.
|
|
const StepsState = "steps"
|
|
|
|
// StepsBucket is the bucket of a build module's steps.
|
|
func StepsBucket(module string) string { return module + "_" + StepsState }
|
|
|
|
// The states of a step. A step ends in passed, failed or refused; its key is written for each of them, and that
|
|
// write is the event the step's reader waits on.
|
|
const (
|
|
StepRunning = "running"
|
|
StepPassed = "passed"
|
|
StepFailed = "failed"
|
|
StepRefused = "refused"
|
|
)
|
|
|
|
var stepIDOK = regexp.MustCompile(`^[a-z0-9]+$`)
|
|
|
|
func checkStepID(id string) error {
|
|
if !stepIDOK.MatchString(id) {
|
|
return fmt.Errorf("delivery: the step id %q is not lower-case letters and digits", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StepKey is the key of a node's step: `<node>.<id>`. It refuses a node that is not a name and an id that is not
|
|
// lower-case letters and digits (one key token, never a dot).
|
|
func StepKey(node, id string) (string, error) {
|
|
if err := checkName("node", node); err != nil {
|
|
return "", err
|
|
}
|
|
if err := checkStepID(id); err != nil {
|
|
return "", err
|
|
}
|
|
return node + "." + id, nil
|
|
}
|
|
|
|
// ParseStepKey is the inverse of StepKey.
|
|
func ParseStepKey(key string) (node, id string, err error) {
|
|
node, id, ok := strings.Cut(key, ".")
|
|
if !ok {
|
|
return "", "", fmt.Errorf("delivery: %q is not a step key, <node>.<id>", key)
|
|
}
|
|
if _, err := StepKey(node, id); err != nil {
|
|
return "", "", fmt.Errorf("delivery: %q is not a step key: %w", key, err)
|
|
}
|
|
return node, id, nil
|
|
}
|
|
|
|
// Step is the value of a steps key: which tool runs on which node, how far it is, when it started and ended (UTC, as
|
|
// the toolchains' At), why it was refused or failed, and what it returned.
|
|
type Step struct {
|
|
V int `json:"v"`
|
|
ID string `json:"id"`
|
|
Tool string `json:"tool"`
|
|
Node string `json:"node"`
|
|
State string `json:"state"`
|
|
Started time.Time `json:"started"`
|
|
Ended time.Time `json:"ended,omitzero"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
// What says what the step is, for a reader of the steps state: an ask's kind, repository, head and levels or
|
|
// module (issue 1890). Empty for a step a tool started by hand.
|
|
What string `json:"what,omitempty"`
|
|
// Phase is the part of its job a running ask's step is in (fetch, a level's digest, lint or tests), and PhaseAt
|
|
// when it entered it: the step key is written again at each phase, so a slow step says where it stands (issue 1890).
|
|
// A reader waiting for the end treats such a write as any running one.
|
|
Phase string `json:"phase,omitempty"`
|
|
PhaseAt time.Time `json:"phase-at,omitzero"`
|
|
}
|
|
|
|
func (s Step) check() error {
|
|
if err := checkStepID(s.ID); err != nil {
|
|
return err
|
|
}
|
|
if err := checkName("node", s.Node); err != nil {
|
|
return err
|
|
}
|
|
if s.Tool == "" {
|
|
return fmt.Errorf("delivery: a step needs its tool")
|
|
}
|
|
switch s.State {
|
|
case StepRunning, StepPassed, StepFailed, StepRefused:
|
|
default:
|
|
return fmt.Errorf("delivery: the step state %q is not running, passed, failed or refused", s.State)
|
|
}
|
|
if len(s.Result) > 0 && !json.Valid(s.Result) {
|
|
return fmt.Errorf("delivery: the step's result is not JSON")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeStep is s as JSON at Version; it refuses an id that is not one key token, a node that is not a name, an empty
|
|
// tool, a state outside the four and a result that is not JSON.
|
|
func EncodeStep(s Step) ([]byte, error) {
|
|
if err := s.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
s.V = Version
|
|
return json.Marshal(s)
|
|
}
|
|
|
|
// DecodeStep reads a steps value; it refuses an unknown version (ErrUnknownVersion) and what EncodeStep refuses.
|
|
func DecodeStep(data []byte) (Step, error) {
|
|
var s Step
|
|
if err := decode(data, &s); err != nil {
|
|
return Step{}, err
|
|
}
|
|
if err := s.check(); err != nil {
|
|
return Step{}, err
|
|
}
|
|
return s, nil
|
|
}
|