| 1 | // Command zddprobe is the probe app for the zero-downtime deploy roll notes |
| 2 | // (docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md). |
| 3 | // It serves HTTP on a unix socket. Every request sleeps 50 ms, so requests are |
| 4 | // in flight across a swap, then answers "ok <version>". SIGTERM or SIGINT |
| 5 | // drains in-flight requests, waiting at most 15 s, and exits 0. |
| 6 | // |
| 7 | // The platform starts an exec-backed instance as |
| 8 | // `<binary> --socket <sock> --db <db>`, and `carlos deploy`'s vet starts it as |
| 9 | // `<binary> -socket <sock>`. The probe takes both. It keeps no data, so --db |
| 10 | // is accepted and ignored. |
| 11 | // |
| 12 | // On a platform launch, the one that passes --db, the probe checks before |
| 13 | // it listens what a real app may rely on (zero-downtime spec rev 19, rule |
| 14 | // 5), and exits 1 with a line on stderr if either is wrong: CARLOS_HOST |
| 15 | // must be set, and the file name of --socket must be the flag-off one, |
| 16 | // app.sock (nested) or $CARLOS_HOST.sock (flat). A generation launch that |
| 17 | // named its socket anything else crash-looped a tenant whose framework |
| 18 | // reads its host from that name (amadan #272); the probe ignored the name |
| 19 | // and passed. Vet's boot passes no --db, no CARLOS_HOST and a socket name |
| 20 | // of its own choosing, so it skips the check and the probe serves. (The |
| 21 | // flag package reads -db and --db alike, so --db being given is the test.) |
| 22 | // |
| 23 | // version is set at build time with -ldflags "-X main.version=...". |
| 24 | // |
| 25 | // With ZDDPROBE_TRACE=1 the probe writes one line, "serving", to stdout as a |
| 26 | // request enters the handler, so a test can tell a request is in flight |
| 27 | // before it signals. Without it the probe writes nothing to stdout. |
| 28 | package main |
| 29 | |
| 30 | import ( |
| 31 | "context" |
| 32 | "errors" |
| 33 | "flag" |
| 34 | "fmt" |
| 35 | "io" |
| 36 | "net" |
| 37 | "net/http" |
| 38 | "os" |
| 39 | "os/signal" |
| 40 | "path/filepath" |
| 41 | "syscall" |
| 42 | "time" |
| 43 | ) |
| 44 | |
| 45 | var version = "dev" |
| 46 | |
| 47 | func main() { |
| 48 | sigs := make(chan os.Signal, 1) |
| 49 | signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT) |
| 50 | if err := run(os.Args[1:], sigs); err != nil { |
| 51 | fmt.Fprintln(os.Stderr, err) |
| 52 | os.Exit(1) |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // checkLaunch is rule 5's check of a platform launch: host (CARLOS_HOST) |
| 57 | // is set, and sock's file name is app.sock or <host>.sock, the names a |
| 58 | // flag-off launch gives. |
| 59 | func checkLaunch(sock, host string) error { |
| 60 | if host == "" { |
| 61 | return errors.New("zddprobe: CARLOS_HOST is empty; the platform sets it on every instance launch") |
| 62 | } |
| 63 | if base := filepath.Base(sock); base != "app.sock" && base != host+".sock" { |
| 64 | return fmt.Errorf("zddprobe: --socket file name is %q; want app.sock or %s.sock, the flag-off names", base, host) |
| 65 | } |
| 66 | return nil |
| 67 | } |
| 68 | |
| 69 | func run(args []string, stop <-chan os.Signal) error { |
| 70 | fs := flag.NewFlagSet("zddprobe", flag.ContinueOnError) |
| 71 | fs.SetOutput(os.Stderr) |
| 72 | sock := fs.String("socket", "", "unix socket to serve on") |
| 73 | fs.String("db", "", "accepted for the platform's argv and ignored") |
| 74 | if err := fs.Parse(args); err != nil { |
| 75 | return err |
| 76 | } |
| 77 | if *sock == "" { |
| 78 | return errors.New("zddprobe: --socket is required") |
| 79 | } |
| 80 | platform := false |
| 81 | fs.Visit(func(f *flag.Flag) { platform = platform || f.Name == "db" }) |
| 82 | if platform { |
| 83 | if err := checkLaunch(*sock, os.Getenv("CARLOS_HOST")); err != nil { |
| 84 | return err |
| 85 | } |
| 86 | } |
| 87 | _ = os.Remove(*sock) |
| 88 | l, err := net.Listen("unix", *sock) |
| 89 | if err != nil { |
| 90 | return err |
| 91 | } |
| 92 | |
| 93 | trace := os.Getenv("ZDDPROBE_TRACE") == "1" |
| 94 | mux := http.NewServeMux() |
| 95 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
| 96 | if trace { |
| 97 | fmt.Fprintln(os.Stdout, "serving") |
| 98 | } |
| 99 | time.Sleep(50 * time.Millisecond) // keep requests in flight across the switch |
| 100 | _, _ = io.WriteString(w, "ok "+version+"\n") |
| 101 | }) |
| 102 | srv := &http.Server{Handler: mux} |
| 103 | |
| 104 | drained := make(chan error, 1) |
| 105 | go func() { |
| 106 | <-stop |
| 107 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) |
| 108 | defer cancel() |
| 109 | drained <- srv.Shutdown(ctx) |
| 110 | }() |
| 111 | if err := srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) { |
| 112 | return err |
| 113 | } |
| 114 | return <-drained |
| 115 | } |
| 116 | |