Skip to content

Commit 750d664

Browse files
mojazamojazayeriCopilot
authored
[rush-daemon][WS2.8][7/9] Wire request admission (#5970)
* Add rushd request admission Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix rushd admission review findings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix request admission compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: mojaza <mojazayeri@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b0a511d commit 750d664

33 files changed

Lines changed: 1501 additions & 130 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/rush-daemon-protocol",
5+
"comment": "Add parsed command origin, request admission options, queue progress messages, and typed admission failures.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/rush-daemon-protocol",
10+
"email": "mojazayeri@users.noreply.github.com"
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/rush-daemon",
5+
"comment": "Classify parsed built-in commands and admit phased and global requests through bounded workspace and graph scheduling.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/rush-daemon",
10+
"email": "mojazayeri@users.noreply.github.com"
11+
}

common/reviews/api/rush-daemon-protocol.api.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export const DAEMON_CONTROL_MESSAGE_KINDS: readonly [
2424
'error',
2525
'setRawMode',
2626
'rawModeChanged',
27-
'terminalPolicy'
27+
'terminalPolicy',
28+
'queuePosition'
2829
];
2930

3031
// @beta
@@ -52,11 +53,14 @@ export const DAEMON_INTERACTIVE_IO_PROTOCOL_MINOR: number;
5253
// @beta
5354
export const DAEMON_PROTOCOL_VERSION: IDaemonProtocolVersion;
5455

56+
// @beta
57+
export const DAEMON_REQUEST_ADMISSION_PROTOCOL_MINOR: number;
58+
5559
// @beta
5660
export type DaemonCommandOutcome = 'success' | 'success-with-warning' | 'failure' | 'aborted';
5761

5862
// @beta
59-
export type DaemonControlMessage = IDaemonHelloMessage | IDaemonHelloAckMessage | IDaemonSubscribeMessage | IDaemonUnsubscribeMessage | IDaemonPingMessage | IDaemonPongMessage | IDaemonErrorMessage | IDaemonSetRawModeMessage | IDaemonRawModeChangedMessage | IDaemonTerminalPolicyMessage;
63+
export type DaemonControlMessage = IDaemonHelloMessage | IDaemonHelloAckMessage | IDaemonSubscribeMessage | IDaemonUnsubscribeMessage | IDaemonPingMessage | IDaemonPongMessage | IDaemonErrorMessage | IDaemonSetRawModeMessage | IDaemonRawModeChangedMessage | IDaemonTerminalPolicyMessage | IDaemonRequestQueuePositionMessage;
6064

6165
// @beta
6266
export type DaemonControlMessageKind = (typeof DAEMON_CONTROL_MESSAGE_KINDS)[number];
@@ -121,6 +125,12 @@ export class DaemonProtocolError extends Error {
121125
// @beta
122126
export type DaemonProtocolErrorCode = 'frameTooLarge' | 'unknownFrameType' | 'malformedPayload' | 'malformedControlMessage' | 'protocolVersionMismatch';
123127

128+
// @beta
129+
export type DaemonRequestAdmissionErrorCode = 'aborted' | 'no-wait' | 'wait-timeout';
130+
131+
// @beta
132+
export type DaemonRushCommandOrigin = 'built-in' | 'custom';
133+
124134
// @beta
125135
export type DaemonTerminalPolicyDecision = 'runInDaemon' | 'requiresInProcess';
126136

@@ -181,12 +191,14 @@ export interface IDaemonClientCaps {
181191
readonly columns?: number;
182192
readonly isTTY: boolean;
183193
readonly supportsInteractiveIO?: boolean;
194+
readonly supportsRequestAdmission?: boolean;
184195
readonly verbosity?: DaemonVerbosity;
185196
}
186197

187198
// @beta
188199
export interface IDaemonCommandResult {
189200
readonly aborted: boolean;
201+
readonly admissionErrorCode?: DaemonRequestAdmissionErrorCode;
190202
readonly errorMessage?: string;
191203
readonly exitCode: number;
192204
readonly outcome: DaemonCommandOutcome;
@@ -337,7 +349,9 @@ export interface IDaemonPhasedOperationSelection {
337349
// @beta
338350
export interface IDaemonPhasedRequest {
339351
readonly acceptsStdin?: boolean;
352+
readonly admission?: IDaemonRequestAdmissionOptions;
340353
readonly commandName: string;
354+
readonly commandOrigin?: DaemonRushCommandOrigin;
341355
readonly engineShape: IDaemonPhasedEngineShape;
342356
readonly environment: Readonly<Record<string, string>>;
343357
readonly operationSelection: ReadonlyArray<IDaemonPhasedOperationSelection>;
@@ -394,6 +408,23 @@ export interface IDaemonRawModeChangedMessage {
394408
};
395409
}
396410

411+
// @beta
412+
export interface IDaemonRequestAdmissionOptions {
413+
readonly noWait?: boolean;
414+
readonly waitTimeoutMs?: number;
415+
}
416+
417+
// @beta
418+
export interface IDaemonRequestQueuePositionMessage {
419+
// (undocumented)
420+
readonly kind: 'queuePosition';
421+
// (undocumented)
422+
readonly payload: {
423+
readonly position: number;
424+
readonly requestId: string;
425+
};
426+
}
427+
397428
// @beta
398429
export interface IDaemonSetRawModeMessage {
399430
// (undocumented)
@@ -478,6 +509,9 @@ export const LENGTH_FIELD_BYTES: number;
478509
// @beta
479510
export const LENGTH_FIELD_OFFSET: number;
480511

512+
// @beta
513+
export const MAX_DAEMON_REQUEST_WAIT_TIMEOUT_MS: number;
514+
481515
// @beta
482516
export const MAX_OPERATION_ID_BYTES: number;
483517

@@ -536,4 +570,7 @@ export function validateDaemonControlMessage(value: unknown): void;
536570
// @beta
537571
export function validateDaemonEventEnvelope(value: unknown): IDaemonEventEnvelope;
538572

573+
// @beta
574+
export function validateDaemonRequestAdmissionOptions(options: IDaemonRequestAdmissionOptions | undefined): void;
575+
539576
```

common/reviews/api/rush-daemon.api.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
/// <reference types="node" />
88

99
import * as childProcess from 'node:child_process';
10+
import type { DaemonRushCommandOrigin } from '@rushstack/rush-daemon-protocol';
1011
import type { DaemonTerminalRequirement } from '@rushstack/rush-daemon-protocol';
1112
import type { GetInputsSnapshotAsyncFn } from '@microsoft/rush-lib';
1213
import type { IDaemonCommandResult } from '@rushstack/rush-daemon-protocol';
1314
import type { IDaemonEventEnvelope } from '@rushstack/rush-daemon-protocol';
1415
import type { IDaemonPaths } from '@rushstack/rush-daemon-transport';
1516
import type { IDaemonPhasedRequest } from '@rushstack/rush-daemon-protocol';
1617
import type { IDaemonPhasedRequestResult } from '@rushstack/rush-daemon-protocol';
18+
import type { IDaemonRequestAdmissionOptions } from '@rushstack/rush-daemon-protocol';
19+
import type { IDaemonRequestQueuePositionMessage } from '@rushstack/rush-daemon-protocol';
1720
import type { IDaemonSetRawModeMessage } from '@rushstack/rush-daemon-protocol';
1821
import type { IDaemonTerminalPolicyResult } from '@rushstack/rush-daemon-protocol';
1922
import type { IInputsSnapshot } from '@microsoft/rush-lib';
@@ -138,6 +141,8 @@ export interface IGlobalCommandExecutionResult {
138141
export interface IGlobalCommandRequestClient {
139142
readonly abortSignal: AbortSignal;
140143
readonly interactiveSession?: IInteractiveRequestSession;
144+
readonly supportsRequestAdmission?: boolean;
145+
writeQueuePositionAsync?(message: IDaemonRequestQueuePositionMessage): Promise<void>;
141146
writeResultAsync(result: IDaemonCommandResult): Promise<void>;
142147
writeTerminalChunkAsync(stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
143148
writeTerminalPolicyAsync(result: IDaemonTerminalPolicyResult): Promise<void>;
@@ -248,8 +253,10 @@ export interface IPhasedRequestClient {
248253
readonly interactiveInputSink?: IInteractiveRequestInputSink;
249254
readonly interactiveSession?: IInteractiveRequestSession;
250255
readonly sessionId: string;
256+
readonly supportsRequestAdmission?: boolean;
251257
writeEventAsync(event: IDaemonEventEnvelope): Promise<void>;
252258
writeLogChunkAsync(operationId: string, stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
259+
writeQueuePositionAsync?(message: IDaemonRequestQueuePositionMessage): Promise<void>;
253260
writeResultAsync(result: IDaemonPhasedRequestResult): Promise<void>;
254261
writeTerminalPolicyAsync(result: IDaemonTerminalPolicyResult): Promise<void>;
255262
}
@@ -273,9 +280,13 @@ export interface IRequestSchedulerAcquireOptions {
273280

274281
// @beta
275282
export interface IResolvedGlobalCommandRequest {
283+
// (undocumented)
284+
readonly admission: IDaemonRequestAdmissionOptions | undefined;
276285
// (undocumented)
277286
readonly commandName: string;
278287
// (undocumented)
288+
readonly commandOrigin: DaemonRushCommandOrigin;
289+
// (undocumented)
279290
readonly cwd: string;
280291
// (undocumented)
281292
readonly environment: IGlobalCommandEnvironment;
@@ -287,9 +298,13 @@ export interface IResolvedGlobalCommandRequest {
287298

288299
// @beta
289300
export interface IResolveGlobalCommandRequestOptions {
301+
// (undocumented)
302+
readonly admission?: IDaemonRequestAdmissionOptions;
290303
// (undocumented)
291304
readonly commandName: string;
292305
// (undocumented)
306+
readonly commandOrigin: DaemonRushCommandOrigin;
307+
// (undocumented)
293308
readonly cwd: string;
294309
// (undocumented)
295310
readonly environment: Readonly<NodeJS.ProcessEnv>;

libraries/rush-daemon-protocol/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ The engine-agnostic **wire layer** spoken by every client of the Rush daemon (`r
1818
- **Per-subscription verbosity** — a pure filter applied at event serialization so each
1919
client receives its own verbosity subset without mutating shared engine state.
2020
- **Resolved phased-request contracts** — engine-agnostic request, enabled-state selection,
21-
and client-scoped result types for integrations that have already parsed a command and
22-
resolved it against a real warm operation graph.
21+
parsed built-in/custom command origin, and client-scoped result types for integrations that
22+
have already parsed a command and resolved it against a real warm operation graph.
2323
- **Final command result contract** — one typed success, warning, failure, or abort outcome
2424
with the authoritative Rush-compatible exit code, delivered after request output drains.
2525
- **Interactive request contracts** — request-tagged stdin frames preserve arbitrary bytes, while
2626
acknowledged raw-mode controls and typed terminal-policy results remain scoped to one request.
27+
- **Request admission contracts** — resolved no-wait and bounded-timeout options, typed admission
28+
failure codes, and capability-gated one-based queue-position control messages.
2729

2830
Part of the Rush 6 / rushd re-architecture:
2931
[microsoft/rushstack#5894](https://github.com/microsoft/rushstack/issues/5894).

libraries/rush-daemon-protocol/src/ControlMessageValidation.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
validateRawModeControl,
1010
validateTerminalPolicyControl
1111
} from './InteractiveControlValidation';
12+
import {
13+
validateRequestAdmissionCapability,
14+
validateRequestQueuePositionControl
15+
} from './RequestAdmissionControlValidation';
1216
/** Returns `true` when `value` is a non-null control record. @beta */
1317
export function isDaemonControlRecord(value: unknown): value is Record<string, unknown> {
1418
return typeof value === 'object' && value !== null;
@@ -33,43 +37,37 @@ function requireNumberField(record: Record<string, unknown>, field: string): voi
3337
fail(`Control message field "${field}" must be a number.`);
3438
}
3539
}
36-
3740
function requireVersion(payload: Record<string, unknown>): void {
3841
const version: Record<string, unknown> = requireRecordField(payload, 'protocolVersion');
3942
requireNumberField(version, 'major');
4043
requireNumberField(version, 'minor');
4144
}
42-
4345
function validateHelloAck(payload: Record<string, unknown>): void {
4446
requireVersion(payload);
4547
requireStringField(payload, 'sessionId');
4648
}
47-
4849
function validatePong(payload: Record<string, unknown>): void {
4950
if (payload.daemonVersion !== undefined) requireStringField(payload, 'daemonVersion');
5051
if (payload.protocolVersion !== undefined) requireVersion(payload);
5152
requireNumberField(payload, 'uptimeMs');
5253
}
53-
5454
function validateSubscribe(payload: Record<string, unknown>): void {
5555
if (typeof payload.isTTY !== 'boolean') {
5656
fail('Subscribe message payload.isTTY must be a boolean.');
5757
}
5858
validateInteractiveCapability(payload);
59+
validateRequestAdmissionCapability(payload);
5960
requireSubscribeVerbosity(payload);
6061
}
61-
6262
function requireSubscribeVerbosity(payload: Record<string, unknown>): void {
6363
if (payload.verbosity !== undefined && !isDaemonVerbosity(payload.verbosity)) {
6464
fail('Subscribe message payload.verbosity is not a known verbosity level.');
6565
}
6666
}
67-
6867
function validateError(payload: Record<string, unknown>): void {
6968
requireStringField(payload, 'code');
7069
requireStringField(payload, 'message');
7170
}
72-
7371
type ControlValidator = (payload: Record<string, unknown>) => void;
7472

7573
const noopValidator: ControlValidator = () => undefined;
@@ -84,7 +82,8 @@ const VALIDATORS_BY_KIND: Record<string, ControlValidator> = {
8482
error: validateError,
8583
setRawMode: validateRawModeControl,
8684
rawModeChanged: validateRawModeControl,
87-
terminalPolicy: validateTerminalPolicyControl
85+
terminalPolicy: validateTerminalPolicyControl,
86+
queuePosition: validateRequestQueuePositionControl
8887
};
8988

9089
/** Structurally validates a parsed control message. @beta */

libraries/rush-daemon-protocol/src/DaemonClientCaps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface IDaemonClientCaps {
1818
readonly isTTY: boolean;
1919
/** Whether the client supports request-scoped stdin and acknowledged raw-mode control. */
2020
readonly supportsInteractiveIO?: boolean;
21+
/** Whether the client supports request admission progress controls and typed failures. */
22+
readonly supportsRequestAdmission?: boolean;
2123
/** The verbosity subset this client receives. Defaults to `normal`. */
2224
readonly verbosity?: DaemonVerbosity;
2325
/** The client's terminal width in columns, when known. */

libraries/rush-daemon-protocol/src/DaemonCommandResult.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4+
import type { DaemonRequestAdmissionErrorCode } from './DaemonRequestAdmission';
5+
46
/**
57
* The semantic outcome of a daemon command.
68
*
@@ -16,6 +18,8 @@ export type DaemonCommandOutcome = 'success' | 'success-with-warning' | 'failure
1618
export interface IDaemonCommandResult {
1719
/** Whether cancellation or disconnect was observed, even if a cleanup failure determines the outcome. */
1820
readonly aborted: boolean;
21+
/** The typed admission failure, when execution never started. */
22+
readonly admissionErrorCode?: DaemonRequestAdmissionErrorCode;
1923
/** The process exit code a compatible in-process Rush invocation would return. */
2024
readonly exitCode: number;
2125
/** A failure description for execution or cleanup failures that were not already operation-scoped. */

libraries/rush-daemon-protocol/src/DaemonControlKinds.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ export const DAEMON_CONTROL_MESSAGE_KINDS: readonly [
1212
'error',
1313
'setRawMode',
1414
'rawModeChanged',
15-
'terminalPolicy'
15+
'terminalPolicy',
16+
'queuePosition'
1617
] = [
1718
'hello', 'helloAck', 'subscribe', 'unsubscribe', 'ping', 'pong', 'error',
18-
'setRawMode', 'rawModeChanged', 'terminalPolicy'
19+
'setRawMode', 'rawModeChanged', 'terminalPolicy', 'queuePosition'
1920
];
2021

2122
/** The union of control message `kind` discriminants. @beta */

libraries/rush-daemon-protocol/src/DaemonControlMessage.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
import type { IDaemonPongMessage } from './DaemonPongMessage';
1111
import type { DaemonProtocolErrorCode } from './DaemonProtocolError';
1212
import type { IDaemonProtocolVersion } from './DaemonProtocolVersion';
13+
import type { IDaemonRequestQueuePositionMessage } from './DaemonRequestAdmission';
1314

1415
/** The empty payload of control messages that carry no data. @beta */
1516
export type DaemonEmptyPayload = Record<string, never>;
@@ -74,4 +75,5 @@ export type DaemonControlMessage =
7475
| IDaemonErrorMessage
7576
| IDaemonSetRawModeMessage
7677
| IDaemonRawModeChangedMessage
77-
| IDaemonTerminalPolicyMessage;
78+
| IDaemonTerminalPolicyMessage
79+
| IDaemonRequestQueuePositionMessage;

0 commit comments

Comments
 (0)