fix: clear PDF_TIMEOUT timers after successful render, fix test unhandled rejections
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 12m59s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 12m59s
This commit is contained in:
parent
f9caef82e6
commit
4473641ee1
2 changed files with 40 additions and 19 deletions
|
|
@ -218,7 +218,16 @@ describe("browser pool", () => {
|
||||||
expect(stats.pdfCount).toBe(1);
|
expect(stats.pdfCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("cleans up timeout timer after successful render", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
await browserModule.initBrowser();
|
||||||
|
await browserModule.renderPdf("<h1>Hello</h1>");
|
||||||
|
expect(vi.getTimerCount()).toBe(0);
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects with PDF_TIMEOUT after 30s", async () => {
|
it("rejects with PDF_TIMEOUT after 30s", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
await browserModule.initBrowser();
|
await browserModule.initBrowser();
|
||||||
// Make ALL pages' setContent hang so whichever is picked will timeout
|
// Make ALL pages' setContent hang so whichever is picked will timeout
|
||||||
for (const b of mockBrowsers) {
|
for (const b of mockBrowsers) {
|
||||||
|
|
@ -227,11 +236,13 @@ describe("browser pool", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const renderPromise = browserModule.renderPdf("<h1>slow</h1>");
|
const renderPromise = browserModule.renderPdf("<h1>slow</h1>");
|
||||||
|
const renderResult = renderPromise.catch((e: Error) => e);
|
||||||
await vi.advanceTimersByTimeAsync(30_001);
|
await vi.advanceTimersByTimeAsync(30_001);
|
||||||
|
|
||||||
await expect(renderPromise).rejects.toThrow("PDF_TIMEOUT");
|
const err = await renderResult;
|
||||||
|
expect(err).toBeInstanceOf(Error);
|
||||||
|
expect((err as Error).message).toBe("PDF_TIMEOUT");
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -322,6 +333,7 @@ describe("browser pool", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects with QUEUE_FULL after 30s timeout when all pages busy", async () => {
|
it("rejects with QUEUE_FULL after 30s timeout when all pages busy", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
await browserModule.initBrowser();
|
await browserModule.initBrowser();
|
||||||
|
|
||||||
// Make all pages hang
|
// Make all pages hang
|
||||||
|
|
@ -331,20 +343,27 @@ describe("browser pool", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume all 4 pages (these will hang)
|
// Consume all 4 pages (these will hang) — catch their rejections
|
||||||
browserModule.renderPdf("<p>1</p>");
|
const hanging = [
|
||||||
browserModule.renderPdf("<p>2</p>");
|
browserModule.renderPdf("<p>1</p>").catch(() => {}),
|
||||||
browserModule.renderPdf("<p>3</p>");
|
browserModule.renderPdf("<p>2</p>").catch(() => {}),
|
||||||
browserModule.renderPdf("<p>4</p>");
|
browserModule.renderPdf("<p>3</p>").catch(() => {}),
|
||||||
|
browserModule.renderPdf("<p>4</p>").catch(() => {}),
|
||||||
|
];
|
||||||
|
|
||||||
vi.useFakeTimers();
|
// 5th request should queue — attach catch immediately to prevent unhandled rejection
|
||||||
// 5th request should queue
|
|
||||||
const queued = browserModule.renderPdf("<p>5</p>");
|
const queued = browserModule.renderPdf("<p>5</p>");
|
||||||
|
const queuedResult = queued.catch((e: Error) => e);
|
||||||
|
|
||||||
// Advance past queue timeout
|
// Advance past all timeouts (queue + PDF_TIMEOUT for hanging renders)
|
||||||
await vi.advanceTimersByTimeAsync(30_001);
|
await vi.advanceTimersByTimeAsync(30_001);
|
||||||
|
|
||||||
await expect(queued).rejects.toThrow("QUEUE_FULL");
|
const err = await queuedResult;
|
||||||
|
expect(err).toBeInstanceOf(Error);
|
||||||
|
expect((err as Error).message).toBe("QUEUE_FULL");
|
||||||
|
|
||||||
|
// Let hanging PDF_TIMEOUT rejections settle
|
||||||
|
await Promise.allSettled(hanging);
|
||||||
|
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,7 @@ export async function renderPdf(
|
||||||
try {
|
try {
|
||||||
await page.setJavaScriptEnabled(false);
|
await page.setJavaScriptEnabled(false);
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
const result = await Promise.race([
|
const result = await Promise.race([
|
||||||
(async () => {
|
(async () => {
|
||||||
await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 15_000 });
|
await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 15_000 });
|
||||||
|
|
@ -264,10 +265,10 @@ export async function renderPdf(
|
||||||
});
|
});
|
||||||
return Buffer.from(pdf);
|
return Buffer.from(pdf);
|
||||||
})(),
|
})(),
|
||||||
new Promise<never>((_, reject) =>
|
new Promise<never>((_, reject) => {
|
||||||
setTimeout(() => reject(new Error("PDF_TIMEOUT")), 30_000)
|
timeoutId = setTimeout(() => reject(new Error("PDF_TIMEOUT")), 30_000);
|
||||||
),
|
}),
|
||||||
]);
|
]).finally(() => clearTimeout(timeoutId));
|
||||||
const durationMs = Date.now() - startTime;
|
const durationMs = Date.now() - startTime;
|
||||||
logger.info(`PDF rendered in ${durationMs}ms (html, ${result.length} bytes)`);
|
logger.info(`PDF rendered in ${durationMs}ms (html, ${result.length} bytes)`);
|
||||||
return { pdf: result, durationMs };
|
return { pdf: result, durationMs };
|
||||||
|
|
@ -320,6 +321,7 @@ export async function renderUrlPdf(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
const result = await Promise.race([
|
const result = await Promise.race([
|
||||||
(async () => {
|
(async () => {
|
||||||
await page.goto(url, {
|
await page.goto(url, {
|
||||||
|
|
@ -342,10 +344,10 @@ export async function renderUrlPdf(
|
||||||
});
|
});
|
||||||
return Buffer.from(pdf);
|
return Buffer.from(pdf);
|
||||||
})(),
|
})(),
|
||||||
new Promise<never>((_, reject) =>
|
new Promise<never>((_, reject) => {
|
||||||
setTimeout(() => reject(new Error("PDF_TIMEOUT")), 30_000)
|
timeoutId = setTimeout(() => reject(new Error("PDF_TIMEOUT")), 30_000);
|
||||||
),
|
}),
|
||||||
]);
|
]).finally(() => clearTimeout(timeoutId));
|
||||||
const durationMs = Date.now() - startTime;
|
const durationMs = Date.now() - startTime;
|
||||||
logger.info(`PDF rendered in ${durationMs}ms (url, ${result.length} bytes)`);
|
logger.info(`PDF rendered in ${durationMs}ms (url, ${result.length} bytes)`);
|
||||||
return { pdf: result, durationMs };
|
return { pdf: result, durationMs };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue