UNPKG

@rushstack/operation-graph

Version:

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

138 lines 6.72 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import { Async } from '@rushstack/node-core-library'; import { OperationStatus } from './OperationStatus'; import { calculateCriticalPathLengths } from './calculateCriticalPath'; import { WorkQueue } from './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 */ export 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 = 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.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(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.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 = 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.NoOp : abortSignal.aborted ? OperationStatus.Aborted : hasReportedFailures ? OperationStatus.Failure : OperationStatus.Success; return finalStatus; } } //# sourceMappingURL=OperationExecutionManager.js.map