fix(core): Ensure running job cleanup when a workflow run rejects (#37621)

Co-authored-by: Claude Fable 5 <[email protected]>
This commit is contained in:
Mike Repeć
2026-09-02 09:49:33 +00:00
committed by GitHub
co-authored by Claude Fable 5
parent 5aeb96cdb9
commit 51ac147cd0
4 changed files with 67 additions and 4 deletions
@@ -217,6 +217,45 @@ describe('JobProcessor', () => {
},
);
it('should remove the running job entry when the workflow run rejects', async () => {
const executionPersistence = mock<ExecutionPersistence>();
executionPersistence.findSingleExecution.mockResolvedValue(
mock<IExecutionResponse>({
mode: 'manual',
workflowData: { nodes: [], staticData: {} },
data: mock<IRunExecutionData>({
executionData: undefined,
}),
}),
);
const manualExecutionService = mock<ManualExecutionService>();
manualExecutionService.runManually.mockReturnValue(
Promise.reject(new Error('workflow run rejected')) as ReturnType<
ManualExecutionService['runManually']
>,
);
const jobProcessor = new JobProcessor(
logger,
mock<ExecutionRepository>(),
executionPersistence,
mock(),
mock(),
mock(),
manualExecutionService,
executionsConfig,
mock(),
mock(),
);
const job = mock<Job>({ id: 'job-1' });
await expect(jobProcessor.processJob(job)).rejects.toThrow('workflow run rejected');
expect(jobProcessor.getRunningJobIds()).toEqual([]);
});
it('should send job-finished with success=false when execution has errors', async () => {
const executionRepository = mock<ExecutionRepository>();
const executionPersistence = mock<ExecutionPersistence>();
@@ -258,6 +258,22 @@ describe('ScalingService', () => {
expect(queue.pause).toHaveBeenCalled();
expect(stopQueueRecoverySpy).not.toHaveBeenCalled();
});
it('should log the execution IDs it is waiting for while draining', async () => {
// @ts-expect-error readonly property
instanceSettings.instanceType = 'worker';
await scalingService.setupQueue();
jobProcessor.getRunningJobIds.mockReturnValueOnce(['1']).mockReturnValue([]);
jobProcessor.getRunningJobsSummary.mockReturnValue([mock({ executionId: 'exec-1' })]);
await scalingService.stop();
// @ts-expect-error private property
expect(scalingService.logger.info).toHaveBeenCalledWith(
'Waiting for 1 active executions to finish... (execution IDs: exec-1)',
{ executionIds: ['exec-1'] },
);
});
});
});
+8 -3
View File
@@ -329,9 +329,14 @@ export class JobProcessor {
this.runningJobs[job.id] = runningJob;
const run = await workflowRun;
delete this.runningJobs[job.id];
let run: IRun;
try {
run = await workflowRun;
} finally {
// An entry left behind on rejection keeps the count of running jobs
// above zero forever, which prevents shutdown from ever completing.
delete this.runningJobs[job.id];
}
if (run?.status === 'canceled') {
throw new ManualExecutionCancelledError(executionId);
+4 -1
View File
@@ -186,8 +186,11 @@ export class ScalingService {
while (this.getRunningJobsCount() !== 0) {
if (count++ % 4 === 0) {
const summaries = this.jobProcessor.getRunningJobsSummary();
const executionIds = summaries.map((summary) => summary.executionId);
this.logger.info(
`Waiting for ${this.getRunningJobsCount()} active executions to finish...`,
`Waiting for ${executionIds.length} active executions to finish... (execution IDs: ${executionIds.join(', ')})`,
{ executionIds },
);
}