-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
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.15% 90.08% -0.08%
==========================================
Files 639 643 +4
Lines 188201 189048 +847
Branches 36915 37062 +147
==========================================
+ Hits 169675 170302 +627
- Misses 11274 11419 +145
- Partials 7252 7327 +75
🚀 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
|
Failed to start CI⚠ Commits were pushed since the last approving review: ⚠ - worker: add web locks api ⚠ - use WebIDL convertors ⚠ - add lock and lockmanager to globals doc ⚠ - remove lock and lockmanager from globals ⚠ - attemp to atach catch on c++ by defering to next microtask ⚠ - separate promise fulfillment and rejection handlers ⚠ - add cjs/esm code in locks doc ⚠ - add more tests and doc ✘ Refusing to run CI on potentially unsafe PRhttps://github.com/nodejs/node/actions/runs/15825543437 |
There are relevant test failures in CI (web locks web platform tests) |
External::New(isolate, reject_holder)) | ||
.ToLocal(&on_rejected_callback)) { | ||
delete fulfill_holder; | ||
delete reject_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.
These might need to be handled separately. If the first call succeeds, then the first created function will take ownership over the fulfill_holder
. If the second call then fails for whatever reason, we are deleting the fulfill_holder
while it's external is still holding the reference to it that it assumes it owns. It would be best to separate this into two separate calls rather than aggregating them together like this. Create one, create it's external and it's function, then create the second...
Or, can we at least be certain that we won't end up with a free-after-free type error when deleting these while the External is still holding them?
Local<Value> rejection_value = promise->Result(); | ||
grantable_request->waiting_promise() | ||
->Reject(context, rejection_value) | ||
.Check(); |
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.
Not something to do here, but using Check()
here has the same issue as using ToLocalChecked()
in that it will just crash the process rather than propagate the error. This is a common issue throughout the code, however, so not something I would block this PR on. We need to handle these better in general.
If you did want to handle this here, then changing these to check if the return value is empty then doing some proper error propagation similar to the way the ToLocal(...) results are handled would be ideal.
grantable_request->waiting_promise() | ||
->Resolve(context, callback_result) | ||
.Check(); | ||
USE(promise->Then( |
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.
We should avoid using USE
for the same error propagation reasons. Calling Then(...)
can cause a JavaScript error to be scheduled. USE would cause that to be ignored when we ought to propagate it.
->Resolve(context, callback_result) | ||
.Check(); | ||
Local<Value> promise_args[] = {callback_result}; | ||
USE(on_fulfilled_callback->Call( |
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.
Likewise here. Calling the callback could throw. The error should be appropriately propagated.
CHECK(args[0]->IsString()); | ||
CHECK(args[1]->IsString()); | ||
CHECK(args[2]->IsString()); | ||
CHECK(args[3]->IsBoolean()); | ||
CHECK(args[4]->IsBoolean()); | ||
CHECK(args[5]->IsFunction()); |
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.
CHECK(args[0]->IsString()); | |
CHECK(args[1]->IsString()); | |
CHECK(args[2]->IsString()); | |
CHECK(args[3]->IsBoolean()); | |
CHECK(args[4]->IsBoolean()); | |
CHECK(args[5]->IsFunction()); | |
CHECK(args[0]->IsString()); // name | |
CHECK(args[1]->IsString()); // clientId | |
CHECK(args[2]->IsString()); // mode | |
CHECK(args[3]->IsBoolean()); // steal | |
CHECK(args[4]->IsBoolean()); // ifAvailable | |
CHECK(args[5]->IsFunction()); // callback |
Just to help connect these back to the documentation comment at the top
Local<Promise::Resolver> waiting_promise; | ||
if (!Promise::Resolver::New(context).ToLocal(&waiting_promise)) { | ||
return; | ||
} | ||
Local<Promise::Resolver> released_promise; | ||
if (!Promise::Resolver::New(context).ToLocal(&released_promise)) { | ||
return; | ||
} |
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.
Local<Promise::Resolver> waiting_promise; | |
if (!Promise::Resolver::New(context).ToLocal(&waiting_promise)) { | |
return; | |
} | |
Local<Promise::Resolver> released_promise; | |
if (!Promise::Resolver::New(context).ToLocal(&released_promise)) { | |
return; | |
} | |
Local<Promise::Resolver> waiting_promise; | |
Local<Promise::Resolver> released_promise; | |
if (!Promise::Resolver::New(context).ToLocal(&waiting_promise) || | |
!Promise::Resolver::New(context).ToLocal(&released_promise)) { | |
return; | |
} |
return; | ||
} | ||
|
||
args.GetReturnValue().Set(released_promise->GetPromise()); |
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.
It's a bit unusual (by convention) to set the return value and then perform additional actions. It makes it rather easy to miss that the return value is set at all. Could this be moved to the end of the function?
if (!Promise::Resolver::New(context).ToLocal(&resolver)) { | ||
return; | ||
} | ||
args.GetReturnValue().Set(resolver->GetPromise()); |
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.
Likewise here... setting the return value in the middle here is a bit unusual.
for (const auto& resource_entry : manager->held_locks_) { | ||
for (const auto& held_lock : resource_entry.second) { | ||
if (held_lock->env() == env) { | ||
Local<Object> lock_info = |
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.
You might consider moving the declaration for the Local<Object> lock_info
to outside of the for loop and just reset it here so that we're reusing the same declaration on each iteration rather than creating a new one.
index = 0; | ||
for (const auto& pending_request : manager->pending_queue_) { | ||
if (pending_request->env() == env) { | ||
Local<Object> lock_info = |
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.
Same here... if the Local<Object> lock_info;
is moved outside of the for loops then it can just be reused here.
result->Set(context, FIXED_ONE_BYTE_STRING(isolate, "held"), held_list) | ||
.Check(); | ||
result->Set(context, FIXED_ONE_BYTE_STRING(isolate, "pending"), pending_list) | ||
.Check(); |
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.
Let's avoid using Check()
here and propagate the errors appropriately.
Local<Object> obj = Object::New(isolate); | ||
|
||
Local<String> name_string; | ||
if (!String::NewFromTwoByte(isolate, |
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.
TODO for later (no need to do it in this PR)... we should add a ToV8Value variant that accepts the std::u16string
return Local<Object>(); | ||
} | ||
obj->Set(context, FIXED_ONE_BYTE_STRING(isolate, "name"), name_string) | ||
.Check(); |
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.
Avoid Check here... and elsewhere in this function. Specifically, if you can modify this function to return a MaybeLocal<Object>
instead, then if these return IsNothing() == true
, you should just return an empty MaybeLocal<Object>
to propagate the error.
LockManager::GetCurrent()->CleanupEnvironment(env); | ||
} | ||
|
||
static Local<Object> CreateLockInfoObject(Isolate* isolate, |
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.
Consider having this return a MaybeLocal<Object>
to better facilitate error propagation
if (String::NewFromUtf8(isolate, kSharedMode).ToLocal(&shared_mode)) { | ||
target->Set(FIXED_ONE_BYTE_STRING(isolate, "LOCK_MODE_SHARED"), | ||
shared_mode); | ||
} |
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.
Would NODE_DEFINE_STRING_CONSTANT
work for these here instead?
I think this is very close! Added a few comments and notice that there are WPT test failures. |
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