diff --git a/packages/stack/src/runtime/Container.integration.test.ts b/packages/stack/src/runtime/Container.integration.test.ts index c431ac9791..18d3ba4ba3 100644 --- a/packages/stack/src/runtime/Container.integration.test.ts +++ b/packages/stack/src/runtime/Container.integration.test.ts @@ -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"); @@ -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])), ); @@ -2236,7 +2236,7 @@ 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, ) { @@ -2244,6 +2244,10 @@ const makeStalledStartSpawner = Effect.fn("ContainerTest.stalledStartSpawner")(f 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( diff --git a/packages/stack/src/runtime/Container.ts b/packages/stack/src/runtime/Container.ts index 6e6dbe50b6..df2f25be14 100644 --- a/packages/stack/src/runtime/Container.ts +++ b/packages/stack/src/runtime/Container.ts @@ -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"), + 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 = attached === undefined ? run(["wait", name], {}).pipe(