Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add a per-iteration, host-driven runner persist policy so IPC runners can be kept hot or torn down per operation per iteration, instead of fixing persistence at plugin construction.",
"type": "patch"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"type": "patch"
"type": "minor"

}
]
}
2 changes: 2 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export interface ICobuildLockProvider {
// @alpha
export interface IConfigurableOperation extends IBaseOperationExecutionResult {
enabled: boolean;
shouldRunnerPersist: boolean;
}

// @public
Expand Down Expand Up @@ -613,6 +614,7 @@ export interface IOperationExecutionResult extends IBaseOperationExecutionResult
readonly logFilePaths: ILogFilePaths | undefined;
readonly nonCachedDurationMs: number | undefined;
readonly problemCollector: IProblemCollector;
readonly shouldRunnerPersist: boolean;
readonly silent: boolean;
readonly status: OperationStatus;
readonly stdioSummarizer: StdioSummarizer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export interface IConfigurableOperation extends IBaseOperationExecutionResult {
* True if the operation should execute in this iteration, false otherwise.
*/
enabled: boolean;

/**
* True if the operation's runner should remain active after this iteration, false otherwise.
* Defaults to true.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we flip this so it defaults to false?

*/
shouldRunnerPersist: boolean;
}

/**
Expand Down Expand Up @@ -94,6 +100,10 @@ export interface IOperationExecutionResult extends IBaseOperationExecutionResult
* True if the operation should execute in this iteration, false otherwise.
*/
readonly enabled: boolean;
/**
* True if the operation's runner should remain active after this iteration, false otherwise.
*/
readonly shouldRunnerPersist: boolean;
/**
* Object tracking execution timing.
*/
Expand Down
11 changes: 0 additions & 11 deletions libraries/rush-lib/src/logic/operations/IPCOperationRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export interface IIPCOperationRunnerOptions {
initialCommand: string;
incrementalCommand: string | undefined;
commandForHash: string;
persist: boolean;
ignoredParameterValues: ReadonlyArray<string>;
}

Expand Down Expand Up @@ -58,7 +57,6 @@ export class IPCOperationRunner implements IOperationRunner {
private readonly _initialCommand: string;
private readonly _incrementalCommand: string | undefined;
private readonly _commandForHash: string;
private readonly _persist: boolean;
private readonly _ignoredParameterValues: ReadonlyArray<string>;

private _ipcProcess: ChildProcess | undefined;
Expand All @@ -72,7 +70,6 @@ export class IPCOperationRunner implements IOperationRunner {
initialCommand,
incrementalCommand,
commandForHash,
persist,
ignoredParameterValues
} = options;
this.name = name;
Expand All @@ -83,7 +80,6 @@ export class IPCOperationRunner implements IOperationRunner {
this._incrementalCommand = incrementalCommand;
this._commandForHash = commandForHash;

this._persist = persist;
this._ignoredParameterValues = ignoredParameterValues;
}

Expand All @@ -100,7 +96,6 @@ export class IPCOperationRunner implements IOperationRunner {
const invalidate: (reason: string) => void = context.getInvalidateCallback();
return await context.runWithTerminalAsync(
async (terminal: ITerminal, terminalProvider: ITerminalProvider): Promise<OperationStatus> => {
let isConnected: boolean = false;
if (!this._ipcProcess || typeof this._ipcProcess.exitCode === 'number') {
// Log any ignored parameters
if (this._ignoredParameterValues.length > 0) {
Expand Down Expand Up @@ -202,9 +197,7 @@ export class IPCOperationRunner implements IOperationRunner {
subProcess.on('message', finishHandler);
subProcess.on('error', reject);
subProcess.on('exit', onExit);

this._processReadyPromise!.then(() => {
isConnected = true;
terminal.writeLine('Child supports IPC protocol. Sending "run" command...');
const runCommand: IRunCommandMessage = {
command: 'run'
Expand All @@ -213,10 +206,6 @@ export class IPCOperationRunner implements IOperationRunner {
}, reject);
});

if (isConnected && !this._persist) {
await this.closeAsync();
}
Comment thread
bmiddha marked this conversation as resolved.

// @rushstack/operation-graph does not currently have a concept of "Success with Warning"
// To match existing ShellOperationRunner behavior we treat any stderr as a warning.
return status === OperationStatus.Success && hasWarningOrError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin {
initialCommand,
incrementalCommand,
commandForHash,
persist: true,
ignoredParameterValues
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export class OperationExecutionRecord implements IOperationRunnerContext, IOpera
*/
public enabled: boolean;

/**
* If true, this operation's runner should remain active after this iteration.
*/
public shouldRunnerPersist: boolean = true;

/**
* This number represents how far away this Operation is from the furthest "root" operation (i.e.
* an operation with no consumers). This helps us to calculate the critical path (i.e. the
Expand Down Expand Up @@ -448,11 +453,18 @@ export class OperationExecutionRecord implements IOperationRunnerContext, IOpera
// Delegate global state reporting
await executeContext.onResultAsync(this);
} finally {
if (this.isTerminal) {
this._collatedWriter?.close();
this.stdioSummarizer.close();
this.problemCollector.close();
}
this.finalize();
}
}

/**
* Closes per-record output resources after the record reaches a terminal state.
*/
public finalize(): void {
if (this.isTerminal) {
this._collatedWriter?.close();
this.stdioSummarizer.close();
this.problemCollector.close();
}
}
}
41 changes: 40 additions & 1 deletion libraries/rush-lib/src/logic/operations/OperationGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,8 @@ export class OperationGraph implements IOperationGraph {
onStartAsync: onOperationStartAsync,
onResultAsync: onOperationCompleteAsync
};
const recordsWithRunnerCleanup: Set<OperationExecutionRecord> = new Set();
const graph: OperationGraph = this;

if (!this.quietMode) {
const plural: string = totalOperations === 1 ? '' : 's';
Expand Down Expand Up @@ -873,9 +875,36 @@ export class OperationGraph implements IOperationGraph {
});
}

const recordsToClose: OperationExecutionRecord[] = [];
for (const record of executionRecords.values()) {
if (!recordsWithRunnerCleanup.has(record) && !record.shouldRunnerPersist) {
recordsToClose.push(record);
}
}
function reportRunnerCleanupFailure(record: OperationExecutionRecord, error: Error): void {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a non-persistent runner fails to close at end-of-iteration (not mid-execution), should this always turn the iteration into a Failure, or should it be a Warning?

record.error = error;
record.status = OperationStatus.Failure;
_reportOperationErrorIfAny(record);
state.hasAnyFailures = true;
}
await Async.forEachAsync(
recordsToClose,
async (record: OperationExecutionRecord) => {
try {
await this.closeRunnersAsync([record.operation]);
} catch (e) {
reportRunnerCleanupFailure(record, e);
}
},
{ concurrency: this.parallelism }
);
Comment on lines +890 to +900

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are runners designed to handle concurrent closeAsync() calls if they're still being used elsewhere? Seems handled by the fact that non-persistent runners are closed immediately after completion (lines 1059-1068), but worth documenting.

for (const record of executionRecords.values()) {
record.finalize();
}

const status: OperationStatus = (() => {
if (bailStatus) return bailStatus;
if (state.hasAnyFailures) return OperationStatus.Failure;
if (bailStatus) return bailStatus;
if (state.hasAnyAborted) return OperationStatus.Aborted;
if (state.hasAnyNonAllowedWarnings) return OperationStatus.SuccessWithWarning;
if (iterationContext.totalOperations === 0) return OperationStatus.NoOp;
Expand Down Expand Up @@ -1027,6 +1056,16 @@ export class OperationGraph implements IOperationGraph {
record.error = e;
record.status = OperationStatus.Failure;
}
if (!record.shouldRunnerPersist) {
recordsWithRunnerCleanup.add(record);
try {
await graph.closeRunnersAsync([record.operation]);
} catch (e) {
_reportOperationErrorIfAny(record);
record.error = e;
record.status = OperationStatus.Failure;
}
}
_onOperationComplete(record, state);
}
}
Expand Down
Loading