@rushstack/operation-graph
Version:
Library for managing and executing operations in a directed acyclic graph.
142 lines • 7.08 kB
JavaScript
;
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
exports.OperationExecutionManager = void 0;
const node_core_library_1 = require("@rushstack/node-core-library");
const OperationStatus_1 = require("./OperationStatus");
const calculateCriticalPath_1 = require("./calculateCriticalPath");
const WorkQueue_1 = require("./WorkQueue");
/**
* A class which manages the execution of a set of tasks with interdependencies.
* Initially, and at the end of each task execution, all unblocked tasks
* are added to a ready queue which is then executed. This is done continually until all
* tasks are complete, or prematurely fails if any of the tasks fail.
*
* @beta
*/
class OperationExecutionManager {
constructor(operations) {
let trackedOperationCount = 0;
for (const operation of operations) {
if (!operation.runner?.silent) {
// Only count non-silent operations
trackedOperationCount++;
}
}
this._trackedOperationCount = trackedOperationCount;
this._operations = (0, calculateCriticalPath_1.calculateCriticalPathLengths)(operations);
this._groupRecords = new Set(Array.from(this._operations, (e) => e.group).filter((e) => e !== undefined));
for (const consumer of operations) {
for (const dependency of consumer.dependencies) {
if (!operations.has(dependency)) {
throw new Error(`Operation ${JSON.stringify(consumer.name)} declares a dependency on operation ` +
`${JSON.stringify(dependency.name)} that is not in the set of operations to execute.`);
}
}
}
}
/**
* Executes all operations which have been registered, returning a promise which is resolved when all the
* operations are completed successfully, or rejects when any operation fails.
*/
async executeAsync(executionOptions) {
let hasReportedFailures = false;
const { abortSignal, parallelism, terminal, requestRun } = executionOptions;
if (abortSignal.aborted) {
return OperationStatus_1.OperationStatus.Aborted;
}
const startedGroups = new Set();
const finishedGroups = new Set();
const maxParallelism = Math.min(this._operations.length, parallelism);
for (const groupRecord of this._groupRecords) {
groupRecord.reset();
}
for (const operation of this._operations) {
operation.reset();
}
terminal.writeVerboseLine(`Executing a maximum of ${maxParallelism} simultaneous tasks...`);
const workQueueAbortController = new AbortController();
const abortHandler = () => workQueueAbortController.abort();
abortSignal.addEventListener('abort', abortHandler, { once: true });
try {
const workQueue = new WorkQueue_1.WorkQueue(workQueueAbortController.signal);
const executionContext = {
terminal,
abortSignal,
requestRun,
queueWork: (workFn, priority) => {
return workQueue.pushAsync(workFn, priority);
},
beforeExecute: (operation) => {
// Initialize group if uninitialized and log the group name
const { group, runner } = operation;
if (group) {
if (!startedGroups.has(group)) {
startedGroups.add(group);
group.startTimer();
terminal.writeLine(` ---- ${group.name} started ---- `);
executionOptions.beforeExecuteOperationGroup?.(group);
}
}
if (!runner?.silent) {
executionOptions.beforeExecuteOperation?.(operation);
}
},
afterExecute: (operation, state) => {
const { group, runner } = operation;
if (group) {
group.setOperationAsComplete(operation, state);
}
if (state.status === OperationStatus_1.OperationStatus.Failure) {
// This operation failed. Mark it as such and all reachable dependents as blocked.
// Failed operations get reported, even if silent.
// Generally speaking, silent operations shouldn't be able to fail, so this is a safety measure.
const message = state.error?.message;
if (message) {
terminal.writeErrorLine(message);
}
hasReportedFailures = true;
}
if (!runner?.silent) {
executionOptions.afterExecuteOperation?.(operation);
}
if (group) {
// Log out the group name and duration if it is the last operation in the group
if (group?.finished && !finishedGroups.has(group)) {
finishedGroups.add(group);
const finishedLoggingWord = group.hasFailures
? 'encountered an error'
: group.hasCancellations
? 'cancelled'
: 'finished';
terminal.writeLine(` ---- ${group.name} ${finishedLoggingWord} (${group.duration.toFixed(3)}s) ---- `);
executionOptions.afterExecuteOperationGroup?.(group);
}
}
}
};
const workQueuePromise = node_core_library_1.Async.forEachAsync(workQueue, (workFn) => workFn(), {
concurrency: maxParallelism
});
await Promise.all(this._operations.map((record) => record._executeAsync(executionContext)));
// Terminate queue execution.
workQueueAbortController.abort();
await workQueuePromise;
}
finally {
// Cleanup resources
abortSignal.removeEventListener('abort', abortHandler);
}
const finalStatus = this._trackedOperationCount === 0
? OperationStatus_1.OperationStatus.NoOp
: abortSignal.aborted
? OperationStatus_1.OperationStatus.Aborted
: hasReportedFailures
? OperationStatus_1.OperationStatus.Failure
: OperationStatus_1.OperationStatus.Success;
return finalStatus;
}
}
exports.OperationExecutionManager = OperationExecutionManager;
//# sourceMappingURL=OperationExecutionManager.js.map