Skip to content

Commit 40ee4bc

Browse files
committed
Split TaskRunner and decouple it from TaskSelector
1 parent daeb7ce commit 40ee4bc

File tree

8 files changed

+321
-234
lines changed

8 files changed

+321
-234
lines changed

apps/rush-lib/src/cli/scriptActions/BulkScriptAction.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { Stopwatch } from '../../utilities/Stopwatch';
2121
import { AlreadyReportedError } from '../../utilities/AlreadyReportedError';
2222
import { BaseScriptAction, IBaseScriptActionOptions } from './BaseScriptAction';
2323
import { FileSystem } from '@microsoft/node-core-library';
24+
import { TaskRunner } from '../../logic/taskRunner/TaskRunner';
25+
import { TaskCollection } from '../../logic/taskRunner/TaskCollection';
2426

2527
/**
2628
* Constructor parameters for BulkScriptAction.
@@ -94,22 +96,29 @@ export class BulkScriptAction extends BaseScriptAction {
9496

9597
const changedProjectsOnly: boolean = this.actionName === 'build' && this._changedProjectsOnly.value;
9698

97-
const tasks: TaskSelector = new TaskSelector({
99+
const taskSelector: TaskSelector = new TaskSelector({
98100
rushConfiguration: this.rushConfiguration,
99101
toFlags: this._mergeToProjects(),
100102
fromFlags: this._fromFlag.values,
101103
commandToRun: this._commandToRun,
102104
customParameterValues,
103-
isQuietMode,
104-
parallelism,
105+
isQuietMode: isQuietMode,
105106
isIncrementalBuildAllowed: this.actionName === 'build',
106-
changedProjectsOnly,
107107
ignoreMissingScript: this._ignoreMissingScript,
108-
ignoreDependencyOrder: this._ignoreDependencyOrder,
108+
ignoreDependencyOrder: this._ignoreDependencyOrder
109+
});
110+
111+
// Register all tasks with the task collection
112+
const taskCollection: TaskCollection = taskSelector.registerTasks();
113+
114+
const taskRunner: TaskRunner = new TaskRunner(taskCollection.getOrderedTasks(), {
115+
quietMode: isQuietMode,
116+
parallelism: parallelism,
117+
changedProjectsOnly: changedProjectsOnly,
109118
allowWarningsInSuccessfulBuild: this._allowWarningsInSuccessfulBuild
110119
});
111120

112-
return tasks.execute().then(() => {
121+
return taskRunner.execute().then(() => {
113122
stopwatch.stop();
114123
console.log(colors.green(`rush ${this.actionName} (${stopwatch.toString()})`));
115124
this._doAfterTask(stopwatch, true);

apps/rush-lib/src/logic/TaskSelector.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {
55
import { RushConfigurationProject } from '../api/RushConfigurationProject';
66
import { JsonFile } from '@microsoft/node-core-library';
77

8-
import { TaskRunner } from '../logic/taskRunner/TaskRunner';
98
import { ProjectTask } from '../logic/taskRunner/ProjectTask';
109
import { PackageChangeAnalyzer } from './PackageChangeAnalyzer';
10+
import { TaskCollection } from './taskRunner/TaskCollection';
1111

1212
export interface ITaskSelectorConstructor {
1313
rushConfiguration: RushConfiguration;
@@ -16,12 +16,9 @@ export interface ITaskSelectorConstructor {
1616
commandToRun: string;
1717
customParameterValues: string[];
1818
isQuietMode: boolean;
19-
parallelism: string | undefined;
2019
isIncrementalBuildAllowed: boolean;
21-
changedProjectsOnly: boolean;
2220
ignoreMissingScript: boolean;
2321
ignoreDependencyOrder: boolean;
24-
allowWarningsInSuccessfulBuild: boolean;
2522
}
2623

2724
/**
@@ -33,7 +30,7 @@ export interface ITaskSelectorConstructor {
3330
* This class is currently only used by CustomRushAction
3431
*/
3532
export class TaskSelector {
36-
private _taskRunner: TaskRunner;
33+
private _taskCollection: TaskCollection;
3734
private _dependentList: Map<string, Set<string>>;
3835
private _rushLinkJson: IRushLinkJson;
3936
private _options: ITaskSelectorConstructor;
@@ -43,11 +40,8 @@ export class TaskSelector {
4340
this._options = options;
4441

4542
this._packageChangeAnalyzer = new PackageChangeAnalyzer(options.rushConfiguration);
46-
this._taskRunner = new TaskRunner({
47-
quietMode: this._options.isQuietMode,
48-
parallelism: this._options.parallelism,
49-
changedProjectsOnly: this._options.changedProjectsOnly,
50-
allowWarningsInSuccessfulBuild: this._options.allowWarningsInSuccessfulBuild
43+
this._taskCollection = new TaskCollection({
44+
quietMode: options.isQuietMode
5145
});
5246

5347
try {
@@ -56,7 +50,9 @@ export class TaskSelector {
5650
throw new Error(`Could not read "${this._options.rushConfiguration.rushLinkJsonFilename}".`
5751
+ ` Did you run "rush install" or "rush update"?`);
5852
}
53+
}
5954

55+
public registerTasks(): TaskCollection {
6056
if (this._options.toFlags.length > 0) {
6157
this._registerToFlags(this._options.toFlags);
6258
}
@@ -66,10 +62,8 @@ export class TaskSelector {
6662
if (this._options.toFlags.length === 0 && this._options.fromFlags.length === 0) {
6763
this._registerAll();
6864
}
69-
}
7065

71-
public execute(): Promise<void> {
72-
return this._taskRunner.execute();
66+
return this._taskCollection;
7367
}
7468

7569
private _registerToFlags(toFlags: ReadonlyArray<string>): void {
@@ -87,7 +81,7 @@ export class TaskSelector {
8781

8882
if (!this._options.ignoreDependencyOrder) {
8983
// Add ordering relationships for each dependency
90-
deps.forEach(dep => this._taskRunner.addDependencies(dep, this._rushLinkJson.localLinks[dep] || []));
84+
deps.forEach(dep => this._taskCollection.addDependencies(dep, this._rushLinkJson.localLinks[dep] || []));
9185
}
9286
}
9387
}
@@ -116,7 +110,7 @@ export class TaskSelector {
116110
// Only add ordering relationships for projects which have been registered
117111
// e.g. package C may depend on A & B, but if we are only building A's downstream, we will ignore B
118112
dependents.forEach(dependent =>
119-
this._taskRunner.addDependencies(dependent,
113+
this._taskCollection.addDependencies(dependent,
120114
(this._rushLinkJson.localLinks[dependent] || []).filter(dep => dependents.has(dep))));
121115
}
122116
}
@@ -130,7 +124,7 @@ export class TaskSelector {
130124
if (!this._options.ignoreDependencyOrder) {
131125
// Add ordering relationships for each dependency
132126
for (const projectName of Object.keys(this._rushLinkJson.localLinks)) {
133-
this._taskRunner.addDependencies(projectName, this._rushLinkJson.localLinks[projectName]);
127+
this._taskCollection.addDependencies(projectName, this._rushLinkJson.localLinks[projectName]);
134128
}
135129
}
136130
}
@@ -186,8 +180,8 @@ export class TaskSelector {
186180
packageChangeAnalyzer: this._packageChangeAnalyzer
187181
});
188182

189-
if (!this._taskRunner.hasTask(projectTask.name)) {
190-
this._taskRunner.addTask(projectTask);
183+
if (!this._taskCollection.hasTask(projectTask.name)) {
184+
this._taskCollection.addTask(projectTask);
191185
}
192186
}
193187
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import {
5+
Terminal,
6+
ConsoleTerminalProvider
7+
} from '@microsoft/node-core-library';
8+
9+
import { ITask, ITaskDefinition } from './ITask';
10+
import { TaskStatus } from './TaskStatus';
11+
12+
export interface ITaskCollectionOptions {
13+
quietMode: boolean;
14+
terminal?: Terminal;
15+
}
16+
17+
/**
18+
* A class which manages the execution of a set of tasks with interdependencies.
19+
* Any class of task definition may be registered, and dependencies between tasks are
20+
* easily specified. Initially, and at the end of each task execution, all unblocked tasks
21+
* are added to a ready queue which is then executed. This is done continually until all
22+
* tasks are complete, or prematurely fails if any of the tasks fail.
23+
*/
24+
export class TaskCollection {
25+
private _tasks: Map<string, ITask>;
26+
private _quietMode: boolean;
27+
private _terminal: Terminal;
28+
29+
constructor(options: ITaskCollectionOptions) {
30+
const {
31+
quietMode,
32+
terminal = new Terminal(new ConsoleTerminalProvider())
33+
} = options;
34+
this._tasks = new Map<string, ITask>();
35+
this._quietMode = quietMode;
36+
this._terminal = terminal;
37+
}
38+
39+
/**
40+
* Registers a task definition to the map of defined tasks
41+
*/
42+
public addTask(taskDefinition: ITaskDefinition): void {
43+
if (this._tasks.has(taskDefinition.name)) {
44+
throw new Error('A task with that name has already been registered.');
45+
}
46+
47+
const task: ITask = taskDefinition as ITask;
48+
task.dependencies = new Set<ITask>();
49+
task.dependents = new Set<ITask>();
50+
task.status = TaskStatus.Ready;
51+
task.criticalPathLength = undefined;
52+
this._tasks.set(task.name, task);
53+
54+
if (!this._quietMode) {
55+
this._terminal.writeLine(`Registered ${task.name}`);
56+
}
57+
}
58+
59+
/**
60+
* Returns true if a task with that name has been registered
61+
*/
62+
public hasTask(taskName: string): boolean {
63+
return this._tasks.has(taskName);
64+
}
65+
66+
/**
67+
* Defines the list of dependencies for an individual task.
68+
* @param taskName - the string name of the task for which we are defining dependencies. A task with this
69+
* name must already have been registered.
70+
*/
71+
public addDependencies(taskName: string, taskDependencies: string[]): void {
72+
const task: ITask | undefined = this._tasks.get(taskName);
73+
74+
if (!task) {
75+
throw new Error(`The task '${taskName}' has not been registered`);
76+
}
77+
if (!taskDependencies) {
78+
throw new Error('The list of dependencies must be defined');
79+
}
80+
81+
for (const dependencyName of taskDependencies) {
82+
if (!this._tasks.has(dependencyName)) {
83+
throw new Error(`The project '${dependencyName}' has not been registered.`);
84+
}
85+
const dependency: ITask = this._tasks.get(dependencyName)!;
86+
task.dependencies.add(dependency);
87+
dependency.dependents.add(task);
88+
}
89+
}
90+
91+
/**
92+
* Executes all tasks which have been registered, returning a promise which is resolved when all the
93+
* tasks are completed successfully, or rejects when any task fails.
94+
*/
95+
public getOrderedTasks(): ITask[] {
96+
this._checkForCyclicDependencies(this._tasks.values(), []);
97+
98+
// Precalculate the number of dependent packages
99+
this._tasks.forEach((task: ITask) => {
100+
this._calculateCriticalPaths(task);
101+
});
102+
103+
const buildQueue: ITask[] = [];
104+
// Add everything to the buildQueue
105+
this._tasks.forEach((task: ITask) => {
106+
buildQueue.push(task);
107+
});
108+
109+
// Sort the queue in descending order, nothing will mess with the order
110+
buildQueue.sort((taskA: ITask, taskB: ITask): number => {
111+
return taskB.criticalPathLength! - taskA.criticalPathLength!;
112+
});
113+
114+
return buildQueue;
115+
}
116+
117+
/**
118+
* Checks for projects that indirectly depend on themselves.
119+
*/
120+
private _checkForCyclicDependencies(tasks: Iterable<ITask>, dependencyChain: string[]): void {
121+
for (const task of tasks) {
122+
if (dependencyChain.indexOf(task.name) >= 0) {
123+
throw new Error('A cyclic dependency was encountered:\n'
124+
+ ' ' + [...dependencyChain, task.name].reverse().join('\n -> ')
125+
+ '\nConsider using the cyclicDependencyProjects option for rush.json.');
126+
}
127+
dependencyChain.push(task.name);
128+
this._checkForCyclicDependencies(task.dependents, dependencyChain);
129+
dependencyChain.pop();
130+
}
131+
}
132+
133+
/**
134+
* Calculate the number of packages which must be built before we reach
135+
* the furthest away "root" node
136+
*/
137+
private _calculateCriticalPaths(task: ITask): number {
138+
// Return the memoized value
139+
if (task.criticalPathLength !== undefined) {
140+
return task.criticalPathLength;
141+
}
142+
143+
// If no dependents, we are in a "root"
144+
if (task.dependents.size === 0) {
145+
return task.criticalPathLength = 0;
146+
} else {
147+
// Otherwise we are as long as the longest package + 1
148+
const depsLengths: number[] = [];
149+
task.dependents.forEach(dep => depsLengths.push(this._calculateCriticalPaths(dep)));
150+
return task.criticalPathLength = Math.max(...depsLengths) + 1;
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)