Skip to content

Commit

Permalink
feat(core): propagate errors and retry on cancellation
Browse files Browse the repository at this point in the history
  • Loading branch information
AgentEnder committed Jan 15, 2025
1 parent 519360c commit b44f168
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 32 deletions.
4 changes: 2 additions & 2 deletions docs/generated/devkit/readCachedProjectGraph.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Function: readCachedProjectGraph

**readCachedProjectGraph**(): [`ProjectGraph`](../../devkit/documents/ProjectGraph)
**readCachedProjectGraph**(): [`ProjectGraph`](../../devkit/documents/ProjectGraph) & \{ `computedAt`: `number` ; `errors`: `ProjectGraphErrorTypes`[] }

Synchronously reads the latest cached copy of the workspace's ProjectGraph.

#### Returns

[`ProjectGraph`](../../devkit/documents/ProjectGraph)
[`ProjectGraph`](../../devkit/documents/ProjectGraph) & \{ `computedAt`: `number` ; `errors`: `ProjectGraphErrorTypes`[] }

**`Throws`**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,12 @@ async function processFilesAndCreateAndSerializeProjectGraph(
};
}
}

writeCache(
g.projectFileMapCache,
g.projectGraph,
projectConfigurationsResult.sourceMaps,
errors
);
if (errors.length > 0) {
return {
error: new DaemonProjectGraphError(
Expand All @@ -316,11 +321,6 @@ async function processFilesAndCreateAndSerializeProjectGraph(
serializedSourceMaps: null,
};
} else {
writeCache(
g.projectFileMapCache,
g.projectGraph,
projectConfigurationsResult.sourceMaps
);
return g;
}
} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions packages/nx/src/native/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export declare class FileLock {
locked: boolean
constructor(lockFilePath: string)
unlock(): void
check(): boolean
wait(): Promise<void>
lock(): void
}
Expand Down
15 changes: 15 additions & 0 deletions packages/nx/src/native/utils/file_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ impl FileLock {
self.locked = false;
}

#[napi]
pub fn check(&mut self) -> Result<bool> {
// Check if the file is locked
let file_lock: std::result::Result<(), std::io::Error> = self.file.try_lock_exclusive();

if file_lock.is_ok() {
// Checking if the file is locked, locks it, so unlock it.
self.file.unlock()?;
}

self.locked = file_lock.is_err();
Ok(self.locked)
}

#[napi(ts_return_type = "Promise<void>")]
pub fn wait(&mut self, env: Env) -> napi::Result<napi::JsObject> {
if self.locked {
Expand All @@ -74,6 +88,7 @@ impl FileLock {
.create(true)
.open(&lock_file_path)?;
file.lock_shared()?;
file.unlock()?;
Ok(())
})
} else {
Expand Down
19 changes: 10 additions & 9 deletions packages/nx/src/project-graph/error-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@ import { ProjectConfiguration } from '../config/workspace-json-project-json';
import { ProjectGraph } from '../config/project-graph';
import { CreateNodesFunctionV2 } from './plugins/public-api';

export type ProjectGraphErrorTypes =
| AggregateCreateNodesError
| MergeNodesError
| CreateMetadataError
| ProjectsWithNoNameError
| MultipleProjectsWithSameNameError
| ProcessDependenciesError
| WorkspaceValidityError;

export class ProjectGraphError extends Error {
readonly #partialProjectGraph: ProjectGraph;
readonly #partialSourceMaps: ConfigurationSourceMaps;

constructor(
private readonly errors: Array<
| AggregateCreateNodesError
| MergeNodesError
| ProjectsWithNoNameError
| MultipleProjectsWithSameNameError
| ProcessDependenciesError
| CreateMetadataError
| WorkspaceValidityError
>,
private readonly errors: Array<ProjectGraphErrorTypes>,
partialProjectGraph: ProjectGraph,
partialSourceMaps: ConfigurationSourceMaps
) {
Expand Down
14 changes: 11 additions & 3 deletions packages/nx/src/project-graph/nx-deps-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { PackageJson } from '../utils/package-json';
import { nxVersion } from '../utils/versions';
import { ConfigurationSourceMaps } from './utils/project-configuration-utils';
import { ProjectGraphError, ProjectGraphErrorTypes } from './error-types';

export interface FileMapCache {
version: string;
Expand Down Expand Up @@ -80,7 +81,9 @@ export function readFileMapCache(): null | FileMapCache {
return data ?? null;
}

export function readProjectGraphCache(): null | ProjectGraph {
export function readProjectGraphCache():
| null
| (ProjectGraph & { errors: ProjectGraphErrorTypes[]; computedAt: number }) {
performance.mark('read project-graph:start');
ensureCacheDirectory();

Expand Down Expand Up @@ -152,7 +155,8 @@ export function createProjectFileMapCache(
export function writeCache(
cache: FileMapCache,
projectGraph: ProjectGraph,
sourceMaps: ConfigurationSourceMaps
sourceMaps: ConfigurationSourceMaps,
errors: ProjectGraphErrorTypes[]
): void {
performance.mark('write cache:start');
let retry = 1;
Expand All @@ -169,7 +173,11 @@ export function writeCache(
const tmpSourceMapPath = `${nxSourceMaps}~${unique}`;

try {
writeJsonFile(tmpProjectGraphPath, projectGraph);
writeJsonFile(tmpProjectGraphPath, {
...projectGraph,
errors,
computedAt: Date.now(),
});
renameSync(tmpProjectGraphPath, nxProjectGraph);

writeJsonFile(tmpFileMapPath, cache);
Expand Down
43 changes: 32 additions & 11 deletions packages/nx/src/project-graph/project-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isAggregateProjectGraphError,
ProjectConfigurationsError,
ProjectGraphError,
ProjectGraphErrorTypes,
} from './error-types';
import {
readFileMapCache,
Expand All @@ -44,8 +45,11 @@ import { DelayedSpinner } from '../utils/delayed-spinner';
* Synchronously reads the latest cached copy of the workspace's ProjectGraph.
* @throws {Error} if there is no cached ProjectGraph to read from
*/
export function readCachedProjectGraph(): ProjectGraph {
const projectGraphCache: ProjectGraph = readProjectGraphCache();
export function readCachedProjectGraph(): ProjectGraph & {
errors: ProjectGraphErrorTypes[];
computedAt: number;
} {
const projectGraphCache = readProjectGraphCache();
if (!projectGraphCache) {
const angularSpecificError = fileExists(`${workspaceRoot}/angular.json`)
? stripIndents`
Expand Down Expand Up @@ -168,12 +172,13 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
...(projectGraphError?.errors ?? []),
];

if (cacheEnabled) {
writeCache(projectFileMapCache, projectGraph, sourceMaps, errors);
}

if (errors.length > 0) {
throw new ProjectGraphError(errors, projectGraph, sourceMaps);
} else {
if (cacheEnabled) {
writeCache(projectFileMapCache, projectGraph, sourceMaps);
}
return { projectGraph, sourceMaps };
}
}
Expand Down Expand Up @@ -252,7 +257,11 @@ export async function createProjectGraphAsync(
if (process.env.NX_FORCE_REUSE_CACHED_GRAPH === 'true') {
try {
// If no cached graph is found, we will fall through to the normal flow
return readCachedGraphAndHydrateFileMap();
const graph = await readCachedGraphAndHydrateFileMap();
if (graph.errors.length > 0) {
throw new ProjectGraphError(graph.errors, graph, readSourceMapsCache());
}
return graph;
} catch (e) {
logger.verbose('Unable to use cached project graph', e);
}
Expand All @@ -276,8 +285,10 @@ export async function createProjectGraphAndSourceMapsAsync(
const lock = new FileLock(
join(workspaceDataDirectory, 'project-graph.lock')
);
const initiallyLocked = lock.locked;
let locked = lock.locked;

if (lock.locked) {
while (lock.locked) {
logger.verbose(
'Waiting for graph construction in another process to complete'
);
Expand All @@ -300,12 +311,22 @@ export async function createProjectGraphAndSourceMapsAsync(
'The project graph was computed in another process, but the source maps are missing.'
);
}
return {
projectGraph: await readCachedGraphAndHydrateFileMap(),
sourceMaps,
};
const graph = await readCachedGraphAndHydrateFileMap();
if (graph.errors.length > 0) {
throw new ProjectGraphError(graph.errors, graph, sourceMaps);
}
if (Date.now() - graph.computedAt < 10_000) {
// If the graph was computed in the last 10 seconds, we can assume it's fresh
return {
projectGraph: graph,
sourceMaps,
};
}
locked = lock.check();
}
// if (!initiallyLocked) {
lock.lock();
// }
try {
const res = await buildProjectGraphAndSourceMapsWithoutDaemon();
performance.measure(
Expand Down
2 changes: 1 addition & 1 deletion packages/nx/src/utils/project-graph-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function getSourceDirOfDependentProjects(
*/
export function findAllProjectNodeDependencies(
parentNodeName: string,
projectGraph = readCachedProjectGraph(),
projectGraph: ProjectGraph = readCachedProjectGraph(),
includeExternalDependencies = false
): string[] {
const dependencyNodeNames = new Set<string>();
Expand Down

0 comments on commit b44f168

Please sign in to comment.