@@ -344,15 +344,26 @@ fn contextualize_subtree_failure(context: &str, error: String) -> NodeError {
344344}
345345
346346#[ derive( Debug , PartialEq , Eq ) ]
347- enum IsolatedIterationFailure {
348- Continue ( String ) ,
349- Propagate ( String ) ,
347+ enum FailureIsolatedBodyOutcome {
348+ Succeeded ( String ) ,
349+ ApplicationFailed ,
350+ Break ( String ) ,
350351}
351352
352- fn classify_isolated_iteration_failure ( error : String ) -> IsolatedIterationFailure {
353- match decode_subtree_application_failure ( & error) {
354- Some ( message) => IsolatedIterationFailure :: Continue ( message) ,
355- None => IsolatedIterationFailure :: Propagate ( error) ,
353+ fn classify_isolated_body_result (
354+ result : Result < String , String > ,
355+ results : & mut HashMap < String , String > ,
356+ ) -> Result < FailureIsolatedBodyOutcome , NodeError > {
357+ match result {
358+ Ok ( raw) => match parse_subtree_envelope ( & raw , "LOOP iteration" , results) {
359+ Ok ( body_result) => Ok ( FailureIsolatedBodyOutcome :: Succeeded ( body_result) ) ,
360+ Err ( NodeError :: Break ( value) ) => Ok ( FailureIsolatedBodyOutcome :: Break ( value) ) ,
361+ Err ( error) => Err ( error) ,
362+ } ,
363+ Err ( error) => match decode_subtree_application_failure ( & error) {
364+ Some ( _) => Ok ( FailureIsolatedBodyOutcome :: ApplicationFailed ) ,
365+ None => Err ( NodeError :: Failure ( error) ) ,
366+ } ,
356367 }
357368}
358369
@@ -1120,7 +1131,27 @@ async fn run_loop_iteration(
11201131 Err ( NodeError :: Failure ( error) ) => return Err ( NodeError :: Failure ( error) ) ,
11211132 } ;
11221133
1123- // While-condition: if present and false, exit the loop.
1134+ Box :: pin ( evaluate_loop_condition (
1135+ ctx,
1136+ graph,
1137+ node,
1138+ condition_node,
1139+ body_result,
1140+ results,
1141+ exec_ctx,
1142+ ) )
1143+ . await
1144+ }
1145+
1146+ async fn evaluate_loop_condition (
1147+ ctx : & OrchestrationContext ,
1148+ graph : & FunctionGraph ,
1149+ node : & FunctionNode ,
1150+ condition_node : Option < & str > ,
1151+ body_result : String ,
1152+ results : & mut HashMap < String , String > ,
1153+ exec_ctx : & ExecutionContext ,
1154+ ) -> Result < Option < String > , NodeError > {
11241155 if let Some ( condition_node_id) = condition_node {
11251156 ctx. trace_info ( "Evaluating loop condition" ) ;
11261157 let condition_result =
@@ -1152,48 +1183,32 @@ async fn run_loop_iteration(
11521183 Ok ( None )
11531184}
11541185
1155- async fn run_failure_isolated_iteration (
1186+ async fn run_failure_isolated_body (
11561187 ctx : & OrchestrationContext ,
11571188 graph : & FunctionGraph ,
1158- node : & FunctionNode ,
11591189 loop_node_id : & str ,
11601190 body_id : & str ,
11611191 results : & mut HashMap < String , String > ,
11621192 exec_ctx : & ExecutionContext ,
1163- ) -> Result < Option < String > , NodeError > {
1193+ ) -> Result < FailureIsolatedBodyOutcome , NodeError > {
11641194 let child_input = build_subtree_input ( graph, body_id, results, exec_ctx) ?;
11651195 let child_id = subtree_instance_id ( ctx, body_id) ;
11661196
11671197 ctx. trace_info ( format ! (
11681198 "Starting failure-isolated iteration for LOOP node {loop_node_id} as child {child_id}"
11691199 ) ) ;
11701200
1171- let raw = match ctx
1201+ let child_result = ctx
11721202 . schedule_sub_orchestration_with_id ( SUBTREE_NAME , child_id, child_input)
1173- . await
1174- {
1175- Ok ( raw) => raw,
1176- Err ( error) => match classify_isolated_iteration_failure ( error) {
1177- IsolatedIterationFailure :: Continue ( error) => {
1178- ctx. trace_warn ( format ! (
1179- "LOOP node {loop_node_id} iteration application failure; continuing: {error}"
1180- ) ) ;
1181- return Ok ( None ) ;
1182- }
1183- IsolatedIterationFailure :: Propagate ( error) => {
1184- return Err ( NodeError :: Failure ( error) ) ;
1185- }
1186- } ,
1187- } ;
1203+ . await ;
11881204
1189- match parse_subtree_envelope ( & raw , "LOOP iteration" , results) {
1190- Ok ( _) => Ok ( None ) ,
1191- Err ( NodeError :: Break ( value) ) => {
1192- store_named_result ( ctx, node, & value, results, "LOOP" ) ;
1193- Ok ( Some ( value) )
1194- }
1195- Err ( error) => Err ( error) ,
1205+ let outcome = classify_isolated_body_result ( child_result, results) ?;
1206+ if outcome == FailureIsolatedBodyOutcome :: ApplicationFailed {
1207+ ctx. trace_warn ( format ! (
1208+ "LOOP node {loop_node_id} iteration application failure; continuing"
1209+ ) ) ;
11961210 }
1211+ Ok ( outcome)
11971212}
11981213
11991214/// Execute a loop node inline, driving the *current* orchestration's `continue_as_new`.
@@ -1237,10 +1252,29 @@ async fn execute_loop_node(
12371252 ctx. trace_info ( "Executing loop iteration" ) ;
12381253
12391254 let final_result = if config. continue_on_failure {
1240- Box :: pin ( run_failure_isolated_iteration (
1241- ctx, graph, node , node_id, body_id, results, exec_ctx,
1255+ match Box :: pin ( run_failure_isolated_body (
1256+ ctx, graph, node_id, body_id, results, exec_ctx,
12421257 ) )
1243- . await
1258+ . await ?
1259+ {
1260+ FailureIsolatedBodyOutcome :: Succeeded ( body_result) => {
1261+ Box :: pin ( evaluate_loop_condition (
1262+ ctx,
1263+ graph,
1264+ node,
1265+ config. condition_node . as_deref ( ) ,
1266+ body_result,
1267+ results,
1268+ exec_ctx,
1269+ ) )
1270+ . await ?
1271+ }
1272+ FailureIsolatedBodyOutcome :: ApplicationFailed => None ,
1273+ FailureIsolatedBodyOutcome :: Break ( value) => {
1274+ store_named_result ( ctx, node, & value, results, "LOOP" ) ;
1275+ Some ( value)
1276+ }
1277+ }
12441278 } else {
12451279 Box :: pin ( run_loop_iteration (
12461280 ctx,
@@ -1251,10 +1285,10 @@ async fn execute_loop_node(
12511285 results,
12521286 exec_ctx,
12531287 ) )
1254- . await
1288+ . await ?
12551289 } ;
12561290
1257- if let Some ( final_result) = final_result? {
1291+ if let Some ( final_result) = final_result {
12581292 return Ok ( final_result) ;
12591293 }
12601294
@@ -2161,6 +2195,31 @@ mod tests {
21612195 assert ! ( MAX_LOOP_ITERATIONS . is_power_of_two( ) ) ;
21622196 }
21632197
2198+ #[ test]
2199+ fn isolated_iteration_continues_typed_application_failure ( ) {
2200+ let encoded = encode_subtree_application_failure ( "division by zero" ) ;
2201+ assert_eq ! (
2202+ encoded,
2203+ r#"{"pg_durable_subtree_failure":"application","message":"division by zero"}"#
2204+ ) ;
2205+ let mut results = HashMap :: new ( ) ;
2206+ assert_eq ! (
2207+ classify_isolated_body_result( Err ( encoded) , & mut results) . unwrap( ) ,
2208+ FailureIsolatedBodyOutcome :: ApplicationFailed
2209+ ) ;
2210+ }
2211+
2212+ #[ test]
2213+ fn isolated_iteration_propagates_unrecognized_runtime_failure ( ) {
2214+ let runtime_error =
2215+ "sub-orchestration instance id 'child' already exists and is terminal" . to_string ( ) ;
2216+ let mut results = HashMap :: new ( ) ;
2217+ match classify_isolated_body_result ( Err ( runtime_error. clone ( ) ) , & mut results) {
2218+ Err ( NodeError :: Failure ( error) ) => assert_eq ! ( error, runtime_error) ,
2219+ other => panic ! ( "expected runtime failure, got {other:?}" ) ,
2220+ }
2221+ }
2222+
21642223 #[ test]
21652224 fn subtree_instance_ids_are_stable_and_generation_scoped ( ) {
21662225 assert_eq ! (
@@ -2174,25 +2233,57 @@ mod tests {
21742233 }
21752234
21762235 #[ test]
2177- fn isolated_iteration_continues_typed_application_failure ( ) {
2178- let encoded = encode_subtree_application_failure ( "division by zero" ) ;
2236+ fn isolated_body_success_returns_result_and_merges_results ( ) {
2237+ let raw = envelope_json (
2238+ Some ( "Normal" ) ,
2239+ "42" ,
2240+ serde_json:: json!( { "body_result" : "stored" } ) ,
2241+ ) ;
2242+ let mut results = HashMap :: new ( ) ;
2243+
21792244 assert_eq ! (
2180- encoded ,
2181- r#"{"pg_durable_subtree_failure":"application","message":"division by zero"}"#
2245+ classify_isolated_body_result ( Ok ( raw ) , & mut results ) . unwrap ( ) ,
2246+ FailureIsolatedBodyOutcome :: Succeeded ( "42" . to_string ( ) )
21822247 ) ;
21832248 assert_eq ! (
2184- classify_isolated_iteration_failure ( encoded ) ,
2185- IsolatedIterationFailure :: Continue ( "division by zero" . to_string ( ) )
2249+ results . get ( "body_result" ) . map ( String :: as_str ) ,
2250+ Some ( "stored" )
21862251 ) ;
21872252 }
21882253
21892254 #[ test]
2190- fn isolated_iteration_propagates_unrecognized_runtime_failure ( ) {
2191- let runtime_error =
2192- "sub-orchestration instance id 'child' already exists and is terminal" . to_string ( ) ;
2255+ fn isolated_body_consumes_typed_application_failure ( ) {
2256+ let mut results = HashMap :: new ( ) ;
2257+
2258+ assert_eq ! (
2259+ classify_isolated_body_result(
2260+ Err ( encode_subtree_application_failure( "boom" ) ) ,
2261+ & mut results,
2262+ )
2263+ . unwrap( ) ,
2264+ FailureIsolatedBodyOutcome :: ApplicationFailed
2265+ ) ;
2266+ }
2267+
2268+ #[ test]
2269+ fn isolated_body_propagates_unrecognized_runtime_failure ( ) {
2270+ let mut results = HashMap :: new ( ) ;
2271+
2272+ match classify_isolated_body_result ( Err ( "instance id collision" . to_string ( ) ) , & mut results)
2273+ {
2274+ Err ( NodeError :: Failure ( error) ) => assert_eq ! ( error, "instance id collision" ) ,
2275+ other => panic ! ( "expected runtime failure, got {other:?}" ) ,
2276+ }
2277+ }
2278+
2279+ #[ test]
2280+ fn isolated_body_preserves_break_value ( ) {
2281+ let raw = envelope_json ( Some ( "Break" ) , r#"{"status":"done"}"# , serde_json:: json!( { } ) ) ;
2282+ let mut results = HashMap :: new ( ) ;
2283+
21932284 assert_eq ! (
2194- classify_isolated_iteration_failure ( runtime_error . clone ( ) ) ,
2195- IsolatedIterationFailure :: Propagate ( runtime_error )
2285+ classify_isolated_body_result ( Ok ( raw ) , & mut results ) . unwrap ( ) ,
2286+ FailureIsolatedBodyOutcome :: Break ( r#"{"status":"done"}"# . to_string ( ) )
21962287 ) ;
21972288 }
21982289
0 commit comments