Skip to content
Merged
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
10 changes: 7 additions & 3 deletions packages/stack/src/runtime/Container.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1550,7 +1550,7 @@ describe("container process adapter", { timeout: 120_000 }, () => {
);

it.effect(
"names the stalled engine subcommand and its stderr when an engine command times out",
"names the stalled engine subcommand, its stderr, and the container state when start times out",
() =>
Effect.gen(function* () {
const engine = yield* makeStalledStartSpawner("Error: waiting for network sandbox");
Expand All @@ -1572,7 +1572,7 @@ describe("container process adapter", { timeout: 120_000 }, () => {
expect(failure instanceof ContainerLaunchError).toBe(true);
expect(failureKind(failure)).toBe("engine-timeout");
expect(failure instanceof ContainerLaunchError ? failure.failure.message : undefined).toBe(
"Engine command 'start' did not finish within 30s; stderr: Error: waiting for network sandbox",
"Engine command 'start' did not finish within 30s; stderr: Error: waiting for network sandbox; container state: status=created started=0001-01-01T00:00:00Z pid=0 error=",
);
}).pipe(Effect.provide([NodeServices.layer, NodeCrypto.layer])),
);
Expand Down Expand Up @@ -2236,14 +2236,18 @@ const successfulHandle = (stdout = "") =>
unref: Effect.succeed(Effect.void),
});

/** Answers every engine command except `start`, which writes `stderr` and never exits. */
/** Answers every engine command except `start`, which writes `stderr` and never exits, leaving the container created. */
const makeStalledStartSpawner = Effect.fn("ContainerTest.stalledStartSpawner")(function* (
stderr: string,
) {
const stderrRead = yield* Deferred.make<void>();
const spawner = ChildProcessSpawner.make((command) => {
if (!ChildProcess.isStandardCommand(command)) return Effect.die("unexpected piped command");
if (command.args[0] === "info") return Effect.succeed(successfulHandle("fake-daemon-id"));
if (command.args[0] === "inspect")
return Effect.succeed(
successfulHandle("status=created started=0001-01-01T00:00:00Z pid=0 error="),
);
if (command.args[0] !== "start") return Effect.succeed(successfulHandle());
// The second pull only happens once the reader has consumed the stderr line.
const stalledStderr = Stream.succeed(new TextEncoder().encode(stderr)).pipe(
Expand Down
30 changes: 29 additions & 1 deletion packages/stack/src/runtime/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,35 @@ export const makeContainerRuntime = (options: {
)
.pipe(Effect.mapError((cause) => errorFor("start", cause)))
: undefined;
if (!interactive) yield* run(["start", name]);
if (!interactive)
yield* run(["start", name]).pipe(
// Whether the engine stalled before or after starting the process decides the fix.
Effect.catchTag("ContainerError", (failure) =>
failure.kind !== "engine-timeout"
? Effect.fail(failure)
: run(
[
"inspect",
"--format",
"status={{.State.Status}} started={{.State.StartedAt}} pid={{.State.Pid}} error={{.State.Error}}",
name,
],
{ timeout: "5 seconds" },
).pipe(
Effect.orElseSucceed(() => "unavailable"),
Comment thread
jgoux marked this conversation as resolved.
Resolved
Effect.flatMap((state) =>
Effect.fail(
new ContainerError({
operation: failure.operation,
message: `${failure.message}; container state: ${state}`,
cause: failure.cause,
kind: failure.kind,
}),
),
),
),
),
);
const wait: Effect.Effect<number, ContainerError> =
attached === undefined
? run(["wait", name], {}).pipe(
Expand Down
Loading