refactor: reuse puppeteer browser and log print job phases
This commit is contained in:
parent
4c5e5bdf5a
commit
ef83703f5a
|
|
@ -1,26 +1,113 @@
|
|||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Logger, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import * as pdfPrinter from 'pdf-to-printer';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { PrintHtmlDto } from '../dto/print-html.dto';
|
||||
|
||||
@Processor('printer')
|
||||
export class PrinterProcessor extends WorkerHost {
|
||||
|
||||
async process(job: Job<any>): Promise<any> {
|
||||
export class PrinterProcessor
|
||||
extends WorkerHost
|
||||
implements OnApplicationShutdown
|
||||
{
|
||||
private readonly logger = new Logger(PrinterProcessor.name);
|
||||
private browser: puppeteer.Browser | undefined;
|
||||
private browserLaunching: Promise<puppeteer.Browser> | undefined;
|
||||
|
||||
private async getBrowser(): Promise<puppeteer.Browser> {
|
||||
if (this.browser?.isConnected()) return this.browser;
|
||||
if (this.browserLaunching) return this.browserLaunching;
|
||||
|
||||
this.browserLaunching = puppeteer
|
||||
.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
})
|
||||
.then((browser) => {
|
||||
this.browser = browser;
|
||||
return browser;
|
||||
})
|
||||
.finally(() => {
|
||||
this.browserLaunching = undefined;
|
||||
});
|
||||
|
||||
return this.browserLaunching;
|
||||
}
|
||||
|
||||
async onApplicationShutdown(): Promise<void> {
|
||||
try {
|
||||
await this.browser?.close();
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to close Puppeteer browser: ${String(err)}`);
|
||||
} finally {
|
||||
this.browser = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private logJob(
|
||||
level: 'log' | 'warn' | 'error' | 'debug',
|
||||
payload: Record<string, unknown>,
|
||||
error?: unknown,
|
||||
): void {
|
||||
const message = JSON.stringify(payload);
|
||||
if (level === 'error') {
|
||||
const stack = error instanceof Error ? error.stack : undefined;
|
||||
this.logger.error(message, stack);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level === 'warn') {
|
||||
this.logger.warn(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level === 'debug') {
|
||||
this.logger.debug(message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(message);
|
||||
}
|
||||
|
||||
async process(job: Job<PrintHtmlDto>): Promise<any> {
|
||||
const data = job.data;
|
||||
const tempFilePath = path.join(os.tmpdir(), `print-${job.id}-${Date.now()}.pdf`);
|
||||
let browser;
|
||||
const printerName = data?.printerName;
|
||||
const jobCtx = {
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
printerName,
|
||||
};
|
||||
|
||||
const tempFilePath = path.join(
|
||||
os.tmpdir(),
|
||||
`print-${job.id}-${Date.now()}.pdf`,
|
||||
);
|
||||
let page: puppeteer.Page | undefined;
|
||||
let phase: 'render' | 'pdf' | 'spool' | 'unknown' = 'unknown';
|
||||
|
||||
const durationsMs: {
|
||||
render?: number;
|
||||
pdf?: number;
|
||||
spool?: number;
|
||||
total?: number;
|
||||
} = {};
|
||||
|
||||
const totalStart = performance.now();
|
||||
|
||||
this.logJob('log', {
|
||||
event: 'print_job_start',
|
||||
...jobCtx,
|
||||
width: data?.width,
|
||||
height: data?.height,
|
||||
});
|
||||
|
||||
try {
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
const browser = await this.getBrowser();
|
||||
page = await browser.newPage();
|
||||
const width = data.width || '80mm';
|
||||
const height = data.height;
|
||||
|
||||
|
|
@ -40,30 +127,97 @@ export class PrinterProcessor extends WorkerHost {
|
|||
</html>
|
||||
`;
|
||||
|
||||
phase = 'render';
|
||||
const renderStart = performance.now();
|
||||
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
|
||||
|
||||
const pdfOptions: any = {
|
||||
path: tempFilePath,
|
||||
durationsMs.render = performance.now() - renderStart;
|
||||
this.logJob('log', {
|
||||
event: 'print_job_phase',
|
||||
phase,
|
||||
durationMs: durationsMs.render,
|
||||
...jobCtx,
|
||||
});
|
||||
|
||||
const pdfOptions: any = {
|
||||
path: tempFilePath,
|
||||
printBackground: true,
|
||||
width: width,
|
||||
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' }
|
||||
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' },
|
||||
};
|
||||
|
||||
if (height) pdfOptions.height = height;
|
||||
|
||||
phase = 'pdf';
|
||||
const pdfStart = performance.now();
|
||||
await page.pdf(pdfOptions);
|
||||
await browser.close();
|
||||
durationsMs.pdf = performance.now() - pdfStart;
|
||||
this.logJob('log', {
|
||||
event: 'print_job_phase',
|
||||
phase,
|
||||
durationMs: durationsMs.pdf,
|
||||
...jobCtx,
|
||||
});
|
||||
|
||||
await pdfPrinter.print(tempFilePath, {
|
||||
await page.close();
|
||||
page = undefined;
|
||||
|
||||
phase = 'spool';
|
||||
const spoolStart = performance.now();
|
||||
await pdfPrinter.print(tempFilePath, {
|
||||
printer: data.printerName,
|
||||
silent: true
|
||||
silent: true,
|
||||
});
|
||||
durationsMs.spool = performance.now() - spoolStart;
|
||||
this.logJob('log', {
|
||||
event: 'print_job_phase',
|
||||
phase,
|
||||
durationMs: durationsMs.spool,
|
||||
...jobCtx,
|
||||
});
|
||||
|
||||
durationsMs.total = performance.now() - totalStart;
|
||||
this.logJob('log', {
|
||||
event: 'print_job_success',
|
||||
...jobCtx,
|
||||
durationsMs,
|
||||
});
|
||||
|
||||
return { status: 'success', jobId: job.id };
|
||||
|
||||
} catch (error) {
|
||||
if (browser) await browser.close();
|
||||
console.error(`Erro no Job ${job.id}:`, error);
|
||||
if (page) {
|
||||
try {
|
||||
await page.close();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
durationsMs.total = performance.now() - totalStart;
|
||||
this.logJob(
|
||||
'error',
|
||||
{
|
||||
event: 'print_job_error',
|
||||
...jobCtx,
|
||||
phase,
|
||||
durationsMs,
|
||||
errorMessage: message,
|
||||
errorType: error instanceof Error ? error.name : typeof error,
|
||||
},
|
||||
error,
|
||||
);
|
||||
|
||||
// If the shared browser crashed/closed, reset it so next job can relaunch.
|
||||
if (/Target closed|Browser closed|Protocol error/i.test(message)) {
|
||||
try {
|
||||
await this.browser?.close();
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
this.browser = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
if (fs.existsSync(tempFilePath)) {
|
||||
|
|
@ -71,4 +225,4 @@ export class PrinterProcessor extends WorkerHost {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue