Skip to content

Commit 8ce0c9b

Browse files
committed
diagnostics_channel: add diagnostics channels for web locks
1 parent 6b5178f commit 8ce0c9b

File tree

3 files changed

+270
-3
lines changed

3 files changed

+270
-3
lines changed

doc/api/diagnostics_channel.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,50 @@ Emitted when a new process is created.
14251425

14261426
Emitted when [`process.execve()`][] is invoked.
14271427

1428+
#### Web Locks
1429+
1430+
> Stability: 1 - Experimental
1431+
1432+
<!-- YAML
1433+
added: REPLACEME
1434+
-->
1435+
1436+
These channels are emitted for each [`locks.request()`][] call. See
1437+
[`worker_threads.locks`][] for details on Web Locks.
1438+
1439+
##### Event: `'locks.request.start'`
1440+
1441+
* `name` {string} The name of the requested lock resource.
1442+
* `mode` {string} The lock mode: `'exclusive'` or `'shared'`.
1443+
1444+
Emitted when a lock request is initiated, before the lock is granted.
1445+
1446+
##### Event: `'locks.request.grant'`
1447+
1448+
* `name` {string} The name of the requested lock resource.
1449+
* `mode` {string} The lock mode: `'exclusive'` or `'shared'`.
1450+
1451+
Emitted when a lock is successfully granted and the callback is about to run.
1452+
1453+
##### Event: `'locks.request.miss'`
1454+
1455+
* `name` {string} The name of the requested lock resource.
1456+
* `mode` {string} The lock mode: `'exclusive'` or `'shared'`.
1457+
1458+
Emitted when `ifAvailable` is `true` and the lock is not immediately available.
1459+
The callback is invoked with `null` instead of a `Lock` object.
1460+
1461+
##### Event: `'locks.request.end'`
1462+
1463+
* `name` {string} The name of the requested lock resource.
1464+
* `mode` {string} The lock mode: `'exclusive'` or `'shared'`.
1465+
* `steal` {boolean} Whether the request uses steal semantics.
1466+
* `ifAvailable` {boolean} Whether the request uses ifAvailable semantics.
1467+
* `error` {Error|undefined} The error thrown by the callback, if any.
1468+
1469+
Emitted when a lock request has finished, whether the callback succeeded,
1470+
threw an error, or the lock was stolen.
1471+
14281472
#### Worker Thread
14291473

14301474
> Stability: 1 - Experimental
@@ -1453,7 +1497,9 @@ Emitted when a new thread is created.
14531497
[`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels
14541498
[`end` event]: #endevent
14551499
[`error` event]: #errorevent
1500+
[`locks.request()`]: worker_threads.md#locksrequestname-options-callback
14561501
[`net.Server.listen()`]: net.md#serverlisten
14571502
[`process.execve()`]: process.md#processexecvefile-args-env
14581503
[`start` event]: #startevent
1504+
[`worker_threads.locks`]: worker_threads.md#worker_threadslocks
14591505
[context loss]: async_context.md#troubleshooting-context-loss

lib/internal/locks.js

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ const {
2929
createEnumConverter,
3030
createDictionaryConverter,
3131
} = require('internal/webidl');
32+
const dc = require('diagnostics_channel');
3233

3334
const locks = internalBinding('locks');
35+
const lockRequestStartChannel = dc.channel('locks.request.start');
36+
const lockRequestGrantChannel = dc.channel('locks.request.grant');
37+
const lockRequestMissChannel = dc.channel('locks.request.miss');
38+
const lockRequestEndChannel = dc.channel('locks.request.end');
3439

3540
const kName = Symbol('kName');
3641
const kMode = Symbol('kMode');
@@ -113,6 +118,30 @@ function convertLockError(error) {
113118
return error;
114119
}
115120

121+
function publishLockRequestStart(name, mode) {
122+
if (lockRequestStartChannel.hasSubscribers) {
123+
lockRequestStartChannel.publish({ name, mode });
124+
}
125+
}
126+
127+
function publishLockRequestGrant(name, mode) {
128+
if (lockRequestGrantChannel.hasSubscribers) {
129+
lockRequestGrantChannel.publish({ name, mode });
130+
}
131+
}
132+
133+
function publishLockRequestMiss(name, mode, ifAvailable) {
134+
if (ifAvailable && lockRequestMissChannel.hasSubscribers) {
135+
lockRequestMissChannel.publish({ name, mode });
136+
}
137+
}
138+
139+
function publishLockRequestEnd(name, mode, ifAvailable, steal, error) {
140+
if (lockRequestEndChannel.hasSubscribers) {
141+
lockRequestEndChannel.publish({ name, mode, ifAvailable, steal, error });
142+
}
143+
}
144+
116145
// https://w3c.github.io/web-locks/#api-lock-manager
117146
class LockManager {
118147
constructor(symbol = undefined) {
@@ -192,6 +221,7 @@ class LockManager {
192221
}
193222

194223
const clientId = `node-${process.pid}-${threadId}`;
224+
publishLockRequestStart(name, mode);
195225

196226
// Handle requests with AbortSignal
197227
if (signal) {
@@ -212,6 +242,8 @@ class LockManager {
212242
return undefined;
213243
}
214244
lockGranted = true;
245+
publishLockRequestGrant(name, mode);
246+
215247
return callback(createLock(lock));
216248
});
217249
};
@@ -228,27 +260,49 @@ class LockManager {
228260

229261
// When released promise settles, clean up listener and resolve main promise
230262
SafePromisePrototypeFinally(
231-
PromisePrototypeThen(released, resolve, (error) => reject(convertLockError(error))),
263+
PromisePrototypeThen(
264+
released,
265+
(result) => {
266+
publishLockRequestEnd(name, mode, ifAvailable, steal, undefined);
267+
resolve(result);
268+
},
269+
(error) => {
270+
const convertedError = convertLockError(error);
271+
publishLockRequestEnd(name, mode, ifAvailable, steal, convertedError);
272+
reject(convertedError);
273+
},
274+
),
232275
() => signal.removeEventListener('abort', abortListener),
233276
);
234277
} catch (error) {
235278
signal.removeEventListener('abort', abortListener);
236-
reject(convertLockError(error));
279+
const convertedError = convertLockError(error);
280+
publishLockRequestEnd(name, mode, ifAvailable, steal, convertedError);
281+
reject(convertedError);
237282
}
238283
});
239284
}
240285

241286
// When ifAvailable: true and lock is not available, C++ passes null to indicate no lock granted
242287
const wrapCallback = (internalLock) => {
288+
if (internalLock === null) {
289+
publishLockRequestMiss(name, mode, ifAvailable);
290+
} else {
291+
publishLockRequestGrant(name, mode);
292+
}
243293
const lock = createLock(internalLock);
244294
return callback(lock);
245295
};
246296

247297
// Standard request without signal
248298
try {
249-
return await locks.request(name, clientId, mode, steal, ifAvailable, wrapCallback);
299+
const result = await locks.request(name, clientId, mode, steal, ifAvailable, wrapCallback);
300+
publishLockRequestEnd(name, mode, ifAvailable, steal, undefined);
301+
302+
return result;
250303
} catch (error) {
251304
const convertedError = convertLockError(error);
305+
publishLockRequestEnd(name, mode, ifAvailable, steal, convertedError);
252306
throw convertedError;
253307
}
254308
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { describe, it } = require('node:test');
5+
const assert = require('node:assert');
6+
const dc = require('node:diagnostics_channel');
7+
8+
function subscribe({ start, grant, miss, end }) {
9+
if (start) dc.subscribe('locks.request.start', start);
10+
if (grant) dc.subscribe('locks.request.grant', grant);
11+
if (miss) dc.subscribe('locks.request.miss', miss);
12+
if (end) dc.subscribe('locks.request.end', end);
13+
14+
return () => {
15+
if (start) dc.unsubscribe('locks.request.start', start);
16+
if (grant) dc.unsubscribe('locks.request.grant', grant);
17+
if (miss) dc.unsubscribe('locks.request.miss', miss);
18+
if (end) dc.unsubscribe('locks.request.end', end);
19+
};
20+
}
21+
22+
describe('Web Locks diagnostics channel', () => {
23+
it('emits start, grant, and end on success', async () => {
24+
let startEvent;
25+
const unsubscribe = subscribe({
26+
start: common.mustCall((e) => startEvent = e),
27+
grant: common.mustCall(),
28+
miss: common.mustNotCall(),
29+
end: common.mustCall(),
30+
});
31+
32+
try {
33+
const result = await navigator.locks.request('normal-lock', async () => 'done');
34+
assert.strictEqual(result, 'done');
35+
assert.strictEqual(startEvent.name, 'normal-lock');
36+
assert.strictEqual(startEvent.mode, 'exclusive');
37+
} finally {
38+
unsubscribe();
39+
}
40+
});
41+
42+
it('emits start, miss, and end when lock is unavailable', async () => {
43+
await navigator.locks.request('ifavailable-true-lock', common.mustCall(async () => {
44+
let startEvent;
45+
const unsubscribe = subscribe({
46+
start: common.mustCall((e) => startEvent = e),
47+
grant: common.mustNotCall(),
48+
miss: common.mustCall(),
49+
end: common.mustCall(),
50+
});
51+
52+
try {
53+
const result = await navigator.locks.request(
54+
'ifavailable-true-lock',
55+
{ ifAvailable: true },
56+
(lock) => lock,
57+
);
58+
59+
assert.strictEqual(result, null);
60+
assert.strictEqual(startEvent.name, 'ifavailable-true-lock');
61+
assert.strictEqual(startEvent.mode, 'exclusive');
62+
} finally {
63+
unsubscribe();
64+
}
65+
}));
66+
});
67+
68+
it('queued lock request emits start, grant, and end without miss', async () => {
69+
// Outer fires first, inner is queued — so events arrive in insertion order
70+
let outerStartEvent, innerStartEvent;
71+
const unsubscribe = subscribe({
72+
start: common.mustCall((e) => (outerStartEvent ? innerStartEvent = e : outerStartEvent = e), 2),
73+
grant: common.mustCall(2),
74+
miss: common.mustNotCall(),
75+
end: common.mustCall(2),
76+
});
77+
78+
try {
79+
let innerDone;
80+
81+
const outerResult = await navigator.locks.request('ifavailable-false-lock', common.mustCall(async () => {
82+
innerDone = navigator.locks.request(
83+
'ifavailable-false-lock',
84+
{ ifAvailable: false },
85+
common.mustCall(async (lock) => {
86+
assert.ok(lock);
87+
return 'inner-done';
88+
}),
89+
);
90+
await new Promise((resolve) => setTimeout(resolve, 20));
91+
return 'outer-done';
92+
}));
93+
94+
assert.strictEqual(outerResult, 'outer-done');
95+
assert.strictEqual(await innerDone, 'inner-done');
96+
97+
assert.strictEqual(outerStartEvent.name, 'ifavailable-false-lock');
98+
assert.strictEqual(outerStartEvent.mode, 'exclusive');
99+
assert.strictEqual(innerStartEvent.name, 'ifavailable-false-lock');
100+
assert.strictEqual(innerStartEvent.mode, 'exclusive');
101+
} finally {
102+
unsubscribe();
103+
}
104+
});
105+
106+
it('reports callback error in end event', async () => {
107+
const expectedError = new Error('Callback error');
108+
let endEvent;
109+
const unsubscribe = subscribe({
110+
start: common.mustCall(),
111+
grant: common.mustCall(),
112+
miss: common.mustNotCall(),
113+
end: common.mustCall((e) => endEvent = e),
114+
});
115+
116+
try {
117+
await assert.rejects(
118+
navigator.locks.request('error-lock', async () => { throw expectedError; }),
119+
(error) => error === expectedError,
120+
);
121+
122+
assert.strictEqual(endEvent.name, 'error-lock');
123+
assert.strictEqual(endEvent.mode, 'exclusive');
124+
assert.strictEqual(endEvent.error, expectedError);
125+
} finally {
126+
unsubscribe();
127+
}
128+
});
129+
130+
it('stolen lock ends original request with AbortError', async () => {
131+
let stolenEndEvent, stealerEndEvent;
132+
const unsubscribe = subscribe({
133+
start: common.mustCall(2),
134+
grant: common.mustCall(2),
135+
miss: common.mustNotCall(),
136+
end: common.mustCall((e) => (e.steal ? stealerEndEvent = e : stolenEndEvent = e), 2),
137+
});
138+
139+
try {
140+
let resolveGranted;
141+
const granted = new Promise((r) => { resolveGranted = r; });
142+
143+
const originalRejected = assert.rejects(
144+
navigator.locks.request('steal-lock', async () => {
145+
resolveGranted();
146+
await new Promise((r) => setTimeout(r, 200));
147+
}),
148+
{ name: 'AbortError' },
149+
);
150+
151+
await granted;
152+
await navigator.locks.request('steal-lock', { steal: true }, async () => {});
153+
await originalRejected;
154+
155+
assert.strictEqual(stolenEndEvent.name, 'steal-lock');
156+
assert.strictEqual(stolenEndEvent.mode, 'exclusive');
157+
assert.strictEqual(stolenEndEvent.steal, false);
158+
assert.strictEqual(stealerEndEvent.name, 'steal-lock');
159+
assert.strictEqual(stealerEndEvent.mode, 'exclusive');
160+
assert.strictEqual(stealerEndEvent.steal, true);
161+
assert.strictEqual(stolenEndEvent.error.name, 'AbortError');
162+
assert.strictEqual(stealerEndEvent.error, undefined);
163+
} finally {
164+
unsubscribe();
165+
}
166+
});
167+
});

0 commit comments

Comments
 (0)