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()} }