From 8ff8ee5225fd6fa049d609ca6509d47aa2f4cf5f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 07:42:07 +0000 Subject: [PATCH 1/2] fix(stack): report the helper container state when its readiness times out A database helper that never prints its ready line now fails with the container's status, exit code, engine error and log tail, read before the attach is killed so `--rm` has not removed it yet. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EMySvvbM4B1zdmhw3pp1wA --- .../DockerDatabaseStorage.integration.test.ts | 17 +++++++++-- .../src/storage/DockerDatabaseStorage.ts | 30 ++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/stack/src/storage/DockerDatabaseStorage.integration.test.ts b/packages/stack/src/storage/DockerDatabaseStorage.integration.test.ts index 436f181c09..1e05e2b7b6 100644 --- a/packages/stack/src/storage/DockerDatabaseStorage.integration.test.ts +++ b/packages/stack/src/storage/DockerDatabaseStorage.integration.test.ts @@ -413,7 +413,7 @@ describe.runIf(testEngine === "docker")( ).pipe(Effect.provide(NodeServices.layer)), ); - it.effect("reports a helper readiness timeout alongside Docker warnings", () => + it.effect("reports a helper readiness timeout with Docker warnings and the helper state", () => Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -431,6 +431,11 @@ describe.runIf(testEngine === "docker")( commands.push([...command.args]); const creating = command.args[0] === "create"; const attaching = command.args[0] === "start"; + const engineOutput: Record = { + inspect: "running (exit 0)\n", + logs: "supabase-helper-ready\n", + }; + const output = engineOutput[command.args[0] ?? ""]; return ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(0), exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)), @@ -442,7 +447,9 @@ describe.runIf(testEngine === "docker")( Stream.drain, Stream.concat(Stream.never), ) - : Stream.empty, + : output === undefined + ? Stream.empty + : Stream.succeed(new TextEncoder().encode(output)), // Docker prints the platform warning while creating the container, before start. stderr: creating ? Stream.succeed(new TextEncoder().encode(`${warning}\n`)) @@ -482,7 +489,11 @@ describe.runIf(testEngine === "docker")( yield* TestClock.adjust("30 seconds"); const failure = yield* Fiber.join(operation); expect(failure.message).toBe( - `Database helper did not become ready within 30 seconds: ${warning}`, + [ + `Database helper did not become ready within 30 seconds: ${warning}`, + "helper state: running (exit 0)", + "helper logs: supabase-helper-ready", + ].join("\n"), ); const create = commands.find((args) => args[0] === "create"); const name = create?.[create.indexOf("--name") + 1]; diff --git a/packages/stack/src/storage/DockerDatabaseStorage.ts b/packages/stack/src/storage/DockerDatabaseStorage.ts index c5a6c145fe..ce99c1cb6a 100644 --- a/packages/stack/src/storage/DockerDatabaseStorage.ts +++ b/packages/stack/src/storage/DockerDatabaseStorage.ts @@ -638,6 +638,29 @@ export const makeDockerDatabaseStorage = Effect.fn("DockerDatabaseStorage.make") ) .pipe(Effect.mapError((cause) => errorFor("helper", cause))); const stderr = yield* stderrTail(child); + const stalledHelperState = Effect.fn("DockerDatabaseStorage.stalledHelperState")( + function* () { + const [state, logs] = yield* Effect.all( + [ + engineCommand([ + "inspect", + "--format", + "{{.State.Status}} (exit {{.State.ExitCode}}){{if .State.Error}}: {{.State.Error}}{{end}}", + name, + ]), + engineCommand(["logs", "--tail", "20", name]), + ].map((command) => + command.pipe( + Effect.timeout("5 seconds"), + Effect.orElseSucceed(() => "unavailable"), + ), + ), + { concurrency: "unbounded" }, + ); + yield* Effect.annotateCurrentSpan("helper.state", state); + return `helper state: ${state}\nhelper logs: ${logs || "(empty)"}`; + }, + ); return yield* child.stdout.pipe( Stream.decodeText, Stream.splitLines, @@ -650,6 +673,11 @@ export const makeDockerDatabaseStorage = Effect.fn("DockerDatabaseStorage.make") exit.value.value === "supabase-helper-ready" ? Effect.succeed(name) : Effect.gen(function* () { + const timedOut = + Exit.isFailure(exit) && + Option.exists(Cause.findErrorOption(exit.cause), Cause.isTimeoutError); + // Inspect before the kill: `--rm` removes the helper once its attach ends. + const state = timedOut ? yield* stalledHelperState() : ""; yield* child .kill({ killSignal: "SIGTERM", forceKillAfter: "5 seconds" }) .pipe(Effect.ignore); @@ -659,7 +687,7 @@ export const makeDockerDatabaseStorage = Effect.fn("DockerDatabaseStorage.make") ); // Docker prints warnings such as a platform mismatch on `create`, so keep that // output alongside what `start` wrote. - const tail = [createTail, diagnostic.trim()] + const tail = [createTail, diagnostic.trim(), state] .filter((part) => part !== "") .join("\n"); const reason = Exit.isFailure(exit) From c5eaae5717602187b4cfa936ae396d5af0e11640 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 07:50:09 +0000 Subject: [PATCH 2/2] fix(stack): keep helper log stderr and diagnostic failure reasons Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EMySvvbM4B1zdmhw3pp1wA --- .../DockerDatabaseStorage.integration.test.ts | 33 ++++++++++++------- .../src/storage/DockerDatabaseStorage.ts | 19 ++++++++--- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/stack/src/storage/DockerDatabaseStorage.integration.test.ts b/packages/stack/src/storage/DockerDatabaseStorage.integration.test.ts index 1e05e2b7b6..7416259ac0 100644 --- a/packages/stack/src/storage/DockerDatabaseStorage.integration.test.ts +++ b/packages/stack/src/storage/DockerDatabaseStorage.integration.test.ts @@ -422,6 +422,7 @@ describe.runIf(testEngine === "docker")( const root = yield* fs.makeTempDirectoryScoped({ prefix: "docker-helper-timeout-" }); const waiting = yield* Deferred.make(); const commands: string[][] = []; + let attachKilled = false; const warning = "WARNING: The requested image's platform does not match the host platform"; const spawner = ChildProcessSpawner.make((command) => @@ -431,29 +432,36 @@ describe.runIf(testEngine === "docker")( commands.push([...command.args]); const creating = command.args[0] === "create"; const attaching = command.args[0] === "start"; - const engineOutput: Record = { - inspect: "running (exit 0)\n", - logs: "supabase-helper-ready\n", - }; - const output = engineOutput[command.args[0] ?? ""]; + const encode = (text: string) => Stream.succeed(new TextEncoder().encode(text)); + // `--rm` removes the helper once its attach is killed, so later inspection fails. + const removed = command.args[0] === "inspect" && attachKilled; return ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(0), - exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)), + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(removed ? 1 : 0)), isRunning: Effect.succeed(false), - kill: () => Effect.void, + kill: () => + Effect.sync(() => { + if (attaching) attachKilled = true; + }), stdin: Sink.drain, stdout: attaching ? Stream.fromEffect(Deferred.succeed(waiting, undefined)).pipe( Stream.drain, Stream.concat(Stream.never), ) - : output === undefined - ? Stream.empty - : Stream.succeed(new TextEncoder().encode(output)), + : command.args[0] === "inspect" && !removed + ? encode("running (exit 0)\n") + : command.args[0] === "logs" + ? encode("supabase-helper-ready\n") + : Stream.empty, // Docker prints the platform warning while creating the container, before start. stderr: creating - ? Stream.succeed(new TextEncoder().encode(`${warning}\n`)) - : Stream.empty, + ? encode(`${warning}\n`) + : removed + ? encode("Error: No such container\n") + : command.args[0] === "logs" + ? encode("helper stderr line\n") + : Stream.empty, all: Stream.empty, getInputFd: () => Sink.drain, getOutputFd: () => Stream.empty, @@ -493,6 +501,7 @@ describe.runIf(testEngine === "docker")( `Database helper did not become ready within 30 seconds: ${warning}`, "helper state: running (exit 0)", "helper logs: supabase-helper-ready", + "helper stderr line", ].join("\n"), ); const create = commands.find((args) => args[0] === "create"); diff --git a/packages/stack/src/storage/DockerDatabaseStorage.ts b/packages/stack/src/storage/DockerDatabaseStorage.ts index ce99c1cb6a..1191857a3c 100644 --- a/packages/stack/src/storage/DockerDatabaseStorage.ts +++ b/packages/stack/src/storage/DockerDatabaseStorage.ts @@ -375,7 +375,10 @@ export const makeDockerDatabaseStorage = Effect.fn("DockerDatabaseStorage.make") ); const engineCommand = Effect.fn("DockerDatabaseStorage.engineCommand")( - (args: ReadonlyArray): Effect.Effect => + ( + args: ReadonlyArray, + output: "stdout" | "all" = "stdout", + ): Effect.Effect => Effect.scoped( Effect.gen(function* () { yield* Effect.annotateCurrentSpan({ @@ -415,7 +418,9 @@ export const makeDockerDatabaseStorage = Effect.fn("DockerDatabaseStorage.make") "engine", stderr.trim() || `Container engine exited with ${code}`, ); - return stdout.trim(); + return output === "all" + ? [stdout.trim(), stderr.trim()].filter((part) => part !== "").join("\n") + : stdout.trim(); }), ).pipe(Effect.mapError((cause) => errorFor("engine", cause))), ); @@ -648,11 +653,15 @@ export const makeDockerDatabaseStorage = Effect.fn("DockerDatabaseStorage.make") "{{.State.Status}} (exit {{.State.ExitCode}}){{if .State.Error}}: {{.State.Error}}{{end}}", name, ]), - engineCommand(["logs", "--tail", "20", name]), + // `logs` relays the helper's stderr on the engine's own stderr. + engineCommand(["logs", "--tail", "20", name], "all"), ].map((command) => command.pipe( - Effect.timeout("5 seconds"), - Effect.orElseSucceed(() => "unavailable"), + Effect.timeoutOrElse({ + duration: "5 seconds", + orElse: () => Effect.succeed("unavailable: timed out after 5 seconds"), + }), + Effect.catch((cause) => Effect.succeed(`unavailable: ${cause.message}`)), ), ), { concurrency: "unbounded" },