-
Notifications
You must be signed in to change notification settings - Fork 9
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
Safe ownership for EventHandler #31
Comments
One question worth asking is whether the name should somehow reflect the "queue" or "Arc" nature of the API. I couldn't think of any ideas that didn't feel clunky, but I'm open to ideas.
These names all stink IMO 😝 |
That said, maybe "event handler" was too abstract of a name, and we really should consider something closer to the intuition that this is a thread-safe handle for a function. |
More thinking-out-loud about naming: It occurs to me that what I didn't like about the "thread-safe callback" name from the C++ Node plugin ecosystem is that "callback" is a contextual term, but it's used decontextualized. That's what led me to the intuition of "event handler" since it explains what the callback is for. But the other direction would be to emphasize that this is a function handle, and not talk about it being a callback in the name. I looked at the stdlib and see "sync" as an intuition that's maybe useful here (it's the Rust terminology for "thread-safe"). Some more ideas following that intuition:
Actually that middle one is not bad -- it turns out not to create a name conflict since there's no Trying it out for feel: let mut this = cx.this();
let callback = neon::sync::Function::new(cx.argument(0)?);
thread::spawn(move || {
for i in 0..100 {
// do some work ....
thread::sleep(Duration::from_millis(40));
// schedule a call into javascript
callback.clone().schedule(move |cx| {
// successful result to be passed to the event handler
Ok(cx.number(i))
}
}
}); |
I really, really like the idea of a
The high level threadsafe |
I like the concept of "persistent", that seems worth playing with. I think I see what you mean about decoupling primitives, but I'm not sure if I know how to make that work. So I think there's a few key elements to the design in this issue, let me see if I can brainstorm how to generalize it to arbitrary values:
So maybe the way to do this for general values is that when you schedule a closure to execute on the main thread, it drops the Maybe there's a way to make this a The name |
What that might look like in the example: let mut this = cx.this();
let callback = Persistent::new(cx.argument::<JsFunction>(0)?);
thread::spawn(move || {
for i in 0..100 {
// do some work ....
thread::sleep(Duration::from_millis(40));
// schedule a call into javascript
callback.clone().schedule(move |cx, f| {
let args = vec![cx.number(i)];
f.call(cx, args)
});
}
}); |
I was thinking that let executor = neon::sync::Executor::new(&mut cx)?;
let this = cx.this().persistent(&mut cx)?;
let callback = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?;
for i in 0..4 {
let executor = executor.clone();
let this = this.clone();
let callback = callback.clone();
std::thread::spawn(move || {
executor.schedule(move |mut cx| {
let this = this.deref(&mut cx)?;
let callback = callback.deref(&mut cx)?;
let res = cx.number(i);
let args = vec![cx.undefined().upcast(), res.upcast()];
callback.call(&mut cx, this, args);
});
});
} I don't think the Then the user wouldn't need to manage the lifetime of it and could call This API is more flexible, but, also much more verbose/less ergonomic than the current API being discussed. However, the goal would be for the higher level API to be able to be built using only safe, public primitives. |
I had a great call with @kjvalencik today. He helped me understand a few more of the constraints:
This suggests a design with two different types of persistent, ref-counted handles, analogous to Rust's For the sake of argument, let's call these, respectively:
Constructing a So the example might look like this: let queue = neon::event::EventQueue::from(&mut cx)?;
let this = cx.this().persistent(queue)?;
let callback = cx.argument::<JsFunction>(0)?.persistent(queue))?;
for i in 0..100 {
let this = this.clone();
let callback = callback.clone();
std::thread::spawn(move || {
// do some work ....
thread::sleep(Duration::from_millis(40));
// schedule a call into JavaScript
queue.enqueue(move |mut cx| {
let this = this.deref(&mut cx)?;
let callback = callback.deref(&mut cx)?;
let res = cx.number(i);
let args = vec![cx.undefined().upcast(), res.upcast()];
callback.call(&mut cx, this, args);
});
});
} |
We can probably defer the |
As @kjvalencik points out in neon-bindings/neon#551 and neon-bindings/neon#552, the memory management model for
EventHandler
is still not safe: it's susceptible to leaks and races.The problem is that the we aren't coordinating the overall lifetime of the queue of pending invocations of the event handler (which all run on the main thread); we're only tracking the places in random other threads where invocations are requested. So when the last request is made, we drop the underlying queue even though there may be pending invocations.
Instead, I propose we expose the ownership model into the public API of
EventHandler
. That is, the user should understandEventHandler
as an atomically refcounted handle to a JS event handler callback. The relevant changes to the API would be:self
..clone()
it.The types would look something like this:
Notice that the implementation of the
schedule*
methods would need to clone theArc<InvocationQueue>
and send it to the main thread in order to ensure that it's kept alive until all the invocations have terminated. This prevents the race where the invocation queue is shut down before all the invocations have executed.This should also make
EventHandler
virtually leak-proof: the only way to keep it alive indefinitely is to either keep a local computation running infinitely or to keep cloning it and passing it on to other computations infinitely. Otherwise, by default it will be shut down once all cloned instances have either dropped or scheduled and executed their invocations.This is an example of what would look different in user code. The example from the RFC would just need one more line to explicitly clone the
EventHandler
on each iteration:The text was updated successfully, but these errors were encountered: