feat(core): lock graph creation when running in another process (#29408)
## Current Behavior
Running Nx in multiple processes at the same time with the daemon
disabled can cripple a system due to excess memory usage when creating
the graph. This is due to plugin workers being started per-parent
process when there is no daemon. This change enables a file lock to
prevent the simultaneous processing, and read from the cache when the
first run completes.
Currently, running `nx show projects` 30 times in parallel looks
something like this:
30 processes exited within 37535ms
## Expected Behavior
30 processes exited within 6435ms
## Test Script
```js
//@ts-check
const { spawn } = require('child_process');
let alive = new Set();
let start = Date.now();
let iterations = 30;
for (let i = 0; i < iterations; i++) {
const cp = spawn('npx nx show projects', [], {
shell: true,
env: {
...process.env,
NX_DAEMON: 'false',
NX_VERBOSE_LOGGING: 'true',
},
});
alive.add(i);
// cp.stdout.on('data', (data) => {
// console.log(`stdout [${i}]: ${data}`);
// });
cp.stderr.on('data', (data) => {
console.error(`stderr [${i}]: ${data}`);
});
cp.on('exit', (code) => {
console.log(`child process ${i} exited with code ${code}`);
alive.delete(i);
});
}
const i = setInterval(() => {
if (alive.size > 0) {
} else {
clearInterval(i);
console.log(
`${iterations} processes exited within ${Date.now() - start}ms`
);
}
}, 1);
```
This commit is contained in:
parent
cbbe14b8e5
commit
5721ea3c21
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1509,6 +1509,7 @@ dependencies = [
|
|||||||
"swc_ecma_visit",
|
"swc_ecma_visit",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
|||||||
@ -20,6 +20,7 @@ It only uses language primitives and immutable objects
|
|||||||
### Classes
|
### Classes
|
||||||
|
|
||||||
- [AggregateCreateNodesError](../../devkit/documents/AggregateCreateNodesError)
|
- [AggregateCreateNodesError](../../devkit/documents/AggregateCreateNodesError)
|
||||||
|
- [StaleProjectGraphCacheError](../../devkit/documents/StaleProjectGraphCacheError)
|
||||||
|
|
||||||
### Interfaces
|
### Interfaces
|
||||||
|
|
||||||
|
|||||||
144
docs/generated/devkit/StaleProjectGraphCacheError.md
Normal file
144
docs/generated/devkit/StaleProjectGraphCacheError.md
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# Class: StaleProjectGraphCacheError
|
||||||
|
|
||||||
|
## Hierarchy
|
||||||
|
|
||||||
|
- `Error`
|
||||||
|
|
||||||
|
↳ **`StaleProjectGraphCacheError`**
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
### Constructors
|
||||||
|
|
||||||
|
- [constructor](../../devkit/documents/StaleProjectGraphCacheError#constructor)
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
- [cause](../../devkit/documents/StaleProjectGraphCacheError#cause): unknown
|
||||||
|
- [message](../../devkit/documents/StaleProjectGraphCacheError#message): string
|
||||||
|
- [name](../../devkit/documents/StaleProjectGraphCacheError#name): string
|
||||||
|
- [stack](../../devkit/documents/StaleProjectGraphCacheError#stack): string
|
||||||
|
- [prepareStackTrace](../../devkit/documents/StaleProjectGraphCacheError#preparestacktrace): Function
|
||||||
|
- [stackTraceLimit](../../devkit/documents/StaleProjectGraphCacheError#stacktracelimit): number
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- [captureStackTrace](../../devkit/documents/StaleProjectGraphCacheError#capturestacktrace)
|
||||||
|
|
||||||
|
## Constructors
|
||||||
|
|
||||||
|
### constructor
|
||||||
|
|
||||||
|
• **new StaleProjectGraphCacheError**(): [`StaleProjectGraphCacheError`](../../devkit/documents/StaleProjectGraphCacheError)
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
[`StaleProjectGraphCacheError`](../../devkit/documents/StaleProjectGraphCacheError)
|
||||||
|
|
||||||
|
#### Overrides
|
||||||
|
|
||||||
|
Error.constructor
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### cause
|
||||||
|
|
||||||
|
• `Optional` **cause**: `unknown`
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
Error.cause
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### message
|
||||||
|
|
||||||
|
• **message**: `string`
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
Error.message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### name
|
||||||
|
|
||||||
|
• **name**: `string`
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
Error.name
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### stack
|
||||||
|
|
||||||
|
• `Optional` **stack**: `string`
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
Error.stack
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### prepareStackTrace
|
||||||
|
|
||||||
|
▪ `Static` `Optional` **prepareStackTrace**: (`err`: `Error`, `stackTraces`: `CallSite`[]) => `any`
|
||||||
|
|
||||||
|
Optional override for formatting stack traces
|
||||||
|
|
||||||
|
**`See`**
|
||||||
|
|
||||||
|
https://v8.dev/docs/stack-trace-api#customizing-stack-traces
|
||||||
|
|
||||||
|
#### Type declaration
|
||||||
|
|
||||||
|
▸ (`err`, `stackTraces`): `any`
|
||||||
|
|
||||||
|
##### Parameters
|
||||||
|
|
||||||
|
| Name | Type |
|
||||||
|
| :------------ | :----------- |
|
||||||
|
| `err` | `Error` |
|
||||||
|
| `stackTraces` | `CallSite`[] |
|
||||||
|
|
||||||
|
##### Returns
|
||||||
|
|
||||||
|
`any`
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
Error.prepareStackTrace
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### stackTraceLimit
|
||||||
|
|
||||||
|
▪ `Static` **stackTraceLimit**: `number`
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
Error.stackTraceLimit
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### captureStackTrace
|
||||||
|
|
||||||
|
▸ **captureStackTrace**(`targetObject`, `constructorOpt?`): `void`
|
||||||
|
|
||||||
|
Create .stack property on a target object
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
| Name | Type |
|
||||||
|
| :---------------- | :--------- |
|
||||||
|
| `targetObject` | `object` |
|
||||||
|
| `constructorOpt?` | `Function` |
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
`void`
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
Error.captureStackTrace
|
||||||
@ -1,9 +1,15 @@
|
|||||||
# Function: readCachedProjectGraph
|
# Function: readCachedProjectGraph
|
||||||
|
|
||||||
▸ **readCachedProjectGraph**(): [`ProjectGraph`](../../devkit/documents/ProjectGraph)
|
▸ **readCachedProjectGraph**(`minimumComputedAt?`): [`ProjectGraph`](../../devkit/documents/ProjectGraph)
|
||||||
|
|
||||||
Synchronously reads the latest cached copy of the workspace's ProjectGraph.
|
Synchronously reads the latest cached copy of the workspace's ProjectGraph.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| :------------------- | :------- | :----------------------------------------------------------------------------- |
|
||||||
|
| `minimumComputedAt?` | `number` | The minimum timestamp that the cached ProjectGraph must have been computed at. |
|
||||||
|
|
||||||
#### Returns
|
#### Returns
|
||||||
|
|
||||||
[`ProjectGraph`](../../devkit/documents/ProjectGraph)
|
[`ProjectGraph`](../../devkit/documents/ProjectGraph)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ It only uses language primitives and immutable objects
|
|||||||
### Classes
|
### Classes
|
||||||
|
|
||||||
- [AggregateCreateNodesError](../../devkit/documents/AggregateCreateNodesError)
|
- [AggregateCreateNodesError](../../devkit/documents/AggregateCreateNodesError)
|
||||||
|
- [StaleProjectGraphCacheError](../../devkit/documents/StaleProjectGraphCacheError)
|
||||||
|
|
||||||
### Interfaces
|
### Interfaces
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export function parseTargetString(
|
|||||||
targetString: string,
|
targetString: string,
|
||||||
projectGraphOrCtx?: ProjectGraph | ExecutorContext
|
projectGraphOrCtx?: ProjectGraph | ExecutorContext
|
||||||
): Target {
|
): Target {
|
||||||
let projectGraph =
|
let projectGraph: ProjectGraph =
|
||||||
projectGraphOrCtx && 'projectGraph' in projectGraphOrCtx
|
projectGraphOrCtx && 'projectGraph' in projectGraphOrCtx
|
||||||
? projectGraphOrCtx.projectGraph
|
? projectGraphOrCtx.projectGraph
|
||||||
: (projectGraphOrCtx as ProjectGraph);
|
: (projectGraphOrCtx as ProjectGraph);
|
||||||
|
|||||||
@ -74,3 +74,5 @@ assert_fs = "1.0.10"
|
|||||||
# This is only used for unit tests
|
# This is only used for unit tests
|
||||||
swc_ecma_dep_graph = "0.109.1"
|
swc_ecma_dep_graph = "0.109.1"
|
||||||
tempfile = "3.13.0"
|
tempfile = "3.13.0"
|
||||||
|
# We only explicitly use tokio for async tests
|
||||||
|
tokio = "1.38.0"
|
||||||
|
|||||||
@ -299,7 +299,12 @@ async function processFilesAndCreateAndSerializeProjectGraph(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
writeCache(
|
||||||
|
g.projectFileMapCache,
|
||||||
|
g.projectGraph,
|
||||||
|
projectConfigurationsResult.sourceMaps,
|
||||||
|
errors
|
||||||
|
);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return {
|
return {
|
||||||
error: new DaemonProjectGraphError(
|
error: new DaemonProjectGraphError(
|
||||||
@ -316,7 +321,6 @@ async function processFilesAndCreateAndSerializeProjectGraph(
|
|||||||
serializedSourceMaps: null,
|
serializedSourceMaps: null,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
writeCache(g.projectFileMapCache, g.projectGraph);
|
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -63,7 +63,10 @@ export type {
|
|||||||
PostTasksExecutionContext,
|
PostTasksExecutionContext,
|
||||||
} from './project-graph/plugins';
|
} from './project-graph/plugins';
|
||||||
|
|
||||||
export { AggregateCreateNodesError } from './project-graph/error-types';
|
export {
|
||||||
|
AggregateCreateNodesError,
|
||||||
|
StaleProjectGraphCacheError,
|
||||||
|
} from './project-graph/error-types';
|
||||||
|
|
||||||
export { createNodesFromFiles } from './project-graph/plugins';
|
export { createNodesFromFiles } from './project-graph/plugins';
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
use crate::native::db::connection::NxDbConnection;
|
use crate::native::db::connection::NxDbConnection;
|
||||||
use fs4::fs_std::FileExt;
|
|
||||||
use rusqlite::{Connection, OpenFlags};
|
use rusqlite::{Connection, OpenFlags};
|
||||||
use std::fs::{remove_file, File};
|
use std::fs::{remove_file, File};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@ -12,9 +11,7 @@ pub(super) struct LockFile {
|
|||||||
|
|
||||||
pub(super) fn unlock_file(lock_file: &LockFile) {
|
pub(super) fn unlock_file(lock_file: &LockFile) {
|
||||||
if lock_file.path.exists() {
|
if lock_file.path.exists() {
|
||||||
lock_file
|
fs4::fs_std::FileExt::unlock(&lock_file.file)
|
||||||
.file
|
|
||||||
.unlock()
|
|
||||||
.and_then(|_| remove_file(&lock_file.path))
|
.and_then(|_| remove_file(&lock_file.path))
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@ -26,8 +23,7 @@ pub(super) fn create_lock_file(db_path: &Path) -> anyhow::Result<LockFile> {
|
|||||||
.map_err(|e| anyhow::anyhow!("Unable to create db lock file: {:?}", e))?;
|
.map_err(|e| anyhow::anyhow!("Unable to create db lock file: {:?}", e))?;
|
||||||
|
|
||||||
trace!("Getting lock on db lock file");
|
trace!("Getting lock on db lock file");
|
||||||
lock_file
|
fs4::fs_std::FileExt::lock_exclusive(&lock_file)
|
||||||
.lock_exclusive()
|
|
||||||
.inspect(|_| trace!("Got lock on db lock file"))
|
.inspect(|_| trace!("Got lock on db lock file"))
|
||||||
.map_err(|e| anyhow::anyhow!("Unable to lock the db lock file: {:?}", e))?;
|
.map_err(|e| anyhow::anyhow!("Unable to lock the db lock file: {:?}", e))?;
|
||||||
Ok(LockFile {
|
Ok(LockFile {
|
||||||
@ -77,7 +73,10 @@ pub(super) fn initialize_db(nx_version: String, db_path: &Path) -> anyhow::Resul
|
|||||||
Ok(c)
|
Ok(c)
|
||||||
}
|
}
|
||||||
Err(reason) => {
|
Err(reason) => {
|
||||||
trace!("Unable to connect to existing database because: {:?}", reason);
|
trace!(
|
||||||
|
"Unable to connect to existing database because: {:?}",
|
||||||
|
reason
|
||||||
|
);
|
||||||
trace!("Removing existing incompatible database");
|
trace!("Removing existing incompatible database");
|
||||||
remove_file(db_path)?;
|
remove_file(db_path)?;
|
||||||
|
|
||||||
|
|||||||
9
packages/nx/src/native/index.d.ts
vendored
9
packages/nx/src/native/index.d.ts
vendored
@ -13,6 +13,15 @@ export declare class ChildProcess {
|
|||||||
onOutput(callback: (message: string) => void): void
|
onOutput(callback: (message: string) => void): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare class FileLock {
|
||||||
|
locked: boolean
|
||||||
|
constructor(lockFilePath: string)
|
||||||
|
unlock(): void
|
||||||
|
check(): boolean
|
||||||
|
wait(): Promise<void>
|
||||||
|
lock(): void
|
||||||
|
}
|
||||||
|
|
||||||
export declare class HashPlanner {
|
export declare class HashPlanner {
|
||||||
constructor(nxJson: NxJson, projectGraph: ExternalObject<ProjectGraph>)
|
constructor(nxJson: NxJson, projectGraph: ExternalObject<ProjectGraph>)
|
||||||
getPlans(taskIds: Array<string>, taskGraph: TaskGraph): Record<string, string[]>
|
getPlans(taskIds: Array<string>, taskGraph: TaskGraph): Record<string, string[]>
|
||||||
|
|||||||
@ -362,6 +362,7 @@ if (!nativeBinding) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports.ChildProcess = nativeBinding.ChildProcess
|
module.exports.ChildProcess = nativeBinding.ChildProcess
|
||||||
|
module.exports.FileLock = nativeBinding.FileLock
|
||||||
module.exports.HashPlanner = nativeBinding.HashPlanner
|
module.exports.HashPlanner = nativeBinding.HashPlanner
|
||||||
module.exports.ImportResult = nativeBinding.ImportResult
|
module.exports.ImportResult = nativeBinding.ImportResult
|
||||||
module.exports.NxCache = nativeBinding.NxCache
|
module.exports.NxCache = nativeBinding.NxCache
|
||||||
|
|||||||
@ -85,15 +85,18 @@ function __napi_rs_initialize_modules(__napiInstance) {
|
|||||||
__napiInstance.exports['__napi_register__ExternalDependenciesInput_struct_36']?.()
|
__napiInstance.exports['__napi_register__ExternalDependenciesInput_struct_36']?.()
|
||||||
__napiInstance.exports['__napi_register__DepsOutputsInput_struct_37']?.()
|
__napiInstance.exports['__napi_register__DepsOutputsInput_struct_37']?.()
|
||||||
__napiInstance.exports['__napi_register__NxJson_struct_38']?.()
|
__napiInstance.exports['__napi_register__NxJson_struct_38']?.()
|
||||||
__napiInstance.exports['__napi_register__WorkspaceContext_struct_39']?.()
|
__napiInstance.exports['__napi_register__FileLock_struct_39']?.()
|
||||||
__napiInstance.exports['__napi_register__WorkspaceContext_impl_48']?.()
|
__napiInstance.exports['__napi_register__FileLock_impl_41']?.()
|
||||||
__napiInstance.exports['__napi_register__WorkspaceErrors_49']?.()
|
__napiInstance.exports['__napi_register__WorkspaceContext_struct_42']?.()
|
||||||
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_50']?.()
|
__napiInstance.exports['__napi_register__WorkspaceContext_impl_51']?.()
|
||||||
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_51']?.()
|
__napiInstance.exports['__napi_register__WorkspaceErrors_52']?.()
|
||||||
__napiInstance.exports['__napi_register__UpdatedWorkspaceFiles_struct_52']?.()
|
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_53']?.()
|
||||||
__napiInstance.exports['__napi_register__FileMap_struct_53']?.()
|
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_54']?.()
|
||||||
__napiInstance.exports['__napi_register____test_only_transfer_file_map_54']?.()
|
__napiInstance.exports['__napi_register__UpdatedWorkspaceFiles_struct_55']?.()
|
||||||
|
__napiInstance.exports['__napi_register__FileMap_struct_56']?.()
|
||||||
|
__napiInstance.exports['__napi_register____test_only_transfer_file_map_57']?.()
|
||||||
}
|
}
|
||||||
|
export const FileLock = __napiModule.exports.FileLock
|
||||||
export const HashPlanner = __napiModule.exports.HashPlanner
|
export const HashPlanner = __napiModule.exports.HashPlanner
|
||||||
export const ImportResult = __napiModule.exports.ImportResult
|
export const ImportResult = __napiModule.exports.ImportResult
|
||||||
export const TaskHasher = __napiModule.exports.TaskHasher
|
export const TaskHasher = __napiModule.exports.TaskHasher
|
||||||
|
|||||||
@ -116,15 +116,18 @@ function __napi_rs_initialize_modules(__napiInstance) {
|
|||||||
__napiInstance.exports['__napi_register__ExternalDependenciesInput_struct_36']?.()
|
__napiInstance.exports['__napi_register__ExternalDependenciesInput_struct_36']?.()
|
||||||
__napiInstance.exports['__napi_register__DepsOutputsInput_struct_37']?.()
|
__napiInstance.exports['__napi_register__DepsOutputsInput_struct_37']?.()
|
||||||
__napiInstance.exports['__napi_register__NxJson_struct_38']?.()
|
__napiInstance.exports['__napi_register__NxJson_struct_38']?.()
|
||||||
__napiInstance.exports['__napi_register__WorkspaceContext_struct_39']?.()
|
__napiInstance.exports['__napi_register__FileLock_struct_39']?.()
|
||||||
__napiInstance.exports['__napi_register__WorkspaceContext_impl_48']?.()
|
__napiInstance.exports['__napi_register__FileLock_impl_41']?.()
|
||||||
__napiInstance.exports['__napi_register__WorkspaceErrors_49']?.()
|
__napiInstance.exports['__napi_register__WorkspaceContext_struct_42']?.()
|
||||||
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_50']?.()
|
__napiInstance.exports['__napi_register__WorkspaceContext_impl_51']?.()
|
||||||
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_51']?.()
|
__napiInstance.exports['__napi_register__WorkspaceErrors_52']?.()
|
||||||
__napiInstance.exports['__napi_register__UpdatedWorkspaceFiles_struct_52']?.()
|
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_53']?.()
|
||||||
__napiInstance.exports['__napi_register__FileMap_struct_53']?.()
|
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_54']?.()
|
||||||
__napiInstance.exports['__napi_register____test_only_transfer_file_map_54']?.()
|
__napiInstance.exports['__napi_register__UpdatedWorkspaceFiles_struct_55']?.()
|
||||||
|
__napiInstance.exports['__napi_register__FileMap_struct_56']?.()
|
||||||
|
__napiInstance.exports['__napi_register____test_only_transfer_file_map_57']?.()
|
||||||
}
|
}
|
||||||
|
module.exports.FileLock = __napiModule.exports.FileLock
|
||||||
module.exports.HashPlanner = __napiModule.exports.HashPlanner
|
module.exports.HashPlanner = __napiModule.exports.HashPlanner
|
||||||
module.exports.ImportResult = __napiModule.exports.ImportResult
|
module.exports.ImportResult = __napiModule.exports.ImportResult
|
||||||
module.exports.TaskHasher = __napiModule.exports.TaskHasher
|
module.exports.TaskHasher = __napiModule.exports.TaskHasher
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
const { FileLock } = require('../../native-bindings.js');
|
||||||
|
const ora = require('ora');
|
||||||
|
const tmp = require('os').tmpdir();
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const lock = new FileLock(
|
||||||
|
require('path').join(tmp, 'nx-unit-tests', 'file-lock-fixture')
|
||||||
|
);
|
||||||
|
if (lock.locked) {
|
||||||
|
const s = ora('Waiting for lock').start();
|
||||||
|
await lock.wait();
|
||||||
|
s.stop();
|
||||||
|
console.log('waited for lock');
|
||||||
|
} else {
|
||||||
|
await lock.lock();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
console.log('ran with lock');
|
||||||
|
await lock.unlock();
|
||||||
|
}
|
||||||
|
})();
|
||||||
44
packages/nx/src/native/tests/file-lock.spec.ts
Normal file
44
packages/nx/src/native/tests/file-lock.spec.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { fork } from 'child_process';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
describe('file-lock', () => {
|
||||||
|
it('should block the second call until the first one is done', async () => {
|
||||||
|
let combinedOutputs = [];
|
||||||
|
let a = fork(join(__dirname, './__fixtures__/file-lock.fixture.js'), {
|
||||||
|
env: {
|
||||||
|
LABEL: 'a',
|
||||||
|
NX_NATIVE_LOGGING: 'trace',
|
||||||
|
},
|
||||||
|
stdio: 'pipe',
|
||||||
|
execArgv: ['--require', 'ts-node/register'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gives a bit of time to make the outputs of the tests more predictable...
|
||||||
|
// if both start at the same time, its hard to guarantee that a will get the lock before b.
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
|
||||||
|
let b = fork(join(__dirname, './__fixtures__/file-lock.fixture.js'), {
|
||||||
|
env: {
|
||||||
|
LABEL: 'b',
|
||||||
|
NX_NATIVE_LOGGING: 'trace',
|
||||||
|
},
|
||||||
|
stdio: 'pipe',
|
||||||
|
execArgv: ['--require', 'ts-node/register'],
|
||||||
|
});
|
||||||
|
|
||||||
|
a.stdout.on('data', (data) => {
|
||||||
|
combinedOutputs.push('A: ' + data.toString().trim());
|
||||||
|
});
|
||||||
|
b.stdout.on('data', (data) => {
|
||||||
|
combinedOutputs.push('B: ' + data.toString().trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
a.stderr.pipe(process.stderr);
|
||||||
|
b.stderr.pipe(process.stderr);
|
||||||
|
|
||||||
|
await Promise.all([a, b].map((p) => new Promise((r) => p.once('exit', r))));
|
||||||
|
|
||||||
|
expect(combinedOutputs).toContain('A: ran with lock');
|
||||||
|
expect(combinedOutputs).toContain('B: waited for lock');
|
||||||
|
});
|
||||||
|
});
|
||||||
156
packages/nx/src/native/utils/file_lock.rs
Normal file
156
packages/nx/src/native/utils/file_lock.rs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
use napi::bindgen_prelude::*;
|
||||||
|
use std::{
|
||||||
|
fs::{self, OpenOptions},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use fs4::fs_std::FileExt;
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub struct FileLock {
|
||||||
|
#[napi]
|
||||||
|
pub locked: bool,
|
||||||
|
file: fs::File,
|
||||||
|
lock_file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// const lock = new FileLock('lockfile.lock');
|
||||||
|
/// if (lock.locked) {
|
||||||
|
/// lock.wait()
|
||||||
|
/// readFromCache()
|
||||||
|
/// } else {
|
||||||
|
/// lock.lock()
|
||||||
|
/// ... do some work
|
||||||
|
/// writeToCache()
|
||||||
|
/// lock.unlock()
|
||||||
|
/// }
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl FileLock {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(lock_file_path: String) -> anyhow::Result<Self> {
|
||||||
|
// Creates the directory where the lock file will be stored
|
||||||
|
fs::create_dir_all(Path::new(&lock_file_path).parent().unwrap())?;
|
||||||
|
|
||||||
|
// Opens the lock file
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(&lock_file_path)?;
|
||||||
|
|
||||||
|
trace!("Locking file {}", lock_file_path);
|
||||||
|
|
||||||
|
// Check if the file is locked
|
||||||
|
let file_lock: std::result::Result<(), std::io::Error> = file.try_lock_exclusive();
|
||||||
|
|
||||||
|
if file_lock.is_ok() {
|
||||||
|
// Checking if the file is locked, locks it, so unlock it.
|
||||||
|
fs4::fs_std::FileExt::unlock(&file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
file: file,
|
||||||
|
locked: file_lock.is_err(),
|
||||||
|
lock_file_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn unlock(&mut self) -> Result<()> {
|
||||||
|
fs4::fs_std::FileExt::unlock(&self.file)?;
|
||||||
|
self.locked = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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.
|
||||||
|
fs4::fs_std::FileExt::unlock(&self.file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let lock_file_path = self.lock_file_path.clone();
|
||||||
|
self.locked = false;
|
||||||
|
env.spawn_future(async move {
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(&lock_file_path)?;
|
||||||
|
fs4::fs_std::FileExt::lock_shared(&file)?;
|
||||||
|
fs4::fs_std::FileExt::unlock(&file)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
env.spawn_future(async move { Ok(()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn lock(&mut self) -> napi::Result<()> {
|
||||||
|
self.file.lock_exclusive()?;
|
||||||
|
self.locked = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl FileLock {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(lock_file_path: String) -> anyhow::Result<Self> {
|
||||||
|
anyhow::bail!("FileLock is not supported on WASM")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fix the tests
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use assert_fs::prelude::*;
|
||||||
|
use assert_fs::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_lock() {
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
let lock_file = tmp_dir.child("test_lock_file");
|
||||||
|
let lock_file_path = lock_file.path().to_path_buf();
|
||||||
|
let lock_file_path_str = lock_file_path.into_os_string().into_string().unwrap();
|
||||||
|
let mut file_lock = FileLock::new(lock_file_path_str).unwrap();
|
||||||
|
assert_eq!(file_lock.locked, false);
|
||||||
|
let _ = file_lock.lock();
|
||||||
|
assert_eq!(file_lock.locked, true);
|
||||||
|
assert!(lock_file.exists());
|
||||||
|
let _ = file_lock.unlock();
|
||||||
|
assert_eq!(file_lock.locked, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_drop() {
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
let lock_file = tmp_dir.child("test_lock_file");
|
||||||
|
let lock_file_path = lock_file.path().to_path_buf();
|
||||||
|
let lock_file_path_str = lock_file_path.into_os_string().into_string().unwrap();
|
||||||
|
{
|
||||||
|
let mut file_lock = FileLock::new(lock_file_path_str.clone()).unwrap();
|
||||||
|
let _ = file_lock.lock();
|
||||||
|
}
|
||||||
|
let file_lock = FileLock::new(lock_file_path_str.clone());
|
||||||
|
assert_eq!(file_lock.unwrap().locked, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,5 +11,6 @@ pub use normalize_trait::Normalize;
|
|||||||
#[cfg_attr(target_arch = "wasm32", path = "atomics/wasm.rs")]
|
#[cfg_attr(target_arch = "wasm32", path = "atomics/wasm.rs")]
|
||||||
pub mod atomics;
|
pub mod atomics;
|
||||||
pub mod ci;
|
pub mod ci;
|
||||||
|
pub mod file_lock;
|
||||||
|
|
||||||
pub use atomics::*;
|
pub use atomics::*;
|
||||||
|
|||||||
@ -6,22 +6,31 @@ import { ProjectConfiguration } from '../config/workspace-json-project-json';
|
|||||||
import { ProjectGraph } from '../config/project-graph';
|
import { ProjectGraph } from '../config/project-graph';
|
||||||
import { CreateNodesFunctionV2 } from './plugins/public-api';
|
import { CreateNodesFunctionV2 } from './plugins/public-api';
|
||||||
|
|
||||||
|
export type ProjectGraphErrorTypes =
|
||||||
|
| AggregateCreateNodesError
|
||||||
|
| MergeNodesError
|
||||||
|
| CreateMetadataError
|
||||||
|
| ProjectsWithNoNameError
|
||||||
|
| MultipleProjectsWithSameNameError
|
||||||
|
| ProcessDependenciesError
|
||||||
|
| WorkspaceValidityError;
|
||||||
|
|
||||||
|
export class StaleProjectGraphCacheError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
'The project graph cache was stale. Ensure that it has been recently created before using `readCachedProjectGraph`.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ProjectGraphError extends Error {
|
export class ProjectGraphError extends Error {
|
||||||
readonly #partialProjectGraph: ProjectGraph;
|
readonly #partialProjectGraph: ProjectGraph;
|
||||||
readonly #partialSourceMaps: ConfigurationSourceMaps;
|
readonly #partialSourceMaps: ConfigurationSourceMaps;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly errors: Array<
|
private readonly errors: Array<ProjectGraphErrorTypes>,
|
||||||
| AggregateCreateNodesError
|
|
||||||
| MergeNodesError
|
|
||||||
| ProjectsWithNoNameError
|
|
||||||
| MultipleProjectsWithSameNameError
|
|
||||||
| ProcessDependenciesError
|
|
||||||
| CreateMetadataError
|
|
||||||
| WorkspaceValidityError
|
|
||||||
>,
|
|
||||||
partialProjectGraph: ProjectGraph,
|
partialProjectGraph: ProjectGraph,
|
||||||
partialSourceMaps: ConfigurationSourceMaps
|
partialSourceMaps: ConfigurationSourceMaps | null
|
||||||
) {
|
) {
|
||||||
const messageFragments = ['Failed to process project graph.'];
|
const messageFragments = ['Failed to process project graph.'];
|
||||||
const mergeNodesErrors = [];
|
const mergeNodesErrors = [];
|
||||||
|
|||||||
@ -18,6 +18,12 @@ import {
|
|||||||
} from '../utils/fileutils';
|
} from '../utils/fileutils';
|
||||||
import { PackageJson } from '../utils/package-json';
|
import { PackageJson } from '../utils/package-json';
|
||||||
import { nxVersion } from '../utils/versions';
|
import { nxVersion } from '../utils/versions';
|
||||||
|
import { ConfigurationSourceMaps } from './utils/project-configuration-utils';
|
||||||
|
import {
|
||||||
|
ProjectGraphError,
|
||||||
|
ProjectGraphErrorTypes,
|
||||||
|
StaleProjectGraphCacheError,
|
||||||
|
} from './error-types';
|
||||||
|
|
||||||
export interface FileMapCache {
|
export interface FileMapCache {
|
||||||
version: string;
|
version: string;
|
||||||
@ -34,6 +40,8 @@ export const nxProjectGraph = join(
|
|||||||
);
|
);
|
||||||
export const nxFileMap = join(workspaceDataDirectory, 'file-map.json');
|
export const nxFileMap = join(workspaceDataDirectory, 'file-map.json');
|
||||||
|
|
||||||
|
export const nxSourceMaps = join(workspaceDataDirectory, 'source-maps.json');
|
||||||
|
|
||||||
export function ensureCacheDirectory(): void {
|
export function ensureCacheDirectory(): void {
|
||||||
try {
|
try {
|
||||||
if (!existsSync(workspaceDataDirectory)) {
|
if (!existsSync(workspaceDataDirectory)) {
|
||||||
@ -77,28 +85,94 @@ export function readFileMapCache(): null | FileMapCache {
|
|||||||
return data ?? null;
|
return data ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readProjectGraphCache(): null | ProjectGraph {
|
export function readProjectGraphCache(
|
||||||
|
minimumComputedAt?: number
|
||||||
|
): null | ProjectGraph {
|
||||||
performance.mark('read project-graph:start');
|
performance.mark('read project-graph:start');
|
||||||
ensureCacheDirectory();
|
ensureCacheDirectory();
|
||||||
|
|
||||||
let data = null;
|
|
||||||
try {
|
try {
|
||||||
if (fileExists(nxProjectGraph)) {
|
if (fileExists(nxProjectGraph)) {
|
||||||
data = readJsonFile(nxProjectGraph);
|
const {
|
||||||
|
computedAt,
|
||||||
|
errors,
|
||||||
|
...projectGraphCache
|
||||||
|
}: ProjectGraph & {
|
||||||
|
errors?: Error[];
|
||||||
|
computedAt?: number;
|
||||||
|
} = readJsonFile(nxProjectGraph);
|
||||||
|
|
||||||
|
if (
|
||||||
|
minimumComputedAt &&
|
||||||
|
(!computedAt || computedAt < minimumComputedAt)
|
||||||
|
) {
|
||||||
|
throw new StaleProjectGraphCacheError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
if (!minimumComputedAt) {
|
||||||
|
// If you didn't pass minimum computed at, we do not know if
|
||||||
|
// the errors on the cached graph would be relevant to what you
|
||||||
|
// are running. Prior to adding error handling here, the graph
|
||||||
|
// would not have been written to the cache. As such, this matches
|
||||||
|
// existing behavior of the public API.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new ProjectGraphError(
|
||||||
|
errors,
|
||||||
|
projectGraphCache,
|
||||||
|
readSourceMapsCache()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectGraphCache;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof StaleProjectGraphCacheError ||
|
||||||
|
error instanceof ProjectGraphError
|
||||||
|
) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`Error reading '${nxProjectGraph}'. Continue the process without the cache.`
|
`Error reading '${nxProjectGraph}'. Continue the process without the cache.`
|
||||||
);
|
);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
return null;
|
||||||
|
} finally {
|
||||||
performance.mark('read project-graph:end');
|
performance.mark('read project-graph:end');
|
||||||
performance.measure(
|
performance.measure(
|
||||||
'read cache',
|
'read cache',
|
||||||
'read project-graph:start',
|
'read project-graph:start',
|
||||||
'read project-graph:end'
|
'read project-graph:end'
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return data ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +197,9 @@ export function createProjectFileMapCache(
|
|||||||
|
|
||||||
export function writeCache(
|
export function writeCache(
|
||||||
cache: FileMapCache,
|
cache: FileMapCache,
|
||||||
projectGraph: ProjectGraph
|
projectGraph: ProjectGraph,
|
||||||
|
sourceMaps: ConfigurationSourceMaps,
|
||||||
|
errors: ProjectGraphErrorTypes[]
|
||||||
): void {
|
): void {
|
||||||
performance.mark('write cache:start');
|
performance.mark('write cache:start');
|
||||||
let retry = 1;
|
let retry = 1;
|
||||||
@ -137,13 +213,21 @@ export function writeCache(
|
|||||||
const unique = (Math.random().toString(16) + '0000000').slice(2, 10);
|
const unique = (Math.random().toString(16) + '0000000').slice(2, 10);
|
||||||
const tmpProjectGraphPath = `${nxProjectGraph}~${unique}`;
|
const tmpProjectGraphPath = `${nxProjectGraph}~${unique}`;
|
||||||
const tmpFileMapPath = `${nxFileMap}~${unique}`;
|
const tmpFileMapPath = `${nxFileMap}~${unique}`;
|
||||||
|
const tmpSourceMapPath = `${nxSourceMaps}~${unique}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
writeJsonFile(tmpProjectGraphPath, projectGraph);
|
writeJsonFile(tmpProjectGraphPath, {
|
||||||
|
...projectGraph,
|
||||||
|
errors,
|
||||||
|
computedAt: Date.now(),
|
||||||
|
});
|
||||||
renameSync(tmpProjectGraphPath, nxProjectGraph);
|
renameSync(tmpProjectGraphPath, nxProjectGraph);
|
||||||
|
|
||||||
writeJsonFile(tmpFileMapPath, cache);
|
writeJsonFile(tmpFileMapPath, cache);
|
||||||
renameSync(tmpFileMapPath, nxFileMap);
|
renameSync(tmpFileMapPath, nxFileMap);
|
||||||
|
|
||||||
|
writeJsonFile(tmpSourceMapPath, sourceMaps);
|
||||||
|
renameSync(tmpSourceMapPath, nxSourceMaps);
|
||||||
done = true;
|
done = true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
|
|||||||
@ -21,10 +21,12 @@ import {
|
|||||||
isAggregateProjectGraphError,
|
isAggregateProjectGraphError,
|
||||||
ProjectConfigurationsError,
|
ProjectConfigurationsError,
|
||||||
ProjectGraphError,
|
ProjectGraphError,
|
||||||
|
StaleProjectGraphCacheError,
|
||||||
} from './error-types';
|
} from './error-types';
|
||||||
import {
|
import {
|
||||||
readFileMapCache,
|
readFileMapCache,
|
||||||
readProjectGraphCache,
|
readProjectGraphCache,
|
||||||
|
readSourceMapsCache,
|
||||||
writeCache,
|
writeCache,
|
||||||
} from './nx-deps-cache';
|
} from './nx-deps-cache';
|
||||||
import { ConfigurationResult } from './utils/project-configuration-utils';
|
import { ConfigurationResult } from './utils/project-configuration-utils';
|
||||||
@ -34,13 +36,21 @@ import {
|
|||||||
} from './utils/retrieve-workspace-files';
|
} from './utils/retrieve-workspace-files';
|
||||||
import { getPlugins } from './plugins/get-plugins';
|
import { getPlugins } from './plugins/get-plugins';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { FileLock, IS_WASM } from '../native';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { workspaceDataDirectory } from '../utils/cache-directory';
|
||||||
|
import { DelayedSpinner } from '../utils/delayed-spinner';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronously reads the latest cached copy of the workspace's ProjectGraph.
|
* Synchronously reads the latest cached copy of the workspace's ProjectGraph.
|
||||||
|
*
|
||||||
|
* @param {number} [minimumComputedAt] - The minimum timestamp that the cached ProjectGraph must have been computed at.
|
||||||
* @throws {Error} if there is no cached ProjectGraph to read from
|
* @throws {Error} if there is no cached ProjectGraph to read from
|
||||||
*/
|
*/
|
||||||
export function readCachedProjectGraph(): ProjectGraph {
|
export function readCachedProjectGraph(
|
||||||
const projectGraphCache: ProjectGraph = readProjectGraphCache();
|
minimumComputedAt?: number
|
||||||
|
): ProjectGraph {
|
||||||
|
const projectGraphCache = readProjectGraphCache(minimumComputedAt);
|
||||||
if (!projectGraphCache) {
|
if (!projectGraphCache) {
|
||||||
const angularSpecificError = fileExists(`${workspaceRoot}/angular.json`)
|
const angularSpecificError = fileExists(`${workspaceRoot}/angular.json`)
|
||||||
? stripIndents`
|
? stripIndents`
|
||||||
@ -163,12 +173,13 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
|
|||||||
...(projectGraphError?.errors ?? []),
|
...(projectGraphError?.errors ?? []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (cacheEnabled) {
|
||||||
|
writeCache(projectFileMapCache, projectGraph, sourceMaps, errors);
|
||||||
|
}
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
throw new ProjectGraphError(errors, projectGraph, sourceMaps);
|
throw new ProjectGraphError(errors, projectGraph, sourceMaps);
|
||||||
} else {
|
} else {
|
||||||
if (cacheEnabled) {
|
|
||||||
writeCache(projectFileMapCache, projectGraph);
|
|
||||||
}
|
|
||||||
return { projectGraph, sourceMaps };
|
return { projectGraph, sourceMaps };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,6 +214,20 @@ export function handleProjectGraphError(opts: { exitOnError: boolean }, e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readCachedGraphAndHydrateFileMap(minimumComputedAt?: number) {
|
||||||
|
const graph = readCachedProjectGraph(minimumComputedAt);
|
||||||
|
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.
|
* Computes and returns a ProjectGraph.
|
||||||
*
|
*
|
||||||
@ -232,19 +257,13 @@ export async function createProjectGraphAsync(
|
|||||||
): Promise<ProjectGraph> {
|
): Promise<ProjectGraph> {
|
||||||
if (process.env.NX_FORCE_REUSE_CACHED_GRAPH === 'true') {
|
if (process.env.NX_FORCE_REUSE_CACHED_GRAPH === 'true') {
|
||||||
try {
|
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
|
// If no cached graph is found, we will fall through to the normal flow
|
||||||
|
const graph = await readCachedGraphAndHydrateFileMap();
|
||||||
|
return graph;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e instanceof ProjectGraphError) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
logger.verbose('Unable to use cached project graph', e);
|
logger.verbose('Unable to use cached project graph', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,6 +283,57 @@ export async function createProjectGraphAndSourceMapsAsync(
|
|||||||
performance.mark('create-project-graph-async:start');
|
performance.mark('create-project-graph-async:start');
|
||||||
|
|
||||||
if (!daemonClient.enabled()) {
|
if (!daemonClient.enabled()) {
|
||||||
|
const lock = !IS_WASM
|
||||||
|
? new FileLock(join(workspaceDataDirectory, 'project-graph.lock'))
|
||||||
|
: null;
|
||||||
|
let locked = lock?.locked;
|
||||||
|
while (locked) {
|
||||||
|
logger.verbose(
|
||||||
|
'Waiting for graph construction in another process to complete'
|
||||||
|
);
|
||||||
|
const spinner = new DelayedSpinner(
|
||||||
|
'Waiting for graph construction in another process to complete'
|
||||||
|
);
|
||||||
|
const start = Date.now();
|
||||||
|
await lock.wait();
|
||||||
|
spinner.cleanup();
|
||||||
|
|
||||||
|
// Note: This will currently throw if any of the caches are missing...
|
||||||
|
// It would be nice if one of the processes that was waiting for the lock
|
||||||
|
// could pick up the slack and build the graph if it's missing, but
|
||||||
|
// we wouldn't want either of the below to happen:
|
||||||
|
// - All of the waiting processes to build the graph
|
||||||
|
// - Even one of the processes building the graph on a legitimate error
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensuring that computedAt was after this process started
|
||||||
|
// waiting for the graph to complete, means that the graph
|
||||||
|
// was computed by the process was already working.
|
||||||
|
const graph = await readCachedGraphAndHydrateFileMap(start);
|
||||||
|
|
||||||
|
const sourceMaps = readSourceMapsCache();
|
||||||
|
if (!sourceMaps) {
|
||||||
|
throw new Error(
|
||||||
|
'The project graph was computed in another process, but the source maps are missing.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectGraph: graph,
|
||||||
|
sourceMaps,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// If the error is that the cached graph is stale after unlock,
|
||||||
|
// the process that was working on the graph must have been canceled,
|
||||||
|
// so we will fall through to the normal flow to ensure
|
||||||
|
// its created by one of the processes that was waiting
|
||||||
|
if (!(e instanceof StaleProjectGraphCacheError)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
locked = lock.check();
|
||||||
|
}
|
||||||
|
lock?.lock();
|
||||||
try {
|
try {
|
||||||
const res = await buildProjectGraphAndSourceMapsWithoutDaemon();
|
const res = await buildProjectGraphAndSourceMapsWithoutDaemon();
|
||||||
performance.measure(
|
performance.measure(
|
||||||
@ -290,6 +360,8 @@ export async function createProjectGraphAndSourceMapsAsync(
|
|||||||
return res;
|
return res;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleProjectGraphError(opts, e);
|
handleProjectGraphError(opts, e);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export function projectHasTargetAndConfiguration(
|
|||||||
|
|
||||||
export function getSourceDirOfDependentProjects(
|
export function getSourceDirOfDependentProjects(
|
||||||
projectName: string,
|
projectName: string,
|
||||||
projectGraph = readCachedProjectGraph()
|
projectGraph: ProjectGraph = readCachedProjectGraph()
|
||||||
): [projectDirs: string[], warnings: string[]] {
|
): [projectDirs: string[], warnings: string[]] {
|
||||||
if (!projectGraph.nodes[projectName]) {
|
if (!projectGraph.nodes[projectName]) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -58,7 +58,7 @@ export function getSourceDirOfDependentProjects(
|
|||||||
*/
|
*/
|
||||||
export function findAllProjectNodeDependencies(
|
export function findAllProjectNodeDependencies(
|
||||||
parentNodeName: string,
|
parentNodeName: string,
|
||||||
projectGraph = readCachedProjectGraph(),
|
projectGraph: ProjectGraph = readCachedProjectGraph(),
|
||||||
includeExternalDependencies = false
|
includeExternalDependencies = false
|
||||||
): string[] {
|
): string[] {
|
||||||
const dependencyNodeNames = new Set<string>();
|
const dependencyNodeNames = new Set<string>();
|
||||||
|
|||||||
@ -11,6 +11,8 @@
|
|||||||
"**/*.spec.tsx",
|
"**/*.spec.tsx",
|
||||||
"**/*.spec.js",
|
"**/*.spec.js",
|
||||||
"**/*.spec.jsx",
|
"**/*.spec.jsx",
|
||||||
|
"**/*.fixture.js",
|
||||||
|
"**/*.fixture.ts",
|
||||||
"**/*.d.ts",
|
"**/*.d.ts",
|
||||||
"./src/internal-testing-utils/**/*.ts",
|
"./src/internal-testing-utils/**/*.ts",
|
||||||
"jest.config.ts"
|
"jest.config.ts"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user