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 ace802e

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 f3fd7ed commit ace802e
Copy full SHA for ace802e

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
@@ -1443,6 +1443,50 @@ Emitted when [`child_process.spawn()`][] encounters an error.
14431443

14441444
Emitted when [`process.execve()`][] is invoked.
14451445

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

14481492
> Stability: 1 - Experimental
@@ -1472,7 +1516,9 @@ Emitted when a new thread is created.
14721516
[`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels
14731517
[`end` event]: #endevent
14741518
[`error` event]: #errorevent
1519+
[`locks.request()`]: worker_threads.md#locksrequestname-options-callback
14751520
[`net.Server.listen()`]: net.md#serverlisten
14761521
[`process.execve()`]: process.md#processexecvefile-args-env
14771522
[`start` event]: #startevent
1523+
[`worker_threads.locks`]: worker_threads.md#worker_threadslocks
14781524
[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.