Skip to content

Commit 633def6

Browse files
committed
Fix aborrt controller
1 parent 6fa3d40 commit 633def6

File tree

2 files changed

+97
-198
lines changed

2 files changed

+97
-198
lines changed
Lines changed: 93 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
use std::{
2-
cell::Ref,
3-
mem,
4-
sync::{Arc, OnceLock},
1+
use std::sync::{
2+
Arc, OnceLock,
3+
atomic::{AtomicU8, Ordering, fence},
54
};
65

76
use neon::{
@@ -10,7 +9,6 @@ use neon::{
109
prelude::{Context, JsResult},
1110
types::{JsFunction, JsObject, JsValue},
1211
};
13-
use parking_lot::{Mutex, Once};
1412

1513
use super::{BridgeResult, JsCallback, TryIntoJs, errors::IntoThrow as _};
1614

@@ -25,27 +23,61 @@ pub type JsAbortSignal = JsValue;
2523
/// The JS counterpart object is lazily instantiated when the signal gets converted to JS (through
2624
/// the `TryIntoJs` trait); this ensures that the Rust side can be created without a JS `Context`.
2725
pub struct AbortController {
28-
inner: AbortControllerInner,
26+
inner: Arc<AbortControllerInner>,
2927
drop_abort_reason: String,
3028
}
3129

3230
/// An object that models the signal of a JavaScript `AbortController`.
3331
pub struct AbortSignal {
34-
inner: AbortControllerInner,
32+
inner: Arc<AbortControllerInner>,
3533
}
3634

37-
/// The inner state of an `AbortController`, shared between the Rust's controller and its signal.
38-
type AbortControllerInner = Arc<Mutex<AbortControllerInnerState>>;
35+
/// The inner state of an `AbortController`, shared between the Rust and JS sides.
36+
struct AbortControllerInner {
37+
// The fact that we require a `Context` to initialize the JS counterpart means that we are running
38+
// on the Node's thread, which guarantees that there can't be multiple threads calling into that
39+
// function concurrently; that should in theory aleviate the need to use a lock on `js_counterpart`.
40+
//
41+
// It is however possible for the rust-side controller to get aborted from a non-Node thread
42+
// while the JS-side controller is being created on the Node thread, in which case we don't
43+
// want the Rust-side thread to get blocked for the JS-side to complete instantiation.
44+
//
45+
// By modelling the "JS initialization" and "is aborted" states as two distinct independant
46+
// structures, we ensure that we're never blocking execution of either thread. This however
47+
// means that either step may happen before the other, so we need to be careful not to miss
48+
// sending the abort signal. The good news is that nothing bad will happen if we call the JS
49+
// abort callback multiple times.
50+
js_counterpart: OnceLock<AbortControllerJsCounterpart>,
51+
aborted: OnceLock<String>,
52+
53+
state: AtomicU8,
54+
}
55+
56+
struct AbortControllerJsCounterpart {
57+
controller: Root<JsObject>,
58+
abort: JsCallback<(String,), ()>,
59+
}
60+
61+
const STATE_UNINITIALIZED: u8 = 0;
62+
const STATE_ARMED: u8 = 1;
63+
const STATE_ABORTED: u8 = 2;
64+
const STATE_DISARMED: u8 = 3;
3965

4066
impl AbortController {
41-
/// Create a new `AbortController` and `AbortSignal` pair.
67+
/// Create a new `AbortController`.
4268
///
4369
/// The `drop_abort_reason` string will be used as the reason for the abort
4470
/// if the controller is dropped from the Rust side.
4571
#[must_use]
4672
pub fn new(drop_abort_reason: String) -> Self {
73+
let inner = AbortControllerInner {
74+
js_counterpart: OnceLock::new(),
75+
aborted: OnceLock::new(),
76+
state: AtomicU8::new(STATE_UNINITIALIZED),
77+
};
78+
let inner = Arc::new(inner);
4779
Self {
48-
inner: Arc::new(Mutex::new(AbortControllerInnerState::Uninitialized)),
80+
inner: inner.clone(),
4981
drop_abort_reason,
5082
}
5183
}
@@ -54,7 +86,7 @@ impl AbortController {
5486
/// be called at any time (i.e. even after the controller has been cancelled),
5587
/// any number of times (i.e. to pass a same signal to multiple JS functions),
5688
/// and from any thread.
57-
pub fn signal(&self) -> AbortSignal {
89+
pub fn get_signal(&self) -> AbortSignal {
5890
AbortSignal {
5991
inner: self.inner.clone(),
6092
}
@@ -65,12 +97,7 @@ impl AbortController {
6597
/// This method can be called at any time (i.e. even before the controller has been armed,
6698
/// or after it has been disarmed), and from any thread.
6799
pub fn abort(&self, reason: impl Into<String>) {
68-
let call_abort = self.inner.lock().abort(reason);
69-
if let Some(call_abort) = call_abort {
70-
call_abort
71-
.abort_cb
72-
.call_on_js_thread((call_abort.abort_reason,));
73-
}
100+
self.inner.abort(reason);
74101
}
75102

76103
/// Disarm the controller, so that it can no longer be aborted.
@@ -80,7 +107,7 @@ impl AbortController {
80107
/// on the JS side, e.g. when the called JS function has returned, as this will prevent the
81108
/// overhead of implicit abortion when the controller is dropped.
82109
pub fn disarm(&self) {
83-
self.inner.lock().disarm();
110+
self.inner.disarm();
84111
}
85112
}
86113

@@ -95,220 +122,89 @@ impl Drop for AbortController {
95122
impl TryIntoJs for AbortSignal {
96123
type Output = JsAbortSignal;
97124
fn try_into_js<'cx>(self, cx: &mut impl Context<'cx>) -> JsResult<'cx, JsAbortSignal> {
98-
self.inner.get_signal(cx).into_throw(cx)
125+
let controller = self.inner.get_or_init_controller(cx).into_throw(cx)?;
126+
controller.get(cx, "signal")
99127
}
100128
}
101129

102-
////////////////////////////////////////////////////////////////////////////////////////////////////
103-
104130
impl AbortControllerInner {
105131
/// Create the JS `AbortController` if it hasn't been created yet.
106132
/// Returns a reference to the signal object that can be passed to JS.
107-
fn get_js_controller<'cx, C: Context<'cx>>(
133+
fn get_or_init_controller<'cx, C: Context<'cx>>(
108134
&self,
109135
cx: &mut C,
110136
) -> BridgeResult<Handle<'cx, JsAbortController>> {
111-
// Not using `get_or_init` here because initialization of JS classes can theoretically throw.
112-
// But anyway, having a `Context` implies that we are running on the single Node's thread,
113-
// so there's no risk of having two threads racing to initialize the JS `AbortController`.
114-
if let Some(js_controller) = self.state.get_controller() {
115-
return Ok(js_controller.to_inner(cx));
137+
if let Some(js_counterpart) = self.js_counterpart.get() {
138+
// Already initialized, return the controller
139+
return Ok(js_counterpart.controller.to_inner(cx).upcast());
116140
}
117141

118-
// Instantiate the JS AbortController counterpart
142+
// Not initialized yet, create the JS AbortController
119143
let global = cx.global_object();
120144
let abort_controller_class = global.get::<JsFunction, _, _>(cx, "AbortController")?;
121145
let js_controller = abort_controller_class.construct(cx, [])?;
122146

123-
// Get the JS `abort` method and make a callback out of it
124147
let abort_fn = js_controller.get::<JsFunction, _, _>(cx, "abort")?;
125148
let abort_cb = JsCallback::new(cx, abort_fn, Some(js_controller));
126149

127-
let js_controller = js_controller.root(cx);
128-
let js_controller = match self.js_controller.set(js_controller) {
129-
Ok(()) => {
130-
// We confirmed instantiation of the JS counterpart; now update the state machine
131-
let mut state = self.state.lock();
132-
match *state {
133-
AbortControllerInnerState::Uninitialized => {
134-
*state = AbortControllerInnerState::Armed(abort_cb);
135-
}
136-
AbortControllerInnerState::AbortedBeforeArmed(reason) => {
137-
// Controller was aborted before the JS counterpart was initialized, so we must
138-
// immediately and synchronously propagate the abort to the JS controller object.
139-
140-
// ... but let's not hold on to the state mutex while calling the abort callback
141-
*state = AbortControllerInnerState::Disarmed;
142-
mem::drop(state);
150+
let js_counterpart = AbortControllerJsCounterpart {
151+
controller: js_controller.root(cx),
152+
abort: abort_cb,
153+
};
143154

144-
// Call the abort callback
145-
let _ = abort_cb.call(cx, (reason,));
146-
}
147-
AbortControllerInnerState::Armed(_) => {
148-
panic!(
149-
"AbortController: Incoherent state:JS AbortController already initialized"
150-
);
151-
}
152-
_ => {}
155+
let controller = match self.js_counterpart.set(js_counterpart) {
156+
Ok(()) => {
157+
// Ordering: Write to `state` implies previous write to `js_counterpart` is visible to other threads
158+
if self.state.fetch_or(STATE_ARMED, Ordering::Release) == STATE_ABORTED {
159+
// Ordering: Previous concurrent write to `aborted` must be visible at this point
160+
fence(Ordering::Acquire);
161+
162+
// The controller was aborted before it was armed; immediately call the abort callback
163+
164+
// Fire and forget
165+
let _ = self
166+
.js_counterpart
167+
.get()
168+
.unwrap()
169+
.abort
170+
.call(cx, (self.aborted.get().unwrap().clone(),));
153171
}
154-
155-
// If the Rust controller has already been aborted, call the JS abort callback now
156-
// if let Some(aborted) = self.aborted.get() {
157-
// // Fire and forget
158-
// let _ = js_counterpart.abort.call_on_js_thread((aborted.clone(),));
159-
// }
160-
js_controller.clone()
161-
}
162-
Err(js_controller) => {
163-
// That case is totally unexpected, but should that ever happen, it's ok to just
164-
// return the existing JS controller; the JS objects we just created have not
165-
// yet been exposed anyway, so they will simply get garbage collected.
166-
js_controller.clone()
172+
js_controller
167173
}
174+
Err(js_counterpart) => js_counterpart.controller.to_inner(cx).upcast(),
168175
};
169176

170-
Ok(js_controller.to_inner(cx))
171-
}
172-
173-
/// Get a JS `AbortSignal` object for this `AbortController`. Initializes the
174-
/// JS `AbortController` if it hasn't been initialized yet.
175-
fn get_signal<'cx, C: Context<'cx>>(
176-
&self,
177-
cx: &mut C,
178-
) -> BridgeResult<Handle<'cx, JsAbortSignal>> {
179-
let abort_controller = self.get_js_controller(cx)?;
180-
Ok(abort_controller.get(cx, "signal")?)
177+
Ok(controller)
181178
}
182179

183180
/// Immediately abort the `AbortController`, causing the JS side `signal` to fire.
184-
/// The controller is marked as aborted even if the JS counterpart has not yet been initialized;
185-
/// in that case, the abort signal will be fired as soon as the JS side controller is initialized.
186-
///
187-
/// This method can be called from any thread.
188181
fn abort(&self, reason: impl Into<String>) {
189182
let reason = reason.into();
190183
if self.aborted.set(reason.clone()) == Ok(()) {
191184
// If we haven't created the JS AbortController yet, there's nothing to abort
192-
// VALIDATE: Do we need a memory barrier here to ensure that js_counterpart and aborted are coherent?
193-
if let Some(js_counterpart) = self.js_counterpart.get() {
194-
// Fire and forget
195-
let _ = js_counterpart.abort.call_on_js_thread((reason,));
196-
}
197-
}
198-
}
199-
}
200-
201-
////////
185+
// Ordering: Write to `state` implies previous write to `aborted` is visible to other threads
186+
if self.state.fetch_or(STATE_ABORTED, Ordering::Release) == STATE_ARMED {
187+
// Ordering: Previous concurrent write to `js_counterpart` must be visible at this point
188+
fence(Ordering::Acquire);
202189

203-
struct AbortControllerInnerState2 {
204-
//
205-
js_controller: Option<Root<JsAbortController>>,
206-
abort_cb: Option<JsCallback<(String,), ()>>,
207-
}
208-
209-
enum AbortControllerInnerState {
210-
/// The `AbortController` has just been created; the JS counterpart has not yet been initialized.
211-
///
212-
/// Transitions:
213-
/// - Calling `abort()` will move to the `AbortedBeforeArmed` state
214-
/// - Initializing the JS counterpart will move to the `Armed` state
215-
Uninitialized,
216-
217-
/// The `AbortController` has been aborted before the JS counterpart was initialized.
218-
///
219-
/// Transitions:
220-
/// - Calling `abort()` is a no-op
221-
/// - Initializing the JS counterpart will result in calling the abort
222-
/// callback, and move to the `Disabled` state
223-
AbortedBeforeArmed(String),
224-
225-
/// The JS counterpart has been initialized.
226-
///
227-
/// Transitions:
228-
/// - Calling `abort()` will result in calling the abort callback, and move to the `Disabled` state
229-
/// - Calling `disable()` is a no-op
230-
Armed(Root<JsAbortController>, JsCallback<(String,), ()>),
231-
232-
/// The `AbortController` has been disarmed, though it was actually never armed.
233-
///
234-
/// Transitions:
235-
/// - Calling `abort()` is a no-op
236-
/// - Initializing the JS counterpart will move to the `Disarmed` state
237-
UninitializedDisarmed,
238-
239-
/// The `AbortController` has been disabled, either by being aborted or by calling `disable()`.
240-
/// It is still possible to obtain a `signal` from the controller, but it's abort status
241-
/// will not change anymore.
242-
///
243-
/// Any transitions from this state is a no-op.
244-
Disarmed(Root<JsAbortController>),
245-
}
246-
247-
impl AbortControllerInnerState {
248-
fn arm(
249-
&mut self,
250-
js_controller: Root<JsAbortController>,
251-
abort_cb: JsCallback<(String,), ()>,
252-
) -> Option<CallAbortDetails> {
253-
match self {
254-
Self::Uninitialized => {
255-
*self = Self::Armed(js_controller, abort_cb);
256-
None
257-
}
258-
Self::AbortedBeforeArmed(abort_reason) => {
259-
let abort_reason = abort_reason.clone();
260-
*self = Self::Disarmed(js_controller);
261-
Some(CallAbortDetails {
262-
abort_reason,
263-
abort_cb,
264-
})
265-
}
266-
Self::UninitializedDisarmed => {
267-
*self = Self::Disarmed(js_controller);
268-
None
269-
}
270-
_ => None,
271-
}
272-
}
273-
274-
fn abort(&mut self, reason: impl Into<String>) -> Option<CallAbortDetails> {
275-
let current = mem::replace(self, Self::Uninitialized);
276-
match current {
277-
Self::Uninitialized => {
278-
*self = Self::AbortedBeforeArmed(reason.into());
279-
None
280-
}
281-
Self::Armed(js_controller, abort_cb) => {
282-
*self = Self::Disarmed(js_controller);
283-
Some(CallAbortDetails {
284-
abort_reason: reason.into(),
285-
abort_cb,
286-
})
287-
}
288-
other => {
289-
*self = other;
290-
None
190+
// Fire and forget
191+
let _ = self
192+
.js_counterpart
193+
.get()
194+
.unwrap()
195+
.abort
196+
.call_on_js_thread((reason,));
291197
}
292198
}
293199
}
294200

295-
fn disarm(&mut self) {
296-
let current = mem::replace(self, Self::Uninitialized);
297-
match current {
298-
Self::Uninitialized => {
299-
*self = Self::UninitializedDisarmed;
300-
}
301-
Self::Armed(js_controller, _) => {
302-
*self = Self::Disarmed(js_controller);
303-
}
304-
other => {
305-
*self = other;
306-
}
307-
}
201+
fn disarm(&self) {
202+
// Ordering: this requires no dependency on any other state
203+
self.state.store(STATE_DISARMED, Ordering::Relaxed);
308204
}
309205
}
310206

311-
struct CallAbortDetails {
312-
abort_reason: String,
313-
abort_cb: JsCallback<(String,), ()>,
207+
#[cfg(feature = "test")]
208+
mod tests {
209+
use super::*;
314210
}

0 commit comments

Comments
 (0)