UNPKG

@rushstack/operation-graph

Version:

Library for managing and executing operations in a directed acyclic graph.

146 lines 7.29 kB
"use strict"; // 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