Skip to content

Commit f3ae3f8

Browse files
authored
fix(executor): stop parallel execution when block errors (#2940)
1 parent 66dfe2c commit f3ae3f8

File tree

6 files changed

+522
-56
lines changed

6 files changed

+522
-56
lines changed

apps/sim/executor/dag/builder.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,71 @@ function createBlock(id: string, metadataId: string): SerializedBlock {
2424
}
2525
}
2626

27+
describe('DAGBuilder disabled subflow validation', () => {
28+
it('skips validation for disabled loops with no blocks inside', () => {
29+
const workflow: SerializedWorkflow = {
30+
version: '1',
31+
blocks: [
32+
createBlock('start', BlockType.STARTER),
33+
{ ...createBlock('loop-block', BlockType.FUNCTION), enabled: false },
34+
],
35+
connections: [],
36+
loops: {
37+
'loop-1': {
38+
id: 'loop-1',
39+
nodes: [], // Empty loop - would normally throw
40+
iterations: 3,
41+
},
42+
},
43+
}
44+
45+
const builder = new DAGBuilder()
46+
// Should not throw even though loop has no blocks inside
47+
expect(() => builder.build(workflow)).not.toThrow()
48+
})
49+
50+
it('skips validation for disabled parallels with no blocks inside', () => {
51+
const workflow: SerializedWorkflow = {
52+
version: '1',
53+
blocks: [createBlock('start', BlockType.STARTER)],
54+
connections: [],
55+
loops: {},
56+
parallels: {
57+
'parallel-1': {
58+
id: 'parallel-1',
59+
nodes: [], // Empty parallel - would normally throw
60+
},
61+
},
62+
}
63+
64+
const builder = new DAGBuilder()
65+
// Should not throw even though parallel has no blocks inside
66+
expect(() => builder.build(workflow)).not.toThrow()
67+
})
68+
69+
it('skips validation for loops where all inner blocks are disabled', () => {
70+
const workflow: SerializedWorkflow = {
71+
version: '1',
72+
blocks: [
73+
createBlock('start', BlockType.STARTER),
74+
{ ...createBlock('inner-block', BlockType.FUNCTION), enabled: false },
75+
],
76+
connections: [],
77+
loops: {
78+
'loop-1': {
79+
id: 'loop-1',
80+
nodes: ['inner-block'], // Has node but it's disabled
81+
iterations: 3,
82+
},
83+
},
84+
}
85+
86+
const builder = new DAGBuilder()
87+
// Should not throw - loop is effectively disabled since all inner blocks are disabled
88+
expect(() => builder.build(workflow)).not.toThrow()
89+
})
90+
})
91+
2792
describe('DAGBuilder human-in-the-loop transformation', () => {
2893
it('creates trigger nodes and rewires edges for pause blocks', () => {
2994
const workflow: SerializedWorkflow = {

apps/sim/executor/dag/builder.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,18 @@ export class DAGBuilder {
136136
nodes: string[] | undefined,
137137
type: 'Loop' | 'Parallel'
138138
): void {
139+
const sentinelStartId =
140+
type === 'Loop' ? buildSentinelStartId(id) : buildParallelSentinelStartId(id)
141+
const sentinelStartNode = dag.nodes.get(sentinelStartId)
142+
143+
if (!sentinelStartNode) return
144+
139145
if (!nodes || nodes.length === 0) {
140146
throw new Error(
141147
`${type} has no blocks inside. Add at least one block to the ${type.toLowerCase()}.`
142148
)
143149
}
144150

145-
const sentinelStartId =
146-
type === 'Loop' ? buildSentinelStartId(id) : buildParallelSentinelStartId(id)
147-
const sentinelStartNode = dag.nodes.get(sentinelStartId)
148-
if (!sentinelStartNode) return
149-
150151
const hasConnections = Array.from(sentinelStartNode.outgoingEdges.values()).some((edge) =>
151152
nodes.includes(extractBaseBlockId(edge.target))
152153
)

0 commit comments

Comments
 (0)