Skip to content

Commit fdfa972

Browse files
committed
Threadsafe Handles
1 parent 88e2f13 commit fdfa972

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed

text/0000-thread-safe-handles.md

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
- Feature Name: Threadsafe Handles
2+
- Start Date: 2020-07-21
3+
- RFC PR: (leave this empty)
4+
- Neon Issue: (leave this empty)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
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.
10+
11+
* `EventQueue`: Threadsafe handle that allows sending closures to be executed on the main thread.
12+
* `Persistent<T>`: Opaque handle that can be sent across threads, but only dereferenced on the main thread.
13+
14+
# Motivation
15+
[motivation]: #motivation
16+
17+
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).
18+
19+
The Threadsafe Handlers feature attempts to decompose the high level `EventHandler` API into lower level, more flexible primitives.
20+
21+
These primitives can be used to build a higher level and _safe_ `EventHandler` API allowing experimentation with patterns outside of `neon`.
22+
23+
# Guide-level explanation
24+
[guide-level-explanation]: #guide-level-explanation
25+
26+
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`.
27+
28+
### `neon::handle::Persistent<T>`
29+
30+
As a developer, I may want to retain a reference to a JavaScript value while returning control back to the VM.
31+
32+
`Handle<'_, _>` may not outlive the context that created them.
33+
34+
```rust
35+
// Does not compile because `cb` cannot be sent across threads
36+
fn thread_log(mut cx: FunctionContext) -> JsResult<JsUndefined> {
37+
let cb = cx.argument::<JsFunction>(0)?;
38+
39+
std::thread::spawn(move || {
40+
println!("{:?}", cb);
41+
});
42+
43+
cx.undefined()
44+
}
45+
```
46+
47+
However, a persistent reference may be created from a `Handle<'_, _>` which can be sent across threads.
48+
49+
```rust
50+
// Compiles
51+
fn thread_log(mut cx: FunctionContext) -> JsResult<JsUndefined> {
52+
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?;
53+
54+
std::thread::spawn(move || {
55+
println!("{:?}", cb);
56+
});
57+
58+
cx.undefined()
59+
}
60+
```
61+
62+
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.
63+
64+
```rust
65+
fn log(mut cx: FunctionContext) -> JsResult<JsUndefined> {
66+
let persistent_cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?;
67+
let cb = persistent_cb.deref(&cx);
68+
69+
println!("{:?}", cb);
70+
71+
cx.undefined()
72+
}
73+
```
74+
75+
### `neon::handle::EventQueue`
76+
77+
Once a value is wrapped in a `Persistent<_>` handle, it must be sent back to the main JavaScript thread to dereference. `EventQueue` provides a mechanism for requesting work be peformed on the main thread.
78+
79+
To schedule work, send a closure to the event queue:
80+
81+
```rust
82+
fn thread_callback(mut cx: FunctionContext) -> JsResult<JsUndefined> {
83+
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?;
84+
let queue = EventQueue::new(&mut cx)?;
85+
86+
std::thread::spawn(move || {
87+
queue.send(move |mut cx| {
88+
let this = cx.undefined();
89+
let msg = cx.string("Hello, World!");
90+
let cb = cb.deref(&cx).unwrap();
91+
92+
cb.call(&mut cx, this, vec![msg]).unwrap();
93+
});
94+
});
95+
96+
cx.undefined()
97+
}
98+
```
99+
100+
`Persistent<_>` and `EventQueue` are clone-able and can be used many times.
101+
102+
```rust
103+
fn thread_callback(mut cx: FunctionContext) -> JsResult<JsUndefined> {
104+
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?;
105+
let queue = EventQueue::new(&mut cx)?;
106+
107+
for i in 1..=10 {
108+
let queue = queue.clone();
109+
let cb = cb.clone();
110+
111+
std::thread::spawn(move || {
112+
queue.send(move |mut cx| {
113+
let this = cx.undefined();
114+
let msg = cx.string(format!("Count: {}", i));
115+
let cb = cb.deref(&cx).unwrap();
116+
117+
cb.call(&mut cx, this, vec![msg]).unwrap();
118+
});
119+
});
120+
}
121+
122+
cx.undefined()
123+
}
124+
```
125+
126+
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.
127+
128+
However, calls to `EventQueue::schedule` _might_ not execute before the process exits.
129+
130+
```rust
131+
fn thread_callback(mut cx: FunctionContext) -> JsResult<JsUndefined> {
132+
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx)?;
133+
let mut queue = EventQueue::new(&mut cx)?;
134+
135+
queue.unref();
136+
137+
std::thread::spawn(move || {
138+
std::thread::sleep(std::time::Duration::from_secs(1));
139+
140+
// If the event queue is empty, the process may exit before this executes
141+
queue.send(move |mut cx| {
142+
let this = cx.undefined();
143+
let msg = cx.string("Hello, World!");
144+
let cb = cb.deref(&cx).unwrap();
145+
146+
cb.call(&mut cx, this, vec![msg]).unwrap();
147+
});
148+
});
149+
150+
cx.undefined()
151+
}
152+
```
153+
154+
# Reference-level explanation
155+
[reference-level-explanation]: #reference-level-explanation
156+
157+
### `neon::handle::Persistent<T: Object>`
158+
159+
`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.
160+
161+
_`Persistent` handles may only be used for `Object` and `Function` types. This is a limitation of `n-api`._
162+
163+
```rust
164+
/// `Send + Sync` Persistent handle to JavaScript objects
165+
impl Persistent<T: Object> {
166+
pub fn new<'a, C: Context<'a>>(
167+
cx: &mut C,
168+
v: Handle<T>,
169+
) -> NeonResult<Self>;
170+
171+
pub fn deref<'a, C: Context<'a>>(
172+
self,
173+
cx: &mut C,
174+
) -> JsResult<'a, T>;
175+
}
176+
```
177+
178+
#### Design Notes
179+
180+
`Persistent` utilize an atomic reference count to track when they can be safely dropped. There are two circumstances where a `Persistent` may be dropped:
181+
182+
* `Persistent` is no longer held. For example, if it is a member of a struct that is dropped.
183+
* `Persistent::deref` is called and it decrements the count to zero.
184+
185+
`Persistent::deref` takes ownership of `self` to allow an optimization in the most common usage. In the case of a `Persistent` being dropped after a call to `deref`, neon may immediately mark the n-api reference for garbage collection while the main thread is still available.
186+
187+
In the event that a `Persistent` is dropped when not executing on the main thread, the destruction must be scheduled by a global `EventQueue`.
188+
189+
### `neon::handle::EventQueue`
190+
191+
```rust
192+
impl EventQueue {
193+
/// Creates an unbounded queue
194+
pub fn new<'a, C: Context<'a>>(cx: &mut C) -> Self;
195+
196+
/// Creates a bounded queue
197+
pub fn with_capacity<'a, C: Context<'a>>(cx: &mut C, size: usize) -> Self;
198+
199+
/// Decrements the strong count on the underlying threadsafe function.
200+
/// When the count reaches zero, the event loop will not be kept running.
201+
/// An unreferenced `EventQueue` may not execute submitted functions.
202+
/// _Idempotent_
203+
pub fn unref(&mut self);
204+
205+
/// Increments the strong count on the underlying function.
206+
/// If the count is greater than zero, the event loop will be kept running.
207+
/// _Idempotent_
208+
pub fn ref(&mut self);
209+
210+
/// Schedules a closure to execute on the main JavaScript thread
211+
/// Blocks if the event queue is full
212+
pub fn send(
213+
&self,
214+
f: impl FnOnce(TaskContext) -> NeonResult<()>,
215+
) -> Result<(), EventQueueError>;
216+
217+
/// Schedules a closure to execute on the main JavaScript thread
218+
/// Non-blocking
219+
pub fn try_send(
220+
&self,
221+
f: impl FnOnce(TaskContext) -> NeonResult<()>,
222+
) -> Result<(), EventQueueError>;
223+
}
224+
```
225+
226+
The native `napi_call_threadsafe_function` can fail in three ways:
227+
228+
* `napi_queue_full` if the queue is full
229+
* `napi_invalid_arg` if the thread count is zero. This is due to misuse and statically enforced by ownership rules and the `Drop` trait.
230+
* `napi_generic_failure` if the call to `uv_async_send` fails
231+
232+
These three failures may be reduced to two and are represented by the `EventQueueError<F>` enum.
233+
234+
```rust
235+
enum EventQueueErrpub trait JsResultExt<'a, V: Value> {
236+
fn or_throw<'b, C: Context<'b>>(self, cx: &mut C) -> JsResult<'a, V>;
237+
}a generic failure. Most likely on `uv_async_send(&async)`
238+
Unknown,
239+
}
240+
```
241+
242+
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.
243+
244+
#### Design Notes
245+
246+
The backing `napi_threadsafe_function` is already `Send` and `Sync`, allowing the use of `send` with a shared reference.
247+
248+
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>`.
249+
250+
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`.
251+
252+
# Drawbacks
253+
[drawbacks]: #drawbacks
254+
255+
![standards](https://imgs.xkcd.com/comics/standards.png)
256+
257+
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.
258+
259+
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.
260+
261+
# Rationale and alternatives
262+
[alternatives]: #alternatives
263+
264+
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.
265+
266+
# Unresolved questions
267+
[unresolved]: #unresolved-questions
268+
269+
- ~Should this be implemented for the legacy runtime or only n-api?~ Only for `n-api`.
270+
- ~Should `EventQueue` be `Arc` wrapped internally to encourage users to share instances across threads?~ No. This can be covered in documentation and matches the abstraction of neon's proposed [`JsBox`](https://github.com/neon-bindings/rfcs/pull/33) and [`tokio::runtime::Runtime`](https://docs.rs/tokio/0.1.22/tokio/runtime/struct.Runtime.html).
271+
- ~A global `EventQueue` is necessary for dropping `Persistent`. Should this be exposed?~ No. This can be a follow-up RFC.
272+
- 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? We can also use `thread_local` and an `unref` `EventQueue`.
273+
- 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.
274+
- `persistent(&mut cx)` is a little difficult to type. Should it have a less complicated name?

0 commit comments

Comments
 (0)