refactor: reuse puppeteer browser and log print job phases

This commit is contained in:
Joelbrit0 2026-01-26 18:31:37 -03:00
parent 4c5e5bdf5a
commit ef83703f5a
1 changed files with 176 additions and 22 deletions

View File

@ -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 {
}
}
}
}
}