Skip to content

Commit afd2b72

Browse files
authored
[operation-graph] Require operation names, support detail in requestRun (#5360)
* [operation-graph] Logging enhancements * Fixup required options * Fixup allow zero weight --------- Co-authored-by: David Michon <dmichon-msft@users.noreply.github.com>
1 parent b8d06c8 commit afd2b72

15 files changed

Lines changed: 126 additions & 56 deletions

apps/heft/src/cli/HeftActionRunner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Operation,
1414
OperationExecutionManager,
1515
OperationGroupRecord,
16+
type OperationRequestRunCallback,
1617
OperationStatus,
1718
WatchLoop
1819
} from '@rushstack/operation-graph';
@@ -370,7 +371,7 @@ export class HeftActionRunner {
370371
private async _executeOnceAsync(
371372
executionManager: OperationExecutionManager<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>,
372373
abortSignal: AbortSignal,
373-
requestRun?: (requestor?: string) => void
374+
requestRun?: OperationRequestRunCallback
374375
): Promise<OperationStatus> {
375376
const { taskStart, taskFinish, phaseStart, phaseFinish } = this._internalHeftSession.lifecycle.hooks;
376377
// Record this as the start of task execution.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Enhance logging for IPC mode by allowing IPC runners to report detailed reasons for rerun, e.g. specific changed files.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/heft",
5+
"comment": "Enhance logging in watch mode by allowing plugins to report detailed reasons for requesting rerun, e.g. specific changed files.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/heft"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/operation-graph",
5+
"comment": "Require the \"requestor\" parameter and add a new \"detail\" parameter for watch-mode rerun requests. Make \"name\" a required field for operations.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/operation-graph"
10+
}

common/reviews/api/operation-graph.api.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface IExecuteOperationContext extends Omit<IOperationRunnerContext,
3333
afterExecuteAsync(operation: Operation, state: IOperationState): Promise<void>;
3434
beforeExecuteAsync(operation: Operation, state: IOperationState): Promise<void>;
3535
queueWork(workFn: () => Promise<OperationStatus>, priority: number): Promise<OperationStatus>;
36-
requestRun?: (requestor?: string) => void;
36+
requestRun?: OperationRequestRunCallback;
3737
terminal: ITerminal;
3838
}
3939

@@ -58,7 +58,7 @@ export interface IOperationExecutionOptions<TOperationMetadata extends {} = {},
5858
// (undocumented)
5959
parallelism: number;
6060
// (undocumented)
61-
requestRun?: (requestor?: string) => void;
61+
requestRun?: OperationRequestRunCallback;
6262
// (undocumented)
6363
terminal: ITerminal;
6464
}
@@ -67,7 +67,7 @@ export interface IOperationExecutionOptions<TOperationMetadata extends {} = {},
6767
export interface IOperationOptions<TMetadata extends {} = {}, TGroupMetadata extends {} = {}> {
6868
group?: OperationGroupRecord<TGroupMetadata> | undefined;
6969
metadata?: TMetadata | undefined;
70-
name?: string | undefined;
70+
name: string;
7171
runner?: IOperationRunner | undefined;
7272
weight?: number | undefined;
7373
}
@@ -83,7 +83,7 @@ export interface IOperationRunner {
8383
export interface IOperationRunnerContext {
8484
abortSignal: AbortSignal;
8585
isFirstRun: boolean;
86-
requestRun?: () => void;
86+
requestRun?: (detail?: string) => void;
8787
}
8888

8989
// @beta
@@ -105,10 +105,10 @@ export type IPCHost = Pick<typeof process, 'on' | 'send'>;
105105

106106
// @beta
107107
export interface IRequestRunEventMessage {
108+
detail?: string;
108109
// (undocumented)
109110
event: 'requestRun';
110-
// (undocumented)
111-
requestor?: string;
111+
requestor: string;
112112
}
113113

114114
// @beta
@@ -136,20 +136,20 @@ export interface IWatchLoopOptions {
136136
executeAsync: (state: IWatchLoopState) => Promise<OperationStatus>;
137137
onAbort: () => void;
138138
onBeforeExecute: () => void;
139-
onRequestRun: (requestor?: string) => void;
139+
onRequestRun: OperationRequestRunCallback;
140140
}
141141

142142
// @beta
143143
export interface IWatchLoopState {
144144
// (undocumented)
145145
get abortSignal(): AbortSignal;
146146
// (undocumented)
147-
requestRun: (requestor?: string) => void;
147+
requestRun: OperationRequestRunCallback;
148148
}
149149

150150
// @beta
151151
export class Operation<TMetadata extends {} = {}, TGroupMetadata extends {} = {}> implements IOperationStates {
152-
constructor(options?: IOperationOptions<TMetadata, TGroupMetadata>);
152+
constructor(options: IOperationOptions<TMetadata, TGroupMetadata>);
153153
// (undocumented)
154154
addDependency(dependency: Operation<TMetadata, TGroupMetadata>): void;
155155
readonly consumers: Set<Operation<TMetadata, TGroupMetadata>>;
@@ -163,7 +163,7 @@ export class Operation<TMetadata extends {} = {}, TGroupMetadata extends {} = {}
163163
lastState: IOperationState | undefined;
164164
// (undocumented)
165165
readonly metadata: TMetadata;
166-
readonly name: string | undefined;
166+
readonly name: string;
167167
// (undocumented)
168168
reset(): void;
169169
runner: IOperationRunner | undefined;
@@ -213,6 +213,9 @@ export class OperationGroupRecord<TMetadata extends {} = {}> {
213213
startTimer(): void;
214214
}
215215

216+
// @beta
217+
export type OperationRequestRunCallback = (requestor: string, detail?: string) => void;
218+
216219
// @beta
217220
export enum OperationStatus {
218221
Aborted = "ABORTED",
@@ -244,7 +247,7 @@ export class Stopwatch {
244247
export class WatchLoop implements IWatchLoopState {
245248
constructor(options: IWatchLoopOptions);
246249
get abortSignal(): AbortSignal;
247-
requestRun: (requestor?: string) => void;
250+
requestRun: OperationRequestRunCallback;
248251
runIPCAsync(host?: IPCHost): Promise<void>;
249252
runUntilAbortedAsync(abortSignal: AbortSignal, onWaiting: () => void): Promise<void>;
250253
runUntilStableAsync(abortSignal: AbortSignal): Promise<OperationStatus>;

libraries/operation-graph/src/IOperationRunner.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ export interface IOperationRunnerContext {
2626
/**
2727
* A callback to the overarching orchestrator to request that the operation be invoked again.
2828
* Used in watch mode to signal that inputs have changed.
29+
*
30+
* @param detail - Optional detail about why the rerun is requested, e.g. the name of a changed file.
2931
*/
30-
requestRun?: () => void;
32+
requestRun?: (detail?: string) => void;
3133
}
3234

3335
/**

libraries/operation-graph/src/Operation.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface IOperationOptions<TMetadata extends {} = {}, TGroupMetadata ext
2323
/**
2424
* The name of this operation, for logging.
2525
*/
26-
name?: string | undefined;
26+
name: string;
2727

2828
/**
2929
* The group that this operation belongs to. Will be used for logging and duration tracking.
@@ -47,6 +47,12 @@ export interface IOperationOptions<TMetadata extends {} = {}, TGroupMetadata ext
4747
metadata?: TMetadata | undefined;
4848
}
4949

50+
/**
51+
* Type for the `requestRun` callback.
52+
* @beta
53+
*/
54+
export type OperationRequestRunCallback = (requestor: string, detail?: string) => void;
55+
5056
/**
5157
* Information provided to `executeAsync` by the `OperationExecutionManager`.
5258
*
@@ -73,8 +79,11 @@ export interface IExecuteOperationContext extends Omit<IOperationRunnerContext,
7379
/**
7480
* A callback to the overarching orchestrator to request that the operation be invoked again.
7581
* Used in watch mode to signal that inputs have changed.
82+
*
83+
* @param requestor - The name of the operation requesting a rerun.
84+
* @param detail - Optional detail about why the rerun is requested, e.g. the name of a changed file.
7685
*/
77-
requestRun?: (requestor?: string) => void;
86+
requestRun?: OperationRequestRunCallback;
7887

7988
/**
8089
* Terminal to write output to.
@@ -113,7 +122,7 @@ export class Operation<TMetadata extends {} = {}, TGroupMetadata extends {} = {}
113122
/**
114123
* The name of this operation, for logging.
115124
*/
116-
public readonly name: string | undefined;
125+
public readonly name: string;
117126

118127
/**
119128
* When the scheduler is ready to process this `Operation`, the `runner` implements the actual work of
@@ -188,12 +197,12 @@ export class Operation<TMetadata extends {} = {}, TGroupMetadata extends {} = {}
188197

189198
public readonly metadata: TMetadata;
190199

191-
public constructor(options?: IOperationOptions<TMetadata, TGroupMetadata>) {
192-
this.group = options?.group;
193-
this.runner = options?.runner;
194-
this.weight = options?.weight || 1;
195-
this.name = options?.name;
196-
this.metadata = options?.metadata || ({} as TMetadata);
200+
public constructor(options: IOperationOptions<TMetadata, TGroupMetadata>) {
201+
this.group = options.group;
202+
this.runner = options.runner;
203+
this.weight = options.weight ?? 1;
204+
this.name = options.name;
205+
this.metadata = options.metadata || ({} as TMetadata);
197206

198207
if (this.group) {
199208
this.group.addOperation(this);
@@ -276,7 +285,7 @@ export class Operation<TMetadata extends {} = {}, TGroupMetadata extends {} = {}
276285
abortSignal,
277286
isFirstRun: !state.hasBeenRun,
278287
requestRun: requestRun
279-
? () => {
288+
? (detail?: string) => {
280289
switch (this.state?.status) {
281290
case OperationStatus.Waiting:
282291
case OperationStatus.Ready:
@@ -299,7 +308,7 @@ export class Operation<TMetadata extends {} = {}, TGroupMetadata extends {} = {}
299308
// The requestRun callback is assumed to remain constant
300309
// throughout the lifetime of the process, so it is safe
301310
// to capture here.
302-
return requestRun(this.name);
311+
return requestRun(this.name, detail);
303312
default:
304313
// This line is here to enforce exhaustiveness
305314
const currentStatus: undefined = this.state?.status;

libraries/operation-graph/src/OperationExecutionManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Async } from '@rushstack/node-core-library';
55
import type { ITerminal } from '@rushstack/terminal';
66

77
import type { IOperationState } from './IOperationRunner';
8-
import type { IExecuteOperationContext, Operation } from './Operation';
8+
import type { IExecuteOperationContext, Operation, OperationRequestRunCallback } from './Operation';
99
import type { OperationGroupRecord } from './OperationGroupRecord';
1010
import { OperationStatus } from './OperationStatus';
1111
import { calculateCriticalPathLengths } from './calculateCriticalPath';
@@ -24,7 +24,7 @@ export interface IOperationExecutionOptions<
2424
parallelism: number;
2525
terminal: ITerminal;
2626

27-
requestRun?: (requestor?: string) => void;
27+
requestRun?: OperationRequestRunCallback;
2828

2929
beforeExecuteOperationAsync?: (operation: Operation<TOperationMetadata, TGroupMetadata>) => Promise<void>;
3030
afterExecuteOperationAsync?: (operation: Operation<TOperationMetadata, TGroupMetadata>) => Promise<void>;

libraries/operation-graph/src/WatchLoop.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { once } from 'node:events';
55

66
import { AlreadyReportedError } from '@rushstack/node-core-library';
77

8+
import type { OperationRequestRunCallback } from './Operation';
89
import { OperationStatus } from './OperationStatus';
910
import type {
1011
IAfterExecuteEventMessage,
@@ -30,8 +31,11 @@ export interface IWatchLoopOptions {
3031
onBeforeExecute: () => void;
3132
/**
3233
* Logging callback when a run is requested (and hasn't already been).
34+
*
35+
* @param requestor - The name of the operation requesting a rerun.
36+
* @param detail - Optional detail about why the rerun is requested, e.g. the name of a changed file.
3337
*/
34-
onRequestRun: (requestor?: string) => void;
38+
onRequestRun: OperationRequestRunCallback;
3539
/**
3640
* Logging callback when a run is aborted.
3741
*/
@@ -45,7 +49,7 @@ export interface IWatchLoopOptions {
4549
*/
4650
export interface IWatchLoopState {
4751
get abortSignal(): AbortSignal;
48-
requestRun: (requestor?: string) => void;
52+
requestRun: OperationRequestRunCallback;
4953
}
5054

5155
/**
@@ -59,8 +63,8 @@ export class WatchLoop implements IWatchLoopState {
5963
private _abortController: AbortController;
6064
private _isRunning: boolean;
6165
private _runRequested: boolean;
62-
private _requestRunPromise: Promise<string | undefined>;
63-
private _resolveRequestRun!: (requestor?: string) => void;
66+
private _requestRunPromise: Promise<[string, string?]>;
67+
private _resolveRequestRun!: (value: [string, string?]) => void;
6468

6569
public constructor(options: IWatchLoopOptions) {
6670
this._options = options;
@@ -69,7 +73,7 @@ export class WatchLoop implements IWatchLoopState {
6973
this._isRunning = false;
7074
// Always start as true, so that any requests prior to first run are silenced.
7175
this._runRequested = true;
72-
this._requestRunPromise = new Promise<string | undefined>((resolve) => {
76+
this._requestRunPromise = new Promise<[string, string?]>((resolve) => {
7377
this._resolveRequestRun = resolve;
7478
});
7579
}
@@ -146,7 +150,7 @@ export class WatchLoop implements IWatchLoopState {
146150
}
147151
}
148152

149-
function requestRunFromHost(requestor?: string): void {
153+
function requestRunFromHost(requestor: string, detail?: string): void {
150154
if (runRequestedFromHost) {
151155
return;
152156
}
@@ -155,7 +159,8 @@ export class WatchLoop implements IWatchLoopState {
155159

156160
const requestRunMessage: IRequestRunEventMessage = {
157161
event: 'requestRun',
158-
requestor
162+
requestor,
163+
detail
159164
};
160165

161166
tryMessageHost(requestRunMessage);
@@ -192,8 +197,12 @@ export class WatchLoop implements IWatchLoopState {
192197
try {
193198
status = await this.runUntilStableAsync(abortController.signal);
194199
// ESLINT: "Promises must be awaited, end with a call to .catch, end with a call to .then ..."
195-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
196-
this._requestRunPromise.finally(requestRunFromHost);
200+
this._requestRunPromise.then(
201+
([requestor, detail]) => requestRunFromHost(requestor, detail),
202+
(error: Error) => {
203+
// Unreachable code. The promise will never be rejected.
204+
}
205+
);
197206
} catch (err) {
198207
status = OperationStatus.Failure;
199208
return reject(err);
@@ -224,16 +233,16 @@ export class WatchLoop implements IWatchLoopState {
224233
/**
225234
* Requests that a new run occur.
226235
*/
227-
public requestRun: (requestor?: string) => void = (requestor?: string) => {
236+
public requestRun: OperationRequestRunCallback = (requestor: string, detail?: string) => {
228237
if (!this._runRequested) {
229-
this._options.onRequestRun(requestor);
238+
this._options.onRequestRun(requestor, detail);
230239
this._runRequested = true;
231240
if (this._isRunning) {
232241
this._options.onAbort();
233242
this._abortCurrent();
234243
}
235244
}
236-
this._resolveRequestRun(requestor);
245+
this._resolveRequestRun([requestor, detail]);
237246
};
238247

239248
/**
@@ -260,7 +269,7 @@ export class WatchLoop implements IWatchLoopState {
260269

261270
if (this._runRequested) {
262271
this._runRequested = false;
263-
this._requestRunPromise = new Promise<string | undefined>((resolve) => {
272+
this._requestRunPromise = new Promise<[string, string?]>((resolve) => {
264273
this._resolveRequestRun = resolve;
265274
});
266275
}

libraries/operation-graph/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ export type {
2323
IPCHost
2424
} from './protocol.types';
2525

26-
export { type IExecuteOperationContext, type IOperationOptions, Operation } from './Operation';
26+
export {
27+
type IExecuteOperationContext,
28+
type IOperationOptions,
29+
Operation,
30+
type OperationRequestRunCallback
31+
} from './Operation';
2732

2833
export { OperationError } from './OperationError';
2934

0 commit comments

Comments
 (0)