-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
88e2f13
commit 44da5af
Showing
1 changed file
with
277 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
- Feature Name: Threadsafe Handles | ||
- Start Date: 2020-07-21 | ||
- RFC PR: (leave this empty) | ||
- Neon Issue: (leave this empty) | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
Two new `Send + Sync` features are introduced: `EventQueue` and `Persistent`. These features work together to allow multi-threaded modules to interact with the Javascript main thread. | ||
|
||
* `EventQueue`: Threadsafe handle that allows sending closures to be executed on the main thread. | ||
* `Persistent<T>`: Opaque handle that can be sent across threads, but only dereferenced on the main thread. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
High level event handlers have gone through [several](https://github.com/neon-bindings/rfcs/pull/25) [iterations](https://github.com/neon-bindings/rfcs/pull/28) without quite providing [ideal](https://github.com/neon-bindings/rfcs/issues/31) ergonomics or [safety](https://github.com/neon-bindings/neon/issues/551). | ||
|
||
The Threadsafe Handlers feature attempts to decompose the high level `EventHandler` API into lower level, more flexible primitives. | ||
|
||
These primitives can be used to build a higher level and _safe_ `EventHandler` API allowing experimentation with patterns outside of `neon`. | ||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
Neon provides a smart pointer type `Handle<'a, T: Value>` for referencing values on the Javascript heap. These references are bound by the lifetime of `Context<'a>` to ensure they can only be used while the VM is locked in a synchronous neon function. These are neither `Send` or `Sync`. | ||
|
||
### `neon::handle::Persistent<T>` | ||
|
||
As a developer, I may want to retain a reference to a Javascript value while returning control back to the VM. | ||
|
||
`Handle<_>` may not outlive the context that created them. | ||
|
||
```rust | ||
// Does not compile because `cb` cannot be sent across threads | ||
fn thread_log(mut cx: FunctionContext) -> JsResult<JsUndefined> { | ||
let cb = cx.argument::<JsFunction>(0)?; | ||
|
||
std::thread::spawn(move || { | ||
println!("{:?}", cb); | ||
}); | ||
|
||
cx.undefined() | ||
} | ||
``` | ||
|
||
However, a persistent reference may be created from a Handle which can be sent across threads. | ||
|
||
```rust | ||
// Compiles | ||
fn thread_log(mut cx: FunctionContext) -> JsResult<JsUndefined> { | ||
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?; | ||
|
||
std::thread::spawn(move || { | ||
println!("{:?}", cb); | ||
}); | ||
|
||
cx.undefined() | ||
} | ||
``` | ||
|
||
While `Persistent<_>` may be shared across threads, they can only be dereferenced on the main Javascript thread. This is controlled by requiring a `Context<_>` to dereference. | ||
|
||
```rust | ||
fn log(mut cx: FunctionContext) -> JsResult<JsUndefined> { | ||
let persistent_cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?; | ||
let cb = persistent_cb.deref(&cx); | ||
|
||
println!("{:?}", cb); | ||
|
||
cx.undefined() | ||
} | ||
``` | ||
|
||
### `neon::handle::EventQueue` | ||
|
||
Once a value is wrapped in a `Persistent<_>` handle, it must be sent back to the main Javascript thread to dereference. The `EventQueue` handle provides a mechanism for requesting work be peformed on the main thread. | ||
|
||
To schedule work, _send_ a closure to the event queue: | ||
|
||
```rust | ||
fn thread_callback(mut cx: FunctionContext) -> JsResult<JsUndefined> { | ||
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?; | ||
let queue = EventQueue::new(&mut cx)?; | ||
|
||
std::thread::spawn(move || { | ||
queue.send(move |mut cx| { | ||
let this = cx.undefined(); | ||
let msg = cx.string("Hello, World!"); | ||
let cb = cb.deref(&cx).unwrap(); | ||
|
||
cb.call(&mut cx, this, vec![msg]).unwrap(); | ||
}); | ||
}); | ||
|
||
cx.undefined() | ||
} | ||
``` | ||
|
||
`Persistent<_>` and `EventQueue` are clone-able and can be used many times. | ||
|
||
```rust | ||
fn thread_callback(mut cx: FunctionContext) -> JsResult<JsUndefined> { | ||
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?; | ||
let queue = EventQueue::new(&mut cx)?; | ||
|
||
for i in 1..=10 { | ||
let queue = queue.clone(); | ||
let cb = cb.clone(); | ||
|
||
std::thread::spawn(move || { | ||
queue.send(move |mut cx| { | ||
let this = cx.undefined(); | ||
let msg = cx.string(format!("Count: {}", i)); | ||
let cb = cb.deref(&cx).unwrap(); | ||
|
||
cb.call(&mut cx, this, vec![msg]).unwrap(); | ||
}); | ||
}); | ||
} | ||
|
||
cx.undefined() | ||
} | ||
``` | ||
|
||
Instances of `EventQueue` will keep the event loop running and prevent the process from exiting. The `EventQueue::unref` method is provided to change this behavior and allow the process to exit while an instance of `EventQueue` still exists. | ||
|
||
However, calls to `EventQueue::schedule` _might_ not execute before the process exits. | ||
|
||
```rust | ||
fn thread_callback(mut cx: FunctionContext) -> JsResult<JsUndefined> { | ||
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?; | ||
let mut queue = EventQueue::new(&mut cx)?; | ||
|
||
queue.unref(); | ||
|
||
std::thread::spawn(move || { | ||
std::thread::sleep(std::time::Duration::from_secs(1)); | ||
|
||
// If the event queue is empty, the process may exit before this executes | ||
queue.send(move |mut cx| { | ||
let this = cx.undefined(); | ||
let msg = cx.string("Hello, World!"); | ||
let cb = cb.deref(&cx).unwrap(); | ||
|
||
cb.call(&mut cx, this, vec![msg]).unwrap(); | ||
}); | ||
}); | ||
|
||
cx.undefined() | ||
} | ||
``` | ||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
### `neon::handle::Persistent<T: Object>` | ||
|
||
`Persistent` handles are references to objects on the v8 heap that prevent objects from being garbage collected. If objects are moved, the internal pointer will be updated. | ||
|
||
_`Persistent` handles may only be used for `Object` and `Function` types. This is a limitation of `n-api`._ | ||
|
||
```rust | ||
/// `Send + Sync` Persistent handle to Javascript objects | ||
impl Persistent<T: Object> { | ||
pub fn new<'a, C: Context<'a>>( | ||
cx: &mut C, | ||
v: Handle<T>, | ||
) -> NeonResult<Self>; | ||
|
||
pub fn deref<'a, C: Context<'a>>( | ||
self, | ||
cx: &mut C, | ||
) -> JsResult<'a, T>; | ||
} | ||
``` | ||
|
||
#### Design Notes | ||
|
||
`Persistent` utilize an atomic reference count to track when they can be safely dropped. There are two circumstances where a `Persistent` may be dropped: | ||
|
||
* `Persistent` is no longer held. For example, if it is a member of a struct that is dropped. | ||
* `Persistent::deref` is called and it decrements the count to zero. | ||
|
||
In the case of a `Persistent` being dropped after a call to `deref`, neon may mark the n-api reference for garbage collection immediately while still on the main thread. | ||
|
||
In the event that a `Persistent` is dropped when not executing on the main thread, the destruction must be scheduled by a global `EventQueue`. | ||
|
||
### `neon::handle::EventQueue` | ||
|
||
```rust | ||
impl EventQueue { | ||
/// Creates an unbounded queue | ||
pub fn new<'a, C: Context<'a>>(cx: &mut C) -> Self; | ||
|
||
/// Creates a bounded queue | ||
pub fn with_capacity<'a, C: Context<'a>>(cx: &mut C, size: usize) -> Self; | ||
|
||
/// Decrements the strong count on the underlying function. | ||
/// When the count reaches zero, the event loop will not be kept running. | ||
/// An unreferenced `EventQueue` may not execute submitted functions. | ||
/// _Idempotent_ | ||
pub fn unref(&mut self); | ||
|
||
/// Increments the strong count on the underlying function. | ||
/// If the count is greater than zero, the event loop will be kept running. | ||
/// _Idempotent_ | ||
pub fn ref(&mut self); | ||
|
||
/// Schedules a closure to execute on the main javascript thread | ||
/// Blocks if the event queue is full | ||
pub fn send( | ||
&self, | ||
f: impl FnOnce(TaskContext) -> NeonResult<()>, | ||
) -> Result<(), EventQueueError>; | ||
|
||
/// Schedules a closure to execute on the main javascript thread | ||
/// Non-blocking | ||
pub fn try_send( | ||
&self, | ||
f: impl FnOnce(TaskContext) -> NeonResult<()>, | ||
) -> Result<(), EventQueueError>; | ||
} | ||
``` | ||
|
||
The native `napi_call_threadsafe_function` can fail in three ways: | ||
|
||
* `napi_queue_full` if the queue is full | ||
* `napi_invalid_arg` if the thread count is zero. This is due to misuse and statically enforced by ownership rules and the `Drop` trait. | ||
* `napi_generic_failure` if the call to `uv_async_send` fails | ||
|
||
These three failures may be reduced to two and are represented by the `EventQueueError<F>` enum. | ||
|
||
```rust | ||
enum EventQueueError<F> { | ||
/// On a bounded queue, indicates that the queue is full. This variant returns | ||
/// the closure back for re-scheduling. | ||
QueueFull(F), | ||
|
||
/// Indicates a generic failure. Most likely on `uv_async_send(&async)` | ||
Unknown, | ||
} | ||
``` | ||
|
||
In the event of an `napi_invalid_arg` error, neon will `panic` as this indicates a bug in neon and not an expected error condition. | ||
|
||
#### Design Notes | ||
|
||
The backing `napi_threadsafe_function` is already `Send` and `Sync`, allowing the use of `send` with a shared reference. | ||
|
||
It is possible to implement `Clone` for `EventQueue` using `napi_acquire_threadsafe_function`; however, it was considered more idiomatic and flexible to leave this to users with Rust reference counting. E.g., `Arc<EventQueue>`. | ||
|
||
Both `deref` and `ref` mutate the behavior of the underlying threadsafe function and require exclusive references. These methods are infallible because Rust is capable of statically enforcing the invariants. We may want to optimize with `assert_debug!` on the `napi_status`. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
 | ||
|
||
Neon already has `Task`, `EventHandler`, a [proposed](https://github.com/neon-bindings/rfcs/pull/30) `TaskBuilder` and an accepted, but currently unimplemented, [update](https://github.com/neon-bindings/rfcs/pull/28) to `EventHandler`. This is a large amount of API surface area without clear indication of what a user should use. | ||
|
||
This can be mitigated with documentation and soft deprecation of existing methods as we get a clearer picture of what a high-level, ergonomic API would look like. | ||
|
||
# Rationale and alternatives | ||
[alternatives]: #alternatives | ||
|
||
These are fairly thin RAII wrappers around N-API primitives. The most compelling alternatives are continuing to improve the existing high level APIs. No other designs were considered. | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
- Should this be implemented for the legacy runtime or only n-api? | ||
- Should `EventQueue` be `Arc` wrapped internally to encourage users to share instances across threads? | ||
- A global `EventQueue` is necessary for dropping `Persistent`. Should this be exposed? | ||
- The global `EventQueue` requires instance data. Are we okay using an [experimental](https://nodejs.org/api/n-api.html#n_api_environment_life_cycle_apis) API? | ||
- Should `EventQueue::send` accept a closure that returns `()` instead of `NeonResult<()>`? In most cases, the user will want to allow `Throw` to become an `uncaughtException` instead of a `panic` and `NeonResult<()>` provides a small ergonomics improvement. | ||
- `JsBox` is a related feature that will combine for powerful APIs, but is out of scope for this RFC |