nx
Version:
313 lines • 14.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TaskOrchestrator = void 0;
const tslib_1 = require("tslib");
const workspaces_1 = require("../config/workspaces");
const perf_hooks_1 = require("perf_hooks");
const forked_process_task_runner_1 = require("./forked-process-task-runner");
const workspace_root_1 = require("../utils/workspace-root");
const cache_1 = require("./cache");
const utils_1 = require("./utils");
const tasks_schedule_1 = require("./tasks-schedule");
class TaskOrchestrator {
// endregion internal state
constructor(hasher, initiatingProject, projectGraph, taskGraph, options, bail) {
this.hasher = hasher;
this.initiatingProject = initiatingProject;
this.projectGraph = projectGraph;
this.taskGraph = taskGraph;
this.options = options;
this.bail = bail;
this.cache = new cache_1.Cache(this.options);
this.workspace = new workspaces_1.Workspaces(workspace_root_1.workspaceRoot);
this.forkedProcessTaskRunner = new forked_process_task_runner_1.ForkedProcessTaskRunner(this.options);
this.nxJson = this.workspace.readNxJson();
this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.hasher, this.nxJson, this.projectGraph, this.taskGraph, this.workspace, this.options);
// region internal state
this.reverseTaskDeps = (0, utils_1.calculateReverseDeps)(this.taskGraph);
this.completedTasks = {};
this.waitingForTasks = [];
this.groups = [];
this.bailed = false;
}
run() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
// initial scheduling
yield this.tasksSchedule.scheduleNextTasks();
perf_hooks_1.performance.mark('task-execution-begins');
const threads = [];
// initial seeding of the queue
for (let i = 0; i < this.options.parallel; ++i) {
threads.push(this.executeNextBatchOfTasksUsingTaskSchedule());
}
yield Promise.all(threads);
perf_hooks_1.performance.mark('task-execution-ends');
perf_hooks_1.performance.measure('command-execution', 'task-execution-begins', 'task-execution-ends');
this.cache.removeOldCacheRecords();
return this.completedTasks;
});
}
executeNextBatchOfTasksUsingTaskSchedule() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
// completed all the tasks
if (!this.tasksSchedule.hasTasks() || this.bailed) {
return null;
}
const doNotSkipCache = this.options.skipNxCache === false ||
this.options.skipNxCache === undefined;
const batch = this.tasksSchedule.nextBatch();
if (batch) {
const groupId = this.closeGroup();
yield this.applyFromCacheOrRunBatch(doNotSkipCache, batch, groupId);
this.openGroup(groupId);
return this.executeNextBatchOfTasksUsingTaskSchedule();
}
const task = this.tasksSchedule.nextTask();
if (task) {
const groupId = this.closeGroup();
yield this.applyFromCacheOrRunTask(doNotSkipCache, task, groupId);
this.openGroup(groupId);
return this.executeNextBatchOfTasksUsingTaskSchedule();
}
// block until some other task completes, then try again
return new Promise((res) => this.waitingForTasks.push(res)).then(() => this.executeNextBatchOfTasksUsingTaskSchedule());
});
}
// region Applying Cache
applyCachedResults(tasks) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const cacheableTasks = tasks.filter((t) => (0, utils_1.isCacheableTask)(t, this.options));
const res = yield Promise.all(cacheableTasks.map((t) => this.applyCachedResult(t)));
return res.filter((r) => r !== null);
});
}
applyCachedResult(task) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const cachedResult = yield this.cache.get(task);
if (!cachedResult || cachedResult.code !== 0)
return null;
const outputs = (0, utils_1.getOutputs)(this.projectGraph.nodes, task);
const shouldCopyOutputsFromCache = !!outputs.length &&
(yield this.cache.shouldCopyOutputsFromCache({ task, cachedResult }, outputs));
if (shouldCopyOutputsFromCache) {
yield this.cache.copyFilesFromCache(task.hash, cachedResult, outputs);
}
const status = cachedResult.remote
? 'remote-cache'
: shouldCopyOutputsFromCache
? 'local-cache'
: 'local-cache-kept-existing';
this.options.lifeCycle.printTaskTerminalOutput(task, status, cachedResult.terminalOutput);
return {
task,
status,
};
});
}
// endregion Applying Cache
// region Batch
applyFromCacheOrRunBatch(doNotSkipCache, batch, groupId) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const taskEntries = Object.entries(batch.taskGraph.tasks);
const tasks = taskEntries.map(([, task]) => task);
yield this.preRunSteps(tasks, { groupId });
let results = doNotSkipCache ? yield this.applyCachedResults(tasks) : [];
// Run tasks that were not cached
if (results.length !== taskEntries.length) {
const unrunTaskGraph = (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, results.map(({ task }) => task.id));
// cache prep
for (const task of Object.values(unrunTaskGraph.tasks)) {
const taskOutputs = (0, utils_1.getOutputs)(this.projectGraph.nodes, task);
yield this.cache.removeRecordedOutputsHashes(taskOutputs);
}
const batchResults = yield this.runBatch({
executorName: batch.executorName,
taskGraph: unrunTaskGraph,
});
results.push(...batchResults);
}
yield this.postRunSteps(results, { groupId });
const tasksCompleted = taskEntries.filter(([taskId]) => this.completedTasks[taskId]);
// Batch is still not done, run it again
if (tasksCompleted.length !== taskEntries.length) {
yield this.applyFromCacheOrRunBatch(doNotSkipCache, {
executorName: batch.executorName,
taskGraph: (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, tasksCompleted.map(([taskId]) => taskId)),
}, groupId);
}
});
}
runBatch(batch) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
const results = yield this.forkedProcessTaskRunner.forkProcessForBatch(batch);
const batchResultEntries = Object.entries(results);
return batchResultEntries.map(([taskId, result]) => ({
task: this.taskGraph.tasks[taskId],
status: (result.success ? 'success' : 'failure'),
terminalOutput: result.terminalOutput,
}));
}
catch (e) {
return batch.taskGraph.roots.map((rootTaskId) => ({
task: this.taskGraph.tasks[rootTaskId],
status: 'failure',
}));
}
});
}
// endregion Batch
// region Single Task
applyFromCacheOrRunTask(doNotSkipCache, task, groupId) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
yield this.preRunSteps([task], { groupId });
// hash the task here
let results = doNotSkipCache ? yield this.applyCachedResults([task]) : [];
// the task wasn't cached
if (results.length === 0) {
// cache prep
const taskOutputs = (0, utils_1.getOutputs)(this.projectGraph.nodes, task);
yield this.cache.removeRecordedOutputsHashes(taskOutputs);
const { code, terminalOutput } = yield this.runTaskInForkedProcess(task);
results.push({
task,
status: code === 0 ? 'success' : 'failure',
terminalOutput,
});
}
yield this.postRunSteps(results, { groupId });
});
}
runTaskInForkedProcess(task) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
// obtain metadata
const temporaryOutputPath = this.cache.temporaryOutputPath(task);
const streamOutput = (0, utils_1.shouldStreamOutput)(task, this.initiatingProject, this.options);
const pipeOutput = this.pipeOutputCapture(task);
// execution
const { code, terminalOutput } = pipeOutput
? yield this.forkedProcessTaskRunner.forkProcessPipeOutputCapture(task, {
temporaryOutputPath,
streamOutput,
})
: yield this.forkedProcessTaskRunner.forkProcessDirectOutputCapture(task, {
temporaryOutputPath,
streamOutput,
});
return {
code,
terminalOutput,
};
}
catch (e) {
return {
code: 1,
};
}
});
}
// endregion Single Task
// region Lifecycle
preRunSteps(tasks, metadata) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
this.options.lifeCycle.startTasks(tasks, metadata);
});
}
postRunSteps(results, { groupId }) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
// cache the results
yield Promise.all(results
.filter(({ status }) => status !== 'local-cache' &&
status !== 'local-cache-kept-existing' &&
status !== 'remote-cache' &&
status !== 'skipped')
.map((result) => (Object.assign(Object.assign({}, result), { code: result.status === 'local-cache' ||
result.status === 'local-cache-kept-existing' ||
result.status === 'remote-cache' ||
result.status === 'success'
? 0
: 1, outputs: (0, utils_1.getOutputs)(this.projectGraph.nodes, result.task) })))
.filter(({ task, code }) => this.shouldCacheTaskResult(task, code))
.filter(({ terminalOutput, outputs }) => terminalOutput || outputs)
.map(({ task, code, terminalOutput, outputs }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
yield this.cache.put(task, terminalOutput, outputs, code);
})));
this.options.lifeCycle.endTasks(results.map((result) => {
const code = result.status === 'success' ||
result.status === 'local-cache' ||
result.status === 'local-cache-kept-existing' ||
result.status === 'remote-cache'
? 0
: 1;
return {
task: result.task,
status: result.status,
code,
};
}), { groupId });
this.complete(results.map(({ task, status }) => {
return {
taskId: task.id,
status,
};
}));
yield this.tasksSchedule.scheduleNextTasks();
// release blocked threads
this.waitingForTasks.forEach((f) => f(null));
this.waitingForTasks.length = 0;
});
}
complete(taskResults) {
this.tasksSchedule.complete(taskResults.map(({ taskId }) => taskId));
for (const { taskId, status } of taskResults) {
if (this.completedTasks[taskId] === undefined) {
this.completedTasks[taskId] = status;
if (status === 'failure' || status === 'skipped') {
if (this.bail) {
// mark the execution as bailed which will stop all further execution
// only the tasks that are currently running will finish
this.bailed = true;
}
else {
// only mark the packages that depend on the current task as skipped
// other tasks will continue to execute
this.complete(this.reverseTaskDeps[taskId].map((depTaskId) => ({
taskId: depTaskId,
status: 'skipped',
})));
}
}
}
}
}
//endregion Lifecycle
// region utils
pipeOutputCapture(task) {
try {
return ((0, utils_1.getExecutorForTask)(task, this.workspace, this.projectGraph, this.nxJson)
.schema.outputCapture === 'pipe' ||
process.env.NX_STREAM_OUTPUT === 'true');
}
catch (e) {
return false;
}
}
shouldCacheTaskResult(task, code) {
return ((0, utils_1.isCacheableTask)(task, this.options) &&
(process.env.NX_CACHE_FAILURES == 'true' ? true : code === 0));
}
closeGroup() {
for (let i = 0; i < this.options.parallel; i++) {
if (!this.groups[i]) {
this.groups[i] = true;
return i;
}
}
}
openGroup(id) {
this.groups[id] = false;
}
}
exports.TaskOrchestrator = TaskOrchestrator;
//# sourceMappingURL=task-orchestrator.js.map