zdd rev 19: probe checks only platform launches; rule 4 numbers
zddprobe enforces rule 5 only on a platform launch, the one that passes --db (the flag package reads -db and --db alike). Vet's boot passes no --db, no CARLOS_HOST and a socket name of its own, so it skips the check and serves; before this the probe could not pass vet at all. Tests cover both vet forms (-socket/--socket, any name) and both db forms. The amendment's rule 4 now states the measured bound: the host alone, at the largest generation number, so flat hosts up to 59 bytes get generation paths against 82 flag-off, and why a row must not change treatment as the counter grows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+73
−36
cmd/zddprobe/main.go+19 −13cmd/zddprobe/main_test.go+46 −23docs/superpowers/specs/2026-10-13-zero-downtime-deploys-rev19-amendment.md+8 −0
diff --git a/cmd/zddprobe/main.go b/cmd/zddprobe/main.go| index 66d7f10a7..a13cf9574 100644 |
| --- a/cmd/zddprobe/main.go |
| +++ b/cmd/zddprobe/main.go |
| @@ -9,14 +9,16 @@ |
| // `<binary> -socket <sock>`. The probe takes both. It keeps no data, so --db |
| // is accepted and ignored. |
| // |
| -// Before it listens, the probe checks what a real app may rely on (zero- |
| -// downtime spec rev 19, rule 5), and exits 1 with a line on stderr if |
| -// either is wrong: CARLOS_HOST must be set, and the file name of --socket |
| -// must be the flag-off one, app.sock (nested) or $CARLOS_HOST.sock (flat). |
| -// A generation launch that named its socket anything else crash-looped a |
| -// tenant whose framework reads its host from that name (amadan #272); the |
| -// probe ignored the name and passed. A vet boot of the probe needs |
| -// CARLOS_HOST in its environment too. |
| +// On a platform launch, the one that passes --db, the probe checks before |
| +// it listens what a real app may rely on (zero-downtime spec rev 19, rule |
| +// 5), and exits 1 with a line on stderr if either is wrong: CARLOS_HOST |
| +// must be set, and the file name of --socket must be the flag-off one, |
| +// app.sock (nested) or $CARLOS_HOST.sock (flat). A generation launch that |
| +// named its socket anything else crash-looped a tenant whose framework |
| +// reads its host from that name (amadan #272); the probe ignored the name |
| +// and passed. Vet's boot passes no --db, no CARLOS_HOST and a socket name |
| +// of its own choosing, so it skips the check and the probe serves. (The |
| +// flag package reads -db and --db alike, so --db being given is the test.) |
| // |
| // version is set at build time with -ldflags "-X main.version=...". |
| // |
| @@ -51,9 +53,9 @@ func main() { |
| } |
| } |
| -// checkLaunch is rule 5's check of the launch: host (CARLOS_HOST) is set, |
| -// and sock's file name is app.sock or <host>.sock, the names a flag-off |
| -// launch gives. |
| +// checkLaunch is rule 5's check of a platform launch: host (CARLOS_HOST) |
| +// is set, and sock's file name is app.sock or <host>.sock, the names a |
| +// flag-off launch gives. |
| func checkLaunch(sock, host string) error { |
| if host == "" { |
| return errors.New("zddprobe: CARLOS_HOST is empty; the platform sets it on every instance launch") |
| @@ -75,8 +77,12 @@ func run(args []string, stop <-chan os.Signal) error { |
| if *sock == "" { |
| return errors.New("zddprobe: --socket is required") |
| } |
| - if err := checkLaunch(*sock, os.Getenv("CARLOS_HOST")); err != nil { |
| - return err |
| + platform := false |
| + fs.Visit(func(f *flag.Flag) { platform = platform || f.Name == "db" }) |
| + if platform { |
| + if err := checkLaunch(*sock, os.Getenv("CARLOS_HOST")); err != nil { |
| + return err |
| + } |
| } |
| _ = os.Remove(*sock) |
| l, err := net.Listen("unix", *sock) |
diff --git a/cmd/zddprobe/main_test.go b/cmd/zddprobe/main_test.go| index 1a9db42ae..28dfbda6b 100644 |
| --- a/cmd/zddprobe/main_test.go |
| +++ b/cmd/zddprobe/main_test.go |
| @@ -123,7 +123,9 @@ func TestProbeServesWithThePlatformsRunArgv(t *testing.T) { |
| func TestProbeServesWithVetsArgv(t *testing.T) { |
| sock := filepath.Join(t.TempDir(), "app.sock") |
| - launch(t, []string{"CARLOS_HOST=shop.example.com"}, "-socket", sock) // internal/vet/vet.go bootHealthz |
| + // Vet's argv: no --db and no CARLOS_HOST. Rule 5 is a platform-launch |
| + // check and does not apply. |
| + launch(t, []string{"CARLOS_HOST="}, "-socket", sock) // internal/vet/vet.go bootHealthz: no --db, no CARLOS_HOST |
| if body := getOverUnix(t, sock, "/"); !strings.HasPrefix(body, "ok ") { |
| t.Fatalf("body %q", body) |
| } |
| @@ -161,10 +163,24 @@ func TestProbeServesOnTheFlatHostSocketName(t *testing.T) { |
| } |
| } |
| -// Rev 19, rule 5: a launch whose socket file name is not a flag-off one, or |
| -// that has no CARLOS_HOST, exits 1 with a line on stderr, before it |
| -// listens, so a probe on such a launch fails its health check as the |
| -// tenant did. |
| +// A vet boot is not checked whatever its socket is called: vet picks the |
| +// name, and CARLOS_HOST is not set. |
| +func TestProbeServesVetsArgvWithAnyName(t *testing.T) { |
| + for _, form := range []string{"-socket", "--socket"} { |
| + t.Run(form, func(t *testing.T) { |
| + sock := filepath.Join(t.TempDir(), "probe.3.sock") |
| + launch(t, []string{"CARLOS_HOST="}, form, sock) |
| + if body := getOverUnix(t, sock, "/"); !strings.HasPrefix(body, "ok ") { |
| + t.Fatalf("body %q", body) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// Rev 19, rule 5: a platform launch (it passes --db) whose socket file name |
| +// is not a flag-off one, or that has no CARLOS_HOST, exits 1 with a line on |
| +// stderr, before it listens, so a probe on such a launch fails its health |
| +// check as the tenant did. -db and --db are the same flag. |
| func TestProbeRefusesALaunchARealAppMightRefuse(t *testing.T) { |
| for _, tc := range []struct { |
| name, host, file, want string |
| @@ -175,24 +191,31 @@ func TestProbeRefusesALaunchARealAppMightRefuse(t *testing.T) { |
| {"no CARLOS_HOST, nested name", "", "app.sock", "CARLOS_HOST is empty"}, |
| {"no CARLOS_HOST, flat name", "", "shop.example.com.sock", "CARLOS_HOST is empty"}, |
| } { |
| - t.Run(tc.name, func(t *testing.T) { |
| - sock := filepath.Join(t.TempDir(), tc.file) |
| - cmd := exec.Command(os.Args[0], "--socket", sock, "--db", "x.db") |
| - cmd.Env = append(os.Environ(), "ZDDPROBE_AS_MAIN=1", "CARLOS_HOST="+tc.host) |
| - var stderr strings.Builder |
| - cmd.Stderr = &stderr |
| - err := cmd.Run() |
| - var exit *exec.ExitError |
| - if !errors.As(err, &exit) || exit.ExitCode() != 1 { |
| - t.Fatalf("exit = %v, want status 1", err) |
| - } |
| - if !strings.Contains(stderr.String(), tc.want) { |
| - t.Fatalf("stderr %q, want it to say %q", stderr.String(), tc.want) |
| - } |
| - if _, err := os.Lstat(sock); !os.IsNotExist(err) { |
| - t.Fatalf("the probe listened before refusing (lstat %s: %v)", sock, err) |
| - } |
| - }) |
| + for _, form := range []string{"--db", "-db"} { |
| + t.Run(tc.name+" "+form, func(t *testing.T) { |
| + refused(t, tc.host, tc.file, form, tc.want) |
| + }) |
| + } |
| + } |
| +} |
| + |
| +func refused(t *testing.T, host, file, dbFlag, want string) { |
| + t.Helper() |
| + sock := filepath.Join(t.TempDir(), file) |
| + cmd := exec.Command(os.Args[0], "--socket", sock, dbFlag, "x.db") |
| + cmd.Env = append(os.Environ(), "ZDDPROBE_AS_MAIN=1", "CARLOS_HOST="+host) |
| + var stderr strings.Builder |
| + cmd.Stderr = &stderr |
| + err := cmd.Run() |
| + var exit *exec.ExitError |
| + if !errors.As(err, &exit) || exit.ExitCode() != 1 { |
| + t.Fatalf("exit = %v, want status 1", err) |
| + } |
| + if !strings.Contains(stderr.String(), want) { |
| + t.Fatalf("stderr %q, want it to say %q", stderr.String(), want) |
| + } |
| + if _, err := os.Lstat(sock); !os.IsNotExist(err) { |
| + t.Fatalf("the probe listened before refusing (lstat %s: %v)", sock, err) |
| } |
| } |
diff --git a/docs/superpowers/specs/2026-10-13-zero-downtime-deploys-rev19-amendment.md b/docs/superpowers/specs/2026-10-13-zero-downtime-deploys-rev19-amendment.md| index 672cad915..1e2a533c5 100644 |
| --- a/docs/superpowers/specs/2026-10-13-zero-downtime-deploys-rev19-amendment.md |
| +++ b/docs/superpowers/specs/2026-10-13-zero-downtime-deploys-rev19-amendment.md |
| @@ -62,6 +62,14 @@ ignores its socket name. |
| because the flag-off path `/run/carlos/<host>.sock` is already near the |
| bound for the same host. Nested paths are bounded by the 40-byte instance |
| identifier and stay well under it. |
| + |
| + The bound is measured on the host alone: the control socket |
| + `/run/carlos/g/<n>/<host>.sock.control` at the largest generation number |
| + (20 digits). So flat hosts of up to 59 bytes get generation paths, |
| + against 82 bytes for the flag-off control socket. Measuring at the |
| + current number would let a row change treatment as the counter grows, |
| + even in the middle of a swap: an attempt admitted as an overlap could |
| + meet a generation that cannot bind. |
| 5. **The probe checks it.** `cmd/zddprobe` exits 1, with a clear line on |
| stderr, before it listens if either of these is wrong: |
| - the base name of `--socket` is neither `app.sock` nor |