Skip to content

Commit ddaebaf

Browse files
committed
feat(workflow): Multiple Improvements to the Workflow Bundler
1 parent 34ff54b commit ddaebaf

File tree

1 file changed

+103
-27
lines changed

1 file changed

+103
-27
lines changed

packages/worker/src/workflow/bundler.ts

Lines changed: 103 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class WorkflowCodeBundler {
5151
public readonly workflowInterceptorModules: string[];
5252
protected readonly payloadConverterPath?: string;
5353
protected readonly failureConverterPath?: string;
54+
protected readonly preloadedModules: string[];
5455
protected readonly ignoreModules: string[];
5556
protected readonly webpackConfigHook: (config: Configuration) => Configuration;
5657
protected readonly plugins: BundlerPlugin[];
@@ -68,6 +69,7 @@ export class WorkflowCodeBundler {
6869
payloadConverterPath,
6970
failureConverterPath,
7071
workflowInterceptorModules,
72+
preloadedModules,
7173
ignoreModules,
7274
webpackConfigHook,
7375
} = options;
@@ -76,6 +78,7 @@ export class WorkflowCodeBundler {
7678
this.payloadConverterPath = payloadConverterPath;
7779
this.failureConverterPath = failureConverterPath;
7880
this.workflowInterceptorModules = workflowInterceptorModules ?? [];
81+
this.preloadedModules = preloadedModules ?? [];
7982
this.ignoreModules = ignoreModules ?? [];
8083
this.webpackConfigHook = webpackConfigHook ?? ((config) => config);
8184
}
@@ -157,23 +160,29 @@ export class WorkflowCodeBundler {
157160
.map((v) => `require(/* webpackMode: "eager" */ ${JSON.stringify(v)})`)
158161
.join(', \n');
159162

163+
const preloadedModulesImports = [...new Set(this.preloadedModules)]
164+
.map((v) => `require(/* webpackMode: "eager" */ ${JSON.stringify(v)})`)
165+
.join(';\n');
166+
160167
const code = `
161-
const api = require('@temporalio/workflow/lib/worker-interface.js');
162-
exports.api = api;
168+
const api = require('@temporalio/workflow/lib/worker-interface.js');
169+
exports.api = api;
163170
164-
const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js');
165-
overrideGlobals();
171+
const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js');
172+
overrideGlobals();
166173
167-
exports.importWorkflows = function importWorkflows() {
168-
return require(/* webpackMode: "eager" */ ${JSON.stringify(this.workflowsPath)});
169-
}
174+
${preloadedModulesImports}
170175
171-
exports.importInterceptors = function importInterceptors() {
172-
return [
173-
${interceptorImports}
174-
];
175-
}
176-
`;
176+
exports.importWorkflows = function importWorkflows() {
177+
return require(/* webpackMode: "eager" */ ${JSON.stringify(this.workflowsPath)});
178+
}
179+
180+
exports.importInterceptors = function importInterceptors() {
181+
return [
182+
${interceptorImports}
183+
];
184+
}
185+
`;
177186
try {
178187
vol.mkdirSync(path.dirname(target), { recursive: true });
179188
} catch (err: any) {
@@ -198,7 +207,9 @@ exports.importInterceptors = function importInterceptors() {
198207
: data.request ?? '';
199208

200209
if (moduleMatches(module, disallowedModules) && !moduleMatches(module, this.ignoreModules)) {
201-
this.foundProblematicModules.add(module);
210+
// this.foundProblematicModules.add(module);
211+
// // callback(new Error(`Import of disallowed module: '${module}'`));
212+
throw new Error(`Import of disallowed module: '${module}'`);
202213
}
203214

204215
return undefined;
@@ -215,6 +226,7 @@ exports.importInterceptors = function importInterceptors() {
215226
__temporal_custom_failure_converter$: this.failureConverterPath ?? false,
216227
...Object.fromEntries([...this.ignoreModules, ...disallowedModules].map((m) => [m, false])),
217228
},
229+
conditionNames: ['temporalio:workflow', '...'],
218230
},
219231
externals: captureProblematicModules,
220232
module: {
@@ -255,7 +267,8 @@ exports.importInterceptors = function importInterceptors() {
255267
ignoreWarnings: [/Failed to parse source map/],
256268
};
257269

258-
const compiler = webpack(this.webpackConfigHook(options));
270+
const finalOptions = this.webpackConfigHook(options);
271+
const compiler = webpack(finalOptions);
259272

260273
// Cast to any because the type declarations are inaccurate
261274
compiler.inputFileSystem = inputFilesystem as any;
@@ -267,22 +280,27 @@ exports.importInterceptors = function importInterceptors() {
267280
return await new Promise<string>((resolve, reject) => {
268281
compiler.run((err, stats) => {
269282
if (stats !== undefined) {
270-
const hasError = stats.hasErrors();
283+
let userStatsOptions: Parameters<typeof stats.toString>[0];
284+
switch (typeof (finalOptions.stats ?? undefined)) {
285+
case 'string':
286+
case 'boolean':
287+
userStatsOptions = { preset: finalOptions.stats as string | boolean };
288+
break;
289+
case 'object':
290+
userStatsOptions = finalOptions.stats as object;
291+
break;
292+
default:
293+
userStatsOptions = undefined;
294+
}
295+
271296
// To debug webpack build:
272297
// const lines = stats.toString({ preset: 'verbose' }).split('\n');
273298
const webpackOutput = stats.toString({
274299
chunks: false,
275300
colors: hasColorSupport(this.logger),
276301
errorDetails: true,
302+
...userStatsOptions,
277303
});
278-
this.logger[hasError ? 'error' : 'info'](webpackOutput);
279-
if (hasError) {
280-
reject(
281-
new Error(
282-
"Webpack finished with errors, if you're unsure what went wrong, visit our troubleshooting page at https://docs.temporal.io/develop/typescript/debugging#webpack-errors"
283-
)
284-
);
285-
}
286304

287305
if (this.foundProblematicModules.size) {
288306
const err = new Error(
@@ -295,10 +313,22 @@ exports.importInterceptors = function importInterceptors() {
295313
` • Make sure that activity code is not imported from workflow code. Use \`import type\` to import activity function signatures.\n` +
296314
` • Move code that has non-deterministic behaviour to activities.\n` +
297315
` • If you know for sure that a disallowed module will not be used at runtime, add its name to 'WorkerOptions.bundlerOptions.ignoreModules' in order to dismiss this warning.\n` +
298-
`See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/typescript/determinism.`
316+
`See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/develop/typescript/debugging#webpack-errors.`
299317
);
300318

301319
reject(err);
320+
return;
321+
}
322+
323+
if (stats.hasErrors()) {
324+
this.logger.error(webpackOutput);
325+
reject(
326+
new Error(
327+
"Webpack finished with errors, if you're unsure what went wrong, visit our troubleshooting page at https://docs.temporal.io/develop/typescript/debugging#webpack-errors"
328+
)
329+
);
330+
} else if (finalOptions.stats !== 'none') {
331+
this.logger.info(webpackOutput);
302332
}
303333

304334
const outputFilename = Object.keys(stats.compilation.assets)[0];
@@ -345,36 +375,82 @@ export interface BundleOptions {
345375
* Path to look up workflows in, any function exported in this path will be registered as a Workflows when the bundle is loaded by a Worker.
346376
*/
347377
workflowsPath: string;
378+
348379
/**
349380
* List of modules to import Workflow interceptors from.
350381
*
351382
* Modules should export an `interceptors` variable of type {@link WorkflowInterceptorsFactory}.
352383
*/
353384
workflowInterceptorModules?: string[];
385+
354386
/**
355387
* Optional logger for logging Webpack output
356388
*/
357389
logger?: Logger;
390+
358391
/**
359392
* Path to a module with a `payloadConverter` named export.
360393
* `payloadConverter` should be an instance of a class that implements {@link PayloadConverter}.
361394
*/
362395
payloadConverterPath?: string;
396+
363397
/**
364398
* Path to a module with a `failureConverter` named export.
365399
* `failureConverter` should be an instance of a class that implements {@link FailureConverter}.
366400
*/
367401
failureConverterPath?: string;
402+
368403
/**
369404
* List of modules to be excluded from the Workflows bundle.
370405
*
406+
* > WARN: This is an advanced option that should be used with care. Improper usage may result in
407+
* > runtime errors (e.g. "Cannot read properties of undefined") in Workflow code.
408+
*
371409
* Use this option when your Workflow code references an import that cannot be used in isolation,
372410
* e.g. a Node.js built-in module. Modules listed here **MUST** not be used at runtime.
373-
*
374-
* > NOTE: This is an advanced option that should be used with care.
375411
*/
376412
ignoreModules?: string[];
377413

414+
/**
415+
* List of modules to be preloaded into the Workflow sandbox execution context.
416+
*
417+
* > WARN: This is an advanced option that should be used with care. Improper usage may result in
418+
* > non-deterministic behaviors and/or context leaks across workflow executions.
419+
*
420+
* When the Worker is configured with `reuseV8Context: true`, a single v8 execution context is
421+
* reused by multiple Workflow executions. That is, a single v8 execution context is created at
422+
* launch time; the source code of the workflow bundle gets injected into that context, and some
423+
* modules get `require`d, which forces the actual loading of those modules (i.e. module code gets
424+
* parsed, module variables and functions objects get instantiated, module gets registered into
425+
* the `require` cache, etc). After that initial loading, the execution context's globals and all
426+
* cached loaded modules get frozen, to avoid further mutations that could result in context
427+
* leaks between workflow executions.
428+
*
429+
* Then, every time a workflow is started, the workflow sandbox is restored to its pristine state,
430+
* and the workflow module gets `require`d, which results in loading the workflow module and any
431+
* other modules imported from that one. Importantly, modules loaded at that point will be
432+
* per-workflow-instance, and will therefore honor workflow-specific isolation guarantees without
433+
* requirement of being frozen. That notably means that module-level variables will be distinct
434+
* between workflow executions.
435+
*
436+
* Use this option to force preloading of some modules during the preparation phase of the
437+
* workflow execution context. This may be done for two reasons:
438+
*
439+
* - Preloading modules may reduce the per-workflow runtime cost of those modules, notably in
440+
* terms memory footprint and workflow startup time.
441+
* - Preloading modules may be necessary if those modules need to modify global variables that
442+
* would get frozen after the preparation phase, such as polyfills.
443+
*
444+
* Be warned, however, that preloaded modules will themselves get frozen, and may therefore be
445+
* unable to use module-level variables in some ways. There are ways to work around the
446+
* limitations incurred by freezing modules (e.g. use of `Map` or `Set`, closures, ECMA
447+
* `#privateFields`, etc.), but doing so may result in code that exhibits non-deterministic
448+
* behaviors and/or that may leak context across workflow executions.
449+
*
450+
* This option will have no noticeable effect if `reuseV8Context` is disabled.
451+
*/
452+
preloadedModules?: string[];
453+
378454
/**
379455
* Before Workflow code is bundled with Webpack, `webpackConfigHook` is called with the Webpack
380456
* {@link https://webpack.js.org/configuration/ | configuration} object so you can modify it.

0 commit comments

Comments
 (0)