diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3c37650ea2..4bf1a1b5602 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -684,8 +684,8 @@ jobs: # browser: firefox - host: macos-latest browser: webkit - # - host: windows-latest - # browser: chromium + - host: windows-latest + browser: chromium runs-on: ${{ matrix.settings.host }} diff --git a/e2e/adapters-e2e/playwright.config.ts b/e2e/adapters-e2e/playwright.config.ts index cf7e0e59a69..1483b6432c8 100644 --- a/e2e/adapters-e2e/playwright.config.ts +++ b/e2e/adapters-e2e/playwright.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ }, // Increase global timeout for service worker tests - timeout: 30000, + timeout: process.env.CI ? 120000 : 30000, projects: [ { @@ -44,5 +44,6 @@ export default defineConfig({ port: 3000, stdout: 'pipe', reuseExistingServer: !process.env.CI, + timeout: process.env.CI ? 120000 : 30000, }, }); diff --git a/e2e/qwik-cli-e2e/utils/index.ts b/e2e/qwik-cli-e2e/utils/index.ts index 72db5824710..b8328e4cf67 100644 --- a/e2e/qwik-cli-e2e/utils/index.ts +++ b/e2e/qwik-cli-e2e/utils/index.ts @@ -205,4 +205,4 @@ export function log(text: string) { console.log(yellow('E2E: ' + text)); } -export const DEFAULT_TIMEOUT = 30000; +export const DEFAULT_TIMEOUT = process.env.CI ? 120000 : 30000; diff --git a/e2e/qwik-react-e2e/playwright.config.ts b/e2e/qwik-react-e2e/playwright.config.ts index cf7e0e59a69..1483b6432c8 100644 --- a/e2e/qwik-react-e2e/playwright.config.ts +++ b/e2e/qwik-react-e2e/playwright.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ }, // Increase global timeout for service worker tests - timeout: 30000, + timeout: process.env.CI ? 120000 : 30000, projects: [ { @@ -44,5 +44,6 @@ export default defineConfig({ port: 3000, stdout: 'pipe', reuseExistingServer: !process.env.CI, + timeout: process.env.CI ? 120000 : 30000, }, }); diff --git a/packages/qwik-city/src/static/main-thread.ts b/packages/qwik-city/src/static/main-thread.ts index be77f87b928..82dcd176f04 100644 --- a/packages/qwik-city/src/static/main-thread.ts +++ b/packages/qwik-city/src/static/main-thread.ts @@ -216,7 +216,10 @@ export async function mainThread(sys: System) { flushQueue(); }; - loadStaticRoutes(); + loadStaticRoutes().catch((e) => { + console.error('SSG route loading failed', e); + reject(e); + }); } catch (e) { reject(e); } diff --git a/packages/qwik-city/src/static/node/node-main.ts b/packages/qwik-city/src/static/node/node-main.ts index 4bf079ba0b6..6c4fcead575 100644 --- a/packages/qwik-city/src/static/node/node-main.ts +++ b/packages/qwik-city/src/static/node/node-main.ts @@ -13,7 +13,6 @@ import { Worker } from 'node:worker_threads'; import { isAbsolute, resolve } from 'node:path'; import { ensureDir } from './node-system'; import { normalizePath } from '../../utils/fs'; -import { createSingleThreadWorker } from '../worker-thread'; export async function createNodeMainProcess(sys: System, opts: StaticGenerateOptions) { const ssgWorkers: StaticGeneratorWorker[] = []; @@ -50,34 +49,14 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt sitemapOutFile = resolve(outDir, sitemapOutFile); } } - - const singleThreadWorker = await createSingleThreadWorker(sys); - - const createWorker = (workerIndex: number) => { - if (workerIndex === 0) { - // same thread worker, don't start a new process - const ssgSameThreadWorker: StaticGeneratorWorker = { - activeTasks: 0, - totalTasks: 0, - - render: async (staticRoute) => { - ssgSameThreadWorker.activeTasks++; - ssgSameThreadWorker.totalTasks++; - const result = await singleThreadWorker(staticRoute); - ssgSameThreadWorker.activeTasks--; - return result; - }, - - terminate: async () => {}, - }; - return ssgSameThreadWorker; - } - + const createWorker = () => { let terminateResolve: (() => void) | null = null; const mainTasks = new Map(); let workerFilePath: string | URL; + let terminateTimeout: number | null = null; + // Launch the worker using the package's index module, which bootstraps the worker thread. if (typeof __filename === 'string') { workerFilePath = __filename; } else { @@ -89,6 +68,7 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt } const nodeWorker = new Worker(workerFilePath, { workerData: opts }); + nodeWorker.unref(); const ssgWorker: StaticGeneratorWorker = { activeTasks: 0, @@ -116,7 +96,9 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt terminateResolve = resolve; nodeWorker.postMessage(msg); }); - await nodeWorker.terminate(); + terminateTimeout = setTimeout(async () => { + await nodeWorker.terminate(); + }, 1000) as unknown as number; }, }; @@ -146,7 +128,11 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt }); nodeWorker.on('exit', (code) => { - if (code !== 1) { + if (terminateTimeout) { + clearTimeout(terminateTimeout); + terminateTimeout = null; + } + if (code !== 0) { console.error(`worker exit ${code}`); } }); @@ -200,9 +186,15 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt console.error(e); } } - ssgWorkers.length = 0; await Promise.all(promises); + ssgWorkers.length = 0; + + // On Windows, give extra time for all workers to fully exit + // This prevents resource conflicts in back-to-back builds + if (process.platform === 'win32') { + await new Promise((resolve) => setTimeout(resolve, 300)); + } }; if (sitemapOutFile) { @@ -214,7 +206,11 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt } for (let i = 0; i < maxWorkers; i++) { - ssgWorkers.push(createWorker(i)); + ssgWorkers.push(createWorker()); + // On Windows, add delay between worker creation to avoid resource contention + if (process.platform === 'win32' && i < maxWorkers - 1) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } } const mainCtx: MainContext = { diff --git a/packages/qwik-city/src/static/node/node-worker.ts b/packages/qwik-city/src/static/node/node-worker.ts index 904486e684d..a2701143a77 100644 --- a/packages/qwik-city/src/static/node/node-worker.ts +++ b/packages/qwik-city/src/static/node/node-worker.ts @@ -6,5 +6,8 @@ export async function createNodeWorkerProcess( ) { parentPort?.on('message', async (msg: WorkerInputMessage) => { parentPort?.postMessage(await onMessage(msg)); + if (msg.type === 'close') { + parentPort?.close(); + } }); } diff --git a/packages/qwik-city/src/static/worker-thread.ts b/packages/qwik-city/src/static/worker-thread.ts index 962631cb683..ffbd366adb0 100644 --- a/packages/qwik-city/src/static/worker-thread.ts +++ b/packages/qwik-city/src/static/worker-thread.ts @@ -39,23 +39,6 @@ export async function workerThread(sys: System) { }); } -export async function createSingleThreadWorker(sys: System) { - const ssgOpts = sys.getOptions(); - const pendingPromises = new Set>(); - - const opts: StaticGenerateHandlerOptions = { - ...ssgOpts, - render: (await import(pathToFileURL(ssgOpts.renderModulePath).href)).default, - qwikCityPlan: (await import(pathToFileURL(ssgOpts.qwikCityPlanModulePath).href)).default, - }; - - return (staticRoute: StaticRoute) => { - return new Promise((resolve) => { - workerRender(sys, opts, staticRoute, pendingPromises, resolve); - }); - }; -} - async function workerRender( sys: System, opts: StaticGenerateHandlerOptions,