@@ -26,7 +26,7 @@ export function setUnhandledRejectionHandler(getWorkflowByRunId: (runId: string)
26
26
if ( runId !== undefined ) {
27
27
const workflow = getWorkflowByRunId ( runId ) ;
28
28
if ( workflow !== undefined ) {
29
- workflow . setUnhandledRejection ( err ) ;
29
+ workflow . setUnhandledRejection ( new UnhandledRejectionError ( `Unhandled Promise rejection: ${ err } ` , err ) ) ;
30
30
return ;
31
31
}
32
32
}
@@ -323,97 +323,100 @@ export abstract class BaseVMWorkflow implements Workflow {
323
323
public async activate (
324
324
activation : coresdk . workflow_activation . IWorkflowActivation
325
325
) : Promise < coresdk . workflow_completion . IWorkflowActivationCompletion > {
326
- if ( this . context === undefined ) throw new IllegalStateError ( 'Workflow isolate context uninitialized' ) ;
327
- activation = coresdk . workflow_activation . WorkflowActivation . fromObject ( activation ) ;
328
- if ( ! activation . jobs ) throw new TypeError ( 'Expected workflow activation jobs to be defined' ) ;
329
-
330
- // Queries are particular in many ways, and Core guarantees that a single activation will not
331
- // contain both queries and other jobs. So let's handle them separately.
332
- const [ queries , nonQueries ] = partition ( activation . jobs , ( { queryWorkflow } ) => queryWorkflow != null ) ;
333
- if ( queries . length > 0 ) {
334
- if ( nonQueries . length > 0 ) throw new TypeError ( 'Got both queries and other jobs in a single activation' ) ;
335
- return this . activateQueries ( activation ) ;
336
- }
326
+ try {
327
+ if ( this . context === undefined ) throw new IllegalStateError ( 'Workflow isolate context uninitialized' ) ;
328
+ activation = coresdk . workflow_activation . WorkflowActivation . fromObject ( activation ) ;
329
+ if ( ! activation . jobs ) throw new TypeError ( 'Expected workflow activation jobs to be defined' ) ;
330
+
331
+ // Queries are particular in many ways, and Core guarantees that a single activation will not
332
+ // contain both queries and other jobs. So let's handle them separately.
333
+ const [ queries , nonQueries ] = partition ( activation . jobs , ( { queryWorkflow } ) => queryWorkflow != null ) ;
334
+ if ( queries . length > 0 ) {
335
+ if ( nonQueries . length > 0 ) throw new TypeError ( 'Got both queries and other jobs in a single activation' ) ;
336
+ return this . activateQueries ( activation ) ;
337
+ }
337
338
338
- // Update the activator's state in preparation for a non-query activation.
339
- // This is done early, so that we can then rely on the activator while processing the activation.
340
- if ( activation . timestamp == null )
341
- throw new TypeError ( 'Expected activation.timestamp to be set for non-query activation' ) ;
342
- this . activator . now = tsToMs ( activation . timestamp ) ;
343
- this . activator . mutateWorkflowInfo ( ( info ) => ( {
344
- ...info ,
345
- historyLength : activation . historyLength as number ,
346
- // Exact truncation for multi-petabyte histories
347
- // historySize === 0 means WFT was generated by pre-1.20.0 server, and the history size is unknown
348
- historySize : activation . historySizeBytes ?. toNumber ( ) ?? 0 ,
349
- continueAsNewSuggested : activation . continueAsNewSuggested ?? false ,
350
- currentBuildId : activation . buildIdForCurrentTask ?? undefined ,
351
- unsafe : {
352
- ...info . unsafe ,
353
- isReplaying : activation . isReplaying ?? false ,
354
- } ,
355
- } ) ) ;
356
- this . activator . addKnownFlags ( activation . availableInternalFlags ?? [ ] ) ;
339
+ // Update the activator's state in preparation for a non-query activation.
340
+ // This is done early, so that we can then rely on the activator while processing the activation.
341
+ if ( activation . timestamp == null )
342
+ throw new TypeError ( 'Expected activation.timestamp to be set for non-query activation' ) ;
343
+ this . activator . now = tsToMs ( activation . timestamp ) ;
344
+ this . activator . mutateWorkflowInfo ( ( info ) => ( {
345
+ ...info ,
346
+ historyLength : activation . historyLength as number ,
347
+ // Exact truncation for multi-petabyte histories
348
+ // historySize === 0 means WFT was generated by pre-1.20.0 server, and the history size is unknown
349
+ historySize : activation . historySizeBytes ?. toNumber ( ) ?? 0 ,
350
+ continueAsNewSuggested : activation . continueAsNewSuggested ?? false ,
351
+ currentBuildId : activation . buildIdForCurrentTask ?? undefined ,
352
+ unsafe : {
353
+ ...info . unsafe ,
354
+ isReplaying : activation . isReplaying ?? false ,
355
+ } ,
356
+ } ) ) ;
357
+ this . activator . addKnownFlags ( activation . availableInternalFlags ?? [ ] ) ;
357
358
358
- // Initialization of the workflow must happen before anything else. Yet, keep the init job in
359
- // place in the list as we'll use it as a marker to know when to start the workflow function.
360
- const initWorkflowJob = activation . jobs . find ( ( job ) => job . initializeWorkflow != null ) ?. initializeWorkflow ;
361
- if ( initWorkflowJob ) this . workflowModule . initialize ( initWorkflowJob ) ;
359
+ // Initialization of the workflow must happen before anything else. Yet, keep the init job in
360
+ // place in the list as we'll use it as a marker to know when to start the workflow function.
361
+ const initWorkflowJob = activation . jobs . find ( ( job ) => job . initializeWorkflow != null ) ?. initializeWorkflow ;
362
+ if ( initWorkflowJob ) this . workflowModule . initialize ( initWorkflowJob ) ;
362
363
363
- const hasSignals = activation . jobs . some ( ( { signalWorkflow } ) => signalWorkflow != null ) ;
364
- const doSingleBatch = ! hasSignals || this . activator . hasFlag ( SdkFlags . ProcessWorkflowActivationJobsAsSingleBatch ) ;
364
+ const hasSignals = activation . jobs . some ( ( { signalWorkflow } ) => signalWorkflow != null ) ;
365
+ const doSingleBatch = ! hasSignals || this . activator . hasFlag ( SdkFlags . ProcessWorkflowActivationJobsAsSingleBatch ) ;
365
366
366
- const [ patches , nonPatches ] = partition ( activation . jobs , ( { notifyHasPatch } ) => notifyHasPatch != null ) ;
367
- for ( const { notifyHasPatch } of patches ) {
368
- if ( notifyHasPatch == null ) throw new TypeError ( 'Expected notifyHasPatch to be set' ) ;
369
- this . activator . notifyHasPatch ( notifyHasPatch ) ;
370
- }
367
+ const [ patches , nonPatches ] = partition ( activation . jobs , ( { notifyHasPatch } ) => notifyHasPatch != null ) ;
368
+ for ( const { notifyHasPatch } of patches ) {
369
+ if ( notifyHasPatch == null ) throw new TypeError ( 'Expected notifyHasPatch to be set' ) ;
370
+ this . activator . notifyHasPatch ( notifyHasPatch ) ;
371
+ }
371
372
372
- if ( doSingleBatch ) {
373
- // updateRandomSeed requires the same special handling as patches (before anything else, and don't
374
- // unblock conditions after each job). Unfortunately, prior to ProcessWorkflowActivationJobsAsSingleBatch,
375
- // they were handled as regular jobs, making it unsafe to properly handle that job above, with patches.
376
- const [ updateRandomSeed , rest ] = partition ( nonPatches , ( { updateRandomSeed } ) => updateRandomSeed != null ) ;
377
- if ( updateRandomSeed . length > 0 )
378
- this . activator . updateRandomSeed ( updateRandomSeed [ updateRandomSeed . length - 1 ] . updateRandomSeed ! ) ;
379
- this . workflowModule . activate (
380
- coresdk . workflow_activation . WorkflowActivation . fromObject ( { ...activation , jobs : rest } )
381
- ) ;
382
- this . tryUnblockConditionsAndMicrotasks ( ) ;
383
- } else {
384
- const [ signals , nonSignals ] = partition (
385
- nonPatches ,
386
- // Move signals to a first batch; all the rest goes in a second batch.
387
- ( { signalWorkflow } ) => signalWorkflow != null
388
- ) ;
389
-
390
- // Loop and invoke each batch, waiting for microtasks to complete after each batch.
391
- let batchIndex = 0 ;
392
- for ( const jobs of [ signals , nonSignals ] ) {
393
- if ( jobs . length === 0 ) continue ;
373
+ if ( doSingleBatch ) {
374
+ // updateRandomSeed requires the same special handling as patches (before anything else, and don't
375
+ // unblock conditions after each job). Unfortunately, prior to ProcessWorkflowActivationJobsAsSingleBatch,
376
+ // they were handled as regular jobs, making it unsafe to properly handle that job above, with patches.
377
+ const [ updateRandomSeed , rest ] = partition ( nonPatches , ( { updateRandomSeed } ) => updateRandomSeed != null ) ;
378
+ if ( updateRandomSeed . length > 0 )
379
+ this . activator . updateRandomSeed ( updateRandomSeed [ updateRandomSeed . length - 1 ] . updateRandomSeed ! ) ;
394
380
this . workflowModule . activate (
395
- coresdk . workflow_activation . WorkflowActivation . fromObject ( { ...activation , jobs } ) ,
396
- batchIndex ++
381
+ coresdk . workflow_activation . WorkflowActivation . fromObject ( { ...activation , jobs : rest } )
397
382
) ;
398
383
this . tryUnblockConditionsAndMicrotasks ( ) ;
384
+ } else {
385
+ const [ signals , nonSignals ] = partition (
386
+ nonPatches ,
387
+ // Move signals to a first batch; all the rest goes in a second batch.
388
+ ( { signalWorkflow } ) => signalWorkflow != null
389
+ ) ;
390
+
391
+ // Loop and invoke each batch, waiting for microtasks to complete after each batch.
392
+ let batchIndex = 0 ;
393
+ for ( const jobs of [ signals , nonSignals ] ) {
394
+ if ( jobs . length === 0 ) continue ;
395
+ this . workflowModule . activate (
396
+ coresdk . workflow_activation . WorkflowActivation . fromObject ( { ...activation , jobs } ) ,
397
+ batchIndex ++
398
+ ) ;
399
+ this . tryUnblockConditionsAndMicrotasks ( ) ;
400
+ }
399
401
}
400
- }
401
402
402
- const completion = this . workflowModule . concludeActivation ( ) ;
403
+ const completion = this . workflowModule . concludeActivation ( ) ;
403
404
404
- // Give unhandledRejection handler a chance to be triggered.
405
- await new Promise ( setImmediate ) ;
406
- if ( this . unhandledRejection ) {
405
+ // Give unhandledRejection handler a chance to be triggered.
406
+ await new Promise ( setImmediate ) ;
407
+ if ( this . unhandledRejection ) throw this . unhandledRejection ;
408
+
409
+ return completion ;
410
+ } catch ( err ) {
407
411
return {
408
412
runId : this . activator . info . runId ,
409
413
// FIXME: Calling `activator.errorToFailure()` directly from outside the VM is unsafe, as it
410
414
// depends on the `failureConverter` and `payloadConverter`, which may be customized and
411
415
// therefore aren't guaranteed not to access `global` or to cause scheduling microtasks.
412
416
// Admitingly, the risk is very low, so we're leaving it as is for now.
413
- failed : { failure : this . activator . errorToFailure ( this . unhandledRejection ) } ,
417
+ failed : { failure : this . activator . errorToFailure ( err ) } ,
414
418
} ;
415
419
}
416
- return completion ;
417
420
}
418
421
419
422
private activateQueries (
@@ -434,14 +437,22 @@ export abstract class BaseVMWorkflow implements Workflow {
434
437
* If called (by an external unhandledRejection handler), activations will fail with provided error.
435
438
*/
436
439
public setUnhandledRejection ( err : unknown ) : void {
440
+ if ( this . activator ) {
441
+ // This is very unlikely to make a difference, as unhandled rejections should be reported
442
+ // on the next macro task of the outer execution context (i.e. not inside the VM), at which
443
+ // point we are done handling the workflow activation anyway. But just in case, copying the
444
+ // error to the activator will ensure that any attempt to make progress in the workflow
445
+ // VM will immediately fail.
446
+ this . activator . workflowTaskError = err ;
447
+ }
437
448
this . unhandledRejection = err ;
438
449
}
439
450
440
451
/**
441
452
* Call into the Workflow context to attempt to unblock any blocked conditions and microtasks.
442
453
*
443
- * This is performed in a loop allowing microtasks to be processed between
444
- * each iteration until there are no more conditions to unblock.
454
+ * This is performed in a loop, going in and out of the VM, allowing microtasks to be processed
455
+ * between each iteration of the outer loop, until there are no more conditions to unblock.
445
456
*/
446
457
protected tryUnblockConditionsAndMicrotasks ( ) : void {
447
458
for ( ; ; ) {
0 commit comments