From 609e7d08085badd112a1429af464436c8d4b1434 Mon Sep 17 00:00:00 2001 From: OpenClawd Date: Tue, 24 Feb 2026 07:51:05 +0000 Subject: [PATCH] fix: hot-swap browser restart to prevent QUEUE_FULL with single browser Root cause: With BROWSER_COUNT=1, the hourly browser restart set restarting=true, drained all pages, closed the browser, THEN launched a new one. During that window (seconds), all requests queued and timed out after 30s with QUEUE_FULL errors. Fix: Launch the new browser BEFORE closing the old one (hot-swap). This ensures zero downtime during browser recycling, even with a single browser instance. --- src/services/browser.ts | 45 ++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/services/browser.ts b/src/services/browser.ts index 9e30d92..33d04ea 100644 --- a/src/services/browser.ts +++ b/src/services/browser.ts @@ -99,9 +99,21 @@ export function releasePage(page: Page, inst: BrowserInstance): void { async function scheduleRestart(inst: BrowserInstance): Promise { if (inst.restarting) return; inst.restarting = true; - logger.info(`Scheduling browser ${inst.id} restart`); + logger.info(`Scheduling browser ${inst.id} restart (hot-swap)`); - // Wait for pages to drain (max 30s) + // Launch new browser FIRST so we never have zero capacity + const newBrowser = await puppeteer.launch({ + headless: true, + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, + args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage", + "--disable-background-networking", "--disable-default-apps", "--disable-extensions", + "--disable-sync", "--disable-translate", "--metrics-recording-only", + "--no-first-run", "--safebrowsing-disable-auto-update"], + }); + const newPages = await createPages(newBrowser, PAGES_PER_BROWSER); + + // Wait for in-flight pages to drain (max 30s) + const oldBrowser = inst.browser; await Promise.race([ new Promise(resolve => { const check = () => { @@ -113,23 +125,24 @@ async function scheduleRestart(inst: BrowserInstance): Promise { new Promise(r => setTimeout(r, 30000)), ]); - for (const page of inst.availablePages) await page.close().catch(() => {}); - inst.availablePages.length = 0; - try { await inst.browser.close(); } catch {} - - inst.browser = await puppeteer.launch({ - headless: true, - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, - args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage", - "--disable-background-networking", "--disable-default-apps", "--disable-extensions", - "--disable-sync", "--disable-translate", "--metrics-recording-only", - "--no-first-run", "--safebrowsing-disable-auto-update"], - }); - inst.availablePages.push(...await createPages(inst.browser, PAGES_PER_BROWSER)); + // Swap: install new browser and pages, then clean up old + const oldPages = inst.availablePages.splice(0); + inst.browser = newBrowser; + inst.availablePages.push(...newPages); inst.jobCount = 0; inst.lastRestartTime = Date.now(); inst.restarting = false; - logger.info(`Browser ${inst.id} restarted`); + logger.info(`Browser ${inst.id} restarted (hot-swap complete)`); + + // Clean up old browser in background + for (const page of oldPages) await page.close().catch(() => {}); + try { await oldBrowser.close(); } catch {} + + // Drain any waiters now that pages are available + while (waitingQueue.length > 0 && inst.availablePages.length > 0) { + const waiter = waitingQueue.shift()!; + waiter.resolve({ page: inst.availablePages.pop()!, instance: inst }); + } } export async function initBrowser(): Promise {