Skip to content

Commit 294a19c

Browse files
committed
vfs: assign a monotonic layer id to each instance
Each VirtualFileSystem now exposes a per-process monotonically increasing `layerId`, assigned at construction. The id is stable across mount/unmount cycles for the lifetime of the instance and surfaces in: - debug() output for register / deregister so the layer stack is visible when NODE_DEBUG=vfs is enabled; - the overlap ERR_INVALID_STATE message, which now names the layer ids of the conflicting mounts. The id is the building block for tagging cache entries with the owning VFS, which a follow-up will use to replace the global loader-cache flush in deregisterVFS with a scoped purge. Refs: #63653 Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 12c47b5 commit 294a19c

3 files changed

Lines changed: 68 additions & 4 deletions

File tree

lib/internal/vfs/file_system.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ const kProvider = Symbol('kProvider');
4343
const kMountPoint = Symbol('kMountPoint');
4444
const kMounted = Symbol('kMounted');
4545
const kPromises = Symbol('kPromises');
46+
const kLayerId = Symbol('kLayerId');
47+
48+
// Per-process monotonically increasing counter that gives each
49+
// VirtualFileSystem instance a unique, stable id. Useful for ordering
50+
// mounted layers in debug output and disambiguating instances in error
51+
// messages.
52+
let nextLayerId = 0;
4653

4754
// Lazy-loaded VFS setup
4855
let registerVFS;
@@ -94,6 +101,17 @@ class VirtualFileSystem {
94101
this[kMountPoint] = null;
95102
this[kMounted] = false;
96103
this[kPromises] = null; // Lazy-initialized
104+
this[kLayerId] = nextLayerId++;
105+
}
106+
107+
/**
108+
* Per-process monotonically increasing identifier assigned to this
109+
* VFS instance at construction. Stable across mount/unmount cycles
110+
* for the lifetime of the instance.
111+
* @returns {number}
112+
*/
113+
get layerId() {
114+
return this[kLayerId];
97115
}
98116

99117
/**

lib/internal/vfs/setup.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ function registerVFS(vfs) {
7171
const newMount = vfs.mountPoint;
7272
if (newMount != null) {
7373
for (let i = 0; i < activeVFSList.length; i++) {
74-
const existingMount = activeVFSList[i].mountPoint;
74+
const existing = activeVFSList[i];
75+
const existingMount = existing.mountPoint;
7576
if (existingMount == null) continue;
7677
// Use path.sep so the trailing-separator guard works on Windows where
7778
// mountPoint values are resolved to drive-letter / backslash paths.
@@ -81,13 +82,15 @@ function registerVFS(vfs) {
8182
StringPrototypeStartsWith(newMount, existingPrefix) ||
8283
StringPrototypeStartsWith(existingMount, newPrefix)) {
8384
throw new ERR_INVALID_STATE(
84-
`VFS mount '${newMount}' overlaps with existing mount '${existingMount}'`,
85+
`VFS mount '${newMount}' (layer ${vfs.layerId}) overlaps with ` +
86+
`existing mount '${existingMount}' (layer ${existing.layerId})`,
8587
);
8688
}
8789
}
8890
}
8991
ArrayPrototypePush(activeVFSList, vfs);
90-
debug('register mount=%s active=%d', newMount, activeVFSList.length);
92+
debug('register layer=%d mount=%s active=%d',
93+
vfs.layerId, newMount, activeVFSList.length);
9194
if (!hooksInstalled) {
9295
installHooks();
9396
}
@@ -97,7 +100,7 @@ function deregisterVFS(vfs) {
97100
const index = ArrayPrototypeIndexOf(activeVFSList, vfs);
98101
if (index === -1) return;
99102
ArrayPrototypeSplice(activeVFSList, index, 1);
100-
debug('deregister active=%d', activeVFSList.length);
103+
debug('deregister layer=%d active=%d', vfs.layerId, activeVFSList.length);
101104
// Loader/path caches are shared across all VFSes and we can't tell
102105
// which entries belonged to the one going away, so flush them on
103106
// every unmount. The cost is bounded; correctness wins.

test/parallel/test-vfs-layer-id.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Flags: --experimental-vfs
2+
'use strict';
3+
4+
// `vfs.layerId` is a per-process monotonically increasing identifier
5+
// assigned at construction. It is stable across mount/unmount cycles
6+
// and surfaces in overlap error messages.
7+
8+
require('../common');
9+
const assert = require('assert');
10+
const vfs = require('node:vfs');
11+
12+
{
13+
const a = vfs.create();
14+
const b = vfs.create();
15+
const c = vfs.create();
16+
assert.strictEqual(typeof a.layerId, 'number');
17+
assert.ok(b.layerId > a.layerId);
18+
assert.ok(c.layerId > b.layerId);
19+
// Stable across mount/unmount.
20+
const idBefore = a.layerId;
21+
a.mount('/mnt-layer-stable');
22+
assert.strictEqual(a.layerId, idBefore);
23+
a.unmount();
24+
assert.strictEqual(a.layerId, idBefore);
25+
}
26+
27+
// layerId appears in the overlap error message so the user can tell
28+
// which instance lost the race.
29+
{
30+
const outer = vfs.create();
31+
const inner = vfs.create();
32+
outer.mount('/mnt-layer-overlap');
33+
assert.throws(
34+
() => inner.mount('/mnt-layer-overlap/sub'),
35+
(err) => {
36+
assert.strictEqual(err.code, 'ERR_INVALID_STATE');
37+
assert.match(err.message, new RegExp(`layer ${inner.layerId}`));
38+
assert.match(err.message, new RegExp(`layer ${outer.layerId}`));
39+
return true;
40+
},
41+
);
42+
outer.unmount();
43+
}

0 commit comments

Comments
 (0)