From 84fea8b9b1e1a8e08dffae0e6266b55a973b4f95 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 20 Oct 2020 09:58:18 -0400 Subject: [PATCH 01/15] Expand WebSocket functionality This commit is multi-faceted. It: 1. Drastically simplifies much of the subscription-related functionality. 2. Expands the WebSocketClient's capabilities to be able to handle other types of requests other than just subscriptions (i.e. it implements the Client trait). Signed-off-by: Thane Thomson --- light-client/src/components/io.rs | 4 +- light-client/src/evidence.rs | 2 +- rpc/Cargo.toml | 4 +- rpc/src/client.rs | 54 +- rpc/src/client/subscription.rs | 620 +----------------- rpc/src/client/transport.rs | 18 +- rpc/src/client/transport/http.rs | 57 +- rpc/src/client/transport/mock.rs | 81 ++- rpc/src/client/transport/mock/subscription.rs | 212 ------ rpc/src/client/transport/router.rs | 179 +++++ rpc/src/client/transport/utils.rs | 16 + rpc/src/client/transport/websocket.rs | 416 +++++++----- rpc/src/endpoint/abci_info.rs | 2 + rpc/src/endpoint/abci_query.rs | 2 + rpc/src/endpoint/block.rs | 2 + rpc/src/endpoint/block_results.rs | 2 + rpc/src/endpoint/blockchain.rs | 2 + rpc/src/endpoint/broadcast/tx_async.rs | 2 + rpc/src/endpoint/broadcast/tx_commit.rs | 2 + rpc/src/endpoint/broadcast/tx_sync.rs | 2 + rpc/src/endpoint/commit.rs | 2 + rpc/src/endpoint/evidence.rs | 8 +- rpc/src/endpoint/genesis.rs | 2 + rpc/src/endpoint/health.rs | 2 + rpc/src/endpoint/net_info.rs | 2 + rpc/src/endpoint/status.rs | 2 + rpc/src/endpoint/subscribe.rs | 20 +- rpc/src/endpoint/unsubscribe.rs | 18 +- rpc/src/endpoint/validators.rs | 2 + rpc/src/id.rs | 23 +- rpc/src/lib.rs | 9 +- rpc/src/request.rs | 16 +- rpc/src/utils.rs | 18 + tendermint/tests/integration.rs | 4 +- 34 files changed, 718 insertions(+), 1089 deletions(-) delete mode 100644 rpc/src/client/transport/mock/subscription.rs create mode 100644 rpc/src/client/transport/router.rs create mode 100644 rpc/src/client/transport/utils.rs create mode 100644 rpc/src/utils.rs diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index 1170ee8f8..96b76f761 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -126,7 +126,7 @@ mod prod { } fn fetch_signed_header(&self, height: AtHeight) -> Result { - let client = self.rpc_client.clone(); + let mut client = self.rpc_client.clone(); let res = block_on( async move { match height { @@ -152,7 +152,7 @@ mod prod { AtHeight::At(height) => height, }; - let client = self.rpc_client.clone(); + let mut client = self.rpc_client.clone(); let task = async move { client.validators(height).await }; let res = block_on(task, self.peer_id, self.timeout)?; diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index a8804d635..bbc864a14 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -41,7 +41,7 @@ mod prod { impl EvidenceReporter for ProdEvidenceReporter { #[pre(self.peer_map.contains_key(&peer))] fn report(&self, e: Evidence, peer: PeerId) -> Result { - let client = self.rpc_client_for(peer)?; + let mut client = self.rpc_client_for(peer)?; let task = async move { client.broadcast_evidence(e).await }; let res = block_on(task, peer, None)?; diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 2de010e39..7fff4d6c9 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -35,7 +35,8 @@ websocket-client = [ "tokio/macros", "tokio/stream", "tokio/sync", - "tokio/time" + "tokio/time", + "tracing" ] [dependencies] @@ -55,3 +56,4 @@ futures = { version = "0.3", optional = true } http = { version = "0.2", optional = true } hyper = { version = "0.13", optional = true } tokio = { version = "0.2", optional = true } +tracing = { version = "0.1", optional = true } diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 0a574be1d..ac34908d2 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -1,13 +1,11 @@ //! Tendermint RPC client. mod subscription; -pub use subscription::{Subscription, SubscriptionClient, SubscriptionId}; +pub use subscription::{Subscription, SubscriptionClient}; pub mod sync; mod transport; -pub use transport::mock::{ - MockClient, MockRequestMatcher, MockRequestMethodMatcher, MockSubscriptionClient, -}; +pub use transport::mock::{MockClient, MockRequestMatcher, MockRequestMethodMatcher}; #[cfg(feature = "http-client")] pub use transport::http::HttpClient; @@ -15,7 +13,7 @@ pub use transport::http::HttpClient; pub use transport::websocket::{WebSocketClient, WebSocketClientDriver}; use crate::endpoint::*; -use crate::{Request, Result}; +use crate::{Result, SimpleRequest}; use async_trait::async_trait; use tendermint::abci::{self, Transaction}; use tendermint::block::Height; @@ -32,13 +30,13 @@ use tendermint::Genesis; #[async_trait] pub trait Client { /// `/abci_info`: get information about the ABCI application. - async fn abci_info(&self) -> Result { + async fn abci_info(&mut self) -> Result { Ok(self.perform(abci_info::Request).await?.response) } /// `/abci_query`: query the ABCI application async fn abci_query( - &self, + &mut self, path: Option, data: V, height: Option, @@ -54,7 +52,7 @@ pub trait Client { } /// `/block`: get block at a given height. - async fn block(&self, height: H) -> Result + async fn block(&mut self, height: H) -> Result where H: Into + Send, { @@ -62,12 +60,12 @@ pub trait Client { } /// `/block`: get the latest block. - async fn latest_block(&self) -> Result { + async fn latest_block(&mut self) -> Result { self.perform(block::Request::default()).await } /// `/block_results`: get ABCI results for a block at a particular height. - async fn block_results(&self, height: H) -> Result + async fn block_results(&mut self, height: H) -> Result where H: Into + Send, { @@ -76,7 +74,7 @@ pub trait Client { } /// `/block_results`: get ABCI results for the latest block. - async fn latest_block_results(&self) -> Result { + async fn latest_block_results(&mut self) -> Result { self.perform(block_results::Request::default()).await } @@ -85,7 +83,7 @@ pub trait Client { /// Block headers are returned in descending order (highest first). /// /// Returns at most 20 items. - async fn blockchain(&self, min: H, max: H) -> Result + async fn blockchain(&mut self, min: H, max: H) -> Result where H: Into + Send, { @@ -95,24 +93,30 @@ pub trait Client { } /// `/broadcast_tx_async`: broadcast a transaction, returning immediately. - async fn broadcast_tx_async(&self, tx: Transaction) -> Result { + async fn broadcast_tx_async( + &mut self, + tx: Transaction, + ) -> Result { self.perform(broadcast::tx_async::Request::new(tx)).await } /// `/broadcast_tx_sync`: broadcast a transaction, returning the response /// from `CheckTx`. - async fn broadcast_tx_sync(&self, tx: Transaction) -> Result { + async fn broadcast_tx_sync(&mut self, tx: Transaction) -> Result { self.perform(broadcast::tx_sync::Request::new(tx)).await } /// `/broadcast_tx_sync`: broadcast a transaction, returning the response /// from `CheckTx`. - async fn broadcast_tx_commit(&self, tx: Transaction) -> Result { + async fn broadcast_tx_commit( + &mut self, + tx: Transaction, + ) -> Result { self.perform(broadcast::tx_commit::Request::new(tx)).await } /// `/commit`: get block commit at a given height. - async fn commit(&self, height: H) -> Result + async fn commit(&mut self, height: H) -> Result where H: Into + Send, { @@ -120,7 +124,7 @@ pub trait Client { } /// `/validators`: get validators a given height. - async fn validators(&self, height: H) -> Result + async fn validators(&mut self, height: H) -> Result where H: Into + Send, { @@ -128,41 +132,41 @@ pub trait Client { } /// `/commit`: get the latest block commit - async fn latest_commit(&self) -> Result { + async fn latest_commit(&mut self) -> Result { self.perform(commit::Request::default()).await } /// `/health`: get node health. /// /// Returns empty result (200 OK) on success, no response in case of an error. - async fn health(&self) -> Result<()> { + async fn health(&mut self) -> Result<()> { self.perform(health::Request).await?; Ok(()) } /// `/genesis`: get genesis file. - async fn genesis(&self) -> Result { + async fn genesis(&mut self) -> Result { Ok(self.perform(genesis::Request).await?.genesis) } /// `/net_info`: obtain information about P2P and other network connections. - async fn net_info(&self) -> Result { + async fn net_info(&mut self) -> Result { self.perform(net_info::Request).await } /// `/status`: get Tendermint status including node info, pubkey, latest /// block hash, app hash, block height and time. - async fn status(&self) -> Result { + async fn status(&mut self) -> Result { self.perform(status::Request).await } /// `/broadcast_evidence`: broadcast an evidence. - async fn broadcast_evidence(&self, e: Evidence) -> Result { + async fn broadcast_evidence(&mut self, e: Evidence) -> Result { self.perform(evidence::Request::new(e)).await } /// Perform a request against the RPC endpoint - async fn perform(&self, request: R) -> Result + async fn perform(&mut self, request: R) -> Result where - R: Request; + R: SimpleRequest; } diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index a7b7adf99..88fd0e5d8 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -1,20 +1,13 @@ //! Subscription- and subscription management-related functionality. -#[cfg(feature = "websocket-client")] -pub use two_phase_router::{SubscriptionState, TwoPhaseSubscriptionRouter}; - -use crate::client::sync::{unbounded, ChannelRx, ChannelTx}; +use crate::client::sync::{ChannelRx, ChannelTx}; use crate::event::Event; use crate::query::Query; -use crate::{Error, Id, Result}; +use crate::Result; use async_trait::async_trait; use futures::task::{Context, Poll}; use futures::Stream; -use getrandom::getrandom; -use std::collections::HashMap; -use std::convert::TryInto; use std::pin::Pin; -use std::str::FromStr; /// A client that exclusively provides [`Event`] subscription capabilities, /// without any other RPC method support. @@ -24,15 +17,31 @@ use std::str::FromStr; pub trait SubscriptionClient { /// `/subscribe`: subscribe to receive events produced by the given query. async fn subscribe(&mut self, query: Query) -> Result; + + /// `/unsubscribe`: unsubscribe from events relating to the given query. + /// + /// This method is particularly useful when you want to terminate multiple + /// [`Subscription`]s to the same [`Query`] simultaneously, or if you've + /// joined multiple `Subscription`s together using [`select_all`] and you + /// no longer have access to the individual `Subscription` instances to + /// terminate them separately. + /// + /// [`Subscription`]: struct.Subscription.html + /// [`Query`]: struct.Query.html + /// [`select_all`]: https://docs.rs/futures/*/futures/stream/fn.select_all.html + async fn unsubscribe(&mut self, query: Query) -> Result<()>; } +pub(crate) type SubscriptionTx = ChannelTx>; +pub(crate) type SubscriptionRx = ChannelRx>; + /// An interface that can be used to asynchronously receive [`Event`]s for a /// particular subscription. /// /// ## Examples /// /// ``` -/// use tendermint_rpc::{SubscriptionId, Subscription}; +/// use tendermint_rpc::Subscription; /// use futures::StreamExt; /// /// /// Prints `count` events from the given subscription. @@ -55,598 +64,33 @@ pub trait SubscriptionClient { /// [`Event`]: ./event/struct.Event.html #[derive(Debug)] pub struct Subscription { - /// The query for which events will be produced. - pub query: Query, - /// The ID of this subscription (automatically assigned). - pub id: SubscriptionId, + // A unique identifier for this subscription. + id: String, + // The query for which events will be produced. + query: Query, // Our internal result event receiver for this subscription. - event_rx: ChannelRx>, - // Allows us to interact with the subscription driver (exclusively to - // terminate this subscription). - cmd_tx: ChannelTx, + rx: SubscriptionRx, } impl Stream for Subscription { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.event_rx.poll_recv(cx) + self.rx.poll_recv(cx) } } impl Subscription { - pub(crate) fn new( - id: SubscriptionId, - query: Query, - event_rx: ChannelRx>, - cmd_tx: ChannelTx, - ) -> Self { - Self { - id, - query, - event_rx, - cmd_tx, - } - } - - /// Gracefully terminate this subscription and consume it. - /// - /// The `Subscription` can be moved to any asynchronous context, and this - /// method provides a way to terminate it from that same context. - pub async fn terminate(mut self) -> Result<()> { - let (result_tx, mut result_rx) = unbounded(); - self.cmd_tx - .send(SubscriptionDriverCmd::Unsubscribe { - id: self.id.clone(), - query: self.query.clone(), - result_tx, - }) - .await?; - result_rx.recv().await.ok_or_else(|| { - Error::client_internal_error( - "failed to hear back from subscription termination request".to_string(), - ) - })? - } -} - -/// A command that can be sent to the subscription driver. -/// -/// It is assumed that all [`SubscriptionClient`] implementations will follow a -/// handle/driver concurrency model, where the client itself will just be a -/// handle to a driver that runs in a separate coroutine. -/// -/// [`SubscriptionClient`]: trait.SubscriptionClient.html -#[derive(Debug, Clone)] -pub enum SubscriptionDriverCmd { - /// Initiate a new subscription. - Subscribe { - /// The desired ID for the new subscription. - id: SubscriptionId, - /// The query for which to initiate the subscription. - query: Query, - /// Where to send events received for this subscription. - event_tx: ChannelTx>, - /// Where to send the result of this subscription command. - result_tx: ChannelTx>, - }, - /// Terminate an existing subscription. - Unsubscribe { - /// The ID of the subscription to terminate. - id: SubscriptionId, - /// The query associated with the subscription we want to terminate. - query: Query, - /// Where to send the result of this unsubscribe command. - result_tx: ChannelTx>, - }, - /// Terminate the subscription driver entirely. - Terminate, -} - -/// Each new subscription is automatically assigned an ID. -/// -/// By default, we generate random [UUIDv4] IDs for each subscription to -/// minimize chances of collision. -/// -/// [UUIDv4]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random) -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SubscriptionId(String); - -impl Default for SubscriptionId { - fn default() -> Self { - let mut bytes = [0; 16]; - getrandom(&mut bytes).expect("RNG failure!"); - - let uuid = uuid::Builder::from_bytes(bytes) - .set_variant(uuid::Variant::RFC4122) - .set_version(uuid::Version::Random) - .build(); - - Self(uuid.to_string()) - } -} - -impl std::fmt::Display for SubscriptionId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Into for SubscriptionId { - fn into(self) -> Id { - Id::Str(self.0) - } -} - -impl TryInto for Id { - type Error = Error; - - fn try_into(self) -> std::result::Result { - match self { - Id::Str(s) => Ok(SubscriptionId(s)), - Id::Num(i) => Ok(SubscriptionId(format!("{}", i))), - Id::None => Err(Error::client_internal_error( - "cannot convert an empty JSON-RPC ID into a subscription ID", - )), - } - } -} - -impl FromStr for SubscriptionId { - type Err = (); - - fn from_str(s: &str) -> std::result::Result { - Ok(Self(s.to_string())) - } -} - -impl SubscriptionId { - pub fn as_str(&self) -> &str { - self.0.as_str() - } -} - -/// Provides a mechanism for tracking [`Subscription`]s and routing [`Event`]s -/// to those subscriptions. -/// -/// [`Subscription`]: struct.Subscription.html -/// [`Event`]: ./event/struct.Event.html -#[derive(Debug)] -pub struct SubscriptionRouter { - // A map of subscription queries to collections of subscription IDs and - // their result channels. Used for publishing events relating to a specific - // query. - subscriptions: HashMap>>>, -} - -impl SubscriptionRouter { - /// Publishes the given event to all of the subscriptions to which the - /// event is relevant. At present, it matches purely based on the query - /// associated with the event, and only queries that exactly match that of - /// the event's. - pub async fn publish(&mut self, ev: Event) { - let subs_for_query = match self.subscriptions.get_mut(&ev.query) { - Some(s) => s, - None => return, - }; - // We assume here that any failure to publish an event is an indication - // that the receiver end of the channel has been dropped, which allows - // us to safely stop tracking the subscription. - let mut disconnected = Vec::::new(); - for (id, event_tx) in subs_for_query { - if event_tx.send(Ok(ev.clone())).await.is_err() { - disconnected.push(id.clone()); - } - } - // Obtain a mutable reference because the previous reference was - // consumed in the above for loop. We should panic if there are no - // longer any subscriptions for this query. - let subs_for_query = self.subscriptions.get_mut(&ev.query).unwrap(); - for id in disconnected { - subs_for_query.remove(&id); - } - } - - /// Immediately add a new subscription to the router without waiting for - /// confirmation. - pub fn add(&mut self, id: &SubscriptionId, query: String, event_tx: ChannelTx>) { - let subs_for_query = match self.subscriptions.get_mut(&query) { - Some(s) => s, - None => { - self.subscriptions.insert(query.clone(), HashMap::new()); - self.subscriptions.get_mut(&query).unwrap() - } - }; - subs_for_query.insert(id.clone(), event_tx); - } - - /// Immediately remove the subscription with the given query and ID. - pub fn remove(&mut self, id: &SubscriptionId, query: String) { - let subs_for_query = match self.subscriptions.get_mut(&query) { - Some(s) => s, - None => return, - }; - subs_for_query.remove(id); - } -} - -impl Default for SubscriptionRouter { - fn default() -> Self { - Self { - subscriptions: HashMap::new(), - } - } -} - -#[cfg(feature = "websocket-client")] -mod two_phase_router { - use super::*; - - /// A subscription router that can manage pending subscribe and unsubscribe - /// requests, as well as their confirmation/cancellation. - /// - /// This is useful in instances where the underlying transport is complex, - /// e.g. WebSocket connections, where many messages are multiplexed on the - /// same communication line. In such cases, a response from the remote - /// endpoint immediately after a subscribe/unsubscribe request may not be - /// relevant to that request. - #[derive(Debug)] - pub struct TwoPhaseSubscriptionRouter { - // The underlying router that exclusively keeps track of confirmed and - // active subscriptions. - router: SubscriptionRouter, - // A map of JSON-RPC request IDs (for `/subscribe` requests) to pending - // subscription requests. - pending_subscribe: HashMap, - // A map of JSON-RPC request IDs (for the `/unsubscribe` requests) to pending - // unsubscribe requests. - pending_unsubscribe: HashMap, - } - - impl Default for TwoPhaseSubscriptionRouter { - fn default() -> Self { - Self { - router: SubscriptionRouter::default(), - pending_subscribe: HashMap::new(), - pending_unsubscribe: HashMap::new(), - } - } - } - - impl TwoPhaseSubscriptionRouter { - /// Publishes the given event to all of the subscriptions to which the - /// event is relevant. - pub async fn publish(&mut self, ev: Event) { - self.router.publish(ev).await - } - - /// Keep track of a pending subscription, which can either be confirmed or - /// cancelled. - /// - /// `req_id` must be a unique identifier for this particular pending - /// subscription request operation, where `subs_id` must be the unique ID - /// of the subscription we eventually want added. - pub fn pending_add( - &mut self, - req_id: &str, - subs_id: &SubscriptionId, - query: String, - event_tx: ChannelTx>, - result_tx: ChannelTx>, - ) { - self.pending_subscribe.insert( - req_id.to_string(), - PendingSubscribe { - id: subs_id.clone(), - query, - event_tx, - result_tx, - }, - ); - } - - /// Attempts to confirm the pending subscription request with the given ID. - /// - /// Returns an error if it fails to respond to the original caller to - /// indicate success. - pub async fn confirm_add(&mut self, req_id: &str) -> Result<()> { - match self.pending_subscribe.remove(req_id) { - Some(mut pending_subscribe) => { - self.router.add( - &pending_subscribe.id, - pending_subscribe.query.clone(), - pending_subscribe.event_tx, - ); - Ok(pending_subscribe.result_tx.send(Ok(())).await?) - } - None => Ok(()), - } - } - - /// Attempts to cancel the pending subscription with the given ID, sending - /// the specified error to the original creator of the attempted - /// subscription. - pub async fn cancel_add(&mut self, req_id: &str, err: impl Into) -> Result<()> { - match self.pending_subscribe.remove(req_id) { - Some(mut pending_subscribe) => Ok(pending_subscribe - .result_tx - .send(Err(err.into())) - .await - .map_err(|_| { - Error::client_internal_error(format!( - "failed to communicate result of pending subscription with ID: {}", - pending_subscribe.id, - )) - })?), - None => Ok(()), - } - } - - /// Keeps track of a pending unsubscribe request, which can either be - /// confirmed or cancelled. - pub fn pending_remove( - &mut self, - req_id: &str, - subs_id: &SubscriptionId, - query: String, - result_tx: ChannelTx>, - ) { - self.pending_unsubscribe.insert( - req_id.to_string(), - PendingUnsubscribe { - id: subs_id.clone(), - query, - result_tx, - }, - ); - } - - /// Confirm the pending unsubscribe request for the subscription with the - /// given ID. - pub async fn confirm_remove(&mut self, req_id: &str) -> Result<()> { - match self.pending_unsubscribe.remove(req_id) { - Some(mut pending_unsubscribe) => { - self.router - .remove(&pending_unsubscribe.id, pending_unsubscribe.query.clone()); - Ok(pending_unsubscribe.result_tx.send(Ok(())).await?) - } - None => Ok(()), - } - } - - /// Cancel the pending unsubscribe request for the subscription with the - /// given ID, responding with the given error. - pub async fn cancel_remove(&mut self, req_id: &str, err: impl Into) -> Result<()> { - match self.pending_unsubscribe.remove(req_id) { - Some(mut pending_unsubscribe) => { - Ok(pending_unsubscribe.result_tx.send(Err(err.into())).await?) - } - None => Ok(()), - } - } - - /// Helper to check whether the subscription with the given ID is - /// currently active. - pub fn is_active(&self, id: &SubscriptionId) -> bool { - self.router - .subscriptions - .iter() - .any(|(_query, subs_for_query)| subs_for_query.contains_key(id)) - } - - /// Obtain a mutable reference to the subscription with the given ID (if it - /// exists). - pub fn get_active_subscription_mut( - &mut self, - id: &SubscriptionId, - ) -> Option<&mut ChannelTx>> { - self.router - .subscriptions - .iter_mut() - .find(|(_query, subs_for_query)| subs_for_query.contains_key(id)) - .and_then(|(_query, subs_for_query)| subs_for_query.get_mut(id)) - } - - /// Utility method to determine the current state of the subscription with - /// the given ID. - pub fn subscription_state(&self, req_id: &str) -> Option { - if self.pending_subscribe.contains_key(req_id) { - Some(SubscriptionState::Pending) - } else if self.pending_unsubscribe.contains_key(req_id) { - Some(SubscriptionState::Cancelling) - } else if self.is_active(&SubscriptionId::from_str(req_id).unwrap()) { - Some(SubscriptionState::Active) - } else { - None - } - } + pub(crate) fn new(id: String, query: Query, rx: SubscriptionRx) -> Self { + Self { id, query, rx } } - #[derive(Debug)] - struct PendingSubscribe { - id: SubscriptionId, - query: String, - event_tx: ChannelTx>, - result_tx: ChannelTx>, + /// Return this subscription's ID for informational purposes. + pub fn id(&self) -> &str { + &self.id } - #[derive(Debug)] - struct PendingUnsubscribe { - id: SubscriptionId, - query: String, - result_tx: ChannelTx>, - } - - /// The current state of a subscription. - #[derive(Debug, Clone, PartialEq)] - pub enum SubscriptionState { - Pending, - Active, - Cancelling, - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::client::sync::unbounded; - use crate::event::{Event, WrappedEvent}; - use std::path::PathBuf; - use tokio::fs; - use tokio::time::{self, Duration}; - - async fn read_json_fixture(name: &str) -> String { - fs::read_to_string(PathBuf::from("./tests/support/").join(name.to_owned() + ".json")) - .await - .unwrap() - } - - async fn read_event(name: &str) -> Event { - serde_json::from_str::(read_json_fixture(name).await.as_str()) - .unwrap() - .into_result() - .unwrap() - } - - async fn must_recv(ch: &mut ChannelRx, timeout_ms: u64) -> T { - let mut delay = time::delay_for(Duration::from_millis(timeout_ms)); - tokio::select! { - _ = &mut delay, if !delay.is_elapsed() => panic!("timed out waiting for recv"), - Some(v) = ch.recv() => v, - } - } - - async fn must_not_recv(ch: &mut ChannelRx, timeout_ms: u64) - where - T: std::fmt::Debug, - { - let mut delay = time::delay_for(Duration::from_millis(timeout_ms)); - tokio::select! { - _ = &mut delay, if !delay.is_elapsed() => (), - Some(v) = ch.recv() => panic!("got unexpected result from channel: {:?}", v), - } - } - - #[tokio::test] - async fn router_basic_pub_sub() { - let mut router = SubscriptionRouter::default(); - - let (subs1_id, subs2_id, subs3_id) = ( - SubscriptionId::default(), - SubscriptionId::default(), - SubscriptionId::default(), - ); - let (subs1_event_tx, mut subs1_event_rx) = unbounded(); - let (subs2_event_tx, mut subs2_event_rx) = unbounded(); - let (subs3_event_tx, mut subs3_event_rx) = unbounded(); - - // Two subscriptions with the same query - router.add(&subs1_id, "query1".into(), subs1_event_tx); - router.add(&subs2_id, "query1".into(), subs2_event_tx); - // Another subscription with a different query - router.add(&subs3_id, "query2".into(), subs3_event_tx); - - let mut ev = read_event("event_new_block_1").await; - ev.query = "query1".into(); - router.publish(ev.clone()).await; - - let subs1_ev = must_recv(&mut subs1_event_rx, 500).await.unwrap(); - let subs2_ev = must_recv(&mut subs2_event_rx, 500).await.unwrap(); - must_not_recv(&mut subs3_event_rx, 50).await; - assert_eq!(ev, subs1_ev); - assert_eq!(ev, subs2_ev); - - ev.query = "query2".into(); - router.publish(ev.clone()).await; - - must_not_recv(&mut subs1_event_rx, 50).await; - must_not_recv(&mut subs2_event_rx, 50).await; - let subs3_ev = must_recv(&mut subs3_event_rx, 500).await.unwrap(); - assert_eq!(ev, subs3_ev); - } - - #[cfg(feature = "websocket-client")] - #[tokio::test] - async fn router_pending_subscription() { - let mut router = TwoPhaseSubscriptionRouter::default(); - let subs_id = SubscriptionId::default(); - let (event_tx, mut event_rx) = unbounded(); - let (result_tx, mut result_rx) = unbounded(); - let query = "query".to_string(); - let mut ev = read_event("event_new_block_1").await; - ev.query = query.clone(); - - assert!(router.subscription_state(&subs_id.to_string()).is_none()); - router.pending_add( - subs_id.as_str(), - &subs_id, - query.clone(), - event_tx, - result_tx, - ); - assert_eq!( - SubscriptionState::Pending, - router.subscription_state(subs_id.as_str()).unwrap() - ); - router.publish(ev.clone()).await; - must_not_recv(&mut event_rx, 50).await; - - router.confirm_add(subs_id.as_str()).await.unwrap(); - assert_eq!( - SubscriptionState::Active, - router.subscription_state(subs_id.as_str()).unwrap() - ); - must_not_recv(&mut event_rx, 50).await; - let _ = must_recv(&mut result_rx, 500).await; - - router.publish(ev.clone()).await; - let received_ev = must_recv(&mut event_rx, 500).await.unwrap(); - assert_eq!(ev, received_ev); - - let (result_tx, mut result_rx) = unbounded(); - router.pending_remove(subs_id.as_str(), &subs_id, query.clone(), result_tx); - assert_eq!( - SubscriptionState::Cancelling, - router.subscription_state(subs_id.as_str()).unwrap(), - ); - - router.confirm_remove(subs_id.as_str()).await.unwrap(); - assert!(router.subscription_state(subs_id.as_str()).is_none()); - router.publish(ev.clone()).await; - if must_recv(&mut result_rx, 500).await.is_err() { - panic!("we should have received successful confirmation of the unsubscribe request") - } - } - - #[cfg(feature = "websocket-client")] - #[tokio::test] - async fn router_cancel_pending_subscription() { - let mut router = TwoPhaseSubscriptionRouter::default(); - let subs_id = SubscriptionId::default(); - let (event_tx, mut event_rx) = unbounded::>(); - let (result_tx, mut result_rx) = unbounded::>(); - let query = "query".to_string(); - let mut ev = read_event("event_new_block_1").await; - ev.query = query.clone(); - - assert!(router.subscription_state(subs_id.as_str()).is_none()); - router.pending_add(subs_id.as_str(), &subs_id, query, event_tx, result_tx); - assert_eq!( - SubscriptionState::Pending, - router.subscription_state(subs_id.as_str()).unwrap() - ); - router.publish(ev.clone()).await; - must_not_recv(&mut event_rx, 50).await; - - let cancel_error = Error::client_internal_error("cancelled"); - router - .cancel_add(subs_id.as_str(), cancel_error.clone()) - .await - .unwrap(); - assert!(router.subscription_state(subs_id.as_str()).is_none()); - assert_eq!(Err(cancel_error), must_recv(&mut result_rx, 500).await); - - router.publish(ev.clone()).await; - must_not_recv(&mut event_rx, 50).await; + pub fn query(&self) -> &Query { + &self.query } } diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index edd65bc2b..3aa8c649b 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -1,24 +1,10 @@ //! Tendermint RPC client implementations for different transports. pub mod mock; +mod router; +mod utils; #[cfg(feature = "http-client")] pub mod http; #[cfg(feature = "websocket-client")] pub mod websocket; - -use crate::{Error, Result}; -use tendermint::net; - -// TODO(thane): Should we move this into a separate module? -/// Convenience method to extract the host and port associated with the given -/// address, but only if it's a TCP address (it fails otherwise). -pub fn get_tcp_host_port(address: net::Address) -> Result<(String, u16)> { - match address { - net::Address::Tcp { host, port, .. } => Ok((host, port)), - other => Err(Error::invalid_params(&format!( - "invalid RPC address: {:?}", - other - ))), - } -} diff --git a/rpc/src/client/transport/http.rs b/rpc/src/client/transport/http.rs index fb77d14ec..7becc8801 100644 --- a/rpc/src/client/transport/http.rs +++ b/rpc/src/client/transport/http.rs @@ -1,7 +1,7 @@ //! HTTP-based transport for Tendermint RPC Client. -use crate::client::transport::get_tcp_host_port; -use crate::{Client, Request, Response, Result}; +use crate::client::transport::utils::get_tcp_host_port; +use crate::{Client, Response, Result, SimpleRequest}; use async_trait::async_trait; use bytes::buf::BufExt; use hyper::header; @@ -41,11 +41,31 @@ pub struct HttpClient { #[async_trait] impl Client for HttpClient { - async fn perform(&self, request: R) -> Result + async fn perform(&mut self, request: R) -> Result where - R: Request, + R: SimpleRequest, { - http_request(&self.host, self.port, request).await + let request_body = request.into_json(); + + let mut request = hyper::Request::builder() + .method("POST") + .uri(&format!("http://{}:{}/", self.host, self.port)) + .body(hyper::Body::from(request_body.into_bytes()))?; + + { + let headers = request.headers_mut(); + headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); + headers.insert( + header::USER_AGENT, + format!("tendermint.rs/{}", env!("CARGO_PKG_VERSION")) + .parse() + .unwrap(), + ); + } + let http_client = hyper::Client::builder().build_http(); + let response = http_client.request(request).await?; + let response_body = hyper::body::aggregate(response.into_body()).await?; + R::Response::from_reader(response_body.reader()) } } @@ -56,30 +76,3 @@ impl HttpClient { Ok(HttpClient { host, port }) } } - -pub async fn http_request(host: &str, port: u16, request: R) -> Result -where - R: Request, -{ - let request_body = request.into_json(); - - let mut request = hyper::Request::builder() - .method("POST") - .uri(&format!("http://{}:{}/", host, port)) - .body(hyper::Body::from(request_body.into_bytes()))?; - - { - let headers = request.headers_mut(); - headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); - headers.insert( - header::USER_AGENT, - format!("tendermint.rs/{}", env!("CARGO_PKG_VERSION")) - .parse() - .unwrap(), - ); - } - let http_client = hyper::Client::builder().build_http(); - let response = http_client.request(request).await?; - let response_body = hyper::body::aggregate(response.into_body()).await?; - R::Response::from_reader(response_body.reader()) -} diff --git a/rpc/src/client/transport/mock.rs b/rpc/src/client/transport/mock.rs index 3b41fc18b..a7b8898ce 100644 --- a/rpc/src/client/transport/mock.rs +++ b/rpc/src/client/transport/mock.rs @@ -1,9 +1,11 @@ //! Mock client implementation for use in testing. -mod subscription; -pub use subscription::MockSubscriptionClient; - -use crate::{Client, Error, Method, Request, Response, Result}; +use crate::client::sync::unbounded; +use crate::client::transport::router::SubscriptionRouter; +use crate::event::Event; +use crate::query::Query; +use crate::utils::uuid_str; +use crate::{Client, Error, Method, Request, Response, Result, Subscription, SubscriptionClient}; use async_trait::async_trait; use std::collections::HashMap; @@ -30,7 +32,7 @@ use std::collections::HashMap; /// async fn main() { /// let matcher = MockRequestMethodMatcher::default() /// .map(Method::AbciInfo, Ok(ABCI_INFO_RESPONSE.to_string())); -/// let client = MockClient::new(matcher); +/// let mut client = MockClient::new(matcher); /// /// let abci_info = client.abci_info().await.unwrap(); /// println!("Got mock ABCI info: {:?}", abci_info); @@ -40,11 +42,12 @@ use std::collections::HashMap; #[derive(Debug)] pub struct MockClient { matcher: M, + router: SubscriptionRouter, } #[async_trait] impl Client for MockClient { - async fn perform(&self, request: R) -> Result + async fn perform(&mut self, request: R) -> Result where R: Request, { @@ -57,7 +60,31 @@ impl Client for MockClient { impl MockClient { /// Create a new mock RPC client using the given request matcher. pub fn new(matcher: M) -> Self { - Self { matcher } + Self { + matcher, + router: SubscriptionRouter::default(), + } + } + + /// Publishes the given event to all subscribers whose query exactly + /// matches that of the event. + pub async fn publish(&mut self, ev: &Event) { + let _ = self.router.publish(ev).await; + } +} + +#[async_trait] +impl SubscriptionClient for MockClient { + async fn subscribe(&mut self, query: Query) -> Result { + let id = uuid_str(); + let (subs_tx, subs_rx) = unbounded(); + self.router.add(id.clone(), query.clone(), subs_tx); + Ok(Subscription::new(id, query, subs_rx)) + } + + async fn unsubscribe(&mut self, query: Query) -> Result<()> { + self.router.remove_by_query(query); + Ok(()) } } @@ -116,6 +143,8 @@ impl MockRequestMethodMatcher { #[cfg(test)] mod test { use super::*; + use crate::query::EventType; + use futures::StreamExt; use std::path::PathBuf; use tendermint::block::Height; use tendermint::chain::Id; @@ -127,6 +156,10 @@ mod test { .unwrap() } + async fn read_event(name: &str) -> Event { + Event::from_string(&read_json_fixture(name).await).unwrap() + } + #[tokio::test] async fn mock_client() { let abci_info_fixture = read_json_fixture("abci_info").await; @@ -134,7 +167,7 @@ mod test { let matcher = MockRequestMethodMatcher::default() .map(Method::AbciInfo, Ok(abci_info_fixture)) .map(Method::Block, Ok(block_fixture)); - let client = MockClient::new(matcher); + let mut client = MockClient::new(matcher); let abci_info = client.abci_info().await.unwrap(); assert_eq!("GaiaApp".to_string(), abci_info.data); @@ -144,4 +177,36 @@ mod test { assert_eq!(Height::from(10_u32), block.header.height); assert_eq!("cosmoshub-2".parse::().unwrap(), block.header.chain_id); } + + #[tokio::test] + async fn mock_subscription_client() { + let mut client = MockClient::new(MockRequestMethodMatcher::default()); + let event1 = read_event("event_new_block_1").await; + let event2 = read_event("event_new_block_2").await; + let event3 = read_event("event_new_block_3").await; + let events = vec![event1, event2, event3]; + + let subs1 = client.subscribe(EventType::NewBlock.into()).await.unwrap(); + let subs2 = client.subscribe(EventType::NewBlock.into()).await.unwrap(); + assert_ne!(subs1.id().to_string(), subs2.id().to_string()); + + // We can do this because the underlying channels can buffer the + // messages as we publish them. + let subs1_events = subs1.take(3); + let subs2_events = subs2.take(3); + for ev in &events { + client.publish(ev).await; + } + + // Here each subscription's channel is drained. + let subs1_events = subs1_events.collect::>>().await; + let subs2_events = subs2_events.collect::>>().await; + + assert_eq!(3, subs1_events.len()); + assert_eq!(3, subs2_events.len()); + + for i in 0..3 { + assert!(events[i].eq(subs1_events[i].as_ref().unwrap())); + } + } } diff --git a/rpc/src/client/transport/mock/subscription.rs b/rpc/src/client/transport/mock/subscription.rs deleted file mode 100644 index 8f18386b1..000000000 --- a/rpc/src/client/transport/mock/subscription.rs +++ /dev/null @@ -1,212 +0,0 @@ -//! Subscription functionality for the Tendermint RPC mock client. - -use crate::client::subscription::{SubscriptionDriverCmd, SubscriptionRouter}; -use crate::client::sync::{unbounded, ChannelRx, ChannelTx}; -use crate::event::Event; -use crate::query::Query; -use crate::{Error, Result, Subscription, SubscriptionClient, SubscriptionId}; -use async_trait::async_trait; -use tokio::task::JoinHandle; - -/// A mock client that facilitates [`Event`] subscription. -/// -/// Creating a `MockSubscriptionClient` will immediately spawn an asynchronous -/// driver task that handles routing of incoming [`Event`]s. The -/// `MockSubscriptionClient` then effectively becomes a handle to the -/// asynchronous driver. -/// -/// [`Event`]: event/struct.Event.html -#[derive(Debug)] -pub struct MockSubscriptionClient { - driver_hdl: JoinHandle>, - event_tx: ChannelTx, - cmd_tx: ChannelTx, -} - -#[async_trait] -impl SubscriptionClient for MockSubscriptionClient { - async fn subscribe(&mut self, query: Query) -> Result { - let (event_tx, event_rx) = unbounded(); - let (result_tx, mut result_rx) = unbounded(); - let id = SubscriptionId::default(); - self.send_cmd(SubscriptionDriverCmd::Subscribe { - id: id.clone(), - query: query.clone(), - event_tx, - result_tx, - }) - .await?; - result_rx.recv().await.ok_or_else(|| { - Error::client_internal_error( - "failed to receive subscription confirmation from mock client driver", - ) - })??; - - Ok(Subscription::new(id, query, event_rx, self.cmd_tx.clone())) - } -} - -impl MockSubscriptionClient { - /// Publish the given event to all subscribers whose queries match that of - /// the event. - pub async fn publish(&mut self, ev: Event) -> Result<()> { - self.event_tx.send(ev).await - } - - async fn send_cmd(&mut self, cmd: SubscriptionDriverCmd) -> Result<()> { - self.cmd_tx.send(cmd).await - } - - /// Attempt to gracefully close this client. - pub async fn close(mut self) -> Result<()> { - self.send_cmd(SubscriptionDriverCmd::Terminate).await?; - self.driver_hdl.await.map_err(|e| { - Error::client_internal_error(format!( - "failed to terminate mock client driver task: {}", - e - )) - })? - } -} - -impl Default for MockSubscriptionClient { - fn default() -> Self { - let (event_tx, event_rx) = unbounded(); - let (cmd_tx, cmd_rx) = unbounded(); - let driver = MockSubscriptionClientDriver::new(event_rx, cmd_rx); - let driver_hdl = tokio::spawn(async move { driver.run().await }); - Self { - driver_hdl, - event_tx, - cmd_tx, - } - } -} - -#[derive(Debug)] -struct MockSubscriptionClientDriver { - event_rx: ChannelRx, - cmd_rx: ChannelRx, - router: SubscriptionRouter, -} - -impl MockSubscriptionClientDriver { - fn new(event_rx: ChannelRx, cmd_rx: ChannelRx) -> Self { - Self { - event_rx, - cmd_rx, - router: SubscriptionRouter::default(), - } - } - - async fn run(mut self) -> Result<()> { - loop { - tokio::select! { - Some(ev) = self.event_rx.recv() => self.router.publish(ev).await, - Some(cmd) = self.cmd_rx.recv() => match cmd { - SubscriptionDriverCmd::Subscribe { - id, - query, - event_tx, - result_tx, - } => self.subscribe(id, query, event_tx, result_tx).await?, - SubscriptionDriverCmd::Unsubscribe { - id, - query, - result_tx, - } => self.unsubscribe(id, query, result_tx).await?, - SubscriptionDriverCmd::Terminate => return Ok(()), - }, - } - } - } - - async fn subscribe( - &mut self, - id: SubscriptionId, - query: impl ToString, - event_tx: ChannelTx>, - mut result_tx: ChannelTx>, - ) -> Result<()> { - self.router.add(&id, query.to_string(), event_tx); - result_tx.send(Ok(())).await - } - - async fn unsubscribe( - &mut self, - id: SubscriptionId, - query: impl ToString, - mut result_tx: ChannelTx>, - ) -> Result<()> { - self.router.remove(&id, query.to_string()); - result_tx.send(Ok(())).await - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::query::EventType; - use crate::Response; - use futures::StreamExt; - use std::path::PathBuf; - use tokio::fs; - - async fn read_json_fixture(name: &str) -> String { - fs::read_to_string(PathBuf::from("./tests/support/").join(name.to_owned() + ".json")) - .await - .unwrap() - } - - async fn read_event(name: &str) -> Event { - Event::from_string(&read_json_fixture(name).await).unwrap() - } - - fn take_from_subs_and_terminate( - mut subs: Subscription, - count: usize, - ) -> JoinHandle>> { - tokio::spawn(async move { - let mut res = Vec::new(); - while let Some(res_ev) = subs.next().await { - res.push(res_ev); - if res.len() >= count { - break; - } - } - subs.terminate().await.unwrap(); - res - }) - } - - #[tokio::test] - async fn mock_subscription_client() { - let mut client = MockSubscriptionClient::default(); - let event1 = read_event("event_new_block_1").await; - let event2 = read_event("event_new_block_2").await; - let event3 = read_event("event_new_block_3").await; - let events = vec![event1, event2, event3]; - - let subs1 = client.subscribe(EventType::NewBlock.into()).await.unwrap(); - let subs2 = client.subscribe(EventType::NewBlock.into()).await.unwrap(); - assert_ne!(subs1.id, subs2.id); - - let subs1_events = take_from_subs_and_terminate(subs1, 3); - let subs2_events = take_from_subs_and_terminate(subs2, 3); - for ev in &events { - client.publish(ev.clone()).await.unwrap(); - } - - let subs1_events = subs1_events.await.unwrap(); - let subs2_events = subs2_events.await.unwrap(); - - assert_eq!(3, subs1_events.len()); - assert_eq!(3, subs2_events.len()); - - for i in 0..3 { - assert!(events[i].eq(subs1_events[i].as_ref().unwrap())); - } - - client.close().await.unwrap(); - } -} diff --git a/rpc/src/client/transport/router.rs b/rpc/src/client/transport/router.rs new file mode 100644 index 000000000..c2f59907a --- /dev/null +++ b/rpc/src/client/transport/router.rs @@ -0,0 +1,179 @@ +//! Event routing for subscriptions. + +use crate::client::subscription::SubscriptionTx; +use crate::event::Event; +use std::collections::{HashMap, HashSet}; +use tracing::debug; + +/// Provides a mechanism for tracking [`Subscription`]s and routing [`Event`]s +/// to those subscriptions. +/// +/// [`Subscription`]: struct.Subscription.html +/// [`Event`]: ./event/struct.Event.html +#[derive(Debug)] +pub struct SubscriptionRouter { + // A map of subscription queries to collections of subscription IDs and + // their result channels. Used for publishing events relating to a specific + // query. + subscriptions: HashMap>, +} + +impl SubscriptionRouter { + /// Publishes the given event to all of the subscriptions to which the + /// event is relevant. At present, it matches purely based on the query + /// associated with the event, and only queries that exactly match that of + /// the event's. + pub async fn publish(&mut self, ev: &Event) -> PublishResult { + let subs_for_query = match self.subscriptions.get_mut(&ev.query) { + Some(s) => s, + None => return PublishResult::NoSubscribers, + }; + // We assume here that any failure to publish an event is an indication + // that the receiver end of the channel has been dropped, which allows + // us to safely stop tracking the subscription. + let mut disconnected = HashSet::new(); + for (id, event_tx) in subs_for_query { + if let Err(e) = event_tx.send(Ok(ev.clone())).await { + disconnected.insert(id.clone()); + debug!( + "Automatically disconnecting subscription with ID {} for query \"{}\" due to failure to publish to it: {}", + id, ev.query, e + ); + } + } + // Obtain a mutable reference because the previous reference was + // consumed in the above for loop. We should panic if there are no + // longer any subscriptions for this query. + let subs_for_query = self.subscriptions.get_mut(&ev.query).unwrap(); + for id in disconnected { + subs_for_query.remove(&id); + } + if subs_for_query.is_empty() { + PublishResult::AllDisconnected + } else { + PublishResult::Success + } + } + + /// Immediately add a new subscription to the router without waiting for + /// confirmation. + pub fn add(&mut self, id: impl ToString, query: impl ToString, tx: SubscriptionTx) { + let query = query.to_string(); + let subs_for_query = match self.subscriptions.get_mut(&query) { + Some(s) => s, + None => { + self.subscriptions.insert(query.clone(), HashMap::new()); + self.subscriptions.get_mut(&query).unwrap() + } + }; + subs_for_query.insert(id.to_string(), tx); + } + + /// Returns the number of active subscriptions for the given query. + pub fn num_subscriptions_for_query(&self, query: impl ToString) -> usize { + self.subscriptions + .get(&query.to_string()) + .map(|subs_for_query| subs_for_query.len()) + .unwrap_or(0) + } + + /// Removes all the subscriptions relating to the given query. + pub fn remove_by_query(&mut self, query: impl ToString) -> usize { + self.subscriptions + .remove(&query.to_string()) + .map(|subs_for_query| subs_for_query.len()) + .unwrap_or(0) + } +} + +impl Default for SubscriptionRouter { + fn default() -> Self { + Self { + subscriptions: HashMap::new(), + } + } +} + +#[derive(Debug, Clone)] +pub enum PublishResult { + Success, + NoSubscribers, + AllDisconnected, +} + +#[cfg(test)] +mod test { + use super::*; + use crate::client::sync::{unbounded, ChannelRx}; + use crate::event::{Event, WrappedEvent}; + use crate::utils::uuid_str; + use std::path::PathBuf; + use tokio::fs; + use tokio::time::{self, Duration}; + + async fn read_json_fixture(name: &str) -> String { + fs::read_to_string(PathBuf::from("./tests/support/").join(name.to_owned() + ".json")) + .await + .unwrap() + } + + async fn read_event(name: &str) -> Event { + serde_json::from_str::(read_json_fixture(name).await.as_str()) + .unwrap() + .into_result() + .unwrap() + } + + async fn must_recv(ch: &mut ChannelRx, timeout_ms: u64) -> T { + let mut delay = time::delay_for(Duration::from_millis(timeout_ms)); + tokio::select! { + _ = &mut delay, if !delay.is_elapsed() => panic!("timed out waiting for recv"), + Some(v) = ch.recv() => v, + } + } + + async fn must_not_recv(ch: &mut ChannelRx, timeout_ms: u64) + where + T: std::fmt::Debug, + { + let mut delay = time::delay_for(Duration::from_millis(timeout_ms)); + tokio::select! { + _ = &mut delay, if !delay.is_elapsed() => (), + Some(v) = ch.recv() => panic!("got unexpected result from channel: {:?}", v), + } + } + + #[tokio::test] + async fn router_basic_pub_sub() { + let mut router = SubscriptionRouter::default(); + + let (subs1_id, subs2_id, subs3_id) = (uuid_str(), uuid_str(), uuid_str()); + let (subs1_event_tx, mut subs1_event_rx) = unbounded(); + let (subs2_event_tx, mut subs2_event_rx) = unbounded(); + let (subs3_event_tx, mut subs3_event_rx) = unbounded(); + + // Two subscriptions with the same query + router.add(subs1_id, "query1", subs1_event_tx); + router.add(subs2_id, "query1", subs2_event_tx); + // Another subscription with a different query + router.add(subs3_id, "query2", subs3_event_tx); + + let mut ev = read_event("event_new_block_1").await; + ev.query = "query1".into(); + router.publish(&ev).await; + + let subs1_ev = must_recv(&mut subs1_event_rx, 500).await.unwrap(); + let subs2_ev = must_recv(&mut subs2_event_rx, 500).await.unwrap(); + must_not_recv(&mut subs3_event_rx, 50).await; + assert_eq!(ev, subs1_ev); + assert_eq!(ev, subs2_ev); + + ev.query = "query2".into(); + router.publish(&ev).await; + + must_not_recv(&mut subs1_event_rx, 50).await; + must_not_recv(&mut subs2_event_rx, 50).await; + let subs3_ev = must_recv(&mut subs3_event_rx, 500).await.unwrap(); + assert_eq!(ev, subs3_ev); + } +} diff --git a/rpc/src/client/transport/utils.rs b/rpc/src/client/transport/utils.rs new file mode 100644 index 000000000..16419b1e0 --- /dev/null +++ b/rpc/src/client/transport/utils.rs @@ -0,0 +1,16 @@ +//! Client transport-related utilities. + +use crate::{Error, Result}; +use tendermint::net; + +/// Convenience method to extract the host and port associated with the given +/// address, but only if it's a TCP address (it fails otherwise). +pub(crate) fn get_tcp_host_port(address: net::Address) -> Result<(String, u16)> { + match address { + net::Address::Tcp { host, port, .. } => Ok((host, port)), + other => Err(Error::invalid_params(&format!( + "invalid RPC address: {:?}", + other + ))), + } +} diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index e33e1867c..354ecf9be 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -1,15 +1,17 @@ //! WebSocket-based clients for accessing Tendermint RPC functionality. -use crate::client::subscription::{ - SubscriptionDriverCmd, SubscriptionState, TwoPhaseSubscriptionRouter, -}; +use crate::client::subscription::SubscriptionTx; use crate::client::sync::{unbounded, ChannelRx, ChannelTx}; -use crate::client::transport::get_tcp_host_port; +use crate::client::transport::router::{PublishResult, SubscriptionRouter}; +use crate::client::transport::utils::get_tcp_host_port; use crate::endpoint::{subscribe, unsubscribe}; use crate::event::Event; use crate::query::Query; +use crate::request::Wrapper; +use crate::utils::uuid_str; use crate::{ - request, response, Error, Response, Result, Subscription, SubscriptionClient, SubscriptionId, + response, Client, Error, Id, Request, Response, Result, SimpleRequest, Subscription, + SubscriptionClient, }; use async_trait::async_trait; use async_tungstenite::tokio::{connect_async, TokioAdapter}; @@ -20,11 +22,12 @@ use async_tungstenite::WebSocketStream; use futures::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use std::collections::HashMap; use std::ops::Add; -use std::str::FromStr; use tendermint::net; use tokio::net::TcpStream; use tokio::time::{Duration, Instant}; +use tracing::{debug, error}; // WebSocket connection times out if we haven't heard anything at all from the // server in this long. @@ -39,15 +42,23 @@ const RECV_TIMEOUT: Duration = Duration::from_secs(RECV_TIMEOUT_SECONDS); // Taken from https://github.com/tendermint/tendermint/blob/309e29c245a01825fc9630103311fd04de99fa5e/rpc/jsonrpc/server/ws_handler.go#L28 const PING_INTERVAL: Duration = Duration::from_secs((RECV_TIMEOUT_SECONDS * 9) / 10); -/// Tendermint RPC client that provides [`Event`] subscription capabilities -/// over JSON-RPC over a WebSocket connection. +/// Tendermint RPC client that provides access to all RPC functionality +/// (including [`Event`] subscription) over a WebSocket connection. +/// +/// The `WebSocketClient` itself is effectively just a handle to its driver +/// (see the [`new`] method). The driver is the component of the client that +/// actually interacts with the remote RPC over the WebSocket connection. +/// The `WebSocketClient` can therefore be cloned into different asynchronous +/// contexts, effectively allowing for asynchronous access to the driver. /// -/// In order to not block the calling task, this client spawns an asynchronous -/// driver that continuously interacts with the actual WebSocket connection. -/// The `WebSocketClient` itself is effectively just a handle to this driver. -/// This driver is spawned as the client is created. +/// It is the caller's responsibility to spawn an asynchronous task in which to +/// execute the driver's [`run`] method. See the example below. /// -/// To terminate the client and the driver, simply use its [`close`] method. +/// Dropping [`Subscription`]s will automatically terminate them (the +/// `WebSocketClientDriver` detects a disconnected channel and removes the +/// subscription from its internal routing table). When all subscriptions to a +/// particular query have disconnected, the driver will automatically issue an +/// unsubscribe request to the remote RPC endpoint. /// /// ### Timeouts /// @@ -96,10 +107,6 @@ const PING_INTERVAL: Duration = Duration::from_secs((RECV_TIMEOUT_SECONDS * 9) / /// } /// } /// -/// // Sends an unsubscribe request via the WebSocket connection, but keeps -/// // the connection open. -/// subs.terminate().await.unwrap(); -/// /// // Signal to the driver to terminate. /// client.close().await.unwrap(); /// // Await the driver's termination to ensure proper connection closure. @@ -109,10 +116,13 @@ const PING_INTERVAL: Duration = Duration::from_secs((RECV_TIMEOUT_SECONDS * 9) / /// /// [`Event`]: ./event/struct.Event.html /// [`close`]: struct.WebSocketClient.html#method.close +/// [`new`]: struct.WebSocketClient.html#method.new +/// [`run`]: struct.WebSocketClientDriver.html#method.run +/// [`Subscription`]: struct.Subscription.html /// [tendermint-websocket-ping]: https://github.com/tendermint/tendermint/blob/309e29c245a01825fc9630103311fd04de99fa5e/rpc/jsonrpc/server/ws_handler.go#L28 #[derive(Debug, Clone)] pub struct WebSocketClient { - cmd_tx: ChannelTx, + cmd_tx: ChannelTx, } impl WebSocketClient { @@ -133,7 +143,7 @@ impl WebSocketClient { Ok((Self { cmd_tx }, driver)) } - async fn send_cmd(&mut self, cmd: SubscriptionDriverCmd) -> Result<()> { + async fn send_cmd(&mut self, cmd: DriverCommand) -> Result<()> { self.cmd_tx.send(cmd).await.map_err(|e| { Error::client_internal_error(format!("failed to send command to client driver: {}", e)) }) @@ -141,32 +151,114 @@ impl WebSocketClient { /// Signals to the driver that it must terminate. pub async fn close(mut self) -> Result<()> { - self.send_cmd(SubscriptionDriverCmd::Terminate).await + self.send_cmd(DriverCommand::Terminate).await + } +} + +#[async_trait] +impl Client for WebSocketClient { + async fn perform(&mut self, request: R) -> Result + where + R: SimpleRequest, + { + let wrapper = Wrapper::new(request); + let id = wrapper.id().clone().to_string(); + let wrapped_request = wrapper.into_json(); + let (response_tx, mut response_rx) = unbounded(); + self.cmd_tx + .send(DriverCommand::SimpleRequest(SimpleRequestCommand { + id, + wrapped_request, + response_tx, + })) + .await?; + let response = response_rx.recv().await.ok_or_else(|| { + Error::client_internal_error("failed to hear back from WebSocket driver".to_string()) + })??; + R::Response::from_string(response) } } #[async_trait] impl SubscriptionClient for WebSocketClient { async fn subscribe(&mut self, query: Query) -> Result { - let (event_tx, event_rx) = unbounded(); - let (result_tx, mut result_rx) = unbounded::>(); - let id = SubscriptionId::default(); - self.send_cmd(SubscriptionDriverCmd::Subscribe { - id: id.clone(), - query: query.clone(), - event_tx, - result_tx, - }) + let (subscription_tx, subscription_rx) = unbounded(); + let (response_tx, mut response_rx) = unbounded(); + // By default we use UUIDs to differentiate subscriptions + let id = uuid_str(); + self.send_cmd(DriverCommand::Subscribe(SubscribeCommand { + id: id.to_string(), + query: query.to_string(), + subscription_tx, + response_tx, + })) + .await?; + // Make sure our subscription request went through successfully. + let _ = response_rx.recv().await.ok_or_else(|| { + Error::client_internal_error("failed to hear back from WebSocket driver".to_string()) + })??; + Ok(Subscription::new(id, query, subscription_rx)) + } + + async fn unsubscribe(&mut self, query: Query) -> Result<()> { + let (response_tx, mut response_rx) = unbounded(); + self.send_cmd(DriverCommand::Unsubscribe(UnsubscribeCommand { + query: query.to_string(), + response_tx, + })) .await?; - // Wait to make sure our subscription request went through - // successfully. - result_rx.recv().await.ok_or_else(|| { + let _ = response_rx.recv().await.ok_or_else(|| { Error::client_internal_error("failed to hear back from WebSocket driver".to_string()) })??; - Ok(Subscription::new(id, query, event_rx, self.cmd_tx.clone())) + Ok(()) } } +// The different types of commands that can be sent from the WebSocketClient to +// the driver. +#[derive(Debug, Clone)] +enum DriverCommand { + // Initiate a subscription request. + Subscribe(SubscribeCommand), + // Initiate an unsubscribe request. + Unsubscribe(UnsubscribeCommand), + // For non-subscription-related requests. + SimpleRequest(SimpleRequestCommand), + Terminate, +} + +#[derive(Debug, Clone)] +struct SubscribeCommand { + // The desired ID for the outgoing JSON-RPC request. + id: String, + // The query for which we want to receive events. + query: String, + // Where to send subscription events. + subscription_tx: SubscriptionTx, + // Where to send the result of the subscription request. + response_tx: ChannelTx>, +} + +#[derive(Debug, Clone)] +struct UnsubscribeCommand { + // The query from which to unsubscribe. + query: String, + // Where to send the result of the unsubscribe request. + response_tx: ChannelTx>, +} + +#[derive(Debug, Clone)] +struct SimpleRequestCommand { + // The desired ID for the outgoing JSON-RPC request. Technically we + // could extract this from the wrapped request, but that would mean + // additional unnecessary computational resources for deserialization. + id: String, + // The wrapped and serialized JSON-RPC request. + wrapped_request: String, + // Where to send the result of the simple request. + response_tx: ChannelTx>, +} + #[derive(Serialize, Deserialize, Debug, Clone)] struct GenericJSONResponse(serde_json::Value); @@ -178,20 +270,27 @@ impl Response for GenericJSONResponse {} /// with the remote WebSocket endpoint. #[derive(Debug)] pub struct WebSocketClientDriver { + // The underlying WebSocket network connection. stream: WebSocketStream>, - router: TwoPhaseSubscriptionRouter, - cmd_rx: ChannelRx, + // Facilitates routing of events to their respective subscriptions. + router: SubscriptionRouter, + // How we receive incoming commands from the WebSocketClient. + cmd_rx: ChannelRx, + // Commands we've received but have not yet completed, indexed by their ID. + // A Terminate command is executed immediately. + pending_commands: HashMap, } impl WebSocketClientDriver { fn new( stream: WebSocketStream>, - cmd_rx: ChannelRx, + cmd_rx: ChannelRx, ) -> Self { Self { stream, - router: TwoPhaseSubscriptionRouter::default(), + router: SubscriptionRouter::default(), cmd_rx, + pending_commands: HashMap::new(), } } @@ -205,6 +304,8 @@ impl WebSocketClientDriver { tokio::select! { Some(res) = self.stream.next() => match res { Ok(msg) => { + // Reset the receive timeout every time we successfully + // receive a message from the remote endpoint. recv_timeout.reset(Instant::now().add(PING_INTERVAL)); self.handle_incoming_msg(msg).await? }, @@ -215,18 +316,10 @@ impl WebSocketClientDriver { ), }, Some(cmd) = self.cmd_rx.recv() => match cmd { - SubscriptionDriverCmd::Subscribe { - id, - query, - event_tx, - result_tx, - } => self.subscribe(id, query, event_tx, result_tx).await?, - SubscriptionDriverCmd::Unsubscribe { - id, - query, - result_tx, - } => self.unsubscribe(id, query, result_tx).await?, - SubscriptionDriverCmd::Terminate => return self.close().await, + DriverCommand::Subscribe(subs_cmd) => self.subscribe(subs_cmd).await?, + DriverCommand::Unsubscribe(unsubs_cmd) => self.unsubscribe(unsubs_cmd).await?, + DriverCommand::SimpleRequest(req_cmd) => self.simple_request(req_cmd).await?, + DriverCommand::Terminate => return self.close().await, }, _ = ping_interval.next() => self.ping().await?, _ = &mut recv_timeout => { @@ -245,122 +338,152 @@ impl WebSocketClientDriver { }) } - async fn send_request( - &mut self, - req: request::Wrapper, - result_tx: &mut ChannelTx>, - ) -> Result<()> + async fn send_request(&mut self, wrapper: Wrapper) -> Result<()> where - R: request::Request, + R: Request, { - if let Err(e) = self - .send_msg(Message::Text(serde_json::to_string_pretty(&req).unwrap())) - .await - { - let _ = result_tx.send(Err(e.clone())).await; + self.send_msg(Message::Text( + serde_json::to_string_pretty(&wrapper).unwrap(), + )) + .await + } + + async fn subscribe(&mut self, mut cmd: SubscribeCommand) -> Result<()> { + // If we already have an active subscription for the given query, + // there's no need to initiate another one. Just add this subscription + // to the router. + if self.router.num_subscriptions_for_query(cmd.query.clone()) > 0 { + let (id, query, subscription_tx, mut response_tx) = + (cmd.id, cmd.query, cmd.subscription_tx, cmd.response_tx); + self.router.add(id, query, subscription_tx); + return response_tx.send(Ok(())).await; + } + + // Otherwise, we need to initiate a subscription request. + let wrapper = Wrapper::new_with_id( + Id::Str(cmd.id.clone()), + subscribe::Request::new(cmd.query.clone()), + ); + if let Err(e) = self.send_request(wrapper).await { + cmd.response_tx.send(Err(e.clone())).await?; return Err(e); } + self.pending_commands + .insert(cmd.id.clone(), DriverCommand::Subscribe(cmd)); Ok(()) } - async fn subscribe( - &mut self, - id: SubscriptionId, - query: impl ToString, - event_tx: ChannelTx>, - mut result_tx: ChannelTx>, - ) -> Result<()> { - let query = query.to_string(); - let req = request::Wrapper::new_with_id( - id.clone().into(), - subscribe::Request::new(query.clone()), - ); - let _ = self.send_request(req, &mut result_tx).await; - self.router - .pending_add(id.as_str(), &id, query, event_tx, result_tx); + async fn unsubscribe(&mut self, mut cmd: UnsubscribeCommand) -> Result<()> { + // Terminate all subscriptions for this query immediately. This + // prioritizes acknowledgement of the caller's wishes over networking + // problems. + if self.router.remove_by_query(cmd.query.clone()) == 0 { + // If there were no subscriptions for this query, respond + // immediately. + cmd.response_tx.send(Ok(())).await?; + return Ok(()); + } + + // Unsubscribe requests can (and probably should) have distinct + // JSON-RPC IDs as compared to their subscription IDs. + let wrapper = Wrapper::new(unsubscribe::Request::new(cmd.query.clone())); + let req_id = wrapper.id().clone(); + if let Err(e) = self.send_request(wrapper).await { + cmd.response_tx.send(Err(e.clone())).await?; + return Err(e); + } + self.pending_commands + .insert(req_id.to_string(), DriverCommand::Unsubscribe(cmd)); Ok(()) } - async fn unsubscribe( - &mut self, - id: SubscriptionId, - query: impl ToString, - mut result_tx: ChannelTx>, - ) -> Result<()> { - let query = query.to_string(); - let req = request::Wrapper::new(unsubscribe::Request::new(query.clone())); - let req_id = req.id().to_string(); - let _ = self.send_request(req, &mut result_tx).await; - self.router.pending_remove(&req_id, &id, query, result_tx); + async fn simple_request(&mut self, mut cmd: SimpleRequestCommand) -> Result<()> { + if let Err(e) = self + .send_msg(Message::Text(cmd.wrapped_request.clone())) + .await + { + cmd.response_tx.send(Err(e.clone())).await?; + return Err(e); + } + self.pending_commands + .insert(cmd.id.clone(), DriverCommand::SimpleRequest(cmd)); Ok(()) } async fn handle_incoming_msg(&mut self, msg: Message) -> Result<()> { match msg { Message::Text(s) => self.handle_text_msg(s).await, - Message::Ping(v) => self.pong(v).await?, - _ => (), + Message::Ping(v) => self.pong(v).await, + _ => Ok(()), } - Ok(()) } - async fn handle_text_msg(&mut self, msg: String) { - match Event::from_string(&msg) { - Ok(ev) => { - self.router.publish(ev).await; + async fn handle_text_msg(&mut self, msg: String) -> Result<()> { + if let Ok(ev) = Event::from_string(&msg) { + self.publish_event(ev).await; + return Ok(()); + } + + let wrapper = match serde_json::from_str::>(&msg) { + Ok(w) => w, + Err(e) => { + error!( + "Failed to deserialize incoming message as a JSON-RPC message: {}", + e + ); + debug!("JSON-RPC message: {}", msg); + return Ok(()); } - Err(_) => { - if let Ok(wrapper) = - serde_json::from_str::>(&msg) - { - self.handle_generic_response(wrapper).await; - } + }; + let id = wrapper.id().to_string(); + if let Some(pending_cmd) = self.pending_commands.remove(&id) { + return self + .confirm_pending_command(pending_cmd, wrapper.into_result()) + .await; + }; + // We ignore incoming messages whose ID we don't recognize (could be + // relating to a fire-and-forget unsubscribe request - see the + // publish_event() method below). + Ok(()) + } + + async fn publish_event(&mut self, ev: Event) { + if let PublishResult::AllDisconnected = self.router.publish(&ev).await { + debug!( + "All subscribers for query \"{}\" have disconnected. Unsubscribing from query...", + ev.query + ); + // If all subscribers have disconnected for this query, we need to + // unsubscribe from it. We issue a fire-and-forget unsubscribe + // message. + if let Err(e) = self + .send_request(Wrapper::new(unsubscribe::Request::new(ev.query.clone()))) + .await + { + error!("Failed to send unsubscribe request: {}", e); } } } - async fn handle_generic_response(&mut self, wrapper: response::Wrapper) { - let req_id = wrapper.id().to_string(); - if let Some(state) = self.router.subscription_state(&req_id) { - match wrapper.into_result() { - Ok(_) => match state { - SubscriptionState::Pending => { - let _ = self.router.confirm_add(&req_id).await; - } - SubscriptionState::Cancelling => { - let _ = self.router.confirm_remove(&req_id).await; - } - SubscriptionState::Active => { - if let Some(event_tx) = self.router.get_active_subscription_mut( - &SubscriptionId::from_str(&req_id).unwrap(), - ) { - let _ = event_tx.send( - Err(Error::websocket_error( - "failed to parse incoming response from remote WebSocket endpoint - does this client support the remote's RPC version?", - )), - ).await; - } - } - }, - Err(e) => match state { - SubscriptionState::Pending => { - let _ = self.router.cancel_add(&req_id, e).await; - } - SubscriptionState::Cancelling => { - let _ = self.router.cancel_remove(&req_id, e).await; - } - // This is important to allow the remote endpoint to - // arbitrarily send error responses back to specific - // subscriptions. - SubscriptionState::Active => { - if let Some(event_tx) = self.router.get_active_subscription_mut( - &SubscriptionId::from_str(&req_id).unwrap(), - ) { - let _ = event_tx.send(Err(e)).await; - } - } - }, + async fn confirm_pending_command( + &mut self, + pending_cmd: DriverCommand, + result: Result, + ) -> Result<()> { + match pending_cmd { + DriverCommand::Subscribe(cmd) => { + let (id, query, subscription_tx, mut response_tx) = + (cmd.id, cmd.query, cmd.subscription_tx, cmd.response_tx); + self.router.add(id, query, subscription_tx); + response_tx.send(result.map(|_| ())).await + } + DriverCommand::Unsubscribe(mut cmd) => cmd.response_tx.send(result.map(|_| ())).await, + DriverCommand::SimpleRequest(mut cmd) => { + cmd.response_tx + .send(result.map(|v| serde_json::to_string(&v).unwrap())) + .await } + _ => Ok(()), } } @@ -392,11 +515,10 @@ impl WebSocketClientDriver { mod test { use super::*; use crate::query::EventType; - use crate::{Id, Method}; + use crate::{request, Id, Method}; use async_tungstenite::tokio::accept_async; use futures::StreamExt; use std::collections::HashMap; - use std::convert::TryInto; use std::path::PathBuf; use std::str::FromStr; use tokio::fs; @@ -540,7 +662,7 @@ mod test { terminate_rx: ChannelRx>, // A mapping of subscription queries to subscription IDs for this // connection. - subscriptions: HashMap, + subscriptions: HashMap, } impl TestServerHandlerDriver { @@ -579,7 +701,7 @@ mod test { Some(id) => id.clone(), None => return, }; - let _ = self.send(subs_id.into(), ev).await; + let _ = self.send(Id::Str(subs_id), ev).await; } async fn handle_incoming_msg(&mut self, msg: Message) -> Option> { @@ -611,7 +733,7 @@ mod test { self.add_subscription( req.params().query.clone(), - req.id().clone().try_into().unwrap(), + req.id().to_string(), ); self.send(req.id().clone(), subscribe::Response {}).await; } @@ -644,7 +766,7 @@ mod test { None } - fn add_subscription(&mut self, query: String, id: SubscriptionId) { + fn add_subscription(&mut self, query: String, id: String) { println!("Adding subscription with ID {} for query: {}", &id, &query); self.subscriptions.insert(query, id); } @@ -716,8 +838,6 @@ mod test { break; } } - println!("Terminating subscription..."); - subs.terminate().await.unwrap(); results }); diff --git a/rpc/src/endpoint/abci_info.rs b/rpc/src/endpoint/abci_info.rs index 89cdb1c47..3a02bb4b3 100644 --- a/rpc/src/endpoint/abci_info.rs +++ b/rpc/src/endpoint/abci_info.rs @@ -17,6 +17,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// ABCI information response #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/abci_query.rs b/rpc/src/endpoint/abci_query.rs index 5ad22f5d8..ee01d458d 100644 --- a/rpc/src/endpoint/abci_query.rs +++ b/rpc/src/endpoint/abci_query.rs @@ -49,6 +49,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// ABCI query response wrapper #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/block.rs b/rpc/src/endpoint/block.rs index 2442b0b08..9933f8ad2 100644 --- a/rpc/src/endpoint/block.rs +++ b/rpc/src/endpoint/block.rs @@ -30,6 +30,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Block responses #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/block_results.rs b/rpc/src/endpoint/block_results.rs index 7b8d066a6..5e3b70eed 100644 --- a/rpc/src/endpoint/block_results.rs +++ b/rpc/src/endpoint/block_results.rs @@ -30,6 +30,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// ABCI result response. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/blockchain.rs b/rpc/src/endpoint/blockchain.rs index a150aadb6..a334ae1f6 100644 --- a/rpc/src/endpoint/blockchain.rs +++ b/rpc/src/endpoint/blockchain.rs @@ -41,6 +41,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Block responses #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/broadcast/tx_async.rs b/rpc/src/endpoint/broadcast/tx_async.rs index b2e294a21..2cd5bd8ae 100644 --- a/rpc/src/endpoint/broadcast/tx_async.rs +++ b/rpc/src/endpoint/broadcast/tx_async.rs @@ -26,6 +26,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Response from either an async or sync transaction broadcast request. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/broadcast/tx_commit.rs b/rpc/src/endpoint/broadcast/tx_commit.rs index f0ebe5ead..e398905c9 100644 --- a/rpc/src/endpoint/broadcast/tx_commit.rs +++ b/rpc/src/endpoint/broadcast/tx_commit.rs @@ -34,6 +34,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Response from `/broadcast_tx_commit`. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/broadcast/tx_sync.rs b/rpc/src/endpoint/broadcast/tx_sync.rs index 57120b8e9..706d9d125 100644 --- a/rpc/src/endpoint/broadcast/tx_sync.rs +++ b/rpc/src/endpoint/broadcast/tx_sync.rs @@ -26,6 +26,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Response from either an async or sync transaction broadcast request. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/commit.rs b/rpc/src/endpoint/commit.rs index 1b8a222ee..d08fc06b9 100644 --- a/rpc/src/endpoint/commit.rs +++ b/rpc/src/endpoint/commit.rs @@ -27,6 +27,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Commit responses #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/evidence.rs b/rpc/src/endpoint/evidence.rs index 2a0390103..98bcb3158 100644 --- a/rpc/src/endpoint/evidence.rs +++ b/rpc/src/endpoint/evidence.rs @@ -1,8 +1,6 @@ //! `/broadcast_evidence`: broadcast an evidence. use crate::Method; -use crate::Request as RpcRequest; -use crate::Response as RpcResponse; use serde::{Deserialize, Serialize}; use tendermint::{abci::transaction, evidence::Evidence}; @@ -21,7 +19,7 @@ impl Request { } } -impl RpcRequest for Request { +impl crate::Request for Request { type Response = Response; fn method(&self) -> Method { @@ -29,6 +27,8 @@ impl RpcRequest for Request { } } +impl crate::SimpleRequest for Request {} + /// Response from either an evidence broadcast request. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { @@ -37,4 +37,4 @@ pub struct Response { pub hash: transaction::Hash, } -impl RpcResponse for Response {} +impl crate::Response for Response {} diff --git a/rpc/src/endpoint/genesis.rs b/rpc/src/endpoint/genesis.rs index a09f28f16..bbc75ab7e 100644 --- a/rpc/src/endpoint/genesis.rs +++ b/rpc/src/endpoint/genesis.rs @@ -16,6 +16,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Block responses #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/health.rs b/rpc/src/endpoint/health.rs index a4f24dc94..677dee9e1 100644 --- a/rpc/src/endpoint/health.rs +++ b/rpc/src/endpoint/health.rs @@ -14,6 +14,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Healthcheck responses #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response {} diff --git a/rpc/src/endpoint/net_info.rs b/rpc/src/endpoint/net_info.rs index 5be6b08cd..7efd58739 100644 --- a/rpc/src/endpoint/net_info.rs +++ b/rpc/src/endpoint/net_info.rs @@ -19,6 +19,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Net info responses #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/status.rs b/rpc/src/endpoint/status.rs index 375d1ae8a..b6c147367 100644 --- a/rpc/src/endpoint/status.rs +++ b/rpc/src/endpoint/status.rs @@ -16,6 +16,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Status responses #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/endpoint/subscribe.rs b/rpc/src/endpoint/subscribe.rs index b2040972e..1bd84d10b 100644 --- a/rpc/src/endpoint/subscribe.rs +++ b/rpc/src/endpoint/subscribe.rs @@ -1,9 +1,13 @@ //! `/subscribe` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; -use std::io::Read; /// Subscription request for events. +/// +/// A subscription request is not a [`SimpleRequest`], because it does not +/// return a simple, singular response. +/// +/// [`SimpleRequest`]: ../trait.SimpleRequest.html #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Request { pub query: String, @@ -29,16 +33,4 @@ impl crate::Request for Request { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response {} -/// Subscribe does not have a meaningful response at the moment. -impl crate::Response for Response { - /// We throw away response data JSON string so swallow errors and return the empty Response - fn from_string(_response: impl AsRef<[u8]>) -> Result { - Ok(Response {}) - } - - /// We throw away responses in `subscribe` to swallow errors from the `io::Reader` and provide - /// the Response - fn from_reader(_reader: impl Read) -> Result { - Ok(Response {}) - } -} +impl crate::Response for Response {} diff --git a/rpc/src/endpoint/unsubscribe.rs b/rpc/src/endpoint/unsubscribe.rs index c496201f3..0bae0dc66 100644 --- a/rpc/src/endpoint/unsubscribe.rs +++ b/rpc/src/endpoint/unsubscribe.rs @@ -1,9 +1,8 @@ //! `/unsubscribe` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; -use std::io::Read; -/// Subscription request for events. +/// Request to unsubscribe from events relating to a given query. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Request { pub query: String, @@ -28,17 +27,4 @@ impl crate::Request for Request { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response {} -/// Unsubscribe does not have a meaningful response at the moment. -impl crate::Response for Response { - /// We throw away response data JSON string so swallow errors and return - /// the empty Response - fn from_string(_response: impl AsRef<[u8]>) -> Result { - Ok(Response {}) - } - - /// We throw away responses in `unsubscribe` to swallow errors from the - /// `io::Reader` and provide the Response - fn from_reader(_reader: impl Read) -> Result { - Ok(Response {}) - } -} +impl crate::Response for Response {} diff --git a/rpc/src/endpoint/validators.rs b/rpc/src/endpoint/validators.rs index 3decf346c..f0fa70865 100644 --- a/rpc/src/endpoint/validators.rs +++ b/rpc/src/endpoint/validators.rs @@ -25,6 +25,8 @@ impl crate::Request for Request { } } +impl crate::SimpleRequest for Request {} + /// Validator responses #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response { diff --git a/rpc/src/id.rs b/rpc/src/id.rs index 8519aba96..1a21acb89 100644 --- a/rpc/src/id.rs +++ b/rpc/src/id.rs @@ -1,7 +1,8 @@ //! JSON-RPC IDs -use getrandom::getrandom; +use crate::utils::uuid_str; use serde::{Deserialize, Serialize}; +use std::fmt; /// JSON-RPC ID: request-specific identifier #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Ord, PartialOrd)] @@ -18,24 +19,16 @@ pub enum Id { impl Id { /// Create a JSON-RPC ID containing a UUID v4 (i.e. random) pub fn uuid_v4() -> Self { - let mut bytes = [0; 16]; - getrandom(&mut bytes).expect("RNG failure!"); - - let uuid = uuid::Builder::from_bytes(bytes) - .set_variant(uuid::Variant::RFC4122) - .set_version(uuid::Version::Random) - .build(); - - Id::Str(uuid.to_string()) + Self::Str(uuid_str()) } } -impl ToString for Id { - fn to_string(&self) -> String { +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Id::Num(i) => format!("{}", i), - Id::Str(s) => s.clone(), - Id::None => "none".to_string(), + Id::Num(i) => write!(f, "{}", i), + Id::Str(s) => write!(f, "{}", s), + Id::None => write!(f, ""), } } } diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 7750b30f9..e4ad54e88 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -34,8 +34,8 @@ mod client; #[cfg(any(feature = "http-client", feature = "websocket-client"))] pub use client::{ - Client, MockClient, MockRequestMatcher, MockRequestMethodMatcher, MockSubscriptionClient, - Subscription, SubscriptionClient, SubscriptionId, + Client, MockClient, MockRequestMatcher, MockRequestMethodMatcher, Subscription, + SubscriptionClient, }; #[cfg(feature = "http-client")] @@ -52,9 +52,10 @@ pub mod query; pub mod request; pub mod response; mod result; +mod utils; mod version; pub use self::{ - error::Error, id::Id, method::Method, request::Request, response::Response, result::Result, - version::Version, + error::Error, id::Id, method::Method, request::Request, request::SimpleRequest, + response::Response, result::Result, version::Version, }; diff --git a/rpc/src/request.rs b/rpc/src/request.rs index 17784e462..9aebd50c9 100644 --- a/rpc/src/request.rs +++ b/rpc/src/request.rs @@ -14,10 +14,20 @@ pub trait Request: Debug + DeserializeOwned + Serialize + Sized + Send { /// Serialize this request as JSON fn into_json(self) -> String { - serde_json::to_string_pretty(&Wrapper::new(self)).unwrap() + Wrapper::new(self).into_json() } } +/// Simple JSON-RPC requests which correlate with a single response from the +/// remote endpoint. +/// +/// An example of a request which is not simple would be the event subscription +/// request, which, on success, returns a [`Subscription`] and not just a +/// simple, singular response. +/// +/// [`Subscription`]: struct.Subscription.html +pub trait SimpleRequest: Request {} + /// JSON-RPC request wrapper (i.e. message envelope) #[derive(Debug, Deserialize, Serialize)] pub struct Wrapper { @@ -63,4 +73,8 @@ where pub fn params(&self) -> &R { &self.params } + + pub fn into_json(self) -> String { + serde_json::to_string_pretty(&self).unwrap() + } } diff --git a/rpc/src/utils.rs b/rpc/src/utils.rs new file mode 100644 index 000000000..76934a967 --- /dev/null +++ b/rpc/src/utils.rs @@ -0,0 +1,18 @@ +//! Utility methods for the Tendermint RPC crate. + +use getrandom::getrandom; + +/// Produce a string containing a UUID. +/// +/// Panics if random number generation fails. +pub fn uuid_str() -> String { + let mut bytes = [0; 16]; + getrandom(&mut bytes).expect("RNG failure!"); + + let uuid = uuid::Builder::from_bytes(bytes) + .set_variant(uuid::Variant::RFC4122) + .set_version(uuid::Version::Random) + .build(); + + uuid.to_string() +} diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index 24d911c9a..be62a13a2 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -219,7 +219,7 @@ mod rpc { } async fn simple_transaction_subscription() { - let rpc_client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap(); + let mut rpc_client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap(); let (mut subs_client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) .await @@ -288,7 +288,7 @@ mod rpc { } async fn concurrent_subscriptions() { - let rpc_client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap(); + let mut rpc_client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap(); let (mut subs_client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) .await From eef3d7b68da0209da0a71a1676d9e6eb0dee234d Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 21 Oct 2020 12:41:06 -0400 Subject: [PATCH 02/15] The http-client feature also needs the tracing lib Signed-off-by: Thane Thomson --- rpc/Cargo.toml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 21c335987..6cf88369a 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -25,7 +25,15 @@ all-features = true [features] default = [] -http-client = [ "async-trait", "futures", "http", "hyper", "tokio/fs", "tokio/macros" ] +http-client = [ + "async-trait", + "futures", + "http", + "hyper", + "tokio/fs", + "tokio/macros", + "tracing" +] secp256k1 = [ "tendermint/secp256k1" ] websocket-client = [ "async-trait", From b8628047f2a97e87b402bff1926da913b681ef33 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 21 Oct 2020 12:41:27 -0400 Subject: [PATCH 03/15] Make client mutable in example Signed-off-by: Thane Thomson --- rpc/src/client/transport/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/src/client/transport/http.rs b/rpc/src/client/transport/http.rs index 7becc8801..76e41e066 100644 --- a/rpc/src/client/transport/http.rs +++ b/rpc/src/client/transport/http.rs @@ -19,7 +19,7 @@ use tendermint::net; /// /// #[tokio::main] /// async fn main() { -/// let client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()) +/// let mut client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()) /// .unwrap(); /// /// let abci_info = client.abci_info() From e81759cdb488f82d0cd759e05066f7ec07f6b081 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 10 Nov 2020 16:50:44 -0500 Subject: [PATCH 04/15] Update CHANGELOG Signed-off-by: Thane Thomson --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 831f855e1..ac05fff0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,15 @@ - `[tendermint/proto-compiler]` Protobuf structs generator now also accepts commit IDs from the Tendermint Go repository ([#660]) +### IMPROVEMENTS: + +- `[rpc]` The `WebSocketClient` now adds support for all remaining RPC requests + by way of implementing the `Client` trait ([#646]) [#650]: https://github.com/informalsystems/tendermint-rs/issues/650 [#652]: https://github.com/informalsystems/tendermint-rs/pulls/652 [#639]: https://github.com/informalsystems/tendermint-rs/pull/639 +[#646]: https://github.com/informalsystems/tendermint-rs/pull/646 [#660]: https://github.com/informalsystems/tendermint-rs/issues/660 [#665]: https://github.com/informalsystems/tendermint-rs/issues/665 From b938499b06bbfaa234d721af8dd5edd40d554d2d Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 10 Nov 2020 17:14:24 -0500 Subject: [PATCH 05/15] Update ADR-008 to reflect recent changes Signed-off-by: Thane Thomson --- .../adr-008-event-subscription.md | 101 +++++++++++++----- .../assets/rpc-client-erd.graphml | 49 +++------ docs/architecture/assets/rpc-client-erd.png | Bin 56470 -> 53865 bytes 3 files changed, 85 insertions(+), 65 deletions(-) diff --git a/docs/architecture/adr-008-event-subscription.md b/docs/architecture/adr-008-event-subscription.md index a390c1143..14ad2b927 100644 --- a/docs/architecture/adr-008-event-subscription.md +++ b/docs/architecture/adr-008-event-subscription.md @@ -218,7 +218,7 @@ WebSocket connection to provide subscription functionality (the #[async_trait] pub trait SubscriptionClient { /// `/subscribe`: subscribe to receive events produced by the given query. - async fn subscribe(&mut self, query: String) -> Result; + async fn subscribe(&mut self, query: Query) -> Result; } ``` @@ -323,19 +323,23 @@ pub enum EventType { ValidatorSetUpdates, } -pub struct Condition { - key: String, - op: Operation, -} - -pub enum Operation { - Eq(Operand), - Lt(Operand), - Lte(Operand), - Gt(Operand), - Gte(Operand), - Contains(Operand), - Exists, +// A condition specifies a key (first parameter) and, depending on the +// operation, an value which is an operand of some kind. +pub enum Condition { + // Equals + Eq(String, Operand), + // Less than + Lt(String, Operand), + // Less than or equal to + Lte(String, Operand), + // Greater than + Gt(String, Operand), + // Greater than or equal to + Gte(String, Operand), + // Contains (to check if a key contains a certain sub-string) + Contains(String, String), + // Exists (to check if a key exists) + Exists(String), } // According to https://docs.tendermint.com/master/rpc/#/Websocket/subscribe, @@ -346,7 +350,8 @@ pub enum Operation { // operand types to the `Operand` enum, as this would improve ergonomics. pub enum Operand { String(String), - Integer(i64), + Signed(i64), + Unsigned(u64), Float(f64), Date(chrono::Date), DateTime(chrono::DateTime), @@ -361,7 +366,7 @@ track of all of the queries relating to a particular client. ```rust pub struct SubscriptionRouter { // A map of queries -> (map of subscription IDs -> result event tx channels) - subscriptions: HashMap>>>, + subscriptions: HashMap>, } ``` @@ -372,21 +377,61 @@ server [drops subscription IDs from events][tendermint-2949], which is likely if we want to conform more strictly to the [JSON-RPC standard for notifications][jsonrpc-notifications]. -#### Two-Phase Subscribe/Unsubscribe +### Handling Mixed Events and Responses + +Since a full client needs to implement both the `Client` and +`SubscriptionClient` traits, for certain transports (like a WebSocket +connection) we could end up receiving a mixture of events from subscriptions +and responses to RPC requests. To disambiguate these different types of +incoming messages, a simple mechanism is proposed for the +`WebSocketClientDriver` that keeps track of pending requests and only matures +them once it receives its corresponding response. -Due to the fact that a WebSocket connection lacks request/response semantics, -when managing multiple subscriptions from a single client we need to implement a -**two-phase subscription creation/removal process**: +```rust +pub struct WebSocketClientDriver { + // ... -1. An outgoing, but unconfirmed, subscribe/unsubscribe request is tracked. -2. The subscribe/unsubscribe request is confirmed or cancelled by a response - from the remote WebSocket server. + // Commands we've received but have not yet completed, indexed by their ID. + // A Terminate command is executed immediately. + pending_commands: HashMap, +} -The need for this two-phase subscribe/unsubscribe process is more clearly -illustrated in the following sequence diagram: +// The different types of requests that the WebSocketClient can send to its +// driver. +// +// Each of SubscribeCommand, UnsubscribeCommand and SimpleRequestCommand keep +// a response channel that allows for the driver to send a response later on +// when it receives a relevant one. +enum DriverCommand { + // Initiate a subscription request. + Subscribe(SubscribeCommand), + // Initiate an unsubscribe request. + Unsubscribe(UnsubscribeCommand), + // For non-subscription-related requests. + SimpleRequest(SimpleRequestCommand), + Terminate, +} +``` -![RPC client two-phase -subscribe/unsubscribe](./assets/rpc-client-two-phase-subscribe.png) +IDs of outgoing requests are randomly generated [UUIDv4] strings. + +The logic here is as follows: + +1. A call is made to `WebSocketClient::subscribe` or + `WebSocketClient::perform`. +2. The client sends the relevant `DriverCommand` to its driver via its internal + communication channel. +3. The driver receives the command, sends the relevant simple or subscription + request, and keeps track of the command in its `pending_commands` member + along with its ID. This allows the driver to continue handling outgoing + requests and incoming responses in the meantime. +4. If the driver receives a JSON-RPC message whose ID corresponds to an ID in + its `pending_commands` member, it assumes that response is relevant to that + command and sends back to the original caller by way of a channel stored in + one of the `SubscribeCommand`, `UnsubscribeCommand` or + `SimpleRequestCommand` structs. Failures are also communicated through this + same mechanism. +5. The pending command is evicted from the `pending_commands` member. ## Status @@ -433,4 +478,4 @@ None [futures-stream-mod]: https://docs.rs/futures/*/futures/stream/index.html [tendermint-2949]: https://github.com/tendermint/tendermint/issues/2949 [jsonrpc-notifications]: https://www.jsonrpc.org/specification#notification - +[UUIDv4]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random) diff --git a/docs/architecture/assets/rpc-client-erd.graphml b/docs/architecture/assets/rpc-client-erd.graphml index 3e4e86dd8..2e3bdd713 100644 --- a/docs/architecture/assets/rpc-client-erd.graphml +++ b/docs/architecture/assets/rpc-client-erd.graphml @@ -94,10 +94,10 @@ - + - TwoPhaseSubscriptionRouter + SubscriptionRouter @@ -216,18 +216,7 @@ Detail) - - - - Operation - - - - - - - - + Operand @@ -235,7 +224,7 @@ Detail) - + @@ -305,16 +294,14 @@ Detail) - + - - - + - Has/ -Uses + Has/ +Uses @@ -385,26 +372,15 @@ Uses - - - - Has - - - - - - - - + - Has + Has - + @@ -417,8 +393,7 @@ Uses - - + diff --git a/docs/architecture/assets/rpc-client-erd.png b/docs/architecture/assets/rpc-client-erd.png index f5f7678d77650b7e37f0771e1d75f852d5f182f4..11aa0591037d7f812b280a29af4902bbbe6d8bc8 100644 GIT binary patch literal 53865 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 literal 56470 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 From c3842058b999e038a13865660b55ad6fa9dbf3b2 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 10 Nov 2020 17:42:10 -0500 Subject: [PATCH 06/15] Fix minor bug in response handling Signed-off-by: Thane Thomson --- rpc/src/client/transport/websocket.rs | 45 +++++++++++++-------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index 354ecf9be..abaa64952 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -80,7 +80,8 @@ const PING_INTERVAL: Duration = Duration::from_secs((RECV_TIMEOUT_SECONDS * 9) / /// ## Examples /// /// ```rust,ignore -/// use tendermint_rpc::{WebSocketClient, SubscriptionClient}; +/// use tendermint::abci::Transaction; +/// use tendermint_rpc::{WebSocketClient, SubscriptionClient, Client}; /// use tendermint_rpc::query::EventType; /// use futures::StreamExt; /// @@ -91,6 +92,11 @@ const PING_INTERVAL: Duration = Duration::from_secs((RECV_TIMEOUT_SECONDS * 9) / /// .unwrap(); /// let driver_handle = tokio::spawn(async move { driver.run().await }); /// +/// // Standard client functionality +/// let tx = format!("some-key=some-value"); +/// client.broadcast_tx_async(Transaction::from(tx.into_bytes())).await.unwrap(); +/// +/// // Subscription functionality /// let mut subs = client.subscribe(EventType::NewBlock.into()) /// .await /// .unwrap(); @@ -103,7 +109,7 @@ const PING_INTERVAL: Duration = Duration::from_secs((RECV_TIMEOUT_SECONDS * 9) / /// println!("Got event: {:?}", ev); /// ev_count -= 1; /// if ev_count < 0 { -/// break +/// break; /// } /// } /// @@ -165,13 +171,12 @@ impl Client for WebSocketClient { let id = wrapper.id().clone().to_string(); let wrapped_request = wrapper.into_json(); let (response_tx, mut response_rx) = unbounded(); - self.cmd_tx - .send(DriverCommand::SimpleRequest(SimpleRequestCommand { - id, - wrapped_request, - response_tx, - })) - .await?; + self.send_cmd(DriverCommand::SimpleRequest(SimpleRequestCommand { + id, + wrapped_request, + response_tx, + })) + .await?; let response = response_rx.recv().await.ok_or_else(|| { Error::client_internal_error("failed to hear back from WebSocket driver".to_string()) })??; @@ -260,9 +265,9 @@ struct SimpleRequestCommand { } #[derive(Serialize, Deserialize, Debug, Clone)] -struct GenericJSONResponse(serde_json::Value); +struct GenericJsonResponse(serde_json::Value); -impl Response for GenericJSONResponse {} +impl Response for GenericJsonResponse {} /// Drives the WebSocket connection for a `WebSocketClient` instance. /// @@ -424,7 +429,7 @@ impl WebSocketClientDriver { return Ok(()); } - let wrapper = match serde_json::from_str::>(&msg) { + let wrapper = match serde_json::from_str::>(&msg) { Ok(w) => w, Err(e) => { error!( @@ -437,9 +442,7 @@ impl WebSocketClientDriver { }; let id = wrapper.id().to_string(); if let Some(pending_cmd) = self.pending_commands.remove(&id) { - return self - .confirm_pending_command(pending_cmd, wrapper.into_result()) - .await; + return self.confirm_pending_command(pending_cmd, msg).await; }; // We ignore incoming messages whose ID we don't recognize (could be // relating to a fire-and-forget unsubscribe request - see the @@ -468,21 +471,17 @@ impl WebSocketClientDriver { async fn confirm_pending_command( &mut self, pending_cmd: DriverCommand, - result: Result, + response: String, ) -> Result<()> { match pending_cmd { DriverCommand::Subscribe(cmd) => { let (id, query, subscription_tx, mut response_tx) = (cmd.id, cmd.query, cmd.subscription_tx, cmd.response_tx); self.router.add(id, query, subscription_tx); - response_tx.send(result.map(|_| ())).await - } - DriverCommand::Unsubscribe(mut cmd) => cmd.response_tx.send(result.map(|_| ())).await, - DriverCommand::SimpleRequest(mut cmd) => { - cmd.response_tx - .send(result.map(|v| serde_json::to_string(&v).unwrap())) - .await + response_tx.send(Ok(())).await } + DriverCommand::Unsubscribe(mut cmd) => cmd.response_tx.send(Ok(())).await, + DriverCommand::SimpleRequest(mut cmd) => cmd.response_tx.send(Ok(response)).await, _ => Ok(()), } } From 5bf807496865944bb5355a6ba5eab5b022280dbf Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 10 Nov 2020 17:42:31 -0500 Subject: [PATCH 07/15] Update tests to use new WebSocketClient functionality Signed-off-by: Thane Thomson --- tendermint/tests/integration.rs | 35 +++++++++++++++------------------ 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index dd9309240..4275156d9 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -219,23 +219,24 @@ mod rpc { } async fn simple_transaction_subscription() { - let mut rpc_client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap(); - let (mut subs_client, driver) = - WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) - .await - .unwrap(); + let (mut client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) + .await + .unwrap(); let driver_handle = tokio::spawn(async move { driver.run().await }); - let mut subs = subs_client.subscribe(EventType::Tx.into()).await.unwrap(); + let mut subs = client.subscribe(EventType::Tx.into()).await.unwrap(); // We use Id::uuid_v4() here as a quick hack to generate a random value. let mut expected_tx_values = (0..10_u32) .map(|_| Id::uuid_v4().to_string()) .collect::>(); let broadcast_tx_values = expected_tx_values.clone(); + // We can clone the WebSocket client, because it's just a handle to the + // driver. + let mut inner_client = client.clone(); tokio::spawn(async move { for (tx_count, val) in broadcast_tx_values.into_iter().enumerate() { let tx = format!("tx{}={}", tx_count, val); - rpc_client + inner_client .broadcast_tx_async(Transaction::from(tx.into_bytes())) .await .unwrap(); @@ -283,22 +284,17 @@ mod rpc { } } - subs_client.close().await.unwrap(); + client.close().await.unwrap(); let _ = driver_handle.await.unwrap(); } async fn concurrent_subscriptions() { - let mut rpc_client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap(); - let (mut subs_client, driver) = - WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) - .await - .unwrap(); - let driver_handle = tokio::spawn(async move { driver.run().await }); - let new_block_subs = subs_client - .subscribe(EventType::NewBlock.into()) + let (mut client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) .await .unwrap(); - let tx_subs = subs_client.subscribe(EventType::Tx.into()).await.unwrap(); + let driver_handle = tokio::spawn(async move { driver.run().await }); + let new_block_subs = client.subscribe(EventType::NewBlock.into()).await.unwrap(); + let tx_subs = client.subscribe(EventType::Tx.into()).await.unwrap(); // We use Id::uuid_v4() here as a quick hack to generate a random value. let mut expected_tx_values = (0..10_u32) @@ -307,10 +303,11 @@ mod rpc { let broadcast_tx_values = expected_tx_values.clone(); let mut expected_new_blocks = 5_i32; + let mut inner_client = client.clone(); tokio::spawn(async move { for (tx_count, val) in broadcast_tx_values.into_iter().enumerate() { let tx = format!("tx{}={}", tx_count, val); - rpc_client + inner_client .broadcast_tx_async(Transaction::from(tx.into_bytes())) .await .unwrap(); @@ -348,7 +345,7 @@ mod rpc { } } - subs_client.close().await.unwrap(); + client.close().await.unwrap(); let _ = driver_handle.await.unwrap(); } } From 8f5fb248d8f6e96e3f9289cc52a8eac0eef7c63e Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 10 Nov 2020 18:07:26 -0500 Subject: [PATCH 08/15] Fix/ignore clippy warnings Signed-off-by: Thane Thomson --- rpc/src/client/sync.rs | 1 + rpc/src/client/transport/router.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/rpc/src/client/sync.rs b/rpc/src/client/sync.rs index fa0d7a6a1..0d053ad8b 100644 --- a/rpc/src/client/sync.rs +++ b/rpc/src/client/sync.rs @@ -39,6 +39,7 @@ pub struct ChannelRx(mpsc::UnboundedReceiver); impl ChannelRx { /// Wait indefinitely until we receive a value from the channel (or the /// channel is closed). + #[allow(dead_code)] pub async fn recv(&mut self) -> Option { self.0.recv().await } diff --git a/rpc/src/client/transport/router.rs b/rpc/src/client/transport/router.rs index c2f59907a..ede0dc3c3 100644 --- a/rpc/src/client/transport/router.rs +++ b/rpc/src/client/transport/router.rs @@ -69,18 +69,21 @@ impl SubscriptionRouter { subs_for_query.insert(id.to_string(), tx); } - /// Returns the number of active subscriptions for the given query. - pub fn num_subscriptions_for_query(&self, query: impl ToString) -> usize { + /// Removes all the subscriptions relating to the given query. + pub fn remove_by_query(&mut self, query: impl ToString) -> usize { self.subscriptions - .get(&query.to_string()) + .remove(&query.to_string()) .map(|subs_for_query| subs_for_query.len()) .unwrap_or(0) } +} - /// Removes all the subscriptions relating to the given query. - pub fn remove_by_query(&mut self, query: impl ToString) -> usize { +#[cfg(feature = "websocket-client")] +impl SubscriptionRouter { + /// Returns the number of active subscriptions for the given query. + pub fn num_subscriptions_for_query(&self, query: impl ToString) -> usize { self.subscriptions - .remove(&query.to_string()) + .get(&query.to_string()) .map(|subs_for_query| subs_for_query.len()) .unwrap_or(0) } From f04b8b44bd2f7ac1fe33702bf9a7a4218af4cff1 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 10 Nov 2020 18:07:41 -0500 Subject: [PATCH 09/15] Fix documentation for rpc crate Signed-off-by: Thane Thomson --- rpc/src/lib.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index af7f488b0..c40144dea 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -8,23 +8,21 @@ //! //! Two features are provided at present. //! -//! | Feature | Description | -//! | ------- | ----------- | -//! | `http-client` | Provides [`HttpClient`], which is a basic RPC client that interacts with -//! remote Tendermint nodes via **JSON-RPC over HTTP**. This client does not provide [`Event`] -//! subscription functionality. See the [Tendermint RPC] for more details. | | `websocket-client` | -//! Provides [`WebSocketClient`], which currently only provides [`Event`] subscription functionality -//! over a WebSocket connection. See the [`/subscribe` endpoint] in the Tendermint RPC for more -//! details. This client does not yet provide access to the RPC methods provided by the [`Client`] -//! trait (this is planned for a future release). | +//! * `http-client` - Provides [`HttpClient`], which is a basic RPC client that +//! interacts with remote Tendermint nodes via **JSON-RPC over HTTP**. This +//! client does not provide [`Event`] subscription functionality. See the +//! [Tendermint RPC] for more details. +//! * `websocket-client` - Provides [`WebSocketClient`], which provides full +//! client functionality, including general RPC functionality (such as that +//! provided by `HttpClient`) as well as [`Event`] subscription +//! functionality. //! //! ### Mock Clients //! //! Mock clients are included when either of the `http-client` or //! `websocket-client` features are enabled to aid in testing. This includes -//! [`MockClient`], which only implements [`Client`] (no subscription -//! functionality), and [`MockSubscriptionClient`], which helps you simulate -//! subscriptions to events being generated by a Tendermint node. +//! [`MockClient`], which implements both [`Client`] and [`SubscriptionClient`] +//! traits. //! //! [`Client`]: trait.Client.html //! [`HttpClient`]: struct.HttpClient.html @@ -33,7 +31,6 @@ //! [Tendermint RPC]: https://docs.tendermint.com/master/rpc/ //! [`/subscribe` endpoint]: https://docs.tendermint.com/master/rpc/#/Websocket/subscribe //! [`MockClient`]: struct.MockClient.html -//! [`MockSubscriptionClient`]: struct.MockSubscriptionClient.html #[cfg(any(feature = "http-client", feature = "websocket-client"))] mod client; From f6eaf88eed179259f2671102f10634108e67bc72 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 10 Nov 2020 18:09:22 -0500 Subject: [PATCH 10/15] Fix broken link in crate docs Signed-off-by: Thane Thomson --- rpc/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index c40144dea..2b692992f 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -25,6 +25,7 @@ //! traits. //! //! [`Client`]: trait.Client.html +//! [`SubscriptionClient`]: trait.SubscriptionClient.html //! [`HttpClient`]: struct.HttpClient.html //! [`Event`]: event/struct.Event.html //! [`WebSocketClient`]: struct.WebSocketClient.html From cb8234290d76e8ec530602ba477c89d73081cbc3 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 10 Nov 2020 18:22:22 -0500 Subject: [PATCH 11/15] Update RPC repo docs Signed-off-by: Thane Thomson --- rpc/README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/rpc/README.md b/rpc/README.md index dc55fc617..f35f68f77 100644 --- a/rpc/README.md +++ b/rpc/README.md @@ -23,18 +23,21 @@ select when using it. Two features are provided at present. -| Feature | Description | -| ------- | ----------- | -| `http-client` | Provides `HttpClient`, which is a basic RPC client that interacts with remote Tendermint nodes via **JSON-RPC over HTTP**. This client does not provide `Event` subscription functionality. See the [Tendermint RPC] for more details. | -| `websocket-client` | Provides `WebSocketClient`, which currently only provides `Event` subscription functionality over a WebSocket connection. See the [`/subscribe` endpoint] in the Tendermint RPC for more details. This client does not yet provide access to the RPC methods provided by the `Client` trait (this is planned for a future release). | +* `http-client` - Provides `HttpClient`, which is a basic RPC client that + interacts with remote Tendermint nodes via **JSON-RPC over HTTP**. This + client does not provide `Event` subscription functionality. See the + [Tendermint RPC] for more details. +* `websocket-client` - Provides `WebSocketClient`, which provides full + client functionality, including general RPC functionality (such as that + provided by `HttpClient`) as well as `Event` subscription + functionality. ### Mock Clients Mock clients are included when either of the `http-client` or `websocket-client` features are enabled to aid in testing. This includes -`MockClient`, which only implements `Client` (no subscription -functionality), and `MockSubscriptionClient`, which helps you simulate -subscriptions to events being generated by a Tendermint node. +`MockClient`, which implements both `Client` and `SubscriptionClient` +traits. ### Related From 56ce9f4ed202e663bcd761dbf0772ca4e507d0fd Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 20 Nov 2020 11:21:59 -0500 Subject: [PATCH 12/15] Refactor to remove ChannelTx mutability ChannelTx::send now no longer needs to be mutable because the underlying channel has no mutability requirement. This leads to viral changes throughout the interfaces and clients (positive ones). ChannelTx::send also doesn't need to be async, because it relies on an unbounded channel. This also has some viral implications for other methods throughout the RPC client. If, in future, we want to support bounded channels, then we can consider making it async again. But we probably shouldn't make it async if we don't need it to be. Signed-off-by: Thane Thomson --- light-client/src/components/io.rs | 4 +- rpc/src/client.rs | 44 ++++----- rpc/src/client/subscription.rs | 4 +- rpc/src/client/sync.rs | 2 +- rpc/src/client/transport/http.rs | 4 +- rpc/src/client/transport/mock.rs | 135 ++++++++++++++++++++++---- rpc/src/client/transport/router.rs | 8 +- rpc/src/client/transport/websocket.rs | 83 ++++++++-------- tendermint/tests/integration.rs | 16 +-- 9 files changed, 195 insertions(+), 105 deletions(-) diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index 416331819..fd869ee17 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -133,7 +133,7 @@ mod prod { } fn fetch_signed_header(&self, height: AtHeight) -> Result { - let mut client = self.rpc_client.clone(); + let client = self.rpc_client.clone(); let res = block_on(self.timeout, async move { match height { AtHeight::Highest => client.latest_commit().await, @@ -155,7 +155,7 @@ mod prod { AtHeight::At(height) => height, }; - let mut client = self.rpc_client.clone(); + let client = self.rpc_client.clone(); let res = block_on(self.timeout, async move { client.validators(height).await })?; match res { diff --git a/rpc/src/client.rs b/rpc/src/client.rs index ac34908d2..d49828851 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -30,13 +30,13 @@ use tendermint::Genesis; #[async_trait] pub trait Client { /// `/abci_info`: get information about the ABCI application. - async fn abci_info(&mut self) -> Result { + async fn abci_info(&self) -> Result { Ok(self.perform(abci_info::Request).await?.response) } /// `/abci_query`: query the ABCI application async fn abci_query( - &mut self, + &self, path: Option, data: V, height: Option, @@ -52,7 +52,7 @@ pub trait Client { } /// `/block`: get block at a given height. - async fn block(&mut self, height: H) -> Result + async fn block(&self, height: H) -> Result where H: Into + Send, { @@ -60,12 +60,12 @@ pub trait Client { } /// `/block`: get the latest block. - async fn latest_block(&mut self) -> Result { + async fn latest_block(&self) -> Result { self.perform(block::Request::default()).await } /// `/block_results`: get ABCI results for a block at a particular height. - async fn block_results(&mut self, height: H) -> Result + async fn block_results(&self, height: H) -> Result where H: Into + Send, { @@ -74,7 +74,7 @@ pub trait Client { } /// `/block_results`: get ABCI results for the latest block. - async fn latest_block_results(&mut self) -> Result { + async fn latest_block_results(&self) -> Result { self.perform(block_results::Request::default()).await } @@ -83,7 +83,7 @@ pub trait Client { /// Block headers are returned in descending order (highest first). /// /// Returns at most 20 items. - async fn blockchain(&mut self, min: H, max: H) -> Result + async fn blockchain(&self, min: H, max: H) -> Result where H: Into + Send, { @@ -93,30 +93,24 @@ pub trait Client { } /// `/broadcast_tx_async`: broadcast a transaction, returning immediately. - async fn broadcast_tx_async( - &mut self, - tx: Transaction, - ) -> Result { + async fn broadcast_tx_async(&self, tx: Transaction) -> Result { self.perform(broadcast::tx_async::Request::new(tx)).await } /// `/broadcast_tx_sync`: broadcast a transaction, returning the response /// from `CheckTx`. - async fn broadcast_tx_sync(&mut self, tx: Transaction) -> Result { + async fn broadcast_tx_sync(&self, tx: Transaction) -> Result { self.perform(broadcast::tx_sync::Request::new(tx)).await } /// `/broadcast_tx_sync`: broadcast a transaction, returning the response /// from `CheckTx`. - async fn broadcast_tx_commit( - &mut self, - tx: Transaction, - ) -> Result { + async fn broadcast_tx_commit(&self, tx: Transaction) -> Result { self.perform(broadcast::tx_commit::Request::new(tx)).await } /// `/commit`: get block commit at a given height. - async fn commit(&mut self, height: H) -> Result + async fn commit(&self, height: H) -> Result where H: Into + Send, { @@ -124,7 +118,7 @@ pub trait Client { } /// `/validators`: get validators a given height. - async fn validators(&mut self, height: H) -> Result + async fn validators(&self, height: H) -> Result where H: Into + Send, { @@ -132,41 +126,41 @@ pub trait Client { } /// `/commit`: get the latest block commit - async fn latest_commit(&mut self) -> Result { + async fn latest_commit(&self) -> Result { self.perform(commit::Request::default()).await } /// `/health`: get node health. /// /// Returns empty result (200 OK) on success, no response in case of an error. - async fn health(&mut self) -> Result<()> { + async fn health(&self) -> Result<()> { self.perform(health::Request).await?; Ok(()) } /// `/genesis`: get genesis file. - async fn genesis(&mut self) -> Result { + async fn genesis(&self) -> Result { Ok(self.perform(genesis::Request).await?.genesis) } /// `/net_info`: obtain information about P2P and other network connections. - async fn net_info(&mut self) -> Result { + async fn net_info(&self) -> Result { self.perform(net_info::Request).await } /// `/status`: get Tendermint status including node info, pubkey, latest /// block hash, app hash, block height and time. - async fn status(&mut self) -> Result { + async fn status(&self) -> Result { self.perform(status::Request).await } /// `/broadcast_evidence`: broadcast an evidence. - async fn broadcast_evidence(&mut self, e: Evidence) -> Result { + async fn broadcast_evidence(&self, e: Evidence) -> Result { self.perform(evidence::Request::new(e)).await } /// Perform a request against the RPC endpoint - async fn perform(&mut self, request: R) -> Result + async fn perform(&self, request: R) -> Result where R: SimpleRequest; } diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 9322929c2..07fb2805c 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -17,7 +17,7 @@ use std::pin::Pin; #[async_trait] pub trait SubscriptionClient { /// `/subscribe`: subscribe to receive events produced by the given query. - async fn subscribe(&mut self, query: Query) -> Result; + async fn subscribe(&self, query: Query) -> Result; /// `/unsubscribe`: unsubscribe from events relating to the given query. /// @@ -30,7 +30,7 @@ pub trait SubscriptionClient { /// [`Subscription`]: struct.Subscription.html /// [`Query`]: struct.Query.html /// [`select_all`]: https://docs.rs/futures/*/futures/stream/fn.select_all.html - async fn unsubscribe(&mut self, query: Query) -> Result<()>; + async fn unsubscribe(&self, query: Query) -> Result<()>; } pub(crate) type SubscriptionTx = ChannelTx>; diff --git a/rpc/src/client/sync.rs b/rpc/src/client/sync.rs index 98d3fec40..275a9b8b8 100644 --- a/rpc/src/client/sync.rs +++ b/rpc/src/client/sync.rs @@ -27,7 +27,7 @@ pub fn unbounded() -> (ChannelTx, ChannelRx) { pub struct ChannelTx(mpsc::UnboundedSender); impl ChannelTx { - pub async fn send(&mut self, value: T) -> Result<()> { + pub fn send(&self, value: T) -> Result<()> { self.0.send(value).map_err(|e| { Error::client_internal_error(format!( "failed to send message to internal channel: {}", diff --git a/rpc/src/client/transport/http.rs b/rpc/src/client/transport/http.rs index 41274478d..7a4f076cd 100644 --- a/rpc/src/client/transport/http.rs +++ b/rpc/src/client/transport/http.rs @@ -19,7 +19,7 @@ use tendermint::net; /// /// #[tokio::main] /// async fn main() { -/// let mut client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()) +/// let client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()) /// .unwrap(); /// /// let abci_info = client.abci_info() @@ -41,7 +41,7 @@ pub struct HttpClient { #[async_trait] impl Client for HttpClient { - async fn perform(&mut self, request: R) -> Result + async fn perform(&self, request: R) -> Result where R: SimpleRequest, { diff --git a/rpc/src/client/transport/mock.rs b/rpc/src/client/transport/mock.rs index 16c4600c0..04bad190e 100644 --- a/rpc/src/client/transport/mock.rs +++ b/rpc/src/client/transport/mock.rs @@ -1,6 +1,7 @@ //! Mock client implementation for use in testing. -use crate::client::sync::unbounded; +use crate::client::subscription::SubscriptionTx; +use crate::client::sync::{unbounded, ChannelRx, ChannelTx}; use crate::client::transport::router::SubscriptionRouter; use crate::event::Event; use crate::query::Query; @@ -34,22 +35,26 @@ use std::collections::HashMap; /// async fn main() { /// let matcher = MockRequestMethodMatcher::default() /// .map(Method::AbciInfo, Ok(ABCI_INFO_RESPONSE.to_string())); -/// let mut client = MockClient::new(matcher); +/// let (client, driver) = MockClient::new(matcher); +/// let driver_hdl = tokio::spawn(async move { driver.run().await }); /// /// let abci_info = client.abci_info().await.unwrap(); /// println!("Got mock ABCI info: {:?}", abci_info); /// assert_eq!("GaiaApp".to_string(), abci_info.data); +/// +/// client.close(); +/// driver_hdl.await.unwrap(); /// } /// ``` #[derive(Debug)] pub struct MockClient { matcher: M, - router: SubscriptionRouter, + driver_tx: ChannelTx, } #[async_trait] impl Client for MockClient { - async fn perform(&mut self, request: R) -> Result + async fn perform(&self, request: R) -> Result where R: Request, { @@ -61,32 +66,117 @@ impl Client for MockClient { impl MockClient { /// Create a new mock RPC client using the given request matcher. - pub fn new(matcher: M) -> Self { - Self { - matcher, - router: SubscriptionRouter::default(), - } + pub fn new(matcher: M) -> (Self, MockClientDriver) { + let (driver_tx, driver_rx) = unbounded(); + ( + Self { matcher, driver_tx }, + MockClientDriver::new(driver_rx), + ) } /// Publishes the given event to all subscribers whose query exactly /// matches that of the event. - pub async fn publish(&mut self, ev: &Event) { - let _ = self.router.publish(ev).await; + pub fn publish(&self, ev: &Event) { + self.driver_tx + .send(DriverCommand::Publish(Box::new(ev.clone()))) + .unwrap(); + } + + /// Signal to the mock client's driver to terminate. + pub fn close(self) { + self.driver_tx.send(DriverCommand::Terminate).unwrap(); } } #[async_trait] impl SubscriptionClient for MockClient { - async fn subscribe(&mut self, query: Query) -> Result { + async fn subscribe(&self, query: Query) -> Result { let id = uuid_str(); let (subs_tx, subs_rx) = unbounded(); - self.router.add(id.clone(), query.clone(), subs_tx); + let (result_tx, mut result_rx) = unbounded(); + self.driver_tx.send(DriverCommand::Subscribe { + id: id.clone(), + query: query.clone(), + subscription_tx: subs_tx, + result_tx, + })?; + result_rx.recv().await.unwrap()?; Ok(Subscription::new(id, query, subs_rx)) } - async fn unsubscribe(&mut self, query: Query) -> Result<()> { + async fn unsubscribe(&self, query: Query) -> Result<()> { + let (result_tx, mut result_rx) = unbounded(); + self.driver_tx + .send(DriverCommand::Unsubscribe { query, result_tx })?; + result_rx.recv().await.unwrap() + } +} + +#[derive(Debug)] +pub enum DriverCommand { + Subscribe { + id: String, + query: Query, + subscription_tx: SubscriptionTx, + result_tx: ChannelTx>, + }, + Unsubscribe { + query: Query, + result_tx: ChannelTx>, + }, + Publish(Box), + Terminate, +} + +#[derive(Debug)] +pub struct MockClientDriver { + router: SubscriptionRouter, + rx: ChannelRx, +} + +impl MockClientDriver { + pub fn new(rx: ChannelRx) -> Self { + Self { + router: SubscriptionRouter::default(), + rx, + } + } + + pub async fn run(mut self) -> Result<()> { + loop { + tokio::select! { + Some(cmd) = self.rx.recv() => match cmd { + DriverCommand::Subscribe { id, query, subscription_tx, result_tx } => { + self.subscribe(id, query, subscription_tx, result_tx); + } + DriverCommand::Unsubscribe { query, result_tx } => { + self.unsubscribe(query, result_tx); + } + DriverCommand::Publish(event) => self.publish(event.as_ref()), + DriverCommand::Terminate => return Ok(()), + } + } + } + } + + fn subscribe( + &mut self, + id: String, + query: Query, + subscription_tx: SubscriptionTx, + result_tx: ChannelTx>, + ) { + self.router.add(id, query, subscription_tx); + result_tx.send(Ok(())).unwrap(); + } + + fn unsubscribe(&mut self, query: Query, result_tx: ChannelTx>) { self.router.remove_by_query(query); - Ok(()) + result_tx.send(Ok(())).unwrap(); + } + + fn publish(&mut self, event: &Event) { + self.router.publish(event); } } @@ -169,7 +259,8 @@ mod test { let matcher = MockRequestMethodMatcher::default() .map(Method::AbciInfo, Ok(abci_info_fixture)) .map(Method::Block, Ok(block_fixture)); - let mut client = MockClient::new(matcher); + let (client, driver) = MockClient::new(matcher); + let driver_hdl = tokio::spawn(async move { driver.run().await }); let abci_info = client.abci_info().await.unwrap(); assert_eq!("GaiaApp".to_string(), abci_info.data); @@ -178,11 +269,16 @@ mod test { let block = client.block(Height::from(10_u32)).await.unwrap().block; assert_eq!(Height::from(10_u32), block.header.height); assert_eq!("cosmoshub-2".parse::().unwrap(), block.header.chain_id); + + client.close(); + driver_hdl.await.unwrap().unwrap(); } #[tokio::test] async fn mock_subscription_client() { - let mut client = MockClient::new(MockRequestMethodMatcher::default()); + let (client, driver) = MockClient::new(MockRequestMethodMatcher::default()); + let driver_hdl = tokio::spawn(async move { driver.run().await }); + let event1 = read_event("event_new_block_1").await; let event2 = read_event("event_new_block_2").await; let event3 = read_event("event_new_block_3").await; @@ -197,7 +293,7 @@ mod test { let subs1_events = subs1.take(3); let subs2_events = subs2.take(3); for ev in &events { - client.publish(ev).await; + client.publish(ev); } // Here each subscription's channel is drained. @@ -210,5 +306,8 @@ mod test { for i in 0..3 { assert!(events[i].eq(subs1_events[i].as_ref().unwrap())); } + + client.close(); + driver_hdl.await.unwrap().unwrap(); } } diff --git a/rpc/src/client/transport/router.rs b/rpc/src/client/transport/router.rs index 31a157aa8..f80f3dea9 100644 --- a/rpc/src/client/transport/router.rs +++ b/rpc/src/client/transport/router.rs @@ -23,7 +23,7 @@ impl SubscriptionRouter { /// event is relevant. At present, it matches purely based on the query /// associated with the event, and only queries that exactly match that of /// the event's. - pub async fn publish(&mut self, ev: &Event) -> PublishResult { + pub fn publish(&mut self, ev: &Event) -> PublishResult { let subs_for_query = match self.subscriptions.get_mut(&ev.query) { Some(s) => s, None => return PublishResult::NoSubscribers, @@ -33,7 +33,7 @@ impl SubscriptionRouter { // us to safely stop tracking the subscription. let mut disconnected = HashSet::new(); for (id, event_tx) in subs_for_query { - if let Err(e) = event_tx.send(Ok(ev.clone())).await { + if let Err(e) = event_tx.send(Ok(ev.clone())) { disconnected.insert(id.clone()); debug!( "Automatically disconnecting subscription with ID {} for query \"{}\" due to failure to publish to it: {}", @@ -163,7 +163,7 @@ mod test { let mut ev = read_event("event_new_block_1").await; ev.query = "query1".into(); - router.publish(&ev).await; + router.publish(&ev); let subs1_ev = must_recv(&mut subs1_event_rx, 500).await.unwrap(); let subs2_ev = must_recv(&mut subs2_event_rx, 500).await.unwrap(); @@ -172,7 +172,7 @@ mod test { assert_eq!(ev, subs2_ev); ev.query = "query2".into(); - router.publish(&ev).await; + router.publish(&ev); must_not_recv(&mut subs1_event_rx, 50).await; must_not_recv(&mut subs2_event_rx, 50).await; diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index 758264975..bfec60b76 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -87,7 +87,7 @@ const PING_INTERVAL: Duration = Duration::from_secs((RECV_TIMEOUT_SECONDS * 9) / /// /// #[tokio::main] /// async fn main() { -/// let (mut client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) +/// let (client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) /// .await /// .unwrap(); /// let driver_handle = tokio::spawn(async move { driver.run().await }); @@ -114,7 +114,7 @@ const PING_INTERVAL: Duration = Duration::from_secs((RECV_TIMEOUT_SECONDS * 9) / /// } /// /// // Signal to the driver to terminate. -/// client.close().await.unwrap(); +/// client.close().unwrap(); /// // Await the driver's termination to ensure proper connection closure. /// let _ = driver_handle.await.unwrap(); /// } @@ -149,21 +149,21 @@ impl WebSocketClient { Ok((Self { cmd_tx }, driver)) } - async fn send_cmd(&mut self, cmd: DriverCommand) -> Result<()> { - self.cmd_tx.send(cmd).await.map_err(|e| { + fn send_cmd(&self, cmd: DriverCommand) -> Result<()> { + self.cmd_tx.send(cmd).map_err(|e| { Error::client_internal_error(format!("failed to send command to client driver: {}", e)) }) } /// Signals to the driver that it must terminate. - pub async fn close(mut self) -> Result<()> { - self.send_cmd(DriverCommand::Terminate).await + pub fn close(self) -> Result<()> { + self.send_cmd(DriverCommand::Terminate) } } #[async_trait] impl Client for WebSocketClient { - async fn perform(&mut self, request: R) -> Result + async fn perform(&self, request: R) -> Result where R: SimpleRequest, { @@ -175,8 +175,7 @@ impl Client for WebSocketClient { id, wrapped_request, response_tx, - })) - .await?; + }))?; let response = response_rx.recv().await.ok_or_else(|| { Error::client_internal_error("failed to hear back from WebSocket driver".to_string()) })??; @@ -186,7 +185,7 @@ impl Client for WebSocketClient { #[async_trait] impl SubscriptionClient for WebSocketClient { - async fn subscribe(&mut self, query: Query) -> Result { + async fn subscribe(&self, query: Query) -> Result { let (subscription_tx, subscription_rx) = unbounded(); let (response_tx, mut response_rx) = unbounded(); // By default we use UUIDs to differentiate subscriptions @@ -196,8 +195,7 @@ impl SubscriptionClient for WebSocketClient { query: query.to_string(), subscription_tx, response_tx, - })) - .await?; + }))?; // Make sure our subscription request went through successfully. let _ = response_rx.recv().await.ok_or_else(|| { Error::client_internal_error("failed to hear back from WebSocket driver".to_string()) @@ -205,13 +203,12 @@ impl SubscriptionClient for WebSocketClient { Ok(Subscription::new(id, query, subscription_rx)) } - async fn unsubscribe(&mut self, query: Query) -> Result<()> { + async fn unsubscribe(&self, query: Query) -> Result<()> { let (response_tx, mut response_rx) = unbounded(); self.send_cmd(DriverCommand::Unsubscribe(UnsubscribeCommand { query: query.to_string(), response_tx, - })) - .await?; + }))?; let _ = response_rx.recv().await.ok_or_else(|| { Error::client_internal_error("failed to hear back from WebSocket driver".to_string()) })??; @@ -353,15 +350,15 @@ impl WebSocketClientDriver { .await } - async fn subscribe(&mut self, mut cmd: SubscribeCommand) -> Result<()> { + async fn subscribe(&mut self, cmd: SubscribeCommand) -> Result<()> { // If we already have an active subscription for the given query, // there's no need to initiate another one. Just add this subscription // to the router. if self.router.num_subscriptions_for_query(cmd.query.clone()) > 0 { - let (id, query, subscription_tx, mut response_tx) = + let (id, query, subscription_tx, response_tx) = (cmd.id, cmd.query, cmd.subscription_tx, cmd.response_tx); self.router.add(id, query, subscription_tx); - return response_tx.send(Ok(())).await; + return response_tx.send(Ok(())); } // Otherwise, we need to initiate a subscription request. @@ -370,7 +367,7 @@ impl WebSocketClientDriver { subscribe::Request::new(cmd.query.clone()), ); if let Err(e) = self.send_request(wrapper).await { - cmd.response_tx.send(Err(e.clone())).await?; + cmd.response_tx.send(Err(e.clone()))?; return Err(e); } self.pending_commands @@ -378,14 +375,14 @@ impl WebSocketClientDriver { Ok(()) } - async fn unsubscribe(&mut self, mut cmd: UnsubscribeCommand) -> Result<()> { + async fn unsubscribe(&mut self, cmd: UnsubscribeCommand) -> Result<()> { // Terminate all subscriptions for this query immediately. This // prioritizes acknowledgement of the caller's wishes over networking // problems. if self.router.remove_by_query(cmd.query.clone()) == 0 { // If there were no subscriptions for this query, respond // immediately. - cmd.response_tx.send(Ok(())).await?; + cmd.response_tx.send(Ok(()))?; return Ok(()); } @@ -394,7 +391,7 @@ impl WebSocketClientDriver { let wrapper = Wrapper::new(unsubscribe::Request::new(cmd.query.clone())); let req_id = wrapper.id().clone(); if let Err(e) = self.send_request(wrapper).await { - cmd.response_tx.send(Err(e.clone())).await?; + cmd.response_tx.send(Err(e.clone()))?; return Err(e); } self.pending_commands @@ -402,12 +399,12 @@ impl WebSocketClientDriver { Ok(()) } - async fn simple_request(&mut self, mut cmd: SimpleRequestCommand) -> Result<()> { + async fn simple_request(&mut self, cmd: SimpleRequestCommand) -> Result<()> { if let Err(e) = self .send_msg(Message::Text(cmd.wrapped_request.clone())) .await { - cmd.response_tx.send(Err(e.clone())).await?; + cmd.response_tx.send(Err(e.clone()))?; return Err(e); } self.pending_commands @@ -451,7 +448,7 @@ impl WebSocketClientDriver { } async fn publish_event(&mut self, ev: Event) { - if let PublishResult::AllDisconnected = self.router.publish(&ev).await { + if let PublishResult::AllDisconnected = self.router.publish(&ev) { debug!( "All subscribers for query \"{}\" have disconnected. Unsubscribing from query...", ev.query @@ -475,13 +472,13 @@ impl WebSocketClientDriver { ) -> Result<()> { match pending_cmd { DriverCommand::Subscribe(cmd) => { - let (id, query, subscription_tx, mut response_tx) = + let (id, query, subscription_tx, response_tx) = (cmd.id, cmd.query, cmd.subscription_tx, cmd.response_tx); self.router.add(id, query, subscription_tx); - response_tx.send(Ok(())).await + response_tx.send(Ok(())) } - DriverCommand::Unsubscribe(mut cmd) => cmd.response_tx.send(Ok(())).await, - DriverCommand::SimpleRequest(mut cmd) => cmd.response_tx.send(Ok(response)).await, + DriverCommand::Unsubscribe(cmd) => cmd.response_tx.send(Ok(())), + DriverCommand::SimpleRequest(cmd) => cmd.response_tx.send(Ok(response)), _ => Ok(()), } } @@ -553,12 +550,12 @@ mod test { } } - async fn publish_event(&mut self, ev: Event) -> Result<()> { - self.event_tx.send(ev).await + fn publish_event(&mut self, ev: Event) -> Result<()> { + self.event_tx.send(ev) } - async fn terminate(mut self) -> Result<()> { - self.terminate_tx.send(Ok(())).await.unwrap(); + async fn terminate(self) -> Result<()> { + self.terminate_tx.send(Ok(())).unwrap(); self.driver_hdl.await.unwrap() } } @@ -588,7 +585,7 @@ mod test { async fn run(mut self) -> Result<()> { loop { tokio::select! { - Some(ev) = self.event_rx.recv() => self.publish_event(ev).await, + Some(ev) = self.event_rx.recv() => self.publish_event(ev), Some(res) = self.listener.next() => self.handle_incoming(res.unwrap()).await, Some(res) = self.terminate_rx.recv() => { self.terminate().await; @@ -600,9 +597,9 @@ mod test { // Publishes the given event to all subscribers for the query relating // to the event. - async fn publish_event(&mut self, ev: Event) { + fn publish_event(&mut self, ev: Event) { for handler in &mut self.handlers { - handler.publish_event(ev.clone()).await; + handler.publish_event(ev.clone()); } } @@ -644,12 +641,12 @@ mod test { } } - async fn publish_event(&mut self, ev: Event) { - let _ = self.event_tx.send(ev).await; + fn publish_event(&mut self, ev: Event) { + let _ = self.event_tx.send(ev); } - async fn terminate(mut self) -> Result<()> { - self.terminate_tx.send(Ok(())).await?; + async fn terminate(self) -> Result<()> { + self.terminate_tx.send(Ok(()))?; self.driver_hdl.await.unwrap() } } @@ -820,7 +817,7 @@ mod test { println!("Starting WebSocket server..."); let mut server = TestServer::new("127.0.0.1:0").await; println!("Creating client RPC WebSocket connection..."); - let (mut client, driver) = WebSocketClient::new(server.node_addr.clone()) + let (client, driver) = WebSocketClient::new(server.node_addr.clone()) .await .unwrap(); let driver_handle = tokio::spawn(async move { driver.run().await }); @@ -843,13 +840,13 @@ mod test { println!("Publishing events"); // Publish the events from this context for ev in &test_events { - server.publish_event(ev.clone()).await.unwrap(); + server.publish_event(ev.clone()).unwrap(); } println!("Collecting results from subscription..."); let collected_results = subs_collector_hdl.await.unwrap(); - client.close().await.unwrap(); + client.close().unwrap(); server.terminate().await.unwrap(); let _ = driver_handle.await.unwrap(); println!("Closed client and terminated server"); diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index 5255cc42f..f18a8eaf5 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -186,7 +186,7 @@ mod rpc { #[tokio::test] #[ignore] async fn subscription_interface() { - let (mut client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) + let (client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) .await .unwrap(); let driver_handle = tokio::spawn(async move { driver.run().await }); @@ -203,7 +203,7 @@ mod rpc { } } - client.close().await.unwrap(); + client.close().unwrap(); let _ = driver_handle.await.unwrap(); } @@ -219,7 +219,7 @@ mod rpc { } async fn simple_transaction_subscription() { - let (mut client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) + let (client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) .await .unwrap(); let driver_handle = tokio::spawn(async move { driver.run().await }); @@ -232,7 +232,7 @@ mod rpc { // We can clone the WebSocket client, because it's just a handle to the // driver. - let mut inner_client = client.clone(); + let inner_client = client.clone(); tokio::spawn(async move { for (tx_count, val) in broadcast_tx_values.into_iter().enumerate() { let tx = format!("tx{}={}", tx_count, val); @@ -284,12 +284,12 @@ mod rpc { } } - client.close().await.unwrap(); + client.close().unwrap(); let _ = driver_handle.await.unwrap(); } async fn concurrent_subscriptions() { - let (mut client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) + let (client, driver) = WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) .await .unwrap(); let driver_handle = tokio::spawn(async move { driver.run().await }); @@ -303,7 +303,7 @@ mod rpc { let broadcast_tx_values = expected_tx_values.clone(); let mut expected_new_blocks = 5_i32; - let mut inner_client = client.clone(); + let inner_client = client.clone(); tokio::spawn(async move { for (tx_count, val) in broadcast_tx_values.into_iter().enumerate() { let tx = format!("tx{}={}", tx_count, val); @@ -345,7 +345,7 @@ mod rpc { } } - client.close().await.unwrap(); + client.close().unwrap(); let _ = driver_handle.await.unwrap(); } } From 741cecbba4e013627c20753da6fe6359cafa0850 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 20 Nov 2020 11:29:35 -0500 Subject: [PATCH 13/15] Client no longer needs to be mutable Signed-off-by: Thane Thomson --- light-client/src/evidence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index ea45b068c..d18b10c7e 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -42,7 +42,7 @@ mod prod { impl EvidenceReporter for ProdEvidenceReporter { #[pre(self.peer_map.contains_key(&peer))] fn report(&self, e: Evidence, peer: PeerId) -> Result { - let mut client = self.rpc_client_for(peer)?; + let client = self.rpc_client_for(peer)?; let res = block_on( self.timeout, From eefced1ae1f813887aab13feffb68b8d0bdb4c09 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 25 Nov 2020 15:42:01 -0500 Subject: [PATCH 14/15] Use explicit borrow_mut to avoid having to obtain subscriptions map twice Signed-off-by: Thane Thomson --- rpc/src/client/transport/router.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/rpc/src/client/transport/router.rs b/rpc/src/client/transport/router.rs index f80f3dea9..4e4e3ccd1 100644 --- a/rpc/src/client/transport/router.rs +++ b/rpc/src/client/transport/router.rs @@ -2,6 +2,7 @@ use crate::client::subscription::SubscriptionTx; use crate::event::Event; +use std::borrow::BorrowMut; use std::collections::{HashMap, HashSet}; use tracing::debug; @@ -32,7 +33,7 @@ impl SubscriptionRouter { // that the receiver end of the channel has been dropped, which allows // us to safely stop tracking the subscription. let mut disconnected = HashSet::new(); - for (id, event_tx) in subs_for_query { + for (id, event_tx) in subs_for_query.borrow_mut() { if let Err(e) = event_tx.send(Ok(ev.clone())) { disconnected.insert(id.clone()); debug!( @@ -41,10 +42,6 @@ impl SubscriptionRouter { ); } } - // Obtain a mutable reference because the previous reference was - // consumed in the above for loop. We should panic if there are no - // longer any subscriptions for this query. - let subs_for_query = self.subscriptions.get_mut(&ev.query).unwrap(); for id in disconnected { subs_for_query.remove(&id); } From 62cbdcaa66bb707734880ab7410b64d3e254abda Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 25 Nov 2020 16:36:19 -0500 Subject: [PATCH 15/15] Rename method for clarity Signed-off-by: Thane Thomson --- rpc/src/client/transport/websocket.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index bfec60b76..b5ef65b0b 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -439,7 +439,7 @@ impl WebSocketClientDriver { }; let id = wrapper.id().to_string(); if let Some(pending_cmd) = self.pending_commands.remove(&id) { - return self.confirm_pending_command(pending_cmd, msg).await; + return self.respond_to_pending_command(pending_cmd, msg).await; }; // We ignore incoming messages whose ID we don't recognize (could be // relating to a fire-and-forget unsubscribe request - see the @@ -465,7 +465,7 @@ impl WebSocketClientDriver { } } - async fn confirm_pending_command( + async fn respond_to_pending_command( &mut self, pending_cmd: DriverCommand, response: String,