@@ -1035,7 +1035,7 @@ describe("Orchestration Executor", () => {
10351035 expect ( completeAction ?. getResult ( ) ?. getValue ( ) ) . toEqual ( "[0,1,2,3,4,5,6,7,8,9]" ) ;
10361036 } ) ;
10371037
1038- it ( "should test that a fan-in works correctly when one of the tasks fails" , async ( ) => {
1038+ it ( "should test that a fan-in completes as failed only after all tasks finish when one fails" , async ( ) => {
10391039 const printInt = ( _ : any , value : number ) => {
10401040 return value . toString ( ) ;
10411041 } ;
@@ -1061,25 +1061,42 @@ describe("Orchestration Executor", () => {
10611061 oldEvents . push ( newTaskScheduledEvent ( i + 1 , activityName , i . toString ( ) ) ) ;
10621062 }
10631063
1064- // Chaos test with 5 tasks completing successfully, 1 failing and 4 still running
1065- // we exect that the orchestration fails immediately now and does not wait for the 4 that are running
1066- const newEvents : any [ ] = [ ] ;
1067-
1064+ // Wait-all (issue #301): 5 tasks complete successfully, 1 fails, and 4 are still running.
1065+ // The whenAll must NOT go terminal yet — it waits for the 4 outstanding tasks rather than
1066+ // failing immediately (which used to happen under fail-fast).
1067+ const ex = new Error ( "Kah-BOOOOM!!!" ) ;
1068+ const partialEvents : any [ ] = [ ] ;
10681069 for ( let i = 0 ; i < 5 ; i ++ ) {
1069- newEvents . push ( newTaskCompletedEvent ( i + 1 , printInt ( null , i ) ) ) ;
1070+ partialEvents . push ( newTaskCompletedEvent ( i + 1 , printInt ( null , i ) ) ) ;
10701071 }
1072+ partialEvents . push ( newTaskFailedEvent ( 6 , ex ) ) ;
10711073
1072- const ex = new Error ( "Kah-BOOOOM!!!" ) ;
1073- newEvents . push ( newTaskFailedEvent ( 6 , ex ) ) ;
1074+ const waitingResult = await new OrchestrationExecutor ( registry , testLogger ) . execute (
1075+ TEST_INSTANCE_ID ,
1076+ oldEvents ,
1077+ partialEvents ,
1078+ ) ;
1079+ // Still waiting on tasks 7-10 — no complete action is emitted.
1080+ expect ( waitingResult . actions . some ( ( a ) => a . hasCompleteorchestration ( ) ) ) . toBe ( false ) ;
1081+ expect ( waitingResult . actions . length ) . toEqual ( 0 ) ;
1082+
1083+ // The remaining 4 tasks now finish (delivered in a later batch). Only now does the whenAll
1084+ // complete — as failed, aggregating the single failure into an AggregateError.
1085+ const remainingEvents : any [ ] = [ ] ;
1086+ for ( let i = 6 ; i < 10 ; i ++ ) {
1087+ remainingEvents . push ( newTaskCompletedEvent ( i + 1 , printInt ( null , i ) ) ) ;
1088+ }
10741089
1075- // Now test with the full set of new events
1076- // We expect the orchestration to complete
1077- const executor = new OrchestrationExecutor ( registry , testLogger ) ;
1078- const result = await executor . execute ( TEST_INSTANCE_ID , oldEvents , newEvents ) ;
1090+ const result = await new OrchestrationExecutor ( registry , testLogger ) . execute (
1091+ TEST_INSTANCE_ID ,
1092+ [ ...oldEvents , ...partialEvents ] ,
1093+ remainingEvents ,
1094+ ) ;
10791095
10801096 const completeAction = getAndValidateSingleCompleteOrchestrationAction ( result ) ;
10811097 expect ( completeAction ?. getOrchestrationstatus ( ) ) . toEqual ( pb . OrchestrationStatus . ORCHESTRATION_STATUS_FAILED ) ;
1082- expect ( completeAction ?. getFailuredetails ( ) ?. getErrortype ( ) ) . toEqual ( "TaskFailedError" ) ;
1098+ // whenAll failures are aggregated — even a single failure is wrapped in an AggregateError.
1099+ expect ( completeAction ?. getFailuredetails ( ) ?. getErrortype ( ) ) . toEqual ( "AggregateError" ) ;
10831100 expect ( completeAction ?. getFailuredetails ( ) ?. getErrormessage ( ) ) . toContain ( ex . message ) ;
10841101 } ) ;
10851102
@@ -1772,11 +1789,12 @@ describe("Orchestration Executor", () => {
17721789
17731790 const completeAction = getAndValidateSingleCompleteOrchestrationAction ( result ) ;
17741791 expect ( completeAction ?. getOrchestrationstatus ( ) ) . toEqual ( pb . OrchestrationStatus . ORCHESTRATION_STATUS_FAILED ) ;
1775- expect ( completeAction ?. getFailuredetails ( ) ?. getErrortype ( ) ) . toEqual ( "TaskFailedError" ) ;
1792+ // whenAll aggregates failures into an AggregateError (always, even for one failure).
1793+ expect ( completeAction ?. getFailuredetails ( ) ?. getErrortype ( ) ) . toEqual ( "AggregateError" ) ;
17761794 expect ( completeAction ?. getFailuredetails ( ) ?. getErrormessage ( ) ) . toContain ( ex . message ) ;
17771795 } ) ;
17781796
1779- it ( "should not crash when additional tasks complete after whenAll fails fast " , async ( ) => {
1797+ it ( "should complete whenAll as failed after all tasks finish when one task fails " , async ( ) => {
17801798 const printInt = ( _ : any , value : number ) => {
17811799 return value . toString ( ) ;
17821800 } ;
@@ -1802,7 +1820,8 @@ describe("Orchestration Executor", () => {
18021820 oldEvents . push ( newTaskScheduledEvent ( i + 1 , activityName , i . toString ( ) ) ) ;
18031821 }
18041822
1805- // First task fails, then remaining tasks complete in the same batch
1823+ // First task fails; the remaining tasks complete in the same batch. Under wait-all the
1824+ // whenAll completes as failed only once every task is terminal, aggregating the failure.
18061825 const ex = new Error ( "First task failed" ) ;
18071826 const newEvents : any [ ] = [
18081827 newTaskFailedEvent ( 1 , ex ) ,
@@ -1815,7 +1834,7 @@ describe("Orchestration Executor", () => {
18151834
18161835 const completeAction = getAndValidateSingleCompleteOrchestrationAction ( result ) ;
18171836 expect ( completeAction ?. getOrchestrationstatus ( ) ) . toEqual ( pb . OrchestrationStatus . ORCHESTRATION_STATUS_FAILED ) ;
1818- expect ( completeAction ?. getFailuredetails ( ) ?. getErrortype ( ) ) . toEqual ( "TaskFailedError " ) ;
1837+ expect ( completeAction ?. getFailuredetails ( ) ?. getErrortype ( ) ) . toEqual ( "AggregateError " ) ;
18191838 expect ( completeAction ?. getFailuredetails ( ) ?. getErrormessage ( ) ) . toContain ( ex . message ) ;
18201839 } ) ;
18211840
@@ -1849,7 +1868,8 @@ describe("Orchestration Executor", () => {
18491868 oldEvents . push ( newTaskScheduledEvent ( i + 1 , activityName , i . toString ( ) ) ) ;
18501869 }
18511870
1852- // First task fails, then remaining tasks complete in the same batch
1871+ // First task fails; the remaining tasks complete in the same batch. The failure is caught
1872+ // by the orchestrator once the whenAll completes (as failed) after all tasks are terminal.
18531873 const ex = new Error ( "One task failed" ) ;
18541874 const newEvents : any [ ] = [
18551875 newTaskFailedEvent ( 1 , ex ) ,
@@ -1865,6 +1885,62 @@ describe("Orchestration Executor", () => {
18651885 expect ( completeAction ?. getResult ( ) ?. getValue ( ) ) . toEqual ( JSON . stringify ( "handled" ) ) ;
18661886 } ) ;
18671887
1888+ it ( "should keep waiting until all whenAll siblings fail across separate event batches (issue #301)" , async ( ) => {
1889+ // Regression for issue #301: when a whenAll fan-out has multiple failing siblings whose
1890+ // TaskFailed events arrive in separate batches, the whenAll must NOT go terminal on the
1891+ // first failure. If it did, the orchestration would go terminal early and the later
1892+ // sibling's TaskFailed would be dropped against a terminal instance, leaving a bare
1893+ // TaskScheduled with no completion — which is what deadlocks on rewind.
1894+ const printInt = ( _ : any , value : number ) => value . toString ( ) ;
1895+
1896+ const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext ) : any {
1897+ const a = ctx . callActivity ( printInt , 0 ) ;
1898+ const b = ctx . callActivity ( printInt , 1 ) ;
1899+ return yield whenAll ( [ a , b ] ) ;
1900+ } ;
1901+
1902+ const registry = new Registry ( ) ;
1903+ const orchestratorName = registry . addOrchestrator ( orchestrator ) ;
1904+ const activityName = registry . addActivity ( printInt ) ;
1905+
1906+ const scheduledHistory = [
1907+ newOrchestratorStartedEvent ( ) ,
1908+ newExecutionStartedEvent ( orchestratorName , TEST_INSTANCE_ID ) ,
1909+ newTaskScheduledEvent ( 1 , activityName , "0" ) ,
1910+ newTaskScheduledEvent ( 2 , activityName , "1" ) ,
1911+ ] ;
1912+
1913+ const firstFailure = new Error ( "first activity failed" ) ;
1914+ const secondFailure = new Error ( "second activity failed" ) ;
1915+
1916+ // Batch 1: only the first sibling fails. The whenAll is still waiting on the second
1917+ // sibling, so the orchestration must NOT emit any complete action.
1918+ const batch1 = await new OrchestrationExecutor ( registry , testLogger ) . execute (
1919+ TEST_INSTANCE_ID ,
1920+ scheduledHistory ,
1921+ [ newTaskFailedEvent ( 1 , firstFailure ) ] ,
1922+ ) ;
1923+ expect ( batch1 . actions . some ( ( a ) => a . hasCompleteorchestration ( ) ) ) . toBe ( false ) ;
1924+ expect ( batch1 . actions . length ) . toEqual ( 0 ) ;
1925+
1926+ // Batch 2: the second sibling fails too (delivered in a later batch, with batch 1 now part
1927+ // of the replayed history). Only now does the whenAll complete — as failed, aggregating BOTH
1928+ // sibling failures into an AggregateError whose message inlines every child message.
1929+ const batch2 = await new OrchestrationExecutor ( registry , testLogger ) . execute (
1930+ TEST_INSTANCE_ID ,
1931+ [ ...scheduledHistory , newTaskFailedEvent ( 1 , firstFailure ) ] ,
1932+ [ newTaskFailedEvent ( 2 , secondFailure ) ] ,
1933+ ) ;
1934+
1935+ const completeAction = getAndValidateSingleCompleteOrchestrationAction ( batch2 ) ;
1936+ expect ( completeAction ?. getOrchestrationstatus ( ) ) . toEqual ( pb . OrchestrationStatus . ORCHESTRATION_STATUS_FAILED ) ;
1937+ expect ( completeAction ?. getFailuredetails ( ) ?. getErrortype ( ) ) . toEqual ( "AggregateError" ) ;
1938+ // BOTH siblings' failures are captured (proving neither TaskFailed was dropped): the
1939+ // aggregate message inlines every child message.
1940+ expect ( completeAction ?. getFailuredetails ( ) ?. getErrormessage ( ) ) . toContain ( "first activity failed" ) ;
1941+ expect ( completeAction ?. getFailuredetails ( ) ?. getErrormessage ( ) ) . toContain ( "second activity failed" ) ;
1942+ } ) ;
1943+
18681944 it ( "should complete nested whenAny(whenAll, whenAll) when first inner group finishes" , async ( ) => {
18691945 const hello = ( _ : any , name : string ) => {
18701946 return `Hello ${ name } !` ;
@@ -2036,14 +2112,15 @@ describe("Orchestration Executor", () => {
20362112 expect ( completeAction ?. getFailuredetails ( ) ?. getErrormessage ( ) ) . toContain ( "non-Task" ) ;
20372113 } ) ;
20382114
2039- it ( "should propagate inner whenAll failure to outer whenAny in nested composites " , async ( ) => {
2115+ it ( "should propagate inner whenAll failure to outer whenAny only after all whenAll children finish " , async ( ) => {
20402116 const hello = ( _ : any , name : string ) => {
20412117 return `Hello ${ name } !` ;
20422118 } ;
20432119
20442120 // Orchestrator: yield whenAny([whenAll([a, b])])
2045- // If an inner task fails, the whenAll should fail-fast and notify the outer whenAny.
2046- // WhenAny completes with the failed task — the orchestrator inspects the winner.
2121+ // Under wait-all, the inner whenAll does NOT fail-fast: it completes (as failed) only once
2122+ // BOTH children are terminal, and only then notifies the outer whenAny. So the outer whenAny
2123+ // completes strictly later than it would have under fail-fast.
20472124 const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext ) : any {
20482125 const group = [ ctx . callActivity ( hello , "a" ) , ctx . callActivity ( hello , "b" ) ] ;
20492126 const winner : Task < string [ ] > = yield whenAny ( [ whenAll ( group ) ] ) ;
@@ -2061,12 +2138,24 @@ describe("Orchestration Executor", () => {
20612138 newTaskScheduledEvent ( 2 , activityName , JSON . stringify ( "b" ) ) ,
20622139 ] ;
20632140
2064- // Task 1 fails — whenAll should fail-fast, and outer whenAny should complete
20652141 const ex = new Error ( "task a failed" ) ;
2066- const completionEvents = [ newTaskFailedEvent ( 1 , ex ) ] ;
20672142
2068- const executor = new OrchestrationExecutor ( registry , testLogger ) ;
2069- const result = await executor . execute ( TEST_INSTANCE_ID , replayEvents , completionEvents ) ;
2143+ // Task 1 fails but task 2 is still outstanding: the inner whenAll must keep waiting, so the
2144+ // outer whenAny — and therefore the orchestration — does NOT complete yet (no complete action).
2145+ const waitingResult = await new OrchestrationExecutor ( registry , testLogger ) . execute (
2146+ TEST_INSTANCE_ID ,
2147+ replayEvents ,
2148+ [ newTaskFailedEvent ( 1 , ex ) ] ,
2149+ ) ;
2150+ expect ( waitingResult . actions . length ) . toEqual ( 0 ) ;
2151+
2152+ // Task 2 now completes: the inner whenAll completes as failed and notifies the outer whenAny,
2153+ // which resolves with the failed whenAll as its winner. The orchestrator inspects the winner.
2154+ const result = await new OrchestrationExecutor ( registry , testLogger ) . execute (
2155+ TEST_INSTANCE_ID ,
2156+ [ ...replayEvents , newTaskFailedEvent ( 1 , ex ) ] ,
2157+ [ newTaskCompletedEvent ( 2 , JSON . stringify ( hello ( null , "b" ) ) ) ] ,
2158+ ) ;
20702159
20712160 const completeAction = getAndValidateSingleCompleteOrchestrationAction ( result ) ;
20722161 expect ( completeAction ?. getOrchestrationstatus ( ) ) . toEqual ( pb . OrchestrationStatus . ORCHESTRATION_STATUS_COMPLETED ) ;
0 commit comments