Skip to content

Commit b1d9b73

Browse files
lourwFrozenPandaz
authored andcommitted
fix(core): prevent batch executor error on prematurely completed tasks (#35015)
## Current Behavior When a task is prematurely completed (e.g., due to a failure that causes early termination) before its dependents have been scheduled, calling `scheduleNextTasks` crashes the batch executor. The crash occurs in `processTaskForBatches`, which traverses reverse dependencies and attempts to read `notScheduledTaskGraph.dependencies[task.id]` for a task that was already removed from `notScheduledTaskGraph` via `complete()`. This yields `undefined` for the dependencies array, causing downstream code to throw. ## Expected Behavior When a task has been prematurely completed before batch scheduling runs, `processTaskForBatches` should skip that task gracefully rather than crashing. Tasks that were removed from `notScheduledTaskGraph` early should be detected and skipped during batch traversal. ## Related Issue(s) Fixes NXC-4144 (cherry picked from commit f003f56)
1 parent b611b98 commit b1d9b73

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

packages/nx/src/tasks-runner/tasks-schedule.spec.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,139 @@ describe('TasksSchedule', () => {
11151115
});
11161116
});
11171117

1118+
describe('batch scheduling with prematurely completed tasks', () => {
1119+
let taskSchedule: TasksSchedule;
1120+
let taskGraph: TaskGraph;
1121+
let lib1Build: Task;
1122+
let app1Build: Task;
1123+
let originalBatchMode: string | undefined;
1124+
1125+
beforeEach(async () => {
1126+
originalBatchMode = process.env['NX_BATCH_MODE'];
1127+
process.env['NX_BATCH_MODE'] = 'true';
1128+
1129+
lib1Build = createMockTask('lib1:build');
1130+
app1Build = createMockTask('app1:build');
1131+
const app2Build = createMockTask('app2:build');
1132+
1133+
taskGraph = {
1134+
tasks: {
1135+
'lib1:build': lib1Build,
1136+
'app1:build': app1Build,
1137+
'app2:build': app2Build,
1138+
},
1139+
dependencies: {
1140+
'lib1:build': [],
1141+
'app1:build': ['lib1:build'],
1142+
'app2:build': ['lib1:build'],
1143+
},
1144+
continuousDependencies: {
1145+
'lib1:build': [],
1146+
'app1:build': [],
1147+
'app2:build': [],
1148+
},
1149+
roots: ['lib1:build'],
1150+
};
1151+
1152+
jest.spyOn(nxJsonUtils, 'readNxJson').mockReturnValue({});
1153+
jest.spyOn(executorUtils, 'getExecutorInformation').mockReturnValue({
1154+
schema: {
1155+
version: 2,
1156+
properties: {},
1157+
},
1158+
implementationFactory: jest.fn(),
1159+
batchImplementationFactory: jest.fn(),
1160+
isNgCompat: true,
1161+
isNxExecutor: true,
1162+
});
1163+
1164+
const projectGraph: ProjectGraph = {
1165+
nodes: {
1166+
lib1: {
1167+
name: 'lib1',
1168+
type: 'lib',
1169+
data: {
1170+
root: 'lib1',
1171+
targets: {
1172+
build: {
1173+
executor: 'awesome-executors:build',
1174+
},
1175+
},
1176+
},
1177+
},
1178+
app1: {
1179+
name: 'app1',
1180+
type: 'app',
1181+
data: {
1182+
root: 'app1',
1183+
targets: {
1184+
build: {
1185+
executor: 'awesome-executors:build',
1186+
},
1187+
},
1188+
},
1189+
},
1190+
app2: {
1191+
name: 'app2',
1192+
type: 'app',
1193+
data: {
1194+
root: 'app2',
1195+
targets: {
1196+
build: {
1197+
executor: 'awesome-executors:build',
1198+
},
1199+
},
1200+
},
1201+
},
1202+
} as any,
1203+
dependencies: {
1204+
lib1: [],
1205+
app1: [
1206+
{
1207+
source: 'app1',
1208+
target: 'lib1',
1209+
type: DependencyType.static,
1210+
},
1211+
],
1212+
app2: [
1213+
{
1214+
source: 'app2',
1215+
target: 'lib1',
1216+
type: DependencyType.static,
1217+
},
1218+
],
1219+
},
1220+
externalNodes: {},
1221+
version: '5',
1222+
};
1223+
1224+
taskHistory.getEstimatedTaskTimings.mockReturnValue({});
1225+
taskSchedule = new TasksSchedule(projectGraph, taskGraph, {
1226+
lifeCycle,
1227+
});
1228+
await taskSchedule.init();
1229+
});
1230+
1231+
afterEach(() => {
1232+
process.env['NX_BATCH_MODE'] = originalBatchMode;
1233+
});
1234+
1235+
it('should not crash when a dependent task was prematurely completed before batch scheduling', async () => {
1236+
// Simulate a premature task failure: app1:build is completed
1237+
// before it or its dependency lib1:build are scheduled.
1238+
// This removes app1 from notScheduledTaskGraph.
1239+
taskSchedule.complete(['app1:build']);
1240+
1241+
await taskSchedule.scheduleNextTasks();
1242+
1243+
const batch = taskSchedule.nextBatch();
1244+
expect(batch).not.toBeNull();
1245+
expect(batch.taskGraph.tasks).not.toHaveProperty('app1:build');
1246+
expect(batch.taskGraph.tasks).toHaveProperty('lib1:build');
1247+
expect(batch.taskGraph.tasks).toHaveProperty('app2:build');
1248+
});
1249+
});
1250+
11181251
describe('nextTask with filter', () => {
11191252
let taskSchedule: TasksSchedule;
11201253
let discreteTask: Task;

packages/nx/src/tasks-runner/tasks-schedule.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export class TasksSchedule {
7575
for (const taskId of taskIds) {
7676
this.completedTasks.add(taskId);
7777
this.runningTasks.delete(taskId);
78+
delete this.reverseTaskDeps[taskId];
79+
}
80+
const removedSet = new Set(taskIds);
81+
for (const [key, deps] of Object.entries(this.reverseTaskDeps)) {
82+
this.reverseTaskDeps[key] = deps.filter((d) => !removedSet.has(d));
7883
}
7984
this.notScheduledTaskGraph = removeTasksFromTaskGraph(
8085
this.notScheduledTaskGraph,

0 commit comments

Comments
 (0)