Judge how a module says it is ready, beside whether it stays up (hq ADR 0240, to-be 48 Phase B)
Liveness alone could not see a web application whose port was open and whose program ran while every request hung for eleven hours (issue 145). A resource now carries the `health` its module declared: the engine makes http and tcp looks itself from the machine to the endpoint's published port, reads a unit's readiness from the show it already makes, hands an exec command or the image's own check to the runtime as the container's check with the declared timing and reads its state from the inspect it already makes, and asks a module's tool on its own node tools. Starting until the check passed, unhealthy once its looks after the grace fail the declared number of times; never more looks than the measured budget; nothing restarted. The statement says contract 2, which tells the controller this engine may be sent the field.
This commit is contained in:
@@ -1987,6 +1987,12 @@ func containerSpecReading(r *declaration.Container, declares, reads map[string]s
|
||||
if r.Schedule != "" {
|
||||
b.WriteString("schedule " + r.Schedule + "\n")
|
||||
}
|
||||
// And the check the runtime runs as its own (novox/hq ADR 0240 rule 3): an exec command or the
|
||||
// image's own, with the declared timing, is fixed when the container is created. Only those two:
|
||||
// an http, tcp, unit or tool check the engine makes itself, so declaring one recreates nothing.
|
||||
if h := r.Health; h.RunByRuntime() {
|
||||
b.WriteString("health " + strings.Join(runtimeCheckArgs(h), " ") + "\n")
|
||||
}
|
||||
// **What this container reads is part of what it is.**
|
||||
//
|
||||
// A container takes its environment and its mounted files once, at start, and never looks
|
||||
@@ -2230,6 +2236,12 @@ func applyContainer(ctx context.Context, r *declaration.Container, run Runner,
|
||||
for _, v := range r.Volumes {
|
||||
args = append(args, "--volume", v)
|
||||
}
|
||||
// **The one check the container carries is the one the module declared** (ADR 0240 rule 3): an
|
||||
// exec command, or the image's own adopted by name, with the declared timing. Nothing else on the
|
||||
// machine sets a container's check.
|
||||
if r.Health.RunByRuntime() {
|
||||
args = append(args, runtimeCheckArgs(r.Health)...)
|
||||
}
|
||||
for _, h := range r.Hosts {
|
||||
// Written into the container's own hosts file by the runtime. Per container rather than
|
||||
// by editing the machine's resolver configuration: that file belongs to something else on
|
||||
@@ -2287,6 +2299,20 @@ func applyContainer(ctx context.Context, r *declaration.Container, run Runner,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// runtimeCheckArgs is a declared check as the runtime takes it (novox/hq ADR 0240 rule 3, to-be 48 §3):
|
||||
// its timing, and for an exec check its command, run by the container's shell. The runtime's own retries
|
||||
// are the declared failing looks, and its start period the grace — so its retries and start period do the
|
||||
// timing, with no execution per look from outside. For the image's own check no command: the image's
|
||||
// stays, under the declared timing.
|
||||
func runtimeCheckArgs(h *declaration.Health) []string {
|
||||
args := []string{"--health-interval", h.Interval, "--health-timeout", h.Timeout,
|
||||
"--health-retries", strconv.Itoa(h.Looks), "--health-start-period", h.Grace}
|
||||
if h.Kind == declaration.HealthExec {
|
||||
args = append([]string{"--health-cmd", h.Command}, args...)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// applyRunOnce runs a container to completion, once, and requires it to exit 0 (novox/hq ADR 0052).
|
||||
//
|
||||
// It is a step, not a service: the module's own code seeding a store, migrating a schema or
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// The node-engine owns every verdict, and nothing else sets a container's check (novox/hq ADR 0240 rule 3,
|
||||
// "how it is checked"): a declared command becomes the container's check with the declared timing, an
|
||||
// adopted image check keeps the image's command under the declared timing, and a check the engine makes
|
||||
// itself — http, tcp — sets nothing on the container and recreates nothing.
|
||||
func TestADeclaredCommandBecomesTheContainersCheckAndNothingElseSetsOne(t *testing.T) {
|
||||
pinned := "postgres@sha256:" + strings.Repeat("a", 64)
|
||||
ranWith := func(health string) []string {
|
||||
t.Helper()
|
||||
var ran []string
|
||||
run := func(_ context.Context, cmd string, args ...string) (string, error) {
|
||||
if cmd == "docker" && len(args) > 0 && args[0] == "run" {
|
||||
ran = args
|
||||
return "deadbeef\n", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
field := ""
|
||||
if health != "" {
|
||||
field = `,"health":` + health
|
||||
}
|
||||
d := parseTrusted(t, `{"declaration":1,"resources":[
|
||||
{"id":"db","type":"container","name":"db","image":"`+pinned+`","ports":["31001:5432"]`+field+`}
|
||||
]}`)
|
||||
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
||||
if ran == nil {
|
||||
t.Fatal("the container was not run")
|
||||
}
|
||||
return ran
|
||||
}
|
||||
healthFlags := func(args []string) []string {
|
||||
var out []string
|
||||
for i, a := range args {
|
||||
if strings.HasPrefix(a, "--health") || a == "--no-healthcheck" {
|
||||
out = append(out, a)
|
||||
if i+1 < len(args) {
|
||||
out = append(out, args[i+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
exec := healthFlags(ranWith(`{"kind":"exec","command":"pg_isready -q","interval":"30s","timeout":"5s","looks":3,"grace":"1m0s"}`))
|
||||
want := []string{"--health-cmd", "pg_isready -q", "--health-interval", "30s", "--health-timeout", "5s",
|
||||
"--health-retries", "3", "--health-start-period", "1m0s"}
|
||||
if !slices.Equal(exec, want) {
|
||||
t.Errorf("a declared command was run as %v, want %v", exec, want)
|
||||
}
|
||||
adopted := healthFlags(ranWith(`{"kind":"runtime","interval":"20s","timeout":"3s","looks":2,"grace":"30s"}`))
|
||||
if slices.Contains(adopted, "--health-cmd") || !slices.Contains(adopted, "20s") || !slices.Contains(adopted, "2") {
|
||||
t.Errorf("an adopted image check was run as %v: the image's command under the declared timing", adopted)
|
||||
}
|
||||
for _, h := range []string{"", `{"kind":"http","endpoint":"web","port":31001,"path":"/","interval":"30s","timeout":"5s","looks":3,"grace":"1m0s"}`,
|
||||
`{"kind":"tcp","port":31001,"interval":"30s","timeout":"5s","looks":3,"grace":"1m0s"}`} {
|
||||
if got := healthFlags(ranWith(h)); len(got) > 0 {
|
||||
t.Errorf("a container whose check the engine makes itself (%q) was given the runtime's: %v", h, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Declaring an http check recreates nothing; declaring a command does.
|
||||
plain := &declaration.Container{ID: "db", Name: "db", Image: pinned}
|
||||
withHTTP := *plain
|
||||
withHTTP.Health = &declaration.Health{Kind: "http", Port: 31001, Path: "/", Interval: "30s", Timeout: "5s", Looks: 3, Grace: "1m0s"}
|
||||
withExec := *plain
|
||||
withExec.Health = &declaration.Health{Kind: "exec", Command: "true", Interval: "30s", Timeout: "5s", Looks: 3, Grace: "1m0s"}
|
||||
if containerSpec(plain, inputs{}) != containerSpec(&withHTTP, inputs{}) {
|
||||
t.Error("declaring an http check would recreate the container")
|
||||
}
|
||||
if containerSpec(plain, inputs{}) == containerSpec(&withExec, inputs{}) {
|
||||
t.Error("declaring a command would not reach a running container: it is fixed when the container is made")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user