-
Notifications
You must be signed in to change notification settings - Fork 285
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
feature(neon): API for thread-local data #902
Conversation
@dherman Something we need to consider for this feature is if we need internal reference counting to ensure the value isn't dropped early. Specifically, is it possible for the VM to stop and call the |
- `GlobalTable::default()` to avoid boilerplate `new()` method - Rename `borrow()` and `borrow_mut()` to `get()` and `get_mut()` - Add `'static` bound to global contents - Use `cloned()` in test code
… `get()` method.
- `Global<T>::get()` returns an `Option<&T>` - `Global<T>::get_or_init()` returns an `&T` - Lifetime of returned reference is the inner `'cx` since the boxed reference is immutable
Co-authored-by: K.J. Valencik <kjvalencik@gmail.com>
@jrose-signal We're playing around with two different versions of this API: an immutable one and a mutable one. (Either variation of the API should be more or less equally expressive, but they each make certain use cases more or less ergonomic.) I'll describe the pros and cons below but since you filed #728 I'm curious if you have a sense of whether your use cases would lean more on the mutable or immutable side? ImmutablePros
Cons
MutablePros
Cons
|
I'm afraid I've paged out the scheme I was going to use with this feature, especially since @indutny-signal successfully got Node to speed up "running this microtask has put another task on the microtask queue". I think I was just going to stick a unique ID in each instance and then have an API that did something like the following: fn run_or_send(self, cx: &mut impl Context<'_>) {
if UNIQUE_ID.get(cx) == self.target_context_unique_id {
self.run(cx);
} else {
let channel = self.channel.clone();
channel.send(move |cx| self.run(cx));
}
} This is not the most interesting use of instance-global data, but it is perfectly satisfied by the immutable API. I haven't looked at your implementation, but it seems possible for you to provide both APIs, |
Oh yeah, I also had this use case:
This is a bit trickier, because the initialization logic wants to use the Context, and that could end up calling some other Neon function. I think that means I wouldn't be able to use |
Good catch @jrose-signal on @dherman This is use case we had discussed for the closure getting a reference to the context that was passed in. There's also benefit to a Unfortunately, we can't have both versions of the API because it would allow having a mutable and immutable reference at the same time since they have different lifetimes. |
To be clear, even if
and you should not be able to init twice. So I don't think |
I have seen this pattern a million times (an API using a closure to initialize a data structure, which makes the API re-entrant) and I still missed it! Thanks for the eagle eye, @jrose-signal. I'm wondering if we can handle the double-initialization scenario automatically and panic. I think we can plausibly make the case that re-entrant re-initialization is a rare corner case and a bug, so it wouldn't need pollute the signature. There's definitely precedent for having multiple variants to Some possible sketches: impl<T> Global<T> {
fn get_or_init<'cx, 'a, C>(&self, cx: &'a mut C, value: T) -> &'cx T
where
C: Context<'cx>,
{ ... }
fn get_or_init_with<'cx, 'a, C, F>(&self, cx: &'a mut C) -> NeonResult<&'cx T>
where
C: Context<'cx>,
F: FnOnce(&mut C) -> NeonResult<T>,
{ ... }
}
impl<T: Default> Global<T> {
fn get_or_init_default<'cx, 'a, C>(&self, cx: &'a mut C) -> &'cx T
where
C: Context<'cx>,
{
self.get_or_init_with(cx, Default::default)
}
} Thoughts? |
Oops, the |
- Also rename `get_or_init` to `get_or_init_with` - Also add `get_or_init` that takes an owned init value
I pushed an implementation of my ideas above. See what you think? @jrose-signal I think this might work for your use case without you having to do a complicated |
I could be re-entrant, but I think that's okay as long as we define the semantics. It doesn't need to panic, it could overwrite (since we know all other references of dropped). Overwrite semantics would be identical to if you hand wrote something like:
|
- Uses an RAII pattern to ensure `get_or_try_init` always terminates cleanly - All initialization paths are checked for the dirty state to avoid re-entrancy - Also adds API docs and safety comments
I think re-entrant initialization is going to be so rare that we should treat it as an error, but instead of waiting for the outer initialization to fail, we should fail on the inner initialization. I've pushed an implementation that does this checking by setting the state to a third "dirty" state during |
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'm really excited about this! The API looks great and the transaction code is robust. ❤️
Co-authored-by: K.J. Valencik <kjvalencik@gmail.com>
Yeah I'm on board, and in fact, this made me realize that even |
I was worried that someone would expect data set in one addon to be available in another one (per your diagram in #728 (comment)), but since LocalKeys aren't created with any sort of user-visible representation, they'd never be the "same" key across addons anyway, any more than two copies of the same global would be the same variable at runtime in two dynamic libraries. (C header semantics notwithstanding…) So that lets Neon ignore the distinction between "worker/thread-local" and "addon/instance-local". |
@jrose-signal Just to make sure I follow, does that mean you do like this idea of calling it thread-local? (I pushed the change quickly just to see what it looks like, but I'm totally open to feedback.) |
I think it's not bad. I don't love it because JS might some day use "thread" to mean something else; calling it "worker-local" would probably be the equivalent using today's terminology. "instance-local" isn't wrong but pushes people to learn about instances when they may not need to, so I understand why you're trying to move away from it. |
That makes sense. I think the reason I favor thread over worker is that (a) the main thread isn't a worker, and (b) Node refers to workers as threads already anyway. |
MDN refers to "threads" too so yeah, should be clear enough. (And of course there can be a "technically it's per-instance" section in the docs.) |
I'll add something to the docs… |
The other thing I'd definitely want to see in the docs is "a JS thread is not necessarily bound to one OS thread, so you should be using this and not std::thread APIs". |
I added some text to the docs based on these suggestions. Reviews/feedback welcome! |
- Eliminate `get_or_init` and rename `get_or_init_with` to `get_or_init` - Add `# Panic` section to doc comment
Co-authored-by: K.J. Valencik <kjvalencik@gmail.com>
Co-authored-by: K.J. Valencik <kjvalencik@gmail.com>
Co-authored-by: K.J. Valencik <kjvalencik@gmail.com>
Co-authored-by: K.J. Valencik <kjvalencik@gmail.com>
Co-authored-by: K.J. Valencik <kjvalencik@gmail.com>
- Add addon lifecycle diagram - Add panics notes to `Root::{into_inner, to_inner}` - Replace "immutable" with "thread-safe" in list of safe cases
Looks great! Love the docs. |
My sincere thanks to both @jrose-signal and @kjvalencik for all the excellent feedback, ideas, and reviews. I'm happy with how this API came together. |
This PR adds a
neon::thread::LocalKey
API for storing thread-local data:Closes #728.