-
Notifications
You must be signed in to change notification settings - Fork 173
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
fix(websocket client): drop subscriptions that can't keep up with the internal buffer size #166
Changes from all commits
9868f88
15cae68
29beb50
f50becd
213e966
2f72354
0247463
64e480f
b5392d2
4dd85a5
9a44b4a
29ffcd8
f82277f
7678ed4
2645f96
488d626
bba1594
3e94f6a
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 |
---|---|---|
|
@@ -24,15 +24,20 @@ | |
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||
// DEALINGS IN THE SOFTWARE. | ||
|
||
use crate::client::ws::{RawClient, RawClientEvent, RawClientRequestId, WsTransportClient}; | ||
use crate::client::ws::transport::WsConnectError; | ||
use crate::client::ws::{RawClient, RawClientError, RawClientEvent, RawClientRequestId, WsTransportClient}; | ||
use crate::types::error::Error; | ||
use crate::types::jsonrpc::{self, JsonValue}; | ||
// NOTE: this is a sign of a leaky abstraction to expose transport related details | ||
// Should be removed after https://github.com/paritytech/jsonrpsee/issues/154 | ||
use soketto::connection::Error as SokettoError; | ||
|
||
use futures::{ | ||
channel::{mpsc, oneshot}, | ||
future::Either, | ||
pin_mut, | ||
prelude::*, | ||
sink::SinkExt, | ||
}; | ||
use std::{collections::HashMap, io, marker::PhantomData}; | ||
|
||
|
@@ -45,6 +50,29 @@ use std::{collections::HashMap, io, marker::PhantomData}; | |
pub struct Client { | ||
/// Channel to send requests to the background task. | ||
to_back: mpsc::Sender<FrontToBack>, | ||
/// Config. | ||
config: Config, | ||
} | ||
|
||
#[derive(Copy, Clone, Debug)] | ||
/// Configuration. | ||
pub struct Config { | ||
/// Backend channel for serving requests and notifications. | ||
pub request_channel_capacity: usize, | ||
/// Backend channel for each unique subscription. | ||
pub subscription_channel_capacity: usize, | ||
/// Max request body size | ||
pub max_request_body_size: usize, | ||
} | ||
|
||
impl Default for Config { | ||
fn default() -> Self { | ||
Self { | ||
request_channel_capacity: 100, | ||
subscription_channel_capacity: 4, | ||
max_request_body_size: 10 * 1024 * 1024, | ||
} | ||
} | ||
} | ||
|
||
/// Active subscription on a [`Client`]. | ||
|
@@ -54,7 +82,7 @@ pub struct Subscription<Notif> { | |
/// Channel from which we receive notifications from the server, as undecoded `JsonValue`s. | ||
notifs_rx: mpsc::Receiver<JsonValue>, | ||
/// Marker in order to pin the `Notif` parameter. | ||
marker: PhantomData<mpsc::Receiver<Notif>>, | ||
marker: PhantomData<Notif>, | ||
} | ||
|
||
/// Message that the [`Client`] can send to the background task. | ||
|
@@ -104,14 +132,16 @@ impl Client { | |
/// Initializes a new WebSocket client | ||
/// | ||
/// Fails when the URL is invalid. | ||
pub async fn new(target: &str) -> Result<Self, Error> { | ||
pub async fn new(target: impl AsRef<str>, config: Config) -> Result<Self, Error> { | ||
let transport = WsTransportClient::new(target).await.map_err(|e| Error::TransportError(Box::new(e)))?; | ||
let client = RawClient::new(transport); | ||
let (to_back, from_front) = mpsc::channel(16); | ||
|
||
let (to_back, from_front) = mpsc::channel(config.request_channel_capacity); | ||
|
||
async_std::task::spawn(async move { | ||
background_task(client, from_front).await; | ||
background_task(client, from_front, config).await; | ||
}); | ||
Ok(Client { to_back }) | ||
Ok(Client { to_back, config }) | ||
} | ||
|
||
/// Send a notification to the server. | ||
|
@@ -145,8 +175,6 @@ impl Client { | |
.await | ||
.map_err(Error::Internal)?; | ||
|
||
// TODO: send a `ChannelClosed` message if we close the channel unexpectedly | ||
|
||
let json_value = match send_back_rx.await { | ||
Ok(Ok(v)) => v, | ||
Ok(Err(err)) => return Err(err), | ||
|
@@ -196,7 +224,6 @@ impl Client { | |
return Err(Error::TransportError(Box::new(err))); | ||
} | ||
}; | ||
|
||
Ok(Subscription { to_back: self.to_back.clone(), notifs_rx, marker: PhantomData }) | ||
} | ||
} | ||
|
@@ -205,18 +232,18 @@ impl<Notif> Subscription<Notif> | |
where | ||
Notif: jsonrpc::DeserializeOwned, | ||
{ | ||
/// Returns the next notification sent from the server. | ||
/// Returns the next notification from the stream | ||
/// This may return `None` if the subscription has been terminated, may happen if the channel becomes full or dropped. | ||
/// | ||
/// Ignores any malformed packet. | ||
niklasad1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pub async fn next(&mut self) -> Notif { | ||
pub async fn next(&mut self) -> Option<Notif> { | ||
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. This is the major change in this PR. |
||
loop { | ||
match self.notifs_rx.next().await { | ||
Some(n) => { | ||
if let Ok(parsed) = jsonrpc::from_value(n) { | ||
return parsed; | ||
} | ||
} | ||
None => futures::pending!(), | ||
Some(n) => match jsonrpc::from_value(n) { | ||
Ok(parsed) => return Some(parsed), | ||
Err(e) => log::error!("Subscription response error: {:?}", e), | ||
}, | ||
None => return None, | ||
} | ||
} | ||
} | ||
|
@@ -228,17 +255,17 @@ impl<Notif> Drop for Subscription<Notif> { | |
// the channel's buffer will be full, and our unsubscription request will never make it. | ||
// However, when a notification arrives, the background task will realize that the channel | ||
// to the `Subscription` has been closed, and will perform the unsubscribe. | ||
let _ = self.to_back.send(FrontToBack::ChannelClosed).now_or_never(); | ||
let _ = self.to_back.try_send(FrontToBack::ChannelClosed); | ||
} | ||
} | ||
|
||
/// Function being run in the background that processes messages from the frontend. | ||
async fn background_task(mut client: RawClient, mut from_front: mpsc::Receiver<FrontToBack>) { | ||
async fn background_task(mut client: RawClient, mut from_front: mpsc::Receiver<FrontToBack>, config: Config) { | ||
// List of subscription requests that have been sent to the server, with the method name to | ||
// unsubscribe. | ||
let mut pending_subscriptions: HashMap<RawClientRequestId, (oneshot::Sender<_>, _)> = HashMap::new(); | ||
// List of subscription that are active on the server, with the method name to unsubscribe. | ||
let mut active_subscriptions: HashMap<RawClientRequestId, (mpsc::Sender<jsonrpc::JsonValue>, _)> = HashMap::new(); | ||
let mut active_subscriptions: HashMap<RawClientRequestId, (mpsc::Sender<JsonValue>, _)> = HashMap::new(); | ||
// List of requests that the server must answer. | ||
let mut ongoing_requests: HashMap<RawClientRequestId, oneshot::Sender<Result<_, _>>> = HashMap::new(); | ||
|
||
|
@@ -300,68 +327,98 @@ async fn background_task(mut client: RawClient, mut from_front: mpsc::Receiver<F | |
} | ||
} | ||
} | ||
// A subscription has been closed (could be used for requests too.) | ||
Either::Left(Some(FrontToBack::ChannelClosed)) => { | ||
// TODO: there's no way to cancel pending subscriptions and requests, otherwise | ||
// we should clean them up as well | ||
while let Some(rq_id) = active_subscriptions.iter().find(|(_, (v, _))| v.is_closed()).map(|(k, _)| *k) { | ||
let (_, unsubscribe) = active_subscriptions.remove(&rq_id).unwrap(); | ||
client.subscription_by_id(rq_id).unwrap().into_active().unwrap().close(unsubscribe).await.unwrap(); | ||
//TODO: there's no way to cancel pending subscriptions and requests | ||
//TODO: https://github.com/paritytech/jsonrpsee/issues/169 | ||
while let Some(req_id) = active_subscriptions.iter().find(|(_, (v, _))| v.is_closed()).map(|(k, _)| *k) | ||
{ | ||
let (_, unsubscribe) = | ||
active_subscriptions.remove(&req_id).expect("Subscription is active checked above; qed"); | ||
close_subscription(&mut client, req_id, unsubscribe).await; | ||
} | ||
} | ||
|
||
// Received a response to a request from the server. | ||
Either::Right(Ok(RawClientEvent::Response { request_id, result })) => { | ||
log::trace!("[backend] client received response to req={:?}, result={:?}", request_id, result); | ||
let _ = ongoing_requests.remove(&request_id).unwrap().send(result.map_err(Error::Request)); | ||
match ongoing_requests.remove(&request_id) { | ||
Some(r) => { | ||
if let Err(e) = r.send(result.map_err(Error::Request)) { | ||
log::error!("Could not dispatch pending request ID: {:?}, error: {:?}", request_id, e); | ||
} | ||
} | ||
None => log::error!("No pending response found for request ID {:?}", request_id), | ||
} | ||
} | ||
|
||
// Receive a response from the server about a subscription. | ||
// Received a response from the server that a subscription is registered. | ||
Either::Right(Ok(RawClientEvent::SubscriptionResponse { request_id, result })) => { | ||
log::trace!("[backend]: client received response to subscription: {:?}", result); | ||
let (send_back, unsubscribe) = pending_subscriptions.remove(&request_id).unwrap(); | ||
if let Err(err) = result { | ||
let _ = send_back.send(Err(Error::Request(err))); | ||
} else { | ||
// TODO: what's a good limit here? way more tricky than it looks | ||
let (notifs_tx, notifs_rx) = mpsc::channel(4); | ||
let (notifs_tx, notifs_rx) = mpsc::channel(config.subscription_channel_capacity); | ||
|
||
// Send receiving end of `subscription channel` to the frontend | ||
if send_back.send(Ok(notifs_rx)).is_ok() { | ||
active_subscriptions.insert(request_id, (notifs_tx, unsubscribe)); | ||
} else { | ||
client | ||
.subscription_by_id(request_id) | ||
.unwrap() | ||
.into_active() | ||
.unwrap() | ||
.close(unsubscribe) | ||
.await | ||
.unwrap(); | ||
close_subscription(&mut client, request_id, unsubscribe).await; | ||
} | ||
} | ||
} | ||
|
||
// Received a response on a subscription. | ||
Either::Right(Ok(RawClientEvent::SubscriptionNotif { request_id, result })) => { | ||
// TODO: unsubscribe if channel is closed | ||
let (notifs_tx, _) = active_subscriptions.get_mut(&request_id).unwrap(); | ||
if notifs_tx.send(result).await.is_err() { | ||
let (_, unsubscribe) = active_subscriptions.remove(&request_id).unwrap(); | ||
client | ||
.subscription_by_id(request_id) | ||
.unwrap() | ||
.into_active() | ||
.unwrap() | ||
.close(unsubscribe) | ||
.await | ||
.unwrap(); | ||
let notifs_tx = match active_subscriptions.get_mut(&request_id) { | ||
None => { | ||
log::debug!("Invalid subscription response: {:?}", request_id); | ||
continue; | ||
} | ||
Some((notifs_tx, _)) => notifs_tx, | ||
}; | ||
Comment on lines
+375
to
+381
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. This is nice. |
||
|
||
match notifs_tx.try_send(result) { | ||
Ok(()) => (), | ||
// Channel is either full or disconnected, close it. | ||
Err(e) => { | ||
log::error!("Subscription ID: {:?} failed: {:?}", request_id, e); | ||
let (_, unsubscribe) = | ||
active_subscriptions.remove(&request_id).expect("Request is active checked above; qed"); | ||
close_subscription(&mut client, request_id, unsubscribe).await; | ||
} | ||
} | ||
} | ||
|
||
// Request for the server to unsubscribe us has succeeded. | ||
// Request for the server to unsubscribe to us has succeeded. | ||
Either::Right(Ok(RawClientEvent::Unsubscribed { request_id: _ })) => {} | ||
|
||
Either::Right(Err(RawClientError::Inner(WsConnectError::Ws(SokettoError::UnexpectedOpCode(e))))) => { | ||
log::error!( | ||
"Client Error: {:?}, <https://github.com/paritytech/jsonrpsee/issues/154>", | ||
SokettoError::UnexpectedOpCode(e) | ||
); | ||
} | ||
Either::Right(Err(e)) => { | ||
// TODO: https://github.com/paritytech/jsonrpsee/issues/67 | ||
log::error!("Client Error: {:?}", e); | ||
log::error!("Client Error: {:?} terminating connection", e); | ||
break; | ||
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. Unrelated, but it closes the event loop once an error is received. 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. Are the sending ends of the channels going to shut down gracefully when we drop the receivers here? 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. Technically, the senders are not gracefully terminated but once the Thus, not possible for the user to know the exact failure reason without another channel or message (but we have logs lol) EDIT: Then add some similar tests that we have for the 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. Up to you; it's fine to keep here too. |
||
} | ||
} | ||
} | ||
} | ||
|
||
/// Close subscription in RawClient helper. | ||
/// Logs if the subscription couldn't be found. | ||
async fn close_subscription(client: &mut RawClient, request_id: RawClientRequestId, unsubscribe_method: String) { | ||
match client.subscription_by_id(request_id).and_then(|s| s.into_active()) { | ||
Some(mut sub) => { | ||
if let Err(e) = sub.close(&unsubscribe_method).await { | ||
log::error!("RequestID : {:?}, unsubscribe to {} failed: {:?}", request_id, unsubscribe_method, e); | ||
} | ||
} | ||
None => log::error!("Request ID: {:?}, not an active subscription", request_id), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
#![cfg(test)] | ||
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. Is there code missing here perhaps? There was a test removed in |
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.
this becomes useless in practice because we clone the sender every time, see PR description for more info.