-
Notifications
You must be signed in to change notification settings - Fork 30.7k
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
worker: add Locks API #22719
worker: add Locks API #22719
Conversation
30f95f6
to
7c06fe4
Compare
src/env.h
Outdated
V(write_host_object_string, "_writeHostObject") \ | ||
V(write_queue_size_string, "writeQueueSize") \ | ||
V(x_forwarded_string, "x-forwarded-for") \ | ||
#define PER_ISOLATE_STRING_PROPERTIES(V) \ |
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.
Can we make the spacing the same as before on all of these lines so that the diff will be much easier to view?
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.
make format-cpp
just kinda... did that. i'm not sure how to undo it.
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.
They would have to be done manually. +1 on reverting this particular change.
src/node_internals.h
Outdated
V(v8) \ | ||
V(worker) \ | ||
V(zlib) | ||
#define NODE_BUILTIN_STANDARD_MODULES(V) \ |
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.
Ditto here about the spacing
7c06fe4
to
6099150
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.
Some preliminary comments. /cc @addaleax as the go-to C++ person :)
src/node_locks.cc
Outdated
// static | ||
LockManager* LockManager::GetCurrent() { | ||
if (current_ == nullptr) { | ||
current_ = new LockManager(); |
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 is not thread-safe. What if multiple threads calls GetCurrent
at the same time? I'd rather just initialize it for once in the main thread Environment's init method.
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 doesn’t even have to be initialized with new
(and if it is, we should use smart pointers for that) – it can be a LockManager LockManager::current_;
, no pointers or anything involved
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.
The alternative would be double locking this.
src/node_locks.cc
Outdated
LockManager* manager = LockManager::GetCurrent(); | ||
|
||
// Let queue be origin's lock request queue. | ||
auto queue = &manager->pending_requests_; |
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.
Use explicit types and references. This applies to most usage of auto
in this PR.
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.
In particular, use references if that is what you are going for – these lines here actually create copies of pending_requests_
and held_locks_
, but it’s not clear whether that’s intended?
src/node_locks.cc
Outdated
|
||
// Prepend request in origin's lock request queue. | ||
std::unique_ptr<LockRequest> request{this}; | ||
queue->push_front(std::move(request)); |
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.
emplace_front(this)
src/node_locks.h
Outdated
|
||
private: | ||
Mode mode_; | ||
const char* name_; |
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 has unclear data ownership. Use std::string instead.
src/node_locks.h
Outdated
node::Persistent<v8::Value> released_promise_; | ||
}; | ||
|
||
class LockRequest : public v8::Task { |
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.
As discussed offline, there is no benefit to making this a v8::Task
versus just a regular libuv-backed object as used elsewhere in Core.
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 think there's a benefit of simplicity... I would still need a class to store this all and still need to insert it into the foreground queue somehow. Might as well use the API we have specifically for that right?
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 agree with @TimothyGu here, this should ideally be a libuv task (see ThreadPoolWork
in node_internals.h
)
src/node_locks.h
Outdated
|
||
private: | ||
Environment* env_; | ||
const char* name_; |
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.
Ditto about strings.
I edited the OP to add the reference links provided by @inexorabletash in #22702 (comment). |
@@ -505,12 +507,80 @@ function pipeWithoutWarning(source, dest) { | |||
dest._maxListeners = destMaxListeners; | |||
} | |||
|
|||
class LockManager { | |||
request(name, options, callback) { | |||
if (callback === undefined) { |
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.
Nit, to differentiate this from a callback API can we call this ‘disposer’ and not ‘callback’
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 called "callback" in the spec.
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 would prefer this to be consistent with Node.js terminology. A callback for Node.js is an error-back. This would be confusing. This would also apply to our docs.
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 agree. While I generally prefer to keep naming the same or as same as possible to specs, in this case not calling this callback
makes sense. Also, on the off chance that someone decided to try to wrap this in util.promisify()
because it looks like a callback, it may make sense to have a custom promisify implementation for 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.
util.promisify makes no sense here, it already returns a promise. i will rename the variable.
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.
Yes, I know that util.promisify()
makes no sense here, but that doesn't mean users won't try. Being defensive by providing a custom promisify implementation that skips wrapping and returns the actual promise makes sense... or, throw within the custom promisify to indicate that it doesn't make sense.
if (name.startsWith('-')) { | ||
// if name starts with U+002D HYPHEN-MINUS (-), then reject promise with a | ||
// "NotSupportedError" DOMException. | ||
reject(new DOMException('NotSupportedError')); |
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.
To guard against cases where we throw by accident can this be an async function and throw instead of reject here?
We can still do the new promise dance below
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.
Also, not sure we should reject with a DOMException?
src/node_locks.cc
Outdated
// static | ||
LockManager* LockManager::GetCurrent() { | ||
if (current_ == nullptr) { | ||
current_ = new LockManager(); |
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.
The alternative would be double locking this.
src/node_locks.h
Outdated
|
||
Lock(Environment* env, | ||
Lock::Mode mode, | ||
std::string name, |
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.
const std::string& (to avoid extra copies)
src/node_locks.h
Outdated
Mode mode_; | ||
std::string name_; | ||
node::Persistent<v8::Value> waiting_promise_; | ||
node::Persistent<v8::Value> released_promise_; |
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.
The node::
namespacing can be dropped
src/node_locks.h
Outdated
LockRequest(Environment* env, | ||
v8::Local<v8::Value> promise, | ||
v8::Local<v8::Function> callback, | ||
std::string name, |
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.
ditto about std::string here and in name()
below
src/node_locks.h
Outdated
released_promise_.Reset(); | ||
} | ||
|
||
inline std::string name() const { return name_; } |
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.
const std::string& (to avoid extra copies)
src/node_locks.cc
Outdated
@@ -0,0 +1,329 @@ | |||
#include "node_locks.h" | |||
#include <memory> // std::move |
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 seems odd, if you’re after std::move
the header would be <utility>
src/node_locks.cc
Outdated
request->mode() == Lock::Mode::kShared | ||
? "shared" | ||
: "exclusive")) | ||
.ToChecked(); |
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 seems to share a bunch of code with the block below … can we merge that to some degree?
src/node_locks.cc
Outdated
env->mode_string(), | ||
FIXED_ONE_BYTE_STRING( | ||
env->isolate(), | ||
lock->mode() == Lock::Mode::kShared ? "shared" : "exclusive")) |
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 don’t think FIXED_ONE_BYTE_STRING
would work properly when the argument is not a fixed string
src/node_locks.cc
Outdated
CHECK(args[5]->IsBoolean()); | ||
|
||
String::Utf8Value name(env->isolate(), args[2]); | ||
Lock::Mode mode = (Lock::Mode)args[3].As<Number>()->Int32Value(); |
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.
Use static_cast
whenever possible
src/node_locks.h
Outdated
node::Persistent<v8::Value> released_promise_; | ||
}; | ||
|
||
class LockRequest : public v8::Task { |
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 agree with @TimothyGu here, this should ideally be a libuv task (see ThreadPoolWork
in node_internals.h
)
src/node_locks.h
Outdated
|
||
void Snapshot(Environment* env, v8::Local<v8::Value> promise); | ||
void ProcessQueue(Environment* env); | ||
bool IsGrantable(LockRequest* request); |
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 think both the method and the LockRequest*
parameter can be marked const
?
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.
Good work so far
@@ -505,12 +507,80 @@ function pipeWithoutWarning(source, dest) { | |||
dest._maxListeners = destMaxListeners; | |||
} | |||
|
|||
class LockManager { | |||
request(name, options, callback) { | |||
if (callback === undefined) { |
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 would prefer this to be consistent with Node.js terminology. A callback for Node.js is an error-back. This would be confusing. This would also apply to our docs.
48a1307
to
37ec99d
Compare
|
||
// promise, callback, name, mode, ifAvailable, steal | ||
void LockManager::Request(const FunctionCallbackInfo<Value>& args) { | ||
Environment* env = Environment::GetCurrent(args); |
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.
Likely better to push it to a separate follow up PR, but it would likely be helpful to emit trace events to track lock requests, grants, and releases to make them easier to debug.
src/node_locks.cc
Outdated
// If steal is true, then run these steps: | ||
if (steal_) { | ||
// For each lock of held: | ||
for (auto const& lock : manager->held_locks_) { |
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.
@addaleax ... just thinking out loud here... would an std::unordered_map<std::string,std::deque<std::unique_ptr<Lock>>
work better here to avoid having to loop through all locks and perform a string comparison on each?
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.
That’s very much possible – I haven’t really looked at the PR on that level, but it sounds like something that makes sense to me.
src/node_locks.cc
Outdated
->Call(env_->context(), Undefined(env_->isolate()), 1, &null) | ||
.ToLocal(&r)) { | ||
// Resolve promise with r and abort these steps. | ||
promise->Resolve(env_->context(), r).ToChecked(); |
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.
@addaleax ... does this function need to include an InternalCallbackScope
?
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 would say so, yes… That would also mean that this class should inherit from AsyncWrap
as well (but that makes sense, since it does run JS code asynchronously), and ditto for the other place you just commented
if (request->callback() | ||
->Call(env->context(), Undefined(env->isolate()), 1, args) | ||
.ToLocal(&r)) { | ||
waiting->Resolve(env->context(), r).ToChecked(); |
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.
@addaleax ... in this function too... is InternalCallbackScope
required?
3aed3ef
to
81eba51
Compare
81eba51
to
44532ff
Compare
@jasnell is this the correct usage of the trace api? // When lock lock's waiting promise settles (fulfills or rejects),
// enqueue the following steps on the lock task queue:
waitingPromise
.finally(() => undefined)
.then(() => {
// Release the lock lock.
release();
trace('e'.charCodeAt(0), 'node.locks', 'LOCK_RELEASE', 0, { name });
// Resolve lock's released promise with lock's waiting promise.
resolve(waitingPromise);
}); |
|
||
// Remove request from queue. | ||
pending_requests_.erase( | ||
std::remove( |
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.
std::remove
is C++17, which we can't use.
@devsnek What’s the status here? |
@addaleax currently rewriting this to fix some issues... timeline unknown |
@devsnek Any updates? It looks to be bitrotting pretty bad. |
I'd like to get back to this eventually unless someone else wants to take it up. I'll close this pr for now since it's pretty much useless at this point. |
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]>
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]>
Closes #22702
References:
https://wicg.github.io/web-locks/
https://github.com/WICG/web-locks
https://github.com/WICG/web-locks/blob/master/EXPLAINER.md
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes