Skip to content

Commit

Permalink
feat(core): lock graph creation when running in another process
Browse files Browse the repository at this point in the history
  • Loading branch information
AgentEnder committed Dec 18, 2024
1 parent a675bd2 commit c66f06a
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 11 deletions.
39 changes: 28 additions & 11 deletions packages/nx/src/project-graph/project-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import {
} from './utils/retrieve-workspace-files';
import { getPlugins } from './plugins/get-plugins';
import { logger } from '../utils/logger';
import { FileLock } from '../utils/file-lock';
import { join } from 'path';
import { workspaceDataDirectory } from '../utils/cache-directory';

/**
* Synchronously reads the latest cached copy of the workspace's ProjectGraph.
Expand Down Expand Up @@ -206,6 +209,20 @@ export function handleProjectGraphError(opts: { exitOnError: boolean }, e) {
}
}

async function readCachedGraphAndHydrateFileMap() {
const graph = readCachedProjectGraph();
const projectRootMap = Object.fromEntries(
Object.entries(graph.nodes).map(([project, { data }]) => [
data.root,
project,
])
);
const { allWorkspaceFiles, fileMap, rustReferences } =
await retrieveWorkspaceFiles(workspaceRoot, projectRootMap);
hydrateFileMap(fileMap, allWorkspaceFiles, rustReferences);
return graph;
}

/**
* Computes and returns a ProjectGraph.
*
Expand Down Expand Up @@ -235,18 +252,8 @@ export async function createProjectGraphAsync(
): Promise<ProjectGraph> {
if (process.env.NX_FORCE_REUSE_CACHED_GRAPH === 'true') {
try {
const graph = readCachedProjectGraph();
const projectRootMap = Object.fromEntries(
Object.entries(graph.nodes).map(([project, { data }]) => [
data.root,
project,
])
);
const { allWorkspaceFiles, fileMap, rustReferences } =
await retrieveWorkspaceFiles(workspaceRoot, projectRootMap);
hydrateFileMap(fileMap, allWorkspaceFiles, rustReferences);
return graph;
// If no cached graph is found, we will fall through to the normal flow
return readCachedGraphAndHydrateFileMap();
} catch (e) {
logger.verbose('Unable to use cached project graph', e);
}
Expand All @@ -267,6 +274,15 @@ export async function createProjectGraphAndSourceMapsAsync(
performance.mark('create-project-graph-async:start');

if (!daemonClient.enabled()) {
const lock = new FileLock(join(workspaceDataDirectory, 'project-graph'));
if (lock.locked) {
logger.verbose(
'Waiting for graph construction in another process to complete'
);
await lock.wait();
return { projectGraph: readCachedGraphAndHydrateFileMap() };
}
lock.lock();
try {
const res = await buildProjectGraphAndSourceMapsWithoutDaemon();
performance.measure(
Expand All @@ -290,6 +306,7 @@ export async function createProjectGraphAndSourceMapsAsync(
'create-project-graph-async:start',
'create-project-graph-async:end'
);
lock.unlock();
return res;
} catch (e) {
handleProjectGraphError(opts, e);
Expand Down
60 changes: 60 additions & 0 deletions packages/nx/src/utils/file-lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { existsSync, rmSync, watch, writeFileSync } from 'fs';

export class FileLock {
locked: boolean;

private lockFilePath: string;
lockPromise: Promise<void>;

constructor(private file: string) {
this.lockFilePath = `${file}.lock`;
this.locked = existsSync(this.lockFilePath);
}

lock() {
if (this.locked) {
throw new Error(`File ${this.lockFilePath} is already locked`);
}
this.locked = true;
writeFileSync(this.file, '');
}

unlock() {
if (!this.locked) {
throw new Error(`File ${this.lockFilePath} is not locked`);
}
this.locked = false;
rmSync(this.file);
}

wait(timeout?: number) {
return new Promise<void>((res, rej) => {
try {
let watcher = watch(this.lockFilePath);

watcher.on('change', (eventType) => {
if (eventType === 'delete') {
this.locked = false;
res();
watcher.close();
}
});
} catch {
// File watching is not supported
let start = Date.now();
setInterval(() => {
if (!this.locked || !existsSync(this.file)) {
res();
}

const elapsed = Date.now() - start;
if (timeout && elapsed > timeout) {
rej(
new Error(`Timeout waiting for file lock ${this.lockFilePath}`)
);
}
}, 2);
}
});
}
}

0 comments on commit c66f06a

Please sign in to comment.