-
Notifications
You must be signed in to change notification settings - Fork 258
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
feat: add reconnecting-rpc-client
#1396
Changes from 6 commits
7c28327
bafcb78
eb7fcd3
cf88872
b85a547
d9f4718
282f86a
57141cd
6efcc28
38b30d4
c348bb0
4b6c296
1406ec6
3a8b520
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
//! Example to utilize the `reconnecting rpc client` in subxt | ||
//! which hidden behind behind `--feature reconnecting-rpc-client` | ||
//! | ||
//! To utilize full logs from the RPC client use: | ||
//! `RUST_LOG="jsonrpsee=trace,reconnecting_jsonrpsee_ws_client=trace"` | ||
|
||
#![allow(missing_docs)] | ||
|
||
use std::time::Duration; | ||
|
||
use subxt::backend::rpc::reconnecting_rpc_client::{Client, ExponentialBackoff, PingConfig}; | ||
use subxt::backend::rpc::RpcClient; | ||
use subxt::error::{Error, RpcError}; | ||
use subxt::{tx::TxStatus, OnlineClient, PolkadotConfig}; | ||
use subxt_signer::sr25519::dev; | ||
|
||
// Generate an interface that we can use from the node's metadata. | ||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")] | ||
pub mod polkadot {} | ||
|
||
#[tokio::main] | ||
async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
tracing_subscriber::fmt::init(); | ||
|
||
// Create a new client with with a reconnecting RPC client. | ||
let rpc = Client::builder() | ||
// Reconnect with exponential backoff. | ||
.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10))) | ||
// Send period WebSocket pings/pongs every 6th second and if it's not ACK:ed in 30 seconds | ||
// then disconnect. | ||
// | ||
// This is just a way to ensure that the connection isn't idle if no message is sent that often | ||
.enable_ws_ping( | ||
PingConfig::new() | ||
.ping_interval(Duration::from_secs(6)) | ||
.inactive_limit(Duration::from_secs(30)), | ||
) | ||
// There are other configurations as well that can be found here: | ||
// <https://docs.rs/reconnecting-jsonrpsee-ws-client/latest/reconnecting_jsonrpsee_ws_client/struct.ClientBuilder.html> | ||
.build("ws://localhost:9944".to_string()) | ||
.await?; | ||
|
||
let api: OnlineClient<PolkadotConfig> = | ||
OnlineClient::from_rpc_client(RpcClient::new(rpc.clone())).await?; | ||
|
||
// Build a balance transfer extrinsic. | ||
let dest = dev::bob().public_key().into(); | ||
let balance_transfer_tx = polkadot::tx().balances().transfer_allow_death(dest, 10_000); | ||
|
||
// Submit the balance transfer extrinsic from Alice, and wait for it to be successful | ||
// and in a finalized block. We get back the extrinsic events if all is well. | ||
let from = dev::alice(); | ||
|
||
let mut balance_transfer_progress = api | ||
.tx() | ||
.sign_and_submit_then_watch_default(&balance_transfer_tx, &from) | ||
.await?; | ||
|
||
// When the connection is lost, a error RpcError::DisconnectWillReconnect is emitted on the stream. | ||
// in such scenarios the error will be seen here. | ||
// | ||
// In such scenario it's possible that messages are lost when reconnecting | ||
// and if that's acceptable you may just ignore that error message. | ||
while let Some(status) = balance_transfer_progress.next().await { | ||
match status { | ||
// It's finalized in a block! | ||
Ok(TxStatus::InFinalizedBlock(in_block)) => { | ||
// grab the events and fail if no ExtrinsicSuccess event seen: | ||
let events = in_block.wait_for_success().await?; | ||
// We can look for events (this uses the static interface; we can also iterate | ||
// over them and dynamically decode them): | ||
let transfer_event = events.find_first::<polkadot::balances::events::Transfer>()?; | ||
|
||
if transfer_event.is_some() { | ||
println!("Balance transfer success"); | ||
} else { | ||
println!("Failed to find Balances::Transfer Event"); | ||
} | ||
} | ||
// Just log any other status we encounter: | ||
// | ||
// In this example we emit some important status handling for | ||
// here such as Dropped, Invalid etc.... | ||
Ok(_) => { | ||
println!("New status"); | ||
} | ||
// In this example we just ignore when reconnections occurs | ||
// but it's technically possible that we can lose | ||
// messages on the subscription such as `InFinalizedBlock` | ||
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 like this example overall, but when submitting a tx it's particularly tricky to handle as you will lose the subscription anyway won't you and won't get any new messages after reconnecting? So perhaps a better (and simpler, anyway) example is just subscribing to finalized blocks? I can't remember how your reconnecting client handles subscriptions etc; does it automatically re-subscribe behind the scenes? That area is an interesting one because on one hand, it might be better if the reconnecting client just reconnects but doesn't try to re-establish anything (so that the Subxt backend's can choose how best to restart stuff), but on the other hand having the client do it is simpler at least (but I fear may not always do what you want). Ultimately we'd need to agree on a "standard" for what is expected from a reconnecting client when it's used in Subxt 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.
Yes, it re-subscribes to subscriptions and re-transmits pending method calls. It's just a PITA if one have plenty of storage subscriptions opened and manually have to re-subscribe to them. 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. Let's just make it optional and folks can try it out and complain what's bad :) 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. Yeahh for now it's behind a feature flag so we can experiment! My gut feeling is that before it becomes "stable" in Subxt, we'll prob end up wanting the Backend impls to decide when/how to resubscribe etc themselves (eg maybe for TXs we don't try re-sending but for following new/final blocks we just restart it or whatever 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. Alex had a few cool ideas about distinguish between "stateful (submit_and_watch)" and "stateless (subscribe_storage)" subscriptions but still I think this reconnecting rpc client won't really work that well with the rpc-v2 spec. One would need to call a few methods 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.
I want to show how to ignore "DisconnectWillReconnect" and continue if a reconnection occurs. If you just call Added a few comments in the example now hopefully clearer now. 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 think this conversation is good start to open another issue how to deal with it reconnections in subxt properly especially with the rpc v2 spec in mind which is particular tricky. In general, I think just reconnect is probably the way to go and not re-subscribe and so on but we let's try my crate first and collect some feedback what's the best way forward :) 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.
What I mean is just that it's less code to subscribe to finalied blocks and then show how you can ignore the DisconnectedWillReconnect error. Probably closer to being a valid thing that we can handle nicely too (we can just start the subscription up again as your client does atm anyway) With transactions we probably(?) don't want to re-submit automatically so showing it as an example sortof hints at a behaviour that I don't think we'll keep, but anyway it's also just more effort building the tx etc when the point of the example is just handling the DisconnectWillReconnect error :)
Yup I agree; I'm happy with this for now since it's behind a feature flag, but before we stabilise it I thnk we should make the RPC client not try to do anything clever and only reconnect. Instead we can make the backend impls decide how to manage disconnects in followup PRs 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.
Got it, makes sense |
||
// when reconnecting. | ||
Err(Error::Rpc(RpcError::DisconnectedWillReconnect(e))) => { | ||
jsdw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
println!("{:?}", e); | ||
} | ||
Err(err) => { | ||
return Err(err.into()); | ||
} | ||
} | ||
} | ||
|
||
println!("RPC client reconnected `{}` times", rpc.reconnect_count()); | ||
|
||
Ok(()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -101,3 +101,94 @@ impl<T: RpcClientT> RpcClientT for Box<T> { | |
(**self).subscribe_raw(sub, params, unsub) | ||
} | ||
} | ||
|
||
#[cfg(feature = "reconnecting-rpc-client")] | ||
impl RpcClientT for reconnecting_jsonrpsee_ws_client::Client { | ||
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 would probably place this under a Ideally we would have a Considering this is under a feature-flag at the moment, I'd leave that refactoring as a follow-up as probably would need some extra tought. 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. My gut feeling is that for now it should just be a standalone optional RPC client that you can use to instantiate the existing 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 agree with the separate module though; something like |
||
fn request_raw<'a>( | ||
&'a self, | ||
method: &'a str, | ||
params: Option<Box<RawValue>>, | ||
) -> RawRpcFuture<'a, Box<RawValue>> { | ||
use futures::FutureExt; | ||
|
||
async { | ||
self.request_raw(method.to_string(), params) | ||
.await | ||
.map_err(|e| RpcError::ClientError(Box::new(e))) | ||
} | ||
.boxed() | ||
} | ||
|
||
fn subscribe_raw<'a>( | ||
&'a self, | ||
sub: &'a str, | ||
params: Option<Box<RawValue>>, | ||
unsub: &'a str, | ||
) -> RawRpcFuture<'a, RawRpcSubscription> { | ||
use futures::{FutureExt, StreamExt, TryStreamExt}; | ||
use reconnecting_jsonrpsee_ws_client::{RpcError as ReconnError, SubscriptionId}; | ||
|
||
// Helper for more human friendly error messages. | ||
// Workaround for: https://github.com/paritytech/jsonrpsee/issues/1280 | ||
fn rpc_error(err: ReconnError) -> String { | ||
niklasad1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
match err { | ||
ReconnError::Transport(e) => e.to_string(), | ||
e @ ReconnError::InvalidSubscriptionId => e.to_string(), | ||
ReconnError::InvalidRequestId(e) => e.to_string(), | ||
ReconnError::Custom(e) => e, | ||
ReconnError::RestartNeeded(inner_err) => { | ||
// HACK: jsonrpsee has Error::RestartNeeded(Arc<Error>) and | ||
// we need to visit one child because | ||
// RestartNeeded only contains one layer of recursion. | ||
// | ||
// Thus, `RestartNeeded(RestartNeededArc<Error>)` | ||
// and is unreachable. | ||
match &*inner_err { | ||
ReconnError::Transport(e) => e.to_string(), | ||
e @ ReconnError::InvalidSubscriptionId => e.to_string(), | ||
ReconnError::InvalidRequestId(e) => e.to_string(), | ||
ReconnError::Custom(e) => e.to_string(), | ||
// These variants are infallible. | ||
ReconnError::RestartNeeded(_) | ||
| ReconnError::RequestTimeout | ||
| ReconnError::MaxSlotsExceeded | ||
| ReconnError::Call(_) | ||
| ReconnError::ParseError(_) | ||
| ReconnError::HttpNotImplemented | ||
| ReconnError::EmptyBatchRequest(_) | ||
| ReconnError::RegisterMethod(_) => unreachable!(), | ||
} | ||
} | ||
// These variants are infallible. | ||
niklasad1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ReconnError::RequestTimeout | ||
| ReconnError::MaxSlotsExceeded | ||
| ReconnError::Call(_) | ||
| ReconnError::ParseError(_) | ||
| ReconnError::HttpNotImplemented | ||
| ReconnError::EmptyBatchRequest(_) | ||
| ReconnError::RegisterMethod(_) => unreachable!(), | ||
} | ||
} | ||
|
||
async { | ||
let sub = self | ||
.subscribe_raw(sub.to_string(), params, unsub.to_string()) | ||
.await | ||
.map_err(|e| RpcError::ClientError(Box::new(e)))?; | ||
|
||
let id = match sub.id() { | ||
SubscriptionId::Num(n) => n.to_string(), | ||
SubscriptionId::Str(s) => s.to_string(), | ||
}; | ||
let stream = sub | ||
.map_err(|e| RpcError::DisconnectedWillReconnect(Some(rpc_error(e.0)))) | ||
niklasad1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.boxed(); | ||
|
||
Ok(RawRpcSubscription { | ||
stream, | ||
id: Some(id), | ||
}) | ||
} | ||
.boxed() | ||
} | ||
} |
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.
Should we prefix with "unstable" for now until we've settled on the semantics of it (ie probably that the client will reconnect butnot auto resubscribe etc) and handled the DisconnectedWillReconnect?