Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rush] Split ProjectChangeAnalyzer, fix build cache hashes #4476

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

dmichon-msft
Copy link
Contributor

@dmichon-msft dmichon-msft commented Jan 9, 2024

Summary

Fixes #4400

Makes #3994 more feasible.

Details

Splits the functionality of ProjectChangeAnalyzer into two components:

  1. ProjectChangeAnalyzer#_tryGetSnapshotProviderAsync - Performs all the async I/O operations to determine the current state of the repository, including taking a snapshot of process.env so that per-operation hashes can be lazily computed in a stable manner.
  2. InputSnapshot - Synchronous data structure representing the state of the repository, which can be queried for operation-level state hashes, or tracked file lists. Performs the evaluation of input/output collisions as part of evaluating said queries. Includes evaluation of incrementalBuildIgnoredGlobs and dependsOnEnvVars, but does not include the hashes from dependencies, since the Operation graph is not provided to this API.

Moves build cache hash computation to the beforeExecuteOperations tap in CacheableOperationPlugin and updates the computation to factor in the runtime operation graph, rather than only the npm dependencies.

For a specific example of how this alters the build cache, consider a repository with one project and two phases in rush build:
Project A
Phases _phase:first and _phase:second, with _phase:second depends on self: ['_phase:first']

Define a command line parameter --some-flag-for-first that is forwarded only to _phase:first.

Before this change, running rush build and rush build --some-flag-for-first would produce different cache IDs for A (_phase:first), but the same cache id for A (_phase:second).

With this change the cache IDs for A (_phase:second) are also impacted by the presence of the --some-flag-for-first, since it can change the output of A (_phase:first).

How it was tested

For the splitting, added unit tests for InputSnapshot and ProjectChangeAnalyzer#_tryGetSnapshotProviderAsync.

For the build cache IDs, temporarily removed applicability of the --production CLI parameter from _phase:test and validated that running node ./apps/rush/lib/start-dev.js test and node ./apps/rush/lib/start-dev.js test --production still produced different cache keys for _phase:test.

For comparison, validated that the command-line.json alternation and existing rush test and rush test --production had a collision on the cache keys for _phase:test.

Impacted documentation

Any documentation about how build cache keys are computed.

Copy link
Member

@D4N14L D4N14L left a comment

Choose a reason for hiding this comment

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

We should do a pre-publish to verify that everything that is changing here doesn't break our flow, since we're very likely the largest customer on this tool.

"changes": [
{
"packageName": "@rushstack/lookup-by-path",
"comment": "Add \"IReadonlyLookupByPath\" interface to help unit tests for functions that consume \"LookupByPath\".",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"comment": "Add \"IReadonlyLookupByPath\" interface to help unit tests for functions that consume \"LookupByPath\".",
"comment": "Add `IReadonlyLookupByPath` interface to help unit tests for functions that consume `LookupByPath`.",

await projectChangeAnalyzer._ensureInitializedAsync(terminal);

const analyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(this.rushConfiguration);
const snapshotProvider: (() => Promise<IInputSnapshot | undefined>) | undefined =
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const snapshotProvider: (() => Promise<IInputSnapshot | undefined>) | undefined =
const snapshotProvider: IInputSnapshotProvider | undefined =

Comment on lines +560 to +562
terminal.writeLine(
`The Rush monorepo is not in a Git repository. Rush will proceed without incremental build support.`
);
Copy link
Member

Choose a reason for hiding this comment

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

Is this the only reason why the snapshot provider or initial snapshot would be undefined?

import { Colorize, type ITerminal } from '@rushstack/terminal';

import { Git } from './Git';
import { ProjectChangeAnalyzer } from './ProjectChangeAnalyzer';
import type { IInputSnapshot, IInputSnapshotProvider } from './snapshots/InputSnapshot';
Copy link
Member

Choose a reason for hiding this comment

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

The "snapshot" terminology may be a little confusing because Jest snapshots are stored in a similarly-named folder.

* is responsible for change detection in all incremental builds) to determine what actually chanaged.
*
* Calling `waitForChange()` will return a promise that resolves when the package-deps of one or
* more projects differ from the value the previous time it was invoked. The first time will always resolve with the full selection.
*/
export class ProjectWatcher {
private readonly _snapshotProvider: IInputSnapshotProvider;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private readonly _snapshotProvider: IInputSnapshotProvider;
private readonly _getSnapshotAsync: IInputSnapshotProvider;

Comment on lines +390 to +393
return options.buildCacheConfiguration.getCacheEntryId({
projectName: options.project.packageName,
projectStateHash: options.operationStateHash,
phaseName: options.phaseName
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return options.buildCacheConfiguration.getCacheEntryId({
projectName: options.project.packageName,
projectStateHash: options.operationStateHash,
phaseName: options.phaseName
const { buildCacheConfiguration, project: { packageName }, operationStateHash, phaseName } = options;
return buildCacheConfiguration.getCacheEntryId({
projectName: packageName,
projectStateHash: operationStateHash,
phaseName

Comment on lines +4 to +5
import * as path from 'path';
import { createHash, type Hash } from 'crypto';
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
import * as path from 'path';
import { createHash, type Hash } from 'crypto';
import * as path from 'node:path';
import { createHash, type Hash } from 'node:crypto';

for (const projectDeps of projectHashDeps.values()) {
if (shrinkwrapHash) {
projectDeps.set(shrinkwrapFile, shrinkwrapHash);
for (const project of rushConfiguration.projects) {
Copy link
Member

Choose a reason for hiding this comment

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

Always analyzing all projects?

* The methods on this interface are idempotent and will return the same result regardless of when they are executed.
* @beta
*/
export interface IInputSnapshot {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
export interface IInputSnapshot {
export interface IInputsSnapshot {

*
* @internal
*/
export class InputSnapshot implements IInputSnapshot {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
export class InputSnapshot implements IInputSnapshot {
export class InputsSnapshot implements IInputsSnapshot {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

[rush] Build cache keys do not correctly account for configuration changes in dependencies
3 participants