carlos / platform Public

cmd/zddprobe: the roll notes' probe, taking the platform's --socket and --db (#272)

The probe in the roll notes parsed only -socket, so the platform's
`--socket <sock> --db <db>` launch made it exit at once. Move it into the
module, accept both argvs, test it by launching it as the platform does,
and build it from there in the roll notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 59b1895a9351be22f0ad4d3d07305acd3bd0cd0a parent 891c0fb
3 files changed, +248 −50
  • cmd/zddprobe/main.go +83 −0
  • cmd/zddprobe/main_test.go +152 −0
  • docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md +13 −50
diff --git a/cmd/zddprobe/main.go b/cmd/zddprobe/main.go
new file mode 100644
index 000000000..9d692bc01
--- /dev/null
+++ b/cmd/zddprobe/main.go
@@ -0,0 +1,83 @@
+// Command zddprobe is the probe app for the zero-downtime deploy roll notes
+// (docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md).
+// It serves HTTP on a unix socket. Every request sleeps 50 ms, so requests are
+// in flight across a swap, then answers "ok <version>". SIGTERM or SIGINT
+// drains in-flight requests, waiting at most 15 s, and exits 0.
+//
+// The platform starts an exec-backed instance as
+// `<binary> --socket <sock> --db <db>`, and `carlos deploy`'s vet starts it as
+// `<binary> -socket <sock>`. The probe takes both. It keeps no data, so --db
+// is accepted and ignored.
+//
+// version is set at build time with -ldflags "-X main.version=...".
+//
+// With ZDDPROBE_TRACE=1 the probe writes one line, "serving", to stdout as a
+// request enters the handler, so a test can tell a request is in flight
+// before it signals. Without it the probe writes nothing to stdout.
+package main
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+)
+
+var version = "dev"
+
+func main() {
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)
+ if err := run(os.Args[1:], sigs); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+func run(args []string, stop <-chan os.Signal) error {
+ fs := flag.NewFlagSet("zddprobe", flag.ContinueOnError)
+ fs.SetOutput(os.Stderr)
+ sock := fs.String("socket", "", "unix socket to serve on")
+ fs.String("db", "", "accepted for the platform's argv and ignored")
+ if err := fs.Parse(args); err != nil {
+ return err
+ }
+ if *sock == "" {
+ return errors.New("zddprobe: --socket is required")
+ }
+ _ = os.Remove(*sock)
+ l, err := net.Listen("unix", *sock)
+ if err != nil {
+ return err
+ }
+
+ trace := os.Getenv("ZDDPROBE_TRACE") == "1"
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if trace {
+ fmt.Fprintln(os.Stdout, "serving")
+ }
+ time.Sleep(50 * time.Millisecond) // keep requests in flight across the switch
+ _, _ = io.WriteString(w, "ok "+version+"\n")
+ })
+ srv := &http.Server{Handler: mux}
+
+ drained := make(chan error, 1)
+ go func() {
+ <-stop
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer cancel()
+ drained <- srv.Shutdown(ctx)
+ }()
+ if err := srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ return err
+ }
+ return <-drained
+}
diff --git a/cmd/zddprobe/main_test.go b/cmd/zddprobe/main_test.go
new file mode 100644
index 000000000..42a002879
--- /dev/null
+++ b/cmd/zddprobe/main_test.go
@@ -0,0 +1,152 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+)
+
+func TestMain(m *testing.M) {
+ if os.Getenv("ZDDPROBE_AS_MAIN") == "1" {
+ main()
+ os.Exit(0)
+ }
+ os.Exit(m.Run())
+}
+
+// launch starts this test binary as the probe with argv, as the platform
+// starts an instance: argv only, no shell. It returns the command and a
+// reader on the probe's stdout.
+func launch(t *testing.T, env []string, argv ...string) (*exec.Cmd, *bufio.Reader) {
+ t.Helper()
+ cmd := exec.Command(os.Args[0], argv...)
+ cmd.Env = append(append(os.Environ(), "ZDDPROBE_AS_MAIN=1"), env...)
+ cmd.Stderr = os.Stderr
+ out, err := cmd.StdoutPipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = cmd.Process.Kill(); _, _ = cmd.Process.Wait() })
+ return cmd, bufio.NewReader(out)
+}
+
+func unixClient(sock string) *http.Client {
+ return &http.Client{
+ Timeout: 10 * time.Second,
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
+ var d net.Dialer
+ return d.DialContext(ctx, "unix", sock)
+ },
+ },
+ }
+}
+
+// getOverUnix retries until the socket answers, with a 10 s cap, and returns
+// the body.
+func getOverUnix(t *testing.T, sock, path string) string {
+ t.Helper()
+ c := unixClient(sock)
+ deadline := time.Now().Add(10 * time.Second)
+ for {
+ resp, err := c.Get("http://probe" + path)
+ if err == nil {
+ defer resp.Body.Close()
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return string(b)
+ }
+ if time.Now().After(deadline) {
+ t.Fatalf("probe never answered on %s: %v", sock, err)
+ }
+ time.Sleep(50 * time.Millisecond)
+ }
+}
+
+func TestProbeServesWithThePlatformsRunArgv(t *testing.T) {
+ dir := t.TempDir()
+ sock, db := filepath.Join(dir, "app.sock"), filepath.Join(dir, "app.db")
+ cmd, stdout := launch(t, []string{"ZDDPROBE_TRACE=1"}, "--socket", sock, "--db", db) // backend_exec.go:2335
+ if body := getOverUnix(t, sock, "/healthz"); !strings.HasPrefix(body, "ok ") {
+ t.Fatalf("body %q", body)
+ }
+ if line, err := stdout.ReadString('\n'); err != nil || line != "serving\n" {
+ t.Fatalf("trace line %q, %v", line, err)
+ }
+
+ // SIGTERM drains: a request inside the handler still completes, then exit 0.
+ type result struct {
+ status int
+ body string
+ err error
+ }
+ done := make(chan result, 1)
+ go func() {
+ resp, err := unixClient(sock).Get("http://probe/inflight")
+ if err != nil {
+ done <- result{err: err}
+ return
+ }
+ defer resp.Body.Close()
+ b, err := io.ReadAll(resp.Body)
+ done <- result{resp.StatusCode, string(b), err}
+ }()
+ if line, err := stdout.ReadString('\n'); err != nil || line != "serving\n" {
+ t.Fatalf("trace line %q, %v", line, err)
+ }
+ if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
+ t.Fatal(err)
+ }
+ r := <-done
+ if r.err != nil || r.status != 200 || !strings.HasPrefix(r.body, "ok ") {
+ t.Fatalf("in-flight request: %d %q %v", r.status, r.body, r.err)
+ }
+ if err := cmd.Wait(); err != nil {
+ t.Fatalf("exit after SIGTERM: %v", err)
+ }
+}
+
+func TestProbeServesWithVetsArgv(t *testing.T) {
+ sock := filepath.Join(t.TempDir(), "app.sock")
+ launch(t, nil, "-socket", sock) // internal/vet/vet.go:135
+ if body := getOverUnix(t, sock, "/"); !strings.HasPrefix(body, "ok ") {
+ t.Fatalf("body %q", body)
+ }
+}
+
+func TestProbeIsSilentWithoutTrace(t *testing.T) {
+ sock := filepath.Join(t.TempDir(), "app.sock")
+ cmd, stdout := launch(t, []string{"ZDDPROBE_TRACE="}, "--socket", sock)
+ getOverUnix(t, sock, "/")
+ if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
+ t.Fatal(err)
+ }
+ rest, _ := io.ReadAll(stdout)
+ if len(rest) != 0 {
+ t.Fatalf("stdout %q without ZDDPROBE_TRACE", rest)
+ }
+ if err := cmd.Wait(); err != nil {
+ t.Fatalf("exit after SIGTERM: %v", err)
+ }
+}
+
+func TestProbeRequiresSocket(t *testing.T) {
+ cmd, _ := launch(t, nil, "--db", "x.db")
+ if err := cmd.Wait(); err == nil {
+ t.Fatal("probe without --socket exited 0")
+ }
+}
diff --git a/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md
index 41b5d6f2e..6d142cff1 100644
--- a/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md
+++ b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md
@@ -838,59 +838,22 @@ Spec section 12: zero failed requests under continuous load, and no request
held longer than a normal response. Run it at Gate E (dev) and Gate F
(`adopt-probe`).
-### 1. Build a probe app that drains on SIGTERM, in scratch
+### 1. Build the probe app, in scratch
+
+The probe is `cmd/zddprobe` in this repository. It serves on the unix socket,
+sleeps 50 ms per request so some are always in flight, answers `ok <version>`,
+and drains on SIGTERM. It takes the platform's own argv, `--socket <sock> --db
+<db>` (it ignores `--db`), and vet's `-socket <sock>`. The first dev check
+used a probe that parsed only `-socket`, so it exited at once when the
+platform launched it with `--db`.
+
+Build it from a platform checkout at the release commit. The binaries go to
+scratch:
```bash
-mkdir -p "$TMPDIR/zdd-probe" && cd "$TMPDIR/zdd-probe"
-cat > main.go <<'EOF'
-package main
-
-import (
- "context"
- "flag"
- "fmt"
- "net"
- "net/http"
- "os"
- "os/signal"
- "syscall"
- "time"
-)
-
-var version = "dev"
-
-func main() {
- sock := flag.String("socket", "", "unix socket to serve on")
- flag.Parse()
- _ = os.Remove(*sock)
- l, err := net.Listen("unix", *sock)
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
- mux := http.NewServeMux()
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- time.Sleep(50 * time.Millisecond) // keep requests in flight across the switch
- fmt.Fprintf(w, "ok %s\n", version)
- })
- srv := &http.Server{Handler: mux}
- go func() {
- ch := make(chan os.Signal, 1)
- signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
- <-ch
- ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
- defer cancel()
- srv.Shutdown(ctx)
- }()
- if err := srv.Serve(l); err != nil && err != http.ErrServerClosed {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
-}
-EOF
-go mod init zddprobe
+mkdir -p "$TMPDIR/zdd-probe"
for v in zdd-1 zdd-2 zdd-3 zdd-4; do
- CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$v" -o "probe-$v" .
+ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$v" -o "$TMPDIR/zdd-probe/probe-$v" ./cmd/zddprobe
done
```