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 { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||||
|
import { Logger, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { Job } from 'bullmq';
|
import { Job } from 'bullmq';
|
||||||
|
import { performance } from 'node:perf_hooks';
|
||||||
import * as puppeteer from 'puppeteer';
|
import * as puppeteer from 'puppeteer';
|
||||||
import * as pdfPrinter from 'pdf-to-printer';
|
import * as pdfPrinter from 'pdf-to-printer';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import { PrintHtmlDto } from '../dto/print-html.dto';
|
||||||
|
|
||||||
@Processor('printer')
|
@Processor('printer')
|
||||||
export class PrinterProcessor extends WorkerHost {
|
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;
|
||||||
|
|
||||||
async process(job: Job<any>): Promise<any> {
|
private async getBrowser(): Promise<puppeteer.Browser> {
|
||||||
const data = job.data;
|
if (this.browser?.isConnected()) return this.browser;
|
||||||
const tempFilePath = path.join(os.tmpdir(), `print-${job.id}-${Date.now()}.pdf`);
|
if (this.browserLaunching) return this.browserLaunching;
|
||||||
let browser;
|
|
||||||
|
|
||||||
try {
|
this.browserLaunching = puppeteer
|
||||||
browser = await puppeteer.launch({
|
.launch({
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||||
|
})
|
||||||
|
.then((browser) => {
|
||||||
|
this.browser = browser;
|
||||||
|
return browser;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.browserLaunching = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const page = await browser.newPage();
|
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 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 {
|
||||||
|
const browser = await this.getBrowser();
|
||||||
|
page = await browser.newPage();
|
||||||
const width = data.width || '80mm';
|
const width = data.width || '80mm';
|
||||||
const height = data.height;
|
const height = data.height;
|
||||||
|
|
||||||
|
|
@ -40,30 +127,97 @@ export class PrinterProcessor extends WorkerHost {
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
phase = 'render';
|
||||||
|
const renderStart = performance.now();
|
||||||
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
|
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
|
||||||
|
durationsMs.render = performance.now() - renderStart;
|
||||||
|
this.logJob('log', {
|
||||||
|
event: 'print_job_phase',
|
||||||
|
phase,
|
||||||
|
durationMs: durationsMs.render,
|
||||||
|
...jobCtx,
|
||||||
|
});
|
||||||
|
|
||||||
const pdfOptions: any = {
|
const pdfOptions: any = {
|
||||||
path: tempFilePath,
|
path: tempFilePath,
|
||||||
printBackground: true,
|
printBackground: true,
|
||||||
width: width,
|
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;
|
if (height) pdfOptions.height = height;
|
||||||
|
|
||||||
|
phase = 'pdf';
|
||||||
|
const pdfStart = performance.now();
|
||||||
await page.pdf(pdfOptions);
|
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 page.close();
|
||||||
|
page = undefined;
|
||||||
|
|
||||||
|
phase = 'spool';
|
||||||
|
const spoolStart = performance.now();
|
||||||
await pdfPrinter.print(tempFilePath, {
|
await pdfPrinter.print(tempFilePath, {
|
||||||
printer: data.printerName,
|
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 };
|
return { status: 'success', jobId: job.id };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (browser) await browser.close();
|
if (page) {
|
||||||
console.error(`Erro no Job ${job.id}:`, error);
|
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;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
if (fs.existsSync(tempFilePath)) {
|
if (fs.existsSync(tempFilePath)) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue