Skip to content

Commit

Permalink
feat(core): ensure graph is not created in multiple processes at the …
Browse files Browse the repository at this point in the history
…same time
  • Loading branch information
AgentEnder committed Dec 19, 2024
1 parent c66f06a commit d1f6a60
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,11 @@ async function processFilesAndCreateAndSerializeProjectGraph(
serializedSourceMaps: null,
};
} else {
writeCache(g.projectFileMapCache, g.projectGraph);
writeCache(
g.projectFileMapCache,
g.projectGraph,
projectConfigurationsResult.sourceMaps
);
return g;
}
} catch (err) {
Expand Down
35 changes: 34 additions & 1 deletion packages/nx/src/project-graph/nx-deps-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '../utils/fileutils';
import { PackageJson } from '../utils/package-json';
import { nxVersion } from '../utils/versions';
import { ConfigurationSourceMaps } from './utils/project-configuration-utils';

export interface FileMapCache {
version: string;
Expand All @@ -34,6 +35,8 @@ export const nxProjectGraph = join(
);
export const nxFileMap = join(workspaceDataDirectory, 'file-map.json');

export const nxSourceMaps = join(workspaceDataDirectory, 'source-maps.json');

export function ensureCacheDirectory(): void {
try {
if (!existsSync(workspaceDataDirectory)) {
Expand Down Expand Up @@ -102,6 +105,31 @@ export function readProjectGraphCache(): null | ProjectGraph {
return data ?? null;
}

export function readSourceMapsCache(): null | ConfigurationSourceMaps {
performance.mark('read source-maps:start');
ensureCacheDirectory();

let data = null;
try {
if (fileExists(nxSourceMaps)) {
data = readJsonFile(nxSourceMaps);
}
} catch (error) {
console.log(
`Error reading '${nxSourceMaps}'. Continue the process without the cache.`
);
console.log(error);
}

performance.mark('read source-maps:end');
performance.measure(
'read cache',
'read source-maps:start',
'read source-maps:end'
);
return data ?? null;
}

export function createProjectFileMapCache(
nxJson: NxJsonConfiguration<'*' | string[]>,
packageJsonDeps: Record<string, string>,
Expand All @@ -123,7 +151,8 @@ export function createProjectFileMapCache(

export function writeCache(
cache: FileMapCache,
projectGraph: ProjectGraph
projectGraph: ProjectGraph,
sourceMaps: ConfigurationSourceMaps
): void {
performance.mark('write cache:start');
let retry = 1;
Expand All @@ -137,13 +166,17 @@ export function writeCache(
const unique = (Math.random().toString(16) + '0000000').slice(2, 10);
const tmpProjectGraphPath = `${nxProjectGraph}~${unique}`;
const tmpFileMapPath = `${nxFileMap}~${unique}`;
const tmpSourceMapPath = `${nxFileMap}~${unique}`;

try {
writeJsonFile(tmpProjectGraphPath, projectGraph);
renameSync(tmpProjectGraphPath, nxProjectGraph);

writeJsonFile(tmpFileMapPath, cache);
renameSync(tmpFileMapPath, nxFileMap);

writeJsonFile(tmpSourceMapPath, sourceMaps);
renameSync(tmpSourceMapPath, nxSourceMaps);
done = true;
} catch (err: any) {
if (err instanceof Error) {
Expand Down
14 changes: 12 additions & 2 deletions packages/nx/src/project-graph/project-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import {
readFileMapCache,
readProjectGraphCache,
readSourceMapsCache,
writeCache,
} from './nx-deps-cache';
import { ConfigurationResult } from './utils/project-configuration-utils';
Expand Down Expand Up @@ -170,7 +171,7 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
throw new ProjectGraphError(errors, projectGraph, sourceMaps);
} else {
if (cacheEnabled) {
writeCache(projectFileMapCache, projectGraph);
writeCache(projectFileMapCache, projectGraph, sourceMaps);
}
return { projectGraph, sourceMaps };
}
Expand Down Expand Up @@ -280,7 +281,16 @@ export async function createProjectGraphAndSourceMapsAsync(
'Waiting for graph construction in another process to complete'
);
await lock.wait();
return { projectGraph: readCachedGraphAndHydrateFileMap() };
const sourceMaps = readSourceMapsCache();
if (!sourceMaps) {
throw new Error(
'The project graph was computed in another process, but the source maps are missing.'
);
}
return {
projectGraph: await readCachedGraphAndHydrateFileMap(),
sourceMaps,
};
}
lock.lock();
try {
Expand Down
31 changes: 22 additions & 9 deletions packages/nx/src/utils/file-lock.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { existsSync, rmSync, watch, writeFileSync } from 'fs';
import { existsSync, rmSync, watch, writeFileSync, mkdirSync } from 'fs';
import { dirname } from 'path';

export class FileLock {
locked: boolean;

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

constructor(private file: string) {
constructor(file: string) {
// Ensure the directory exists
mkdirSync(dirname(file), { recursive: true });

this.lockFilePath = `${file}.lock`;
this.locked = existsSync(this.lockFilePath);
}
Expand All @@ -16,34 +20,43 @@ export class FileLock {
throw new Error(`File ${this.lockFilePath} is already locked`);
}
this.locked = true;
writeFileSync(this.file, '');
writeFileSync(this.lockFilePath, '');
}

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

wait(timeout?: number) {
return new Promise<void>((res, rej) => {
try {
// If the file watcher is supported, we can use it to wait for the lock file to be deleted.
let watcher = watch(this.lockFilePath);

watcher.on('change', (eventType) => {
if (eventType === 'delete') {
// For whatever reason, the node file watcher can sometimes
// emit rename events instead of delete events.
if (eventType === 'delete' || eventType === 'rename') {
this.locked = false;
res();
watcher.close();
res();
}
});
} catch {
// File watching is not supported
let start = Date.now();
setInterval(() => {
if (!this.locked || !existsSync(this.file)) {
let interval = setInterval(() => {
if (!this.locked || !existsSync(this.lockFilePath)) {
clearInterval(interval);
res();
}

Expand Down

0 comments on commit d1f6a60

Please sign in to comment.