diff --git a/build-tests/rush-redis-cobuild-plugin-integration-test/README.md b/build-tests/rush-redis-cobuild-plugin-integration-test/README.md index cde55a7bff7..0412bcc358d 100644 --- a/build-tests/rush-redis-cobuild-plugin-integration-test/README.md +++ b/build-tests/rush-redis-cobuild-plugin-integration-test/README.md @@ -150,3 +150,24 @@ rm -rf common/temp/build-cache 5. Open command palette (Ctrl+Shift+P or Command+Shift+P) and select `Tasks: Run Task` and select `cobuild`. Expected behavior: Cobuild feature is enabled, cobuild related logs out in both terminals. These two cobuild commands fail because of the failing build of project "A". And, one of them restored the failing build cache created by the other one. + +#### Case 5: Sharded cobuilds + +Enable the `allowCobuildWithoutCache` experiment in `experiments.json`. + +Navigate to the sandbox for sharded cobuilds, +```sh +cd sandbox/sharded-repo +``` + +Next, start up your Redis instance, +```sh +docker compose down && docker compose up -d +``` + +Then, open 2 terminals and run this in each (changing the RUSH_COBUILD_RUNNER_ID across the 2 terminals), +```sh +rm -rf common/temp/build-cache && RUSH_COBUILD_CONTEXT_ID=foo REDIS_PASS=redis123 RUSH_COBUILD_RUNNER_ID=runner1 node ../../lib/runRush.js cobuild -p 10 --timeline +``` + +If all goes well, you should see a bunch of operation with `- shard xx/yy`. Operations `h (build)` and `e (build)` are both sharded heavily and should be cobuild compatible. To validate changes you're making, ensure that the timeline view for all of the shards of those 2 operations are cobuilt across both terminals. If they're not, something is wrong with your update. \ No newline at end of file diff --git a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/config/rush/experiments.json b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/config/rush/experiments.json index fef826208c3..14a02ec1f2f 100644 --- a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/config/rush/experiments.json +++ b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/config/rush/experiments.json @@ -3,7 +3,7 @@ * Rush features. More documentation is available on the Rush website: https://rushjs.io */ { - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", + "$schema": "../../../../../../../libraries/rush-lib/src/schemas/experiments.schema.json", /** * By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'. @@ -40,7 +40,7 @@ * If true, the phased commands feature is enabled. To use this feature, create a "phased" command * in common/config/rush/command-line.json. */ - "phasedCommands": true + "phasedCommands": true, /** * If true, perform a clean install after when running `rush install` or `rush update` if the @@ -52,4 +52,6 @@ * If true, print the outputs of shell commands defined in event hooks to the console. */ // "printEventHooksOutputToConsole": true + + "allowCobuildWithoutCache": true } diff --git a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/projects/e/config/rush-project.json b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/projects/e/config/rush-project.json index aae24577ea5..e6534be5bc8 100644 --- a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/projects/e/config/rush-project.json +++ b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/projects/e/config/rush-project.json @@ -1,18 +1,19 @@ { + "$schema": "../../../../../../../libraries/rush-lib/src/schemas/rush-project.schema.json", "operationSettings": [ { "operationName": "_phase:build", "outputFolderNames": ["dist"], + "allowCobuildOrchestration": true, + "disableBuildCacheForOperation": true, "sharding": { - "count": 75, - "shardOperationSettings": { - "weight": 10 - } + "count": 75 } }, { "operationName": "_phase:build:shard", - "weight": 1 + "weight": 10, + "allowCobuildOrchestration": true } ] } diff --git a/common/changes/@microsoft/rush/sennyeya-allow-cobuild-orchestration_2024-08-08-23-11.json b/common/changes/@microsoft/rush/sennyeya-allow-cobuild-orchestration_2024-08-08-23-11.json new file mode 100644 index 00000000000..54824242087 --- /dev/null +++ b/common/changes/@microsoft/rush/sennyeya-allow-cobuild-orchestration_2024-08-08-23-11.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Adds a new experiment 'allowCobuildWithoutCache' for cobuilds to allow uncacheable operations to benefit from cobuild orchestration without using the build cache.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index b1315c3252d..48ce89bb8d1 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -105,6 +105,7 @@ export class CobuildConfiguration { readonly cobuildFeatureEnabled: boolean; readonly cobuildLeafProjectLogOnlyAllowed: boolean; readonly cobuildRunnerId: string; + readonly cobuildWithoutCacheAllowed: boolean; // (undocumented) createLockProviderAsync(terminal: ITerminal): Promise; // (undocumented) @@ -469,6 +470,7 @@ export interface IExecutionResult { // @beta export interface IExperimentsJson { + allowCobuildWithoutCache?: boolean; buildCacheWithAllowWarningsInSuccessfulBuild?: boolean; buildSkipWithAllowWarningsInSuccessfulBuild?: boolean; cleanInstallAfterNpmrcChanges?: boolean; @@ -625,6 +627,7 @@ export interface IOperationRunnerContext { // @alpha (undocumented) export interface IOperationSettings { + allowCobuildWithoutCache?: boolean; dependsOnAdditionalFiles?: string[]; dependsOnEnvVars?: string[]; disableBuildCacheForOperation?: boolean; diff --git a/libraries/rush-lib/assets/rush-init/common/config/rush/experiments.json b/libraries/rush-lib/assets/rush-init/common/config/rush/experiments.json index 326b7c42af0..8bc016fb1d2 100644 --- a/libraries/rush-lib/assets/rush-init/common/config/rush/experiments.json +++ b/libraries/rush-lib/assets/rush-init/common/config/rush/experiments.json @@ -96,5 +96,11 @@ * This ensures that important notices will be seen by anyone doing active development, since people often * ignore normal discussion group messages or don't know to subscribe. */ - /*[LINE "HYPOTHETICAL"]*/ "rushAlerts": true + /*[LINE "HYPOTHETICAL"]*/ "rushAlerts": true, + + + /** + * When using cobuilds, this experiment allows uncacheable operations to benefit from cobuild orchestration without using the build cache. + */ + /*[LINE "HYPOTHETICAL"]*/ "allowCobuildWithoutCache": true } diff --git a/libraries/rush-lib/src/api/CobuildConfiguration.ts b/libraries/rush-lib/src/api/CobuildConfiguration.ts index 6d274ec43bd..0f169a2966a 100644 --- a/libraries/rush-lib/src/api/CobuildConfiguration.ts +++ b/libraries/rush-lib/src/api/CobuildConfiguration.ts @@ -69,18 +69,26 @@ export class CobuildConfiguration { */ public readonly cobuildLeafProjectLogOnlyAllowed: boolean; + /** + * If true, operations can opt into leveraging cobuilds without restoring from the build cache. + * Operations will need to us the allowCobuildWithoutCache flag to opt into this behavior per phase. + */ + public readonly cobuildWithoutCacheAllowed: boolean; + private _cobuildLockProvider: ICobuildLockProvider | undefined; private readonly _cobuildLockProviderFactory: CobuildLockProviderFactory; private readonly _cobuildJson: ICobuildJson; private constructor(options: ICobuildConfigurationOptions) { - const { cobuildJson, cobuildLockProviderFactory } = options; + const { cobuildJson, cobuildLockProviderFactory, rushConfiguration } = options; this.cobuildContextId = EnvironmentConfiguration.cobuildContextId; this.cobuildFeatureEnabled = this.cobuildContextId ? cobuildJson.cobuildFeatureEnabled : false; this.cobuildRunnerId = EnvironmentConfiguration.cobuildRunnerId || uuidv4(); this.cobuildLeafProjectLogOnlyAllowed = EnvironmentConfiguration.cobuildLeafProjectLogOnlyAllowed ?? false; + this.cobuildWithoutCacheAllowed = + rushConfiguration.experimentsConfiguration.configuration.allowCobuildWithoutCache ?? false; this._cobuildLockProviderFactory = cobuildLockProviderFactory; this._cobuildJson = cobuildJson; diff --git a/libraries/rush-lib/src/api/ExperimentsConfiguration.ts b/libraries/rush-lib/src/api/ExperimentsConfiguration.ts index 0e66fa3dc1c..e9a6f46bec0 100644 --- a/libraries/rush-lib/src/api/ExperimentsConfiguration.ts +++ b/libraries/rush-lib/src/api/ExperimentsConfiguration.ts @@ -106,6 +106,13 @@ export interface IExperimentsJson { * ignore normal discussion group messages or don't know to subscribe. */ rushAlerts?: boolean; + + /** + * Allow cobuilds without using the build cache to store previous execution info. When setting up + * distributed builds, Rush will allow uncacheable projects to still leverage the cobuild feature. + * This is useful when you want to speed up operations that can't (or shouldn't) be cached. + */ + allowCobuildWithoutCache?: boolean; } const _EXPERIMENTS_JSON_SCHEMA: JsonSchema = JsonSchema.fromLoadedObject(schemaJson); diff --git a/libraries/rush-lib/src/api/RushProjectConfiguration.ts b/libraries/rush-lib/src/api/RushProjectConfiguration.ts index 279c6f92b64..5c6b60ff73b 100644 --- a/libraries/rush-lib/src/api/RushProjectConfiguration.ts +++ b/libraries/rush-lib/src/api/RushProjectConfiguration.ts @@ -134,6 +134,11 @@ export interface IOperationSettings { * determined by the -p flag. */ weight?: number; + + /** + * If true, this operation can use cobuilds for orchestration without restoring build cache entries. + */ + allowCobuildWithoutCache?: boolean; } interface IOldRushProjectJson { diff --git a/libraries/rush-lib/src/logic/operations/CacheableOperationPlugin.ts b/libraries/rush-lib/src/logic/operations/CacheableOperationPlugin.ts index a29db3d9c97..ad99f9a41b2 100644 --- a/libraries/rush-lib/src/logic/operations/CacheableOperationPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/CacheableOperationPlugin.ts @@ -99,6 +99,25 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { ? new DisjointSet() : undefined; + for (const operation of recordByOperation.keys()) { + if ( + operation.settings?.allowCobuildWithoutCache && + !cobuildConfiguration?.cobuildWithoutCacheAllowed + ) { + throw new Error( + `Operation ${operation.name} is not allowed to run without the cobuild orchestration experiment enabled. You must enable the "allowCobuildWithoutCache" experiment in experiments.json.` + ); + } + if ( + operation.settings?.allowCobuildWithoutCache && + !operation.settings.disableBuildCacheForOperation + ) { + throw new Error( + `Operation ${operation.name} must have disableBuildCacheForOperation set to true when using the cobuild orchestration experiment. This is to prevent implicit cache dependencies for this operation.` + ); + } + } + await Async.forEachAsync( recordByOperation.keys(), async (operation: Operation) => { @@ -258,7 +277,8 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { phase, configHash, terminal: buildCacheTerminal, - operationMetadataManager + operationMetadataManager, + operation: record.operation }); // Try to acquire the cobuild lock @@ -359,15 +379,17 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { if (cobuildCompletedState) { const { status, cacheId } = cobuildCompletedState; + if (record.operation.settings?.allowCobuildWithoutCache) { + // This should only be enabled if the experiment for cobuild orchestration is enabled. + return status; + } + const restoreFromCacheSuccess: boolean = await restoreCacheAsync( cobuildLock.projectBuildCache, cacheId ); if (restoreFromCacheSuccess) { - if (cobuildCompletedState) { - return cobuildCompletedState.status; - } return status; } } else if (!buildCacheContext.isCacheReadAttempted && buildCacheContext.isCacheReadAllowed) { @@ -569,7 +591,8 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { phase, configHash, terminal, - operationMetadataManager + operationMetadataManager, + operation }: { buildCacheContext: IOperationBuildCacheContext; buildCacheConfiguration: BuildCacheConfiguration | undefined; @@ -578,10 +601,11 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { configHash: string; terminal: ITerminal; operationMetadataManager: OperationMetadataManager | undefined; + operation: Operation; }): Promise { if (!buildCacheContext.projectBuildCache) { const { cacheDisabledReason } = buildCacheContext; - if (cacheDisabledReason) { + if (cacheDisabledReason && !operation.settings?.allowCobuildWithoutCache) { terminal.writeVerboseLine(cacheDisabledReason); return; } @@ -863,7 +887,7 @@ export function clusterOperations( for (const [operation, { cacheDisabledReason }] of operationBuildCacheMap) { const { associatedProject: project, associatedPhase: phase } = operation; if (project && phase) { - if (cacheDisabledReason) { + if (cacheDisabledReason && !operation.settings?.allowCobuildWithoutCache) { /** * Group the project build cache disabled with its consumers. This won't affect too much in * a monorepo with high build cache coverage. diff --git a/libraries/rush-lib/src/schemas/experiments.schema.json b/libraries/rush-lib/src/schemas/experiments.schema.json index c2026513fdb..d18ec334876 100644 --- a/libraries/rush-lib/src/schemas/experiments.schema.json +++ b/libraries/rush-lib/src/schemas/experiments.schema.json @@ -66,6 +66,10 @@ "description": "If true, when running in watch mode, Rush will check for phase scripts named `_phase::ipc` and run them instead of `_phase:` if they exist. The created child process will be provided with an IPC channel and expected to persist across invocations.", "type": "boolean" }, + "allowCobuildWithoutCache": { + "description": "When using cobuilds, this experiment allows uncacheable operations to benefit from cobuild orchestration without using the build cache.", + "type": "boolean" + }, "rushAlerts": { "description": "(UNDER DEVELOPMENT) The Rush alerts feature provides a way to send announcements to engineers working in the monorepo, by printing directly in the user's shell window when they invoke Rush commands. This ensures that important notices will be seen by anyone doing active development, since people often ignore normal discussion group messages or don't know to subscribe.", "type": "boolean" diff --git a/libraries/rush-lib/src/schemas/rush-project.schema.json b/libraries/rush-lib/src/schemas/rush-project.schema.json index 3fe4645d924..50bec9241da 100644 --- a/libraries/rush-lib/src/schemas/rush-project.schema.json +++ b/libraries/rush-lib/src/schemas/rush-project.schema.json @@ -102,6 +102,10 @@ "description": "The number of concurrency units that this operation should take up. The maximum concurrency units is determined by the -p flag.", "type": "integer", "minimum": 0 + }, + "allowCobuildWithoutCache": { + "type": "boolean", + "description": "If true, this operation will not need to use the build cache to leverage cobuilds" } } }