Skip to content

Commit

Permalink
feat(core): lock graph creation when running in another process (#29408)
Browse files Browse the repository at this point in the history
## 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);

```
  • Loading branch information
AgentEnder authored Jan 28, 2025
1 parent cbbe14b commit 5721ea3
Show file tree
Hide file tree
Showing 23 changed files with 629 additions and 64 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/generated/devkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ It only uses language primitives and immutable objects
### Classes

- [AggregateCreateNodesError](../../devkit/documents/AggregateCreateNodesError)
- [StaleProjectGraphCacheError](../../devkit/documents/StaleProjectGraphCacheError)

### Interfaces

Expand Down
144 changes: 144 additions & 0 deletions docs/generated/devkit/StaleProjectGraphCacheError.md
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion docs/generated/devkit/readCachedProjectGraph.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# Function: readCachedProjectGraph

**readCachedProjectGraph**(): [`ProjectGraph`](../../devkit/documents/ProjectGraph)
**readCachedProjectGraph**(`minimumComputedAt?`): [`ProjectGraph`](../../devkit/documents/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

[`ProjectGraph`](../../devkit/documents/ProjectGraph)
Expand Down
1 change: 1 addition & 0 deletions docs/generated/packages/devkit/documents/nx_devkit.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ It only uses language primitives and immutable objects
### Classes

- [AggregateCreateNodesError](../../devkit/documents/AggregateCreateNodesError)
- [StaleProjectGraphCacheError](../../devkit/documents/StaleProjectGraphCacheError)

### Interfaces

Expand Down
2 changes: 1 addition & 1 deletion packages/devkit/src/executors/parse-target-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function parseTargetString(
targetString: string,
projectGraphOrCtx?: ProjectGraph | ExecutorContext
): Target {
let projectGraph =
let projectGraph: ProjectGraph =
projectGraphOrCtx && 'projectGraph' in projectGraphOrCtx
? projectGraphOrCtx.projectGraph
: (projectGraphOrCtx as ProjectGraph);
Expand Down
2 changes: 2 additions & 0 deletions packages/nx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ assert_fs = "1.0.10"
# This is only used for unit tests
swc_ecma_dep_graph = "0.109.1"
tempfile = "3.13.0"
# We only explicitly use tokio for async tests
tokio = "1.38.0"
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,7 +321,6 @@ async function processFilesAndCreateAndSerializeProjectGraph(
serializedSourceMaps: null,
};
} else {
writeCache(g.projectFileMapCache, g.projectGraph);
return g;
}
} catch (err) {
Expand Down
5 changes: 4 additions & 1 deletion packages/nx/src/devkit-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ export type {
PostTasksExecutionContext,
} 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';

Expand Down
13 changes: 6 additions & 7 deletions packages/nx/src/native/db/initialize.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::native::db::connection::NxDbConnection;
use fs4::fs_std::FileExt;
use rusqlite::{Connection, OpenFlags};
use std::fs::{remove_file, File};
use std::path::{Path, PathBuf};
Expand All @@ -12,9 +11,7 @@ pub(super) struct LockFile {

pub(super) fn unlock_file(lock_file: &LockFile) {
if lock_file.path.exists() {
lock_file
.file
.unlock()
fs4::fs_std::FileExt::unlock(&lock_file.file)
.and_then(|_| remove_file(&lock_file.path))
.ok();
}
Expand All @@ -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))?;

trace!("Getting lock on db lock file");
lock_file
.lock_exclusive()
fs4::fs_std::FileExt::lock_exclusive(&lock_file)
.inspect(|_| trace!("Got lock on db lock file"))
.map_err(|e| anyhow::anyhow!("Unable to lock the db lock file: {:?}", e))?;
Ok(LockFile {
Expand Down Expand Up @@ -77,7 +73,10 @@ pub(super) fn initialize_db(nx_version: String, db_path: &Path) -> anyhow::Resul
Ok(c)
}
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");
remove_file(db_path)?;

Expand Down
9 changes: 9 additions & 0 deletions packages/nx/src/native/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ export declare class ChildProcess {
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 {
constructor(nxJson: NxJson, projectGraph: ExternalObject<ProjectGraph>)
getPlans(taskIds: Array<string>, taskGraph: TaskGraph): Record<string, string[]>
Expand Down
1 change: 1 addition & 0 deletions packages/nx/src/native/native-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ if (!nativeBinding) {
}

module.exports.ChildProcess = nativeBinding.ChildProcess
module.exports.FileLock = nativeBinding.FileLock
module.exports.HashPlanner = nativeBinding.HashPlanner
module.exports.ImportResult = nativeBinding.ImportResult
module.exports.NxCache = nativeBinding.NxCache
Expand Down
19 changes: 11 additions & 8 deletions packages/nx/src/native/nx.wasi-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,18 @@ function __napi_rs_initialize_modules(__napiInstance) {
__napiInstance.exports['__napi_register__ExternalDependenciesInput_struct_36']?.()
__napiInstance.exports['__napi_register__DepsOutputsInput_struct_37']?.()
__napiInstance.exports['__napi_register__NxJson_struct_38']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_struct_39']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_impl_48']?.()
__napiInstance.exports['__napi_register__WorkspaceErrors_49']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_50']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_51']?.()
__napiInstance.exports['__napi_register__UpdatedWorkspaceFiles_struct_52']?.()
__napiInstance.exports['__napi_register__FileMap_struct_53']?.()
__napiInstance.exports['__napi_register____test_only_transfer_file_map_54']?.()
__napiInstance.exports['__napi_register__FileLock_struct_39']?.()
__napiInstance.exports['__napi_register__FileLock_impl_41']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_struct_42']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_impl_51']?.()
__napiInstance.exports['__napi_register__WorkspaceErrors_52']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_53']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_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 ImportResult = __napiModule.exports.ImportResult
export const TaskHasher = __napiModule.exports.TaskHasher
Expand Down
19 changes: 11 additions & 8 deletions packages/nx/src/native/nx.wasi.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,18 @@ function __napi_rs_initialize_modules(__napiInstance) {
__napiInstance.exports['__napi_register__ExternalDependenciesInput_struct_36']?.()
__napiInstance.exports['__napi_register__DepsOutputsInput_struct_37']?.()
__napiInstance.exports['__napi_register__NxJson_struct_38']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_struct_39']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_impl_48']?.()
__napiInstance.exports['__napi_register__WorkspaceErrors_49']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_50']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_51']?.()
__napiInstance.exports['__napi_register__UpdatedWorkspaceFiles_struct_52']?.()
__napiInstance.exports['__napi_register__FileMap_struct_53']?.()
__napiInstance.exports['__napi_register____test_only_transfer_file_map_54']?.()
__napiInstance.exports['__napi_register__FileLock_struct_39']?.()
__napiInstance.exports['__napi_register__FileLock_impl_41']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_struct_42']?.()
__napiInstance.exports['__napi_register__WorkspaceContext_impl_51']?.()
__napiInstance.exports['__napi_register__WorkspaceErrors_52']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFiles_struct_53']?.()
__napiInstance.exports['__napi_register__NxWorkspaceFilesExternals_struct_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.ImportResult = __napiModule.exports.ImportResult
module.exports.TaskHasher = __napiModule.exports.TaskHasher
Expand Down
20 changes: 20 additions & 0 deletions packages/nx/src/native/tests/__fixtures__/file-lock.fixture.js
Original file line number Diff line number Diff line change
@@ -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();
}
})();
Loading

0 comments on commit 5721ea3

Please sign in to comment.