解決在 serverless 運作瀏覽器時 spawn ETXTBSY 的問題

最近在嘗試用 RPA 來取代例行的行政工作。

我用的架構是 Vercel + next.js,自動化網頁腳本 puppeteer-core 無頭瀏覽器 @sparticuz/chromium-min

但實務上遇到一個問題是,如果一個請求內併發多個實例。就會發生spawn ETXTBSY (Text File Busy)的錯誤。

單一個實例工作流程會是

  • 下載:下載瀏覽器的壓縮檔案
  • 解壓縮:解壓縮寫入到暫存目錄 /tmp/chromium
  • 執行:puppeteer 啟動瀏覽器

@sparticuz/chromium 套件的 executablePath() 會檢查目錄底下是否已經有瀏覽器,若有的話就會直接進入到 3 執行的階段。

當有多個實例並行時,第一個實例只執行到 2 的階段,正在寫入檔案中。由於一個 chromium-min 也有數百 MB,而寫入的過程是漸進式的,檔案會被先創建再持續寫入個幾秒鐘瀏覽器才算真的完成。

同時第二實例發現當前目錄已經有瀏覽器了(但其實第一個實例的寫入尚未完成)就拿去執行,這時就會發生錯誤,因為檔案正處於被寫入的忙碌狀態。

解決方案:

不管有多少個實例只要其中一個開始下載,其他實例也就共用相同的下載動作(只有一次)。

當下載動作完成時,所有的實例都會拿到相同的路徑。就不會發生其他實例去執行到還沒壓縮完的檔案。

let cachedExecutablePath: string | null = null;
let downloadPromise: Promise<string> | null = null;

async function getChromiumPath(): Promise<string> {
  // Return cached path if available
  if (cachedExecutablePath) return cachedExecutablePath;

  // Prevent concurrent downloads by reusing the same promise
  if (!downloadPromise) {
    const chromium = (await import("@sparticuz/chromium-min")).default;
    downloadPromise = chromium
      .executablePath(
"https://github.com/Sparticuz/chromium/releases/download/v129.0.0/chromium-v129.0.0-pack.tar"
      )
      .then((path) => {
        cachedExecutablePath = path;
        console.log("Chromium path resolved:", path);
        return path;
      })
      .catch((error) => {
        console.error("Failed to get Chromium path:", error);
        downloadPromise = null; // Reset on error to allow retry
        throw error;
      });
  }

  return downloadPromise;
}

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *