-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
src: add web locks api #58666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
src: add web locks api #58666
Conversation
Review requested:
|
370462f
to
ba53a01
Compare
ba53a01
to
5d52680
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #58666 +/- ##
==========================================
- Coverage 90.07% 90.01% -0.07%
==========================================
Files 641 644 +3
Lines 188998 189878 +880
Branches 37069 37245 +176
==========================================
+ Hits 170246 170917 +671
- Misses 11462 11599 +137
- Partials 7290 7362 +72
🚀 New features to boost your workflow:
|
5d52680
to
6fdd577
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this lgtm. I looked through the WPT and other tests and they look all good. I reviewed the JS api to make sure it aligns with expectations and analyzed its source. All is good for a first implementation. I did a cursory review of the C++ part as I'm less experienced there, but in general it looks okay too. Nice work!
Adding
semver-major
|
The
notable-change
Please suggest a text for the release notes if you'd like to include a more detailed summary, then proceed to update the PR description with the text or a link to the notable change suggested text comment. Otherwise, the commit will be placed in the Other Notable Changes section. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just putting the red x on this since CI passed and it has sufficient sign off from others to land but there are still a few outstanding issues to resolve. I want to make sure it doesn't end up getting landed by the commit-queue while some comments are still pending.
// Called when the promise returned from the user's callback rejects | ||
static void OnIfAvailableReject(const FunctionCallbackInfo<Value>& info) { | ||
HandleScope handle_scope(info.GetIsolate()); | ||
auto* holder = static_cast<Global<Promise::Resolver>*>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These's aren't correct here. You really should not be holding a v8::Global<T>
inside a v8::External
, then deleting it like this. It's fine to use indirection through another type... e.g. having the External
hold an instance of a struct that is holding the Global<T>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did experiment with a wrapper that let the callback own a
std::unique_ptr
, so the Global<…>
would be reset automatically, but that changed the timing of the reset and introduced a race that broke the WPT and hanging tests sometimes.
I think that the current pattern is also valid since the Global is reset and freed immediately in the callback and the raw pointer’s ownership is transferred with release()
. Wdyt ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ought to be able to take another look this afternoon. Just catching up after two week of being out of the office ;-) ... just mentioning because I didn't want you to feel I was ignoring your pings on this ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take your time, thanks for your help
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasnell Did you manage to take another look on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this pattern is indeed very odd. Here's a diff that simplifies things a lot without appearing to break anything:
@@ -132,24 +130,15 @@ static void OnLockCallbackRejected(const FunctionCallbackInfo<Value>& info) {
// Called when the promise returned from the user's callback resolves
static void OnIfAvailableFulfill(const FunctionCallbackInfo<Value>& info) {
HandleScope handle_scope(info.GetIsolate());
- auto* holder = static_cast<Global<Promise::Resolver>*>(
- info.Data().As<External>()->Value());
- USE(holder->Get(info.GetIsolate())
+ USE(info.Data().As<Promise::Resolver>()
->Resolve(info.GetIsolate()->GetCurrentContext(), info[0]));
- holder->Reset();
- delete holder;
}
// Called when the promise returned from the user's callback rejects
static void OnIfAvailableReject(const FunctionCallbackInfo<Value>& info) {
HandleScope handle_scope(info.GetIsolate());
- auto* holder = static_cast<Global<Promise::Resolver>*>(
- info.Data().As<External>()->Value());
- USE(holder->Get(info.GetIsolate())
+ USE(info.Data().As<Promise::Resolver>()
->Reject(info.GetIsolate()->GetCurrentContext(), info[0]));
- holder->Reset();
-
- delete holder;
}
void LockManager::CleanupStolenLocks(Environment* env) {
@@ -326,28 +315,17 @@ void LockManager::ProcessQueue(Environment* env) {
if (callback_result->IsPromise()) {
Local<Promise> p = callback_result.As<Promise>();
- // The resolver survives until the promise settles and is freed
- // automatically.
- auto resolve_holder = std::make_unique<Global<Promise::Resolver>>(
- isolate, if_available_request->released_promise());
- auto reject_holder = std::make_unique<Global<Promise::Resolver>>(
- isolate, if_available_request->released_promise());
-
Local<Function> on_fulfilled;
Local<Function> on_rejected;
CHECK(Function::New(context,
OnIfAvailableFulfill,
- External::New(isolate, resolve_holder.get()))
+ if_available_request->released_promise())
.ToLocal(&on_fulfilled));
CHECK(Function::New(context,
OnIfAvailableReject,
- External::New(isolate, reject_holder.get()))
+ if_available_request->released_promise())
.ToLocal(&on_rejected));
- // Transfer ownership to the callbacks.
- resolve_holder.release();
- reject_holder.release();
-
{
TryCatchScope try_catch_scope(env);
if (p->Then(context, on_fulfilled, on_rejected).IsEmpty()) {
Generally speaking, we:
- Strongly want to avoid using
External
, because it is an "unsafe" feature of the V8 API in the sense that it associates a C++ object with a JS object without leaving V8's Garbage Collector, C++ memory management or diagnostic tooling any insight into what's happening (and so anExternal
can easily end up accidentally containing a dangling pointer, or alternatively the C++ object not freed properly when theExternal
is garbage-collected). - Avoid using
Global
s, because those do not properly represent references-relationships between objects (disclaimer: I don't know how the current work on C++ garbage collector integration changes this). If aGlobal
pointing at a JS object B is contained in a C++ object that is attached to some JS object A, the garbage collector cannot see this reference, and it treats the Global as a separate GC root (or a weak reference), neither of which correctly represents the typical intended reference mechanism.
In this case, we should be able to just associate JS data directly with the JS function object in question. If this isn't possible, it's generally advisable to create a BaseObject
with internal fields (in the V8 sense of the word) instead, which solves these issues.
I think the latter (inherit from BaseObject
) is what we'd also want to do for LockHolder
, instead of creating External
s containing pointers to LockHolder
instances.
I still think there may be at least a few issues here relating to how you're using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank for the pings, in particular @BridgeAR for making me aware of this PR over Slack, where messages generally reach me a bit better 🙂
This PR looks great and I am excited to see this feature finally come to Node.js, I think @jasnell had some good comments on the memory management aspects here that I think should be addressed before this PR is merged
auto* lock_holder = | ||
static_cast<LockHolder*>(info.Data().As<External>()->Value()); | ||
std::shared_ptr<Lock> lock = lock_holder->lock(); | ||
delete lock_holder; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fwiw, this should be the change that @jasnell suggested in the resolved comment, still feels a bit cleaner here to me but obviously not a big deal either way
auto* lock_holder = | |
static_cast<LockHolder*>(info.Data().As<External>()->Value()); | |
std::shared_ptr<Lock> lock = lock_holder->lock(); | |
delete lock_holder; | |
std::unique_ptr<LockHolder> lock_holder { | |
static_cast<LockHolder*>(info.Data().As<External>()->Value()) }; | |
std::shared_ptr<Lock> lock = lock_holder->lock(); |
(similar pattern would apply below)
// Called when the promise returned from the user's callback rejects | ||
static void OnIfAvailableReject(const FunctionCallbackInfo<Value>& info) { | ||
HandleScope handle_scope(info.GetIsolate()); | ||
auto* holder = static_cast<Global<Promise::Resolver>*>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this pattern is indeed very odd. Here's a diff that simplifies things a lot without appearing to break anything:
@@ -132,24 +130,15 @@ static void OnLockCallbackRejected(const FunctionCallbackInfo<Value>& info) {
// Called when the promise returned from the user's callback resolves
static void OnIfAvailableFulfill(const FunctionCallbackInfo<Value>& info) {
HandleScope handle_scope(info.GetIsolate());
- auto* holder = static_cast<Global<Promise::Resolver>*>(
- info.Data().As<External>()->Value());
- USE(holder->Get(info.GetIsolate())
+ USE(info.Data().As<Promise::Resolver>()
->Resolve(info.GetIsolate()->GetCurrentContext(), info[0]));
- holder->Reset();
- delete holder;
}
// Called when the promise returned from the user's callback rejects
static void OnIfAvailableReject(const FunctionCallbackInfo<Value>& info) {
HandleScope handle_scope(info.GetIsolate());
- auto* holder = static_cast<Global<Promise::Resolver>*>(
- info.Data().As<External>()->Value());
- USE(holder->Get(info.GetIsolate())
+ USE(info.Data().As<Promise::Resolver>()
->Reject(info.GetIsolate()->GetCurrentContext(), info[0]));
- holder->Reset();
-
- delete holder;
}
void LockManager::CleanupStolenLocks(Environment* env) {
@@ -326,28 +315,17 @@ void LockManager::ProcessQueue(Environment* env) {
if (callback_result->IsPromise()) {
Local<Promise> p = callback_result.As<Promise>();
- // The resolver survives until the promise settles and is freed
- // automatically.
- auto resolve_holder = std::make_unique<Global<Promise::Resolver>>(
- isolate, if_available_request->released_promise());
- auto reject_holder = std::make_unique<Global<Promise::Resolver>>(
- isolate, if_available_request->released_promise());
-
Local<Function> on_fulfilled;
Local<Function> on_rejected;
CHECK(Function::New(context,
OnIfAvailableFulfill,
- External::New(isolate, resolve_holder.get()))
+ if_available_request->released_promise())
.ToLocal(&on_fulfilled));
CHECK(Function::New(context,
OnIfAvailableReject,
- External::New(isolate, reject_holder.get()))
+ if_available_request->released_promise())
.ToLocal(&on_rejected));
- // Transfer ownership to the callbacks.
- resolve_holder.release();
- reject_holder.release();
-
{
TryCatchScope try_catch_scope(env);
if (p->Then(context, on_fulfilled, on_rejected).IsEmpty()) {
Generally speaking, we:
- Strongly want to avoid using
External
, because it is an "unsafe" feature of the V8 API in the sense that it associates a C++ object with a JS object without leaving V8's Garbage Collector, C++ memory management or diagnostic tooling any insight into what's happening (and so anExternal
can easily end up accidentally containing a dangling pointer, or alternatively the C++ object not freed properly when theExternal
is garbage-collected). - Avoid using
Global
s, because those do not properly represent references-relationships between objects (disclaimer: I don't know how the current work on C++ garbage collector integration changes this). If aGlobal
pointing at a JS object B is contained in a C++ object that is attached to some JS object A, the garbage collector cannot see this reference, and it treats the Global as a separate GC root (or a weak reference), neither of which correctly represents the typical intended reference mechanism.
In this case, we should be able to just associate JS data directly with the JS function object in question. If this isn't possible, it's generally advisable to create a BaseObject
with internal fields (in the V8 sense of the word) instead, which solves these issues.
I think the latter (inherit from BaseObject
) is what we'd also want to do for LockHolder
, instead of creating External
s containing pointers to LockHolder
instances.
This PR implements the Web Locks API, Locks are used to coordinate access to shared resources across multiple threads.
This implementation is based on previous work in #22719 and #36502, but takes a C++ native approach for better performance and reliability, this solution uses a singleton
LockManager
with thread-safe data structures to coordinate locks across all workers.exclusive
andshared
modessteal
optionifAvailable
optionsignal
optionquery.https.any.js
tests as unit testsCloses: #22702
Refs: https://w3c.github.io/web-locks