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
16 changes: 16 additions & 0 deletions apps/cli/src/commands/functions/serve/serve.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ function mockFileWatcher(expectedPaths: ReadonlyArray<string> = []) {
function mockDockerLogSpawner(behaviors: ReadonlyArray<LogProcessBehavior>) {
const spawned: Array<{ command: string; args: ReadonlyArray<string> }> = [];
let index = 0;
let liveHandles = 0;
let maxLiveHandles = 0;

return {
layer: Layer.succeed(
Expand All @@ -364,6 +366,16 @@ function mockDockerLogSpawner(behaviors: ReadonlyArray<LogProcessBehavior>) {
args: [...command.args],
};
spawned.push(record);
yield* Effect.acquireRelease(
Effect.sync(() => {
liveHandles += 1;
maxLiveHandles = Math.max(maxLiveHandles, liveHandles);
}),
() =>
Effect.sync(() => {
liveHandles -= 1;
}),
);
const behavior = behaviors[Math.min(index, behaviors.length - 1)] ?? {};
index += 1;
if (behavior.onSpawn !== undefined) yield* behavior.onSpawn();
Expand Down Expand Up @@ -396,6 +408,9 @@ function mockDockerLogSpawner(behaviors: ReadonlyArray<LogProcessBehavior>) {
get spawned() {
return spawned;
},
get maxLiveHandles() {
return maxLiveHandles;
},
};
}

Expand Down Expand Up @@ -2629,6 +2644,7 @@ describe("functions serve integration", () => {
expect(error.message).toContain("supabase_edge_runtime_test-project");
expect(error.message).toContain("5 times");
}
expect(childSpawner.maxLiveHandles).toBe(1);
});
},
);
Expand Down
85 changes: 85 additions & 0 deletions packages/stack/src/runtime/Container.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,71 @@ describe("container process adapter", () => {
).pipe(Effect.provide(NodeServices.layer)),
);

it.live("releases a log follower that exits while its container keeps running", () =>
Effect.scoped(
Effect.gen(function* () {
const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
const released = yield* Deferred.make<void>();
const runtime = yield* makeContainerRuntime({ engine: "docker", root: "." }).pipe(
Effect.provideService(
ChildProcessSpawner.ChildProcessSpawner,
makeLogFollowerSpawner(
delegate,
released,
"console.error('injected log stream failure'); process.exit(1)",
),
),
);
yield* runtime.prepare(image);
const process = yield* runtime.launch({
image,
stackId: "s".repeat(64),
instanceId: "dropped-log-follower",
env: {},
args: ["-e", "setInterval(() => {}, 1000)"],
});
yield* Deferred.await(released).pipe(Effect.timeout("10 seconds"));
expect(
yield* process.stderr.pipe(
Stream.decodeText,
Stream.mkString,
Effect.timeout("10 seconds"),
),
).toContain("injected log stream failure");
expect(yield* running(process.id)).toBe(true);
yield* process.discard;
Comment thread
7ttp marked this conversation as resolved.
Resolved
}),
).pipe(Effect.provide(NodeServices.layer)),
);

it.live("releases a still running log follower when its launch scope closes", () =>
Effect.gen(function* () {
const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
const released = yield* Deferred.make<void>();
yield* Effect.scoped(
Effect.gen(function* () {
const runtime = yield* makeContainerRuntime({ engine: "docker", root: "." }).pipe(
Effect.provideService(
ChildProcessSpawner.ChildProcessSpawner,
makeLogFollowerSpawner(delegate, released, "setInterval(() => {}, 1000)"),
),
);
yield* runtime.prepare(image);
const process = yield* runtime.launch({
image,
stackId: "t".repeat(64),
instanceId: "live-log-follower",
env: {},
args: ["-e", "setInterval(() => {}, 1000)"],
});
expect(yield* Deferred.isDone(released)).toBe(false);
yield* process.discard;
}),
);
expect(yield* Deferred.isDone(released)).toBe(true);
}).pipe(Effect.provide(NodeServices.layer)),
);

it.live("treats an externally removed container as already stopped", () =>
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
Expand Down Expand Up @@ -912,6 +977,26 @@ const makeStopFailureSpawner = (
return delegate.spawn(command);
});

const makeLogFollowerSpawner = (
delegate: ChildProcessSpawnerService["Service"],
released: Deferred.Deferred<void>,
script: string,
) =>
ChildProcessSpawner.make((command) => {
if (
ChildProcess.isStandardCommand(command) &&
command.command === "docker" &&
command.args[0] === "logs"
)
return Effect.gen(function* () {
yield* Effect.addFinalizer(() => Deferred.succeed(released, undefined));
return yield* delegate.spawn(
ChildProcess.make(process.execPath, ["-e", script], { stdin: "ignore" }),
);
});
return delegate.spawn(command);
});

const makeLostRemoveResultSpawner = (
delegate: ChildProcessSpawnerService["Service"],
lostResult: Ref.Ref<boolean>,
Expand Down
28 changes: 21 additions & 7 deletions packages/stack/src/runtime/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,13 +559,27 @@ export const makeContainerRuntime = (options: {
}
const logProcess =
attached ??
(yield* spawner
.spawn(
ChildProcess.make(options.engine, ["logs", "--follow", name], {
stdin: "ignore",
}),
)
.pipe(Effect.mapError((cause) => errorFor("logs", cause))));
(yield* Effect.gen(function* () {
// Released on exit, since a release left for service stop can hit a reused pid.
const followerScope = yield* Scope.fork(owner);
const follower = yield* spawner
.spawn(
ChildProcess.make(options.engine, ["logs", "--follow", name], {
stdin: "ignore",
}),
)
.pipe(
Scope.provide(followerScope),
Effect.mapError((cause) => errorFor("logs", cause)),
);
yield* Effect.forkIn(
Comment thread
7ttp marked this conversation as resolved.
Resolved
Effect.exit(follower.exitCode).pipe(
Effect.andThen(Scope.close(followerScope, Exit.void)),
),
owner,
);
Comment thread
7ttp marked this conversation as resolved.
Resolved
return follower;
}));
return {
...partial,
ports,
Expand Down
Loading