Skip to content

Commit 87f8eca

Browse files
committed
feat(workflow): Multiple Improvements to the Workflow Bundler
1 parent 8d489e3 commit 87f8eca

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

@@ -60,6 +61,7 @@ export class WorkflowCodeBundler {
6061
payloadConverterPath,
6162
failureConverterPath,
6263
workflowInterceptorModules,
64+
preloadedModules,
6365
ignoreModules,
6466
webpackConfigHook,
6567
}: BundleOptions) {
@@ -68,6 +70,7 @@ export class WorkflowCodeBundler {
6870
this.payloadConverterPath = payloadConverterPath;
6971
this.failureConverterPath = failureConverterPath;
7072
this.workflowInterceptorModules = workflowInterceptorModules ?? [];
73+
this.preloadedModules = preloadedModules ?? [];
7174
this.ignoreModules = ignoreModules ?? [];
7275
this.webpackConfigHook = webpackConfigHook ?? ((config) => config);
7376
}
@@ -149,23 +152,29 @@ export class WorkflowCodeBundler {
149152
.map((v) => `require(/* webpackMode: "eager" */ ${JSON.stringify(v)})`)
150153
.join(', \n');
151154

155+
const preloadedModulesImports = [...new Set(this.preloadedModules)]
156+
.map((v) => `require(/* webpackMode: "eager" */ ${JSON.stringify(v)})`)
157+
.join(';\n');
158+
152159
const code = `
153-
const api = require('@temporalio/workflow/lib/worker-interface.js');
154-
exports.api = api;
160+
const api = require('@temporalio/workflow/lib/worker-interface.js');
161+
exports.api = api;
155162
156-
const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js');
157-
overrideGlobals();
163+
const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js');
164+
overrideGlobals();
158165
159-
exports.importWorkflows = function importWorkflows() {
160-
return require(/* webpackMode: "eager" */ ${JSON.stringify(this.workflowsPath)});
161-
}
166+
${preloadedModulesImports}
162167
163-
exports.importInterceptors = function importInterceptors() {
164-
return [
165-
${interceptorImports}
166-
];
167-
}
168-
`;
168+
exports.importWorkflows = function importWorkflows() {
169+
return require(/* webpackMode: "eager" */ ${JSON.stringify(this.workflowsPath)});
170+
}
171+
172+
exports.importInterceptors = function importInterceptors() {
173+
return [
174+
${interceptorImports}
175+
];
176+
}
177+
`;
169178
try {
170179
vol.mkdirSync(path.dirname(target), { recursive: true });
171180
} catch (err: any) {
@@ -190,7 +199,9 @@ exports.importInterceptors = function importInterceptors() {
190199
: data.request ?? '';
191200

192201
if (moduleMatches(module, disallowedModules) && !moduleMatches(module, this.ignoreModules)) {
193-
this.foundProblematicModules.add(module);
202+
// this.foundProblematicModules.add(module);
203+
// // callback(new Error(`Import of disallowed module: '${module}'`));
204+
throw new Error(`Import of disallowed module: '${module}'`);
194205
}
195206

196207
return undefined;
@@ -207,6 +218,7 @@ exports.importInterceptors = function importInterceptors() {
207218
__temporal_custom_failure_converter$: this.failureConverterPath ?? false,
208219
...Object.fromEntries([...this.ignoreModules, ...disallowedModules].map((m) => [m, false])),
209220
},
221+
conditionNames: ['temporalio:workflow', '...'],
210222
},
211223
externals: captureProblematicModules,
212224
module: {
@@ -247,7 +259,8 @@ exports.importInterceptors = function importInterceptors() {
247259
ignoreWarnings: [/Failed to parse source map/],
248260
};
249261

250-
const compiler = webpack(this.webpackConfigHook(options));
262+
const finalOptions = this.webpackConfigHook(options);
263+
const compiler = webpack(finalOptions);
251264

252265
// Cast to any because the type declarations are inaccurate
253266
compiler.inputFileSystem = inputFilesystem as any;
@@ -259,22 +272,27 @@ exports.importInterceptors = function importInterceptors() {
259272
return await new Promise<string>((resolve, reject) => {
260273
compiler.run((err, stats) => {
261274
if (stats !== undefined) {
262-
const hasError = stats.hasErrors();
275+
let userStatsOptions: Parameters<typeof stats.toString>[0];
276+
switch (typeof (finalOptions.stats ?? undefined)) {
277+
case 'string':
278+
case 'boolean':
279+
userStatsOptions = { preset: finalOptions.stats as string | boolean };
280+
break;
281+
case 'object':
282+
userStatsOptions = finalOptions.stats as object;
283+
break;
284+
default:
285+
userStatsOptions = undefined;
286+
}
287+
263288
// To debug webpack build:
264289
// const lines = stats.toString({ preset: 'verbose' }).split('\n');
265290
const webpackOutput = stats.toString({
266291
chunks: false,
267292
colors: hasColorSupport(this.logger),
268293
errorDetails: true,
294+
...userStatsOptions,
269295
});
270-
this.logger[hasError ? 'error' : 'info'](webpackOutput);
271-
if (hasError) {
272-
reject(
273-
new Error(
274-
"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"
275-
)
276-
);
277-
}
278296

279297
if (this.foundProblematicModules.size) {
280298
const err = new Error(
@@ -287,10 +305,22 @@ exports.importInterceptors = function importInterceptors() {
287305
` • Make sure that activity code is not imported from workflow code. Use \`import type\` to import activity function signatures.\n` +
288306
` • Move code that has non-deterministic behaviour to activities.\n` +
289307
` • 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` +
290-
`See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/typescript/determinism.`
308+
`See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/develop/typescript/debugging#webpack-errors.`
291309
);
292310

293311
reject(err);
312+
return;
313+
}
314+
315+
if (stats.hasErrors()) {
316+
this.logger.error(webpackOutput);
317+
reject(
318+
new Error(
319+
"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"
320+
)
321+
);
322+
} else if (finalOptions.stats !== 'none') {
323+
this.logger.info(webpackOutput);
294324
}
295325

296326
const outputFilename = Object.keys(stats.compilation.assets)[0];
@@ -315,36 +345,82 @@ export interface BundleOptions {
315345
* 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.
316346
*/
317347
workflowsPath: string;
348+
318349
/**
319350
* List of modules to import Workflow interceptors from.
320351
*
321352
* Modules should export an `interceptors` variable of type {@link WorkflowInterceptorsFactory}.
322353
*/
323354
workflowInterceptorModules?: string[];
355+
324356
/**
325357
* Optional logger for logging Webpack output
326358
*/
327359
logger?: Logger;
360+
328361
/**
329362
* Path to a module with a `payloadConverter` named export.
330363
* `payloadConverter` should be an instance of a class that implements {@link PayloadConverter}.
331364
*/
332365
payloadConverterPath?: string;
366+
333367
/**
334368
* Path to a module with a `failureConverter` named export.
335369
* `failureConverter` should be an instance of a class that implements {@link FailureConverter}.
336370
*/
337371
failureConverterPath?: string;
372+
338373
/**
339374
* List of modules to be excluded from the Workflows bundle.
340375
*
376+
* > WARN: This is an advanced option that should be used with care. Improper usage may result in
377+
* > runtime errors (e.g. "Cannot read properties of undefined") in Workflow code.
378+
*
341379
* Use this option when your Workflow code references an import that cannot be used in isolation,
342380
* e.g. a Node.js built-in module. Modules listed here **MUST** not be used at runtime.
343-
*
344-
* > NOTE: This is an advanced option that should be used with care.
345381
*/
346382
ignoreModules?: string[];
347383

384+
/**
385+
* List of modules to be preloaded into the Workflow sandbox execution context.
386+
*
387+
* > WARN: This is an advanced option that should be used with care. Improper usage may result in
388+
* > non-deterministic behaviors and/or context leaks across workflow executions.
389+
*
390+
* When the Worker is configured with `reuseV8Context: true`, a single v8 execution context is
391+
* reused by multiple Workflow executions. That is, a single v8 execution context is created at
392+
* launch time; the source code of the workflow bundle gets injected into that context, and some
393+
* modules get `require`d, which forces the actual loading of those modules (i.e. module code gets
394+
* parsed, module variables and functions objects get instantiated, module gets registered into
395+
* the `require` cache, etc). After that initial loading, the execution context's globals and all
396+
* cached loaded modules get frozen, to avoid further mutations that could result in context
397+
* leaks between workflow executions.
398+
*
399+
* Then, every time a workflow is started, the workflow sandbox is restored to its pristine state,
400+
* and the workflow module gets `require`d, which results in loading the workflow module and any
401+
* other modules imported from that one. Importantly, modules loaded at that point will be
402+
* per-workflow-instance, and will therefore honor workflow-specific isolation guarantees without
403+
* requirement of being frozen. That notably means that module-level variables will be distinct
404+
* between workflow executions.
405+
*
406+
* Use this option to force preloading of some modules during the preparation phase of the
407+
* workflow execution context. This may be done for two reasons:
408+
*
409+
* - Preloading modules may reduce the per-workflow runtime cost of those modules, notably in
410+
* terms memory footprint and workflow startup time.
411+
* - Preloading modules may be necessary if those modules need to modify global variables that
412+
* would get frozen after the preparation phase, such as polyfills.
413+
*
414+
* Be warned, however, that preloaded modules will themselves get frozen, and may therefore be
415+
* unable to use module-level variables in some ways. There are ways to work around the
416+
* limitations incurred by freezing modules (e.g. use of `Map` or `Set`, closures, ECMA
417+
* `#privateFields`, etc.), but doing so may result in code that exhibits non-deterministic
418+
* behaviors and/or that may leak context across workflow executions.
419+
*
420+
* This option will have no noticeable effect if `reuseV8Context` is disabled.
421+
*/
422+
preloadedModules?: string[];
423+
348424
/**
349425
* Before Workflow code is bundled with Webpack, `webpackConfigHook` is called with the Webpack
350426
* {@link https://webpack.js.org/configuration/ | configuration} object so you can modify it.

0 commit comments

Comments
 (0)