Skip to content
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

Closed
wants to merge 4 commits into from
Closed

Conversation

devsnek
Copy link
Member

@devsnek devsnek commented Sep 5, 2018

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), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

Sorry, something went wrong.

@devsnek devsnek added wip Issues and PRs that are still a work in progress. worker Issues and PRs related to Worker support. labels Sep 5, 2018
@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. labels Sep 5, 2018
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) \
Copy link
Contributor

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?

Copy link
Member Author

@devsnek devsnek Sep 5, 2018

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.

Copy link
Member

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.

V(v8) \
V(worker) \
V(zlib)
#define NODE_BUILTIN_STANDARD_MODULES(V) \
Copy link
Contributor

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

Copy link
Member

@TimothyGu TimothyGu left a 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 :)

// static
LockManager* LockManager::GetCurrent() {
if (current_ == nullptr) {
current_ = new LockManager();
Copy link
Member

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.

Copy link
Member

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

Copy link
Member

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.

LockManager* manager = LockManager::GetCurrent();

// Let queue be origin's lock request queue.
auto queue = &manager->pending_requests_;
Copy link
Member

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.

Copy link
Member

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?


// Prepend request in origin's lock request queue.
std::unique_ptr<LockRequest> request{this};
queue->push_front(std::move(request));
Copy link
Member

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_;
Copy link
Member

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 {
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto about strings.

@targos
Copy link
Member

targos commented Sep 6, 2018

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) {
Copy link
Member

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’

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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'));
Copy link
Member

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

Copy link
Member

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?

// static
LockManager* LockManager::GetCurrent() {
if (current_ == nullptr) {
current_ = new LockManager();
Copy link
Member

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,
Copy link
Member

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_;
Copy link
Member

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,
Copy link
Member

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_; }
Copy link
Member

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)

@@ -0,0 +1,329 @@
#include "node_locks.h"
#include <memory> // std::move
Copy link
Member

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>

request->mode() == Lock::Mode::kShared
? "shared"
: "exclusive"))
.ToChecked();
Copy link
Member

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?

env->mode_string(),
FIXED_ONE_BYTE_STRING(
env->isolate(),
lock->mode() == Lock::Mode::kShared ? "shared" : "exclusive"))
Copy link
Member

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

CHECK(args[5]->IsBoolean());

String::Utf8Value name(env->isolate(), args[2]);
Lock::Mode mode = (Lock::Mode)args[3].As<Number>()->Int32Value();
Copy link
Member

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 {
Copy link
Member

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);
Copy link
Member

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?

Copy link
Member

@mcollina mcollina left a 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) {
Copy link
Member

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.


// promise, callback, name, mode, ifAvailable, steal
void LockManager::Request(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Copy link
Member

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.

// If steal is true, then run these steps:
if (steal_) {
// For each lock of held:
for (auto const& lock : manager->held_locks_) {
Copy link
Member

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?

Copy link
Member

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.

->Call(env_->context(), Undefined(env_->isolate()), 1, &null)
.ToLocal(&r)) {
// Resolve promise with r and abort these steps.
promise->Resolve(env_->context(), r).ToChecked();
Copy link
Member

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?

Copy link
Member

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();
Copy link
Member

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?

@devsnek devsnek force-pushed the feature/locks-api branch 3 times, most recently from 3aed3ef to 81eba51 Compare September 7, 2018 01:22
@devsnek
Copy link
Member Author

devsnek commented Sep 7, 2018

@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);
              });

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.

// Remove request from queue.
pending_requests_.erase(
std::remove(
Copy link
Member

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.

@addaleax addaleax added the semver-minor PRs that contain new features and should be released in the next minor version. label Sep 16, 2018
@addaleax
Copy link
Member

@devsnek What’s the status here?

@devsnek
Copy link
Member Author

devsnek commented Sep 16, 2018

@addaleax currently rewriting this to fix some issues... timeline unknown

@bnoordhuis
Copy link
Member

@devsnek Any updates? It looks to be bitrotting pretty bad.

@devsnek
Copy link
Member Author

devsnek commented Dec 6, 2018

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.

@devsnek devsnek closed this Dec 6, 2018
targos pushed a commit to addaleax/node that referenced this pull request Dec 14, 2020
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]>
debadree25 pushed a commit to debadree25/node that referenced this pull request Apr 15, 2023
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. semver-minor PRs that contain new features and should be released in the next minor version. wip Issues and PRs that are still a work in progress. worker Issues and PRs related to Worker support.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants