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 @@ -738,7 +738,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 @@ -1738,7 +1738,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",
);

const cp = deployMockState.runCalls.find(
Expand Down
6 changes: 3 additions & 3 deletions apps/cli/src/legacy/commands/start/services/kong.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
* disk when TLS is enabled AND both `cert_path`/`key_path` are configured —
* see {@link LegacyKongContainerSpecInput.tlsCertContent}'s doc comment.
* {@link legacyBuildKongEntrypointScript} reproduces the remaining
* `custom_nginx.template` heredoc + exec line byte-for-byte; see its doc
* comment for the exact shell mechanics.
* `custom_nginx.template` heredoc; the final command is `exec`'d so Kong
* is PID 1 and `docker stop` reaches it directly.
*
* Kong mints no JWTs of its own: `BearerToken`/`QueryToken` are Kong
* `request-transformer`/lua expression STRINGS built from the four
Expand Down Expand Up @@ -174,7 +174,7 @@ export function legacyBuildKongEmailTemplateBind(

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 176 to +177

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 @@ -50,9 +50,23 @@ const LEGACY_LOGFLARE_API_KEY = "api-key";
* the `unless-stopped` restart policy retries until the db is ready —
* running Logflare against an unmigrated database lets Oban die on the
* missing `public.oban_jobs` table instead.
*
* `run.sh` stays PID 1 on purpose: a plain `exec` of `beam.smp` still burned
* Docker's 10s SIGTERM grace (upstream hang). Forward TERM, wait 3s, then
* KILL. Interrupted `wait` is >128; a second `wait` recovers the BEAM's
* status unless it was already reaped (127).
*/
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
12 changes: 6 additions & 6 deletions apps/cli/src/legacy/commands/start/services/vector.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,18 +279,18 @@ const LEGACY_VECTOR_HEALTHCHECK = {

/**
* Builds the Vector entrypoint script: writes the rendered `vector.yaml` via
* a `cat <<'EOF'` heredoc, then blocks on a `wget`-poll loop against
* Logflare's own `/health` endpoint (Vector's `docker_logs`/HTTP sinks would
* otherwise start before Logflare is ready to receive them) before finally
* exec'ing `vector`.
* a `cat <<'EOF'` heredoc, waits on Logflare `/health` (sinks would otherwise
* start too early), then `exec`s Vector so it is PID 1. A TERM trap covers
* the wait so `docker stop` does not burn 10s if Logflare is still down;
* `-T 2` bounds each probe so a hung health endpoint cannot defer the trap.
*/
export function legacyBuildVectorEntrypointScript(vectorYaml: string, logflareId: string): string {
return (
"cat <<'EOF' > /etc/vector/vector.yaml\n" +
vectorYaml +
"\nEOF\nuntil wget --no-verbose --tries=1 --spider http://" +
"\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 -T 2 --spider http://" +
logflareId +
":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\ntrap - TERM\nexec vector --config /etc/vector/vector.yaml\n"
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,9 @@ describe("legacyBuildVectorEntrypointScript", () => {
expect(legacyBuildVectorEntrypointScript("VECTOR_YAML", "supabase_analytics_proj")).toBe(
"cat <<'EOF' > /etc/vector/vector.yaml\n" +
"VECTOR_YAML" +
"\nEOF\nuntil wget --no-verbose --tries=1 --spider http://" +
"\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 -T 2 --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\ntrap - TERM\nexec vector --config /etc/vector/vector.yaml\n",
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,20 @@ export function legacyBuildEdgeRuntimeStartCmd(opts: {

/**
* Builds the `sh -c` entrypoint body that writes each file via a here-document
* (so contents may contain `EOF`) and then runs `cmd`. Byte-for-byte port of
* `buildEdgeRuntimeEntrypoint` (`apps/cli-go/internal/utils/edgeruntime.go`):
* all heredoc openers are joined with `&&` before the bodies so the shell stacks
* them in declaration order; each body ends with a unique sentinel.
* (so contents may contain `EOF`) and then `exec`s `cmd` so edge-runtime is
* PID 1 and an early `docker stop` reaches it directly.
*/
export function legacyBuildEdgeRuntimeEntrypoint(
files: ReadonlyArray<LegacyEdgeRuntimeFile>,
cmd: string,
): string {
if (files.length === 0) return `${cmd}\n`;
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,18 @@ 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.
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 +62,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",
);
});
});
3 changes: 2 additions & 1 deletion apps/cli/src/shared/functions/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1471,7 +1471,8 @@ export function buildServeEntrypointCommand(
command: ReadonlyArray<string>,
multilineEnvScriptPath?: string,
) {
return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}${command.join(" ")}
// `exec` so edge-runtime is PID 1; sourced env survives into the replacement process.
return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}exec ${command.join(" ")}
`;
}

Expand Down
4 changes: 2 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,13 @@ 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");
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