Skip to content
Closed
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
28 changes: 28 additions & 0 deletions packages/stack/src/runtime/Container.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,23 @@ describe("container process adapter", { timeout: 120_000 }, () => {
}).pipe(Effect.provide([NodeServices.layer, NodeCrypto.layer])),
);

it.effect("continues a launch whose start timed out once the container is running", () =>
Effect.gen(function* () {
const engine = yield* makeStalledStartSpawner("", "running");
const runtime = yield* makeContainerRuntime({
target: containerTarget,
root: ".",
hostGateway: sharedHostGateway,
}).pipe(Effect.provide(engine.layer));
const launch = yield* Effect.scoped(
runtime.launch({ image, stackId: "r".repeat(64), instanceId: "slow-start", env: {} }),
).pipe(Effect.provide(engine.layer), Effect.exit, Effect.forkChild);
yield* Deferred.await(engine.stderrRead);
yield* TestClock.adjust("30 seconds");
expect(Exit.isSuccess(yield* Fiber.join(launch))).toBe(true);
}).pipe(Effect.provide([NodeServices.layer, NodeCrypto.layer])),
);

it.effect("removes the container a hung docker create may still create when interrupted", () =>
Effect.gen(function* () {
const engine = yield* makeHangingCreateSpawner();
Expand Down Expand Up @@ -2239,11 +2256,22 @@ const successfulHandle = (stdout = "") =>
/** Answers every engine command except `start`, which writes `stderr` and never exits. */
const makeStalledStartSpawner = Effect.fn("ContainerTest.stalledStartSpawner")(function* (
stderr: string,
containerStatus = "",
) {
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") {
const format = command.args.map(String)[2];
return Effect.succeed(
successfulHandle(
format === "{{.State.Status}}"
? containerStatus
: `{"Ports":{},"Status":"${containerStatus}","ExitCode":0}`,
),
);
}
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
22 changes: 21 additions & 1 deletion packages/stack/src/runtime/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,27 @@ 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(
// A loaded daemon can start the container yet keep the CLI waiting past its
// timeout, so the container's own state decides whether the start failed.
Effect.catchTag("ContainerError", (failure) =>
failure.kind !== "engine-timeout"
? Effect.fail(failure)
: run(["inspect", "--format", "{{.State.Status}}", name], {
timeout: "5 seconds",
}).pipe(
Effect.flatMap((status) =>
status === "running"
? Effect.logWarning(
`Container ${name} is running although '${failure.message}'`,
)
: Effect.fail(failure),
),
Effect.catchTag("ContainerError", () => Effect.fail(failure)),
),
),
);
const wait: Effect.Effect<number, ContainerError> =
attached === undefined
? run(["wait", name], {}).pipe(
Expand Down
Loading