nx 23.3.0 → 24.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -6
- package/dist/nx-cloud-internals.d.ts +1 -0
- package/dist/nx-cloud-internals.js +7 -1
- package/dist/src/adapter/ngcli-adapter.js +1 -0
- package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +0 -12
- package/dist/src/command-line/graph/graph.js +3 -5
- package/dist/src/command-line/init/command-object.js +0 -12
- package/dist/src/command-line/init/init-v2.js +0 -2
- package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +1 -1
- package/dist/src/command-line/migrate/agentic/close-agent-session.js +4 -3
- package/dist/src/command-line/migrate/agentic/master/spawn-master.js +76 -15
- package/dist/src/command-line/migrate/execute-migration.d.ts +2 -2
- package/dist/src/command-line/migrate/execute-migration.js +18 -10
- package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -1
- package/dist/src/command-line/migrate/migrate-commits.js +10 -3
- package/dist/src/command-line/migrate/migrate.js +43 -6
- package/dist/src/command-line/migrate/run/agent-output.d.ts +1 -0
- package/dist/src/command-line/migrate/run/agent-output.js +5 -0
- package/dist/src/command-line/migrate/run/broker.d.ts +14 -5
- package/dist/src/command-line/migrate/run/broker.js +68 -20
- package/dist/src/command-line/migrate/run/index.d.ts +4 -3
- package/dist/src/command-line/migrate/run/index.js +8 -1
- package/dist/src/command-line/migrate/run/orchestrator.js +72 -13
- package/dist/src/command-line/migrate/run/runbook.js +19 -0
- package/dist/src/command-line/migrate/run/state-machine.js +4 -1
- package/dist/src/command-line/migrate/run/util.d.ts +3 -0
- package/dist/src/command-line/migrate/run/util.js +7 -1
- package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +17 -12
- package/dist/src/command-line/release/utils/remote-release-clients/github.js +5 -6
- package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +5 -6
- package/dist/src/command-line/run/executor-utils.js +1 -0
- package/dist/src/command-line/run/run-one.d.ts +20 -1
- package/dist/src/command-line/run/run-one.js +153 -5
- package/dist/src/command-line/show/show-target/info.js +1 -1
- package/dist/src/command-line/show/show-target/utils.js +1 -0
- package/dist/src/command-line/watch/command-object.js +3 -22
- package/dist/src/command-line/watch/watch.d.ts +0 -7
- package/dist/src/command-line/watch/watch.js +1 -1
- package/dist/src/config/misc-interfaces.d.ts +12 -0
- package/dist/src/config/task-graph.d.ts +1 -1
- package/dist/src/config/workspace-json-project-json.d.ts +65 -0
- package/dist/src/core/graph/main.js +1 -1
- package/dist/src/daemon/client/client.d.ts +9 -0
- package/dist/src/daemon/client/client.js +89 -37
- package/dist/src/daemon/server/shutdown-utils.js +0 -3
- package/dist/src/daemon/socket-utils.d.ts +9 -4
- package/dist/src/daemon/socket-utils.js +12 -49
- package/dist/src/daemon/tmp-dir.d.ts +19 -14
- package/dist/src/daemon/tmp-dir.js +142 -354
- package/dist/src/devkit-exports.d.ts +1 -1
- package/dist/src/devkit-internals.d.ts +1 -1
- package/dist/src/devkit-internals.js +1 -0
- package/dist/src/hasher/check-task-files.js +4 -4
- package/dist/src/hasher/file-hasher.js +9 -7
- package/dist/src/hasher/hash-task.js +4 -0
- package/dist/src/hasher/native-task-hasher-impl.js +3 -1
- package/dist/src/migrations/update-24-0-0/find-custom-hashers.d.ts +3 -0
- package/dist/src/migrations/update-24-0-0/find-custom-hashers.js +100 -0
- package/dist/src/migrations/update-24-0-0/find-custom-hashers.md +63 -0
- package/dist/src/migrations/update-24-0-0/move-custom-hashers-to-inputs.md +145 -0
- package/dist/src/native/index.d.ts +180 -12
- package/dist/src/native/native-bindings.js +8 -0
- package/dist/src/native/nx.wasi-browser.js +1 -0
- package/dist/src/native/nx.wasi.cjs +1 -0
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/nx-cloud/update-manager.d.ts +7 -0
- package/dist/src/nx-cloud/update-manager.js +237 -50
- package/dist/src/project-graph/affected/affected-tasks.js +1 -0
- package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +7 -0
- package/dist/src/tasks-runner/create-task-graph.d.ts +8 -7
- package/dist/src/tasks-runner/create-task-graph.js +47 -26
- package/dist/src/tasks-runner/init-tasks-runner.js +13 -0
- package/dist/src/tasks-runner/life-cycle.d.ts +4 -1
- package/dist/src/tasks-runner/life-cycle.js +7 -0
- package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +5 -2
- package/dist/src/tasks-runner/life-cycles/performance-analysis.js +13 -7
- package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +4 -3
- package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +9 -2
- package/dist/src/tasks-runner/readiness/probes.d.ts +9 -0
- package/dist/src/tasks-runner/readiness/probes.js +62 -0
- package/dist/src/tasks-runner/readiness/ready-when.d.ts +38 -0
- package/dist/src/tasks-runner/readiness/ready-when.js +190 -0
- package/dist/src/tasks-runner/run-command.js +5 -4
- package/dist/src/tasks-runner/running-tasks/node-child-process.js +5 -4
- package/dist/src/tasks-runner/task-env.d.ts +10 -0
- package/dist/src/tasks-runner/task-env.js +26 -5
- package/dist/src/tasks-runner/task-graph-utils.d.ts +9 -6
- package/dist/src/tasks-runner/task-graph-utils.js +17 -12
- package/dist/src/tasks-runner/task-orchestrator.d.ts +37 -8
- package/dist/src/tasks-runner/task-orchestrator.js +460 -76
- package/dist/src/tasks-runner/tasks-schedule.d.ts +18 -1
- package/dist/src/tasks-runner/tasks-schedule.js +64 -6
- package/dist/src/tasks-runner/utils.d.ts +20 -5
- package/dist/src/tasks-runner/utils.js +70 -35
- package/dist/src/ultracache/overrides.js +4 -0
- package/dist/src/utils/child-process.js +1 -0
- package/dist/src/utils/git-utils.d.ts +6 -1
- package/dist/src/utils/git-utils.js +26 -5
- package/dist/src/utils/owned-private-dir.d.ts +1 -17
- package/dist/src/utils/owned-private-dir.js +10 -62
- package/dist/src/utils/perf-hooks.d.ts +1 -0
- package/dist/src/utils/plugins/core-plugins.js +1 -1
- package/dist/src/utils/spawn-without-terminal.d.ts +27 -0
- package/dist/src/utils/spawn-without-terminal.js +110 -0
- package/dist/src/utils/string-similarity.d.ts +38 -0
- package/dist/src/utils/string-similarity.js +108 -0
- package/migrations.json +7 -0
- package/package.json +11 -20
- package/readme-template.md +3 -3
- package/schemas/nx-schema.json +70 -0
- package/schemas/project-schema.json +67 -0
- package/dist/src/tasks-runner/legacy-depends-on-warning.d.ts +0 -18
- package/dist/src/tasks-runner/legacy-depends-on-warning.js +0 -109
|
@@ -22,6 +22,8 @@ const cache_1 = require("./cache");
|
|
|
22
22
|
const forked_process_task_runner_1 = require("./forked-process-task-runner");
|
|
23
23
|
const is_tui_enabled_1 = require("./is-tui-enabled");
|
|
24
24
|
const pseudo_terminal_1 = require("./pseudo-terminal");
|
|
25
|
+
const probes_1 = require("./readiness/probes");
|
|
26
|
+
const ready_when_1 = require("./readiness/ready-when");
|
|
25
27
|
const noop_child_process_1 = require("./running-tasks/noop-child-process");
|
|
26
28
|
const output_prefix_1 = require("./running-tasks/output-prefix");
|
|
27
29
|
const shared_running_task_1 = require("./running-tasks/shared-running-task");
|
|
@@ -45,6 +47,31 @@ function hasContent(path) {
|
|
|
45
47
|
return false;
|
|
46
48
|
}
|
|
47
49
|
}
|
|
50
|
+
function createReadinessState(onSettled) {
|
|
51
|
+
const state = {
|
|
52
|
+
settled: false,
|
|
53
|
+
abort: new AbortController(),
|
|
54
|
+
};
|
|
55
|
+
state.promise = new Promise((resolve, reject) => {
|
|
56
|
+
state.resolve = () => {
|
|
57
|
+
if (state.settled)
|
|
58
|
+
return;
|
|
59
|
+
state.settled = true;
|
|
60
|
+
resolve();
|
|
61
|
+
onSettled('ready');
|
|
62
|
+
};
|
|
63
|
+
state.reject = (error) => {
|
|
64
|
+
if (state.settled)
|
|
65
|
+
return;
|
|
66
|
+
state.settled = true;
|
|
67
|
+
reject(error);
|
|
68
|
+
onSettled('failed');
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
// A producer with no waiter still settles; that must not be unhandled
|
|
72
|
+
state.promise.catch(() => { });
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
48
75
|
class TaskOrchestrator {
|
|
49
76
|
// endregion internal state
|
|
50
77
|
constructor(hasher, initiatingProject, initiatingTasks, projectGraph, taskGraph, nxJson, options, bail, daemon,
|
|
@@ -78,13 +105,17 @@ class TaskOrchestrator {
|
|
|
78
105
|
? new native_1.RunningTasksService((0, db_connection_1.getLocalDbConnection)())
|
|
79
106
|
: null;
|
|
80
107
|
this.taskInvocationTracker = !native_1.IS_WASM
|
|
81
|
-
? new native_1.TaskInvocationTracker((0, db_connection_1.getLocalDbConnection)(),
|
|
108
|
+
? new native_1.TaskInvocationTracker((0, db_connection_1.getLocalDbConnection)(), (0, task_env_1.getInvocationRootPid)(), (0, task_env_1.getInvocationAncestorPids)())
|
|
82
109
|
: null;
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
110
|
+
// Task ids this process has already registered. Recursive code paths (e.g.
|
|
111
|
+
// applyFromCacheOrRunBatch looping on incomplete batches) re-enter the
|
|
112
|
+
// detector for the same task; skipping them saves a DB round trip. A task id
|
|
113
|
+
// is unique within one process, so it is enough to identify the row.
|
|
86
114
|
this.registeredInvocations = new Set();
|
|
87
|
-
this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options
|
|
115
|
+
this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options, this.fullTaskGraph, {
|
|
116
|
+
readinessElsewhere: (producerId) => this.runningTasksService?.getTaskReadiness(producerId) ?? null,
|
|
117
|
+
onReadinessHold: (producerId) => this.onReadinessHold(producerId),
|
|
118
|
+
});
|
|
88
119
|
// region internal state
|
|
89
120
|
this.batchEnv = (0, task_env_1.getEnvVariablesForBatchProcess)(this.options.skipNxCache, this.options.captureStderr);
|
|
90
121
|
this.reverseTaskDeps = (0, utils_1.calculateReverseDeps)(this.taskGraph);
|
|
@@ -123,6 +154,14 @@ class TaskOrchestrator {
|
|
|
123
154
|
this.runningDiscreteTasks = new Map();
|
|
124
155
|
this.discreteTaskExitHandled = new Map();
|
|
125
156
|
this.continuousTaskExitHandled = new Map();
|
|
157
|
+
// Keyed by producer id: a producer this process started or shares
|
|
158
|
+
this.readiness = new Map();
|
|
159
|
+
// Producers that failed their check while still running: the exit must not
|
|
160
|
+
// report them again, and the saved log gets the reason after the transcript
|
|
161
|
+
this.readinessFailures = new Map();
|
|
162
|
+
this.readinessFailureReasons = new Map();
|
|
163
|
+
this.readinessSettled = false;
|
|
164
|
+
this.waitingLogged = new Set();
|
|
126
165
|
this.cleanupPromise = null;
|
|
127
166
|
this.signalHandlers = [];
|
|
128
167
|
// Tasks whose runner already owns the write to
|
|
@@ -212,6 +251,7 @@ class TaskOrchestrator {
|
|
|
212
251
|
while (true) {
|
|
213
252
|
if (this.bailed || this.stopRequested)
|
|
214
253
|
break;
|
|
254
|
+
await this.scheduleAfterReadiness();
|
|
215
255
|
// 1. Hash BEFORE processAll so processTask sees hashes set, and so
|
|
216
256
|
// resolveCachedTasksBulk can look them up in the cache. Each task
|
|
217
257
|
// is hashed with its own task-specific env (project/target .env
|
|
@@ -223,7 +263,7 @@ class TaskOrchestrator {
|
|
|
223
263
|
const unhashed = scheduledTasks
|
|
224
264
|
.map((id) => this.taskGraph.tasks[id])
|
|
225
265
|
.filter((t) => !t.hash &&
|
|
226
|
-
this.taskGraph.dependencies[t.id].every((
|
|
266
|
+
this.taskGraph.dependencies[t.id].every(({ id }) => this.completedTasks.has(id)));
|
|
227
267
|
if (unhashed.length > 0) {
|
|
228
268
|
const perTaskEnvs = {};
|
|
229
269
|
for (const task of unhashed) {
|
|
@@ -238,6 +278,11 @@ class TaskOrchestrator {
|
|
|
238
278
|
const resolved = await this.resolveCachedTasksBulk();
|
|
239
279
|
if (resolved)
|
|
240
280
|
continue;
|
|
281
|
+
// A pass from a continuous loop during the waits above may have
|
|
282
|
+
// admitted tasks this iteration never looked up; they must not reach
|
|
283
|
+
// step 5 without a confirmed miss
|
|
284
|
+
if (this.hasUnresolvedCacheableTask())
|
|
285
|
+
continue;
|
|
241
286
|
}
|
|
242
287
|
// 3. Process remaining scheduled tasks (cache misses + non-cacheable).
|
|
243
288
|
this.processAllScheduledTasks();
|
|
@@ -263,6 +308,10 @@ class TaskOrchestrator {
|
|
|
263
308
|
break;
|
|
264
309
|
}
|
|
265
310
|
// 7. Wait for a worker to finish (woken by scheduleNextTasksAndReleaseThreads)
|
|
311
|
+
// A verdict that landed during this iteration found nothing parked,
|
|
312
|
+
// so run its pass instead of parking
|
|
313
|
+
if (this.readinessSettled)
|
|
314
|
+
continue;
|
|
266
315
|
await new Promise((res) => this.waitingForTasks.push(res));
|
|
267
316
|
}
|
|
268
317
|
}
|
|
@@ -272,6 +321,7 @@ class TaskOrchestrator {
|
|
|
272
321
|
if (!this.tasksSchedule.hasTasks() || this.bailed || this.stopRequested) {
|
|
273
322
|
return null;
|
|
274
323
|
}
|
|
324
|
+
await this.scheduleAfterReadiness();
|
|
275
325
|
this.processAllScheduledTasks();
|
|
276
326
|
const task = this.tasksSchedule.nextTask((t) => t.continuous);
|
|
277
327
|
if (task) {
|
|
@@ -293,9 +343,29 @@ class TaskOrchestrator {
|
|
|
293
343
|
return null;
|
|
294
344
|
}
|
|
295
345
|
// block until some other task completes, then try again
|
|
346
|
+
if (this.readinessSettled)
|
|
347
|
+
continue;
|
|
296
348
|
await new Promise((res) => this.waitingForTasks.push(res));
|
|
297
349
|
}
|
|
298
350
|
}
|
|
351
|
+
hasUnresolvedCacheableTask() {
|
|
352
|
+
return this.tasksSchedule
|
|
353
|
+
.getAllScheduledTasks()
|
|
354
|
+
.scheduledTasks.some((id) => {
|
|
355
|
+
const task = this.taskGraph.tasks[id];
|
|
356
|
+
return (!task.continuous &&
|
|
357
|
+
task.cache &&
|
|
358
|
+
(!task.hash || !this.cacheMissedHashes.has(task.hash)));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
// A verdict releases tasks the schedule has not admitted yet. Whichever
|
|
362
|
+
// loop wakes first runs the pass; the coordinator may be inside a batch.
|
|
363
|
+
async scheduleAfterReadiness() {
|
|
364
|
+
if (this.readinessSettled) {
|
|
365
|
+
this.readinessSettled = false;
|
|
366
|
+
await this.scheduleNextTasksAndReleaseThreads();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
299
369
|
// region Processing Scheduled Tasks
|
|
300
370
|
async processTask(taskId) {
|
|
301
371
|
const task = this.taskGraph.tasks[taskId];
|
|
@@ -316,39 +386,59 @@ class TaskOrchestrator {
|
|
|
316
386
|
}
|
|
317
387
|
}
|
|
318
388
|
/**
|
|
319
|
-
* Registers a task invocation and checks for loops across nested Nx
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
389
|
+
* Registers a task invocation and checks for loops across nested Nx
|
|
390
|
+
* processes. registerTask() returns the invocation chain only when an
|
|
391
|
+
* *ancestor* Nx process is already running this task — a genuine loop.
|
|
392
|
+
* Sibling processes running the same task are legitimate and register
|
|
393
|
+
* without complaint.
|
|
394
|
+
*
|
|
395
|
+
* The overrides hash rides along with the task id so the tracker can tell an
|
|
396
|
+
* ancestor re-invoking a task with different arguments from one repeating
|
|
397
|
+
* the same invocation.
|
|
323
398
|
*/
|
|
324
399
|
detectTaskInvocationLoop(task) {
|
|
325
400
|
if (!this.taskInvocationTracker)
|
|
326
401
|
return;
|
|
327
402
|
if (this.registeredInvocations.has(task.id))
|
|
328
403
|
return;
|
|
404
|
+
let chain;
|
|
329
405
|
try {
|
|
330
|
-
this.taskInvocationTracker.registerTask(process.pid, task.id);
|
|
406
|
+
chain = this.taskInvocationTracker.registerTask(process.pid, task.id, (0, utils_1.hashTaskOverrides)(task));
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// Loop detection is diagnostic only; a DB failure must not fail the run
|
|
410
|
+
// or be mistaken for a loop.
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (!chain) {
|
|
331
414
|
this.registeredInvocations.add(task.id);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const chainDisplay = chain.map((r) => r.taskId).join(' -> ');
|
|
418
|
+
output_1.output.error({
|
|
419
|
+
title: 'Recursive task invocation detected',
|
|
420
|
+
bodyLines: [
|
|
421
|
+
`Nx detected a recursive loop of task invocations:`,
|
|
422
|
+
``,
|
|
423
|
+
` ${chainDisplay} -> ${task.id}`,
|
|
424
|
+
``,
|
|
425
|
+
`Task "${task.id}" was already invoked by a parent Nx process in this chain.`,
|
|
426
|
+
`This typically happens when a task's command (e.g., "nx ${task.target.target} ${task.target.project}")`,
|
|
427
|
+
`triggers a chain of tasks that eventually re-invokes itself.`,
|
|
428
|
+
``,
|
|
429
|
+
`To fix this, review the command configuration for the tasks in the chain above.`,
|
|
430
|
+
],
|
|
431
|
+
});
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
releaseTaskInvocation(task) {
|
|
435
|
+
if (!this.registeredInvocations.delete(task.id))
|
|
436
|
+
return;
|
|
437
|
+
try {
|
|
438
|
+
this.taskInvocationTracker?.unregisterTask(process.pid, task.id);
|
|
332
439
|
}
|
|
333
440
|
catch {
|
|
334
|
-
//
|
|
335
|
-
const chain = this.taskInvocationTracker.getInvocationChain();
|
|
336
|
-
const chainDisplay = chain.map((r) => r.taskId).join(' -> ');
|
|
337
|
-
output_1.output.error({
|
|
338
|
-
title: 'Recursive task invocation detected',
|
|
339
|
-
bodyLines: [
|
|
340
|
-
`Nx detected a recursive loop of task invocations:`,
|
|
341
|
-
``,
|
|
342
|
-
` ${chainDisplay} -> ${task.id}`,
|
|
343
|
-
``,
|
|
344
|
-
`Task "${task.id}" was already invoked by a parent Nx process in this chain.`,
|
|
345
|
-
`This typically happens when a task's command (e.g., "nx ${task.target.target} ${task.target.project}")`,
|
|
346
|
-
`triggers a chain of tasks that eventually re-invokes itself.`,
|
|
347
|
-
``,
|
|
348
|
-
`To fix this, review the command configuration for the tasks in the chain above.`,
|
|
349
|
-
],
|
|
350
|
-
});
|
|
351
|
-
process.exit(1);
|
|
441
|
+
// Diagnostic only, like registration. A leftover row is swept as stale.
|
|
352
442
|
}
|
|
353
443
|
}
|
|
354
444
|
// endregion Processing Scheduled Tasks
|
|
@@ -503,8 +593,7 @@ class TaskOrchestrator {
|
|
|
503
593
|
await this.hashBatchTasks(rootTasks);
|
|
504
594
|
const eligible = [];
|
|
505
595
|
for (const task of rootTasks) {
|
|
506
|
-
const
|
|
507
|
-
const hasNonCachedDep = depIds.some((id) => nonCachedTaskIds.has(id));
|
|
596
|
+
const hasNonCachedDep = batch.taskGraph.dependencies[task.id].some(({ id }) => nonCachedTaskIds.has(id));
|
|
508
597
|
if (hasNonCachedDep && this.deferredTaskIds?.has(task.id)) {
|
|
509
598
|
nonCachedTaskIds.add(task.id);
|
|
510
599
|
needsRehashAfterExecution.add(task.id);
|
|
@@ -553,17 +642,19 @@ class TaskOrchestrator {
|
|
|
553
642
|
taskIds: Object.keys(batch.taskGraph.tasks),
|
|
554
643
|
});
|
|
555
644
|
const { cachedResults, needsRehashAfterExecution } = await this.applyBatchCachedResults(batch, doNotSkipCache, groupId);
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
const
|
|
559
|
-
const nonCachedTasks = tasks.filter((t) => !cachedTaskIds.has(t.id));
|
|
645
|
+
// Cached tasks were started and completed inside applyBatchCachedResults;
|
|
646
|
+
// a member skipped meanwhile (a dependency failed) must not run either
|
|
647
|
+
const nonCachedTasks = tasks.filter((t) => !this.completedTasks.has(t.id));
|
|
560
648
|
if (nonCachedTasks.length > 0) {
|
|
561
649
|
await Promise.all(nonCachedTasks.map((task) => this.options.lifeCycle.scheduleTask(task)));
|
|
562
650
|
await this.preRunSteps(nonCachedTasks, { groupId });
|
|
563
651
|
}
|
|
564
652
|
// Phase 2: Run non-cached tasks, then re-hash tasks that read their outputs
|
|
565
|
-
const taskIdsToSkip = cachedResults.map((r) => r.task.id);
|
|
566
653
|
let batchResults = [];
|
|
654
|
+
// Read again: a member can be skipped while the hooks above ran
|
|
655
|
+
const taskIdsToSkip = tasks
|
|
656
|
+
.filter((t) => this.completedTasks.has(t.id))
|
|
657
|
+
.map((t) => t.id);
|
|
567
658
|
if (taskIdsToSkip.length < tasks.length) {
|
|
568
659
|
const runGraph = (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, taskIdsToSkip);
|
|
569
660
|
for (const task of Object.values(runGraph.tasks)) {
|
|
@@ -923,8 +1014,7 @@ class TaskOrchestrator {
|
|
|
923
1014
|
// Wake coordinator — the delete above may satisfy the exit condition
|
|
924
1015
|
// (pendingDiscreteWorkers.size === 0) that was missed when
|
|
925
1016
|
// scheduleNextTasksAndReleaseThreads fired earlier.
|
|
926
|
-
this.
|
|
927
|
-
this.waitingForTasks.length = 0;
|
|
1017
|
+
this.releaseThreads();
|
|
928
1018
|
});
|
|
929
1019
|
this.pendingDiscreteWorkers.add(worker);
|
|
930
1020
|
}
|
|
@@ -947,13 +1037,36 @@ class TaskOrchestrator {
|
|
|
947
1037
|
await this.postRunSteps([{ task, status: 'failure', terminalOutput }], doNotSkipCache, groupId);
|
|
948
1038
|
}
|
|
949
1039
|
/**
|
|
950
|
-
* Spawn and wait on a task's child process
|
|
951
|
-
* lookup. Callers must have already confirmed the task
|
|
952
|
-
* (or disabled caching entirely).
|
|
1040
|
+
* Spawn and wait on a task's child process once its ready dependencies are
|
|
1041
|
+
* ready, with no cache lookup. Callers must have already confirmed the task
|
|
1042
|
+
* is a cache miss (or disabled caching entirely).
|
|
953
1043
|
*/
|
|
954
1044
|
async runTaskDirectly(doNotSkipCache, task, groupId) {
|
|
955
1045
|
// Wait for task to be processed
|
|
956
1046
|
const taskSpecificEnv = await this.processedTasks.get(task.id);
|
|
1047
|
+
const waitError = await this.waitForReadyDependencies(task).then(() => null, (e) => e);
|
|
1048
|
+
// Skipped by a failed dependency's propagation while waiting
|
|
1049
|
+
if (this.completedTasks.has(task.id)) {
|
|
1050
|
+
return {
|
|
1051
|
+
task,
|
|
1052
|
+
code: 1,
|
|
1053
|
+
status: this.completedTasks.get(task.id),
|
|
1054
|
+
terminalOutput: '',
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
if (waitError instanceof ready_when_1.ProducerFailedError) {
|
|
1058
|
+
await this.skipWaiter(task, groupId);
|
|
1059
|
+
return { task, code: 1, status: 'skipped', terminalOutput: '' };
|
|
1060
|
+
}
|
|
1061
|
+
if (waitError) {
|
|
1062
|
+
await this.failTaskBeforeStart(task, groupId, waitError, doNotSkipCache);
|
|
1063
|
+
return {
|
|
1064
|
+
task,
|
|
1065
|
+
code: 1,
|
|
1066
|
+
status: 'failure',
|
|
1067
|
+
terminalOutput: waitError.message,
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
957
1070
|
await this.preRunSteps([task], { groupId });
|
|
958
1071
|
const pipeOutput = await this.pipeOutputCapture(task);
|
|
959
1072
|
const temporaryOutputPath = this.cache.temporaryOutputPath(task);
|
|
@@ -1165,11 +1278,28 @@ class TaskOrchestrator {
|
|
|
1165
1278
|
async startContinuousTask(task, groupId) {
|
|
1166
1279
|
if (this.runningTasksService &&
|
|
1167
1280
|
this.runningTasksService.getRunningTasks([task.id]).length) {
|
|
1281
|
+
let readyWhen;
|
|
1282
|
+
try {
|
|
1283
|
+
readyWhen = this.getReadyWhen(task);
|
|
1284
|
+
}
|
|
1285
|
+
catch (e) {
|
|
1286
|
+
return this.failTaskBeforeStart(task, groupId, e);
|
|
1287
|
+
}
|
|
1168
1288
|
await this.preRunSteps([task], { groupId });
|
|
1169
1289
|
if (this.tuiEnabled) {
|
|
1170
1290
|
this.options.lifeCycle.setTaskStatus(task.id, 8 /* NativeTaskStatus.Shared */);
|
|
1171
1291
|
}
|
|
1172
1292
|
const runningTask = new shared_running_task_1.SharedRunningTask(this.runningTasksService, task.id);
|
|
1293
|
+
if (readyWhen) {
|
|
1294
|
+
const state = this.armReadiness(task.id);
|
|
1295
|
+
this.pollReadinessRow(task, readyWhen, state.abort.signal, true).then(() => {
|
|
1296
|
+
if (!state.abort.signal.aborted)
|
|
1297
|
+
state.resolve();
|
|
1298
|
+
}, (e) => {
|
|
1299
|
+
if (!state.abort.signal.aborted)
|
|
1300
|
+
this.failReadiness(task, state, e);
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1173
1303
|
this.runningContinuousTasks.set(task.id, {
|
|
1174
1304
|
runningTask,
|
|
1175
1305
|
groupId,
|
|
@@ -1183,34 +1313,72 @@ class TaskOrchestrator {
|
|
|
1183
1313
|
}));
|
|
1184
1314
|
// task is already running by another process, we schedule the next tasks
|
|
1185
1315
|
// and release the threads
|
|
1316
|
+
this.tasksSchedule.markContinuousTaskStarted(task.id);
|
|
1186
1317
|
await this.scheduleNextTasksAndReleaseThreads();
|
|
1187
1318
|
return runningTask;
|
|
1188
1319
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
//
|
|
1193
|
-
const temporaryOutputPath = this.cache.temporaryOutputPath(task);
|
|
1194
|
-
// Deliberately not gated on `printsTaskOutput`, unlike `runTaskDirectly`.
|
|
1195
|
-
// `SummaryTerminalOutputLifeCycle.printTaskTerminalOutput` is a no-op and it
|
|
1196
|
-
// only reports at `endCommand`, which never runs for a task that does not
|
|
1197
|
-
// end - so suppressing here would leave a `nx serve` under `summary` with a
|
|
1198
|
-
// permanently silent terminal and no file to read yet. Streaming is the only
|
|
1199
|
-
// channel a continuous task has.
|
|
1200
|
-
const streamOutput = (0, output_1.isStaticOutputStyle)(this.specifiedOutputStyle)
|
|
1201
|
-
? false
|
|
1202
|
-
: (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
|
|
1203
|
-
let env = pipeOutput
|
|
1204
|
-
? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
|
|
1205
|
-
: (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
|
|
1206
|
-
this.detectTaskInvocationLoop(task);
|
|
1207
|
-
const childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
|
|
1320
|
+
// Claim the task before doing any of the work below. getRunningTasks()
|
|
1321
|
+
// above is a check-then-act: until this row exists, a sibling Nx process
|
|
1322
|
+
// asking the same question is told the task is not running and starts a
|
|
1323
|
+
// duplicate of it.
|
|
1208
1324
|
this.runningTasksService?.addRunningTask(task.id);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1325
|
+
let taskSpecificEnv;
|
|
1326
|
+
let readyWhen;
|
|
1327
|
+
try {
|
|
1328
|
+
taskSpecificEnv = await this.processedTasks.get(task.id);
|
|
1329
|
+
readyWhen = this.getReadyWhen(task);
|
|
1330
|
+
await this.waitForReadyDependencies(task);
|
|
1331
|
+
}
|
|
1332
|
+
catch (e) {
|
|
1333
|
+
this.runningTasksService?.removeRunningTask(task.id);
|
|
1334
|
+
if (e instanceof ready_when_1.ProducerFailedError) {
|
|
1335
|
+
await this.skipWaiter(task, groupId);
|
|
1336
|
+
return new noop_child_process_1.NoopChildProcess({ code: 1, terminalOutput: '' });
|
|
1337
|
+
}
|
|
1338
|
+
return this.failTaskBeforeStart(task, groupId, e);
|
|
1339
|
+
}
|
|
1340
|
+
// Skipped by a failed dependency's propagation while waiting
|
|
1341
|
+
if (this.completedTasks.has(task.id)) {
|
|
1342
|
+
this.runningTasksService?.removeRunningTask(task.id);
|
|
1343
|
+
return new noop_child_process_1.NoopChildProcess({ code: 1, terminalOutput: '' });
|
|
1344
|
+
}
|
|
1345
|
+
let childProcess;
|
|
1346
|
+
try {
|
|
1347
|
+
await this.preRunSteps([task], { groupId });
|
|
1348
|
+
const pipeOutput = await this.pipeOutputCapture(task);
|
|
1349
|
+
// obtain metadata
|
|
1350
|
+
const temporaryOutputPath = this.cache.temporaryOutputPath(task);
|
|
1351
|
+
// Deliberately not gated on `printsTaskOutput`, unlike `runTaskDirectly`.
|
|
1352
|
+
// `SummaryTerminalOutputLifeCycle.printTaskTerminalOutput` is a no-op and it
|
|
1353
|
+
// only reports at `endCommand`, which never runs for a task that does not
|
|
1354
|
+
// end - so suppressing here would leave a `nx serve` under `summary` with a
|
|
1355
|
+
// permanently silent terminal and no file to read yet. Streaming is the only
|
|
1356
|
+
// channel a continuous task has.
|
|
1357
|
+
const streamOutput = (0, output_1.isStaticOutputStyle)(this.specifiedOutputStyle)
|
|
1358
|
+
? false
|
|
1359
|
+
: (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
|
|
1360
|
+
let env = pipeOutput
|
|
1361
|
+
? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
|
|
1362
|
+
: (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
|
|
1363
|
+
this.detectTaskInvocationLoop(task);
|
|
1364
|
+
childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
|
|
1365
|
+
this.tasksSchedule.markContinuousTaskStarted(task.id);
|
|
1366
|
+
if (readyWhen) {
|
|
1367
|
+
this.startReadinessProbe(task, readyWhen, childProcess);
|
|
1368
|
+
}
|
|
1369
|
+
this.runningContinuousTasks.set(task.id, {
|
|
1370
|
+
runningTask: childProcess,
|
|
1371
|
+
groupId,
|
|
1372
|
+
ownsRunningTasksService: true,
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
catch (e) {
|
|
1376
|
+
// Nothing owns the claim until runningContinuousTasks holds it — the
|
|
1377
|
+
// release paths (completeContinuousTask, the signal handler) both iterate
|
|
1378
|
+
// that map. Drop it here or siblings wait on a task no one is running.
|
|
1379
|
+
this.runningTasksService?.removeRunningTask(task.id);
|
|
1380
|
+
throw e;
|
|
1381
|
+
}
|
|
1214
1382
|
this.continuousTaskExitHandled.set(task.id, new Promise((resolve) => {
|
|
1215
1383
|
childProcess.onExit(async (code) => {
|
|
1216
1384
|
await this.handleContinuousTaskExit(code, task, groupId, true);
|
|
@@ -1224,6 +1392,194 @@ class TaskOrchestrator {
|
|
|
1224
1392
|
return childProcess;
|
|
1225
1393
|
}
|
|
1226
1394
|
// endregion Single Task
|
|
1395
|
+
// region Readiness
|
|
1396
|
+
getReadyWhen(task) {
|
|
1397
|
+
return (0, ready_when_1.getReadyWhen)(task, this.projectGraph, this.projects, workspace_root_1.workspaceRoot);
|
|
1398
|
+
}
|
|
1399
|
+
async waitForReadyDependencies(task) {
|
|
1400
|
+
const producerIds = (0, ready_when_1.getReadyProducerIds)(task, this.fullTaskGraph);
|
|
1401
|
+
for (const producerId of producerIds) {
|
|
1402
|
+
const producer = this.fullTaskGraph.tasks[producerId];
|
|
1403
|
+
const readyWhen = this.getReadyWhen(producer);
|
|
1404
|
+
if (!readyWhen) {
|
|
1405
|
+
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
|
1406
|
+
console.log(`Task "${producer.id}" declares no "readyWhen", so "${task.id}" runs once it has started.`);
|
|
1407
|
+
}
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
// A producer in this task graph is held by dispatch and settled by its
|
|
1411
|
+
// probe or row poll; one outside it (an agent's flat graph) has only its row
|
|
1412
|
+
if (this.taskGraph.tasks[producerId]) {
|
|
1413
|
+
await this.readinessOf(producerId).promise;
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
this.onReadinessHold(producerId);
|
|
1417
|
+
await this.pollReadinessRow(producer, readyWhen);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
onReadinessHold(producerId) {
|
|
1422
|
+
if (!this.waitingLogged.has(producerId) &&
|
|
1423
|
+
!this.tuiEnabled &&
|
|
1424
|
+
(0, output_1.printsTaskOutput)(this.resolvedOutputStyle)) {
|
|
1425
|
+
this.waitingLogged.add(producerId);
|
|
1426
|
+
console.log(`Waiting for "${producerId}" to be ready...`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
// A row absent from the start means no Nx process this one can see owns the
|
|
1430
|
+
// producer, unless the caller already saw the owner: then it exited. A row
|
|
1431
|
+
// that disappears mid-wait means the producer exited.
|
|
1432
|
+
async pollReadinessRow(producer, readyWhen, signal, ownerObserved = false) {
|
|
1433
|
+
const deadline = Date.now() + readyWhen.timeout;
|
|
1434
|
+
let lastStatus = null;
|
|
1435
|
+
while (true) {
|
|
1436
|
+
if (signal?.aborted) {
|
|
1437
|
+
throw (0, ready_when_1.notReadyError)(producer.id, 'exited');
|
|
1438
|
+
}
|
|
1439
|
+
if (this.stopRequested || this.bailed) {
|
|
1440
|
+
throw (0, ready_when_1.notReadyError)(producer.id, 'was stopped');
|
|
1441
|
+
}
|
|
1442
|
+
const status = this.runningTasksService?.getTaskReadiness(producer.id) ?? null;
|
|
1443
|
+
if (status === null) {
|
|
1444
|
+
if (lastStatus !== null || ownerObserved) {
|
|
1445
|
+
throw (0, ready_when_1.notReadyError)(producer.id, 'exited');
|
|
1446
|
+
}
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (status !== lastStatus) {
|
|
1450
|
+
lastStatus = status;
|
|
1451
|
+
this.options.lifeCycle.setTaskReadiness?.(producer.id, status);
|
|
1452
|
+
}
|
|
1453
|
+
if (status === 1 /* TaskReadiness.Ready */) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (status === 2 /* TaskReadiness.Failed */) {
|
|
1457
|
+
throw new ready_when_1.ProducerFailedError(producer.id);
|
|
1458
|
+
}
|
|
1459
|
+
if (Date.now() >= deadline) {
|
|
1460
|
+
throw (0, ready_when_1.readinessTimeoutError)(producer.id, readyWhen);
|
|
1461
|
+
}
|
|
1462
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
startReadinessProbe(task, readyWhen, runningTask) {
|
|
1466
|
+
const state = this.armReadiness(task.id);
|
|
1467
|
+
this.options.lifeCycle.setTaskReadiness?.(task.id, 0 /* TaskReadiness.Pending */);
|
|
1468
|
+
(0, probes_1.waitForReadiness)(readyWhen, {
|
|
1469
|
+
taskId: task.id,
|
|
1470
|
+
runningTask,
|
|
1471
|
+
cwd: workspace_root_1.workspaceRoot,
|
|
1472
|
+
signal: state.abort.signal,
|
|
1473
|
+
}).then(() => {
|
|
1474
|
+
if (state.abort.signal.aborted)
|
|
1475
|
+
return;
|
|
1476
|
+
this.recordReadiness(task, 1 /* TaskReadiness.Ready */);
|
|
1477
|
+
state.resolve();
|
|
1478
|
+
}, (e) => {
|
|
1479
|
+
if (state.abort.signal.aborted)
|
|
1480
|
+
return;
|
|
1481
|
+
this.recordReadiness(task, 2 /* TaskReadiness.Failed */);
|
|
1482
|
+
this.failReadiness(task, state, e);
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
// A verdict against a producer something in this run waits for fails the
|
|
1486
|
+
// producer, and the skip propagation covers the waiters. Without a waiter
|
|
1487
|
+
// the producer is left alone, as a plain `nx serve` with a probe would be.
|
|
1488
|
+
async failReadiness(task, state, e) {
|
|
1489
|
+
const message = e?.message ?? String(e);
|
|
1490
|
+
const entry = this.runningContinuousTasks.get(task.id);
|
|
1491
|
+
if (entry &&
|
|
1492
|
+
!(e instanceof ready_when_1.NotReadyError) &&
|
|
1493
|
+
this.hasReadyWaiter(task.id)) {
|
|
1494
|
+
if (this.tuiEnabled) {
|
|
1495
|
+
this.options.lifeCycle.appendTaskOutput(task.id, `\r\n${message}\r\n`, false);
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
console.error(message);
|
|
1499
|
+
}
|
|
1500
|
+
this.readinessFailureReasons.set(task.id, message);
|
|
1501
|
+
const completion = this.complete([{ task, status: 'failure', terminalOutput: message }], entry.groupId);
|
|
1502
|
+
this.readinessFailures.set(task.id, completion);
|
|
1503
|
+
try {
|
|
1504
|
+
await completion;
|
|
1505
|
+
}
|
|
1506
|
+
finally {
|
|
1507
|
+
this.readinessFailures.delete(task.id);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
else if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
|
1511
|
+
console.error(message);
|
|
1512
|
+
}
|
|
1513
|
+
state.reject(e);
|
|
1514
|
+
}
|
|
1515
|
+
// On an agent the waiter runs in another process with its own flat graph
|
|
1516
|
+
hasReadyWaiter(producerId) {
|
|
1517
|
+
return Object.values(this.fullTaskGraph.tasks).some((task) => (0, ready_when_1.getReadyProducerIds)(task, this.fullTaskGraph).includes(producerId));
|
|
1518
|
+
}
|
|
1519
|
+
// The producer is not in this task graph, so no skip propagates to the waiter
|
|
1520
|
+
async skipWaiter(task, groupId) {
|
|
1521
|
+
if (!this.completedTasks.has(task.id)) {
|
|
1522
|
+
await this.complete([{ task, status: 'skipped' }], groupId);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
// The probe's verdict stands for local waiters even when the row for
|
|
1526
|
+
// other processes cannot be written
|
|
1527
|
+
recordReadiness(task, status) {
|
|
1528
|
+
this.options.lifeCycle.setTaskReadiness?.(task.id, status);
|
|
1529
|
+
try {
|
|
1530
|
+
this.runningTasksService?.setTaskReadiness(task.id, status);
|
|
1531
|
+
}
|
|
1532
|
+
catch (e) {
|
|
1533
|
+
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
|
1534
|
+
console.error(`Failed to record readiness of "${task.id}":`, e);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
readinessOf(taskId) {
|
|
1539
|
+
return this.readiness.get(taskId) ?? this.armReadiness(taskId);
|
|
1540
|
+
}
|
|
1541
|
+
// Replaces a state settled by an earlier run so a restart re-arms it
|
|
1542
|
+
armReadiness(taskId) {
|
|
1543
|
+
let state = this.readiness.get(taskId);
|
|
1544
|
+
if (!state || state.settled) {
|
|
1545
|
+
this.tasksSchedule.markReadinessPending(taskId);
|
|
1546
|
+
state = createReadinessState((outcome) => {
|
|
1547
|
+
if (outcome === 'ready') {
|
|
1548
|
+
this.tasksSchedule.markReady(taskId);
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
this.tasksSchedule.markReadinessFailed(taskId);
|
|
1552
|
+
}
|
|
1553
|
+
this.readinessSettled = true;
|
|
1554
|
+
this.releaseThreads();
|
|
1555
|
+
});
|
|
1556
|
+
this.readiness.set(taskId, state);
|
|
1557
|
+
}
|
|
1558
|
+
return state;
|
|
1559
|
+
}
|
|
1560
|
+
abortReadiness(taskId, reason) {
|
|
1561
|
+
const state = this.readiness.get(taskId);
|
|
1562
|
+
if (!state)
|
|
1563
|
+
return;
|
|
1564
|
+
state.abort.abort();
|
|
1565
|
+
state.reject((0, ready_when_1.notReadyError)(taskId, reason));
|
|
1566
|
+
}
|
|
1567
|
+
abortAllReadiness() {
|
|
1568
|
+
for (const taskId of this.readiness.keys()) {
|
|
1569
|
+
this.abortReadiness(taskId, 'was stopped');
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async failTaskBeforeStart(task, groupId, e, doNotSkipCache = false) {
|
|
1573
|
+
this.abortReadiness(task.id, 'failed');
|
|
1574
|
+
// Already skipped by a failed dependency's propagation
|
|
1575
|
+
if (!this.completedTasks.has(task.id)) {
|
|
1576
|
+
// Lifecycles pair endTasks with startTasks, so start it before failing it
|
|
1577
|
+
await this.preRunSteps([task], { groupId });
|
|
1578
|
+
await this.handleDiscreteWorkerFailure(doNotSkipCache, task, groupId, e);
|
|
1579
|
+
}
|
|
1580
|
+
return new noop_child_process_1.NoopChildProcess({ code: 1, terminalOutput: e?.message ?? '' });
|
|
1581
|
+
}
|
|
1582
|
+
// endregion Readiness
|
|
1227
1583
|
// region Lifecycle
|
|
1228
1584
|
async preRunSteps(tasks, metadata) {
|
|
1229
1585
|
const now = Date.now();
|
|
@@ -1364,7 +1720,13 @@ class TaskOrchestrator {
|
|
|
1364
1720
|
if (!task.hash)
|
|
1365
1721
|
return;
|
|
1366
1722
|
try {
|
|
1367
|
-
const
|
|
1723
|
+
const outputPath = this.cache.temporaryOutputPath(task);
|
|
1724
|
+
// The runner's exit handler wrote the child's transcript alone
|
|
1725
|
+
const reason = this.readinessFailureReasons.get(task.id);
|
|
1726
|
+
if (reason) {
|
|
1727
|
+
(0, fs_1.appendFileSync)(outputPath, `\n${reason}\n`);
|
|
1728
|
+
}
|
|
1729
|
+
const { size } = (0, fs_1.statSync)(outputPath);
|
|
1368
1730
|
this.cache.recordTerminalOutputs([{ hash: task.hash, size }]);
|
|
1369
1731
|
}
|
|
1370
1732
|
catch {
|
|
@@ -1373,12 +1735,13 @@ class TaskOrchestrator {
|
|
|
1373
1735
|
}
|
|
1374
1736
|
async scheduleNextTasksAndReleaseThreads() {
|
|
1375
1737
|
if (this.stopRequested) {
|
|
1376
|
-
this.
|
|
1377
|
-
this.waitingForTasks.length = 0;
|
|
1738
|
+
this.releaseThreads();
|
|
1378
1739
|
return;
|
|
1379
1740
|
}
|
|
1380
1741
|
await this.tasksSchedule.scheduleNextTasks();
|
|
1381
|
-
|
|
1742
|
+
this.releaseThreads();
|
|
1743
|
+
}
|
|
1744
|
+
releaseThreads() {
|
|
1382
1745
|
this.waitingForTasks.forEach((f) => f(null));
|
|
1383
1746
|
this.waitingForTasks.length = 0;
|
|
1384
1747
|
}
|
|
@@ -1425,8 +1788,7 @@ class TaskOrchestrator {
|
|
|
1425
1788
|
if (this.completedTasks.has(task.id))
|
|
1426
1789
|
continue;
|
|
1427
1790
|
this.completedTasks.set(task.id, status);
|
|
1428
|
-
this.
|
|
1429
|
-
this.registeredInvocations.delete(task.id);
|
|
1791
|
+
this.releaseTaskInvocation(task);
|
|
1430
1792
|
if (this.tuiEnabled) {
|
|
1431
1793
|
this.options.lifeCycle.setTaskStatus(task.id, displayStatus ?? (0, native_1.parseTaskStatus)(status));
|
|
1432
1794
|
}
|
|
@@ -1437,6 +1799,7 @@ class TaskOrchestrator {
|
|
|
1437
1799
|
// mark the execution as bailed which will stop all further execution
|
|
1438
1800
|
// only the tasks that are currently running will finish
|
|
1439
1801
|
this.bailed = true;
|
|
1802
|
+
this.abortAllReadiness();
|
|
1440
1803
|
}
|
|
1441
1804
|
else {
|
|
1442
1805
|
// Collect reverse deps to skip
|
|
@@ -1467,6 +1830,10 @@ class TaskOrchestrator {
|
|
|
1467
1830
|
if (process.env.NX_NATIVE_COMMAND_RUNNER !== 'false') {
|
|
1468
1831
|
return true;
|
|
1469
1832
|
}
|
|
1833
|
+
// The log probe reads the captured output
|
|
1834
|
+
if (this.getReadyWhen(task)?.kind === 'logMatches') {
|
|
1835
|
+
return true;
|
|
1836
|
+
}
|
|
1470
1837
|
// When TUI is enabled, we need to use pipe output capture to support
|
|
1471
1838
|
// progressive output streaming via the onOutput callback
|
|
1472
1839
|
if (this.tuiEnabled) {
|
|
@@ -1520,8 +1887,13 @@ class TaskOrchestrator {
|
|
|
1520
1887
|
}
|
|
1521
1888
|
// endregion utils
|
|
1522
1889
|
async handleContinuousTaskExit(code, task, groupId, ownsRunningTasksService) {
|
|
1523
|
-
|
|
1890
|
+
await this.readinessFailures.get(task.id);
|
|
1891
|
+
// Completed by cleanup, or failed its readiness check while still running
|
|
1524
1892
|
if (this.completedTasks.has(task.id)) {
|
|
1893
|
+
if (this.runningContinuousTasks.delete(task.id) &&
|
|
1894
|
+
ownsRunningTasksService) {
|
|
1895
|
+
this.runningTasksService?.removeRunningTask(task.id);
|
|
1896
|
+
}
|
|
1525
1897
|
return;
|
|
1526
1898
|
}
|
|
1527
1899
|
const stoppingReason = this.runningContinuousTasks.get(task.id)?.stoppingReason;
|
|
@@ -1542,15 +1914,20 @@ class TaskOrchestrator {
|
|
|
1542
1914
|
isContinuousTaskNeeded(taskId) {
|
|
1543
1915
|
return this.tasksSchedule
|
|
1544
1916
|
.getIncompleteTasks()
|
|
1545
|
-
.some((t) => this.taskGraph.continuousDependencies[t.id]?.
|
|
1917
|
+
.some((t) => this.taskGraph.continuousDependencies[t.id]?.some((edge) => edge.id === taskId));
|
|
1546
1918
|
}
|
|
1547
1919
|
async completeContinuousTask(task, groupId, ownsRunningTasksService, reason) {
|
|
1548
|
-
|
|
1549
|
-
return;
|
|
1920
|
+
await this.readinessFailures.get(task.id);
|
|
1550
1921
|
this.runningContinuousTasks.delete(task.id);
|
|
1551
1922
|
if (ownsRunningTasksService) {
|
|
1552
1923
|
this.runningTasksService?.removeRunningTask(task.id);
|
|
1553
1924
|
}
|
|
1925
|
+
// A probe that does not need the child (url, port, command) could still
|
|
1926
|
+
// pass during completion and release dependents of an exited producer
|
|
1927
|
+
this.readiness.get(task.id)?.abort.abort();
|
|
1928
|
+
// Failed its readiness check while still running
|
|
1929
|
+
if (this.completedTasks.has(task.id))
|
|
1930
|
+
return;
|
|
1554
1931
|
task.endTime = Date.now();
|
|
1555
1932
|
if (reason === 'fulfilled') {
|
|
1556
1933
|
await this.complete([
|
|
@@ -1567,6 +1944,11 @@ class TaskOrchestrator {
|
|
|
1567
1944
|
else {
|
|
1568
1945
|
await this.complete([{ task, status: 'stopped' }], groupId);
|
|
1569
1946
|
}
|
|
1947
|
+
// After the skip propagation above, so a waiter is reported as skipped
|
|
1948
|
+
// rather than as failed
|
|
1949
|
+
this.abortReadiness(task.id, 'exited');
|
|
1950
|
+
// Skipped dependents leave nothing in flight to wake the loops
|
|
1951
|
+
this.releaseThreads();
|
|
1570
1952
|
}
|
|
1571
1953
|
async cleanup() {
|
|
1572
1954
|
if (this.cleanupPromise) {
|
|
@@ -1576,6 +1958,8 @@ class TaskOrchestrator {
|
|
|
1576
1958
|
return this.cleanupPromise;
|
|
1577
1959
|
}
|
|
1578
1960
|
async performCleanup() {
|
|
1961
|
+
// Waiters must not sit out a probe once the run is stopping
|
|
1962
|
+
this.abortAllReadiness();
|
|
1579
1963
|
// Mark all running tasks for intentional stop
|
|
1580
1964
|
const reason = this.stopRequested ? 'interrupted' : 'fulfilled';
|
|
1581
1965
|
for (const entry of this.runningContinuousTasks.values()) {
|
|
@@ -1716,7 +2100,7 @@ class TaskOrchestrator {
|
|
|
1716
2100
|
}
|
|
1717
2101
|
const continuousDependencies = this.taskGraph.continuousDependencies[task.id];
|
|
1718
2102
|
for (const continuousDependency of continuousDependencies) {
|
|
1719
|
-
neededContinuousTasks.add(continuousDependency);
|
|
2103
|
+
neededContinuousTasks.add(continuousDependency.id);
|
|
1720
2104
|
}
|
|
1721
2105
|
}
|
|
1722
2106
|
for (const [taskId, entry] of this.runningContinuousTasks) {
|