@rushstack/operation-graph
Version:
Library for managing and executing operations in a directed acyclic graph.
146 lines • 7.29 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 OperationGroupRecord_1 = require("./OperationGroupRecord");
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) {
const groupRecordByName = new Map();
this._groupRecordByName = groupRecordByName;
let trackedOperationCount = 0;
for (const operation of operations) {
const { groupName } = operation;
let group = undefined;
if (groupName && !(group = groupRecordByName.get(groupName))) {
group = new OperationGroupRecord_1.OperationGroupRecord(groupName);
groupRecordByName.set(groupName, group);
}
group?.addOperation(operation);
if (!operation.runner?.silent) {
// Only count non-silent operations
trackedOperationCount++;
}
}
this._trackedOperationCount = trackedOperationCount;
this._operations = (0, calculateCriticalPath_1.calculateCriticalPathLengths)(operations);
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);
const groupRecords = this._groupRecordByName;
for (const groupRecord of groupRecords.values()) {
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 { groupName } = operation;
const groupRecord = groupName
? groupRecords.get(groupName)
: undefined;
if (groupRecord && !startedGroups.has(groupRecord)) {
startedGroups.add(groupRecord);
groupRecord.startTimer();
terminal.writeLine(` ---- ${groupRecord.name} started ---- `);
}
},
afterExecute: (operation, state) => {
const { groupName } = operation;
const groupRecord = groupName
? groupRecords.get(groupName)
: undefined;
if (groupRecord) {
groupRecord.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;
}
// Log out the group name and duration if it is the last operation in the group
if (groupRecord?.finished && !finishedGroups.has(groupRecord)) {
finishedGroups.add(groupRecord);
const finishedLoggingWord = groupRecord.hasFailures
? 'encountered an error'
: groupRecord.hasCancellations
? 'cancelled'
: 'finished';
terminal.writeLine(` ---- ${groupRecord.name} ${finishedLoggingWord} (${groupRecord.duration.toFixed(3)}s) ---- `);
}
}
};
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