167 lines
6.0 KiB
Go
167 lines
6.0 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// **A step left running** (novox/hq issue 1890, note 5). A build module that stops while a step runs leaves its key in
|
|
// state running for good: nothing else writes that key's end, and the stream's redelivery only rewrites it when the same
|
|
// ask comes back. At its start a module ends the steps its node left running before the process began (EndStale), and
|
|
// its step-end tool ends one by hand (EndOne).
|
|
|
|
// RestartReason is why a step a previous process left running is ended.
|
|
const RestartReason = "the build module restarted while it ran"
|
|
|
|
// Scan reads a steps state: its keys and a key's stored value (nil for none).
|
|
type Scan struct {
|
|
Keys func() ([]string, error)
|
|
Get func(key string) (json.RawMessage, error)
|
|
}
|
|
|
|
// AskWhat says what an ask is, in one line: its kind, and its repository, head and levels, or its module and
|
|
// fingerprint.
|
|
func AskWhat(a delivery.StepAsk) string {
|
|
short := func(s string) string {
|
|
if len(s) > 12 {
|
|
return s[:12]
|
|
}
|
|
return s
|
|
}
|
|
str := func(m map[string]any, k string) string { v, _ := m[k].(string); return v }
|
|
parts := []string{a.Kind + " ask"}
|
|
switch {
|
|
case a.Check != nil:
|
|
parts = append(parts, str(a.Check.Fetch, "repository")+"@"+short(str(a.Check.Fetch, "commit")), "levels "+strings.Join(a.Check.Levels, ","))
|
|
if a.Check.Run != "" {
|
|
parts = append(parts, "run "+a.Check.Run)
|
|
}
|
|
case a.Build != nil:
|
|
parts = append(parts, "module "+a.Build.Module)
|
|
if r := str(a.Build.Fetch, "repository"); r != "" {
|
|
parts = append(parts, r+"@"+short(str(a.Build.Fetch, "commit")))
|
|
}
|
|
if a.Build.FP != "" {
|
|
parts = append(parts, "fp "+short(a.Build.FP))
|
|
}
|
|
case a.Compose != nil:
|
|
parts = append(parts, "compose", str(a.Compose.Fetch, "repository")+"@"+short(str(a.Compose.Fetch, "commit")))
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
// endRunning writes step ended failed with why.
|
|
func endRunning(put Put, key string, step delivery.Step, why string) error {
|
|
step.State, step.Reason, step.Ended, step.Result = delivery.StepFailed, why, now().UTC(), nil
|
|
return write(put, key, step)
|
|
}
|
|
|
|
// EndStale ends this node's steps that are running and started before `before` (the start of this process) as failed
|
|
// with RestartReason, and answers their ids. Only keys of node are read; a value that is no step is left alone.
|
|
func EndStale(scan Scan, put Put, node string, before time.Time, logf func(string, ...any)) ([]string, error) {
|
|
keys, err := scan.Keys()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("buildstep: listing the steps of %s: %w", node, err)
|
|
}
|
|
var ended []string
|
|
for _, k := range keys {
|
|
n, _, perr := delivery.ParseStepKey(k)
|
|
if perr != nil || n != node {
|
|
continue
|
|
}
|
|
raw, err := scan.Get(k)
|
|
if err != nil {
|
|
return ended, fmt.Errorf("buildstep: reading the step %s: %w", k, err)
|
|
}
|
|
if len(raw) == 0 {
|
|
continue
|
|
}
|
|
step, err := delivery.DecodeStep(raw)
|
|
if err != nil || step.State != delivery.StepRunning || !step.Started.Before(before) {
|
|
continue
|
|
}
|
|
if err := endRunning(put, k, step, RestartReason); err != nil {
|
|
return ended, fmt.Errorf("buildstep: ending the step %s: %w", k, err)
|
|
}
|
|
if logf != nil {
|
|
logf("the step %s (%s) was left running by a previous process and is ended", k, step.Tool)
|
|
}
|
|
ended = append(ended, step.ID)
|
|
}
|
|
return ended, nil
|
|
}
|
|
|
|
// EndOne ends this node's running step id (or the key <node>.<id>) as failed with why. A step that is not running is
|
|
// refused, and so is one live reports as held by this process.
|
|
func EndOne(scan Scan, put Put, node, id, why string, live func(id string) bool) (delivery.Step, error) {
|
|
id = strings.TrimSpace(id)
|
|
if n, rest, ok := strings.Cut(id, "."); ok {
|
|
if n != node {
|
|
return delivery.Step{}, fmt.Errorf("the step %q is of node %q, this module runs on %q", id, n, node)
|
|
}
|
|
id = rest
|
|
}
|
|
if strings.TrimSpace(why) == "" {
|
|
return delivery.Step{}, fmt.Errorf("say why the step is ended (why)")
|
|
}
|
|
key, err := delivery.StepKey(node, id)
|
|
if err != nil {
|
|
return delivery.Step{}, err
|
|
}
|
|
raw, err := scan.Get(key)
|
|
if err != nil {
|
|
return delivery.Step{}, err
|
|
}
|
|
if len(raw) == 0 {
|
|
return delivery.Step{}, fmt.Errorf("no step %s in the steps state", key)
|
|
}
|
|
step, err := delivery.DecodeStep(raw)
|
|
if err != nil {
|
|
return delivery.Step{}, err
|
|
}
|
|
if step.State != delivery.StepRunning {
|
|
return delivery.Step{}, fmt.Errorf("the step %s is %s, not running", key, step.State)
|
|
}
|
|
if live != nil && live(id) {
|
|
return delivery.Step{}, fmt.Errorf("the step %s is running in this process now; pause the seat or wait for it", key)
|
|
}
|
|
if err := endRunning(put, key, step, why); err != nil {
|
|
return delivery.Step{}, err
|
|
}
|
|
step.State, step.Reason = delivery.StepFailed, why
|
|
return step, nil
|
|
}
|
|
|
|
// StepEndTool is the module's step-end tool, named name (build_go_step_end): it ends one step left running, with a
|
|
// reason, refusing one this process still runs. p may be nil.
|
|
func StepEndTool(name string, node string, scan Scan, put Put, p *Puller) stdio.Tool {
|
|
return stdio.Tool{Name: name,
|
|
Description: "End one step of this machine that is stuck in state running (for instance left by a restart of the module) " +
|
|
"as failed, with the reason given; its asker then sees it over. A step not running, or one this module runs now, is refused. " +
|
|
"id is the step id or the key <node>.<id>.",
|
|
Input: map[string]any{"type": "object", "required": []string{"id", "why"}, "properties": map[string]any{
|
|
"id": map[string]any{"type": "string", "description": "the step id, or the key <node>.<id>"},
|
|
"why": map[string]any{"type": "string", "description": "why it is ended; it becomes the step's reason"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
id, _ := args["id"].(string)
|
|
why, _ := args["why"].(string)
|
|
var live func(string) bool
|
|
if p != nil {
|
|
live = func(id string) bool {
|
|
for _, r := range p.Running() {
|
|
if r.ID == id {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
return EndOne(scan, put, node, id, why, live)
|
|
}}
|
|
}
|