| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
A LocalWorker is a Node.js environemnt that runs within the same thread. This feature is added behind a `--experimental-localworker` flag. This work is based on The synchronous-worker module as originally written by Anna Henningsen and is incorporated here with Anna's permission. Signed-off-by: Matteo Collina <hello@matteocollina.com> Co-Authored-By: James M Snell <jasnell@gmail.com>
|
Review requested:
|
Sorry, something went wrong.
|
The removal of the ability to spin the loop sounds great! This change allows us to model it in a node::Realm when node::Realm is stable with ShadowRealm. It would be good to ensure the API exposed can be migrated to built on top of node::Realm so that we can share the infrastructure between the two (e.g. integration of async hooks as the LocalWorker can create async continuation in the outer environment as well). Name bikeshed: what do you think about naming the API as vm.Realm or vm.NodeRealm? If I understand correctly, its execution blocks the thread. The name of Realm indicates it doesn't create any thread but rather a Context with Node.js built-ins. Also, this would suggest it is an API contrast to the ECMAScript built-in API ShadowRealm. |
Sorry, something went wrong.
|
I agree that having "Worker" in the name is confusing, since the API looks nothing like a Worker. |
Sorry, something went wrong.
|
To me "Worker" already implies it's on a different thread (and a different V8 isolate), so yeah calling this worker is a bit weird as it's essentially just a new context. I think a name with the word "Context" or "Realm" would really be more intuitive. |
Sorry, something went wrong.
|
I'm happy to call it NodeReam. |
Sorry, something went wrong.
Sorry, something went wrong.
There was a problem hiding this comment.
I’m only reviewing the documentation for now but this seems very promising. I wonder if this can replace the unfinished --experimental-vm-modules, at least for the most urgent use cases discussed in #37648. The support for ESM fills a gap that is frustrating for a lot of library authors.
Sorry, something went wrong.
| A `LocalWorker` is effectively a Node.js environment that runs within the | ||
| same thread. |
There was a problem hiding this comment.
I need a bit more detail than this. A Node.js environment . . . with its own global scope? That can have separate NODE_OPTIONS? Is it CommonJS or ESM, or either?
To others’ points, how does this differ from Realm?
Sorry, something went wrong.
There was a problem hiding this comment.
A Realm (in specs terms) does not support ESM, require and all Node.js core modules. It's like a Realm, but with all the Node.js stuff.
Sorry, something went wrong.
| added: REPLACEME | ||
| --> | ||
|
|
||
| * `filename` {string} |
There was a problem hiding this comment.
This is really any module specifier that can be passed to require, right? So not just filenames but also bare specifiers like lodash? Or node:fs?
Sorry, something went wrong.
There was a problem hiding this comment.
this is just require, yes.
Sorry, something went wrong.
There was a problem hiding this comment.
Since this is the same as module.createRequire(), we should copy some of the docs from that API. For example, filename can also be a URL.
Sorry, something went wrong.
| Create a dynamic `import()` function that can be used for loading EcmaScript | ||
| modules inside the inner Node.js instance. |
There was a problem hiding this comment.
What is the signature of the returned function? Does it return a promise? Does it match the signature of import(), where the second argument is an options bag (like { assert: { type: 'json' } })?
Sorry, something went wrong.
|
@GeoffreyBooth the implementation of the ESM support is at best hacky. It works but it involves writing a file, requiring it, and exporting the dynamic import in there 🥶. I would likely need some guidance on the internals of ESM to make it stables Ultimately, I think this is want to use for testing, hot reloading, and generic isolation within the same thread. |
Sorry, something went wrong.
That . . . is pretty hacky. This part? https://github.com/nodejs/node/pull/47855/files#diff-5b84120ee7ea85a4b78330964d42d04d9d7ab1bfa12983e6b379cb33f796a10eR115-R136 It seems like you’re doing this because you need to use require via createRequire? This is already an async function, presumably you could use import() here but I assume there’s something you need from this.#module.require? This would be a bit of a wild solution but one thing we could do is give require support for data: URIs. Then the source you’re generating here could be passed directly to require similarly to my example above, along the lines of require(`data:application/node,${encodeUriComponent(source)}`) (where application/node is the MIME type for CommonJS JavaScript). That would at least eliminate the “write to disk” hackiness. We could perhaps just make a new internal API for “require a source string” rather than actually ship this as a new public-facing feature, to get around this immediate need without signing up for a potentially big expansion of what we support CommonJS being able to do. I also suspect that “require a source string” might already exist in the codebase somewhere, like within the CommonJS loader as the step that happens after the source is loaded from disk. |
Sorry, something went wrong.
| if (process.listenerCount('uncaughtException') === 1) { | ||
| // If we are stopping, silence all errors | ||
| if (!this.#stoppedPromise) { | ||
| this.emit('error', err); |
There was a problem hiding this comment.
| this.emit('error', err); | |
| this.emit('uncaughtException', err); |
nitpick. makes more sense IMO
Sorry, something went wrong.
| ThreadId thread_id = AllocateEnvironmentThreadId(); | ||
| auto inspector_parent_handle = GetInspectorParentHandle( | ||
| outer_env, thread_id, "file:///synchronous-worker.js"); | ||
| env_ = CreateEnvironment(isolate_data_, |
There was a problem hiding this comment.
This doesn't seem to be a good idea - I think so far we've been assuming in the code base that there is a 1:1 relationship between the environment and the isolate, things can be subtly broken once we have n:1. It would be cleaner if we follow the Realm approach and just split out states in the Environment that are unique to each context/realm to a subclass of node::Realm (or maybe it is already just node::PrincipalRealm).
Sorry, something went wrong.
There was a problem hiding this comment.
I think so far we've been assuming in the code base that there is a 1:1 relationship between the environment and the isolate, things can be subtly broken once we have n:1.
Quite a few of the crashes I've been fighting with this approach are due to that. I'm using the reference to env to clean up all the handles. Otherwise, we will have bad crashes after stop() is called (which is why I'm doing this in core). Using a node::PrincipalReam would require removing the deliberate stop() method: how would we shut it down?
The other question I have if we dith the CreateEnvironment call, how do we guarantee some level of isolation?
Sorry, something went wrong.
There was a problem hiding this comment.
You can just move the handles into the Realm instead. Basically, just move things that should belong to individual realms instead of being shared across them to the realm, and do the setup/cleanup on a per-realm basis, which is what we've been trying to do with the ShadowRealm integration.
Sorry, something went wrong.
There was a problem hiding this comment.
You can just move the handles into the Realm instead.
How would you do that?
As an example HandleWrap add things to the Environment here:
Line 110 in c542d3a
Should I try to move that list from the Env to the Realm?
Sorry, something went wrong.
There was a problem hiding this comment.
Yes, just make that realm()->handle_wrap_queue()->PushBack(this). That's what we've been doing to support other BaseObjects in ShadowRealms, we just haven't got to HandleWrap yet. If our intent is to make individual realms have their own sets of handles etc., we'd have to move them into realms and stop mixing them in one giant Environment that contains per-thread information anyway.
Sorry, something went wrong.
There was a problem hiding this comment.
Yeah, tracking handle wraps and request wraps by realms is the plan for shadow realm integration. It's not the current focus yet.
However, moving the list from the Environment to the Realm breaks the postmortem diagnostics since tools like llnode are built on top of the Environment::handle_wrap_queue structure: https://github.com/nodejs/node/blob/main/src/node_postmortem_metadata.cc#L23
An option can be tracking the handle wraps and request wraps by both the Env and its creation Realm.
Sorry, something went wrong.
There was a problem hiding this comment.
I think we don't need to worry much about the postmortem diagnostics data, we only need to leave the offset of the queues within Realms and the tools would then figure out how to adapt to newer versions of Node.js. It's never guaranteed that we'd never change the layout of our internals, only that when we do, we still leave some information in the binary (the offsets) for these tools to figure out how to extract information from a core dump / process memory.
Sorry, something went wrong.
There was a problem hiding this comment.
My understanding is that we won't be able to have a process object without a new Node.js Environment, right?
May we land this PR and work to remove CreateEnvironment in follow-on work?
Sorry, something went wrong.
There was a problem hiding this comment.
I think so far we've been assuming in the code base that there is a 1:1 relationship between the environment and the isolate
That that's not the case is precisely the difference between Environment and IsolateData, though. Just because the Node.js CLI doesn't create multiple Environment instances per Isolate doesn't mean that it's not semantically correct to do so.
Sorry, something went wrong.
There was a problem hiding this comment.
That that's not the case is precisely the difference between Environment and IsolateData, though. Just because the Node.js CLI doesn't create multiple Environment instances per Isolate doesn't mean that it's not semantically correct to do so.
I don't think in practice we have really been writing the code that way. There are some places where we configure V8 isolate hooks (e.g. the ones in Environment::InitializeDiagnostics) with the current Environment as data to be used in the callback. The async hooks where things like Environment::GetCurrent(isolate)->trigger_async_id() is used a lot don't look particularly robust against a multi-Environment architecture either, and there are probably more places than I can think of off the top of my head..
Sorry, something went wrong.
| const w = new LocalWorker(); | ||
| const myAsyncFunction = w.createRequire(fileURLToPath(import.meta.url))('my-module'); | ||
| console.log(await myAsyncFunction()); | ||
| ``` |
There was a problem hiding this comment.
I do think the docs should clarify the difference between this and a ShadowRealm.
Sorry, something went wrong.
There was a problem hiding this comment.
I would also like to understand the differences (and similarities) between this and a worker. Because they look very similar. For example, does a realm have an event loop? Does it share globals? (I'm assuming yes and no?)
Sorry, something went wrong.
Co-authored-by: Moshe Atlow <moshe@atlow.co.il>
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com> Co-authored-by: Moshe Atlow <moshe@atlow.co.il> Co-authored-by: cjihrig <cjihrig@gmail.com>
There was a problem hiding this comment.
@GeoffreyBooth I’ve removed the file hack for createImport() and removed createRequire().
Thank you so much! I really appreciate the support 😄
There are bunch of little questions that need resolving but I agree with the general intent and API of this, and I’m happy to approve once we sort through the details.
Sorry, something went wrong.
| ```mjs | ||
| import { NodeRealm } from 'node:vm'; | ||
| const noderealm = new NodeRealm(); | ||
| const myAsyncFunction = noderealm.createImport(import.meta.url)('my-module'); |
There was a problem hiding this comment.
Could this be simplified to noderealm.import('my-module')? As in, are we able to infer the module parent (import.meta.url) from within createImport, and if so, is there ever a reason we wouldn’t want to import relative to that?
Sorry, something went wrong.
There was a problem hiding this comment.
I don't think retrieving a function caller's location reliably is possible.
Sorry, something went wrong.
| added: REPLACEME | ||
| --> | ||
|
|
||
| #### `noderealm.stop()` |
There was a problem hiding this comment.
| #### `noderealm.stop()` | |
| #### `nodeRealm.stop()` |
And for all following.
Sorry, something went wrong.
| // We might want to change this in the future to use a callback, | ||
| // but at this point it seems like a premature optimization. | ||
| // TODO(@mcollina): refactor to use a close callback | ||
| setTimeout(tryClosing, 100).unref(); |
There was a problem hiding this comment.
Why 100 and not, say, 10?
Sorry, something went wrong.
There was a problem hiding this comment.
See the comment above. Completely arbitrary, it could have been 42. I feel retrying 10 times per second is a good compromise, retrying 100 times per second might be too much.
I will refactor this to have a better solution for this active wait.
Sorry, something went wrong.
There was a problem hiding this comment.
(After this lands)
Sorry, something went wrong.
Co-authored-by: Geoffrey Booth <webadmin@geoffreybooth.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
There was a problem hiding this comment.
Some grammar fixes, but 👍 from me!
Sorry, something went wrong.
| outer_context_.Reset(env->isolate(), outer_context); | ||
| } | ||
|
|
||
| NodeRealm* NodeRealm::Unwrap(const FunctionCallbackInfo<Value>& args) { |
There was a problem hiding this comment.
Should NodeRealm inherit from BaseObject? This seems pretty similar to BaseObject's work.
Sorry, something went wrong.
There was a problem hiding this comment.
Yes. Only reason I didn't do this in synchronous-worker is because it was built outside of Node.js core.
Sorry, something went wrong.
| return MaybeLocal<Value>(); | ||
| } | ||
|
|
||
| NodeRealmScope worker_scope(this); |
There was a problem hiding this comment.
| NodeRealmScope worker_scope(this); | |
| NodeRealmScope realm_scope(this); |
Sorry, something went wrong.
| associated with this Node.js instance are released. This promise resolves on | ||
| the event loop of the _outer_ Node.js instance. | ||
|
|
||
| #### `nodeRealm.createImport(filename)` |
There was a problem hiding this comment.
Can we create the NodeRealm with a specifier?
interface NodeRealm {
constructor(specifier: string);
import(specifier, importAssertions): ModuleNamespace;
}In this way, we can get rid of the higher order function createImport and makes the class method more aligned with ShadowRealm.prototype.importValue:
import { NodeRealm } from 'node:vm';
const nodeRealm = new NodeRealm(import.meta.url);
const { myAsyncFunction } = await nodeRealm.import('my-module');
console.log(await myAsyncFunction());
Sorry, something went wrong.
| ~NodeRealmScope(); | ||
|
|
||
| private: | ||
| NodeRealm* w_; |
There was a problem hiding this comment.
Non-blocking nit: not really a fan of w_ as a member variable name. Something more descriptive would be ideal. realm_ perhaps?
Sorry, something went wrong.
| EnvironmentFlags::kTrackUnmanagedFds), | ||
| thread_id, | ||
| std::move(inspector_parent_handle)); | ||
| assert(env_ != nullptr); |
There was a problem hiding this comment.
assert()s should be turned into CHECKs
Sorry, something went wrong.
| Context::Scope context_scope(outer_context); | ||
| Isolate::SafeForTerminationScope termination_scope(isolate_); | ||
| Local<Value> onexit_v; | ||
| if (!self->Get(outer_context, String::NewFromUtf8Literal(isolate_, "onexit")) |
There was a problem hiding this comment.
This should be using onexit_string()
Sorry, something went wrong.
Co-authored-by: Geoffrey Booth <webadmin@geoffreybooth.com> Co-authored-by: Chengzhong Wu <legendecas@gmail.com> Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com> Co-authored-by: James M Snell <jasnell@gmail.com>
@GeoffreyBooth if anything, wouldn't it be application/node+javascript ? i.e., node.js is a subset of javascript. |
Sorry, something went wrong.
|
This needs a rebase. |
Sorry, something went wrong.
|
Closing this for now. Will reopen when we can do this without issues (roughly after the Olipan implementation). |
Sorry, something went wrong.
|
FWIW if what you are concerned about are AsyncWrap I think there is still a long way to go before we can properly migrate those (as they often have complex references to other native objects). For other simpler objects we are basically blocked by the next V8 upgrade for something like #52295 to avoid using deprecated APIs |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
A NodeRealm is a complete Node.js environment that runs within the same thread. This feature is added behind a --experimental-noderealm flag.
This work is based on #45018, which adds the synchronous-worker module initially written by @addaleax. and is incorporated here with Anna's permission as stated in #45018 by @jasnell (listed as a co-author of this change). The main difference from the synchronous-worker module is that I removed all the "synchronous" parts: it's now impossible to spin the event loop manually.
The reason for including this inside Node.js instead of keeping it as a separate module is access to internal API that would make stop() possible without crashing the process.
This implementation still suffers from a few bugs that I would aim to solve before removing the experimental flags: