Skip to content

Commit 027000d

Browse files
authored
Merge pull request #366 from hjotha/submit/custom-error-handler-routing
fix: preserve custom error handler continuations
2 parents e2d44d1 + a8a6b04 commit 027000d

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
type flowBuilderVariableState struct {
@@ -100,6 +116,10 @@ func (fb *flowBuilder) GetErrors() []string {
100116
return fb.errors
101117
}
102118

119+
func (fb *flowBuilder) hasDeclaredReturnValue() bool {
120+
return fb.returnType != nil && fb.returnType.Type.Kind != ast.TypeVoid
121+
}
122+
103123
// errorExampleDeclareVariable returns an example for declaring a variable.
104124
func errorExampleDeclareVariable(varName string) string {
105125
// 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
@@ -159,12 +159,7 @@ func (fb *flowBuilder) addCallMicroflowAction(s *ast.CallMicroflowStmt) model.ID
159159
fb.registerResultVariableType(s.OutputVariable, fb.lookupMicroflowReturnType(mfQN))
160160
}
161161

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

169164
return activity.ID
170165
}
@@ -219,12 +214,7 @@ func (fb *flowBuilder) addCallNanoflowAction(s *ast.CallNanoflowStmt) model.ID {
219214
fb.registerResultVariableType(s.OutputVariable, fb.lookupNanoflowReturnType(nfQN))
220215
}
221216

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

229219
return activity.ID
230220
}
@@ -348,12 +338,7 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
348338
fb.objects = append(fb.objects, activity)
349339
fb.posX += fb.spacing
350340

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

358343
return activity.ID
359344
}
@@ -450,12 +435,7 @@ func (fb *flowBuilder) addCallJavaScriptActionAction(s *ast.CallJavaScriptAction
450435
fb.objects = append(fb.objects, activity)
451436
fb.posX += fb.spacing
452437

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

460440
return activity.ID
461441
}
@@ -586,12 +566,7 @@ func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt
586566
fb.objects = append(fb.objects, activity)
587567
fb.posX += fb.spacing
588568

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

596571
return activity.ID
597572
}
@@ -1108,12 +1083,7 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID {
11081083
fb.objects = append(fb.objects, activity)
11091084
fb.posX += fb.spacing
11101085

1111-
// Build custom error handler flow if present
1112-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1113-
errorY := fb.posY + VerticalSpacing
1114-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1115-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1116-
}
1086+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
11171087

11181088
return activity.ID
11191089
}
@@ -1317,12 +1287,7 @@ func (fb *flowBuilder) addExecuteDatabaseQueryAction(s *ast.ExecuteDatabaseQuery
13171287
fb.objects = append(fb.objects, activity)
13181288
fb.posX += fb.spacing
13191289

1320-
// Build custom error handler flow if present
1321-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1322-
errorY := fb.posY + VerticalSpacing
1323-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1324-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1325-
}
1290+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
13261291

13271292
return activity.ID
13281293
}
@@ -1389,11 +1354,7 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt)
13891354
}
13901355
}
13911356

1392-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1393-
errorY := fb.posY + VerticalSpacing
1394-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1395-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1396-
}
1357+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, s.OutputVariable)
13971358

13981359
return activity.ID
13991360
}
@@ -1425,11 +1386,7 @@ func (fb *flowBuilder) addTransformJsonAction(s *ast.TransformJsonStmt) model.ID
14251386
fb.objects = append(fb.objects, activity)
14261387
fb.posX += fb.spacing
14271388

1428-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1429-
errorY := fb.posY + VerticalSpacing
1430-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1431-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1432-
}
1389+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")
14331390

14341391
return activity.ID
14351392
}
@@ -1463,11 +1420,7 @@ func (fb *flowBuilder) addExportToMappingAction(s *ast.ExportToMappingStmt) mode
14631420
fb.objects = append(fb.objects, activity)
14641421
fb.posX += fb.spacing
14651422

1466-
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
1467-
errorY := fb.posY + VerticalSpacing
1468-
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
1469-
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
1470-
}
1423+
fb.finishCustomErrorHandler(activity.ID, activityX, s.ErrorHandling, "")
14711424

14721425
return activity.ID
14731426
}

0 commit comments

Comments
 (0)