mesh/delivery-group group feat/335-a-busy-container-is-replaced-when-idle rejected: its order contradicts itself: novox/mesh-catalog@190608193764, novox/mesh-c…
mesh/merge-gate out of date: judged against trunk 56d6313262e8, the trunk is now cbac703e817d; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 56d6313262e8, the trunk is now cbac703e817d; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
A merge into mesh-controller replaced every build agent and threw away the checks in flight (issue 594). The manifest's container busy is checked and sent only to an engine that reads it; a report's busy waits are kept, the gate starts at the report that says the container replaced, and the wait is a phase of the delivery time. mesh-builder busy drains the agent and answers while a build is in hand (novox/hq ADR 0335).
125 lines
4.2 KiB
Go
125 lines
4.2 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// A long-running container may say it is busy (novox/hq ADR 0335 §1).
|
|
//
|
|
// **Beside its health, on the container**: a command run inside it, by its shell, as an exec health check
|
|
// is, that exits 0 while the container holds work a replacement would lose and non-zero when it is idle;
|
|
// and how long one ask may take. Being asked is also its notice that it is about to be replaced: from the
|
|
// first ask it takes no new work. The node-engine asks before it replaces the container, leaves it running
|
|
// while it says busy, and replaces it on the first pass it says idle, or at 15 minutes — the engine's bound,
|
|
// never the module's.
|
|
//
|
|
// Sent only to a node-engine that reads it (link.BusyContract): an older, strict engine refuses a field it
|
|
// does not know, whole, and to it the container is replaced at once, as before.
|
|
|
|
// BusyField is the resource field.
|
|
const BusyField = "busy"
|
|
|
|
// The bounds of one ask (ADR 0335), the node-engine's own, held here near the author.
|
|
const (
|
|
BusyTimeoutDefault = 10 * time.Second
|
|
BusyTimeoutCeiling = time.Minute
|
|
)
|
|
|
|
// Busy is one container's busy, read.
|
|
type Busy struct {
|
|
Command string
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// ReadBusy reads a resource's `busy` field, its default applied. False when it carries none.
|
|
func ReadBusy(r map[string]any) (Busy, bool, []string) {
|
|
raw, present := r[BusyField]
|
|
if !present {
|
|
return Busy{}, false, nil
|
|
}
|
|
id := fmt.Sprint(r["id"])
|
|
fields, ok := raw.(map[string]any)
|
|
if !ok {
|
|
return Busy{}, true, []string{fmt.Sprintf("%s: busy is %T; it is an object with a command", id, raw)}
|
|
}
|
|
var problems []string
|
|
keys := make([]string, 0, len(fields))
|
|
for k := range fields {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
if k != "command" && k != "timeout" {
|
|
problems = append(problems, fmt.Sprintf("%s: its busy says %q; busy has a command and a timeout", id, k))
|
|
}
|
|
}
|
|
b := Busy{Timeout: BusyTimeoutDefault}
|
|
if v, has := fields["command"]; has {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
problems = append(problems, fmt.Sprintf("%s: its busy's command is %T; it is text", id, v))
|
|
}
|
|
b.Command = strings.TrimSpace(s)
|
|
}
|
|
if v, has := fields["timeout"]; has {
|
|
s, _ := v.(string)
|
|
d, err := time.ParseDuration(strings.TrimSpace(s))
|
|
switch {
|
|
case err != nil:
|
|
problems = append(problems, fmt.Sprintf("%s: its busy's timeout is %v; it is a duration such as \"10s\"", id, v))
|
|
case d <= 0 || d > BusyTimeoutCeiling:
|
|
problems = append(problems, fmt.Sprintf("%s: an ask of its busy takes more than nothing and at most %s, not %s",
|
|
id, BusyTimeoutCeiling, d))
|
|
default:
|
|
b.Timeout = d
|
|
}
|
|
}
|
|
return b, true, problems
|
|
}
|
|
|
|
// busyProblems is everything wrong with a manifest's `busy` fields, in the manifest's words: a command on
|
|
// one line, a timeout within its bounds, and only on a container that stays up.
|
|
func busyProblems(m Manifest) []string {
|
|
var problems []string
|
|
for _, r := range m.Resources {
|
|
b, has, read := ReadBusy(r)
|
|
if !has {
|
|
continue
|
|
}
|
|
where := m.Module + ": " + fmt.Sprint(r["id"])
|
|
for _, p := range read {
|
|
problems = append(problems, m.Module+": "+p)
|
|
}
|
|
if fmt.Sprint(r["type"]) != "container" || !LongRunning(r) {
|
|
problems = append(problems, fmt.Sprintf("%s declares busy and is not a container that stays up: busy is "+
|
|
"asked of a running container before it is replaced (ADR 0335)", where))
|
|
}
|
|
if b.Command == "" {
|
|
problems = append(problems, where+"'s busy names no command: one run inside the container, exiting 0 "+
|
|
"while it is busy")
|
|
}
|
|
if strings.ContainsAny(b.Command, "\n\r") {
|
|
problems = append(problems, where+"'s busy command is one line")
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// busyInto composes a container's `busy` into the node-engine's words, or takes it away: for an engine that
|
|
// reads it, the command and its timeout written out; for one that does not, the field is not sent, and the
|
|
// container is replaced at once, as before.
|
|
func busyInto(resource map[string]any, with Rendering) {
|
|
if _, has := resource[BusyField]; !has {
|
|
return
|
|
}
|
|
if !with.ReadsBusy {
|
|
delete(resource, BusyField)
|
|
return
|
|
}
|
|
b, _, _ := ReadBusy(resource)
|
|
resource[BusyField] = map[string]any{"command": b.Command, "timeout": b.Timeout.String()}
|
|
}
|