Skip to content

Commit 775d693

Browse files
addaleaxdevsnek
authored andcommitted
worker: implement Web Locks API
This is based upon nodejs#22719 and exposes the same API. Instead of a C++-backed approach, this variant implements the API mostly in JS. Refs: nodejs#22719 Co-authored-by: Gus Caplan <[email protected]>
1 parent 40d808f commit 775d693

File tree

10 files changed

+1024
-25
lines changed

10 files changed

+1024
-25
lines changed

doc/api/worker_threads.md

Lines changed: 225 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,210 @@ if (isMainThread) {
351351
}
352352
```
353353

354+
## `worker.locks`
355+
<!-- YAML
356+
added: REPLACEME
357+
-->
358+
359+
> Stability: 1 - Experimental
360+
361+
* {LockManager}
362+
363+
An instance of a [`LockManager`][].
364+
365+
### Class: `Lock`
366+
<!-- YAML
367+
added: REPLACEME
368+
-->
369+
370+
The Lock interface provides the name and mode of a previously requested lock,
371+
which is received in the callback to [`locks.request()`][].
372+
373+
#### `lock.name`
374+
<!-- YAML
375+
added: REPLACEME
376+
-->
377+
378+
* {string}
379+
380+
The name of this lock.
381+
382+
#### `lock.mode`
383+
<!-- YAML
384+
added: REPLACEME
385+
-->
386+
387+
* {string}
388+
389+
The mode of this lock. Either `shared` or `exclusive`.
390+
391+
### Class: `LockManager`
392+
<!-- YAML
393+
added: REPLACEME
394+
-->
395+
396+
The `LockManager` interface provides methods for requesting a new [`Lock`][]
397+
object and querying for an existing `Lock` object. To get an instance of
398+
`LockManager`, call `worker_threads.locks`.
399+
400+
With the exception of `AbortController` support, this implementation matches
401+
the [browser `LockManager`][] API.
402+
403+
#### `locks.request(name[, options], callback)`
404+
<!-- YAML
405+
added: REPLACEME
406+
-->
407+
408+
* `name` {string}
409+
* `options` {Object}
410+
* `mode` {string} Either `'exclusive'` or `'shared'`. **Default:**
411+
`'exclusive'`.
412+
* `ifAvailable` {boolean} If `true`, the lock request will only be
413+
granted if it is not already held. If it cannot be granted, the
414+
callback will be invoked with `null` instead of a `Lock` instance.
415+
**Default:** `false`.
416+
* `steal` {boolean} If `true`, then any held locks with the same name will be
417+
released, and the request will be granted, preempting any queued requests
418+
for it. **Default:** `false`.
419+
* `callback` {Function} The function to be invoked while the lock is acquired.
420+
The lock will be released when the function ends, or if the function returns
421+
a promise, when that promise settles.
422+
* Returns: {Promise}
423+
424+
Requests a [`Lock`][] object with parameters specifying its name and
425+
characteristics.
426+
427+
```js
428+
worker_threads.locks.request('my_resource', async (lock) => {
429+
// The lock was granted.
430+
}).then(() => {
431+
// The lock is released here.
432+
});
433+
```
434+
435+
#### `locks.query()`
436+
<!-- YAML
437+
added: REPLACEME
438+
-->
439+
440+
* Returns: {Promise}
441+
442+
Returns a Promise that resolves with a [`LockManagerSnapshot`][] which contains
443+
information about held and pending locks.
444+
445+
```js
446+
worker_threads.locks.query().then((state) => {
447+
state.held.forEach((lock) => {
448+
console.log(`held lock: name ${lock.name}, mode ${lock.mode}`);
449+
});
450+
state.pending.forEach((request) => {
451+
console.log(`requested lock: name ${request.name}, mode ${request.mode}`);
452+
});
453+
});
454+
```
455+
456+
## `worker.locks`
457+
<!-- YAML
458+
added: REPLACEME
459+
-->
460+
461+
> Stability: 1 - Experimental
462+
463+
* {LockManager}
464+
465+
An instance of a [`LockManager`][].
466+
467+
### Class: `Lock`
468+
<!-- YAML
469+
added: REPLACEME
470+
-->
471+
472+
The Lock interface provides the name and mode of a previously requested lock,
473+
which is received in the callback to [`locks.request()`][].
474+
475+
#### `lock.name`
476+
<!-- YAML
477+
added: REPLACEME
478+
-->
479+
480+
* {string}
481+
482+
The name of this lock.
483+
484+
#### `lock.mode`
485+
<!-- YAML
486+
added: REPLACEME
487+
-->
488+
489+
* {string}
490+
491+
The mode of this lock. Either `shared` or `exclusive`.
492+
493+
### Class: `LockManager`
494+
<!-- YAML
495+
added: REPLACEME
496+
-->
497+
498+
The `LockManager` interface provides methods for requesting a new [`Lock`][]
499+
object and querying for an existing `Lock` object. To get an instance of
500+
`LockManager`, call `worker_threads.locks`.
501+
502+
With the exception of `AbortController` support, this implementation matches
503+
the [browser `LockManager`][] API.
504+
505+
#### `locks.request(name[, options], callback)`
506+
<!-- YAML
507+
added: REPLACEME
508+
-->
509+
510+
* `name` {string}
511+
* `options` {Object}
512+
* `mode` {string} Either `'exclusive'` or `'shared'`. **Default:**
513+
`'exclusive'`.
514+
* `ifAvailable` {boolean} If `true`, the lock request will only be
515+
granted if it is not already held. If it cannot be granted, the
516+
callback will be invoked with `null` instead of a `Lock` instance.
517+
**Default:** `false`.
518+
* `steal` {boolean} If `true`, then any held locks with the same name will be
519+
released, and the request will be granted, preempting any queued requests
520+
for it. **Default:** `false`.
521+
* `callback` {Function} The function to be invoked while the lock is acquired.
522+
The lock will be released when the function ends, or if the function returns
523+
a promise, when that promise settles.
524+
* Returns: {Promise}
525+
526+
Requests a [`Lock`][] object with parameters specifying its name and
527+
characteristics.
528+
529+
```js
530+
worker_threads.locks.request('my_resource', async (lock) => {
531+
// The lock was granted.
532+
}).then(() => {
533+
// The lock is released here.
534+
});
535+
```
536+
537+
#### `locks.query()`
538+
<!-- YAML
539+
added: REPLACEME
540+
-->
541+
542+
* Returns: {Promise}
543+
544+
Returns a Promise that resolves with a [`LockManagerSnapshot`][] which contains
545+
information about held and pending locks.
546+
547+
```js
548+
worker_threads.locks.query().then((state) => {
549+
state.held.forEach((lock) => {
550+
console.log(`held lock: name ${lock.name}, mode ${lock.mode}`);
551+
});
552+
state.pending.forEach((request) => {
553+
console.log(`requested lock: name ${request.name}, mode ${request.mode}`);
554+
});
555+
});
556+
```
557+
354558
## Class: `BroadcastChannel extends EventTarget`
355559

356560
<!-- YAML
@@ -1364,21 +1568,25 @@ thread spawned will spawn another until the application crashes.
13641568
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]: errors.md#err_missing_message_port_in_transfer_list
13651569
[`ERR_WORKER_NOT_RUNNING`]: errors.md#err_worker_not_running
13661570
[`EventTarget`]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
1367-
[`FileHandle`]: fs.md#class-filehandle
1368-
[`MessagePort`]: #class-messageport
1571+
[`FileHandle`]: fs.md#fs_class_filehandle
1572+
[`KeyObject`]: crypto.md#crypto_class_keyobject
1573+
[`Lock`]: #worker_threads_class_lock
1574+
[`LockManager`]: #worker_threads_class_lockmanager
1575+
[`LockManagerSnapshot`]: https://developer.mozilla.org/en-US/docs/Web/API/LockManagerSnapshot
1576+
[`MessagePort`]: #worker_threads_class_messageport
13691577
[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
13701578
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
13711579
[`WebAssembly.Module`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module
13721580
[`Worker constructor options`]: #new-workerfilename-options
13731581
[`Worker`]: #class-worker
13741582
[`data:` URL]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
1375-
[`fs.close()`]: fs.md#fsclosefd-callback
1376-
[`fs.open()`]: fs.md#fsopenpath-flags-mode-callback
1377-
[`markAsUntransferable()`]: #workermarkasuntransferableobject
1378-
[`node:cluster` module]: cluster.md
1379-
[`perf_hooks.performance`]: perf_hooks.md#perf_hooksperformance
1380-
[`perf_hooks` `eventLoopUtilization()`]: perf_hooks.md#performanceeventlooputilizationutilization1-utilization2
1381-
[`port.on('message')`]: #event-message
1583+
[`fs.close()`]: fs.md#fs_fs_close_fd_callback
1584+
[`fs.open()`]: fs.md#fs_fs_open_path_flags_mode_callback
1585+
[`locks.request()`]: #worker_threads_locks_request_name_options_callback
1586+
[`markAsUntransferable()`]: #worker_threads_worker_markasuntransferable_object
1587+
[`perf_hooks.performance`]: perf_hooks.md#perf_hooks_perf_hooks_performance
1588+
[`perf_hooks` `eventLoopUtilization()`]: perf_hooks.md#perf_hooks_performance_eventlooputilization_utilization1_utilization2
1589+
[`port.on('message')`]: #worker_threads_event_message
13821590
[`port.onmessage()`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/onmessage
13831591
[`port.postMessage()`]: #portpostmessagevalue-transferlist
13841592
[`process.abort()`]: process.md#processabort
@@ -1399,12 +1607,14 @@ thread spawned will spawn another until the application crashes.
13991607
[`trace_events`]: tracing.md
14001608
[`v8.getHeapSnapshot()`]: v8.md#v8getheapsnapshotoptions
14011609
[`vm`]: vm.md
1402-
[`worker.SHARE_ENV`]: #workershare_env
1403-
[`worker.on('message')`]: #event-message_1
1404-
[`worker.postMessage()`]: #workerpostmessagevalue-transferlist
1405-
[`worker.terminate()`]: #workerterminate
1406-
[`worker.threadId`]: #workerthreadid_1
1407-
[async-resource-worker-pool]: async_context.md#using-asyncresource-for-a-worker-thread-pool
1610+
[`Worker constructor options`]: #worker_threads_new_worker_filename_options
1611+
[`worker.on('message')`]: #worker_threads_event_message_1
1612+
[`worker.postMessage()`]: #worker_threads_worker_postmessage_value_transferlist
1613+
[`worker.SHARE_ENV`]: #worker_threads_worker_share_env
1614+
[`worker.terminate()`]: #worker_threads_worker_terminate
1615+
[`worker.threadId`]: #worker_threads_worker_threadid_1
1616+
[async-resource-worker-pool]: async_hooks.md#async-resource-worker-pool
1617+
[browser `LockManager`]: https://developer.mozilla.org/en-US/docs/Web/API/LockManager
14081618
[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
14091619
[child processes]: child_process.md
14101620
[contextified]: vm.md#what-does-it-mean-to-contextify-an-object

lib/internal/worker.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const dc = require('diagnostics_channel');
9595
const workerThreadsChannel = dc.channel('worker_threads');
9696

9797
let cwdCounter;
98+
let locksInitialized = false;
9899

99100
const environmentData = new SafeMap();
100101

@@ -207,6 +208,13 @@ class Worker extends EventEmitter {
207208
}
208209

209210
debug('instantiating Worker.', `url: ${url}`, `doEval: ${doEval}`);
211+
if (isMainThread && !locksInitialized) {
212+
locksInitialized = true;
213+
// Make sure that locks are initialized before active
214+
// multithreading starts.
215+
require('worker_threads').locks.query();
216+
}
217+
210218
// Set up the C++ handle for the worker, as well as some internal wiring.
211219
this[kHandle] = new WorkerImpl(url,
212220
env === process.env ? null : env,

lib/internal/worker/io.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ class BroadcastChannel extends EventTarget {
429429
super();
430430
this[kType] = 'BroadcastChannel';
431431
this[kName] = `${name}`;
432-
this[kHandle] = broadcastChannel(this[kName]);
432+
this[kHandle] = broadcastChannel(`userland:${this[kName]}`);
433433
this[kOnMessage] = FunctionPrototypeBind(onMessageEvent, this, 'message');
434434
this[kOnMessageError] =
435435
FunctionPrototypeBind(onMessageEvent, this, 'messageerror');
@@ -538,6 +538,7 @@ defineEventHandler(BroadcastChannel.prototype, 'messageerror');
538538
module.exports = {
539539
drainMessagePort,
540540
messageTypes,
541+
kHandle,
541542
kPort,
542543
kIncrementsPortRef,
543544
kWaitingStreams,

0 commit comments

Comments
 (0)