mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
fix(aws-lambda): resolve Chromium for handlePlan before invoking probe
The producer's probe stage launches Chromium when Plan has to resolve browser-only data: root duration unknown, unresolved sub-compositions, or data-hf-auto-start media. Only handleRenderChunk was setting PRODUCER_HEADLESS_SHELL_PATH, so a cold Plan invocation launched puppeteer-core with no executablePath and failed before writing plan.tar.gz. Mirror the renderChunk env-var guard inside handlePlan so the bundled Sparticuz binary gets resolved on the first Plan invocation and reused on warm starts. The skipChromeResolution dep stays honored for SAM-local RIE smokes. A warm Lambda environment can mask this only if it previously served a renderChunk from another execution and left the env var sticky. Within a single Step Functions execution, Plan still runs before RenderChunks. A new dispatch test exercises the guard path by pre-seeding PRODUCER_HEADLESS_SHELL_PATH and asserting Plan does not overwrite it.
This commit is contained in:
@@ -209,6 +209,75 @@ describe("handler dispatch", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("plan honors a pre-set PRODUCER_HEADLESS_SHELL_PATH instead of re-resolving Chrome", async () => {
|
||||
// Mirrors the renderChunk env-var guard — when a caller (e.g. SAM-local
|
||||
// RIE smoke) seeds the path, handlePlan must not overwrite it.
|
||||
const tmpRoot = makeTmpRoot();
|
||||
const s3 = new FakeS3Client();
|
||||
s3.objects.set("s3://bucket/project.tar.gz", await makeMinimalProjectTar());
|
||||
|
||||
const planMock = mock(
|
||||
async (_projectDir: string, _config: unknown, planDir: string): Promise<PlanResult> => {
|
||||
mkdirSync(planDir, { recursive: true });
|
||||
writeFileSync(join(planDir, "plan.json"), JSON.stringify({ planHash: "fakehash" }));
|
||||
mkdirSync(join(planDir, "meta"), { recursive: true });
|
||||
writeFileSync(join(planDir, "meta", "chunks.json"), "[]");
|
||||
return {
|
||||
planDir,
|
||||
planHash: "fakehash",
|
||||
chunkCount: 1,
|
||||
totalFrames: 30,
|
||||
fps: 30 as const,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
format: "mp4" as const,
|
||||
ffmpegVersion: "6.0",
|
||||
producerVersion: "0.0.0-test",
|
||||
};
|
||||
},
|
||||
);
|
||||
const renderChunkMock = mock(async () => {
|
||||
throw new Error("should not be called");
|
||||
});
|
||||
const assembleMock = mock(async () => {
|
||||
throw new Error("should not be called");
|
||||
});
|
||||
|
||||
const event: PlanEvent = {
|
||||
Action: "plan",
|
||||
ProjectS3Uri: "s3://bucket/project.tar.gz",
|
||||
PlanOutputS3Prefix: "s3://bucket/renders/abc/",
|
||||
Config: { fps: 30, width: 1920, height: 1080, format: "mp4" },
|
||||
};
|
||||
|
||||
const sentinel = "/tmp/test-chrome-sentinel";
|
||||
const prev = process.env.PRODUCER_HEADLESS_SHELL_PATH;
|
||||
process.env.PRODUCER_HEADLESS_SHELL_PATH = sentinel;
|
||||
try {
|
||||
// Note: no skipChromeResolution flag — the guard must short-circuit
|
||||
// because PRODUCER_HEADLESS_SHELL_PATH is already set.
|
||||
await handler(event, {
|
||||
s3: s3 as unknown as import("@aws-sdk/client-s3").S3Client,
|
||||
primitives: {
|
||||
plan: planMock as unknown as typeof import("@hyperframes/producer/distributed").plan,
|
||||
renderChunk:
|
||||
renderChunkMock as unknown as typeof import("@hyperframes/producer/distributed").renderChunk,
|
||||
assemble:
|
||||
assembleMock as unknown as typeof import("@hyperframes/producer/distributed").assemble,
|
||||
},
|
||||
tmpRoot,
|
||||
});
|
||||
expect(process.env.PRODUCER_HEADLESS_SHELL_PATH).toBe(sentinel);
|
||||
expect(planMock).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
delete process.env.PRODUCER_HEADLESS_SHELL_PATH;
|
||||
} else {
|
||||
process.env.PRODUCER_HEADLESS_SHELL_PATH = prev;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("routes Action='renderChunk' to the renderChunk primitive", async () => {
|
||||
const tmpRoot = makeTmpRoot();
|
||||
const s3 = new FakeS3Client();
|
||||
|
||||
@@ -229,6 +229,16 @@ async function handlePlan(event: PlanEvent, deps?: HandlerDeps): Promise<PlanLam
|
||||
const s3 = deps?.s3 ?? getS3Client();
|
||||
const primitive = deps?.primitives?.plan ?? plan;
|
||||
|
||||
// The producer's probe stage launches Chromium whenever the composition
|
||||
// needs a runtime duration probe or has unresolved sub-compositions, so
|
||||
// plan has to resolve Chrome the same way renderChunk does. Without this
|
||||
// the probe throws "An `executablePath` or `channel` must be specified
|
||||
// for `puppeteer-core`" the moment runProbeStage calls puppeteer.launch.
|
||||
if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
||||
const chromePath = await resolveChromeExecutablePath();
|
||||
process.env.PRODUCER_HEADLESS_SHELL_PATH = chromePath;
|
||||
}
|
||||
|
||||
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-lambda-plan-"));
|
||||
// We use `.tar.gz` (not `.zip`) as the project archive's on-the-wire
|
||||
// format because Lambda's Amazon Linux base image ships GNU `tar` but
|
||||
|
||||
Reference in New Issue
Block a user