nx 22.7.0-beta.12 → 22.7.0-beta.13
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/dist/schemas/nx-schema.json +25 -0
- package/dist/schemas/project-schema.json +25 -0
- package/dist/src/adapter/ngcli-adapter.js +11 -12
- package/dist/src/command-line/daemon/daemon.js +8 -4
- package/dist/src/command-line/graph/graph.js +8 -1
- package/dist/src/command-line/show/show-target/utils.js +4 -3
- package/dist/src/command-line/yargs-utils/shared-options.js +5 -1
- package/dist/src/config/nx-json.d.ts +3 -1
- package/dist/src/config/workspace-json-project-json.d.ts +2 -1
- package/dist/src/core/graph/main.js +1 -1
- package/dist/src/daemon/client/client.d.ts +10 -3
- package/dist/src/daemon/client/client.js +21 -31
- package/dist/src/daemon/client/daemon-environment.d.ts +4 -0
- package/dist/src/daemon/client/daemon-environment.js +119 -0
- package/dist/src/daemon/client/daemon-socket-messenger.d.ts +2 -5
- package/dist/src/daemon/client/daemon-socket-messenger.js +1 -1
- package/dist/src/daemon/message-types/daemon-message.d.ts +6 -0
- package/dist/src/daemon/message-types/daemon-message.js +6 -0
- package/dist/src/daemon/server/handle-hash-tasks.d.ts +1 -1
- package/dist/src/daemon/server/handle-hash-tasks.js +1 -1
- package/dist/src/daemon/server/handle-outputs-tracking.d.ts +4 -4
- package/dist/src/daemon/server/handle-outputs-tracking.js +11 -11
- package/dist/src/daemon/server/outputs-tracking.d.ts +18 -3
- package/dist/src/daemon/server/outputs-tracking.js +49 -22
- package/dist/src/daemon/server/project-graph-incremental-recomputation.d.ts +2 -1
- package/dist/src/daemon/server/project-graph-incremental-recomputation.js +20 -4
- package/dist/src/daemon/server/server.js +71 -40
- package/dist/src/executors/run-commands/running-tasks.js +2 -3
- package/dist/src/executors/run-script/run-script.impl.js +16 -8
- package/dist/src/hasher/hash-task.d.ts +9 -1
- package/dist/src/hasher/hash-task.js +41 -14
- package/dist/src/hasher/native-task-hasher-impl.d.ts +1 -1
- package/dist/src/hasher/native-task-hasher-impl.js +4 -6
- package/dist/src/hasher/task-hasher.d.ts +20 -9
- package/dist/src/hasher/task-hasher.js +34 -6
- package/dist/src/native/index.d.ts +34 -7
- package/dist/src/native/native-bindings.js +1 -1
- package/dist/src/native/nx.wasi-browser.js +0 -1
- package/dist/src/native/nx.wasi.cjs +0 -1
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/plugins/js/utils/register.js +83 -4
- package/dist/src/project-graph/error-types.js +31 -11
- package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +1 -0
- package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +9 -0
- package/dist/src/project-graph/plugins/isolation/message-types.d.ts +1 -1
- package/dist/src/project-graph/plugins/isolation/messaging.d.ts +9 -0
- package/dist/src/project-graph/plugins/isolation/messaging.js +2 -0
- package/dist/src/project-graph/plugins/isolation/plugin-worker.js +36 -68
- package/dist/src/project-graph/plugins/loaded-nx-plugin.d.ts +6 -0
- package/dist/src/tasks-runner/cache.d.ts +6 -0
- package/dist/src/tasks-runner/cache.js +58 -0
- package/dist/src/tasks-runner/init-tasks-runner.js +13 -7
- package/dist/src/tasks-runner/run-command.js +13 -5
- package/dist/src/tasks-runner/task-env.js +17 -1
- package/dist/src/tasks-runner/task-orchestrator.d.ts +79 -7
- package/dist/src/tasks-runner/task-orchestrator.js +327 -120
- package/dist/src/tasks-runner/tasks-schedule.d.ts +5 -2
- package/dist/src/tasks-runner/tasks-schedule.js +31 -17
- package/dist/src/tasks-runner/utils.d.ts +3 -3
- package/dist/src/tasks-runner/utils.js +5 -6
- package/package.json +12 -12
- package/dist/src/daemon/client/exec-is-server-available.d.ts +0 -1
- package/dist/src/daemon/client/exec-is-server-available.js +0 -11
- package/dist/src/daemon/client/generate-help-output.d.ts +0 -1
- package/dist/src/daemon/client/generate-help-output.js +0 -26
|
@@ -8,6 +8,7 @@ const fs_1 = require("fs");
|
|
|
8
8
|
const pc = tslib_1.__importStar(require("picocolors"));
|
|
9
9
|
const path_1 = require("path");
|
|
10
10
|
const perf_hooks_1 = require("perf_hooks");
|
|
11
|
+
const project_graph_1 = require("../project-graph/project-graph");
|
|
11
12
|
const run_commands_impl_1 = require("../executors/run-commands/run-commands.impl");
|
|
12
13
|
const hash_task_1 = require("../hasher/hash-task");
|
|
13
14
|
const task_graph_utils_1 = require("./task-graph-utils");
|
|
@@ -45,18 +46,22 @@ class TaskOrchestrator {
|
|
|
45
46
|
this.taskDetails = (0, hash_task_1.getTaskDetails)();
|
|
46
47
|
this.cache = (0, cache_1.getCache)(this.options);
|
|
47
48
|
this.tuiEnabled = (0, is_tui_enabled_1.isTuiEnabled)();
|
|
49
|
+
// Derived from projectGraph once — passed to getExecutorForTask /
|
|
50
|
+
// getCustomHasher so they don't have to re-walk the graph per call.
|
|
51
|
+
this.projects = (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(this.projectGraph).projects;
|
|
48
52
|
this.forkedProcessTaskRunner = new forked_process_task_runner_1.ForkedProcessTaskRunner(this.options, this.tuiEnabled);
|
|
49
53
|
this.runningTasksService = !native_1.IS_WASM
|
|
50
54
|
? new native_1.RunningTasksService((0, db_connection_1.getLocalDbConnection)())
|
|
51
55
|
: null;
|
|
52
|
-
this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.taskGraph, this.options);
|
|
56
|
+
this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options);
|
|
53
57
|
// region internal state
|
|
54
58
|
this.batchEnv = (0, task_env_1.getEnvVariablesForBatchProcess)(this.options.skipNxCache, this.options.captureStderr);
|
|
55
59
|
this.reverseTaskDeps = (0, utils_1.calculateReverseDeps)(this.taskGraph);
|
|
56
60
|
this.initializingTaskIds = new Set(this.initiatingTasks.map((t) => t.id));
|
|
57
61
|
this.processedTasks = new Map();
|
|
58
|
-
this.completedTasks =
|
|
62
|
+
this.completedTasks = new Map();
|
|
59
63
|
this.waitingForTasks = [];
|
|
64
|
+
this.pendingDiscreteWorkers = new Set();
|
|
60
65
|
this.groups = [];
|
|
61
66
|
this.continuousTasksStarted = 0;
|
|
62
67
|
this.bailed = false;
|
|
@@ -89,34 +94,31 @@ class TaskOrchestrator {
|
|
|
89
94
|
await this.init();
|
|
90
95
|
perf_hooks_1.performance.mark('task-execution:start');
|
|
91
96
|
const { discrete, continuous, total } = getThreadPoolSize(this.options, this.taskGraph);
|
|
92
|
-
const threads = [];
|
|
93
97
|
process.stdout.setMaxListeners(total + events_1.defaultMaxListeners);
|
|
94
98
|
process.stderr.setMaxListeners(total + events_1.defaultMaxListeners);
|
|
95
99
|
process.setMaxListeners(total + events_1.defaultMaxListeners);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
const doNotSkipCache = this.options.skipNxCache === false ||
|
|
101
|
+
this.options.skipNxCache === undefined;
|
|
102
|
+
// Start continuous task loops (these run independently)
|
|
103
|
+
const continuousLoops = [];
|
|
100
104
|
for (let i = 0; i < continuous; ++i) {
|
|
101
|
-
|
|
105
|
+
continuousLoops.push(this.executeContinuousTaskLoop(continuous));
|
|
102
106
|
}
|
|
107
|
+
// Set up forced shutdown handler
|
|
108
|
+
const shutdownPromise = this.tuiEnabled
|
|
109
|
+
? new Promise((resolve) => {
|
|
110
|
+
this.options.lifeCycle.registerForcedShutdownCallback(() => {
|
|
111
|
+
this.stopRequested = true;
|
|
112
|
+
resolve(undefined);
|
|
113
|
+
});
|
|
114
|
+
})
|
|
115
|
+
: new Promise((resolve) => {
|
|
116
|
+
this.resolveStopPromise = resolve;
|
|
117
|
+
});
|
|
118
|
+
const coordinatorLoop = this.executeCoordinatorLoop(doNotSkipCache, discrete);
|
|
103
119
|
await Promise.race([
|
|
104
|
-
Promise.all(
|
|
105
|
-
|
|
106
|
-
? [
|
|
107
|
-
new Promise((resolve) => {
|
|
108
|
-
this.options.lifeCycle.registerForcedShutdownCallback(() => {
|
|
109
|
-
// The user force quit the TUI with ctrl+c, so proceed onto cleanup
|
|
110
|
-
this.stopRequested = true;
|
|
111
|
-
resolve(undefined);
|
|
112
|
-
});
|
|
113
|
-
}),
|
|
114
|
-
]
|
|
115
|
-
: [
|
|
116
|
-
new Promise((resolve) => {
|
|
117
|
-
this.resolveStopPromise = resolve;
|
|
118
|
-
}),
|
|
119
|
-
]),
|
|
120
|
+
Promise.all([coordinatorLoop, ...continuousLoops]),
|
|
121
|
+
shutdownPromise,
|
|
120
122
|
]);
|
|
121
123
|
perf_hooks_1.performance.mark('task-execution:end');
|
|
122
124
|
perf_hooks_1.performance.measure('task-execution', 'task-execution:start', 'task-execution:end');
|
|
@@ -124,20 +126,57 @@ class TaskOrchestrator {
|
|
|
124
126
|
this.cache.removeOldCacheRecords();
|
|
125
127
|
}
|
|
126
128
|
await this.cleanup();
|
|
127
|
-
|
|
129
|
+
// Public API (defaultTasksRunner) returns a plain object keyed by
|
|
130
|
+
// task id. Internal state is a Map for faster lookup.
|
|
131
|
+
return Object.fromEntries(this.completedTasks);
|
|
128
132
|
}
|
|
129
133
|
nextBatch() {
|
|
130
134
|
return this.tasksSchedule.nextBatch();
|
|
131
135
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Coordinator loop. All batch operations (hashing, cache resolution)
|
|
138
|
+
* happen on this single thread — no races. Cache misses are dispatched
|
|
139
|
+
* as fire-and-forget workers. Workers signal completion via
|
|
140
|
+
* scheduleNextTasksAndReleaseThreads which wakes all waiting loops.
|
|
141
|
+
*
|
|
142
|
+
* Safety: the dispatch phase (step 5) is fully synchronous — no
|
|
143
|
+
* worker can run during it. So all tasks picked up by nextTask()
|
|
144
|
+
* are guaranteed to be in processedTasks from step 1.
|
|
145
|
+
*/
|
|
146
|
+
async executeCoordinatorLoop(doNotSkipCache, parallelism) {
|
|
135
147
|
while (true) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
if (this.bailed || this.stopRequested)
|
|
149
|
+
break;
|
|
150
|
+
// 1. Hash BEFORE processAll so processTask sees hashes set, and so
|
|
151
|
+
// resolveCachedTasksBulk can look them up in the cache. Each task
|
|
152
|
+
// is hashed with its own task-specific env (project/target .env
|
|
153
|
+
// files, custom hasher env reads) — the shared batchEnv would
|
|
154
|
+
// compute a different cache key than the single-task path and
|
|
155
|
+
// risk stale cache reuse after env changes.
|
|
156
|
+
{
|
|
157
|
+
const { scheduledTasks } = this.tasksSchedule.getAllScheduledTasks();
|
|
158
|
+
const unhashed = scheduledTasks
|
|
159
|
+
.map((id) => this.taskGraph.tasks[id])
|
|
160
|
+
.filter((t) => !t.hash &&
|
|
161
|
+
this.taskGraph.dependencies[t.id].every((depId) => this.completedTasks.has(depId)));
|
|
162
|
+
if (unhashed.length > 1) {
|
|
163
|
+
const perTaskEnvs = {};
|
|
164
|
+
for (const task of unhashed) {
|
|
165
|
+
perTaskEnvs[task.id] = (0, task_env_1.getTaskSpecificEnv)(task, this.projectGraph);
|
|
166
|
+
}
|
|
167
|
+
await (0, hash_task_1.hashTasks)(this.hasher, this.projectGraph, this.taskGraphForHashing, perTaskEnvs, this.taskDetails, unhashed);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// 2. Bulk-resolve cache hits before processTask — avoids N
|
|
171
|
+
// lifecycle calls for tasks that will be resolved from cache.
|
|
172
|
+
if (doNotSkipCache) {
|
|
173
|
+
const resolved = await this.resolveCachedTasksBulk();
|
|
174
|
+
if (resolved)
|
|
175
|
+
continue;
|
|
139
176
|
}
|
|
177
|
+
// 3. Process remaining scheduled tasks (cache misses + non-cacheable).
|
|
140
178
|
this.processAllScheduledTasks();
|
|
179
|
+
// 4. Handle batch executors
|
|
141
180
|
const batch = this.nextBatch();
|
|
142
181
|
if (batch) {
|
|
143
182
|
const groupId = this.closeGroup();
|
|
@@ -145,14 +184,20 @@ class TaskOrchestrator {
|
|
|
145
184
|
this.openGroup(groupId);
|
|
146
185
|
continue;
|
|
147
186
|
}
|
|
148
|
-
|
|
149
|
-
|
|
187
|
+
// 5. Dispatch cache misses as individual workers
|
|
188
|
+
while (this.pendingDiscreteWorkers.size < parallelism) {
|
|
189
|
+
const task = this.tasksSchedule.nextTask((t) => !t.continuous);
|
|
190
|
+
if (!task)
|
|
191
|
+
break;
|
|
150
192
|
const groupId = this.closeGroup();
|
|
151
|
-
|
|
152
|
-
this.openGroup(groupId);
|
|
153
|
-
continue;
|
|
193
|
+
this.dispatchDiscreteWorker(doNotSkipCache, task, groupId);
|
|
154
194
|
}
|
|
155
|
-
//
|
|
195
|
+
// 6. Nothing left to dispatch and nothing in flight — done.
|
|
196
|
+
if (!this.tasksSchedule.hasTasks() &&
|
|
197
|
+
this.pendingDiscreteWorkers.size === 0) {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
// 7. Wait for a worker to finish (woken by scheduleNextTasksAndReleaseThreads)
|
|
156
201
|
await new Promise((res) => this.waitingForTasks.push(res));
|
|
157
202
|
}
|
|
158
203
|
}
|
|
@@ -186,14 +231,6 @@ class TaskOrchestrator {
|
|
|
186
231
|
await new Promise((res) => this.waitingForTasks.push(res));
|
|
187
232
|
}
|
|
188
233
|
}
|
|
189
|
-
processTasks(taskIds) {
|
|
190
|
-
for (const taskId of taskIds) {
|
|
191
|
-
// Task is already handled or being handled
|
|
192
|
-
if (!this.processedTasks.has(taskId)) {
|
|
193
|
-
this.processedTasks.set(taskId, this.processTask(taskId));
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
234
|
// region Processing Scheduled Tasks
|
|
198
235
|
async processTask(taskId) {
|
|
199
236
|
const task = this.taskGraph.tasks[taskId];
|
|
@@ -206,41 +243,117 @@ class TaskOrchestrator {
|
|
|
206
243
|
}
|
|
207
244
|
processAllScheduledTasks() {
|
|
208
245
|
const { scheduledTasks } = this.tasksSchedule.getAllScheduledTasks();
|
|
209
|
-
|
|
246
|
+
for (const taskId of scheduledTasks) {
|
|
247
|
+
// Task is already handled or being handled
|
|
248
|
+
if (!this.processedTasks.has(taskId)) {
|
|
249
|
+
this.processedTasks.set(taskId, this.processTask(taskId));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
210
252
|
}
|
|
211
253
|
// endregion Processing Scheduled Tasks
|
|
212
254
|
// region Applying Cache
|
|
213
255
|
async applyCachedResults(tasks) {
|
|
214
256
|
const cacheableTasks = tasks.filter((t) => (0, utils_1.isCacheableTask)(t, this.options));
|
|
215
|
-
|
|
216
|
-
|
|
257
|
+
if (cacheableTasks.length === 0)
|
|
258
|
+
return [];
|
|
259
|
+
const cacheHits = await this.fetchCacheHits(cacheableTasks);
|
|
260
|
+
if (cacheHits.length === 0)
|
|
261
|
+
return [];
|
|
262
|
+
return this.finalizeCacheHits(cacheHits);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Batch cache lookup + filter to successful entries. Handles both
|
|
266
|
+
* local (one rarray SQL call) and remote (parallel HTTP retrievals)
|
|
267
|
+
* inside DbCache.getBatch.
|
|
268
|
+
*/
|
|
269
|
+
async fetchCacheHits(tasks) {
|
|
270
|
+
const batchResults = await this.cache.getBatch(tasks);
|
|
271
|
+
const cacheHits = [];
|
|
272
|
+
for (const task of tasks) {
|
|
273
|
+
const cachedResult = batchResults.get(task.hash);
|
|
274
|
+
if (cachedResult && cachedResult.code === 0) {
|
|
275
|
+
cacheHits.push({ task, cachedResult });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return cacheHits;
|
|
217
279
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
280
|
+
/**
|
|
281
|
+
* For each confirmed cache hit: decide whether to copy outputs from
|
|
282
|
+
* the cache (skipping if the on-disk outputs already match the
|
|
283
|
+
* recorded hash), copy in parallel, derive the task status, print
|
|
284
|
+
* terminal output, and return the assembled results.
|
|
285
|
+
*/
|
|
286
|
+
async finalizeCacheHits(cacheHits) {
|
|
287
|
+
// Batch-check which tasks need outputs copied from cache. Remote
|
|
288
|
+
// cache entries come pre-restored to their output dirs when the
|
|
289
|
+
// db cache is on, so we only check ones that aren't.
|
|
290
|
+
const usingDbCache = (0, cache_1.dbCacheEnabled)();
|
|
291
|
+
const tasksNeedingOutputCheck = cacheHits.filter(({ task, cachedResult }) => task.outputs.length > 0 && (!cachedResult.remote || !usingDbCache));
|
|
292
|
+
const shouldCopyMap = await this.shouldCopyOutputsFromCacheBatch(tasksNeedingOutputCheck.map(({ task }) => ({
|
|
293
|
+
outputs: task.outputs,
|
|
294
|
+
hash: task.hash,
|
|
295
|
+
})));
|
|
296
|
+
// Copy outputs in parallel for tasks that need it.
|
|
297
|
+
await Promise.all(cacheHits.map(async ({ task, cachedResult }) => {
|
|
298
|
+
if (shouldCopyMap.get(task.hash)) {
|
|
299
|
+
await this.cache.copyFilesFromCache(task.hash, cachedResult, task.outputs);
|
|
300
|
+
}
|
|
301
|
+
}));
|
|
302
|
+
// Derive status, print terminal output, build results.
|
|
303
|
+
const results = [];
|
|
304
|
+
for (const { task, cachedResult } of cacheHits) {
|
|
305
|
+
const shouldCopy = shouldCopyMap.get(task.hash) ?? false;
|
|
306
|
+
const status = cachedResult.remote
|
|
307
|
+
? 'remote-cache'
|
|
308
|
+
: shouldCopy
|
|
309
|
+
? 'local-cache'
|
|
310
|
+
: 'local-cache-kept-existing';
|
|
311
|
+
this.options.lifeCycle.printTaskTerminalOutput(task, status, cachedResult.terminalOutput);
|
|
312
|
+
results.push({
|
|
313
|
+
task,
|
|
314
|
+
code: cachedResult.code,
|
|
315
|
+
status,
|
|
316
|
+
terminalOutput: cachedResult.terminalOutput,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return results;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Coordinator wrapper around {@link resolveCachedTasks}: peeks at
|
|
323
|
+
* scheduledTasks (without removing anything from the schedule),
|
|
324
|
+
* filters to cacheable hashed discrete candidates, and delegates the
|
|
325
|
+
* cache fetch + lifecycle to the public method. Returns true if any
|
|
326
|
+
* tasks were resolved.
|
|
327
|
+
*
|
|
328
|
+
* The coordinator relies on this running unconditionally (when cache
|
|
329
|
+
* is enabled): tasks dispatched in step 5 via runTaskDirectly skip
|
|
330
|
+
* their own cache lookup on the assumption that this has already
|
|
331
|
+
* confirmed them as misses. Don't add length-based bails.
|
|
332
|
+
*/
|
|
333
|
+
async resolveCachedTasksBulk() {
|
|
334
|
+
const { scheduledTasks } = this.tasksSchedule.getAllScheduledTasks();
|
|
335
|
+
const candidates = [];
|
|
336
|
+
for (const id of scheduledTasks) {
|
|
337
|
+
const task = this.taskGraph.tasks[id];
|
|
338
|
+
if (task.hash &&
|
|
339
|
+
!task.continuous &&
|
|
340
|
+
(0, utils_1.isCacheableTask)(task, this.options)) {
|
|
341
|
+
candidates.push(task);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (candidates.length === 0)
|
|
345
|
+
return false;
|
|
346
|
+
// postRunSteps → complete() → tasksSchedule.complete() will filter
|
|
347
|
+
// resolved hits out of scheduledTasks before we return, so there's
|
|
348
|
+
// no need to mutate the schedule here.
|
|
349
|
+
const groupId = this.closeGroup();
|
|
350
|
+
try {
|
|
351
|
+
const results = await this.resolveCachedTasks(true, candidates, groupId);
|
|
352
|
+
return results.length > 0;
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
this.openGroup(groupId);
|
|
232
356
|
}
|
|
233
|
-
const status = cachedResult.remote
|
|
234
|
-
? 'remote-cache'
|
|
235
|
-
: shouldCopyOutputsFromCache
|
|
236
|
-
? 'local-cache'
|
|
237
|
-
: 'local-cache-kept-existing';
|
|
238
|
-
this.options.lifeCycle.printTaskTerminalOutput(task, status, cachedResult.terminalOutput);
|
|
239
|
-
return {
|
|
240
|
-
code: cachedResult.code,
|
|
241
|
-
task,
|
|
242
|
-
status,
|
|
243
|
-
};
|
|
244
357
|
}
|
|
245
358
|
// endregion Applying Cache
|
|
246
359
|
// region Batch
|
|
@@ -298,7 +411,15 @@ class TaskOrchestrator {
|
|
|
298
411
|
return { cachedResults, needsRehashAfterExecution };
|
|
299
412
|
}
|
|
300
413
|
async hashBatchTasks(tasks) {
|
|
301
|
-
|
|
414
|
+
// Batch executors run every task in the same forked process, but
|
|
415
|
+
// each task still has its own .env files / custom-hasher env — use
|
|
416
|
+
// task-specific env for hashing so the cache key matches the
|
|
417
|
+
// single-task path.
|
|
418
|
+
const perTaskEnvs = {};
|
|
419
|
+
for (const task of tasks) {
|
|
420
|
+
perTaskEnvs[task.id] = (0, task_env_1.getTaskSpecificEnv)(task, this.projectGraph);
|
|
421
|
+
}
|
|
422
|
+
await (0, hash_task_1.hashTasks)(this.hasher, this.projectGraph, this.taskGraphForHashing, perTaskEnvs, this.taskDetails, tasks);
|
|
302
423
|
}
|
|
303
424
|
async applyFromCacheOrRunBatch(doNotSkipCache, batch, groupId) {
|
|
304
425
|
const applyFromCacheOrRunBatchStart = perf_hooks_1.performance.mark('TaskOrchestrator-apply-from-cache-or-run-batch:start');
|
|
@@ -341,12 +462,12 @@ class TaskOrchestrator {
|
|
|
341
462
|
}
|
|
342
463
|
// Update batch status based on all task results
|
|
343
464
|
const hasFailures = taskEntries.some(([taskId]) => {
|
|
344
|
-
const status = this.completedTasks
|
|
465
|
+
const status = this.completedTasks.get(taskId);
|
|
345
466
|
return status === 'failure' || status === 'skipped';
|
|
346
467
|
});
|
|
347
468
|
this.options.lifeCycle.setBatchStatus?.(batch.id, hasFailures ? "Failure" /* BatchStatus.Failure */ : "Success" /* BatchStatus.Success */);
|
|
348
469
|
this.forkedProcessTaskRunner.cleanUpBatchProcesses();
|
|
349
|
-
const tasksCompleted = taskEntries.filter(([taskId]) => this.completedTasks
|
|
470
|
+
const tasksCompleted = taskEntries.filter(([taskId]) => this.completedTasks.has(taskId));
|
|
350
471
|
// Batch is still not done, run it again
|
|
351
472
|
if (tasksCompleted.length !== taskEntries.length) {
|
|
352
473
|
await this.applyFromCacheOrRunBatch(doNotSkipCache, {
|
|
@@ -420,52 +541,120 @@ class TaskOrchestrator {
|
|
|
420
541
|
}
|
|
421
542
|
// endregion Batch
|
|
422
543
|
// region Single Task
|
|
423
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Bulk-resolve cache hits for a set of tasks: fetch cached entries,
|
|
546
|
+
* copy outputs as needed, fire lifecycle, and return the TaskResults
|
|
547
|
+
* for the hits. Tasks that aren't in the cache (or aren't cacheable)
|
|
548
|
+
* are silently omitted from the return value — callers are responsible
|
|
549
|
+
* for running those via {@link runTaskDirectly}.
|
|
550
|
+
*
|
|
551
|
+
* Fires scheduleTask lifecycle for hits that haven't been through
|
|
552
|
+
* processAllScheduledTasks yet. That's a coordinator gap-filler and
|
|
553
|
+
* a no-op for callers that pre-process the schedule.
|
|
554
|
+
*
|
|
555
|
+
* The caller provides `groupId` — cache hits share one slot since they
|
|
556
|
+
* don't actually compete for parallelism.
|
|
557
|
+
*/
|
|
558
|
+
async resolveCachedTasks(doNotSkipCache, tasks, groupId) {
|
|
559
|
+
if (!doNotSkipCache || tasks.length === 0)
|
|
560
|
+
return [];
|
|
561
|
+
const cacheableTasks = tasks.filter((t) => (0, utils_1.isCacheableTask)(t, this.options));
|
|
562
|
+
if (cacheableTasks.length === 0)
|
|
563
|
+
return [];
|
|
564
|
+
const cacheHits = await this.fetchCacheHits(cacheableTasks);
|
|
565
|
+
if (cacheHits.length === 0)
|
|
566
|
+
return [];
|
|
567
|
+
// scheduleTask lifecycle for hits the coordinator resolved before
|
|
568
|
+
// processAllScheduledTasks could fire it. No-op for callers that
|
|
569
|
+
// already ran processAllScheduledTasks (every hit is in processedTasks).
|
|
570
|
+
await Promise.all(cacheHits
|
|
571
|
+
.filter(({ task }) => !this.processedTasks.has(task.id))
|
|
572
|
+
.map(({ task }) => this.options.lifeCycle.scheduleTask(task)));
|
|
573
|
+
const hitTasks = cacheHits.map((h) => h.task);
|
|
574
|
+
await this.preRunSteps(hitTasks, { groupId });
|
|
575
|
+
const results = await this.finalizeCacheHits(cacheHits);
|
|
576
|
+
await this.postRunSteps(results, doNotSkipCache, { groupId });
|
|
577
|
+
return results;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Fire a discrete-task worker and track it in pendingDiscreteWorkers until
|
|
581
|
+
* it settles. Uses runTaskDirectly (not applyFromCacheOrRun*) because
|
|
582
|
+
* resolveCachedTasksBulk already confirmed this task is a cache miss —
|
|
583
|
+
* another lookup would re-query the DB and (for Nx Cloud users) repeat
|
|
584
|
+
* the remote HTTP retrieval.
|
|
585
|
+
*/
|
|
586
|
+
dispatchDiscreteWorker(doNotSkipCache, task, groupId) {
|
|
587
|
+
const worker = this.runTaskDirectly(doNotSkipCache, task, groupId)
|
|
588
|
+
.catch((e) => this.handleDiscreteWorkerFailure(doNotSkipCache, task, groupId, e))
|
|
589
|
+
.finally(() => {
|
|
590
|
+
this.openGroup(groupId);
|
|
591
|
+
this.pendingDiscreteWorkers.delete(worker);
|
|
592
|
+
// Wake coordinator — the delete above may satisfy the exit condition
|
|
593
|
+
// (pendingDiscreteWorkers.size === 0) that was missed when
|
|
594
|
+
// scheduleNextTasksAndReleaseThreads fired earlier.
|
|
595
|
+
this.waitingForTasks.forEach((f) => f(null));
|
|
596
|
+
this.waitingForTasks.length = 0;
|
|
597
|
+
});
|
|
598
|
+
this.pendingDiscreteWorkers.add(worker);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Route a worker rejection (e.g. remote cache errors) through the normal
|
|
602
|
+
* failure path instead of letting it become an unhandled promise. Guard
|
|
603
|
+
* against double-finalize: completeTasks() populates `completedTasks`,
|
|
604
|
+
* so a rejection arriving after postRunSteps has already finalized the
|
|
605
|
+
* task must not run postRunSteps again.
|
|
606
|
+
*/
|
|
607
|
+
async handleDiscreteWorkerFailure(doNotSkipCache, task, groupId, e) {
|
|
608
|
+
if (this.completedTasks.has(task.id))
|
|
609
|
+
return;
|
|
610
|
+
const terminalOutput = e?.message ?? '';
|
|
611
|
+
this.options.lifeCycle.printTaskTerminalOutput(task, 'failure', terminalOutput);
|
|
612
|
+
await this.postRunSteps([{ task, status: 'failure', terminalOutput }], doNotSkipCache, { groupId });
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Spawn and wait on a task's child process, unconditionally — no cache
|
|
616
|
+
* lookup. Callers must have already confirmed the task is a cache miss
|
|
617
|
+
* (or disabled caching entirely).
|
|
618
|
+
*/
|
|
619
|
+
async runTaskDirectly(doNotSkipCache, task, groupId) {
|
|
424
620
|
// Wait for task to be processed
|
|
425
621
|
const taskSpecificEnv = await this.processedTasks.get(task.id);
|
|
426
622
|
await this.preRunSteps([task], { groupId });
|
|
427
623
|
const pipeOutput = await this.pipeOutputCapture(task);
|
|
428
|
-
// obtain metadata
|
|
429
624
|
const temporaryOutputPath = this.cache.temporaryOutputPath(task);
|
|
430
625
|
const streamOutput = this.outputStyle === 'static'
|
|
431
626
|
? false
|
|
432
627
|
: (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
|
|
433
|
-
|
|
628
|
+
const env = pipeOutput
|
|
434
629
|
? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, process.env.FORCE_COLOR === undefined
|
|
435
630
|
? 'true'
|
|
436
631
|
: process.env.FORCE_COLOR, this.options.skipNxCache, this.options.captureStderr, null, null)
|
|
437
632
|
: (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
|
|
438
|
-
let results = doNotSkipCache ? await this.applyCachedResults([task]) : [];
|
|
439
|
-
// the task wasn't cached
|
|
440
633
|
let resolveDiscreteExit;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
});
|
|
458
|
-
}
|
|
634
|
+
const discreteExitHandled = new Promise((r) => (resolveDiscreteExit = r));
|
|
635
|
+
this.discreteTaskExitHandled.set(task.id, discreteExitHandled);
|
|
636
|
+
const childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
|
|
637
|
+
this.runningDiscreteTasks.set(task.id, {
|
|
638
|
+
runningTask: childProcess,
|
|
639
|
+
stopping: false,
|
|
640
|
+
});
|
|
641
|
+
const { code, terminalOutput } = await childProcess.getResults();
|
|
642
|
+
const isStopping = this.runningDiscreteTasks.get(task.id)?.stopping ?? false;
|
|
643
|
+
this.runningDiscreteTasks.delete(task.id);
|
|
644
|
+
const result = {
|
|
645
|
+
task,
|
|
646
|
+
code,
|
|
647
|
+
status: isStopping ? 'stopped' : code === 0 ? 'success' : 'failure',
|
|
648
|
+
terminalOutput,
|
|
649
|
+
};
|
|
459
650
|
try {
|
|
460
|
-
await this.postRunSteps(
|
|
651
|
+
await this.postRunSteps([result], doNotSkipCache, { groupId });
|
|
461
652
|
}
|
|
462
653
|
finally {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
resolveDiscreteExit();
|
|
466
|
-
}
|
|
654
|
+
this.discreteTaskExitHandled.delete(task.id);
|
|
655
|
+
resolveDiscreteExit();
|
|
467
656
|
}
|
|
468
|
-
return
|
|
657
|
+
return result;
|
|
469
658
|
}
|
|
470
659
|
async runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput) {
|
|
471
660
|
const shouldPrefix = streamOutput &&
|
|
@@ -475,7 +664,7 @@ class TaskOrchestrator {
|
|
|
475
664
|
if (process.env.NX_RUN_COMMANDS_DIRECTLY !== 'false' &&
|
|
476
665
|
targetConfiguration.executor === 'nx:run-commands') {
|
|
477
666
|
try {
|
|
478
|
-
const { schema } = (0, utils_1.getExecutorForTask)(task, this.
|
|
667
|
+
const { schema } = (0, utils_1.getExecutorForTask)(task, this.projects);
|
|
479
668
|
const combinedOptions = (0, params_1.combineOptionsForExecutor)(task.overrides, task.target.configuration ?? targetConfiguration.defaultConfiguration, targetConfiguration, schema, task.target.project, (0, path_1.relative)(task.projectRoot ?? workspace_root_1.workspaceRoot, process.cwd()), process.env.NX_VERBOSE_LOGGING === 'true');
|
|
480
669
|
if (combinedOptions.env) {
|
|
481
670
|
env = {
|
|
@@ -680,13 +869,21 @@ class TaskOrchestrator {
|
|
|
680
869
|
}
|
|
681
870
|
async postRunSteps(results, doNotSkipCache, { groupId }) {
|
|
682
871
|
const now = Date.now();
|
|
683
|
-
|
|
872
|
+
const tasksToRecord = [];
|
|
873
|
+
for (const { task, status } of results) {
|
|
684
874
|
// Only set endTime as fallback (batch provides timing via result.task)
|
|
685
875
|
task.endTime ??= now;
|
|
686
|
-
|
|
687
|
-
|
|
876
|
+
// Skip recording for tasks whose outputs already match the cache —
|
|
877
|
+
// the daemon already has the correct hash recorded.
|
|
878
|
+
if (!this.stopRequested &&
|
|
879
|
+
task.outputs.length > 0 &&
|
|
880
|
+
status !== 'local-cache-kept-existing') {
|
|
881
|
+
tasksToRecord.push({ outputs: task.outputs, hash: task.hash });
|
|
688
882
|
}
|
|
689
883
|
}
|
|
884
|
+
if (tasksToRecord.length > 0) {
|
|
885
|
+
await this.recordOutputsHashBatch(tasksToRecord);
|
|
886
|
+
}
|
|
690
887
|
if (doNotSkipCache && !this.stopRequested) {
|
|
691
888
|
// cache the results
|
|
692
889
|
perf_hooks_1.performance.mark('cache-results-start');
|
|
@@ -744,7 +941,7 @@ class TaskOrchestrator {
|
|
|
744
941
|
const taskIds = [];
|
|
745
942
|
for (const { task, status, terminalOutput } of results) {
|
|
746
943
|
taskIds.push(task.id);
|
|
747
|
-
if (this.completedTasks
|
|
944
|
+
if (!this.completedTasks.has(task.id) && status !== 'skipped') {
|
|
748
945
|
tasksToReport.push({
|
|
749
946
|
task,
|
|
750
947
|
status,
|
|
@@ -766,9 +963,9 @@ class TaskOrchestrator {
|
|
|
766
963
|
// 3. Set completedTasks + update TUI + collect dependent tasks to skip
|
|
767
964
|
const dependentTasksToSkip = [];
|
|
768
965
|
for (const { task, status, displayStatus } of results) {
|
|
769
|
-
if (this.completedTasks
|
|
966
|
+
if (this.completedTasks.has(task.id))
|
|
770
967
|
continue;
|
|
771
|
-
this.completedTasks
|
|
968
|
+
this.completedTasks.set(task.id, status);
|
|
772
969
|
if (this.tuiEnabled) {
|
|
773
970
|
this.options.lifeCycle.setTaskStatus(task.id, displayStatus ?? (0, native_1.parseTaskStatus)(status));
|
|
774
971
|
}
|
|
@@ -814,7 +1011,7 @@ class TaskOrchestrator {
|
|
|
814
1011
|
if (this.tuiEnabled) {
|
|
815
1012
|
return true;
|
|
816
1013
|
}
|
|
817
|
-
const { schema } = (0, utils_1.getExecutorForTask)(task, this.
|
|
1014
|
+
const { schema } = (0, utils_1.getExecutorForTask)(task, this.projects);
|
|
818
1015
|
return (schema.outputCapture === 'pipe' ||
|
|
819
1016
|
process.env.NX_STREAM_OUTPUT === 'true');
|
|
820
1017
|
}
|
|
@@ -837,23 +1034,33 @@ class TaskOrchestrator {
|
|
|
837
1034
|
openGroup(id) {
|
|
838
1035
|
this.groups[id] = false;
|
|
839
1036
|
}
|
|
840
|
-
async
|
|
1037
|
+
async shouldCopyOutputsFromCacheBatch(tasks) {
|
|
1038
|
+
const resultMap = new Map();
|
|
1039
|
+
if (tasks.length === 0)
|
|
1040
|
+
return resultMap;
|
|
841
1041
|
if (this.daemon?.enabled()) {
|
|
842
|
-
|
|
1042
|
+
const matches = await this.daemon.outputsHashesMatchBatch(tasks);
|
|
1043
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
1044
|
+
resultMap.set(tasks[i].hash, !matches[i]);
|
|
1045
|
+
}
|
|
843
1046
|
}
|
|
844
1047
|
else {
|
|
845
|
-
|
|
1048
|
+
// No daemon → can't verify on-disk outputs, always copy.
|
|
1049
|
+
for (const task of tasks) {
|
|
1050
|
+
resultMap.set(task.hash, true);
|
|
1051
|
+
}
|
|
846
1052
|
}
|
|
1053
|
+
return resultMap;
|
|
847
1054
|
}
|
|
848
|
-
async
|
|
1055
|
+
async recordOutputsHashBatch(entries) {
|
|
849
1056
|
if (this.daemon?.enabled()) {
|
|
850
|
-
return this.daemon.
|
|
1057
|
+
return this.daemon.recordOutputsHashBatch(entries);
|
|
851
1058
|
}
|
|
852
1059
|
}
|
|
853
1060
|
// endregion utils
|
|
854
1061
|
async handleContinuousTaskExit(code, task, groupId, ownsRunningTasksService) {
|
|
855
1062
|
// If cleanup already completed this task, nothing left to do
|
|
856
|
-
if (this.completedTasks
|
|
1063
|
+
if (this.completedTasks.has(task.id)) {
|
|
857
1064
|
return;
|
|
858
1065
|
}
|
|
859
1066
|
const stoppingReason = this.runningContinuousTasks.get(task.id)?.stoppingReason;
|
|
@@ -867,7 +1074,7 @@ class TaskOrchestrator {
|
|
|
867
1074
|
}
|
|
868
1075
|
}
|
|
869
1076
|
async completeContinuousTask(task, groupId, ownsRunningTasksService, reason) {
|
|
870
|
-
if (this.completedTasks
|
|
1077
|
+
if (this.completedTasks.has(task.id))
|
|
871
1078
|
return;
|
|
872
1079
|
this.runningContinuousTasks.delete(task.id);
|
|
873
1080
|
if (ownsRunningTasksService) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DefaultTasksRunnerOptions } from './default-tasks-runner';
|
|
2
2
|
import { Task, TaskGraph } from '../config/task-graph';
|
|
3
3
|
import { ProjectGraph } from '../config/project-graph';
|
|
4
|
+
import { ProjectConfiguration } from '../config/workspace-json-project-json';
|
|
4
5
|
export interface Batch {
|
|
5
6
|
id: string;
|
|
6
7
|
executorName: string;
|
|
@@ -8,6 +9,7 @@ export interface Batch {
|
|
|
8
9
|
}
|
|
9
10
|
export declare class TasksSchedule {
|
|
10
11
|
private readonly projectGraph;
|
|
12
|
+
private readonly projects;
|
|
11
13
|
private readonly taskGraph;
|
|
12
14
|
private readonly options;
|
|
13
15
|
private notScheduledTaskGraph;
|
|
@@ -22,7 +24,7 @@ export declare class TasksSchedule {
|
|
|
22
24
|
private estimatedTaskTimings;
|
|
23
25
|
private projectDependencies;
|
|
24
26
|
private batchCounters;
|
|
25
|
-
constructor(projectGraph: ProjectGraph, taskGraph: TaskGraph, options: DefaultTasksRunnerOptions);
|
|
27
|
+
constructor(projectGraph: ProjectGraph, projects: Record<string, ProjectConfiguration>, taskGraph: TaskGraph, options: DefaultTasksRunnerOptions);
|
|
26
28
|
init(): Promise<void>;
|
|
27
29
|
scheduleNextTasks(): Promise<void>;
|
|
28
30
|
hasTasks(): boolean;
|
|
@@ -35,7 +37,8 @@ export declare class TasksSchedule {
|
|
|
35
37
|
nextBatch(): Batch;
|
|
36
38
|
getIncompleteTasks(): Task[];
|
|
37
39
|
private scheduleTasks;
|
|
38
|
-
private
|
|
40
|
+
private scheduleTaskBatch;
|
|
41
|
+
private sortScheduledTasks;
|
|
39
42
|
private scheduleBatches;
|
|
40
43
|
private scheduleBatch;
|
|
41
44
|
private processTaskForBatches;
|