-
Notifications
You must be signed in to change notification settings - Fork 257
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
lightclient: Fix wasm socket closure called after being dropped #1289
Changes from 8 commits
f3eb4cd
cfac283
860f0e0
8660940
b2e6d41
78f4d88
1cdee8f
bd13879
099f6ee
abe77b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,10 +63,10 @@ struct InnerWasmSocket { | |
/// | ||
/// These need to be kept around until the socket is dropped. | ||
type Callbacks = ( | ||
Closure<dyn FnMut()>, | ||
JsValue, | ||
Closure<dyn FnMut(web_sys::MessageEvent)>, | ||
Closure<dyn FnMut(web_sys::Event)>, | ||
Closure<dyn FnMut(web_sys::CloseEvent)>, | ||
JsValue, | ||
JsValue, | ||
); | ||
|
||
impl WasmSocket { | ||
|
@@ -89,7 +89,7 @@ impl WasmSocket { | |
waker: None, | ||
})); | ||
|
||
let open_callback = Closure::<dyn FnMut()>::new({ | ||
let open_callback = Closure::once_into_js({ | ||
let inner = inner.clone(); | ||
move || { | ||
let mut inner = inner.lock().expect("Mutex is poised; qed"); | ||
|
@@ -120,9 +120,9 @@ impl WasmSocket { | |
}); | ||
socket.set_onmessage(Some(message_callback.as_ref().unchecked_ref())); | ||
|
||
let error_callback = Closure::<dyn FnMut(_)>::new({ | ||
let error_callback = Closure::once_into_js({ | ||
let inner = inner.clone(); | ||
move |_| { | ||
move |_event: web_sys::Event| { | ||
// Callback does not provide useful information, signal it back to the stream. | ||
let mut inner = inner.lock().expect("Mutex is poised; qed"); | ||
inner.state = ConnectionState::Error; | ||
|
@@ -134,9 +134,9 @@ impl WasmSocket { | |
}); | ||
socket.set_onerror(Some(error_callback.as_ref().unchecked_ref())); | ||
|
||
let close_callback = Closure::<dyn FnMut(_)>::new({ | ||
let close_callback = Closure::once_into_js({ | ||
let inner = inner.clone(); | ||
move |_| { | ||
move |_event: web_sys::CloseEvent| { | ||
let mut inner = inner.lock().expect("Mutex is poised; qed"); | ||
inner.state = ConnectionState::Closed; | ||
|
||
|
@@ -171,6 +171,10 @@ impl AsyncRead for WasmSocket { | |
let mut inner = self.inner.lock().expect("Mutex is poised; qed"); | ||
inner.waker = Some(cx.waker().clone()); | ||
|
||
if self.socket.ready_state() == web_sys::WebSocket::CONNECTING { | ||
return Poll::Pending; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I think this is ook because any of the callbacks will wake the waker when JS clals them, ensuring we call this |
||
} | ||
|
||
match inner.state { | ||
ConnectionState::Error => { | ||
Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, "Socket error"))) | ||
|
@@ -221,16 +225,24 @@ impl AsyncWrite for WasmSocket { | |
Poll::Ready(Ok(())) | ||
} | ||
|
||
fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), io::Error>> { | ||
Poll::Ready(Ok(())) | ||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> { | ||
if self.socket.ready_state() == web_sys::WebSocket::CLOSED { | ||
return Poll::Ready(Ok(())); | ||
} | ||
|
||
if self.socket.ready_state() != web_sys::WebSocket::CLOSING { | ||
let _ = self.socket.close(); | ||
} | ||
|
||
let mut inner = self.inner.lock().expect("Mutex is poised; qed"); | ||
inner.waker = Some(cx.waker().clone()); | ||
Poll::Pending | ||
} | ||
} | ||
|
||
impl Drop for WasmSocket { | ||
fn drop(&mut self) { | ||
let inner = self.inner.lock().expect("Mutex is poised; qed"); | ||
|
||
if inner.state == ConnectionState::Opened { | ||
if self.socket.ready_state() == web_sys::WebSocket::OPEN { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be safer to say There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've extended this to do clean-up for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Attempting to close the socket regardless of its state will generate on WebSocket connection to 'ws://polkadot-boot.dwellir.com:30334/' failed: WebSocket is closed before the connection is established. The same error is generated if the socket is in the |
||
let _ = self.socket.close(); | ||
} | ||
} | ||
|
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.
The docs for
once_into_js
say:So, since some of these callbacks might never fire, I think we'd be gradually leaking them here for any connection we make that doesn't fire one of the callbacks.
I wonder if the errors we got happened because previously, if
Self
is dropped before one of the JS websocket callbacks is fired, then it would have decallocated the closures and we'd get an error in JS land instead.But anyway, from reading the spec, it sounds like we:
onopen
if connection can't be opened (but will fire it max once)onerror
oronmessage
if no errors/messages sent (not certain ifonerror
will only fire once or not)onclose
(max once).So if I read right, I wonder whether we can do some cleanup in
onclose
closure to ensure that all of the other closures are dropped. (egClosure::new()
for them all, save theClosures
, anddrop()
them all whenonclose
fires or something like that)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.
That makes sense!
I've found another way, which I believe to be more straight forward than performing clean-up on the
onclose
:Closure::new()
around for each callbackThis shall clean-up all memory associated with the rust closures, and deny the javascript real access to the dropped objects