Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 231521e

Browse filesBrowse files
IlyasShabiaduh95
authored andcommitted
diagnostics_channel: add diagnostics channels for web locks
PR-URL: #62123 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent a9703d1 commit 231521e
Copy full SHA for 231521e

3 files changed

+270-3Lines changed: 270 additions & 3 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎doc/api/diagnostics_channel.md‎

Copy file name to clipboardExpand all lines: doc/api/diagnostics_channel.md
+46Lines changed: 46 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,50 @@ Emitted when [`child_process.spawn()`][] encounters an error.
14471447

14481448
Emitted when [`process.execve()`][] is invoked.
14491449

1450+
#### Web Locks
1451+
1452+
> Stability: 1 - Experimental
1453+
1454+
<!-- YAML
1455+
added: REPLACEME
1456+
-->
1457+
1458+
These channels are emitted for each [`locks.request()`][] call. See
1459+
[`worker_threads.locks`][] for details on Web Locks.
1460+
1461+
##### Event: `'locks.request.start'`
1462+
1463+
* `name` {string} The name of the requested lock resource.
1464+
* `mode` {string} The lock mode: `'exclusive'` or `'shared'`.
1465+
1466+
Emitted when a lock request is initiated, before the lock is granted.
1467+
1468+
##### Event: `'locks.request.grant'`
1469+
1470+
* `name` {string} The name of the requested lock resource.
1471+
* `mode` {string} The lock mode: `'exclusive'` or `'shared'`.
1472+
1473+
Emitted when a lock is successfully granted and the callback is about to run.
1474+
1475+
##### Event: `'locks.request.miss'`
1476+
1477+
* `name` {string} The name of the requested lock resource.
1478+
* `mode` {string} The lock mode: `'exclusive'` or `'shared'`.
1479+
1480+
Emitted when `ifAvailable` is `true` and the lock is not immediately available,
1481+
and the request callback is invoked with `null` instead of a `Lock` object.
1482+
1483+
##### Event: `'locks.request.end'`
1484+
1485+
* `name` {string} The name of the requested lock resource.
1486+
* `mode` {string} The lock mode: `'exclusive'` or `'shared'`.
1487+
* `steal` {boolean} Whether the request uses steal semantics.
1488+
* `ifAvailable` {boolean} Whether the request uses ifAvailable semantics.
1489+
* `error` {Error|undefined} The error thrown by the callback, if any.
1490+
1491+
Emitted when a lock request has finished, whether the callback succeeded,
1492+
threw an error, or the lock was stolen.
1493+
14501494
#### Worker Thread
14511495

14521496
> Stability: 1 - Experimental
@@ -1476,7 +1520,9 @@ Emitted when a new thread is created.
14761520
[`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels
14771521
[`end` event]: #endevent
14781522
[`error` event]: #errorevent
1523+
[`locks.request()`]: worker_threads.md#locksrequestname-options-callback
14791524
[`net.Server.listen()`]: net.md#serverlisten
14801525
[`process.execve()`]: process.md#processexecvefile-args-env
14811526
[`start` event]: #startevent
1527+
[`worker_threads.locks`]: worker_threads.md#worker_threadslocks
14821528
[context loss]: async_context.md#troubleshooting-context-loss
Collapse file

‎lib/internal/locks.js‎

Copy file name to clipboardExpand all lines: lib/internal/locks.js
+57-3Lines 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
}
Collapse file
+167Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.