Skip to content

Commit 2807335

Browse files
committed
fix status of execution with for-each and if-then
1 parent 44d6f66 commit 2807335

File tree

5 files changed

+110
-15
lines changed

5 files changed

+110
-15
lines changed

packages/backend/src/helpers/compute-for-each-parameters.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ export function computeForEachParameters({
103103
}
104104

105105
if (
106-
executionStep.appKey === TOOLBOX_APP_KEY &&
107-
executionStep.key === TOOLBOX_ACTIONS.FOR_EACH
106+
executionStep?.appKey === TOOLBOX_APP_KEY &&
107+
executionStep?.key === TOOLBOX_ACTIONS.FOR_EACH
108108
) {
109109
dataValue = get(data, forEachKeyPath as string) || ''
110110
} else if (currentStepIndex > forEachStepIndex) {

packages/backend/src/models/execution-step.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { IExecutionStepMetadata, IJSONObject } from '@plumber/types'
22

3+
import { raw } from 'objection'
4+
35
import appConfig from '@/config/app'
46

57
import Base from './base'
@@ -81,6 +83,68 @@ class ExecutionStep extends Base {
8183

8284
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`
8385
}
86+
87+
static async getForEachExecutionSteps(executionId: string) {
88+
return ExecutionStep.query()
89+
.with('latest_steps', (builder) => {
90+
/**
91+
* NOTE: there is a known issue with knex where 'groupBy' are placed at the end of the 'unionAll' query.
92+
* the workaround is to unionAll both queries with 'true' to wrap the subequery.
93+
*/
94+
builder
95+
.unionAll((qb) => {
96+
qb.select(
97+
'step_id',
98+
raw('max(created_at) as max_created_at'),
99+
raw('min(created_at) as min_created_at'),
100+
)
101+
.from('execution_steps')
102+
.groupBy('step_id')
103+
.where('execution_id', '=', executionId)
104+
.where(raw("metadata = '{}'::jsonb"))
105+
.withSoftDeleted()
106+
}, true)
107+
.unionAll((qb) => {
108+
qb.select(
109+
'step_id',
110+
raw('max(created_at) as max_created_at'),
111+
raw('min(created_at) as min_created_at'),
112+
)
113+
.from('execution_steps')
114+
.groupBy('step_id', raw("metadata->>'iteration'"))
115+
.where('execution_id', '=', executionId)
116+
.where(raw("metadata != '{}'::jsonb"))
117+
.withSoftDeleted()
118+
}, true)
119+
.withSoftDeleted()
120+
})
121+
.join('latest_steps', (builder) => {
122+
builder
123+
.on('execution_steps.step_id', '=', 'latest_steps.step_id')
124+
.andOn(
125+
'execution_steps.created_at',
126+
'=',
127+
'latest_steps.max_created_at',
128+
)
129+
})
130+
.select('execution_steps.*', 'min_created_at')
131+
.withSoftDeleted()
132+
.orderBy('min_created_at', 'asc')
133+
}
134+
135+
static async getForEachExecutionState(executionId: string) {
136+
const executionSteps = await ExecutionStep.getForEachExecutionSteps(
137+
executionId,
138+
)
139+
return {
140+
hasLastIterationRun: executionSteps.some(
141+
(step) => step.metadata?.isLastIteration && step.metadata?.isLastStep,
142+
),
143+
areAllStepsSuccessful: executionSteps.every(
144+
(step) => step.status === 'success',
145+
),
146+
}
147+
}
84148
}
85149

86150
export default ExecutionStep

packages/backend/src/services/action.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,20 @@ export const processAction = async (options: ProcessActionOptions) => {
171171
? 'success'
172172
: 'failure'
173173

174+
/**
175+
* FOR-EACH + IF-THEN SPECIAL CASE
176+
* when there are if-then actions in the for-each, not all steps may run so the lastStep check may not work
177+
* if-then uses stop-execution to terminate the flow, so we need to set the isLastStep to true
178+
* so that the execution status is set to success
179+
*/
180+
if (
181+
!testRun &&
182+
forEachStepIndex > -1 &&
183+
runResult.nextStep?.command === 'stop-execution'
184+
) {
185+
metadata.isLastStep = true
186+
}
187+
174188
const executionStep = await execution
175189
.$relatedQuery('executionSteps')
176190
.insertAndFetch({

packages/backend/src/workers/helpers/make-action-worker.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -168,35 +168,50 @@ export function makeActionWorker(
168168

169169
/**
170170
* FOR-EACH SPECIAL CASE
171-
* 1. nextStep is null for the for-each execution step, return if its not the last iteration
172-
* so that we do not prematurely set the execution status to success and it can be
173-
* reflected accurately as waiting
174-
* 2. if any iteration fails, the execution is set to failed
175-
* 3. if all iterations are successful, the execution is set to success
171+
* nextStep is null for the for-each execution step, return if its not the last iteration
172+
* so that we do not prematurely set the execution status to success and it can be
173+
* reflected accurately as waiting
176174
*/
177175
if (!nextStep && isForEach) {
178176
return
179177
}
180178

181179
if (!nextStep) {
180+
/**
181+
* FOR-EACH SPECIAL CASE
182+
* default state is null (waiting for all iterations to execute)
183+
* if any iteration fails, the execution is immediately set to failure
184+
* if all iterations are successful, the execution is set to success
185+
*/
182186
if (nextStepMetadata?.iteration) {
187+
const { hasLastIterationRun, areAllStepsSuccessful } =
188+
await ExecutionStep.getForEachExecutionState(executionId)
189+
190+
// end of the execution: all iterations ran successfully up to the last iteration and last step
183191
if (
184192
nextStepMetadata?.isLastIteration &&
185193
nextStepMetadata?.isLastStep
186194
) {
187-
const executionSteps = await ExecutionStep.query().where({
188-
execution_id: executionId,
189-
})
190-
191-
const areAllStepsSuccessful = executionSteps.every(
192-
(step) => step.status === 'success',
193-
)
194195
if (!areAllStepsSuccessful) {
195196
await Execution.setStatus(executionId, 'failure')
196197
return
197198
}
198199
} else {
199-
return
200+
// handle failures and retries
201+
if (nextStepMetadata?.isLastStep) {
202+
if (!areAllStepsSuccessful) {
203+
await Execution.setStatus(executionId, 'failure')
204+
return
205+
}
206+
207+
// if the last iteration has not run, it means this flow is still executing
208+
// we return early to preserve the waiting state of the execution
209+
if (!hasLastIterationRun) {
210+
return
211+
}
212+
} else {
213+
return
214+
}
200215
}
201216
}
202217

packages/types/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export interface IExecutionStep {
127127
export interface IExecutionStepMetadata {
128128
isMock?: boolean
129129
iteration?: number
130+
isLastIteration?: boolean
131+
isLastStep?: boolean
130132
}
131133

132134
export interface IExecution {

0 commit comments

Comments
 (0)