browserPool.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import puppeteer from "puppeteer";
  2. let browser;
  3. const pagePool = [];
  4. const MAX_PAGES = Number.parseInt(process.env.CHART_MAX_PAGES || "1", 10);
  5. const DEFAULT_TIMEOUT_MS = Number.parseInt(
  6. process.env.CHART_RENDER_TIMEOUT_MS || "",
  7. 10,
  8. );
  9. const EFFECTIVE_TIMEOUT_MS = Number.isFinite(DEFAULT_TIMEOUT_MS)
  10. ? DEFAULT_TIMEOUT_MS
  11. : 500000;
  12. const isBrowserAlive = () => browser && browser.isConnected();
  13. const clearPagePool = async () => {
  14. const pages = pagePool.splice(0, pagePool.length);
  15. await Promise.allSettled(
  16. pages.map(async (page) => {
  17. if (page && !page.isClosed()) await page.close();
  18. }),
  19. );
  20. };
  21. const createPage = async () => {
  22. if (!isBrowserAlive()) {
  23. await initBrowserPool();
  24. }
  25. const page = await browser.newPage();
  26. page.setDefaultTimeout(EFFECTIVE_TIMEOUT_MS);
  27. page.setDefaultNavigationTimeout(EFFECTIVE_TIMEOUT_MS);
  28. return page;
  29. };
  30. const isUsablePage = (page) =>
  31. page && !page.isClosed() && page.browser()?.isConnected();
  32. /**
  33. * 初始化浏览器 + 预创建 page
  34. */
  35. export const initBrowserPool = async () => {
  36. if (!isBrowserAlive()) {
  37. await clearPagePool();
  38. browser = null;
  39. browser = await puppeteer.launch({
  40. headless: "new",
  41. executablePath: process.env.CHROME_PATH || undefined,
  42. args: [
  43. "--no-sandbox",
  44. "--disable-setuid-sandbox",
  45. "--disable-dev-shm-usage",
  46. "--disable-background-timer-throttling",
  47. "--disable-backgrounding-occluded-windows",
  48. "--disable-renderer-backgrounding",
  49. ],
  50. // 避免渲染压力下 CDP 调用超时(对应报错:Runtime.callFunctionOn timed out)
  51. protocolTimeout: EFFECTIVE_TIMEOUT_MS,
  52. });
  53. browser.on("disconnected", () => {
  54. browser = null;
  55. pagePool.splice(0, pagePool.length);
  56. });
  57. console.log("✅ Browser initialized");
  58. // 预创建 page
  59. for (let i = 0; i < MAX_PAGES; i++) {
  60. pagePool.push(await createPage());
  61. }
  62. console.log(`✅ Page pool initialized: ${MAX_PAGES}`);
  63. }
  64. };
  65. /**
  66. * 获取 page(核心)
  67. */
  68. export const getPage = async () => {
  69. if (!isBrowserAlive()) {
  70. await initBrowserPool();
  71. }
  72. if (pagePool.length > 0) {
  73. while (pagePool.length > 0) {
  74. const page = pagePool.pop();
  75. if (isUsablePage(page)) return page;
  76. await discardPage(page);
  77. }
  78. }
  79. // pool 空了就新建(兜底)
  80. return createPage();
  81. };
  82. /**
  83. * 释放 page
  84. */
  85. export const releasePage = async (page) => {
  86. if (!page) return;
  87. try {
  88. if (!isUsablePage(page)) {
  89. await discardPage(page);
  90. return;
  91. }
  92. if (pagePool.length < MAX_PAGES) {
  93. pagePool.push(page);
  94. } else {
  95. await page.close();
  96. }
  97. } catch (err) {
  98. console.error("❌ releasePage error:", err);
  99. }
  100. };
  101. export const discardPage = async (page) => {
  102. if (!page) return;
  103. try {
  104. if (!page.isClosed()) await page.close();
  105. } catch (err) {
  106. console.warn("⚠️ discardPage failed:", err.message);
  107. }
  108. };
  109. /**
  110. * 关闭
  111. */
  112. export const closeBrowserPool = async () => {
  113. try {
  114. await clearPagePool();
  115. if (browser) {
  116. await browser.close();
  117. browser = null;
  118. }
  119. console.log("🛑 Browser pool closed");
  120. } catch (err) {
  121. console.error("❌ closeBrowserPool error:", err);
  122. }
  123. };