Skip to content

Commit 4deb554

Browse files
committed
Merge remote-tracking branch 'fork/submit/custom-error-handler-routing' into HEAD
2 parents f2eaedb + a8a6b04 commit 4deb554

16 files changed

Lines changed: 1582 additions & 130 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
-- ============================================================================
2+
-- Bug #349: Custom error-handler routing during microflow roundtrip
3+
-- ============================================================================
4+
--
5+
-- Symptom (before fix):
6+
-- Custom error-handler bodies that did NOT explicitly return were
7+
-- rebuilt as DETACHED terminal paths — they got their own EndEvent and
8+
-- never rejoined the next activity. Empty custom handlers could either
9+
-- lose their error flow entirely or rejoin into statements that read
10+
-- an output variable absent on the error path.
11+
--
12+
-- Root cause:
13+
-- The microflow builder handled custom error handlers immediately at
14+
-- the source activity. It did not retain pending handler state until
15+
-- the next safe continuation was known, so a later handler could
16+
-- overwrite an earlier pending handler.
17+
--
18+
-- After fix:
19+
-- - Pending custom-handler state is queued.
20+
-- - Non-terminal handler bodies rejoin through a merge before the
21+
-- next safe continuation, instead of fabricating detached EndEvents.
22+
-- - Empty custom-handler error flows are preserved.
23+
-- - Output-producing handlers terminate before output-dependent
24+
-- continuation in void microflows.
25+
--
26+
-- Usage:
27+
-- mxcli exec mdl-examples/bug-tests/349-custom-error-handler-routing.mdl -p app.mpr
28+
-- mxcli -p app.mpr -c "describe microflow BugTest349.MF_Caller"
29+
-- `mx check` against the resulting MPR must report 0 errors and the
30+
-- non-terminal handler must rejoin the continuation activity.
31+
-- ============================================================================
32+
33+
create module BugTest349;
34+
35+
create microflow BugTest349.MF_RefreshData (
36+
$Token: string
37+
)
38+
begin
39+
log info node 'BugTest349' 'refreshed: ' + $Token;
40+
end;
41+
/
42+
43+
create microflow BugTest349.MF_NextBatch ()
44+
begin
45+
log info node 'BugTest349' 'next batch';
46+
end;
47+
/
48+
49+
-- Caller with non-terminal custom error handler. The handler body logs
50+
-- but does not return, so its tail must rejoin the continuation
51+
-- (`call microflow ... MF_NextBatch ()`) instead of becoming a detached
52+
-- terminal path.
53+
create microflow BugTest349.MF_Caller (
54+
$Token: string
55+
)
56+
begin
57+
call microflow BugTest349.MF_RefreshData (Token = $Token)
58+
on error without rollback {
59+
log error node 'BugTest349' 'refresh failed';
60+
};
61+
62+
call microflow BugTest349.MF_NextBatch ();
63+
end;
64+
/

mdl/ast/ast_microflow.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ type IfStmt struct {
267267
Condition Expression // IF condition
268268
ThenBody []MicroflowStatement // THEN branch
269269
ElseBody []MicroflowStatement // ELSE branch (optional)
270+
HasElse bool // true when the source contained ELSE, even if the body is empty
270271
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
271272
}
272273

mdl/executor/cmd_diff_mdl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ func microflowStatementToMDL(ctx *ExecContext, stmt ast.MicroflowStatement, inde
423423
for _, thenStmt := range s.ThenBody {
424424
lines = append(lines, microflowStatementToMDL(ctx, thenStmt, indent+1)...)
425425
}
426-
if len(s.ElseBody) > 0 {
426+
if s.HasElse || len(s.ElseBody) > 0 {
427427
lines = append(lines, indentStr+"else")
428428
for _, elseStmt := range s.ElseBody {
429429
lines = append(lines, microflowStatementToMDL(ctx, elseStmt, indent+1)...)

mdl/executor/cmd_microflows_builder.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ type flowBuilder struct {
2323
posY int
2424
baseY int // Base Y position (for returning after ELSE branches)
2525
spacing int
26-
returnValue string // Return value expression for RETURN statement (used by buildFlowGraph final EndEvent)
26+
returnValue string // Return value expression for RETURN statement (used by buildFlowGraph final EndEvent)
27+
returnType *ast.MicroflowReturnType
2728
endsWithReturn bool // True if the flow already ends with EndEvent(s) from RETURN statements
29+
lastReturnEndID model.ID // Last explicit RETURN EndEvent, used as a fallback error-handler target
2830
varTypes map[string]string // Variable name -> entity qualified name (for CHANGE statements)
2931
declaredVars map[string]string // Declared primitive variables: name -> type (e.g., "$IsValid" -> "Boolean")
3032
errors []string // Validation errors collected during build
@@ -55,6 +57,20 @@ type flowBuilder struct {
5557
nanoflowsCacheLoaded bool
5658
manualLoopBackTarget model.ID
5759
isNanoflow bool // true when building a nanoflow — default error handling is "" not "Rollback"
60+
// Pending custom error-handler routing uses two representations: the
61+
// currently active handler lives in the flat fields below, while handlers
62+
// postponed across branch boundaries are queued in pendingErrorHandlers.
63+
// Mutate this state through the helper methods in builder_flows.go so the
64+
// active/queued invariant stays synchronized.
65+
emptyErrorHandlerFrom model.ID
66+
errorHandlerTailFrom model.ID
67+
errorHandlerSource model.ID
68+
errorHandlerSkipVar string
69+
errorHandlerTailCase string
70+
errorHandlerTailAnchor *ast.FlowAnchors
71+
errorHandlerTailIsSource bool
72+
errorHandlerReturnValue string
73+
pendingErrorHandlers []pendingErrorHandlerState
5874
}
5975

6076
// addError records a validation error during flow building.
@@ -72,6 +88,10 @@ func (fb *flowBuilder) GetErrors() []string {
7288
return fb.errors
7389
}
7490

91+
func (fb *flowBuilder) hasDeclaredReturnValue() bool {
92+
return fb.returnType != nil && fb.returnType.Type.Kind != ast.TypeVoid
93+
}
94+
7595
// errorExampleDeclareVariable returns an example for declaring a variable.
7696
func errorExampleDeclareVariable(varName string) string {
7797
// Remove $ prefix if present for cleaner display

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,7 @@ func (fb *flowBuilder) addCreateObjectAction(s *ast.CreateObjectStmt) model.ID {
134134
fb.objects = append(fb.objects, activity)
135135
fb.posX += fb.spacing
136136

137-
// Build custom error handler flow if present
138-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
139-
errorY := fb.posY + VerticalSpacing
140-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
141-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
142-
}
137+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.Variable)
143138

144139
return activity.ID
145140
}
@@ -170,12 +165,7 @@ func (fb *flowBuilder) addCommitAction(s *ast.MfCommitStmt) model.ID {
170165
fb.objects = append(fb.objects, activity)
171166
fb.posX += fb.spacing
172167

173-
// Build custom error handler flow if present
174-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
175-
errorY := fb.posY + VerticalSpacing
176-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
177-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
178-
}
168+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")
179169

180170
return activity.ID
181171
}
@@ -204,12 +194,7 @@ func (fb *flowBuilder) addDeleteAction(s *ast.DeleteObjectStmt) model.ID {
204194
fb.objects = append(fb.objects, activity)
205195
fb.posX += fb.spacing
206196

207-
// Build custom error handler flow if present
208-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
209-
errorY := fb.posY + VerticalSpacing
210-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
211-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
212-
}
197+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")
213198

214199
return activity.ID
215200
}
@@ -684,12 +669,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID {
684669
fb.objects = append(fb.objects, activity)
685670
fb.posX += fb.spacing
686671

687-
// Build custom error handler flow if present
688-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
689-
errorY := fb.posY + VerticalSpacing
690-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
691-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
692-
}
672+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.Variable)
693673

694674
return activity.ID
695675
}

mdl/executor/cmd_microflows_builder_annotations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ func (fb *flowBuilder) addEndEventWithReturn(s *ast.ReturnStmt) model.ID {
229229

230230
fb.objects = append(fb.objects, endEvent)
231231
fb.endsWithReturn = true
232+
fb.lastReturnEndID = endEvent.ID
232233
fb.posX += fb.spacing / 2
233234
return endEvent.ID
234235
}

mdl/executor/cmd_microflows_builder_calls.go

Lines changed: 10 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,7 @@ func (fb *flowBuilder) addCallMicroflowAction(s *ast.CallMicroflowStmt) model.ID
160160
fb.registerResultVariableType(s.OutputVariable, fb.lookupMicroflowReturnType(mfQN))
161161
}
162162

163-
// Build custom error handler flow if present
164-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
165-
errorY := fb.posY + VerticalSpacing
166-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
167-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
168-
}
163+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
169164

170165
return activity.ID
171166
}
@@ -220,12 +215,7 @@ func (fb *flowBuilder) addCallNanoflowAction(s *ast.CallNanoflowStmt) model.ID {
220215
fb.registerResultVariableType(s.OutputVariable, fb.lookupNanoflowReturnType(nfQN))
221216
}
222217

223-
// Build custom error handler flow if present
224-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
225-
errorY := fb.posY + VerticalSpacing
226-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
227-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
228-
}
218+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
229219

230220
return activity.ID
231221
}
@@ -349,12 +339,7 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
349339
fb.objects = append(fb.objects, activity)
350340
fb.posX += fb.spacing
351341

352-
// Build custom error handler flow if present
353-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
354-
errorY := fb.posY + VerticalSpacing
355-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
356-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
357-
}
342+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
358343

359344
return activity.ID
360345
}
@@ -451,12 +436,7 @@ func (fb *flowBuilder) addCallJavaScriptActionAction(s *ast.CallJavaScriptAction
451436
fb.objects = append(fb.objects, activity)
452437
fb.posX += fb.spacing
453438

454-
// Build custom error handler flow if present
455-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
456-
errorY := fb.posY + VerticalSpacing
457-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
458-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
459-
}
439+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
460440

461441
return activity.ID
462442
}
@@ -587,12 +567,7 @@ func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt
587567
fb.objects = append(fb.objects, activity)
588568
fb.posX += fb.spacing
589569

590-
// Build custom error handler flow if present
591-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
592-
errorY := fb.posY + VerticalSpacing
593-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
594-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
595-
}
570+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
596571

597572
return activity.ID
598573
}
@@ -1093,12 +1068,7 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID {
10931068
fb.objects = append(fb.objects, activity)
10941069
fb.posX += fb.spacing
10951070

1096-
// Build custom error handler flow if present
1097-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1098-
errorY := fb.posY + VerticalSpacing
1099-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1100-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1101-
}
1071+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
11021072

11031073
return activity.ID
11041074
}
@@ -1302,12 +1272,7 @@ func (fb *flowBuilder) addExecuteDatabaseQueryAction(s *ast.ExecuteDatabaseQuery
13021272
fb.objects = append(fb.objects, activity)
13031273
fb.posX += fb.spacing
13041274

1305-
// Build custom error handler flow if present
1306-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1307-
errorY := fb.posY + VerticalSpacing
1308-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1309-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1310-
}
1275+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
13111276

13121277
return activity.ID
13131278
}
@@ -1374,11 +1339,7 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt)
13741339
}
13751340
}
13761341

1377-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1378-
errorY := fb.posY + VerticalSpacing
1379-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1380-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1381-
}
1342+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
13821343

13831344
return activity.ID
13841345
}
@@ -1410,11 +1371,7 @@ func (fb *flowBuilder) addTransformJsonAction(s *ast.TransformJsonStmt) model.ID
14101371
fb.objects = append(fb.objects, activity)
14111372
fb.posX += fb.spacing
14121373

1413-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1414-
errorY := fb.posY + VerticalSpacing
1415-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1416-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1417-
}
1374+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")
14181375

14191376
return activity.ID
14201377
}
@@ -1448,11 +1405,7 @@ func (fb *flowBuilder) addExportToMappingAction(s *ast.ExportToMappingStmt) mode
14481405
fb.objects = append(fb.objects, activity)
14491406
fb.posX += fb.spacing
14501407

1451-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1452-
errorY := fb.posY + VerticalSpacing
1453-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1454-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1455-
}
1408+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")
14561409

14571410
return activity.ID
14581411
}

0 commit comments

Comments
 (0)