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 @@ -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;
Expand All @@ -422,6 +422,7 @@ describe.runIf(testEngine === "docker")(
const root = yield* fs.makeTempDirectoryScoped({ prefix: "docker-helper-timeout-" });
const waiting = yield* Deferred.make<void>();
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) =>
Expand All @@ -431,22 +432,36 @@ describe.runIf(testEngine === "docker")(
commands.push([...command.args]);
const creating = command.args[0] === "create";
const attaching = command.args[0] === "start";
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),
)
: Stream.empty,
: 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,
Expand Down Expand Up @@ -482,7 +497,12 @@ 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",
"helper stderr line",
].join("\n"),
Comment thread
avallete marked this conversation as resolved.
);
const create = commands.find((args) => args[0] === "create");
const name = create?.[create.indexOf("--name") + 1];
Expand Down
43 changes: 40 additions & 3 deletions packages/stack/src/storage/DockerDatabaseStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,10 @@ export const makeDockerDatabaseStorage = Effect.fn("DockerDatabaseStorage.make")
);

const engineCommand = Effect.fn("DockerDatabaseStorage.engineCommand")(
(args: ReadonlyArray<string>): Effect.Effect<string, DockerDatabaseStorageError> =>
(
args: ReadonlyArray<string>,
output: "stdout" | "all" = "stdout",
): Effect.Effect<string, DockerDatabaseStorageError> =>
Effect.scoped(
Effect.gen(function* () {
yield* Effect.annotateCurrentSpan({
Expand Down Expand Up @@ -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))),
);
Expand Down Expand Up @@ -638,6 +643,33 @@ 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,
]),
// `logs` relays the helper's stderr on the engine's own stderr.
engineCommand(["logs", "--tail", "20", name], "all"),
].map((command) =>
command.pipe(
Effect.timeoutOrElse({
duration: "5 seconds",
orElse: () => Effect.succeed("unavailable: timed out after 5 seconds"),
}),
Effect.catch((cause) => Effect.succeed(`unavailable: ${cause.message}`)),
),
),
{ 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,
Expand All @@ -650,6 +682,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);
Expand All @@ -659,7 +696,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)
Expand Down
Loading