Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ describe("legacy functions serve integration", () => {
toDockerPath(tempRoot.current),
]);
expect(dockerRun.args[dockerRun.args.length - 1]).toBe(
"edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n",
"exec edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n",
);

const envs = yield* Effect.promise(() => extractDockerEnvEntries(dockerRun));
Expand Down Expand Up @@ -1728,7 +1728,7 @@ describe("legacy functions serve integration", () => {

const commandScript = dockerRun.args[dockerRun.args.length - 1] ?? "";
expect(commandScript).toBe(
"edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n",
"exec edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n",
);
expect(
extractFlagValues(dockerRun.args, "-v").some((value) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,13 @@ export function legacyBuildKongEmailTemplateBind(
return `${hostPath}:${dockerPath}:rw`;
}

// `exec` so Kong (not `sh`) is PID 1 and `docker stop`'s SIGTERM reaches it directly — without
// it every stop burns the full 10s grace period before SIGKILL. Same deliberate divergence from
// Go's script as the Postgres entrypoint (`db-bootstrap/postgres.service.ts`); stop timing is
// outside the Go-parity surface (ADR 0016).
const LEGACY_KONG_ENTRYPOINT_HEAD =
"cat <<'EOF' > /home/kong/custom_nginx.template && \\\n" +
"./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n";
"exec ./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n";
Comment on lines 180 to +181

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Record the PID-1 changes in the Go divergence ledger

This intentionally changes legacy Docker behavior relative to the Go reference for Kong, Vector, Logflare, Edge Runtime, and functions serve, but docs/go-cli-divergences.md contains no entry for the new PID-1/signal-handling behavior. Add one behavioral-divergence entry covering these wrappers so support and future parity work do not mistake the changed shutdown semantics for accidental drift.

AGENTS.md reference: apps/cli/AGENTS.md:L540-L544

Useful? React with 👍 / 👎.


/**
* Builds the surviving (non-secret) half of the Kong entrypoint: only the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe("legacyBuildKongEntrypointScript", () => {
const script = legacyBuildKongEntrypointScript("NGINX_TEMPLATE");
expect(script).toBe(
"cat <<'EOF' > /home/kong/custom_nginx.template && \\\n" +
"./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n" +
"exec ./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n" +
"NGINX_TEMPLATE\nEOF\n",
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,34 @@ const LEGACY_LOGFLARE_API_KEY = "api-key";
* running Logflare against an unmigrated database lets Oban die on the
* missing `public.oban_jobs` table instead.
*/
// A trap-and-escalate supervisor rather than a bare `exec`: measured with beam.smp as PID 1
// (a plain `exec` chain), `docker stop -t 10` STILL burned the full grace period — Logflare's
// own SIGTERM shutdown hangs upstream, the same class of problem postgres-meta had before
// postgres-meta#1103. So `run.sh`'s shell stays PID 1 deliberately, with a real handler:
// forward SIGTERM to the BEAM, give its graceful shutdown a 3s window, then SIGKILL it. Today's
// post-timeout outcome is already SIGKILL, so escalating early produces the identical end state
// ~7s sooner (Logflare's durable state lives in Postgres/BigQuery, both crash-safe) — and if
// upstream ever fixes its handler, the graceful window wins first and the KILL never fires.
// The `migrate`-then-`start` sequencing (a failed migrate exits the container so the
// `unless-stopped` policy retries) is unchanged and deliberate — see the doc comment above
// (#6088). Stop timing is outside the Go-parity surface (ADR 0016).
// `run.sh` line by line: migrate (exit on failure — the container-restart retry above), start
// the BEAM in the background, install the TERM handler, then `wait`. A trapped signal interrupts
// `wait` with a >128 status, so the follow-up `wait` collects the BEAM's real exit status once
// the trap's TERM-then-KILL escalation finishes; the `127` guard keeps the first status when the
// BEAM was already reaped (a second `wait` on a reaped pid is an error). A clean BEAM exit takes
// the single-`wait` path untouched.
const LEGACY_LOGFLARE_ENTRYPOINT_SCRIPT =
"cat <<'EOF' > run.sh && sh run.sh\n./logflare eval Logflare.Release.migrate &&\n./logflare start --sname logflare\nEOF\n";
"cat <<'EOF' > run.sh && exec sh run.sh\n" +
"./logflare eval Logflare.Release.migrate || exit $?\n" +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install the Logflare TERM trap before migration

When supabase stop or startup rollback runs while Logflare.Release.migrate is still executing, run.sh is already PID 1 but the TERM trap has not yet been installed. Docker signals only that main shell process; it does not signal the foreground migration child, and PID 1's unhandled terminating signal is ignored, so the stop still reaches Docker's grace timeout and ends in SIGKILL. Install the handler before starting the migration (and make it terminate the migration phase as well) so shutdown is bounded throughout container startup, not only after the BEAM has launched.

Useful? React with 👍 / 👎.

"./logflare start --sname logflare &\n" +
"BEAM_PID=$!\n" +
'trap \'kill -TERM "$BEAM_PID" 2>/dev/null; n=0; while [ "$n" -lt 3 ] && kill -0 "$BEAM_PID" 2>/dev/null; do n=$((n+1)); sleep 1; done; kill -KILL "$BEAM_PID" 2>/dev/null\' TERM\n' +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cover the new shutdown supervisor with a live test

When analytics is enabled, this supervisor's correctness depends on real PID-1 signal delivery and the Logflare image's shutdown behavior, but the added unit test only compares the generated shell string. I checked the existing start.live.test.ts and stop.live.test.ts: neither starts analytics/Logflare and measures its shutdown, so CI cannot detect this trap failing and restoring Docker's 10-second timeout. Add or extend a real-Docker live scenario that enables analytics and verifies the bounded stop behavior.

AGENTS.md reference: apps/cli/AGENTS.md:L520-L523

Useful? React with 👍 / 👎.

'wait "$BEAM_PID"\n' +
"code=$?\n" +
'if [ "$code" -gt 128 ]; then wait "$BEAM_PID" 2>/dev/null; code2=$?; [ "$code2" -ne 127 ] && code=$code2; fi\n' +
'exit "$code"\n' +
"EOF\n";

export interface LegacyLogflareContainerSpecInput {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ describe("legacyBuildLogflareContainerSpec", () => {
expect(spec.entrypoint).toBe("sh");
expect(spec.cmd).toEqual([
"-c",
"cat <<'EOF' > run.sh && sh run.sh\n./logflare eval Logflare.Release.migrate &&\n./logflare start --sname logflare\nEOF\n",
"cat <<'EOF' > run.sh && exec sh run.sh\n" +
"./logflare eval Logflare.Release.migrate || exit $?\n" +
"./logflare start --sname logflare &\n" +
"BEAM_PID=$!\n" +
'trap \'kill -TERM "$BEAM_PID" 2>/dev/null; n=0; while [ "$n" -lt 3 ] && kill -0 "$BEAM_PID" 2>/dev/null; do n=$((n+1)); sleep 1; done; kill -KILL "$BEAM_PID" 2>/dev/null\' TERM\n' +
'wait "$BEAM_PID"\n' +
"code=$?\n" +
'if [ "$code" -gt 128 ]; then wait "$BEAM_PID" 2>/dev/null; code2=$?; [ "$code2" -ne 127 ] && code=$code2; fi\n' +
'exit "$code"\n' +
"EOF\n",
]);
expect(spec.exposedPorts).toEqual([{ containerPort: "4000" }]);
expect(spec.ports).toEqual([{ hostPort: "54327", containerPort: "4000" }]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,11 @@ export function legacyBuildVectorEntrypointScript(vectorYaml: string, logflareId
vectorYaml +
"\nEOF\nuntil wget --no-verbose --tries=1 --spider http://" +
logflareId +
":4000/health 2>/dev/null; do sleep 2; done\nvector --config /etc/vector/vector.yaml\n"
// `exec` so Vector (not `sh`) is PID 1 and `docker stop`'s SIGTERM reaches it directly —
// without it every stop burns the full 10s grace period. The wget wait-loop above still runs
// in the wrapper shell BEFORE the exec, unchanged. Stop timing is outside the Go-parity
// surface (ADR 0016).
":4000/health 2>/dev/null; do sleep 2; done\nexec vector --config /etc/vector/vector.yaml\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle SIGTERM while Vector is polling

When Vector restarts while Logflare is unhealthy, this loop can run indefinitely and execution never reaches the new exec. The shell therefore remains PID 1 and does not forward Docker's stop signal to the active wget/sleep; Docker then waits for its grace timeout before killing the container, so the latency regression this change addresses remains in this failure state. Install a TERM handler around the polling phase (then reset it before exec) and exercise the unhealthy-Logflare path in the required real-Docker live test. Docker documents that stop signals the container's main process and force-kills it after the timeout.

AGENTS.md reference: apps/cli/AGENTS.md:L520-L523

Useful? React with 👍 / 👎.

);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ describe("legacyBuildVectorEntrypointScript", () => {
"VECTOR_YAML" +
"\nEOF\nuntil wget --no-verbose --tries=1 --spider http://" +
"supabase_analytics_proj" +
":4000/health 2>/dev/null; do sleep 2; done\nvector --config /etc/vector/vector.yaml\n",
":4000/health 2>/dev/null; do sleep 2; done\nexec vector --config /etc/vector/vector.yaml\n",
);
});
});
Expand Down
9 changes: 5 additions & 4 deletions apps/cli/src/legacy/shared/db-bootstrap/postgres.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,11 @@ function legacyPostgresExtraEnv(
* The final command is `exec`'d — a deliberate divergence from Go's script
* (which leaves `sh` as PID 1, so SIGTERM is never forwarded and every
* `docker stop` burns the full 10s grace period before SIGKILL; with `exec`,
* Postgres is PID 1 and stops in ~1s). Applies to all three entrypoint
* variants below. Timing is not part of the Go-parity surface (ADR 0016);
* see `shadow-cache.ts`'s own doc comment for why fast shutdown matters to the shadow baseline
* cache's cold path.
* Postgres is PID 1 and stops in ~4s on a full local `db`, ~1s on an
* empty shadow). Applies to all three entrypoint variants below. Timing is
* not part of the Go-parity surface (ADR 0016); see `shadow-cache.ts`'s own
* doc comment for why fast shutdown matters to the shadow baseline cache's
* cold path.
*
* Otherwise byte-for-byte derived from Go's raw-string concatenation —
* `NewContainerConfig(args ...string)` splices `strings.Join(args, " ")`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,18 @@ export function legacyBuildEdgeRuntimeEntrypoint(
files: ReadonlyArray<LegacyEdgeRuntimeFile>,
cmd: string,
): string {
if (files.length === 0) return `${cmd}\n`;
// `exec` diverges from Go's byte-for-byte script on purpose: edge-runtime (not `sh`) becomes
// PID 1, so an early `docker stop`/`rm -f` SIGTERM reaches it directly instead of burning the
// 10s grace period. These containers are `--rm` and normally exit on their own, so this is a
// cancellation-latency nicety, not a stop-path requirement. Timing is outside the Go-parity
// surface (ADR 0016).
if (files.length === 0) return `exec ${cmd}\n`;
let head = "";
let bodies = "";
files.forEach((file, index) => {
const sentinel = `__EDGE_RT_FILE_${index}__`;
head += `cat <<'${sentinel}' > ${file.name} && `;
bodies += `${file.content}\n${sentinel}\n`;
});
return `${head}${cmd}\n${bodies}`;
return `${head}exec ${cmd}\n${bodies}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,21 @@ describe("legacyBuildEdgeRuntimeStartCmd", () => {

describe("legacyBuildEdgeRuntimeEntrypoint", () => {
it("returns just the command (newline-terminated) when there are no files", () => {
expect(legacyBuildEdgeRuntimeEntrypoint([], "edge-runtime start")).toBe("edge-runtime start\n");
expect(legacyBuildEdgeRuntimeEntrypoint([], "edge-runtime start")).toBe(
"exec edge-runtime start\n",
);
});

it("writes a single file via a sentinel here-document then runs the command", () => {
const out = legacyBuildEdgeRuntimeEntrypoint(
[{ name: "index.ts", content: "console.log(1);" }],
"edge-runtime start --main-service=. --port=5",
);
// Byte-for-byte port of Go's buildEdgeRuntimeEntrypoint: openers (joined with
// ` && `) precede the command, then the bodies with their sentinels.
// Port of Go's buildEdgeRuntimeEntrypoint — openers (joined with ` && `) precede the
// command, then the bodies with their sentinels — plus a deliberate `exec` divergence so
// edge-runtime is PID 1 (ADR 0016: timing is outside the parity surface).
expect(out).toBe(
"cat <<'__EDGE_RT_FILE_0__' > index.ts && edge-runtime start --main-service=. --port=5\n" +
"cat <<'__EDGE_RT_FILE_0__' > index.ts && exec edge-runtime start --main-service=. --port=5\n" +
"console.log(1);\n__EDGE_RT_FILE_0__\n",
);
});
Expand All @@ -62,13 +65,15 @@ describe("legacyBuildEdgeRuntimeEntrypoint", () => {
"CMD",
);
expect(out).toBe(
"cat <<'__EDGE_RT_FILE_0__' > index.ts && cat <<'__EDGE_RT_FILE_1__' > .npmrc && CMD\n" +
"cat <<'__EDGE_RT_FILE_0__' > index.ts && cat <<'__EDGE_RT_FILE_1__' > .npmrc && exec CMD\n" +
"A\n__EDGE_RT_FILE_0__\nB\n__EDGE_RT_FILE_1__\n",
);
});

it("preserves file contents that themselves contain EOF-like text", () => {
const out = legacyBuildEdgeRuntimeEntrypoint([{ name: "index.ts", content: "EOF\nmore" }], "C");
expect(out).toBe("cat <<'__EDGE_RT_FILE_0__' > index.ts && C\nEOF\nmore\n__EDGE_RT_FILE_0__\n");
expect(out).toBe(
"cat <<'__EDGE_RT_FILE_0__' > index.ts && exec C\nEOF\nmore\n__EDGE_RT_FILE_0__\n",
);
});
});
7 changes: 6 additions & 1 deletion apps/cli/src/shared/functions/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1441,7 +1441,12 @@ export function buildServeEntrypointCommand(
command: ReadonlyArray<string>,
multilineEnvScriptPath?: string,
) {
return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}${command.join(" ")}
// `exec` so edge-runtime (not `sh`) is PID 1 and `docker stop`'s SIGTERM reaches it directly —
// without it every stop burns the full 10s grace period before SIGKILL. The optional
// multiline-env `.` sourcing above still runs in the wrapper shell before the exec, and its
// exported env survives into the exec'd process. Stop timing is outside the Go-parity surface
// (ADR 0016).
return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}exec ${command.join(" ")}
`;
}

Expand Down
5 changes: 3 additions & 2 deletions apps/cli/src/shared/functions/serve.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { buildServeEntrypointCommand } from "./serve.ts";
describe("buildServeEntrypointCommand", () => {
it("returns the runtime command without embedding the template body", () => {
const script = buildServeEntrypointCommand(["edge-runtime", "start"]);
expect(script).toBe("edge-runtime start\n");
// `exec` so edge-runtime is PID 1 — docker stop must not burn the 10s SIGTERM grace period.
expect(script).toBe("exec edge-runtime start\n");
expect(script).not.toContain("Deno.serve");
});

it("sources the multiline env script before the runtime command when provided", () => {
const script = buildServeEntrypointCommand(["edge-runtime", "start"], "/root/env.sh");
expect(script).toContain(". /root/env.sh\nedge-runtime start");
expect(script).toContain(". /root/env.sh\nexec edge-runtime start");
});

it("keeps the spawned command short even with the real bundled template", async () => {
Expand Down
12 changes: 6 additions & 6 deletions packages/stack/src/ServiceCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export const SERVICE_CATALOG = {
postgrest: {
name: "postgrest",
configKey: "postgrest",
defaultVersion: "14.16",
defaultVersion: "16.1",
runtimeSupport: "native-preferred",
artifact: {
docker: { ownership: "supabase", repository: "postgrest", tagPrefix: "v" },
Expand Down Expand Up @@ -201,7 +201,7 @@ export const SERVICE_CATALOG = {
realtime: {
name: "realtime",
configKey: "realtime",
defaultVersion: "2.123.1",
defaultVersion: "2.124.4",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sync all stack defaults to the Dockerfile manifest

When a consumer relies on @supabase/stack defaults, this partial update still selects Realtime 2.124.4 and Storage 1.69.0 even though apps/cli-go/pkg/config/templates/Dockerfile:14-15 pins 2.128.0 and 1.69.9. The package's sync:versions --check logic derives every catalog entry from that manifest, so the catalog remains out of sync and programmatic stacks run a different service set than supabase start; regenerate both changed entries from the current Dockerfile.

Useful? React with 👍 / 👎.

runtimeSupport: "docker-only",
artifact: { docker: { ownership: "supabase", repository: "realtime", tagPrefix: "v" } },
activation: { startup: "eager", activates: [], owns: [] },
Expand All @@ -210,7 +210,7 @@ export const SERVICE_CATALOG = {
storage: {
name: "storage",
configKey: "storage",
defaultVersion: "1.68.1",
defaultVersion: "1.69.0",
runtimeSupport: "docker-only",
artifact: { docker: { ownership: "supabase", repository: "storage-api", tagPrefix: "v" } },
activation: { startup: "lazy", activates: ["imgproxy"], owns: ["imgproxy"] },
Expand All @@ -237,7 +237,7 @@ export const SERVICE_CATALOG = {
pgmeta: {
name: "pgmeta",
configKey: "pgmeta",
defaultVersion: "0.96.6",
defaultVersion: "0.98.0",
runtimeSupport: "docker-only",
artifact: {
docker: { ownership: "supabase", repository: "postgres-meta", tagPrefix: "v" },
Expand All @@ -248,7 +248,7 @@ export const SERVICE_CATALOG = {
studio: {
name: "studio",
configKey: "studio",
defaultVersion: "2026.08.03-sha-022b374",
defaultVersion: "2026.08.10-sha-5b68af1",
runtimeSupport: "docker-only",
artifact: { docker: { ownership: "supabase", repository: "studio" } },
activation: { startup: "eager", activates: ["analytics"], owns: [] },
Expand All @@ -257,7 +257,7 @@ export const SERVICE_CATALOG = {
analytics: {
name: "analytics",
configKey: "analytics",
defaultVersion: "1.49.2",
defaultVersion: "1.50.2",
runtimeSupport: "docker-only",
artifact: { docker: { ownership: "supabase", repository: "logflare" } },
activation: { startup: "lazy", activates: ["vector"], owns: ["vector"] },
Expand Down
Loading