@@ -26,7 +26,7 @@ export function setUnhandledRejectionHandler(getWorkflowByRunId: (runId: string)
2626 if ( runId !== undefined ) {
2727 const workflow = getWorkflowByRunId ( runId ) ;
2828 if ( workflow !== undefined ) {
29- workflow . setUnhandledRejection ( err ) ;
29+ workflow . setUnhandledRejection ( new UnhandledRejectionError ( `Unhandled Promise rejection: ${ err } ` , err ) ) ;
3030 return ;
3131 }
3232 }
@@ -323,97 +323,100 @@ export abstract class BaseVMWorkflow implements Workflow {
323323 public async activate (
324324 activation : coresdk . workflow_activation . IWorkflowActivation
325325 ) : 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+ }
337338
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 ?? [ ] ) ;
357358
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 ) ;
362363
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 ) ;
365366
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+ }
371372
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 ! ) ;
394380 this . workflowModule . activate (
395- coresdk . workflow_activation . WorkflowActivation . fromObject ( { ...activation , jobs } ) ,
396- batchIndex ++
381+ coresdk . workflow_activation . WorkflowActivation . fromObject ( { ...activation , jobs : rest } )
397382 ) ;
398383 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+ }
399401 }
400- }
401402
402- const completion = this . workflowModule . concludeActivation ( ) ;
403+ const completion = this . workflowModule . concludeActivation ( ) ;
403404
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 ) {
407411 return {
408412 runId : this . activator . info . runId ,
409413 // FIXME: Calling `activator.errorToFailure()` directly from outside the VM is unsafe, as it
410414 // depends on the `failureConverter` and `payloadConverter`, which may be customized and
411415 // therefore aren't guaranteed not to access `global` or to cause scheduling microtasks.
412416 // 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 ) } ,
414418 } ;
415419 }
416- return completion ;
417420 }
418421
419422 private activateQueries (
@@ -434,14 +437,22 @@ export abstract class BaseVMWorkflow implements Workflow {
434437 * If called (by an external unhandledRejection handler), activations will fail with provided error.
435438 */
436439 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+ }
437448 this . unhandledRejection = err ;
438449 }
439450
440451 /**
441452 * Call into the Workflow context to attempt to unblock any blocked conditions and microtasks.
442453 *
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.
445456 */
446457 protected tryUnblockConditionsAndMicrotasks ( ) : void {
447458 for ( ; ; ) {
0 commit comments