Skip to content

Commit cc6ae9d

Browse files
wmertensVarixo
authored andcommitted
chore(router): better error handling for SSG
1 parent 0ceb396 commit cc6ae9d

File tree

8 files changed

+87
-82
lines changed

8 files changed

+87
-82
lines changed

packages/qwik-city/src/adapters/shared/vite/index.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -202,34 +202,6 @@ export function viteAdapter(opts: ViteAdapterPluginOptions) {
202202
`\n==============================================`
203203
);
204204
}
205-
if (opts.ssg !== null) {
206-
/**
207-
* HACK: for some reason the build hangs after SSG. `why-is-node-running` shows 4
208-
* culprits:
209-
*
210-
* ```
211-
* There are 4 handle(s) keeping the process running.
212-
*
213-
* # CustomGC
214-
* ./node_modules/.pnpm/lightningcss@1.30.1/node_modules/lightningcss/node/index.js:20 - module.exports = require(`lightningcss-${parts.join('-')}`);
215-
*
216-
* # CustomGC
217-
* ./node_modules/.pnpm/@tailwindcss+oxide@4.1.12/node_modules/@tailwindcss/oxide/index.js:229 - return require('@tailwindcss/oxide-linux-x64-gnu')
218-
*
219-
* # Timeout
220-
* node_modules/.vite-temp/vite.config.timestamp-1755270314169-a2a97ad5233f9.mjs:357
221-
* ./node_modules/.pnpm/vite@7.1.2_@types+node@24.3.0_jiti@2.5.1_lightningcss@1.30.1_terser@5.43.1_tsx@4.20.4_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-CMEinpL-.js:36657 - return (await import(pathToFileURL(tempFileName).href)).default;
222-
*
223-
* # CustomGC
224-
* ./packages/qwik/dist/optimizer.mjs:1328 - const mod2 = module.default.createRequire(import.meta.url)(`../bindings/${triple.platformArchABI}`);
225-
* ```
226-
*
227-
* For now, we'll force exit the process after SSG with some delay.
228-
*/
229-
setTimeout(() => {
230-
process.exit(0);
231-
}, 5000).unref();
232-
}
233205
}
234206
},
235207
},

packages/qwik-city/src/static/main-thread.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,17 @@ export async function mainThread(sys: System) {
8080
while (!isCompleted && main.hasAvailableWorker() && queue.length > 0) {
8181
const staticRoute = queue.shift();
8282
if (staticRoute) {
83-
render(staticRoute);
83+
render(staticRoute).catch((e) => {
84+
console.error(`render failed for ${staticRoute.pathname}`, e);
85+
});
8486
}
8587
}
8688

8789
if (!isCompleted && isRoutesLoaded && queue.length === 0 && active.size === 0) {
8890
isCompleted = true;
89-
completed();
91+
completed().catch((e) => {
92+
console.error('SSG completion failed', e);
93+
});
9094
}
9195
};
9296

@@ -134,6 +138,7 @@ export async function mainThread(sys: System) {
134138

135139
flushQueue();
136140
} catch (e) {
141+
console.error(`render failed for ${staticRoute.pathname}`, e);
137142
isCompleted = true;
138143
reject(e);
139144
}
@@ -216,8 +221,12 @@ export async function mainThread(sys: System) {
216221
flushQueue();
217222
};
218223

219-
loadStaticRoutes();
224+
loadStaticRoutes().catch((e) => {
225+
console.error('SSG route loading failed', e);
226+
reject(e);
227+
});
220228
} catch (e) {
229+
console.error('SSG main thread failed', e);
221230
reject(e);
222231
}
223232
});
@@ -244,6 +253,6 @@ function validateOptions(opts: StaticGenerateOptions) {
244253
try {
245254
new URL(siteOrigin);
246255
} catch (e) {
247-
throw new Error(`Invalid "origin": ${e}`);
256+
throw new Error(`Invalid "origin"`, { cause: e as Error });
248257
}
249258
}

packages/qwik-city/src/static/node/index.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { StaticGenerateOptions } from '../types';
22
import { createSystem } from './node-system';
3-
import { isMainThread, workerData } from 'node:worker_threads';
3+
import { isMainThread, workerData, threadId } from 'node:worker_threads';
44
import { mainThread } from '../main-thread';
55
import { workerThread } from '../worker-thread';
66

@@ -15,9 +15,20 @@ export async function generate(opts: StaticGenerateOptions) {
1515
}
1616

1717
if (!isMainThread && workerData) {
18+
const opts = workerData as SsgOptions;
1819
(async () => {
19-
// self initializing worker thread with workerData
20-
const sys = await createSystem(workerData);
21-
await workerThread(sys);
22-
})();
20+
try {
21+
if (opts.log === 'debug') {
22+
// eslint-disable-next-line no-console
23+
console.debug(`Worker thread starting (ID: ${threadId})`);
24+
}
25+
// self initializing worker thread with workerData
26+
const sys = await createSystem(opts, threadId);
27+
await workerThread(sys);
28+
} catch (error) {
29+
console.error(`Error occurred in worker thread (ID: ${threadId}): ${error}`);
30+
}
31+
})().catch((e) => {
32+
console.error(e);
33+
});
2334
}

packages/qwik-city/src/static/node/node-main.ts

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Worker } from 'node:worker_threads';
1313
import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
1414
import { ensureDir } from './node-system';
1515
import { normalizePath } from '../../utils/fs';
16-
import { createSingleThreadWorker } from '../worker-thread';
1716

1817
export async function createNodeMainProcess(sys: System, opts: StaticGenerateOptions) {
1918
const ssgWorkers: StaticGeneratorWorker[] = [];
@@ -51,28 +50,7 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt
5150
}
5251
}
5352

54-
const singleThreadWorker = await createSingleThreadWorker(sys);
55-
56-
const createWorker = (workerIndex: number) => {
57-
if (workerIndex === 0) {
58-
// same thread worker, don't start a new process
59-
const ssgSameThreadWorker: StaticGeneratorWorker = {
60-
activeTasks: 0,
61-
totalTasks: 0,
62-
63-
render: async (staticRoute) => {
64-
ssgSameThreadWorker.activeTasks++;
65-
ssgSameThreadWorker.totalTasks++;
66-
const result = await singleThreadWorker(staticRoute);
67-
ssgSameThreadWorker.activeTasks--;
68-
return result;
69-
},
70-
71-
terminate: async () => {},
72-
};
73-
return ssgSameThreadWorker;
74-
}
75-
53+
const createWorker = () => {
7654
let terminateResolve: (() => void) | null = null;
7755
const mainTasks = new Map<string, WorkerMainTask>();
7856

@@ -98,7 +76,7 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt
9876
}
9977

10078
const nodeWorker = new Worker(workerFilePath, { workerData: opts });
101-
79+
nodeWorker.unref();
10280
const ssgWorker: StaticGeneratorWorker = {
10381
activeTasks: 0,
10482
totalTasks: 0,
@@ -223,7 +201,7 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt
223201
}
224202

225203
for (let i = 0; i < maxWorkers; i++) {
226-
ssgWorkers.push(createWorker(i));
204+
ssgWorkers.push(createWorker());
227205
}
228206

229207
const mainCtx: MainContext = {

packages/qwik-city/src/static/node/node-system.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import { createNodeWorkerProcess } from './node-worker';
88
import { normalizePath } from '../../utils/fs';
99

1010
/** @public */
11-
export async function createSystem(opts: StaticGenerateOptions) {
12-
patchGlobalThis();
13-
11+
export async function createSystem(
12+
opts: StaticGenerateOptions,
13+
threadId?: number
14+
): Promise<System> {
1415
const createWriteStream = (filePath: string) => {
1516
return fs.createWriteStream(filePath, {
1617
flags: 'w',
@@ -29,6 +30,13 @@ export async function createSystem(opts: StaticGenerateOptions) {
2930
};
3031

3132
const createLogger = async () => {
33+
if (threadId !== undefined) {
34+
return {
35+
debug: opts.log === 'debug' ? console.debug.bind(console, `[${threadId}]`) : () => {},
36+
error: console.error.bind(console, `[${threadId}]`),
37+
info: console.info.bind(console, `[${threadId}]`),
38+
};
39+
}
3240
return {
3341
debug: opts.log === 'debug' ? console.debug.bind(console) : () => {},
3442
error: console.error.bind(console),

packages/qwik-city/src/static/node/node-worker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ export async function createNodeWorkerProcess(
66
) {
77
parentPort?.on('message', async (msg: WorkerInputMessage) => {
88
parentPort?.postMessage(await onMessage(msg));
9+
if (msg.type === 'close') {
10+
parentPort?.close();
11+
}
912
});
1013
}

packages/qwik-city/src/static/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface System {
66
createMainProcess: (() => Promise<MainContext>) | null;
77
createWorkerProcess: (
88
onMessage: (msg: WorkerInputMessage) => Promise<WorkerOutputMessage>
9-
) => void;
9+
) => void | Promise<void>;
1010
createLogger: () => Promise<Logger>;
1111
getOptions: () => StaticGenerateOptions;
1212
ensureDir: (filePath: string) => Promise<void>;

packages/qwik-city/src/static/worker-thread.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,43 @@ import { _deserializeData, _serializeData, _verifySerializable } from '@builder.
1515
export async function workerThread(sys: System) {
1616
const ssgOpts = sys.getOptions();
1717
const pendingPromises = new Set<Promise<any>>();
18+
const log = await sys.createLogger();
1819

1920
const opts: StaticGenerateHandlerOptions = {
2021
...ssgOpts,
22+
// TODO export this from server
2123
render: (await import(pathToFileURL(ssgOpts.renderModulePath).href)).default,
22-
qwikCityPlan: (await import(pathToFileURL(ssgOpts.qwikCityPlanModulePath).href)).default,
24+
// TODO this should be built-in
25+
qwikRouterConfig: (await import(pathToFileURL(ssgOpts.qwikRouterConfigModulePath).href))
26+
.default,
2327
};
2428

25-
sys.createWorkerProcess(async (msg) => {
26-
switch (msg.type) {
27-
case 'render': {
28-
return new Promise<StaticWorkerRenderResult>((resolve) => {
29-
workerRender(sys, opts, msg, pendingPromises, resolve);
30-
});
31-
}
32-
case 'close': {
33-
const promises = Array.from(pendingPromises);
34-
pendingPromises.clear();
35-
await Promise.all(promises);
36-
return { type: 'close' };
29+
sys
30+
.createWorkerProcess(async (msg) => {
31+
switch (msg.type) {
32+
case 'render': {
33+
log.debug(`Worker thread rendering: ${msg.pathname}`);
34+
return new Promise<StaticWorkerRenderResult>((resolve) => {
35+
workerRender(sys, opts, msg, pendingPromises, resolve).catch((e) => {
36+
console.error('Error during render', msg.pathname, e);
37+
});
38+
});
39+
}
40+
case 'close': {
41+
if (pendingPromises.size) {
42+
log.debug(`Worker thread closing, waiting for ${pendingPromises.size} pending renders`);
43+
const promises = Array.from(pendingPromises);
44+
pendingPromises.clear();
45+
await Promise.all(promises);
46+
}
47+
log.debug(`Worker thread closed`);
48+
return { type: 'close' };
49+
}
3750
}
38-
}
39-
});
51+
})
52+
?.catch((e) => {
53+
console.error('Worker process creation failed', e);
54+
});
4055
}
4156

4257
export async function createSingleThreadWorker(sys: System) {
@@ -51,7 +66,9 @@ export async function createSingleThreadWorker(sys: System) {
5166

5267
return (staticRoute: StaticRoute) => {
5368
return new Promise<StaticWorkerRenderResult>((resolve) => {
54-
workerRender(sys, opts, staticRoute, pendingPromises, resolve);
69+
workerRender(sys, opts, staticRoute, pendingPromises, resolve).catch((e) => {
70+
console.error('Error during render', staticRoute.pathname, e);
71+
});
5572
});
5673
};
5774
}
@@ -254,6 +271,13 @@ async function workerRender(
254271
}
255272
}
256273
})
274+
.catch((e) => {
275+
console.error('Unhandled error during request handling', staticRoute.pathname, e);
276+
result.error = {
277+
message: String(e),
278+
stack: e.stack || '',
279+
};
280+
})
257281
.finally(() => {
258282
pendingPromises.delete(promise);
259283
callback(result);

0 commit comments

Comments
 (0)