fix(core): Suspend jobs that register after worker shutdown asked for suspension
Some checks failed
CI: Python / Checks (push) Has been cancelled

This commit is contained in:
Mike Repeć 2026-10-04 11:50:16 +02:00
parent f7d4c24840
commit 9f5a71e137
No known key found for this signature in database
2 changed files with 51 additions and 0 deletions

View File

@ -327,6 +327,50 @@ describe('JobProcessor', () => {
await processPromise;
});
it('should suspend a suspendable job that registers after suspension was requested', async () => {
const executionPersistence = mock<ExecutionPersistence>();
let resolveExecution!: (execution: IExecutionResponse) => void;
executionPersistence.findSingleExecution.mockReturnValue(
new Promise<IExecutionResponse>((r) => (resolveExecution = r)),
);
vi.spyOn(WorkflowExecuteAdditionalData, 'getBase').mockResolvedValue(
mock<IWorkflowExecuteAdditionalData>(),
);
const jobProcessor = createJobProcessor(executionPersistence);
let resolveRun!: (run: IRun) => void;
processRunExecutionDataMock.mockReturnValue(new Promise<IRun>((r) => (resolveRun = r)));
const job = mock<Job>({
id: 'job-1',
data: {
executionId: 'exec-1',
loadStaticData: false,
streamingEnabled: false,
isMcpExecution: false,
},
});
const processPromise = jobProcessor.processJob(job);
// Shutdown arrives while the job is still in its preflight reads.
jobProcessor.suspendRunningJobs();
expect(workflowExecuteSuspendMock).not.toHaveBeenCalled();
resolveExecution(
mock<IExecutionResponse>({
mode: 'webhook',
workflowData: { nodes: [], staticData: {} },
data: mock<IRunExecutionData>(),
}),
);
await vi.waitFor(() => expect(jobProcessor.getRunningJobIds()).toEqual(['job-1']));
expect(workflowExecuteSuspendMock).toHaveBeenCalledTimes(1);
resolveRun(successRun());
await processPromise;
});
it('should not attach a suspend handle to a non-suspendable job', async () => {
const executionPersistence = mock<ExecutionPersistence>();
executionPersistence.findSingleExecution.mockResolvedValue(

View File

@ -369,6 +369,9 @@ export class JobProcessor {
if (workflowExecute && this.isJobSuspendable(job, execution)) {
const suspendable = workflowExecute;
runningJob.suspend = () => suspendable.suspend();
// A job that was still in its preflight reads when shutdown asked the
// running jobs to suspend would otherwise run to completion unasked.
if (this.suspensionRequested) runningJob.suspend();
}
this.runningJobs[job.id] = runningJob;
@ -589,6 +592,9 @@ export class JobProcessor {
* streaming responses cannot migrate mid-stream, and MCP executions are
* pinned to their session.
*/
/** Set once at shutdown; jobs that register afterwards suspend right away. */
private suspensionRequested = false;
private isJobSuspendable(job: Job, execution: IExecutionResponse): boolean {
const isProductionMode = ['webhook', 'trigger', 'retry'].includes(execution.mode);
@ -602,6 +608,7 @@ export class JobProcessor {
/** Ask every suspendable running job to stop at its next node boundary. */
suspendRunningJobs() {
this.suspensionRequested = true;
const executionIds: string[] = [];
for (const runningJob of Object.values(this.runningJobs)) {