Fix A (found live on 9202): the container sampler reads docker inspect JSON — a -f template stopped at 'no entry for key Health' for every app without a healthcheck, so a healthy restore read 'not started'. Commit only.
gates / gates (push) Successful in 1m27s
gates / gates (push) Successful in 1m27s
This commit is contained in:
1 parent
1ee0353dbd
commit
3a1f09d29f
3 files changed
+48
-11
No files matched your search
@@ -39,6 +39,11 @@ says „not started yet", alarms the operator; a late start is a success; never
|
|||||||
testing: the watch starts inside its own restore, before the page's record ends, so „a restore of the app runs"
|
testing: the watch starts inside its own restore, before the page's record ends, so „a restore of the app runs"
|
||||||
alone ended every watch at once and would have kept every copy (`TestFixA_AStableAppReleasesTheCopyAfterTheWindow`
|
alone ended every watch at once and would have kept every copy (`TestFixA_AStableAppReleasesTheCopyAfterTheWindow`
|
||||||
in the production order, seen failing with the plain check). Each watch reads its timing once, at its start.
|
in the production order, seen failing with the plain check). Each watch reads its timing once, at its start.
|
||||||
|
- **Found live on 9202:** the container sampler first read `docker inspect -f '…{{if .State.Health}}…'`; for a
|
||||||
|
container with NO healthcheck Docker stops with „map has no entry for key Health", so every such app read „not
|
||||||
|
started" (a healthy Vikunja restore was reported not started). It reads the inspect JSON now
|
||||||
|
(`parseContainerSamples`, `TestFixA_ParseContainerSamples`); the sampler's error keeps Docker's stderr, and a DEBUG
|
||||||
|
line says why a sample was not ok.
|
||||||
- **Fix B (R-1279, `appexport/restore.go`):** the import asks the restores' own D48 helper (`PrepareRestoreImages`,
|
- **Fix B (R-1279, `appexport/restore.go`):** the import asks the restores' own D48 helper (`PrepareRestoreImages`,
|
||||||
passed in by main.go — the package may not import `backup`) about the bundle's definition BEFORE step 1 removes the
|
passed in by main.go — the package may not import `backup`) about the bundle's definition BEFORE step 1 removes the
|
||||||
running app's volumes; a version that cannot be downloaded refuses with the two D48 sentences and nothing changed.
|
running app's volumes; a version that cannot be downloaded refuses with the two D48 sentences and nothing changed.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -3465,22 +3466,38 @@ func (a *stackAdapter) ContainerSamples(name string) ([]backup.ContainerSample,
|
|||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
out, err = run(append([]string{"inspect", "-f", "{{.Id}} {{.State.Status}} {{.RestartCount}} {{if .State.Health}}{{.State.Health.Status}}{{end}}"}, ids...)...)
|
// The JSON, not a -f template: a container with no healthcheck has no State.Health, and Docker's template stops
|
||||||
|
// with „map has no entry for key Health" (measured on 9202, 2026-10-25 — every such app read „not started").
|
||||||
|
out, err = run(append([]string{"inspect"}, ids...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return parseContainerSamples(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseContainerSamples reads `docker inspect` JSON into stable-start samples; State.Health is absent for a container
|
||||||
|
// with no healthcheck (Health ""). Pinned by TestFixA_ParseContainerSamples.
|
||||||
|
func parseContainerSamples(out []byte) ([]backup.ContainerSample, error) {
|
||||||
|
var raw []struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
RestartCount int `json:"RestartCount"`
|
||||||
|
State struct {
|
||||||
|
Status string `json:"Status"`
|
||||||
|
Health *struct {
|
||||||
|
Status string `json:"Status"`
|
||||||
|
} `json:"Health"`
|
||||||
|
} `json:"State"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(out, &raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("docker inspect: %w", err)
|
||||||
|
}
|
||||||
var res []backup.ContainerSample
|
var res []backup.ContainerSample
|
||||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
for _, c := range raw {
|
||||||
f := strings.Fields(line)
|
cs := backup.ContainerSample{ID: c.ID, Running: c.State.Status == "running", RestartCount: c.RestartCount}
|
||||||
if len(f) < 3 {
|
if c.State.Health != nil {
|
||||||
continue
|
cs.Health = c.State.Health.Status
|
||||||
}
|
}
|
||||||
n, _ := strconv.Atoi(f[2])
|
res = append(res, cs)
|
||||||
c := backup.ContainerSample{ID: f[0], Running: f[1] == "running", RestartCount: n}
|
|
||||||
if len(f) > 3 {
|
|
||||||
c.Health = f[3]
|
|
||||||
}
|
|
||||||
res = append(res, c)
|
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,3 +20,18 @@ func TestR1279_MainWiresTheImportsDownloadFirst(t *testing.T) {
|
|||||||
t.Fatal("main.go's stack adapter must read container samples for the stable-start rule (fix A)")
|
t.Fatal("main.go's stack adapter must read container samples for the stable-start rule (fix A)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix A: the sampler reads a container with NO healthcheck (no State.Health) — measured on 9202: a -f template stopped
|
||||||
|
// at „map has no entry for key Health", so every such app read „not started".
|
||||||
|
func TestFixA_ParseContainerSamples(t *testing.T) {
|
||||||
|
out := []byte(`[{"Id":"a1","RestartCount":0,"State":{"Status":"running"}},
|
||||||
|
{"Id":"b2","RestartCount":3,"State":{"Status":"running","Health":{"Status":"healthy"}}},
|
||||||
|
{"Id":"c3","RestartCount":1,"State":{"Status":"restarting"}}]`)
|
||||||
|
got, err := parseContainerSamples(out)
|
||||||
|
if err != nil || len(got) != 3 {
|
||||||
|
t.Fatalf("parse: %v %+v", err, got)
|
||||||
|
}
|
||||||
|
if !got[0].Running || got[0].Health != "" || got[1].Health != "healthy" || got[1].RestartCount != 3 || got[2].Running {
|
||||||
|
t.Fatalf("wrong samples: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user