From 9df13ed7d5cfd0e8c0a1aea4e1a1a7c8e80c1266 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 10 Aug 2020 18:24:25 -0400 Subject: [PATCH 01/60] Create basic request/response transport abstraction Signed-off-by: Thane Thomson --- light-client/src/components/io.rs | 8 ++-- light-client/src/evidence.rs | 6 +-- rpc/Cargo.toml | 3 +- rpc/src/client.rs | 66 +++++++++-------------------- rpc/src/client/transport.rs | 15 +++++++ rpc/src/client/transport/http_ws.rs | 62 +++++++++++++++++++++++++++ rpc/src/error.rs | 4 ++ 7 files changed, 111 insertions(+), 53 deletions(-) create mode 100644 rpc/src/client/transport.rs create mode 100644 rpc/src/client/transport/http_ws.rs diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index b1a78639e..05c4c3b03 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -121,7 +121,7 @@ impl ProdIo { peer: PeerId, height: AtHeight, ) -> Result { - let rpc_client = self.rpc_client_for(peer); + let rpc_client = self.rpc_client_for(peer)?; let res = block_on( async { @@ -154,7 +154,7 @@ impl ProdIo { }; let res = block_on( - self.rpc_client_for(peer).validators(height), + self.rpc_client_for(peer)?.validators(height), peer, self.timeout, )?; @@ -167,9 +167,9 @@ impl ProdIo { // FIXME: Cannot enable precondition because of "autoref lifetime" issue // #[pre(self.peer_map.contains_key(&peer))] - fn rpc_client_for(&self, peer: PeerId) -> rpc::Client { + fn rpc_client_for(&self, peer: PeerId) -> Result { let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - rpc::Client::new(peer_addr) + rpc::Client::new(peer_addr).map_err(IoError::from) } } diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index 260eb79be..a9ff6109a 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -29,7 +29,7 @@ pub struct ProdEvidenceReporter { impl EvidenceReporter for ProdEvidenceReporter { #[pre(self.peer_map.contains_key(&peer))] fn report(&self, e: Evidence, peer: PeerId) -> Result { - let res = block_on(self.rpc_client_for(peer).broadcast_evidence(e)); + let res = block_on(self.rpc_client_for(peer)?.broadcast_evidence(e)); match res { Ok(response) => Ok(response.hash), @@ -48,9 +48,9 @@ impl ProdEvidenceReporter { // FIXME: Cannot enable precondition because of "autoref lifetime" issue // #[pre(self.peer_map.contains_key(&peer))] - fn rpc_client_for(&self, peer: PeerId) -> rpc::Client { + fn rpc_client_for(&self, peer: PeerId) -> Result { let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - rpc::Client::new(peer_addr) + rpc::Client::new(peer_addr).map_err(IoError::from) } } diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 65bbb5ddf..b89e346ec 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -25,7 +25,7 @@ all-features = true [features] default = [] -client = [ "async-tungstenite", "futures", "http", "hyper", "tokio" ] +client = [ "async-tungstenite", "futures", "http", "hyper", "tokio", "async-trait" ] secp256k1 = ["tendermint/secp256k1"] [dependencies] @@ -43,3 +43,4 @@ futures = { version = "0.3", optional = true } http = { version = "0.2", optional = true } hyper = { version = "0.13", optional = true } tokio = { version = "0.2", features = ["macros"], optional = true } +async-trait = { version = "0.1.36", optional = true } \ No newline at end of file diff --git a/rpc/src/client.rs b/rpc/src/client.rs index d7c1eaa90..6377be22c 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -1,31 +1,35 @@ //! Tendermint RPC client -use bytes::buf::ext::BufExt; -use hyper::header; - -use tendermint::abci::{self, Transaction}; -use tendermint::block::Height; -use tendermint::evidence::Evidence; -use tendermint::net; -use tendermint::Genesis; - -use crate::{endpoint::*, Error, Request, Response}; +use tendermint::{ + abci::{self, Transaction}, + block::Height, + evidence::Evidence, + net, Genesis, +}; + +use crate::{ + client::transport::{http_ws::HttpWsTransport, Transport}, + endpoint::*, + Error, Request, Response, +}; pub mod event_listener; +pub mod transport; /// Tendermint RPC client. /// /// Presently supports JSONRPC via HTTP. -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Client { - /// Address of the RPC server - address: net::Address, + transport: Box, } impl Client { /// Create a new Tendermint RPC client, connecting to the given address - pub fn new(address: net::Address) -> Self { - Self { address } + pub fn new(address: net::Address) -> Result { + Ok(Self { + transport: Box::new(HttpWsTransport::new(address)?), + }) } /// `/abci_info`: get information about the ABCI application. @@ -165,35 +169,7 @@ impl Client { R: Request, { let request_body = request.into_json(); - - let (host, port) = match &self.address { - net::Address::Tcp { host, port, .. } => (host, port), - other => { - return Err(Error::invalid_params(&format!( - "invalid RPC address: {:?}", - other - ))) - } - }; - - 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()) + let response_body = self.transport.request(request_body).await?; + R::Response::from_string(response_body) } } diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs new file mode 100644 index 000000000..c23a307de --- /dev/null +++ b/rpc/src/client/transport.rs @@ -0,0 +1,15 @@ +//! Transport layer abstraction for the Tendermint RPC client. + +use async_trait::async_trait; + +use crate::Error; + +pub mod http_ws; + +/// Abstracting the transport layer allows us to easily simulate interactions +/// with remote Tendermint nodes' RPC endpoints. +#[async_trait] +pub trait Transport: std::fmt::Debug { + /// Perform a request to the remote endpoint, expecting a response. + async fn request(&self, request: String) -> Result; +} diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs new file mode 100644 index 000000000..d1925da2c --- /dev/null +++ b/rpc/src/client/transport/http_ws.rs @@ -0,0 +1,62 @@ +//! HTTP-based transport for Tendermint RPC Client, with WebSockets-based +//! subscription handling mechanism. + +use async_trait::async_trait; +use hyper::header; +use tendermint::net; + +use crate::{client::transport::Transport, Error}; +use bytes::buf::BufExt; +use std::io::Read; + +#[derive(Debug)] +pub struct HttpWsTransport { + host: String, + port: u16, +} + +#[async_trait] +impl Transport for HttpWsTransport { + async fn request(&self, request_body: String) -> Result { + 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?; + let mut response_string = String::new(); + let _ = response_body + .reader() + .read_to_string(&mut response_string) + .map_err(|e| Error::internal_error(format!("failed to read response body: {}", e))); + Ok(response_string) + } +} + +impl HttpWsTransport { + /// Create a new HTTP/WebSockets + pub fn new(address: net::Address) -> Result { + let (host, port) = match address { + net::Address::Tcp { host, port, .. } => (host, port), + other => { + return Err(Error::invalid_params(&format!( + "invalid RPC address: {:?}", + other + ))) + } + }; + Ok(HttpWsTransport { host, port }) + } +} diff --git a/rpc/src/error.rs b/rpc/src/error.rs index dcea88f12..fd01e886b 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -73,6 +73,10 @@ impl Error { Error::new(Code::ServerError, Some(data.to_string())) } + pub fn internal_error(cause: impl Into) -> Error { + Error::new(Code::InternalError, Some(cause.into())) + } + /// Obtain the `rpc::error::Code` for this error pub fn code(&self) -> Code { self.code From a01883d506ef9033240a71e63db860e9eac5cf3b Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 10 Aug 2020 19:32:41 -0400 Subject: [PATCH 02/60] Add fixture-based transport with failing test Signed-off-by: Thane Thomson --- rpc/Cargo.toml | 2 +- rpc/src/client.rs | 33 +++++++++++++++++++- rpc/src/client/testing.rs | 64 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 rpc/src/client/testing.rs diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index b89e346ec..1fa1ac8ca 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -42,5 +42,5 @@ async-tungstenite = { version="0.5", features = ["tokio-runtime"], optional = tr futures = { version = "0.3", optional = true } http = { version = "0.2", optional = true } hyper = { version = "0.13", optional = true } -tokio = { version = "0.2", features = ["macros"], optional = true } +tokio = { version = "0.2", features = ["macros", "fs"], optional = true } async-trait = { version = "0.1.36", optional = true } \ No newline at end of file diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 6377be22c..aed91d4bd 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -14,6 +14,8 @@ use crate::{ }; pub mod event_listener; +#[cfg(test)] +pub mod testing; pub mod transport; /// Tendermint RPC client. @@ -25,7 +27,10 @@ pub struct Client { } impl Client { - /// Create a new Tendermint RPC client, connecting to the given address + /// Create a new Tendermint RPC client, connecting to the given address. + /// By default this uses the [`HttpWsTransport`] transport layer. This + /// transport lazily initializes subscription mechanisms when first + /// subscribing to events generated by a particular query. pub fn new(address: net::Address) -> Result { Ok(Self { transport: Box::new(HttpWsTransport::new(address)?), @@ -173,3 +178,29 @@ impl Client { R::Response::from_string(response_body) } } + +impl From> for Client { + fn from(transport: Box) -> Self { + Self { transport } + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::path::PathBuf; + + #[tokio::test] + async fn test_client_interface() { + let mut ft = testing::MappedFixtureTransport::new(); + ft.read_success_fixture( + PathBuf::from("./testing/fixtures/abci_info_request.json").as_path(), + PathBuf::from("../tests/support/abci_info.json").as_path(), + ) + .await; + let transport: Box = Box::new(ft); + let client = Client::from(transport); + let abci_info = client.abci_info().await.unwrap(); + assert_eq!(abci_info.data, "GaiaApp".to_string()); + } +} diff --git a/rpc/src/client/testing.rs b/rpc/src/client/testing.rs new file mode 100644 index 000000000..cf1e9587b --- /dev/null +++ b/rpc/src/client/testing.rs @@ -0,0 +1,64 @@ +//! Testing-related utilities for the Tendermint RPC Client. Here we aim to +//! provide some useful abstractions in cases where you may want to use an RPC +//! client in your code, but mock its remote endpoint(s). + +use async_trait::async_trait; +use std::{collections::HashMap, path::Path}; +use tokio::fs; + +use crate::{client::transport::Transport, Error}; + +/// A rudimentary fixture-based transport, where certain requests are +/// preconfigured to always produce specific kinds of responses. +#[derive(Debug)] +pub struct MappedFixtureTransport { + successes: HashMap, + failures: HashMap, +} + +#[async_trait] +impl Transport for MappedFixtureTransport { + async fn request(&self, request: String) -> Result { + match self.successes.get(&request) { + Some(response) => Ok(response.clone()), + None => match self.failures.get(&request) { + Some(e) => Err(e.clone()), + None => Err(Error::internal_error( + "no request/response mapping for supplied request", + )), + }, + } + } +} + +impl MappedFixtureTransport { + /// Instantiate a new, empty mapped fixture transport (all requests will + /// generate errors). + pub fn new() -> Self { + Self { + successes: HashMap::new(), + failures: HashMap::new(), + } + } + + /// Reads a JSON fixture for a request and response from the given + /// filesystem paths. + pub async fn read_success_fixture( + &mut self, + request_path: &Path, + response_path: &Path, + ) -> &mut Self { + self.successes.insert( + fs::read_to_string(request_path).await.unwrap(), + fs::read_to_string(response_path).await.unwrap(), + ); + self + } + + /// Reads a JSON fixture for a request and maps it to the given error. + pub async fn read_failure_fixture(&mut self, request_path: &Path, err: Error) -> &mut Self { + self.failures + .insert(fs::read_to_string(request_path).await.unwrap(), err); + self + } +} From a03d0abb873950018755d1d1002bc6a6c5c8dd16 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 11 Aug 2020 15:09:14 -0400 Subject: [PATCH 03/60] Refactor mock transport This version of the transport provides a request matching interface, along with an implementation of a request method-based matcher (produces specific fixed responses according to the request's method). Signed-off-by: Thane Thomson --- rpc/src/client.rs | 107 +++++++++++++++++-- rpc/src/client/testing.rs | 71 +++++------- rpc/src/client/testing/matching_transport.rs | 77 +++++++++++++ 3 files changed, 199 insertions(+), 56 deletions(-) create mode 100644 rpc/src/client/testing/matching_transport.rs diff --git a/rpc/src/client.rs b/rpc/src/client.rs index aed91d4bd..9de79bb50 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -188,19 +188,106 @@ impl From> for Client { #[cfg(test)] mod test { use super::*; - use std::path::PathBuf; + use crate::Method; + use testing::matching_transport::{MethodMatcher, RequestMatchingTransport}; + + // TODO: Read from a fixture in the crate. + const ABCI_INFO_RESPONSE: &str = r#"{ + "jsonrpc": "2.0", + "id": "", + "result": { + "response": { + "data": "GaiaApp", + "last_block_height": "488120", + "last_block_app_hash": "2LnCw0fN+Zq/gs5SOuya/GRHUmtWftAqAkTUuoxl4g4=" + } + } +} +"#; + + // TODO: Read from a fixture in the crate. + const BLOCK_RESPONSE: &str = r#"{ + "jsonrpc": "2.0", + "id": "", + "result": { + "block_id": { + "hash": "4FFD15F274758E474898498A191EB8CA6FC6C466576255DA132908A12AC1674C", + "parts": { + "total": "1", + "hash": "BBA710736635FA20CDB4F48732563869E90871D31FE9E7DE3D900CD4334D8775" + } + }, + "block": { + "header": { + "version": { + "block": "10", + "app": "1" + }, + "chain_id": "cosmoshub-2", + "height": "10", + "time": "2020-03-15T16:57:08.151Z", + "last_block_id": { + "hash": "760E050B2404A4BC661635CA552FF45876BCD927C367ADF88961E389C01D32FF", + "parts": { + "total": "1", + "hash": "485070D01F9543827B3F9BAF11BDCFFBFD2BDED0B63D7192FA55649B94A1D5DE" + } + }, + "last_commit_hash": "594F029060D5FAE6DDF82C7DC4612055EC7F941DFED34D43B2754008DC3BBC77", + "data_hash": "", + "validators_hash": "3C0A744897A1E0DBF1DEDE1AF339D65EDDCF10E6338504368B20C508D6D578DC", + "next_validators_hash": "3C0A744897A1E0DBF1DEDE1AF339D65EDDCF10E6338504368B20C508D6D578DC", + "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F", + "app_hash": "0000000000000000", + "last_results_hash": "", + "evidence_hash": "", + "proposer_address": "12CC3970B3AE9F19A4B1D98BE1799F2CB923E0A3" + }, + "data": { + "txs": null + }, + "evidence": { + "evidence": null + }, + "last_commit": { + "height": "9", + "round": "0", + "block_id": { + "hash": "760E050B2404A4BC661635CA552FF45876BCD927C367ADF88961E389C01D32FF", + "parts": { + "total": "1", + "hash": "485070D01F9543827B3F9BAF11BDCFFBFD2BDED0B63D7192FA55649B94A1D5DE" + } + }, + "signatures": [ + { + "block_id_flag": 2, + "validator_address": "12CC3970B3AE9F19A4B1D98BE1799F2CB923E0A3", + "timestamp": "2020-03-15T16:57:08.151Z", + "signature": "GRBX/UNaf19vs5byJfAuXk2FQ05soOHmaMFCbrNBhHdNZtFKHp6J9eFwZrrG+YCxKMdqPn2tQWAes6X8kpd1DA==" + } + ] + } + } + } +}"#; #[tokio::test] - async fn test_client_interface() { - let mut ft = testing::MappedFixtureTransport::new(); - ft.read_success_fixture( - PathBuf::from("./testing/fixtures/abci_info_request.json").as_path(), - PathBuf::from("../tests/support/abci_info.json").as_path(), - ) - .await; - let transport: Box = Box::new(ft); + async fn test_mocked_transport() { + let mt = RequestMatchingTransport::new(MethodMatcher::new( + Method::AbciInfo, + Ok(ABCI_INFO_RESPONSE.into()), + )) + .push(MethodMatcher::new(Method::Block, Ok(BLOCK_RESPONSE.into()))); + + let transport: Box = Box::new(mt); let client = Client::from(transport); + let abci_info = client.abci_info().await.unwrap(); - assert_eq!(abci_info.data, "GaiaApp".to_string()); + assert_eq!("GaiaApp".to_string(), abci_info.data); + + // supplied height is irrelevant when using MethodMatcher + let block = client.block(Height::from(1234)).await.unwrap().block; + assert_eq!(Height::from(10), block.header.height); } } diff --git a/rpc/src/client/testing.rs b/rpc/src/client/testing.rs index cf1e9587b..dbb0ca7a8 100644 --- a/rpc/src/client/testing.rs +++ b/rpc/src/client/testing.rs @@ -1,64 +1,43 @@ //! Testing-related utilities for the Tendermint RPC Client. Here we aim to //! provide some useful abstractions in cases where you may want to use an RPC -//! client in your code, but mock its remote endpoint(s). +//! client in your code, but mock its remote endpoint's responses. -use async_trait::async_trait; -use std::{collections::HashMap, path::Path}; +use std::path::PathBuf; use tokio::fs; -use crate::{client::transport::Transport, Error}; +pub mod matching_transport; -/// A rudimentary fixture-based transport, where certain requests are -/// preconfigured to always produce specific kinds of responses. -#[derive(Debug)] -pub struct MappedFixtureTransport { - successes: HashMap, - failures: HashMap, +/// A fixture that can refer to a file in the filesystem or is a string in its +/// own right. +#[derive(Debug, Clone)] +pub enum Fixture { + File(PathBuf), + Raw(String), } -#[async_trait] -impl Transport for MappedFixtureTransport { - async fn request(&self, request: String) -> Result { - match self.successes.get(&request) { - Some(response) => Ok(response.clone()), - None => match self.failures.get(&request) { - Some(e) => Err(e.clone()), - None => Err(Error::internal_error( - "no request/response mapping for supplied request", - )), - }, +impl Fixture { + async fn read(&self) -> String { + match self { + Fixture::File(path) => fs::read_to_string(path.as_path()).await.unwrap(), + Fixture::Raw(s) => s.clone(), } } } -impl MappedFixtureTransport { - /// Instantiate a new, empty mapped fixture transport (all requests will - /// generate errors). - pub fn new() -> Self { - Self { - successes: HashMap::new(), - failures: HashMap::new(), - } +impl Into for String { + fn into(self) -> Fixture { + Fixture::Raw(self) } +} - /// Reads a JSON fixture for a request and response from the given - /// filesystem paths. - pub async fn read_success_fixture( - &mut self, - request_path: &Path, - response_path: &Path, - ) -> &mut Self { - self.successes.insert( - fs::read_to_string(request_path).await.unwrap(), - fs::read_to_string(response_path).await.unwrap(), - ); - self +impl Into for &str { + fn into(self) -> Fixture { + Fixture::Raw(self.to_string()) } +} - /// Reads a JSON fixture for a request and maps it to the given error. - pub async fn read_failure_fixture(&mut self, request_path: &Path, err: Error) -> &mut Self { - self.failures - .insert(fs::read_to_string(request_path).await.unwrap(), err); - self +impl Into for PathBuf { + fn into(self) -> Fixture { + Fixture::File(self) } } diff --git a/rpc/src/client/testing/matching_transport.rs b/rpc/src/client/testing/matching_transport.rs new file mode 100644 index 000000000..859df7447 --- /dev/null +++ b/rpc/src/client/testing/matching_transport.rs @@ -0,0 +1,77 @@ +use async_trait::async_trait; + +use crate::{ + client::{testing::Fixture, transport::Transport}, + Error, Method, +}; + +/// A rudimentary fixture-based transport. +/// +/// Fixtures, if read from the file system, are lazily evaluated. +#[derive(Debug)] +pub struct RequestMatchingTransport { + matchers: Vec>, +} + +/// Implement this trait to facilitate different kinds of request matching. +pub trait RequestMatcher: Send + Sync + std::fmt::Debug { + /// Does the given request match? + fn matches(&self, request: &str) -> bool; + + /// The response we need to return if the request matches. + fn response(&self) -> Result; +} + +/// A simple matcher that just returns a specific response every time it gets +/// a request of a particular request method. +#[derive(Debug)] +pub struct MethodMatcher { + method: Method, + response: Result, +} + +#[async_trait] +impl Transport for RequestMatchingTransport { + async fn request(&self, request: String) -> Result { + for matcher in &self.matchers { + if matcher.matches(&request) { + let response = matcher.response()?.read().await; + return Ok(response); + } + } + Err(Error::internal_error(format!( + "no matcher for request: {}", + request + ))) + } +} + +impl RequestMatchingTransport { + pub fn new(matcher: impl RequestMatcher + 'static) -> Self { + Self { + matchers: vec![Box::new(matcher)], + } + } + + pub fn push(mut self, matcher: impl RequestMatcher + 'static) -> Self { + self.matchers.push(Box::new(matcher)); + self + } +} + +impl MethodMatcher { + pub fn new(method: Method, response: Result) -> Self { + Self { method, response } + } +} + +impl RequestMatcher for MethodMatcher { + fn matches(&self, request: &str) -> bool { + let request_json = serde_json::from_str::(request).unwrap(); + return self.method.to_string() == request_json.get("method").unwrap().as_str().unwrap(); + } + + fn response(&self) -> Result { + self.response.clone() + } +} From 73c262632b56c1703cb33d7a0fc16397d08f2236 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 11 Aug 2020 15:11:08 -0400 Subject: [PATCH 04/60] Take client creation interface change into account Signed-off-by: Thane Thomson --- tendermint/tests/integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index 21d9e313e..a4a6d5469 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -18,7 +18,7 @@ mod rpc { /// Get the address of the local node pub fn localhost_rpc_client() -> Client { - Client::new("tcp://127.0.0.1:26657".parse().unwrap()) + Client::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap() } /// `/health` endpoint From 8650db117453d5f77cfaa92548e76056ab8174e1 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 11 Aug 2020 15:13:12 -0400 Subject: [PATCH 05/60] Reword docstring for Transport Signed-off-by: Thane Thomson --- rpc/src/client/transport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index c23a307de..5fa5d9072 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -6,7 +6,7 @@ use crate::Error; pub mod http_ws; -/// Abstracting the transport layer allows us to easily simulate interactions +/// Transport layer abstraction that allows us to easily simulate interactions /// with remote Tendermint nodes' RPC endpoints. #[async_trait] pub trait Transport: std::fmt::Debug { From cf67215b88a26e4f3c0f07d5a507012c861f2311 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 11 Aug 2020 21:25:01 -0400 Subject: [PATCH 06/60] Add initial subscription management mechanism This commit adds the bulk of the subscription management machinery as well as a rudimentary integration test (which should be marked as `#[ignore]` ASAP) just to demonstrate obtaining new block events from a Tendermint instance running locally with default settings. Signed-off-by: Thane Thomson --- rpc/src/client.rs | 22 +- rpc/src/client/subscription.rs | 206 +++++++++++++++++ rpc/src/client/testing/matching_transport.rs | 9 +- rpc/src/client/transport.rs | 74 +++++- rpc/src/client/transport/http_ws.rs | 224 ++++++++++++++++++- rpc/src/endpoint.rs | 1 + rpc/src/endpoint/subscribe.rs | 9 +- rpc/src/endpoint/unsubscribe.rs | 44 ++++ rpc/src/events.rs | 74 ++++++ rpc/src/lib.rs | 1 + rpc/src/method.rs | 7 +- tendermint/tests/integration.rs | 23 ++ 12 files changed, 677 insertions(+), 17 deletions(-) create mode 100644 rpc/src/client/subscription.rs create mode 100644 rpc/src/endpoint/unsubscribe.rs create mode 100644 rpc/src/events.rs diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 9de79bb50..3aeded6e3 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -8,15 +8,20 @@ use tendermint::{ }; use crate::{ - client::transport::{http_ws::HttpWsTransport, Transport}, + client::{ + subscription::SubscriptionManager, + transport::{http_ws::HttpWsTransport, Transport}, + }, endpoint::*, Error, Request, Response, }; pub mod event_listener; +pub mod subscription; +pub mod transport; + #[cfg(test)] pub mod testing; -pub mod transport; /// Tendermint RPC client. /// @@ -168,6 +173,17 @@ impl Client { self.perform(evidence::Request::new(e)).await } + /// Creates a subscription management interface for this RPC client. This + /// interface facilitates subscribing and unsubscribing from receiving + /// events produced by specific RPC queries. + pub async fn new_subscription_manager( + &self, + event_buf_size: usize, + ) -> Result { + let conn = self.transport.new_event_connection(event_buf_size).await?; + Ok(SubscriptionManager::new(conn, 10)) + } + /// Perform a request against the RPC endpoint pub async fn perform(&self, request: R) -> Result where @@ -273,7 +289,7 @@ mod test { }"#; #[tokio::test] - async fn test_mocked_transport() { + async fn mocked_transport() { let mt = RequestMatchingTransport::new(MethodMatcher::new( Method::AbciInfo, Ok(ABCI_INFO_RESPONSE.into()), diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs new file mode 100644 index 000000000..1bc129129 --- /dev/null +++ b/rpc/src/client/subscription.rs @@ -0,0 +1,206 @@ +//! Subscription- and subscription management-related functionality. + +use futures::{ + task::{Context, Poll}, + Stream, +}; +use std::{ + collections::HashMap, + pin::Pin, + sync::atomic::{AtomicUsize, Ordering}, +}; +use tokio::{stream::StreamExt, sync::mpsc, task::JoinHandle}; + +use crate::{client::transport::EventConnection, events::Event, Error}; + +/// The subscription manager is just an interface/handle to the subscription +/// router, which runs asynchronously in a separate process. +#[derive(Debug)] +pub struct SubscriptionManager { + router: JoinHandle>, + cmd_tx: mpsc::Sender, + next_subs_id: AtomicUsize, +} + +/// An interface that can be used to asynchronously receive events for a +/// particular subscription. +#[derive(Debug)] +pub struct Subscription { + id: SubscriptionId, + query: String, + event_rx: mpsc::Receiver, +} + +// The subscription router does the heavy lifting of managing subscriptions and +// routing incoming events to their relevant subscribers. +struct SubscriptionRouter { + conn: EventConnection, + cmd_rx: mpsc::Receiver, + // Maps queries -> (maps of subscription IDs -> channels) + subscriptions: HashMap>>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct SubscriptionId(usize); + +#[derive(Debug)] +enum RouterCmd { + Subscribe { + id: SubscriptionId, + query: String, + event_tx: mpsc::Sender, + }, + Unsubscribe(Subscription), + Terminate, +} + +impl SubscriptionManager { + pub(crate) fn new(conn: EventConnection, cmd_buf_size: usize) -> Self { + let (cmd_tx, cmd_rx) = mpsc::channel(cmd_buf_size); + let router = SubscriptionRouter::new(conn, cmd_rx); + Self { + router: tokio::spawn(async move { router.run().await }), + cmd_tx, + next_subs_id: AtomicUsize::new(0), + } + } + + pub async fn subscribe( + &mut self, + query: String, + buf_size: usize, + ) -> Result { + let (event_tx, event_rx) = mpsc::channel(buf_size); + let id = self.next_subs_id(); + let _ = self + .cmd_tx + .send(RouterCmd::Subscribe { + id: id.clone(), + query: query.clone(), + event_tx, + }) + .await + .map_err(|e| { + Error::internal_error(format!( + "failed to transmit subscription request to async task: {}", + e + )) + })?; + Ok(Subscription { + id, + query, + event_rx, + }) + } + + pub async fn unsubscribe(&mut self, subs: Subscription) -> Result<(), Error> { + self.cmd_tx + .send(RouterCmd::Unsubscribe(subs)) + .await + .map_err(|e| { + Error::internal_error(format!( + "failed to transmit unsubscribe request to async task: {}", + e + )) + }) + } + + /// Gracefully terminate the subscription manager and its router (which + /// runs in an asynchronous task). + pub async fn terminate(mut self) -> Result<(), Error> { + let _ = self.cmd_tx.send(RouterCmd::Terminate).await.map_err(|e| { + Error::internal_error(format!( + "failed to transmit termination request to async task: {}", + e + )) + })?; + self.router + .await + .map_err(|e| Error::internal_error(format!("failed to terminate async task: {}", e)))? + } + + fn next_subs_id(&self) -> SubscriptionId { + SubscriptionId(self.next_subs_id.fetch_add(1, Ordering::SeqCst)) + } +} + +impl SubscriptionRouter { + fn new(conn: EventConnection, cmd_rx: mpsc::Receiver) -> Self { + Self { + conn, + cmd_rx, + subscriptions: HashMap::new(), + } + } + + async fn run(mut self) -> Result<(), Error> { + loop { + tokio::select! { + Some(ev) = self.conn.event_producer.next() => self.route_event(ev).await, + Some(cmd) = self.cmd_rx.next() => match cmd { + RouterCmd::Subscribe { id, query, event_tx } => self.subscribe(id, query, event_tx).await?, + RouterCmd::Unsubscribe(subs) => self.unsubscribe(subs).await?, + RouterCmd::Terminate => return self.terminate().await, + }, + } + } + } + + async fn route_event(&mut self, ev: Event) { + let subs_for_query = match self.subscriptions.get_mut(&ev.query) { + Some(s) => s, + None => return, + }; + let mut disconnected = Vec::new(); + for (subs_id, tx) in subs_for_query { + // TODO: Right now we automatically remove any disconnected or full + // channels. We must handle full channels differently to + // disconnected ones. + if let Err(_) = tx.send(ev.clone()).await { + disconnected.push(subs_id.clone()); + } + } + let subs_for_query = self.subscriptions.get_mut(&ev.query).unwrap(); + for subs_id in disconnected { + subs_for_query.remove(&subs_id); + } + } + + async fn subscribe( + &mut self, + id: SubscriptionId, + query: String, + event_tx: mpsc::Sender, + ) -> Result<(), Error> { + 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, event_tx); + self.conn.transport.subscribe(query).await + } + + async fn unsubscribe(&mut self, subs: Subscription) -> Result<(), Error> { + let subs_for_query = match self.subscriptions.get_mut(&subs.query) { + Some(s) => s, + None => return Ok(()), + }; + subs_for_query.remove(&subs.id); + self.conn.transport.unsubscribe(subs.query).await + } + + async fn terminate(mut self) -> Result<(), Error> { + self.conn.terminate().await + } +} + +impl Stream for Subscription { + type Item = Event; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.event_rx.poll_recv(cx) + } +} diff --git a/rpc/src/client/testing/matching_transport.rs b/rpc/src/client/testing/matching_transport.rs index 859df7447..61417699d 100644 --- a/rpc/src/client/testing/matching_transport.rs +++ b/rpc/src/client/testing/matching_transport.rs @@ -1,7 +1,10 @@ use async_trait::async_trait; use crate::{ - client::{testing::Fixture, transport::Transport}, + client::{ + testing::Fixture, + transport::{EventConnection, Transport}, + }, Error, Method, }; @@ -44,6 +47,10 @@ impl Transport for RequestMatchingTransport { request ))) } + + async fn new_event_connection(&self, event_buf_size: usize) -> Result { + unimplemented!() + } } impl RequestMatchingTransport { diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index 5fa5d9072..0a3adcfce 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -1,15 +1,83 @@ //! Transport layer abstraction for the Tendermint RPC client. use async_trait::async_trait; +use futures::{ + task::{Context, Poll}, + Stream, +}; +use std::pin::Pin; +use tokio::sync::mpsc; -use crate::Error; +use crate::{events::Event, Error}; pub mod http_ws; -/// Transport layer abstraction that allows us to easily simulate interactions -/// with remote Tendermint nodes' RPC endpoints. +/// Transport layer abstraction for interacting with real or mocked Tendermint +/// full nodes. #[async_trait] pub trait Transport: std::fmt::Debug { /// Perform a request to the remote endpoint, expecting a response. async fn request(&self, request: String) -> Result; + + /// Provides access to a stream of incoming events. These would be + /// produced, for example, once at least one subscription has been + /// initiated by the RPC client. + async fn new_event_connection(&self, event_buf_size: usize) -> Result; +} + +/// The part of the transport layer that exclusively deals with +/// subscribe/unsubscribe requests. +#[async_trait] +pub trait SubscriptionTransport: std::fmt::Debug + Send { + /// Send a subscription request through the transport layer. + async fn subscribe(&mut self, query: String) -> Result<(), Error>; + + /// Send an unsubscribe request through the transport layer. + async fn unsubscribe(&mut self, query: String) -> Result<(), Error>; + + /// Attempt to gracefully terminate the transport layer. + async fn close(&mut self) -> Result<(), Error>; +} + +/// An `EventConnection` allows us to send subscribe/unsubscribe requests via +/// the transport layer, as well as receive incoming events from subscriptions. +#[derive(Debug)] +pub struct EventConnection { + // The `EventConnection` struct is a workaround for the fact that we need + // to use `Transport` as a trait object, but trait objects are not allowed + // to use generics in their method signatures. + pub(crate) transport: Box, + pub(crate) event_producer: EventProducer, +} + +impl EventConnection { + pub fn new(transport: Box, event_producer: EventProducer) -> Self { + Self { + transport, + event_producer, + } + } + + pub async fn terminate(&mut self) -> Result<(), Error> { + self.transport.close().await + } +} + +#[derive(Debug)] +pub struct EventProducer { + event_rx: mpsc::Receiver, +} + +impl Stream for EventProducer { + type Item = Event; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.event_rx.poll_recv(cx) + } +} + +impl EventProducer { + pub fn new(event_rx: mpsc::Receiver) -> Self { + Self { event_rx } + } } diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index d1925da2c..e2b9af3b6 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -2,19 +2,62 @@ //! subscription handling mechanism. use async_trait::async_trait; +use async_tungstenite::{ + tokio::{connect_async, TokioAdapter}, + tungstenite::{ + protocol::{frame::coding::CloseCode, CloseFrame}, + Message, + }, + WebSocketStream, +}; +use bytes::buf::BufExt; +use futures::{stream::StreamExt, SinkExt}; use hyper::header; +use std::{borrow::Cow, io::Read}; use tendermint::net; +use tokio::{net::TcpStream, sync::mpsc, task::JoinHandle}; -use crate::{client::transport::Transport, Error}; -use bytes::buf::BufExt; -use std::io::Read; +use crate::{ + client::transport::{EventConnection, EventProducer, SubscriptionTransport, Transport}, + endpoint::{subscribe, unsubscribe}, + events::Event, + response::Response, + Error, Request, +}; +// We anticipate that this will be a relatively low-traffic command signaling +// mechanism (these commands are limited to subscribe/unsubscribe requests, +// which we assume won't occur very frequently). +const DEFAULT_WEBSOCKET_CMD_BUF_SIZE: usize = 50; + +/// An HTTP-based transport layer for the Tendermint RPC Client. Subscriptions +/// are managed via a WebSockets connection which is maintained separately to +/// the HTTP request mechanisms. #[derive(Debug)] pub struct HttpWsTransport { host: String, port: u16, } +#[derive(Debug)] +struct WsSubscriptionTransport { + driver_hdl: JoinHandle>, + cmd_tx: mpsc::Sender, +} + +#[derive(Debug)] +struct WsDriver { + stream: WebSocketStream>, + event_tx: mpsc::Sender, + cmd_rx: mpsc::Receiver, +} + +enum WsCmd { + Subscribe { query: String }, + Unsubscribe { query: String }, + Close, +} + #[async_trait] impl Transport for HttpWsTransport { async fn request(&self, request_body: String) -> Result { @@ -43,10 +86,21 @@ impl Transport for HttpWsTransport { .map_err(|e| Error::internal_error(format!("failed to read response body: {}", e))); Ok(response_string) } + + /// Initiates a new WebSocket connection to the remote endpoint. + async fn new_event_connection(&self, event_buf_size: usize) -> Result { + let (transport, event_producer) = WsSubscriptionTransport::connect( + &format!("ws://{}:{}/websocket", self.host, self.port), + event_buf_size, + DEFAULT_WEBSOCKET_CMD_BUF_SIZE, + ) + .await?; + Ok(EventConnection::new(Box::new(transport), event_producer)) + } } impl HttpWsTransport { - /// Create a new HTTP/WebSockets + /// Create a new HTTP/WebSockets transport layer. pub fn new(address: net::Address) -> Result { let (host, port) = match address { net::Address::Tcp { host, port, .. } => (host, port), @@ -60,3 +114,165 @@ impl HttpWsTransport { Ok(HttpWsTransport { host, port }) } } + +#[async_trait] +impl SubscriptionTransport for WsSubscriptionTransport { + async fn subscribe(&mut self, query: String) -> Result<(), Error> { + self.cmd_tx + .send(WsCmd::Subscribe { query }) + .await + .map_err(|e| { + Error::internal_error(format!( + "failed to transmit subscription command to async WebSocket driver: {}", + e + )) + }) + } + + async fn unsubscribe(&mut self, query: String) -> Result<(), Error> { + self.cmd_tx + .send(WsCmd::Unsubscribe { query }) + .await + .map_err(|e| { + Error::internal_error(format!( + "failed to transmit unsubscribe command to async WebSocket driver: {}", + e + )) + }) + } + + async fn close(&mut self) -> Result<(), Error> { + self.cmd_tx.send(WsCmd::Close).await.map_err(|e| { + Error::internal_error(format!( + "failed to send termination command to async task: {}", + e + )) + }) + + // TODO: Find a way to wait for the driver to terminate. + } +} + +impl WsSubscriptionTransport { + async fn connect( + url: &str, + event_buf_size: usize, + cmd_buf_size: usize, + ) -> Result<(WsSubscriptionTransport, EventProducer), Error> { + let (stream, _response) = connect_async(url).await?; + let (event_tx, event_rx) = mpsc::channel(event_buf_size); + let (cmd_tx, cmd_rx) = mpsc::channel(cmd_buf_size); + let driver = WsDriver { + stream, + event_tx, + cmd_rx, + }; + let driver_hdl = tokio::spawn(async move { driver.run().await }); + Ok(( + WsSubscriptionTransport { driver_hdl, cmd_tx }, + EventProducer::new(event_rx), + )) + } +} + +impl WsDriver { + async fn run(mut self) -> Result<(), Error> { + // TODO: Should this loop initiate a keepalive (ping) to the server on a regular basis? + loop { + tokio::select! { + Some(res) = self.stream.next() => match res { + Ok(msg) => self.handle_incoming_msg(msg).await?, + Err(e) => return Err( + Error::websocket_error( + format!("failed to read from WebSocket connection: {}", e), + ), + ), + }, + Some(cmd) = self.cmd_rx.next() => match cmd { + WsCmd::Subscribe { query } => self.subscribe(query).await?, + WsCmd::Unsubscribe { query } => self.unsubscribe(query).await?, + WsCmd::Close => return self.close().await, + } + } + } + } + + async fn subscribe(&mut self, query: String) -> Result<(), Error> { + let req = subscribe::Request::new(query); + self.stream + .send(Message::Text(req.into_json())) + .await + .map_err(|e| { + Error::websocket_error(format!("failed to write to WebSocket connection: {}", e)) + }) + } + + async fn unsubscribe(&mut self, query: String) -> Result<(), Error> { + let req = unsubscribe::Request::new(query); + self.stream + .send(Message::Text(req.into_json())) + .await + .map_err(|e| { + Error::websocket_error(format!("failed to write to WebSocket connection: {}", e)) + }) + } + + async fn handle_incoming_msg(&mut self, msg: Message) -> Result<(), Error> { + match msg { + Message::Text(s) => self.handle_text_msg(s).await, + Message::Ping(v) => self.pong(v).await, + Message::Pong(_) | Message::Binary(_) => Ok(()), + Message::Close(_) => Ok(()), + } + } + + async fn handle_text_msg(&mut self, msg: String) -> Result<(), Error> { + match Event::from_string(msg) { + Ok(ev) => self.handle_event(ev).await, + // TODO: Should we just ignore messages we can't deserialize? + // There are a number of possible messages we may receive + // from the WebSocket endpoint that we'll end up ignoring + // anyways (like responses for subscribe/unsubscribe + // requests). + Err(_) => Ok(()), + } + } + + async fn handle_event(&mut self, ev: Event) -> Result<(), Error> { + self.event_tx.send(ev).await.map_err(|e| { + Error::internal_error(format!( + "failed to publish incoming event to event producer: {}", + e + )) + }) + } + + async fn pong(&mut self, v: Vec) -> Result<(), Error> { + self.stream.send(Message::Pong(v)).await.map_err(|e| { + Error::websocket_error(format!("failed to write WebSocket pong message: {}", e)) + }) + } + + async fn close(&mut self) -> Result<(), Error> { + let _ = self + .stream + .send(Message::Close(Some(CloseFrame { + code: CloseCode::Normal, + reason: Cow::from("client closed WebSocket connection"), + }))) + .await + .map_err(|e| { + Error::websocket_error(format!( + "failed to cleanly terminate WebSocket connection: {}", + e + )) + })?; + + while let Some(res) = self.stream.next().await { + if let Err(_) = res { + return Ok(()); + } + } + Ok(()) + } +} diff --git a/rpc/src/endpoint.rs b/rpc/src/endpoint.rs index b4522bff5..e613c212a 100644 --- a/rpc/src/endpoint.rs +++ b/rpc/src/endpoint.rs @@ -13,4 +13,5 @@ pub mod health; pub mod net_info; pub mod status; pub mod subscribe; +pub mod unsubscribe; pub mod validators; diff --git a/rpc/src/endpoint/subscribe.rs b/rpc/src/endpoint/subscribe.rs index 4dfc28876..11163e8bf 100644 --- a/rpc/src/endpoint/subscribe.rs +++ b/rpc/src/endpoint/subscribe.rs @@ -3,14 +3,15 @@ use serde::{Deserialize, Serialize}; use std::io::Read; -/// Subscribe request for events on websocket +/// Subscription request for events. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Request { query: String, } impl Request { - /// Query the Tendermint nodes event and stream events over web socket + /// Query the Tendermint nodes event and stream events (by default over a + /// WebSocket connection). pub fn new(query: String) -> Self { Self { query } } @@ -28,9 +29,7 @@ impl crate::Request for Request { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response {} -/// Subcribe is weird RPC endpoint. It's only meaningful at websocket response and there isn't a -/// synchronous reponse offered. It there is an error it's asynchronous and we don't try and stich -/// the async response back together with the request. +/// 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 { diff --git a/rpc/src/endpoint/unsubscribe.rs b/rpc/src/endpoint/unsubscribe.rs new file mode 100644 index 000000000..eb1a417e8 --- /dev/null +++ b/rpc/src/endpoint/unsubscribe.rs @@ -0,0 +1,44 @@ +//! `/unsubscribe` endpoint JSONRPC wrapper + +use serde::{Deserialize, Serialize}; +use std::io::Read; + +/// Subscribe request for events on websocket +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct Request { + query: String, +} + +impl Request { + /// Create a new unsubscribe request with the query from which to + /// unsubscribe. + pub fn new(query: String) -> Self { + Self { query } + } +} + +impl crate::Request for Request { + type Response = Response; + + fn method(&self) -> crate::Method { + crate::Method::Unsubscribe + } +} + +/// Status responses +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Response {} + +/// Unsubscribe does not have a meaningful response. +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 {}) + } +} diff --git a/rpc/src/events.rs b/rpc/src/events.rs new file mode 100644 index 000000000..9b6311796 --- /dev/null +++ b/rpc/src/events.rs @@ -0,0 +1,74 @@ +//! RPC subscription event-related data structures. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tendermint::{ + abci::responses::{BeginBlock, EndBlock}, + Block, +}; + +use crate::{response::Wrapper, Response}; + +/// An incoming event produced by a subscription. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Event { + /// The query that produced the event. + pub query: String, + /// The data associated with the event. + pub data: EventData, + /// Event type and attributes map. + pub events: Option>>, +} +impl Response for Event {} + +/// A JSONRPC-wrapped event. +pub type WrappedEvent = Wrapper; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "value")] +pub enum EventData { + #[serde(alias = "tendermint/event/NewBlock")] + NewBlock { + block: Option, + result_begin_block: Option, + result_end_block: Option, + }, + #[serde(alias = "tendermint/event/Tx")] + Tx { + tx_result: TxResult, + }, + GenericJSONEvent(serde_json::Value), +} + +/// Tx Result +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TxResult { + pub height: String, + pub index: i64, + pub tx: String, + pub result: TxResultResult, +} + +/// TX Results Results +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TxResultResult { + pub log: String, + pub gas_wanted: String, + pub gas_used: String, + pub events: Vec, +} + +/// Tendermint ABCI Events +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TmEvent { + #[serde(rename = "type")] + pub event_type: String, + pub attributes: Vec, +} + +/// Event Attributes +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Attribute { + pub key: String, + pub value: String, +} diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 21d1049bf..77fb0547d 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -7,6 +7,7 @@ pub use client::{event_listener, Client}; pub mod endpoint; pub mod error; +pub mod events; mod id; mod method; pub mod request; diff --git a/rpc/src/method.rs b/rpc/src/method.rs index 46e6b85d9..107c42fda 100644 --- a/rpc/src/method.rs +++ b/rpc/src/method.rs @@ -54,9 +54,12 @@ pub enum Method { /// Get validator info for a block Validators, - /// Subscribe to events over the websocket + /// Subscribe to events Subscribe, + /// Unsubscribe from events + Unsubscribe, + /// Broadcast evidence BroadcastEvidence, } @@ -81,6 +84,7 @@ impl Method { Method::Validators => "validators", Method::Subscribe => "subscribe", Method::BroadcastEvidence => "broadcast_evidence", + Method::Unsubscribe => "unsubscribe", } } } @@ -105,6 +109,7 @@ impl FromStr for Method { "status" => Method::Status, "validators" => Method::Validators, "subscribe" => Method::Subscribe, + "unsubscribe" => Method::Unsubscribe, "broadcast_evidence" => Method::BroadcastEvidence, other => return Err(Error::method_not_found(other)), }) diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index a4a6d5469..20fdcfb99 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -13,6 +13,7 @@ mod rpc { use tendermint_rpc::{event_listener, Client}; + use futures::StreamExt; use tendermint::abci::Code; use tendermint::abci::Log; @@ -145,6 +146,28 @@ mod rpc { assert_eq!(status.validator_info.voting_power.value(), 10); } + #[tokio::test] + async fn subscription_interface() { + let client = localhost_rpc_client(); + let mut subs_mgr = client.new_subscription_manager(10).await.unwrap(); + let mut subs = subs_mgr + .subscribe("tm.event='NewBlock'".to_string(), 10) + .await + .unwrap(); + let mut ev_count: usize = 0; + + dbg!("Attempting to grab 10 new blocks"); + while let Some(ev) = subs.next().await { + dbg!("Got event: {:?}", ev); + ev_count += 1; + if ev_count > 10 { + break; + } + } + + subs_mgr.terminate().await.unwrap(); + } + #[tokio::test] #[ignore] async fn event_subscription() { From 20cce0af937be558ed797bca9fd81e29f1bc6cd2 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 07:23:09 -0400 Subject: [PATCH 07/60] Ignore integration test Signed-off-by: Thane Thomson --- tendermint/tests/integration.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index 20fdcfb99..36d42aa43 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -147,6 +147,7 @@ mod rpc { } #[tokio::test] + #[ignore] async fn subscription_interface() { let client = localhost_rpc_client(); let mut subs_mgr = client.new_subscription_manager(10).await.unwrap(); From 92cae5f991f64d4fd7ca19c048c7b54992dda6ac Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 11:52:31 -0400 Subject: [PATCH 08/60] Update rpc/Cargo.toml Co-authored-by: Alexander Simmerl --- rpc/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 1fa1ac8ca..78863d68e 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -25,7 +25,7 @@ all-features = true [features] default = [] -client = [ "async-tungstenite", "futures", "http", "hyper", "tokio", "async-trait" ] +client = [ "async-trait", "async-tungstenite", "futures", "http", "hyper", "tokio" ] secp256k1 = ["tendermint/secp256k1"] [dependencies] @@ -43,4 +43,4 @@ futures = { version = "0.3", optional = true } http = { version = "0.2", optional = true } hyper = { version = "0.13", optional = true } tokio = { version = "0.2", features = ["macros", "fs"], optional = true } -async-trait = { version = "0.1.36", optional = true } \ No newline at end of file +async-trait = { version = "0.1.36", optional = true } From 40e7de2b679bef204bc87c0d3ddbba1ef78fa7a4 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 11:53:10 -0400 Subject: [PATCH 09/60] Update rpc/src/client.rs Co-authored-by: Alexander Simmerl --- rpc/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 3aeded6e3..fc9b6c729 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -21,7 +21,7 @@ pub mod subscription; pub mod transport; #[cfg(test)] -pub mod testing; +pub mod test; /// Tendermint RPC client. /// From 4634e488394009ece4b7d2c247dc7e225793484a Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 11:53:23 -0400 Subject: [PATCH 10/60] Update rpc/src/client/subscription.rs Co-authored-by: Alexander Simmerl --- rpc/src/client/subscription.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 1bc129129..11334c178 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -13,7 +13,7 @@ use tokio::{stream::StreamExt, sync::mpsc, task::JoinHandle}; use crate::{client::transport::EventConnection, events::Event, Error}; -/// The subscription manager is just an interface/handle to the subscription +/// The subscription manager is an interface to the subscription /// router, which runs asynchronously in a separate process. #[derive(Debug)] pub struct SubscriptionManager { From 82cf5713d5e2a777792d3e558bb84563e4b57b07 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 11:53:37 -0400 Subject: [PATCH 11/60] Update rpc/src/client/subscription.rs Co-authored-by: Alexander Simmerl --- rpc/src/client/subscription.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 11334c178..8e2b77a18 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -41,7 +41,7 @@ struct SubscriptionRouter { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) struct SubscriptionId(usize); +pub struct SubscriptionId(usize); #[derive(Debug)] enum RouterCmd { From 52c36d0439fc8085f76fb1b58f4557d707bb6701 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 11:53:46 -0400 Subject: [PATCH 12/60] Update rpc/src/client/subscription.rs Co-authored-by: Alexander Simmerl --- rpc/src/client/subscription.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 8e2b77a18..afc99a925 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -153,7 +153,7 @@ impl SubscriptionRouter { }; let mut disconnected = Vec::new(); for (subs_id, tx) in subs_for_query { - // TODO: Right now we automatically remove any disconnected or full + // TODO(thane): Right now we automatically remove any disconnected or full // channels. We must handle full channels differently to // disconnected ones. if let Err(_) = tx.send(ev.clone()).await { From 1089e8dde5792984f4fab8ef4ea0dba9d883da82 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 11:53:54 -0400 Subject: [PATCH 13/60] Update rpc/src/client/transport.rs Co-authored-by: Alexander Simmerl --- rpc/src/client/transport.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index 0a3adcfce..abdceeb12 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -46,8 +46,8 @@ pub struct EventConnection { // The `EventConnection` struct is a workaround for the fact that we need // to use `Transport` as a trait object, but trait objects are not allowed // to use generics in their method signatures. - pub(crate) transport: Box, - pub(crate) event_producer: EventProducer, + pub transport: Box, + pub event_producer: EventProducer, } impl EventConnection { From 5994999a1a1d29f0d2907d27a658abbec7bbc355 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 12:02:35 -0400 Subject: [PATCH 14/60] Simplify submodule naming Signed-off-by: Thane Thomson --- rpc/src/client/{testing.rs => test.rs} | 0 rpc/src/client/{testing => test}/matching_transport.rs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename rpc/src/client/{testing.rs => test.rs} (100%) rename rpc/src/client/{testing => test}/matching_transport.rs (100%) diff --git a/rpc/src/client/testing.rs b/rpc/src/client/test.rs similarity index 100% rename from rpc/src/client/testing.rs rename to rpc/src/client/test.rs diff --git a/rpc/src/client/testing/matching_transport.rs b/rpc/src/client/test/matching_transport.rs similarity index 100% rename from rpc/src/client/testing/matching_transport.rs rename to rpc/src/client/test/matching_transport.rs From 1ef2305ed4b8bbf3f843955b82b25bc12fbe2787 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 12:03:59 -0400 Subject: [PATCH 15/60] Reorder optional dependencies alphabetically Signed-off-by: Thane Thomson --- rpc/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 78863d68e..c82c12078 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -39,8 +39,8 @@ thiserror = "1" uuid = { version = "0.8", default-features = false } async-tungstenite = { version="0.5", features = ["tokio-runtime"], optional = true } +async-trait = { version = "0.1", optional = true } futures = { version = "0.3", optional = true } http = { version = "0.2", optional = true } hyper = { version = "0.13", optional = true } tokio = { version = "0.2", features = ["macros", "fs"], optional = true } -async-trait = { version = "0.1.36", optional = true } From a364187902f9ea648716cd8fdcf3eaf657a34a9c Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 12:20:14 -0400 Subject: [PATCH 16/60] Rename module to singular Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 2 +- rpc/src/client/transport.rs | 2 +- rpc/src/client/transport/http_ws.rs | 2 +- rpc/src/{events.rs => event.rs} | 0 rpc/src/lib.rs | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename rpc/src/{events.rs => event.rs} (100%) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index afc99a925..9ffa9b812 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -11,7 +11,7 @@ use std::{ }; use tokio::{stream::StreamExt, sync::mpsc, task::JoinHandle}; -use crate::{client::transport::EventConnection, events::Event, Error}; +use crate::{client::transport::EventConnection, event::Event, Error}; /// The subscription manager is an interface to the subscription /// router, which runs asynchronously in a separate process. diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index abdceeb12..f0b4ba558 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -8,7 +8,7 @@ use futures::{ use std::pin::Pin; use tokio::sync::mpsc; -use crate::{events::Event, Error}; +use crate::{event::Event, Error}; pub mod http_ws; diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index e2b9af3b6..0c631e783 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -20,7 +20,7 @@ use tokio::{net::TcpStream, sync::mpsc, task::JoinHandle}; use crate::{ client::transport::{EventConnection, EventProducer, SubscriptionTransport, Transport}, endpoint::{subscribe, unsubscribe}, - events::Event, + event::Event, response::Response, Error, Request, }; diff --git a/rpc/src/events.rs b/rpc/src/event.rs similarity index 100% rename from rpc/src/events.rs rename to rpc/src/event.rs diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 77fb0547d..541f3da08 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -7,7 +7,7 @@ pub use client::{event_listener, Client}; pub mod endpoint; pub mod error; -pub mod events; +pub mod event; mod id; mod method; pub mod request; From 15d5765d3204976fc3476d38f91595d7b67acf7d Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 12:23:16 -0400 Subject: [PATCH 17/60] Destructure and reformat imports Signed-off-by: Thane Thomson --- rpc/src/client.rs | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/rpc/src/client.rs b/rpc/src/client.rs index fc9b6c729..bebfbc1d6 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -1,20 +1,14 @@ //! Tendermint RPC client -use tendermint::{ - abci::{self, Transaction}, - block::Height, - evidence::Evidence, - net, Genesis, -}; - -use crate::{ - client::{ - subscription::SubscriptionManager, - transport::{http_ws::HttpWsTransport, Transport}, - }, - endpoint::*, - Error, Request, Response, -}; +use crate::client::subscription::SubscriptionManager; +use crate::client::transport::http_ws::HttpWsTransport; +use crate::client::transport::Transport; +use crate::endpoint::*; +use crate::{Error, Request, Response}; +use tendermint::abci::{self, Transaction}; +use tendermint::block::Height; +use tendermint::evidence::Evidence; +use tendermint::{net, Genesis}; pub mod event_listener; pub mod subscription; From 7e6d15dd5fa9b451f14655d176a990f59840ad72 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 20:38:00 -0400 Subject: [PATCH 18/60] Refactor subscription mechanism to simplify The previous iteration of the subscription mechanism relied on two asynchronous tasks to manage subscriptions and route events from the WebSockets connection to their relevant subscribers. This version only uses one async task (the WebSocket driver, which must interact asynchronously with the connection). In addition to this, I tried experimenting with using generics instead of trait objects for the transport layer for the `Client`. This works, but the downside of using generics is that, if you want generalizability at layers higher up, it comes at the cost of decreased readability of the code. Signed-off-by: Thane Thomson --- light-client/src/components/io.rs | 10 +- light-client/src/evidence.rs | 10 +- rpc/src/client.rs | 139 ++++--- rpc/src/client/subscription.rs | 221 ++++------- rpc/src/client/test/matching_transport.rs | 84 ----- rpc/src/client/{test.rs => test_support.rs} | 0 .../client/test_support/matching_transport.rs | 96 +++++ rpc/src/client/transport.rs | 122 +++---- rpc/src/client/transport/http_ws.rs | 345 ++++++++++-------- rpc/src/endpoint/subscribe.rs | 2 +- rpc/src/endpoint/unsubscribe.rs | 18 +- rpc/src/lib.rs | 2 +- rpc/src/request.rs | 2 +- tendermint/tests/integration.rs | 24 +- 14 files changed, 545 insertions(+), 530 deletions(-) delete mode 100644 rpc/src/client/test/matching_transport.rs rename rpc/src/client/{test.rs => test_support.rs} (100%) create mode 100644 rpc/src/client/test_support/matching_transport.rs diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index 05c4c3b03..e0c95e52d 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -166,10 +166,16 @@ impl ProdIo { } // FIXME: Cannot enable precondition because of "autoref lifetime" issue + // TODO(thane): Generalize over client transport (instead of using HttpTransport directly). // #[pre(self.peer_map.contains_key(&peer))] - fn rpc_client_for(&self, peer: PeerId) -> Result { + fn rpc_client_for( + &self, + peer: PeerId, + ) -> Result, IoError> { let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - rpc::Client::new(peer_addr).map_err(IoError::from) + Ok(rpc::Client::new( + rpc::transport::http_ws::HttpTransport::new(peer_addr).map_err(IoError::from)?, + )) } } diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index a9ff6109a..8f9fbaa0f 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -47,10 +47,16 @@ impl ProdEvidenceReporter { } // FIXME: Cannot enable precondition because of "autoref lifetime" issue + // TODO(thane): Generalize over client transport (instead of using HttpTransport directly). // #[pre(self.peer_map.contains_key(&peer))] - fn rpc_client_for(&self, peer: PeerId) -> Result { + fn rpc_client_for( + &self, + peer: PeerId, + ) -> Result, IoError> { let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - rpc::Client::new(peer_addr).map_err(IoError::from) + Ok(rpc::Client::new( + rpc::transport::http_ws::HttpTransport::new(peer_addr).map_err(IoError::from)?, + )) } } diff --git a/rpc/src/client.rs b/rpc/src/client.rs index bebfbc1d6..3600139ea 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -1,39 +1,39 @@ //! Tendermint RPC client -use crate::client::subscription::SubscriptionManager; -use crate::client::transport::http_ws::HttpWsTransport; -use crate::client::transport::Transport; +use crate::client::subscription::Subscription; +use crate::client::transport::{SubscriptionTransport, Transport}; use crate::endpoint::*; -use crate::{Error, Request, Response}; +use crate::{Error, Request}; use tendermint::abci::{self, Transaction}; use tendermint::block::Height; use tendermint::evidence::Evidence; -use tendermint::{net, Genesis}; +use tendermint::Genesis; +use tokio::sync::mpsc; pub mod event_listener; pub mod subscription; pub mod transport; #[cfg(test)] -pub mod test; +pub mod test_support; -/// Tendermint RPC client. -/// -/// Presently supports JSONRPC via HTTP. +/// The default number of events we buffer in a [`Subscription`] if you do not +/// specify the buffer size when creating it. +pub const DEFAULT_SUBSCRIPTION_BUF_SIZE: usize = 100; + +/// The base Tendermint RPC client, which is responsible for handling most +/// requests with the exception of subscription mechanics. Once you have an RPC +/// client, you can create a [`SubscriptionClient`] using +/// [`new_subscription_client`]. #[derive(Debug)] -pub struct Client { - transport: Box, +pub struct Client { + transport: T, } -impl Client { - /// Create a new Tendermint RPC client, connecting to the given address. - /// By default this uses the [`HttpWsTransport`] transport layer. This - /// transport lazily initializes subscription mechanisms when first - /// subscribing to events generated by a particular query. - pub fn new(address: net::Address) -> Result { - Ok(Self { - transport: Box::new(HttpWsTransport::new(address)?), - }) +impl Client { + /// Create a new Tendermint RPC client using the given transport layer. + pub fn new(transport: T) -> Self { + Self { transport } } /// `/abci_info`: get information about the ABCI application. @@ -167,39 +167,96 @@ impl Client { self.perform(evidence::Request::new(e)).await } - /// Creates a subscription management interface for this RPC client. This - /// interface facilitates subscribing and unsubscribing from receiving - /// events produced by specific RPC queries. - pub async fn new_subscription_manager( - &self, - event_buf_size: usize, - ) -> Result { - let conn = self.transport.new_event_connection(event_buf_size).await?; - Ok(SubscriptionManager::new(conn, 10)) - } - /// Perform a request against the RPC endpoint pub async fn perform(&self, request: R) -> Result where R: Request, { - let request_body = request.into_json(); - let response_body = self.transport.request(request_body).await?; - R::Response::from_string(response_body) + self.transport.request(request).await } + + /// Gracefully terminate the underlying connection (if relevant - depends + /// on the underlying transport). + pub async fn close(self) -> Result<(), Error> { + self.transport.close().await + } +} + +/// A client solely dedicated to facilitating subscriptions to [`Event`]s. +/// +/// [`Event`]: crate::event::Event +#[derive(Debug)] +pub struct SubscriptionClient { + transport: T, +} + +/// Create a new subscription client derived from the given RPC client. +pub async fn new_subscription_client( + client: &Client, +) -> Result, Error> +where + T: Transport, + ::SubscriptionTransport: SubscriptionTransport, +{ + Ok(SubscriptionClient::new( + client.transport.subscription_transport().await?, + )) } -impl From> for Client { - fn from(transport: Box) -> Self { +impl SubscriptionClient { + fn new(transport: T) -> Self { Self { transport } } + + /// Subscribe to events generated by the given query. + /// + /// The `buf_size` parameter allows for control over how many events get + /// buffered by the returned [`Subscription`]. The faster you can process + /// incoming events, the smaller this buffer size needs to be. + pub async fn subscribe_with_buf_size( + &mut self, + query: String, + buf_size: usize, + ) -> Result { + let (event_tx, event_rx) = mpsc::channel(buf_size); + let id = self + .transport + .subscribe(subscribe::Request::new(query.clone()), event_tx) + .await?; + Ok(Subscription::new(id, query, event_rx)) + } + + /// Subscribe to events generated by the given query, using the + /// [`DEFAULT_SUBSCRIPTION_BUF_SIZE`]. + pub async fn subscribe(&mut self, query: String) -> Result { + self.subscribe_with_buf_size(query, DEFAULT_SUBSCRIPTION_BUF_SIZE) + .await + } + + /// Terminate the given subscription and consume it. + pub async fn unsubscribe(&mut self, subscription: Subscription) -> Result<(), Error> { + self.transport + .unsubscribe( + unsubscribe::Request::new(subscription.query.clone()), + subscription, + ) + .await + } + + /// Gracefully terminate the underlying connection (if relevant - depends + /// on the underlying transport). + pub async fn close(self) -> Result<(), Error> { + self.transport.close().await + } } #[cfg(test)] mod test { use super::*; + use crate::client::test_support::matching_transport::{ + MethodMatcher, RequestMatchingTransport, + }; use crate::Method; - use testing::matching_transport::{MethodMatcher, RequestMatchingTransport}; // TODO: Read from a fixture in the crate. const ABCI_INFO_RESPONSE: &str = r#"{ @@ -283,15 +340,13 @@ mod test { }"#; #[tokio::test] - async fn mocked_transport() { - let mt = RequestMatchingTransport::new(MethodMatcher::new( + async fn mocked_client_transport() { + let transport = RequestMatchingTransport::new(MethodMatcher::new( Method::AbciInfo, Ok(ABCI_INFO_RESPONSE.into()), )) .push(MethodMatcher::new(Method::Block, Ok(BLOCK_RESPONSE.into()))); - - let transport: Box = Box::new(mt); - let client = Client::from(transport); + let client = Client::new(transport); let abci_info = client.abci_info().await.unwrap(); assert_eq!("GaiaApp".to_string(), abci_info.data); diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 9ffa9b812..65eb2a18a 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -1,206 +1,115 @@ //! Subscription- and subscription management-related functionality. -use futures::{ - task::{Context, Poll}, - Stream, -}; -use std::{ - collections::HashMap, - pin::Pin, - sync::atomic::{AtomicUsize, Ordering}, -}; -use tokio::{stream::StreamExt, sync::mpsc, task::JoinHandle}; - -use crate::{client::transport::EventConnection, event::Event, Error}; - -/// The subscription manager is an interface to the subscription -/// router, which runs asynchronously in a separate process. -#[derive(Debug)] -pub struct SubscriptionManager { - router: JoinHandle>, - cmd_tx: mpsc::Sender, - next_subs_id: AtomicUsize, -} +use crate::event::Event; +use futures::task::{Context, Poll}; +use futures::Stream; +use std::collections::HashMap; +use std::pin::Pin; +use tokio::sync::mpsc; /// An interface that can be used to asynchronously receive events for a /// particular subscription. #[derive(Debug)] pub struct Subscription { + pub query: String, id: SubscriptionId, - query: String, event_rx: mpsc::Receiver, } -// The subscription router does the heavy lifting of managing subscriptions and -// routing incoming events to their relevant subscribers. -struct SubscriptionRouter { - conn: EventConnection, - cmd_rx: mpsc::Receiver, - // Maps queries -> (maps of subscription IDs -> channels) - subscriptions: HashMap>>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SubscriptionId(usize); +impl Stream for Subscription { + type Item = Event; -#[derive(Debug)] -enum RouterCmd { - Subscribe { - id: SubscriptionId, - query: String, - event_tx: mpsc::Sender, - }, - Unsubscribe(Subscription), - Terminate, + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.event_rx.poll_recv(cx) + } } -impl SubscriptionManager { - pub(crate) fn new(conn: EventConnection, cmd_buf_size: usize) -> Self { - let (cmd_tx, cmd_rx) = mpsc::channel(cmd_buf_size); - let router = SubscriptionRouter::new(conn, cmd_rx); +impl Subscription { + pub fn new(id: SubscriptionId, query: String, event_rx: mpsc::Receiver) -> Self { Self { - router: tokio::spawn(async move { router.run().await }), - cmd_tx, - next_subs_id: AtomicUsize::new(0), - } - } - - pub async fn subscribe( - &mut self, - query: String, - buf_size: usize, - ) -> Result { - let (event_tx, event_rx) = mpsc::channel(buf_size); - let id = self.next_subs_id(); - let _ = self - .cmd_tx - .send(RouterCmd::Subscribe { - id: id.clone(), - query: query.clone(), - event_tx, - }) - .await - .map_err(|e| { - Error::internal_error(format!( - "failed to transmit subscription request to async task: {}", - e - )) - })?; - Ok(Subscription { id, query, event_rx, - }) + } } +} - pub async fn unsubscribe(&mut self, subs: Subscription) -> Result<(), Error> { - self.cmd_tx - .send(RouterCmd::Unsubscribe(subs)) - .await - .map_err(|e| { - Error::internal_error(format!( - "failed to transmit unsubscribe request to async task: {}", - e - )) - }) - } +/// Each new subscription is automatically assigned an ID. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SubscriptionId(usize); - /// Gracefully terminate the subscription manager and its router (which - /// runs in an asynchronous task). - pub async fn terminate(mut self) -> Result<(), Error> { - let _ = self.cmd_tx.send(RouterCmd::Terminate).await.map_err(|e| { - Error::internal_error(format!( - "failed to transmit termination request to async task: {}", - e - )) - })?; - self.router - .await - .map_err(|e| Error::internal_error(format!("failed to terminate async task: {}", e)))? +impl From for SubscriptionId { + fn from(u: usize) -> Self { + SubscriptionId(u) } +} - fn next_subs_id(&self) -> SubscriptionId { - SubscriptionId(self.next_subs_id.fetch_add(1, Ordering::SeqCst)) +impl From for usize { + fn from(subs_id: SubscriptionId) -> Self { + subs_id.0 } } -impl SubscriptionRouter { - fn new(conn: EventConnection, cmd_rx: mpsc::Receiver) -> Self { - Self { - conn, - cmd_rx, - subscriptions: HashMap::new(), - } +impl SubscriptionId { + pub fn next(&self) -> SubscriptionId { + SubscriptionId(self.0 + 1) } +} - async fn run(mut self) -> Result<(), Error> { - loop { - tokio::select! { - Some(ev) = self.conn.event_producer.next() => self.route_event(ev).await, - Some(cmd) = self.cmd_rx.next() => match cmd { - RouterCmd::Subscribe { id, query, event_tx } => self.subscribe(id, query, event_tx).await?, - RouterCmd::Unsubscribe(subs) => self.unsubscribe(subs).await?, - RouterCmd::Terminate => return self.terminate().await, - }, - } - } +/// Provides a mechanism for tracking subscriptions and routing events to those +/// subscriptions. This is useful when implementing your own RPC client +/// transport layer. +#[derive(Debug, Clone)] +pub struct SubscriptionRouter(HashMap>>); + +impl SubscriptionRouter { + /// Create a new empty `SubscriptionRouter`. + pub fn new() -> Self { + Self(HashMap::new()) } - async fn route_event(&mut self, ev: Event) { - let subs_for_query = match self.subscriptions.get_mut(&ev.query) { + /// 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.0.get_mut(&ev.query) { Some(s) => s, None => return, }; - let mut disconnected = Vec::new(); - for (subs_id, tx) in subs_for_query { - // TODO(thane): Right now we automatically remove any disconnected or full - // channels. We must handle full channels differently to - // disconnected ones. - if let Err(_) = tx.send(ev.clone()).await { - disconnected.push(subs_id.clone()); + let mut disconnected = Vec::::new(); + for (id, event_tx) in subs_for_query { + // TODO(thane): Right now we automatically remove any disconnected + // or full channels. We must handle full channels + // differently to disconnected ones. + if let Err(_) = event_tx.send(ev.clone()).await { + disconnected.push(id.clone()); } } - let subs_for_query = self.subscriptions.get_mut(&ev.query).unwrap(); - for subs_id in disconnected { - subs_for_query.remove(&subs_id); + let subs_for_query = self.0.get_mut(&ev.query).unwrap(); + for id in disconnected { + subs_for_query.remove(&id); } } - async fn subscribe( - &mut self, - id: SubscriptionId, - query: String, - event_tx: mpsc::Sender, - ) -> Result<(), Error> { - let subs_for_query = match self.subscriptions.get_mut(&query) { + /// Keep track of a new subscription for a particular query. + pub fn add(&mut self, id: SubscriptionId, query: String, event_tx: mpsc::Sender) { + let subs_for_query = match self.0.get_mut(&query) { Some(s) => s, None => { - self.subscriptions.insert(query.clone(), HashMap::new()); - self.subscriptions.get_mut(&query).unwrap() + self.0.insert(query.clone(), HashMap::new()); + self.0.get_mut(&query).unwrap() } }; subs_for_query.insert(id, event_tx); - self.conn.transport.subscribe(query).await } - async fn unsubscribe(&mut self, subs: Subscription) -> Result<(), Error> { - let subs_for_query = match self.subscriptions.get_mut(&subs.query) { + /// Remove the given subscription and consume it. + pub fn remove(&mut self, subs: Subscription) { + let subs_for_query = match self.0.get_mut(&subs.query) { Some(s) => s, - None => return Ok(()), + None => return, }; subs_for_query.remove(&subs.id); - self.conn.transport.unsubscribe(subs.query).await - } - - async fn terminate(mut self) -> Result<(), Error> { - self.conn.terminate().await - } -} - -impl Stream for Subscription { - type Item = Event; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.event_rx.poll_recv(cx) } } diff --git a/rpc/src/client/test/matching_transport.rs b/rpc/src/client/test/matching_transport.rs deleted file mode 100644 index 61417699d..000000000 --- a/rpc/src/client/test/matching_transport.rs +++ /dev/null @@ -1,84 +0,0 @@ -use async_trait::async_trait; - -use crate::{ - client::{ - testing::Fixture, - transport::{EventConnection, Transport}, - }, - Error, Method, -}; - -/// A rudimentary fixture-based transport. -/// -/// Fixtures, if read from the file system, are lazily evaluated. -#[derive(Debug)] -pub struct RequestMatchingTransport { - matchers: Vec>, -} - -/// Implement this trait to facilitate different kinds of request matching. -pub trait RequestMatcher: Send + Sync + std::fmt::Debug { - /// Does the given request match? - fn matches(&self, request: &str) -> bool; - - /// The response we need to return if the request matches. - fn response(&self) -> Result; -} - -/// A simple matcher that just returns a specific response every time it gets -/// a request of a particular request method. -#[derive(Debug)] -pub struct MethodMatcher { - method: Method, - response: Result, -} - -#[async_trait] -impl Transport for RequestMatchingTransport { - async fn request(&self, request: String) -> Result { - for matcher in &self.matchers { - if matcher.matches(&request) { - let response = matcher.response()?.read().await; - return Ok(response); - } - } - Err(Error::internal_error(format!( - "no matcher for request: {}", - request - ))) - } - - async fn new_event_connection(&self, event_buf_size: usize) -> Result { - unimplemented!() - } -} - -impl RequestMatchingTransport { - pub fn new(matcher: impl RequestMatcher + 'static) -> Self { - Self { - matchers: vec![Box::new(matcher)], - } - } - - pub fn push(mut self, matcher: impl RequestMatcher + 'static) -> Self { - self.matchers.push(Box::new(matcher)); - self - } -} - -impl MethodMatcher { - pub fn new(method: Method, response: Result) -> Self { - Self { method, response } - } -} - -impl RequestMatcher for MethodMatcher { - fn matches(&self, request: &str) -> bool { - let request_json = serde_json::from_str::(request).unwrap(); - return self.method.to_string() == request_json.get("method").unwrap().as_str().unwrap(); - } - - fn response(&self) -> Result { - self.response.clone() - } -} diff --git a/rpc/src/client/test.rs b/rpc/src/client/test_support.rs similarity index 100% rename from rpc/src/client/test.rs rename to rpc/src/client/test_support.rs diff --git a/rpc/src/client/test_support/matching_transport.rs b/rpc/src/client/test_support/matching_transport.rs new file mode 100644 index 000000000..031421d01 --- /dev/null +++ b/rpc/src/client/test_support/matching_transport.rs @@ -0,0 +1,96 @@ +use crate::client::test_support::Fixture; +use crate::client::transport::{ClosableTransport, Transport}; +use crate::{Error, Method, Request, Response}; +use async_trait::async_trait; + +/// A rudimentary fixture-based transport. +/// +/// Fixtures, if read from the file system, are lazily evaluated. +#[derive(Debug)] +pub struct RequestMatchingTransport { + matchers: Vec, +} + +#[async_trait] +impl Transport for RequestMatchingTransport { + // This transport does not facilitate any subscription mechanism. + type SubscriptionTransport = (); + + async fn request(&self, request: R) -> Result + where + R: Request, + { + for matcher in &self.matchers { + if matcher.matches(&request) { + let response_json = matcher.response()?.read().await; + return R::Response::from_string(response_json); + } + } + Err(Error::internal_error(format!( + "no matcher for request: {:?}", + request + ))) + } + + async fn subscription_transport(&self) -> Result { + unimplemented!() + } +} + +#[async_trait] +impl ClosableTransport for RequestMatchingTransport { + async fn close(self) -> Result<(), Error> { + Ok(()) + } +} + +impl RequestMatchingTransport { + pub fn new(matcher: M) -> Self { + Self { + matchers: vec![matcher], + } + } + + pub fn push(mut self, matcher: M) -> Self { + self.matchers.push(matcher); + self + } +} + +/// Implement this trait to facilitate different kinds of request matching. +pub trait RequestMatcher: Send + Sync + std::fmt::Debug { + /// Does the given request match? + fn matches(&self, request: &R) -> bool + where + R: Request; + + /// The response we need to return if the request matches. + fn response(&self) -> Result; +} + +/// A simple matcher that just returns a specific response every time it gets +/// a request of a particular request method. +#[derive(Debug)] +pub struct MethodMatcher { + method: Method, + response: Result, +} + +impl MethodMatcher { + pub fn new(method: Method, response: Result) -> Self { + Self { method, response } + } +} + +impl RequestMatcher for MethodMatcher { + fn matches(&self, request: &R) -> bool + where + R: Request, + { + return self.method == request.method(); + } + + fn response(&self) -> Result { + self.response.clone() + } +} diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index f0b4ba558..afbf0d094 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -1,83 +1,77 @@ //! Transport layer abstraction for the Tendermint RPC client. +use crate::client::subscription::{Subscription, SubscriptionId}; +use crate::endpoint::{subscribe, unsubscribe}; +use crate::event::Event; +use crate::{Error, Request}; use async_trait::async_trait; -use futures::{ - task::{Context, Poll}, - Stream, -}; -use std::pin::Pin; +use std::fmt::Debug; use tokio::sync::mpsc; -use crate::{event::Event, Error}; - pub mod http_ws; /// Transport layer abstraction for interacting with real or mocked Tendermint /// full nodes. +/// +/// The transport is separated into one part responsible for request/response +/// mechanics and another for event subscription mechanics. This allows for +/// lazy instantiation of subscription mechanism because, depending on the +/// transport layer, they generally require more resources than the +/// request/response mechanism. #[async_trait] -pub trait Transport: std::fmt::Debug { +pub trait Transport: ClosableTransport { + type SubscriptionTransport; + + // TODO(thane): Do we also need this request method to operate on a mutable + // `self`? If the underlying transport were purely WebSockets, + // it would need to be due to the need to mutate channels when + // communicating with the async task. This would then have + // mutability implications for the RPC client. /// Perform a request to the remote endpoint, expecting a response. - async fn request(&self, request: String) -> Result; + async fn request(&self, request: R) -> Result + where + R: Request; - /// Provides access to a stream of incoming events. These would be - /// produced, for example, once at least one subscription has been - /// initiated by the RPC client. - async fn new_event_connection(&self, event_buf_size: usize) -> Result; + /// Produces a transport layer interface specifically for handling event + /// subscriptions. + async fn subscription_transport(&self) -> Result; } -/// The part of the transport layer that exclusively deals with -/// subscribe/unsubscribe requests. +/// A layer intended to be superimposed upon the [`Transport`] layer that only +/// provides subscription mechanics. #[async_trait] -pub trait SubscriptionTransport: std::fmt::Debug + Send { - /// Send a subscription request through the transport layer. - async fn subscribe(&mut self, query: String) -> Result<(), Error>; - - /// Send an unsubscribe request through the transport layer. - async fn unsubscribe(&mut self, query: String) -> Result<(), Error>; - - /// Attempt to gracefully terminate the transport layer. - async fn close(&mut self) -> Result<(), Error>; -} - -/// An `EventConnection` allows us to send subscribe/unsubscribe requests via -/// the transport layer, as well as receive incoming events from subscriptions. -#[derive(Debug)] -pub struct EventConnection { - // The `EventConnection` struct is a workaround for the fact that we need - // to use `Transport` as a trait object, but trait objects are not allowed - // to use generics in their method signatures. - pub transport: Box, - pub event_producer: EventProducer, -} +pub trait SubscriptionTransport: ClosableTransport + Debug + Sized { + /// Send a subscription request via the transport. For this we need the + /// body of the request, as well as a return path (the sender half of an + /// [`mpsc` channel]) for the events generated by this subscription. + /// + /// On success, returns the ID of the subscription that's just been + /// created. + /// + /// We ignore any responses returned by the remote endpoint right now. + /// + /// [`mpsc` channel]: tokio::sync::mpsc + /// + async fn subscribe( + &mut self, + request: subscribe::Request, + event_tx: mpsc::Sender, + ) -> Result; -impl EventConnection { - pub fn new(transport: Box, event_producer: EventProducer) -> Self { - Self { - transport, - event_producer, - } - } - - pub async fn terminate(&mut self) -> Result<(), Error> { - self.transport.close().await - } -} - -#[derive(Debug)] -pub struct EventProducer { - event_rx: mpsc::Receiver, + /// Send an unsubscribe request via the transport. The subscription is + /// terminated and consumed. + /// + /// We ignore any responses returned by the remote endpoint right now. + async fn unsubscribe( + &mut self, + request: unsubscribe::Request, + subscription: Subscription, + ) -> Result<(), Error>; } -impl Stream for EventProducer { - type Item = Event; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.event_rx.poll_recv(cx) - } -} - -impl EventProducer { - pub fn new(event_rx: mpsc::Receiver) -> Self { - Self { event_rx } - } +/// A transport that can be gracefully closed. +#[async_trait] +pub trait ClosableTransport { + /// Attempt to gracefully close the transport, consuming it in the process. + async fn close(self) -> Result<(), Error>; } diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index 0c631e783..6d675b551 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -1,66 +1,57 @@ //! HTTP-based transport for Tendermint RPC Client, with WebSockets-based //! subscription handling mechanism. +use crate::client::subscription::{Subscription, SubscriptionId, SubscriptionRouter}; +use crate::client::transport::{ClosableTransport, SubscriptionTransport, Transport}; +use crate::endpoint::{subscribe, unsubscribe}; +use crate::event::Event; +use crate::response::Response; +use crate::{Error, Method, Request}; use async_trait::async_trait; -use async_tungstenite::{ - tokio::{connect_async, TokioAdapter}, - tungstenite::{ - protocol::{frame::coding::CloseCode, CloseFrame}, - Message, - }, - WebSocketStream, -}; +use async_tungstenite::tokio::{connect_async, TokioAdapter}; +use async_tungstenite::tungstenite::protocol::frame::coding::CloseCode; +use async_tungstenite::tungstenite::protocol::CloseFrame; +use async_tungstenite::tungstenite::Message; +use async_tungstenite::WebSocketStream; use bytes::buf::BufExt; use futures::{stream::StreamExt, SinkExt}; use hyper::header; -use std::{borrow::Cow, io::Read}; +use std::borrow::Cow; use tendermint::net; use tokio::{net::TcpStream, sync::mpsc, task::JoinHandle}; -use crate::{ - client::transport::{EventConnection, EventProducer, SubscriptionTransport, Transport}, - endpoint::{subscribe, unsubscribe}, - event::Event, - response::Response, - Error, Request, -}; - // We anticipate that this will be a relatively low-traffic command signaling // mechanism (these commands are limited to subscribe/unsubscribe requests, // which we assume won't occur very frequently). -const DEFAULT_WEBSOCKET_CMD_BUF_SIZE: usize = 50; +const DEFAULT_WEBSOCKET_CMD_BUF_SIZE: usize = 20; -/// An HTTP-based transport layer for the Tendermint RPC Client. Subscriptions -/// are managed via a WebSockets connection which is maintained separately to -/// the HTTP request mechanisms. +/// An HTTP-based transport layer for the Tendermint RPC Client. #[derive(Debug)] -pub struct HttpWsTransport { +pub struct HttpTransport { host: String, port: u16, } -#[derive(Debug)] -struct WsSubscriptionTransport { - driver_hdl: JoinHandle>, - cmd_tx: mpsc::Sender, -} +#[async_trait] +impl Transport for HttpTransport { + type SubscriptionTransport = WebSocketSubscriptionTransport; -#[derive(Debug)] -struct WsDriver { - stream: WebSocketStream>, - event_tx: mpsc::Sender, - cmd_rx: mpsc::Receiver, -} + async fn request(&self, request: R) -> Result + where + R: Request, + { + // TODO(thane): Find a way to enforce this at compile time. + match request.method() { + Method::Subscribe | Method::Unsubscribe => return Err( + Error::internal_error( + "HttpTransport does not support subscribe/unsubscribe methods - rather use WebSocketSubscriptionTransport" + ) + ), + _ => (), + } -enum WsCmd { - Subscribe { query: String }, - Unsubscribe { query: String }, - Close, -} + let request_body = request.into_json(); -#[async_trait] -impl Transport for HttpWsTransport { - async fn request(&self, request_body: String) -> Result { let mut request = hyper::Request::builder() .method("POST") .uri(&format!("http://{}:{}/", self.host, self.port)) @@ -79,28 +70,24 @@ impl Transport for HttpWsTransport { 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?; - let mut response_string = String::new(); - let _ = response_body - .reader() - .read_to_string(&mut response_string) - .map_err(|e| Error::internal_error(format!("failed to read response body: {}", e))); - Ok(response_string) + R::Response::from_reader(response_body.reader()) } - /// Initiates a new WebSocket connection to the remote endpoint. - async fn new_event_connection(&self, event_buf_size: usize) -> Result { - let (transport, event_producer) = WsSubscriptionTransport::connect( - &format!("ws://{}:{}/websocket", self.host, self.port), - event_buf_size, - DEFAULT_WEBSOCKET_CMD_BUF_SIZE, - ) - .await?; - Ok(EventConnection::new(Box::new(transport), event_producer)) + async fn subscription_transport(&self) -> Result { + WebSocketSubscriptionTransport::new(&format!("ws://{}:{}/websocket", self.host, self.port)) + .await + } +} + +#[async_trait] +impl ClosableTransport for HttpTransport { + async fn close(self) -> Result<(), Error> { + Ok(()) } } -impl HttpWsTransport { - /// Create a new HTTP/WebSockets transport layer. +impl HttpTransport { + /// Create a new HTTP transport layer. pub fn new(address: net::Address) -> Result { let (host, port) = match address { net::Address::Tcp { host, port, .. } => (host, port), @@ -111,73 +98,110 @@ impl HttpWsTransport { ))) } }; - Ok(HttpWsTransport { host, port }) + Ok(HttpTransport { host, port }) } } -#[async_trait] -impl SubscriptionTransport for WsSubscriptionTransport { - async fn subscribe(&mut self, query: String) -> Result<(), Error> { - self.cmd_tx - .send(WsCmd::Subscribe { query }) - .await - .map_err(|e| { - Error::internal_error(format!( - "failed to transmit subscription command to async WebSocket driver: {}", - e - )) - }) - } +/// A WebSocket-based transport for interacting with the Tendermint RPC +/// subscriptions interface. +#[derive(Debug)] +pub struct WebSocketSubscriptionTransport { + driver_hdl: JoinHandle>, + cmd_tx: mpsc::Sender, + next_subs_id: SubscriptionId, +} - async fn unsubscribe(&mut self, query: String) -> Result<(), Error> { - self.cmd_tx - .send(WsCmd::Unsubscribe { query }) - .await - .map_err(|e| { - Error::internal_error(format!( - "failed to transmit unsubscribe command to async WebSocket driver: {}", - e - )) - }) +impl WebSocketSubscriptionTransport { + async fn new(url: &str) -> Result { + let (stream, _response) = connect_async(url).await?; + let (cmd_tx, cmd_rx) = mpsc::channel(DEFAULT_WEBSOCKET_CMD_BUF_SIZE); + let driver = WebSocketSubscriptionDriver::new(stream, cmd_rx); + let driver_hdl = tokio::spawn(async move { driver.run().await }); + Ok(Self { + driver_hdl, + cmd_tx, + next_subs_id: SubscriptionId::from(0), + }) } - async fn close(&mut self) -> Result<(), Error> { - self.cmd_tx.send(WsCmd::Close).await.map_err(|e| { + async fn send_cmd(&mut self, cmd: WebSocketDriverCmd) -> Result<(), Error> { + self.cmd_tx.send(cmd).await.map_err(|e| { Error::internal_error(format!( - "failed to send termination command to async task: {}", + "failed to transmit command to async WebSocket driver: {}", e )) }) + } - // TODO: Find a way to wait for the driver to terminate. + fn next_subscription_id(&mut self) -> SubscriptionId { + let res = self.next_subs_id.clone(); + self.next_subs_id = self.next_subs_id.next(); + res } } -impl WsSubscriptionTransport { - async fn connect( - url: &str, - event_buf_size: usize, - cmd_buf_size: usize, - ) -> Result<(WsSubscriptionTransport, EventProducer), Error> { - let (stream, _response) = connect_async(url).await?; - let (event_tx, event_rx) = mpsc::channel(event_buf_size); - let (cmd_tx, cmd_rx) = mpsc::channel(cmd_buf_size); - let driver = WsDriver { - stream, +#[async_trait] +impl SubscriptionTransport for WebSocketSubscriptionTransport { + async fn subscribe( + &mut self, + request: subscribe::Request, + event_tx: mpsc::Sender, + ) -> Result { + let id = self.next_subscription_id(); + self.send_cmd(WebSocketDriverCmd::Subscribe { + request, + id: id.clone(), event_tx, - cmd_rx, - }; - let driver_hdl = tokio::spawn(async move { driver.run().await }); - Ok(( - WsSubscriptionTransport { driver_hdl, cmd_tx }, - EventProducer::new(event_rx), - )) + }) + .await?; + Ok(id) } + + async fn unsubscribe( + &mut self, + request: unsubscribe::Request, + subscription: Subscription, + ) -> Result<(), Error> { + self.send_cmd(WebSocketDriverCmd::Unsubscribe { + request, + subscription, + }) + .await + } +} + +#[async_trait] +impl ClosableTransport for WebSocketSubscriptionTransport { + async fn close(mut self) -> Result<(), Error> { + self.send_cmd(WebSocketDriverCmd::Close).await?; + self.driver_hdl.await.map_err(|e| { + Error::internal_error(format!("failed to join async WebSocket driver task: {}", e)) + })? + } +} + +#[derive(Debug)] +struct WebSocketSubscriptionDriver { + stream: WebSocketStream>, + router: SubscriptionRouter, + cmd_rx: mpsc::Receiver, } -impl WsDriver { +impl WebSocketSubscriptionDriver { + fn new( + stream: WebSocketStream>, + cmd_rx: mpsc::Receiver, + ) -> Self { + Self { + stream, + router: SubscriptionRouter::new(), + cmd_rx, + } + } + async fn run(mut self) -> Result<(), Error> { - // TODO: Should this loop initiate a keepalive (ping) to the server on a regular basis? + // TODO(thane): Should this loop initiate a keepalive (ping) to the + // server on a regular basis? loop { tokio::select! { Some(res) = self.stream.next() => match res { @@ -189,32 +213,41 @@ impl WsDriver { ), }, Some(cmd) = self.cmd_rx.next() => match cmd { - WsCmd::Subscribe { query } => self.subscribe(query).await?, - WsCmd::Unsubscribe { query } => self.unsubscribe(query).await?, - WsCmd::Close => return self.close().await, + WebSocketDriverCmd::Subscribe { request, id, event_tx } => self.subscribe(request, id, event_tx).await?, + WebSocketDriverCmd::Unsubscribe { request, subscription } => self.unsubscribe(request, subscription).await?, + WebSocketDriverCmd::Close => return self.close().await, } } } } - async fn subscribe(&mut self, query: String) -> Result<(), Error> { - let req = subscribe::Request::new(query); - self.stream - .send(Message::Text(req.into_json())) - .await - .map_err(|e| { - Error::websocket_error(format!("failed to write to WebSocket connection: {}", e)) - }) + async fn send(&mut self, msg: Message) -> Result<(), Error> { + self.stream.send(msg).await.map_err(|e| { + Error::websocket_error(format!("failed to write to WebSocket connection: {}", e)) + }) } - async fn unsubscribe(&mut self, query: String) -> Result<(), Error> { - let req = unsubscribe::Request::new(query); - self.stream - .send(Message::Text(req.into_json())) - .await - .map_err(|e| { - Error::websocket_error(format!("failed to write to WebSocket connection: {}", e)) - }) + async fn subscribe( + &mut self, + request: subscribe::Request, + id: SubscriptionId, + event_tx: mpsc::Sender, + ) -> Result<(), Error> { + self.send(Message::Text(request.clone().into_json())) + .await?; + self.router.add(id, request.query, event_tx); + Ok(()) + } + + async fn unsubscribe( + &mut self, + request: unsubscribe::Request, + subs: Subscription, + ) -> Result<(), Error> { + self.send(Message::Text(request.clone().into_json())) + .await?; + self.router.remove(subs); + Ok(()) } async fn handle_incoming_msg(&mut self, msg: Message) -> Result<(), Error> { @@ -228,45 +261,29 @@ impl WsDriver { async fn handle_text_msg(&mut self, msg: String) -> Result<(), Error> { match Event::from_string(msg) { - Ok(ev) => self.handle_event(ev).await, - // TODO: Should we just ignore messages we can't deserialize? - // There are a number of possible messages we may receive - // from the WebSocket endpoint that we'll end up ignoring - // anyways (like responses for subscribe/unsubscribe - // requests). + Ok(ev) => { + self.router.publish(ev).await; + Ok(()) + } + // TODO(thane): Should we just ignore messages we can't + // deserialize? There are a number of possible + // messages we may receive from the WebSocket endpoint + // that we'll end up ignoring anyways (like responses + // for subscribe/unsubscribe requests). Err(_) => Ok(()), } } - async fn handle_event(&mut self, ev: Event) -> Result<(), Error> { - self.event_tx.send(ev).await.map_err(|e| { - Error::internal_error(format!( - "failed to publish incoming event to event producer: {}", - e - )) - }) - } - async fn pong(&mut self, v: Vec) -> Result<(), Error> { - self.stream.send(Message::Pong(v)).await.map_err(|e| { - Error::websocket_error(format!("failed to write WebSocket pong message: {}", e)) - }) + self.send(Message::Pong(v)).await } - async fn close(&mut self) -> Result<(), Error> { - let _ = self - .stream - .send(Message::Close(Some(CloseFrame { - code: CloseCode::Normal, - reason: Cow::from("client closed WebSocket connection"), - }))) - .await - .map_err(|e| { - Error::websocket_error(format!( - "failed to cleanly terminate WebSocket connection: {}", - e - )) - })?; + async fn close(mut self) -> Result<(), Error> { + self.send(Message::Close(Some(CloseFrame { + code: CloseCode::Normal, + reason: Cow::from("client closed WebSocket connection"), + }))) + .await?; while let Some(res) = self.stream.next().await { if let Err(_) = res { @@ -276,3 +293,17 @@ impl WsDriver { Ok(()) } } + +#[derive(Debug)] +enum WebSocketDriverCmd { + Subscribe { + request: subscribe::Request, + id: SubscriptionId, + event_tx: mpsc::Sender, + }, + Unsubscribe { + request: unsubscribe::Request, + subscription: Subscription, + }, + Close, +} diff --git a/rpc/src/endpoint/subscribe.rs b/rpc/src/endpoint/subscribe.rs index 11163e8bf..301233939 100644 --- a/rpc/src/endpoint/subscribe.rs +++ b/rpc/src/endpoint/subscribe.rs @@ -6,7 +6,7 @@ use std::io::Read; /// Subscription request for events. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Request { - query: String, + pub query: String, } impl Request { diff --git a/rpc/src/endpoint/unsubscribe.rs b/rpc/src/endpoint/unsubscribe.rs index eb1a417e8..83afbfc39 100644 --- a/rpc/src/endpoint/unsubscribe.rs +++ b/rpc/src/endpoint/unsubscribe.rs @@ -3,15 +3,14 @@ use serde::{Deserialize, Serialize}; use std::io::Read; -/// Subscribe request for events on websocket +/// Subscription request for events. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Request { - query: String, + pub query: String, } impl Request { - /// Create a new unsubscribe request with the query from which to - /// unsubscribe. + /// Unsubscribe from all events generated by the given query. pub fn new(query: String) -> Self { Self { query } } @@ -21,7 +20,7 @@ impl crate::Request for Request { type Response = Response; fn method(&self) -> crate::Method { - crate::Method::Unsubscribe + crate::Method::Subscribe } } @@ -29,15 +28,16 @@ impl crate::Request for Request { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Response {} -/// Unsubscribe does not have a meaningful 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 + /// 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 + /// 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 {}) } diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 541f3da08..fde89fd52 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -3,7 +3,7 @@ #[cfg(feature = "client")] mod client; #[cfg(feature = "client")] -pub use client::{event_listener, Client}; +pub use client::{event_listener, new_subscription_client, transport, Client, SubscriptionClient}; pub mod endpoint; pub mod error; diff --git a/rpc/src/request.rs b/rpc/src/request.rs index e74b75068..01ce5934a 100644 --- a/rpc/src/request.rs +++ b/rpc/src/request.rs @@ -5,7 +5,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::fmt::Debug; /// JSONRPC requests -pub trait Request: Debug + DeserializeOwned + Serialize + Sized { +pub trait Request: Debug + DeserializeOwned + Serialize + Sized + Send { /// Response type for this command type Response: super::response::Response; diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index 36d42aa43..2e6c536f9 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -11,15 +11,16 @@ mod rpc { use std::cmp::min; - use tendermint_rpc::{event_listener, Client}; + use tendermint_rpc::transport::http_ws::HttpTransport; + use tendermint_rpc::{event_listener, new_subscription_client, Client}; use futures::StreamExt; use tendermint::abci::Code; use tendermint::abci::Log; /// Get the address of the local node - pub fn localhost_rpc_client() -> Client { - Client::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap() + pub fn localhost_rpc_client() -> Client { + Client::new(HttpTransport::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap()) } /// `/health` endpoint @@ -150,23 +151,24 @@ mod rpc { #[ignore] async fn subscription_interface() { let client = localhost_rpc_client(); - let mut subs_mgr = client.new_subscription_manager(10).await.unwrap(); - let mut subs = subs_mgr - .subscribe("tm.event='NewBlock'".to_string(), 10) + let mut subs_client = new_subscription_client(&client).await.unwrap(); + let mut subs = subs_client + .subscribe("tm.event='NewBlock'".to_string()) .await .unwrap(); - let mut ev_count: usize = 0; + let mut ev_count = 10_i32; - dbg!("Attempting to grab 10 new blocks"); + dbg!("Attempting to grab {} new blocks", ev_count); while let Some(ev) = subs.next().await { dbg!("Got event: {:?}", ev); - ev_count += 1; - if ev_count > 10 { + ev_count -= 1; + if ev_count < 0 { break; } } - subs_mgr.terminate().await.unwrap(); + subs_client.close().await.unwrap(); + client.close().await.unwrap(); } #[tokio::test] From da0247a12aa38f5f43fbe06dfed3477165e32564 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 20:44:54 -0400 Subject: [PATCH 19/60] Add note for myself on TODO Signed-off-by: Thane Thomson --- rpc/src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 3600139ea..24f58b4db 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -258,7 +258,7 @@ mod test { }; use crate::Method; - // TODO: Read from a fixture in the crate. + // TODO(thane): Read from a fixture in the crate. const ABCI_INFO_RESPONSE: &str = r#"{ "jsonrpc": "2.0", "id": "", @@ -272,7 +272,7 @@ mod test { } "#; - // TODO: Read from a fixture in the crate. + // TODO(thane): Read from a fixture in the crate. const BLOCK_RESPONSE: &str = r#"{ "jsonrpc": "2.0", "id": "", From cafa8d797a2ee1270897393e2f7dd2dfca5636d4 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 20:46:45 -0400 Subject: [PATCH 20/60] Move mod declarations before use statements Signed-off-by: Thane Thomson --- rpc/src/client.rs | 13 ++++++------- rpc/src/client/test_support.rs | 4 ++-- rpc/src/client/transport.rs | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 24f58b4db..88a543995 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -1,5 +1,11 @@ //! Tendermint RPC client +pub mod event_listener; +pub mod subscription; +#[cfg(test)] +pub mod test_support; +pub mod transport; + use crate::client::subscription::Subscription; use crate::client::transport::{SubscriptionTransport, Transport}; use crate::endpoint::*; @@ -10,13 +16,6 @@ use tendermint::evidence::Evidence; use tendermint::Genesis; use tokio::sync::mpsc; -pub mod event_listener; -pub mod subscription; -pub mod transport; - -#[cfg(test)] -pub mod test_support; - /// The default number of events we buffer in a [`Subscription`] if you do not /// specify the buffer size when creating it. pub const DEFAULT_SUBSCRIPTION_BUF_SIZE: usize = 100; diff --git a/rpc/src/client/test_support.rs b/rpc/src/client/test_support.rs index dbb0ca7a8..47455321f 100644 --- a/rpc/src/client/test_support.rs +++ b/rpc/src/client/test_support.rs @@ -2,11 +2,11 @@ //! provide some useful abstractions in cases where you may want to use an RPC //! client in your code, but mock its remote endpoint's responses. +pub mod matching_transport; + use std::path::PathBuf; use tokio::fs; -pub mod matching_transport; - /// A fixture that can refer to a file in the filesystem or is a string in its /// own right. #[derive(Debug, Clone)] diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index afbf0d094..54c51fcb7 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -1,5 +1,7 @@ //! Transport layer abstraction for the Tendermint RPC client. +pub mod http_ws; + use crate::client::subscription::{Subscription, SubscriptionId}; use crate::endpoint::{subscribe, unsubscribe}; use crate::event::Event; @@ -8,8 +10,6 @@ use async_trait::async_trait; use std::fmt::Debug; use tokio::sync::mpsc; -pub mod http_ws; - /// Transport layer abstraction for interacting with real or mocked Tendermint /// full nodes. /// From 9ad109b40d37e10e55987dcf26f94ff53ac0d4af Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 12 Aug 2020 20:55:44 -0400 Subject: [PATCH 21/60] Fix clippy warnings Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 2 +- rpc/src/client/transport/http_ws.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 65eb2a18a..3d574339f 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -82,7 +82,7 @@ impl SubscriptionRouter { // TODO(thane): Right now we automatically remove any disconnected // or full channels. We must handle full channels // differently to disconnected ones. - if let Err(_) = event_tx.send(ev.clone()).await { + if event_tx.send(ev.clone()).await.is_err() { disconnected.push(id.clone()); } } diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index 6d675b551..1888f6087 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -286,7 +286,7 @@ impl WebSocketSubscriptionDriver { .await?; while let Some(res) = self.stream.next().await { - if let Err(_) = res { + if res.is_err() { return Ok(()); } } From ecb099f41cee9253a58b115d0ddb1d3a29bd415c Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 14 Aug 2020 11:35:26 -0400 Subject: [PATCH 22/60] Refactor interface entirely After much consideration, I decided to do away with the `Transport` and related traits entirely. It makes more sense, and results in better ability to test, if we abstract the `Client` interface instead. This also allows for greater flexibility when implementing different kinds of transports, while still allowing for lazy/optional instantiation of the subscription mechanism. Additional integration tests are included here, as well as additional documentation. Signed-off-by: Thane Thomson --- light-client/Cargo.toml | 2 +- light-client/src/components/io.rs | 12 +- light-client/src/evidence.rs | 12 +- rpc/Cargo.toml | 1 + rpc/src/client.rs | 337 +++++++---------------- rpc/src/client/subscription.rs | 26 +- rpc/src/client/test_support.rs | 43 --- rpc/src/client/transport.rs | 81 +----- rpc/src/client/transport/http_ws.rs | 325 ++++++++++++---------- rpc/src/lib.rs | 8 +- rpc/src/result.rs | 4 + rpc/tests/client.rs | 258 +++++++++++++++++ rpc/tests/endpoint.rs | 292 ++++++++++++++++++++ rpc/tests/integration.rs | 301 +------------------- rpc/tests/support/event_new_block_1.json | 73 +++++ rpc/tests/support/event_new_block_2.json | 73 +++++ rpc/tests/support/event_new_block_3.json | 73 +++++ tendermint/Cargo.toml | 2 +- tendermint/tests/integration.rs | 47 +--- 19 files changed, 1108 insertions(+), 862 deletions(-) delete mode 100644 rpc/src/client/test_support.rs create mode 100644 rpc/src/result.rs create mode 100644 rpc/tests/client.rs create mode 100644 rpc/tests/endpoint.rs create mode 100644 rpc/tests/support/event_new_block_1.json create mode 100644 rpc/tests/support/event_new_block_2.json create mode 100644 rpc/tests/support/event_new_block_3.json diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index f72d09622..5e0d44e47 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -20,7 +20,7 @@ description = """ [dependencies] tendermint = { version = "0.15.0", path = "../tendermint" } -tendermint-rpc = { version = "0.15.0", path = "../rpc", features = ["client"] } +tendermint-rpc = { version = "0.15.0", path = "../rpc", features = ["client", "http_ws"] } anomaly = { version = "0.2.0", features = ["serializer"] } contracts = "0.4.0" diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index e0c95e52d..f61e09eb1 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -12,6 +12,7 @@ use tendermint::{ }; use tendermint_rpc as rpc; +use tendermint_rpc::MinimalClient; use crate::{ bail, @@ -166,16 +167,11 @@ impl ProdIo { } // FIXME: Cannot enable precondition because of "autoref lifetime" issue - // TODO(thane): Generalize over client transport (instead of using HttpTransport directly). + // TODO(thane): Generalize over client transport (instead of using HttpClient directly). // #[pre(self.peer_map.contains_key(&peer))] - fn rpc_client_for( - &self, - peer: PeerId, - ) -> Result, IoError> { + fn rpc_client_for(&self, peer: PeerId) -> Result { let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - Ok(rpc::Client::new( - rpc::transport::http_ws::HttpTransport::new(peer_addr).map_err(IoError::from)?, - )) + Ok(rpc::HttpClient::new(peer_addr).map_err(IoError::from)?) } } diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index 8f9fbaa0f..342aea9f6 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -4,6 +4,7 @@ use crate::{components::io::IoError, types::PeerId}; use tendermint::abci::transaction::Hash; use tendermint_rpc as rpc; +use tendermint_rpc::MinimalClient; use contracts::{contract_trait, pre}; use std::collections::HashMap; @@ -47,16 +48,11 @@ impl ProdEvidenceReporter { } // FIXME: Cannot enable precondition because of "autoref lifetime" issue - // TODO(thane): Generalize over client transport (instead of using HttpTransport directly). + // TODO(thane): Generalize over client transport (instead of using HttpClient directly). // #[pre(self.peer_map.contains_key(&peer))] - fn rpc_client_for( - &self, - peer: PeerId, - ) -> Result, IoError> { + fn rpc_client_for(&self, peer: PeerId) -> Result { let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - Ok(rpc::Client::new( - rpc::transport::http_ws::HttpTransport::new(peer_addr).map_err(IoError::from)?, - )) + Ok(rpc::HttpClient::new(peer_addr).map_err(IoError::from)?) } } diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index c82c12078..18d7ed448 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -26,6 +26,7 @@ all-features = true [features] default = [] client = [ "async-trait", "async-tungstenite", "futures", "http", "hyper", "tokio" ] +http_ws = [ "async-trait", "async-tungstenite", "futures", "http", "hyper", "tokio" ] secp256k1 = ["tendermint/secp256k1"] [dependencies] diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 88a543995..6228c837f 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -1,53 +1,67 @@ -//! Tendermint RPC client +//! Tendermint RPC client. +//! +//! The RPC client comes in two flavors: a [`MinimalClient`] and a +//! [`FullClient`]. A `MinimalClient` implementation provides access to all +//! RPC endpoints with the exception of the [`Event`] subscription ones, +//! whereas a `FullClient` implementation provides access to all RPC +//! functionality. The reason for this distinction is because `Event` +//! subscription usually requires more resources to manage, and may not be +//! necessary for all applications making use of the Tendermint RPC. +//! +//! This is only available when specifying the `client` feature flag. +//! Transport-specific client support is provided by way of additional feature +//! flags (where right now we only have one transport, but intend on providing +//! more in future): +//! +//! * `http_ws`: Provides an HTTP interface for request/response interactions +//! (see [`HttpClient`]), and a WebSocket-based interface for `Event` +//! subscription (see [`HttpWebSocketClient`]). +//! +//! [`Event`]: event::Event + +mod subscription; +pub use subscription::{Subscription, SubscriptionId, SubscriptionRouter}; + +mod transport; +#[cfg(feature = "http_ws")] +pub use transport::{HttpClient, HttpWebSocketClient}; -pub mod event_listener; -pub mod subscription; -#[cfg(test)] -pub mod test_support; -pub mod transport; - -use crate::client::subscription::Subscription; -use crate::client::transport::{SubscriptionTransport, Transport}; use crate::endpoint::*; -use crate::{Error, Request}; +use crate::{Request, Result}; +use async_trait::async_trait; use tendermint::abci::{self, Transaction}; use tendermint::block::Height; use tendermint::evidence::Evidence; use tendermint::Genesis; -use tokio::sync::mpsc; /// The default number of events we buffer in a [`Subscription`] if you do not /// specify the buffer size when creating it. pub const DEFAULT_SUBSCRIPTION_BUF_SIZE: usize = 100; -/// The base Tendermint RPC client, which is responsible for handling most -/// requests with the exception of subscription mechanics. Once you have an RPC -/// client, you can create a [`SubscriptionClient`] using -/// [`new_subscription_client`]. -#[derive(Debug)] -pub struct Client { - transport: T, -} - -impl Client { - /// Create a new Tendermint RPC client using the given transport layer. - pub fn new(transport: T) -> Self { - Self { transport } - } - +/// A `MinimalClient` provides lightweight access to the Tendermint RPC. It +/// gives access to all endpoints with the exception of the event +/// subscription-related ones. +/// +/// To access event subscription capabilities, use a client that implements the +/// [`FullClient`] trait. +#[async_trait] +pub trait MinimalClient { /// `/abci_info`: get information about the ABCI application. - pub async fn abci_info(&self) -> Result { + async fn abci_info(&self) -> Result { Ok(self.perform(abci_info::Request).await?.response) } /// `/abci_query`: query the ABCI application - pub async fn abci_query( + async fn abci_query( &self, path: Option, - data: impl Into>, + data: V, height: Option, prove: bool, - ) -> Result { + ) -> Result + where + V: Into> + Send, + { Ok(self .perform(abci_query::Request::new(path, data, height, prove)) .await? @@ -55,26 +69,29 @@ impl Client { } /// `/block`: get block at a given height. - pub async fn block(&self, height: impl Into) -> Result { + async fn block(&self, height: H) -> Result + where + H: Into + Send, + { self.perform(block::Request::new(height.into())).await } /// `/block`: get the latest block. - pub async fn latest_block(&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. - pub async fn block_results(&self, height: H) -> Result + async fn block_results(&self, height: H) -> Result where - H: Into, + H: Into + Send, { self.perform(block_results::Request::new(height.into())) .await } /// `/block_results`: get ABCI results for the latest block. - pub async fn latest_block_results(&self) -> Result { + async fn latest_block_results(&self) -> Result { self.perform(block_results::Request::default()).await } @@ -83,275 +100,129 @@ impl Client { /// Block headers are returned in descending order (highest first). /// /// Returns at most 20 items. - pub async fn blockchain( - &self, - min: impl Into, - max: impl Into, - ) -> Result { + async fn blockchain(&self, min: H, max: H) -> Result + where + H: Into + Send, + { // TODO(tarcieri): return errors for invalid params before making request? self.perform(blockchain::Request::new(min.into(), max.into())) .await } /// `/broadcast_tx_async`: broadcast a transaction, returning immediately. - pub async fn broadcast_tx_async( - &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`. - pub async fn broadcast_tx_sync( - &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`. - pub async fn broadcast_tx_commit( - &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. - pub async fn commit(&self, height: impl Into) -> Result { + async fn commit(&self, height: H) -> Result + where + H: Into + Send, + { self.perform(commit::Request::new(height.into())).await } /// `/validators`: get validators a given height. - pub async fn validators(&self, height: H) -> Result + async fn validators(&self, height: H) -> Result where - H: Into, + H: Into + Send, { self.perform(validators::Request::new(height.into())).await } /// `/commit`: get the latest block commit - pub async fn latest_commit(&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. - pub async fn health(&self) -> Result<(), Error> { + async fn health(&self) -> Result<()> { self.perform(health::Request).await?; Ok(()) } /// `/genesis`: get genesis file. - pub async fn genesis(&self) -> Result { + async fn genesis(&self) -> Result { Ok(self.perform(genesis::Request).await?.genesis) } /// `/net_info`: obtain information about P2P and other network connections. - pub async fn net_info(&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. - pub async fn status(&self) -> Result { + async fn status(&self) -> Result { self.perform(status::Request).await } /// `/broadcast_evidence`: broadcast an evidence. - pub async fn broadcast_evidence(&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 - pub async fn perform(&self, request: R) -> Result + async fn perform(&self, request: R) -> Result where - R: Request, - { - self.transport.request(request).await - } + R: Request; /// Gracefully terminate the underlying connection (if relevant - depends /// on the underlying transport). - pub async fn close(self) -> Result<(), Error> { - self.transport.close().await - } -} - -/// A client solely dedicated to facilitating subscriptions to [`Event`]s. -/// -/// [`Event`]: crate::event::Event -#[derive(Debug)] -pub struct SubscriptionClient { - transport: T, -} - -/// Create a new subscription client derived from the given RPC client. -pub async fn new_subscription_client( - client: &Client, -) -> Result, Error> -where - T: Transport, - ::SubscriptionTransport: SubscriptionTransport, -{ - Ok(SubscriptionClient::new( - client.transport.subscription_transport().await?, - )) + async fn close(self) -> Result<()>; } -impl SubscriptionClient { - fn new(transport: T) -> Self { - Self { transport } - } - - /// Subscribe to events generated by the given query. +/// A `FullClient` is one that augments a [`MinimalClient`] functionality with +/// subscription capabilities. +#[async_trait] +pub trait FullClient: MinimalClient { + /// `/subscribe`: subscribe to receive events produced by the given query. + /// + /// Allows for specification of the `buf_size` parameter, which determines + /// how many events can be buffered in the resulting [`Subscription`]. The + /// size of this buffer must be tuned according to how quickly your + /// application can process the incoming events from this particular query. + /// The slower your application processes events, the larger this buffer + /// needs to be. + /// + /// [`Subscription`]: client::subscription::Subscription /// - /// The `buf_size` parameter allows for control over how many events get - /// buffered by the returned [`Subscription`]. The faster you can process - /// incoming events, the smaller this buffer size needs to be. - pub async fn subscribe_with_buf_size( + async fn subscribe_with_buf_size( &mut self, query: String, buf_size: usize, - ) -> Result { - let (event_tx, event_rx) = mpsc::channel(buf_size); - let id = self - .transport - .subscribe(subscribe::Request::new(query.clone()), event_tx) - .await?; - Ok(Subscription::new(id, query, event_rx)) - } + ) -> Result; - /// Subscribe to events generated by the given query, using the - /// [`DEFAULT_SUBSCRIPTION_BUF_SIZE`]. - pub async fn subscribe(&mut self, query: String) -> Result { + /// `/subscribe`: subscribe to receive events produced by the given query. + /// + /// Uses [`DEFAULT_SUBSCRIPTION_BUF_SIZE`] as the buffer size for the + /// returned [`Subscription`]. + /// + /// [`Subscription`]: client::subscription::Subscription + /// + async fn subscribe(&mut self, query: String) -> Result { self.subscribe_with_buf_size(query, DEFAULT_SUBSCRIPTION_BUF_SIZE) .await } - /// Terminate the given subscription and consume it. - pub async fn unsubscribe(&mut self, subscription: Subscription) -> Result<(), Error> { - self.transport - .unsubscribe( - unsubscribe::Request::new(subscription.query.clone()), - subscription, - ) - .await - } - - /// Gracefully terminate the underlying connection (if relevant - depends - /// on the underlying transport). - pub async fn close(self) -> Result<(), Error> { - self.transport.close().await - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::client::test_support::matching_transport::{ - MethodMatcher, RequestMatchingTransport, - }; - use crate::Method; - - // TODO(thane): Read from a fixture in the crate. - const ABCI_INFO_RESPONSE: &str = r#"{ - "jsonrpc": "2.0", - "id": "", - "result": { - "response": { - "data": "GaiaApp", - "last_block_height": "488120", - "last_block_app_hash": "2LnCw0fN+Zq/gs5SOuya/GRHUmtWftAqAkTUuoxl4g4=" - } - } -} -"#; - - // TODO(thane): Read from a fixture in the crate. - const BLOCK_RESPONSE: &str = r#"{ - "jsonrpc": "2.0", - "id": "", - "result": { - "block_id": { - "hash": "4FFD15F274758E474898498A191EB8CA6FC6C466576255DA132908A12AC1674C", - "parts": { - "total": "1", - "hash": "BBA710736635FA20CDB4F48732563869E90871D31FE9E7DE3D900CD4334D8775" - } - }, - "block": { - "header": { - "version": { - "block": "10", - "app": "1" - }, - "chain_id": "cosmoshub-2", - "height": "10", - "time": "2020-03-15T16:57:08.151Z", - "last_block_id": { - "hash": "760E050B2404A4BC661635CA552FF45876BCD927C367ADF88961E389C01D32FF", - "parts": { - "total": "1", - "hash": "485070D01F9543827B3F9BAF11BDCFFBFD2BDED0B63D7192FA55649B94A1D5DE" - } - }, - "last_commit_hash": "594F029060D5FAE6DDF82C7DC4612055EC7F941DFED34D43B2754008DC3BBC77", - "data_hash": "", - "validators_hash": "3C0A744897A1E0DBF1DEDE1AF339D65EDDCF10E6338504368B20C508D6D578DC", - "next_validators_hash": "3C0A744897A1E0DBF1DEDE1AF339D65EDDCF10E6338504368B20C508D6D578DC", - "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F", - "app_hash": "0000000000000000", - "last_results_hash": "", - "evidence_hash": "", - "proposer_address": "12CC3970B3AE9F19A4B1D98BE1799F2CB923E0A3" - }, - "data": { - "txs": null - }, - "evidence": { - "evidence": null - }, - "last_commit": { - "height": "9", - "round": "0", - "block_id": { - "hash": "760E050B2404A4BC661635CA552FF45876BCD927C367ADF88961E389C01D32FF", - "parts": { - "total": "1", - "hash": "485070D01F9543827B3F9BAF11BDCFFBFD2BDED0B63D7192FA55649B94A1D5DE" - } - }, - "signatures": [ - { - "block_id_flag": 2, - "validator_address": "12CC3970B3AE9F19A4B1D98BE1799F2CB923E0A3", - "timestamp": "2020-03-15T16:57:08.151Z", - "signature": "GRBX/UNaf19vs5byJfAuXk2FQ05soOHmaMFCbrNBhHdNZtFKHp6J9eFwZrrG+YCxKMdqPn2tQWAes6X8kpd1DA==" - } - ] - } - } - } -}"#; - - #[tokio::test] - async fn mocked_client_transport() { - let transport = RequestMatchingTransport::new(MethodMatcher::new( - Method::AbciInfo, - Ok(ABCI_INFO_RESPONSE.into()), - )) - .push(MethodMatcher::new(Method::Block, Ok(BLOCK_RESPONSE.into()))); - let client = Client::new(transport); - - let abci_info = client.abci_info().await.unwrap(); - assert_eq!("GaiaApp".to_string(), abci_info.data); - - // supplied height is irrelevant when using MethodMatcher - let block = client.block(Height::from(1234)).await.unwrap().block; - assert_eq!(Height::from(10), block.header.height); - } + /// `/unsubscribe`: unsubscribe from receiving events for the given + /// subscription. + /// + /// This terminates the given subscription and consumes it, since it is no + /// longer usable. + async fn unsubscribe(&mut self, subscription: Subscription) -> Result<()>; } diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 3d574339f..52c66f020 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -38,21 +38,19 @@ impl Subscription { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SubscriptionId(usize); -impl From for SubscriptionId { - fn from(u: usize) -> Self { - SubscriptionId(u) - } -} - -impl From for usize { - fn from(subs_id: SubscriptionId) -> Self { - subs_id.0 +impl Default for SubscriptionId { + fn default() -> Self { + Self(0) } } impl SubscriptionId { - pub fn next(&self) -> SubscriptionId { - SubscriptionId(self.0 + 1) + /// Advances this subscription ID to the next logical ID, returning the + /// previous one. + pub fn advance(&mut self) -> SubscriptionId { + let cur_id = self.clone(); + self.0 += 1; + cur_id } } @@ -113,3 +111,9 @@ impl SubscriptionRouter { subs_for_query.remove(&subs.id); } } + +impl Default for SubscriptionRouter { + fn default() -> Self { + Self::new() + } +} diff --git a/rpc/src/client/test_support.rs b/rpc/src/client/test_support.rs deleted file mode 100644 index 47455321f..000000000 --- a/rpc/src/client/test_support.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Testing-related utilities for the Tendermint RPC Client. Here we aim to -//! provide some useful abstractions in cases where you may want to use an RPC -//! client in your code, but mock its remote endpoint's responses. - -pub mod matching_transport; - -use std::path::PathBuf; -use tokio::fs; - -/// A fixture that can refer to a file in the filesystem or is a string in its -/// own right. -#[derive(Debug, Clone)] -pub enum Fixture { - File(PathBuf), - Raw(String), -} - -impl Fixture { - async fn read(&self) -> String { - match self { - Fixture::File(path) => fs::read_to_string(path.as_path()).await.unwrap(), - Fixture::Raw(s) => s.clone(), - } - } -} - -impl Into for String { - fn into(self) -> Fixture { - Fixture::Raw(self) - } -} - -impl Into for &str { - fn into(self) -> Fixture { - Fixture::Raw(self.to_string()) - } -} - -impl Into for PathBuf { - fn into(self) -> Fixture { - Fixture::File(self) - } -} diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index 54c51fcb7..ce5348b06 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -1,77 +1,6 @@ -//! Transport layer abstraction for the Tendermint RPC client. +//! Tendermint RPC client implementations for different transports. -pub mod http_ws; - -use crate::client::subscription::{Subscription, SubscriptionId}; -use crate::endpoint::{subscribe, unsubscribe}; -use crate::event::Event; -use crate::{Error, Request}; -use async_trait::async_trait; -use std::fmt::Debug; -use tokio::sync::mpsc; - -/// Transport layer abstraction for interacting with real or mocked Tendermint -/// full nodes. -/// -/// The transport is separated into one part responsible for request/response -/// mechanics and another for event subscription mechanics. This allows for -/// lazy instantiation of subscription mechanism because, depending on the -/// transport layer, they generally require more resources than the -/// request/response mechanism. -#[async_trait] -pub trait Transport: ClosableTransport { - type SubscriptionTransport; - - // TODO(thane): Do we also need this request method to operate on a mutable - // `self`? If the underlying transport were purely WebSockets, - // it would need to be due to the need to mutate channels when - // communicating with the async task. This would then have - // mutability implications for the RPC client. - /// Perform a request to the remote endpoint, expecting a response. - async fn request(&self, request: R) -> Result - where - R: Request; - - /// Produces a transport layer interface specifically for handling event - /// subscriptions. - async fn subscription_transport(&self) -> Result; -} - -/// A layer intended to be superimposed upon the [`Transport`] layer that only -/// provides subscription mechanics. -#[async_trait] -pub trait SubscriptionTransport: ClosableTransport + Debug + Sized { - /// Send a subscription request via the transport. For this we need the - /// body of the request, as well as a return path (the sender half of an - /// [`mpsc` channel]) for the events generated by this subscription. - /// - /// On success, returns the ID of the subscription that's just been - /// created. - /// - /// We ignore any responses returned by the remote endpoint right now. - /// - /// [`mpsc` channel]: tokio::sync::mpsc - /// - async fn subscribe( - &mut self, - request: subscribe::Request, - event_tx: mpsc::Sender, - ) -> Result; - - /// Send an unsubscribe request via the transport. The subscription is - /// terminated and consumed. - /// - /// We ignore any responses returned by the remote endpoint right now. - async fn unsubscribe( - &mut self, - request: unsubscribe::Request, - subscription: Subscription, - ) -> Result<(), Error>; -} - -/// A transport that can be gracefully closed. -#[async_trait] -pub trait ClosableTransport { - /// Attempt to gracefully close the transport, consuming it in the process. - async fn close(self) -> Result<(), Error>; -} +#[cfg(feature = "http_ws")] +mod http_ws; +#[cfg(feature = "http_ws")] +pub use http_ws::{HttpClient, HttpWebSocketClient}; diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index 1888f6087..d03274bda 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -1,12 +1,12 @@ //! HTTP-based transport for Tendermint RPC Client, with WebSockets-based //! subscription handling mechanism. +//! +//! The `client` and `http_ws` features are required to use this module. -use crate::client::subscription::{Subscription, SubscriptionId, SubscriptionRouter}; -use crate::client::transport::{ClosableTransport, SubscriptionTransport, Transport}; +use crate::client::{Subscription, SubscriptionId, SubscriptionRouter}; use crate::endpoint::{subscribe, unsubscribe}; use crate::event::Event; -use crate::response::Response; -use crate::{Error, Method, Request}; +use crate::{Error, FullClient, MinimalClient, Request, Response, Result}; use async_trait::async_trait; use async_tungstenite::tokio::{connect_async, TokioAdapter}; use async_tungstenite::tungstenite::protocol::frame::coding::CloseCode; @@ -14,169 +14,155 @@ use async_tungstenite::tungstenite::protocol::CloseFrame; use async_tungstenite::tungstenite::Message; use async_tungstenite::WebSocketStream; use bytes::buf::BufExt; -use futures::{stream::StreamExt, SinkExt}; +use futures::{SinkExt, StreamExt}; use hyper::header; use std::borrow::Cow; use tendermint::net; -use tokio::{net::TcpStream, sync::mpsc, task::JoinHandle}; +use tokio::net::TcpStream; +use tokio::sync::{mpsc, oneshot}; +use tokio::task::JoinHandle; // We anticipate that this will be a relatively low-traffic command signaling // mechanism (these commands are limited to subscribe/unsubscribe requests, // which we assume won't occur very frequently). const DEFAULT_WEBSOCKET_CMD_BUF_SIZE: usize = 20; -/// An HTTP-based transport layer for the Tendermint RPC Client. -#[derive(Debug)] -pub struct HttpTransport { +/// An HTTP-based Tendermint RPC client (a [`MinimalClient`] implementation). +#[derive(Debug, Clone)] +pub struct HttpClient { host: String, port: u16, } #[async_trait] -impl Transport for HttpTransport { - type SubscriptionTransport = WebSocketSubscriptionTransport; - - async fn request(&self, request: R) -> Result +impl MinimalClient for HttpClient { + async fn perform(&self, request: R) -> Result where R: Request, { - // TODO(thane): Find a way to enforce this at compile time. - match request.method() { - Method::Subscribe | Method::Unsubscribe => return Err( - Error::internal_error( - "HttpTransport does not support subscribe/unsubscribe methods - rather use WebSocketSubscriptionTransport" - ) - ), - _ => (), - } - - 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()) + http_request(&self.host, self.port, request).await } - async fn subscription_transport(&self) -> Result { - WebSocketSubscriptionTransport::new(&format!("ws://{}:{}/websocket", self.host, self.port)) - .await - } -} - -#[async_trait] -impl ClosableTransport for HttpTransport { - async fn close(self) -> Result<(), Error> { + async fn close(self) -> Result<()> { Ok(()) } } -impl HttpTransport { - /// Create a new HTTP transport layer. - pub fn new(address: net::Address) -> Result { - let (host, port) = match address { - net::Address::Tcp { host, port, .. } => (host, port), - other => { - return Err(Error::invalid_params(&format!( - "invalid RPC address: {:?}", - other - ))) - } - }; - Ok(HttpTransport { host, port }) +impl HttpClient { + /// Create a new HTTP-based Tendermint RPC client. + pub fn new(address: net::Address) -> Result { + let (host, port) = get_tcp_host_port(address)?; + Ok(HttpClient { host, port }) } } -/// A WebSocket-based transport for interacting with the Tendermint RPC -/// subscriptions interface. +/// An HTTP- and WebSocket-based Tendermint RPC client. +/// +/// HTTP is used for all requests except those pertaining to [`Event`] +/// subscription. `Event` subscription is facilitated by a WebSocket +/// connection, which is opened as this client is created. #[derive(Debug)] -pub struct WebSocketSubscriptionTransport { - driver_hdl: JoinHandle>, +pub struct HttpWebSocketClient { + host: String, + port: u16, + driver_handle: JoinHandle>, cmd_tx: mpsc::Sender, - next_subs_id: SubscriptionId, + next_subscription_id: SubscriptionId, } -impl WebSocketSubscriptionTransport { - async fn new(url: &str) -> Result { - let (stream, _response) = connect_async(url).await?; +impl HttpWebSocketClient { + /// Construct a full HTTP/WebSocket client directly. + pub async fn new(address: net::Address) -> Result { + let (host, port) = get_tcp_host_port(address)?; + let (stream, _response) = + connect_async(&format!("ws://{}:{}/websocket", &host, port)).await?; let (cmd_tx, cmd_rx) = mpsc::channel(DEFAULT_WEBSOCKET_CMD_BUF_SIZE); let driver = WebSocketSubscriptionDriver::new(stream, cmd_rx); - let driver_hdl = tokio::spawn(async move { driver.run().await }); - Ok(Self { - driver_hdl, + let driver_handle = tokio::spawn(async move { driver.run().await }); + Ok(HttpWebSocketClient { + host, + port, + driver_handle, cmd_tx, - next_subs_id: SubscriptionId::from(0), + next_subscription_id: SubscriptionId::default(), }) } - async fn send_cmd(&mut self, cmd: WebSocketDriverCmd) -> Result<(), Error> { + /// In the absence of an `async` version of [`std::convert::TryFrom`], + /// this constructor provides an `async` way to upgrade an [`HttpClient`] + /// to an [`HttpWebSocketClient`]. + pub async fn try_from(client: HttpClient) -> Result { + HttpWebSocketClient::new(net::Address::Tcp { + peer_id: None, + host: client.host, + port: client.port, + }) + .await + } + + async fn send_cmd(&mut self, cmd: WebSocketDriverCmd) -> Result<()> { self.cmd_tx.send(cmd).await.map_err(|e| { - Error::internal_error(format!( - "failed to transmit command to async WebSocket driver: {}", - e - )) + Error::internal_error(format!("failed to send command to client driver: {}", e)) }) } +} + +#[async_trait] +impl MinimalClient for HttpWebSocketClient { + async fn perform(&self, request: R) -> Result + where + R: Request, + { + http_request(&self.host, self.port, request).await + } - fn next_subscription_id(&mut self) -> SubscriptionId { - let res = self.next_subs_id.clone(); - self.next_subs_id = self.next_subs_id.next(); - res + /// Gracefully terminate the underlying connection. + /// + /// This sends a termination message to the client driver and blocks + /// indefinitely until the driver terminates. If successfully closed, it + /// returns the `Result` of the driver's async task. + async fn close(mut self) -> Result<()> { + self.send_cmd(WebSocketDriverCmd::Close).await?; + self.driver_handle.await.map_err(|e| { + Error::internal_error(format!("failed to join client driver async task: {}", e)) + })? } } #[async_trait] -impl SubscriptionTransport for WebSocketSubscriptionTransport { - async fn subscribe( +impl FullClient for HttpWebSocketClient { + async fn subscribe_with_buf_size( &mut self, - request: subscribe::Request, - event_tx: mpsc::Sender, - ) -> Result { - let id = self.next_subscription_id(); + query: String, + buf_size: usize, + ) -> Result { + let (event_tx, event_rx) = mpsc::channel(buf_size); + let (response_tx, response_rx) = oneshot::channel(); + let id = self.next_subscription_id.advance(); self.send_cmd(WebSocketDriverCmd::Subscribe { - request, id: id.clone(), + query: query.clone(), event_tx, + response_tx, }) .await?; - Ok(id) - } - - async fn unsubscribe( - &mut self, - request: unsubscribe::Request, - subscription: Subscription, - ) -> Result<(), Error> { - self.send_cmd(WebSocketDriverCmd::Unsubscribe { - request, - subscription, - }) - .await + // Wait to make sure our subscription request went through + // successfully. + response_rx.await.map_err(|e| { + Error::internal_error(format!( + "failed to receive response from client driver for subscription request: {}", + e + )) + })??; + Ok(Subscription::new(id, query, event_rx)) } -} -#[async_trait] -impl ClosableTransport for WebSocketSubscriptionTransport { - async fn close(mut self) -> Result<(), Error> { - self.send_cmd(WebSocketDriverCmd::Close).await?; - self.driver_hdl.await.map_err(|e| { - Error::internal_error(format!("failed to join async WebSocket driver task: {}", e)) - })? + async fn unsubscribe(&mut self, subscription: Subscription) -> Result<()> { + // TODO(thane): Should we insist on a response here to ensure the + // subscription was actually terminated? Right now this is + // just fire-and-forget. + self.send_cmd(WebSocketDriverCmd::Unsubscribe(subscription)) + .await } } @@ -199,7 +185,7 @@ impl WebSocketSubscriptionDriver { } } - async fn run(mut self) -> Result<(), Error> { + async fn run(mut self) -> Result<()> { // TODO(thane): Should this loop initiate a keepalive (ping) to the // server on a regular basis? loop { @@ -213,15 +199,20 @@ impl WebSocketSubscriptionDriver { ), }, Some(cmd) = self.cmd_rx.next() => match cmd { - WebSocketDriverCmd::Subscribe { request, id, event_tx } => self.subscribe(request, id, event_tx).await?, - WebSocketDriverCmd::Unsubscribe { request, subscription } => self.unsubscribe(request, subscription).await?, + WebSocketDriverCmd::Subscribe { + id, + query, + event_tx, + response_tx, + } => self.subscribe(id, query, event_tx, response_tx).await?, + WebSocketDriverCmd::Unsubscribe(subscription) => self.unsubscribe(subscription).await?, WebSocketDriverCmd::Close => return self.close().await, } } } } - async fn send(&mut self, msg: Message) -> Result<(), Error> { + async fn send(&mut self, msg: Message) -> Result<()> { self.stream.send(msg).await.map_err(|e| { Error::websocket_error(format!("failed to write to WebSocket connection: {}", e)) }) @@ -229,28 +220,47 @@ impl WebSocketSubscriptionDriver { async fn subscribe( &mut self, - request: subscribe::Request, id: SubscriptionId, + query: String, event_tx: mpsc::Sender, - ) -> Result<(), Error> { - self.send(Message::Text(request.clone().into_json())) - .await?; - self.router.add(id, request.query, event_tx); + response_tx: oneshot::Sender>, + ) -> Result<()> { + if let Err(e) = self + .send(Message::Text( + subscribe::Request::new(query.clone()).into_json(), + )) + .await + { + if response_tx.send(Err(e)).is_err() { + return Err(Error::internal_error( + "failed to respond internally to subscription request", + )); + } + // One failure shouldn't bring down the entire client. + return Ok(()); + } + // TODO(thane): Should we wait for a response from the remote endpoint? + self.router.add(id, query, event_tx); + // TODO(thane): How do we deal with the case where the following + // response fails? + if response_tx.send(Ok(())).is_err() { + return Err(Error::internal_error( + "failed to respond internally to subscription request", + )); + } Ok(()) } - async fn unsubscribe( - &mut self, - request: unsubscribe::Request, - subs: Subscription, - ) -> Result<(), Error> { - self.send(Message::Text(request.clone().into_json())) - .await?; + async fn unsubscribe(&mut self, subs: Subscription) -> Result<()> { + self.send(Message::Text( + unsubscribe::Request::new(subs.query.clone()).into_json(), + )) + .await?; self.router.remove(subs); Ok(()) } - async fn handle_incoming_msg(&mut self, msg: Message) -> Result<(), Error> { + 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, @@ -259,7 +269,7 @@ impl WebSocketSubscriptionDriver { } } - async fn handle_text_msg(&mut self, msg: String) -> Result<(), Error> { + async fn handle_text_msg(&mut self, msg: String) -> Result<()> { match Event::from_string(msg) { Ok(ev) => { self.router.publish(ev).await; @@ -274,11 +284,11 @@ impl WebSocketSubscriptionDriver { } } - async fn pong(&mut self, v: Vec) -> Result<(), Error> { + async fn pong(&mut self, v: Vec) -> Result<()> { self.send(Message::Pong(v)).await } - async fn close(mut self) -> Result<(), Error> { + async fn close(mut self) -> Result<()> { self.send(Message::Close(Some(CloseFrame { code: CloseCode::Normal, reason: Cow::from("client closed WebSocket connection"), @@ -297,13 +307,48 @@ impl WebSocketSubscriptionDriver { #[derive(Debug)] enum WebSocketDriverCmd { Subscribe { - request: subscribe::Request, id: SubscriptionId, + query: String, event_tx: mpsc::Sender, + response_tx: oneshot::Sender>, }, - Unsubscribe { - request: unsubscribe::Request, - subscription: Subscription, - }, + Unsubscribe(Subscription), Close, } + +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()) +} + +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/lib.rs b/rpc/src/lib.rs index fde89fd52..5ac9fa168 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -3,7 +3,9 @@ #[cfg(feature = "client")] mod client; #[cfg(feature = "client")] -pub use client::{event_listener, new_subscription_client, transport, Client, SubscriptionClient}; +pub use client::{FullClient, MinimalClient, Subscription, SubscriptionId, SubscriptionRouter}; +#[cfg(feature = "http_ws")] +pub use client::{HttpClient, HttpWebSocketClient}; pub mod endpoint; pub mod error; @@ -12,8 +14,10 @@ mod id; mod method; pub mod request; pub mod response; +pub mod result; mod version; pub use self::{ - error::Error, id::Id, method::Method, request::Request, response::Response, version::Version, + error::Error, id::Id, method::Method, request::Request, response::Response, result::Result, + version::Version, }; diff --git a/rpc/src/result.rs b/rpc/src/result.rs new file mode 100644 index 000000000..48bf5873e --- /dev/null +++ b/rpc/src/result.rs @@ -0,0 +1,4 @@ +use crate::Error; + +/// RPC client-related result type alias. +pub type Result = std::result::Result; diff --git a/rpc/tests/client.rs b/rpc/tests/client.rs new file mode 100644 index 000000000..5abdc6114 --- /dev/null +++ b/rpc/tests/client.rs @@ -0,0 +1,258 @@ +//! Tendermint RPC client tests. + +use async_trait::async_trait; +use futures::stream::StreamExt; +use std::collections::HashMap; +use std::path::PathBuf; +use tendermint::block::Height; +use tendermint_rpc::event::{Event, EventData, WrappedEvent}; +use tendermint_rpc::{ + Error, FullClient, Method, MinimalClient, Request, Response, Result, Subscription, + SubscriptionId, SubscriptionRouter, +}; +use tokio::fs; +use tokio::sync::{mpsc, oneshot}; +use tokio::task::JoinHandle; + +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() +} + +#[derive(Debug)] +struct MockClient { + responses: HashMap, + driver_handle: JoinHandle>, + event_tx: mpsc::Sender, + cmd_tx: mpsc::Sender, + next_subs_id: SubscriptionId, +} + +#[async_trait] +impl MinimalClient for MockClient { + async fn perform(&self, request: R) -> Result + where + R: Request, + { + self.responses + .get(&request.method()) + .and_then(|res| Some(R::Response::from_string(res).unwrap())) + .ok_or_else(|| { + Error::http_error(format!( + "no response mapping for request method: {}", + request.method() + )) + }) + } + + async fn close(mut self) -> Result<()> { + self.cmd_tx.send(MockClientCmd::Close).await.unwrap(); + self.driver_handle.await.unwrap() + } +} + +#[async_trait] +impl FullClient for MockClient { + async fn subscribe_with_buf_size( + &mut self, + query: String, + buf_size: usize, + ) -> Result { + let (event_tx, event_rx) = mpsc::channel(buf_size); + let (response_tx, response_rx) = oneshot::channel(); + let id = self.next_subs_id.advance(); + self.cmd_tx + .send(MockClientCmd::Subscribe { + id: id.clone(), + query: query.clone(), + event_tx, + response_tx, + }) + .await + .unwrap(); + // We need to wait until the subscription's been created, otherwise we + // risk introducing nondeterminism into the tests. + response_rx.await.unwrap().unwrap(); + Ok(Subscription::new(id, query, event_rx)) + } + + async fn unsubscribe(&mut self, subscription: Subscription) -> Result<()> { + Ok(self + .cmd_tx + .send(MockClientCmd::Unsubscribe(subscription)) + .await + .unwrap()) + } +} + +impl MockClient { + fn new() -> Self { + let (event_tx, event_rx) = mpsc::channel(10); + let (cmd_tx, cmd_rx) = mpsc::channel(10); + let driver = MockClientDriver::new(event_rx, cmd_rx); + let driver_hdl = tokio::spawn(async move { driver.run().await }); + Self { + responses: HashMap::new(), + driver_handle: driver_hdl, + event_tx, + cmd_tx, + next_subs_id: SubscriptionId::default(), + } + } + + fn map(mut self, method: Method, response: String) -> Self { + self.responses.insert(method, response); + self + } + + async fn publish(&mut self, ev: Event) { + match &ev.data { + EventData::NewBlock { block, .. } => println!( + "Sending NewBlock event for height {}", + block.as_ref().unwrap().header.height + ), + _ => (), + } + self.event_tx.send(ev).await.unwrap(); + } +} + +#[derive(Debug)] +enum MockClientCmd { + Subscribe { + id: SubscriptionId, + query: String, + event_tx: mpsc::Sender, + response_tx: oneshot::Sender>, + }, + Unsubscribe(Subscription), + Close, +} + +#[derive(Debug)] +struct MockClientDriver { + event_rx: mpsc::Receiver, + cmd_rx: mpsc::Receiver, + router: SubscriptionRouter, +} + +impl MockClientDriver { + // `event_rx` simulates an incoming event stream (e.g. by way of the + // WebSocket connection). + fn new(event_rx: mpsc::Receiver, cmd_rx: mpsc::Receiver) -> Self { + Self { + event_rx, + cmd_rx, + router: SubscriptionRouter::new(), + } + } + + async fn run(mut self) -> Result<()> { + loop { + tokio::select! { + Some(ev) = self.event_rx.next() => { + match &ev.data { + EventData::NewBlock { block, .. } => println!( + "Publishing NewBlock event at height {}", + block.as_ref().unwrap().header.height, + ), + _ => (), + } + self.router.publish(ev).await; + () + } + Some(cmd) = self.cmd_rx.next() => match cmd { + MockClientCmd::Subscribe { id, query, event_tx, response_tx } => { + self.router.add(id, query, event_tx); + response_tx.send(Ok(())).unwrap(); + () + }, + MockClientCmd::Unsubscribe(subs) => { + self.router.remove(subs); + () + }, + MockClientCmd::Close => return Ok(()), + } + } + } + } +} + +#[tokio::test] +async fn minimal_client() { + let client = MockClient::new() + .map(Method::AbciInfo, read_json_fixture("abci_info").await) + .map(Method::Block, read_json_fixture("block").await); + + let abci_info = client.abci_info().await.unwrap(); + assert_eq!("GaiaApp".to_string(), abci_info.data); + + let block = client.block(10).await.unwrap().block; + assert_eq!(Height::from(10), block.header.height); + assert_eq!("cosmoshub-2", block.header.chain_id.as_str()); + + client.close().await.unwrap(); +} + +#[tokio::test] +async fn full_client() { + let mut client = MockClient::new(); + let incoming_events = vec![ + read_event("event_new_block_1").await, + read_event("event_new_block_2").await, + read_event("event_new_block_3").await, + ]; + let expected_heights = vec![Height::from(165), Height::from(166), Height::from(167)]; + + let subs1 = client + .subscribe("tm.event='NewBlock'".to_string()) + .await + .unwrap(); + let subs2 = client + .subscribe("tm.event='NewBlock'".to_string()) + .await + .unwrap(); + + let subs1_events_task = + tokio::spawn(async move { subs1.take(3).collect::>().await }); + let subs2_events_task = + tokio::spawn(async move { subs2.take(3).collect::>().await }); + + println!("Publishing incoming events..."); + for ev in incoming_events { + client.publish(ev).await; + } + + println!("Collecting incoming events..."); + let subs1_events = subs1_events_task.await.unwrap(); + let subs2_events = subs2_events_task.await.unwrap(); + + client.close().await.unwrap(); + + assert_eq!(3, subs1_events.len()); + assert_eq!(3, subs2_events.len()); + + println!("Checking collected events..."); + for i in 0..3 { + match &subs1_events[i].data { + EventData::NewBlock { block, .. } => { + assert_eq!(expected_heights[i], block.as_ref().unwrap().header.height); + } + _ => panic!("invalid event type for subs1: {:?}", subs1_events[i]), + } + match &subs2_events[i].data { + EventData::NewBlock { block, .. } => { + assert_eq!(expected_heights[i], block.as_ref().unwrap().header.height); + } + _ => panic!("invalid event type for subs2: {:?}", subs2_events[i]), + } + } +} diff --git a/rpc/tests/endpoint.rs b/rpc/tests/endpoint.rs new file mode 100644 index 000000000..9ac9f0bba --- /dev/null +++ b/rpc/tests/endpoint.rs @@ -0,0 +1,292 @@ +//! Tendermint RPC endpoint testing. + +use std::{fs, path::PathBuf}; +use tendermint::abci::Code; + +use tendermint_rpc::{self as rpc, endpoint, Response}; + +const EXAMPLE_APP: &str = "GaiaApp"; +const EXAMPLE_CHAIN: &str = "cosmoshub-2"; + +fn read_json_fixture(name: &str) -> String { + fs::read_to_string(PathBuf::from("./tests/support/").join(name.to_owned() + ".json")).unwrap() +} + +#[test] +fn abci_info() { + let response = endpoint::abci_info::Response::from_string(&read_json_fixture("abci_info")) + .unwrap() + .response; + + assert_eq!(response.data.as_str(), EXAMPLE_APP); + assert_eq!(response.last_block_height.value(), 488_120); +} + +#[test] +fn abci_query() { + let response = endpoint::abci_query::Response::from_string(&read_json_fixture("abci_query")) + .unwrap() + .response; + + assert_eq!(response.height.value(), 1); +} + +#[test] +fn block() { + let response = endpoint::block::Response::from_string(&read_json_fixture("block")).unwrap(); + + let tendermint::Block { + header, + data, + evidence, + last_commit, + } = response.block; + + assert_eq!(header.version.block, 10); + assert_eq!(header.chain_id.as_str(), EXAMPLE_CHAIN); + assert_eq!(header.height.value(), 10); + assert_eq!(data.iter().len(), 0); + assert_eq!(evidence.iter().len(), 0); + assert_eq!(last_commit.unwrap().signatures.len(), 1); +} + +#[test] +fn block_with_evidences() { + let response = + endpoint::block::Response::from_string(&read_json_fixture("block_with_evidences")).unwrap(); + + let tendermint::Block { evidence, .. } = response.block; + let evidence = evidence.iter().next().unwrap(); + + match evidence { + tendermint::evidence::Evidence::DuplicateVote(_) => (), + _ => unreachable!(), + } +} + +// TODO: Update this test and its json file +// #[test] +// fn block_empty_block_id() { +// let response = +// endpoint::block::Response::from_string(&read_json_fixture("block_empty_block_id")) +// .unwrap(); +// +// let tendermint::Block { last_commit, .. } = response.block; +// +// assert_eq!(last_commit.as_ref().unwrap().precommits.len(), 2); +// assert!(last_commit.unwrap().precommits[0] +// .as_ref() +// .unwrap() +// .block_id +// .is_none()); +// } + +#[test] +fn first_block() { + let response = + endpoint::block::Response::from_string(&read_json_fixture("first_block")).unwrap(); + + let tendermint::Block { + header, + data, + evidence, + last_commit, + } = response.block; + + assert_eq!(header.version.block, 10); + assert_eq!(header.chain_id.as_str(), EXAMPLE_CHAIN); + assert_eq!(header.height.value(), 1); + assert!(header.last_block_id.is_none()); + + assert_eq!(data.iter().len(), 0); + assert_eq!(evidence.iter().len(), 0); + assert!(last_commit.is_none()); +} +#[test] +fn block_results() { + let response = + endpoint::block_results::Response::from_string(&read_json_fixture("block_results")) + .unwrap(); + assert_eq!(response.height.value(), 1814); + + let validator_updates = response.validator_updates; + let deliver_tx = response.txs_results.unwrap(); + let log_json = &deliver_tx[0].log.parse_json().unwrap(); + let log_json_value = &log_json.as_array().as_ref().unwrap()[0]; + + assert_eq!(log_json_value["msg_index"].as_str().unwrap(), "0"); + assert_eq!(log_json_value["success"].as_bool().unwrap(), true); + + assert_eq!(deliver_tx[0].gas_wanted.value(), 200_000); + assert_eq!(deliver_tx[0].gas_used.value(), 105_662); + + assert_eq!(validator_updates[0].power.value(), 1_233_243); +} + +#[test] +fn blockchain() { + let response = + endpoint::blockchain::Response::from_string(&read_json_fixture("blockchain")).unwrap(); + + assert_eq!(response.last_height.value(), 488_556); + assert_eq!(response.block_metas.len(), 10); + + let block_meta = &response.block_metas[0]; + assert_eq!(block_meta.header.chain_id.as_str(), EXAMPLE_CHAIN) +} + +#[test] +fn broadcast_tx_async() { + let response = endpoint::broadcast::tx_async::Response::from_string(&read_json_fixture( + "broadcast_tx_async", + )) + .unwrap(); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn broadcast_tx_sync() { + let response = endpoint::broadcast::tx_sync::Response::from_string(&read_json_fixture( + "broadcast_tx_sync", + )) + .unwrap(); + + assert_eq!(response.code, Code::Ok); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn broadcast_tx_sync_int() { + let response = endpoint::broadcast::tx_sync::Response::from_string(&read_json_fixture( + "broadcast_tx_sync_int", + )) + .unwrap(); + + assert_eq!(response.code, Code::Ok); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn broadcast_tx_commit() { + let response = endpoint::broadcast::tx_commit::Response::from_string(&read_json_fixture( + "broadcast_tx_commit", + )) + .unwrap(); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn broadcast_tx_commit_null_data() { + let response = endpoint::broadcast::tx_commit::Response::from_string(&read_json_fixture( + "broadcast_tx_commit_null_data", + )) + .unwrap(); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn commit() { + let response = endpoint::commit::Response::from_string(&read_json_fixture("commit")).unwrap(); + let header = response.signed_header.header; + assert_eq!(header.chain_id.as_ref(), EXAMPLE_CHAIN); + // For now we just want to make sure the commit including precommits and a block_id exist + // in SignedHeader; later we should verify some properties: e.g. block_id.hash matches the + // header etc: + let commit = response.signed_header.commit; + let block_id = commit.block_id; + let _signatures = commit.signatures; + assert_eq!(header.hash(), block_id.hash); +} + +#[test] +fn commit_height_1() { + let response = endpoint::commit::Response::from_string(&read_json_fixture("commit_1")).unwrap(); + let header = response.signed_header.header; + let commit = response.signed_header.commit; + let block_id = commit.block_id; + assert_eq!(header.hash(), block_id.hash); +} + +#[test] +fn genesis() { + let response = endpoint::genesis::Response::from_string(&read_json_fixture("genesis")).unwrap(); + + let tendermint::Genesis { + chain_id, + consensus_params, + .. + } = response.genesis; + + assert_eq!(chain_id.as_str(), EXAMPLE_CHAIN); + assert_eq!(consensus_params.block.max_bytes, 200_000); +} + +#[test] +fn health() { + endpoint::health::Response::from_string(&read_json_fixture("health")).unwrap(); +} + +#[test] +fn net_info() { + let response = + endpoint::net_info::Response::from_string(&read_json_fixture("net_info")).unwrap(); + + assert_eq!(response.n_peers, 2); + assert_eq!(response.peers[0].node_info.network.as_str(), EXAMPLE_CHAIN); +} + +#[test] +fn status() { + let response = endpoint::status::Response::from_string(&read_json_fixture("status")).unwrap(); + + assert_eq!(response.node_info.network.as_str(), EXAMPLE_CHAIN); + assert_eq!(response.sync_info.latest_block_height.value(), 410_744); + assert_eq!(response.validator_info.voting_power.value(), 0); +} + +#[test] +fn validators() { + let response = + endpoint::validators::Response::from_string(&read_json_fixture("validators")).unwrap(); + + assert_eq!(response.block_height.value(), 42); + + let validators = response.validators; + assert_eq!(validators.len(), 65); +} + +#[test] +fn jsonrpc_error() { + let result = endpoint::blockchain::Response::from_string(&read_json_fixture("error")); + + if let Err(err) = result { + assert_eq!(err.code(), rpc::error::Code::InternalError); + assert_eq!(err.message(), "Internal error"); + assert_eq!( + err.data().unwrap(), + "min height 321 can't be greater than max height 123" + ); + } else { + panic!("expected error, got {:?}", result) + } +} diff --git a/rpc/tests/integration.rs b/rpc/tests/integration.rs index 30c02e618..04d58b42c 100644 --- a/rpc/tests/integration.rs +++ b/rpc/tests/integration.rs @@ -1,301 +1,4 @@ //! Tendermint RPC tests -mod endpoints { - use std::{fs, path::PathBuf}; - use tendermint::abci::Code; - - use tendermint_rpc::{self as rpc, endpoint, Response}; - - const EXAMPLE_APP: &str = "GaiaApp"; - const EXAMPLE_CHAIN: &str = "cosmoshub-2"; - - fn read_json_fixture(name: &str) -> String { - fs::read_to_string(PathBuf::from("./tests/support/").join(name.to_owned() + ".json")) - .unwrap() - } - - #[test] - fn abci_info() { - let response = endpoint::abci_info::Response::from_string(&read_json_fixture("abci_info")) - .unwrap() - .response; - - assert_eq!(response.data.as_str(), EXAMPLE_APP); - assert_eq!(response.last_block_height.value(), 488_120); - } - - #[test] - fn abci_query() { - let response = - endpoint::abci_query::Response::from_string(&read_json_fixture("abci_query")) - .unwrap() - .response; - - assert_eq!(response.height.value(), 1); - } - - #[test] - fn block() { - let response = endpoint::block::Response::from_string(&read_json_fixture("block")).unwrap(); - - let tendermint::Block { - header, - data, - evidence, - last_commit, - } = response.block; - - assert_eq!(header.version.block, 10); - assert_eq!(header.chain_id.as_str(), EXAMPLE_CHAIN); - assert_eq!(header.height.value(), 10); - assert_eq!(data.iter().len(), 0); - assert_eq!(evidence.iter().len(), 0); - assert_eq!(last_commit.unwrap().signatures.len(), 1); - } - - #[test] - fn block_with_evidences() { - let response = - endpoint::block::Response::from_string(&read_json_fixture("block_with_evidences")) - .unwrap(); - - let tendermint::Block { evidence, .. } = response.block; - let evidence = evidence.iter().next().unwrap(); - - match evidence { - tendermint::evidence::Evidence::DuplicateVote(_) => (), - _ => unreachable!(), - } - } - - // TODO: Update this test and its json file - // #[test] - // fn block_empty_block_id() { - // let response = - // endpoint::block::Response::from_string(&read_json_fixture("block_empty_block_id")) - // .unwrap(); - // - // let tendermint::Block { last_commit, .. } = response.block; - // - // assert_eq!(last_commit.as_ref().unwrap().precommits.len(), 2); - // assert!(last_commit.unwrap().precommits[0] - // .as_ref() - // .unwrap() - // .block_id - // .is_none()); - // } - - #[test] - fn first_block() { - let response = - endpoint::block::Response::from_string(&read_json_fixture("first_block")).unwrap(); - - let tendermint::Block { - header, - data, - evidence, - last_commit, - } = response.block; - - assert_eq!(header.version.block, 10); - assert_eq!(header.chain_id.as_str(), EXAMPLE_CHAIN); - assert_eq!(header.height.value(), 1); - assert!(header.last_block_id.is_none()); - - assert_eq!(data.iter().len(), 0); - assert_eq!(evidence.iter().len(), 0); - assert!(last_commit.is_none()); - } - #[test] - fn block_results() { - let response = - endpoint::block_results::Response::from_string(&read_json_fixture("block_results")) - .unwrap(); - assert_eq!(response.height.value(), 1814); - - let validator_updates = response.validator_updates; - let deliver_tx = response.txs_results.unwrap(); - let log_json = &deliver_tx[0].log.parse_json().unwrap(); - let log_json_value = &log_json.as_array().as_ref().unwrap()[0]; - - assert_eq!(log_json_value["msg_index"].as_str().unwrap(), "0"); - assert_eq!(log_json_value["success"].as_bool().unwrap(), true); - - assert_eq!(deliver_tx[0].gas_wanted.value(), 200_000); - assert_eq!(deliver_tx[0].gas_used.value(), 105_662); - - assert_eq!(validator_updates[0].power.value(), 1_233_243); - } - - #[test] - fn blockchain() { - let response = - endpoint::blockchain::Response::from_string(&read_json_fixture("blockchain")).unwrap(); - - assert_eq!(response.last_height.value(), 488_556); - assert_eq!(response.block_metas.len(), 10); - - let block_meta = &response.block_metas[0]; - assert_eq!(block_meta.header.chain_id.as_str(), EXAMPLE_CHAIN) - } - - #[test] - fn broadcast_tx_async() { - let response = endpoint::broadcast::tx_async::Response::from_string(&read_json_fixture( - "broadcast_tx_async", - )) - .unwrap(); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); - } - - #[test] - fn broadcast_tx_sync() { - let response = endpoint::broadcast::tx_sync::Response::from_string(&read_json_fixture( - "broadcast_tx_sync", - )) - .unwrap(); - - assert_eq!(response.code, Code::Ok); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); - } - - #[test] - fn broadcast_tx_sync_int() { - let response = endpoint::broadcast::tx_sync::Response::from_string(&read_json_fixture( - "broadcast_tx_sync_int", - )) - .unwrap(); - - assert_eq!(response.code, Code::Ok); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); - } - - #[test] - fn broadcast_tx_commit() { - let response = endpoint::broadcast::tx_commit::Response::from_string(&read_json_fixture( - "broadcast_tx_commit", - )) - .unwrap(); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); - } - - #[test] - fn broadcast_tx_commit_null_data() { - let response = endpoint::broadcast::tx_commit::Response::from_string(&read_json_fixture( - "broadcast_tx_commit_null_data", - )) - .unwrap(); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); - } - - #[test] - fn commit() { - let response = - endpoint::commit::Response::from_string(&read_json_fixture("commit")).unwrap(); - let header = response.signed_header.header; - assert_eq!(header.chain_id.as_ref(), EXAMPLE_CHAIN); - // For now we just want to make sure the commit including precommits and a block_id exist - // in SignedHeader; later we should verify some properties: e.g. block_id.hash matches the - // header etc: - let commit = response.signed_header.commit; - let block_id = commit.block_id; - let _signatures = commit.signatures; - assert_eq!(header.hash(), block_id.hash); - } - - #[test] - fn commit_height_1() { - let response = - endpoint::commit::Response::from_string(&read_json_fixture("commit_1")).unwrap(); - let header = response.signed_header.header; - let commit = response.signed_header.commit; - let block_id = commit.block_id; - assert_eq!(header.hash(), block_id.hash); - } - - #[test] - fn genesis() { - let response = - endpoint::genesis::Response::from_string(&read_json_fixture("genesis")).unwrap(); - - let tendermint::Genesis { - chain_id, - consensus_params, - .. - } = response.genesis; - - assert_eq!(chain_id.as_str(), EXAMPLE_CHAIN); - assert_eq!(consensus_params.block.max_bytes, 200_000); - } - - #[test] - fn health() { - endpoint::health::Response::from_string(&read_json_fixture("health")).unwrap(); - } - - #[test] - fn net_info() { - let response = - endpoint::net_info::Response::from_string(&read_json_fixture("net_info")).unwrap(); - - assert_eq!(response.n_peers, 2); - assert_eq!(response.peers[0].node_info.network.as_str(), EXAMPLE_CHAIN); - } - - #[test] - fn status() { - let response = - endpoint::status::Response::from_string(&read_json_fixture("status")).unwrap(); - - assert_eq!(response.node_info.network.as_str(), EXAMPLE_CHAIN); - assert_eq!(response.sync_info.latest_block_height.value(), 410_744); - assert_eq!(response.validator_info.voting_power.value(), 0); - } - - #[test] - fn validators() { - let response = - endpoint::validators::Response::from_string(&read_json_fixture("validators")).unwrap(); - - assert_eq!(response.block_height.value(), 42); - - let validators = response.validators; - assert_eq!(validators.len(), 65); - } - - #[test] - fn jsonrpc_error() { - let result = endpoint::blockchain::Response::from_string(&read_json_fixture("error")); - - if let Err(err) = result { - assert_eq!(err.code(), rpc::error::Code::InternalError); - assert_eq!(err.message(), "Internal error"); - assert_eq!( - err.data().unwrap(), - "min height 321 can't be greater than max height 123" - ); - } else { - panic!("expected error, got {:?}", result) - } - } -} +mod client; +mod endpoint; diff --git a/rpc/tests/support/event_new_block_1.json b/rpc/tests/support/event_new_block_1.json new file mode 100644 index 000000000..5f711db36 --- /dev/null +++ b/rpc/tests/support/event_new_block_1.json @@ -0,0 +1,73 @@ +{ + "jsonrpc": "2.0", + "id": "de2b057f-445d-4f4b-9f3d-8701de6b3c44", + "result": { + "query": "tm.event='NewBlock'", + "data": { + "type": "tendermint/event/NewBlock", + "value": { + "block": { + "header": { + "version": { + "block": "10", + "app": "1" + }, + "chain_id": "test-chain-WKiDTw", + "height": "165", + "time": "2020-08-14T14:50:32.869147Z", + "last_block_id": { + "hash": "EB6DF76F960C1A19DCAC661E47BA93E91FF6F09C983C97EC3D7CFBA07F01179C", + "parts": { + "total": "1", + "hash": "F09DA4A56C431530BA30BC9C7A8D320944F40818988C124D7528CB0F4DCF7C63" + } + }, + "last_commit_hash": "B6D6C589FB7FF5FFA0BB025D68770283452A9337D193EF42E0AE6864E6B12B90", + "data_hash": "", + "validators_hash": "ABF17FC49D88F55BA29D8BA7890EF44F9684C825483A95666670320CECC60448", + "next_validators_hash": "ABF17FC49D88F55BA29D8BA7890EF44F9684C825483A95666670320CECC60448", + "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F", + "app_hash": "0000000000000000", + "last_results_hash": "", + "evidence_hash": "", + "proposer_address": "F19E2E909675C8461B2CE4D0051255C1FC08E0E3" + }, + "data": { + "txs": null + }, + "evidence": { + "evidence": null + }, + "last_commit": { + "height": "164", + "round": "0", + "block_id": { + "hash": "EB6DF76F960C1A19DCAC661E47BA93E91FF6F09C983C97EC3D7CFBA07F01179C", + "parts": { + "total": "1", + "hash": "F09DA4A56C431530BA30BC9C7A8D320944F40818988C124D7528CB0F4DCF7C63" + } + }, + "signatures": [ + { + "block_id_flag": 2, + "validator_address": "F19E2E909675C8461B2CE4D0051255C1FC08E0E3", + "timestamp": "2020-08-14T14:50:32.869147Z", + "signature": "xPtUT2ihJb22E6/i/PW75m5SuKM05xjrh7PeeAxmXRsW7YGBbszP2VPNCyXO3KHNZmFatkrjy+9rXIwl9wyNCA==" + } + ] + } + }, + "result_begin_block": {}, + "result_end_block": { + "validator_updates": null + } + } + }, + "events": { + "tm.event": [ + "NewBlock" + ] + } + } +} \ No newline at end of file diff --git a/rpc/tests/support/event_new_block_2.json b/rpc/tests/support/event_new_block_2.json new file mode 100644 index 000000000..d38ffbf4b --- /dev/null +++ b/rpc/tests/support/event_new_block_2.json @@ -0,0 +1,73 @@ +{ + "jsonrpc": "2.0", + "id": "de2b057f-445d-4f4b-9f3d-8701de6b3c44", + "result": { + "query": "tm.event='NewBlock'", + "data": { + "type": "tendermint/event/NewBlock", + "value": { + "block": { + "header": { + "version": { + "block": "10", + "app": "1" + }, + "chain_id": "test-chain-WKiDTw", + "height": "166", + "time": "2020-08-14T14:50:33.899482Z", + "last_block_id": { + "hash": "8A003E7438AEE700E8452A34416FBFA482D147590F31BD8757418BC228139277", + "parts": { + "total": "1", + "hash": "19BA877ED58A3F841DF321E9771CACF726D9753BB01551F477BEC5D6B69801C4" + } + }, + "last_commit_hash": "3BD6C71931030C050DBFDD5B0C96EC5BC16DAD25A5C54787D8BCED8F7C1776EF", + "data_hash": "", + "validators_hash": "ABF17FC49D88F55BA29D8BA7890EF44F9684C825483A95666670320CECC60448", + "next_validators_hash": "ABF17FC49D88F55BA29D8BA7890EF44F9684C825483A95666670320CECC60448", + "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F", + "app_hash": "0000000000000000", + "last_results_hash": "", + "evidence_hash": "", + "proposer_address": "F19E2E909675C8461B2CE4D0051255C1FC08E0E3" + }, + "data": { + "txs": null + }, + "evidence": { + "evidence": null + }, + "last_commit": { + "height": "165", + "round": "0", + "block_id": { + "hash": "8A003E7438AEE700E8452A34416FBFA482D147590F31BD8757418BC228139277", + "parts": { + "total": "1", + "hash": "19BA877ED58A3F841DF321E9771CACF726D9753BB01551F477BEC5D6B69801C4" + } + }, + "signatures": [ + { + "block_id_flag": 2, + "validator_address": "F19E2E909675C8461B2CE4D0051255C1FC08E0E3", + "timestamp": "2020-08-14T14:50:33.899482Z", + "signature": "UwrIhrTgqI/sdanT3Yp6ZA9PnTW8bIkj8SwUX1jXlh8rR4C51FMLtFNEQSpjf0+xdiyy0X+sLSfhfd7oqY2xAg==" + } + ] + } + }, + "result_begin_block": {}, + "result_end_block": { + "validator_updates": null + } + } + }, + "events": { + "tm.event": [ + "NewBlock" + ] + } + } +} \ No newline at end of file diff --git a/rpc/tests/support/event_new_block_3.json b/rpc/tests/support/event_new_block_3.json new file mode 100644 index 000000000..5bbed8af3 --- /dev/null +++ b/rpc/tests/support/event_new_block_3.json @@ -0,0 +1,73 @@ +{ + "jsonrpc": "2.0", + "id": "de2b057f-445d-4f4b-9f3d-8701de6b3c44", + "result": { + "query": "tm.event='NewBlock'", + "data": { + "type": "tendermint/event/NewBlock", + "value": { + "block": { + "header": { + "version": { + "block": "10", + "app": "1" + }, + "chain_id": "test-chain-WKiDTw", + "height": "167", + "time": "2020-08-14T14:50:34.926Z", + "last_block_id": { + "hash": "94904329902C4AF455C3D5D13E9CAD98F4513525F0F6D87B17A78ECC2083B3DE", + "parts": { + "total": "1", + "hash": "F0B0B14A93B9FD079556E9B8B309F715C9D37D69377374F38C634D621DB31C5F" + } + }, + "last_commit_hash": "54266A911A5C6E5BBB0F1508525174FD3D05C042A5C7BC9E5098656D3CB8CDBF", + "data_hash": "", + "validators_hash": "ABF17FC49D88F55BA29D8BA7890EF44F9684C825483A95666670320CECC60448", + "next_validators_hash": "ABF17FC49D88F55BA29D8BA7890EF44F9684C825483A95666670320CECC60448", + "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F", + "app_hash": "0000000000000000", + "last_results_hash": "", + "evidence_hash": "", + "proposer_address": "F19E2E909675C8461B2CE4D0051255C1FC08E0E3" + }, + "data": { + "txs": null + }, + "evidence": { + "evidence": null + }, + "last_commit": { + "height": "166", + "round": "0", + "block_id": { + "hash": "94904329902C4AF455C3D5D13E9CAD98F4513525F0F6D87B17A78ECC2083B3DE", + "parts": { + "total": "1", + "hash": "F0B0B14A93B9FD079556E9B8B309F715C9D37D69377374F38C634D621DB31C5F" + } + }, + "signatures": [ + { + "block_id_flag": 2, + "validator_address": "F19E2E909675C8461B2CE4D0051255C1FC08E0E3", + "timestamp": "2020-08-14T14:50:34.926Z", + "signature": "fXkatEuM3iBvhgF9gSN2IYffhre4mNsAlBSMB+Elaok6bUs8hpyyc787aBjfKu1iStBJkeFeWe8SJjLwgf3yCg==" + } + ] + } + }, + "result_begin_block": {}, + "result_end_block": { + "validator_updates": null + } + } + }, + "events": { + "tm.event": [ + "NewBlock" + ] + } + } +} \ No newline at end of file diff --git a/tendermint/Cargo.toml b/tendermint/Cargo.toml index 4bda89e09..ecc7958f2 100644 --- a/tendermint/Cargo.toml +++ b/tendermint/Cargo.toml @@ -57,7 +57,7 @@ zeroize = { version = "1.1", features = ["zeroize_derive"] } ripemd160 = { version = "0.9", optional = true } [dev-dependencies] -tendermint-rpc = { path = "../rpc", features = [ "client" ] } +tendermint-rpc = { path = "../rpc", features = [ "client", "http_ws" ] } tokio = { version = "0.2", features = [ "macros" ] } [features] diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index 2e6c536f9..f31c478b8 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -11,16 +11,15 @@ mod rpc { use std::cmp::min; - use tendermint_rpc::transport::http_ws::HttpTransport; - use tendermint_rpc::{event_listener, new_subscription_client, Client}; + use tendermint_rpc::{FullClient, HttpClient, HttpWebSocketClient, MinimalClient}; use futures::StreamExt; use tendermint::abci::Code; use tendermint::abci::Log; /// Get the address of the local node - pub fn localhost_rpc_client() -> Client { - Client::new(HttpTransport::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap()) + pub fn localhost_rpc_client() -> HttpClient { + HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()).unwrap() } /// `/health` endpoint @@ -150,9 +149,10 @@ mod rpc { #[tokio::test] #[ignore] async fn subscription_interface() { - let client = localhost_rpc_client(); - let mut subs_client = new_subscription_client(&client).await.unwrap(); - let mut subs = subs_client + let mut client = HttpWebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) + .await + .unwrap(); + let mut subs = client .subscribe("tm.event='NewBlock'".to_string()) .await .unwrap(); @@ -167,39 +167,6 @@ mod rpc { } } - subs_client.close().await.unwrap(); client.close().await.unwrap(); } - - #[tokio::test] - #[ignore] - async fn event_subscription() { - let mut client = - event_listener::EventListener::connect("tcp://127.0.0.1:26657".parse().unwrap()) - .await - .unwrap(); - client - .subscribe(event_listener::EventSubscription::BlockSubscription) - .await - .unwrap(); - // client.subscribe("tm.event='NewBlock'".to_owned()).await.unwrap(); - - // Loop here is helpful when debugging parsing of JSON events - // loop{ - let maybe_result_event = client.get_event().await.unwrap(); - dbg!(&maybe_result_event); - // } - let result_event = maybe_result_event.expect("unexpected msg read"); - match result_event.data { - event_listener::TMEventData::EventDataNewBlock(nb) => { - dbg!("got EventDataNewBlock: {:?}", nb); - } - event_listener::TMEventData::EventDataTx(tx) => { - dbg!("got EventDataTx: {:?}", tx); - } - event_listener::TMEventData::GenericJSONEvent(v) => { - panic!("got a GenericJSONEvent: {:?}", v); - } - } - } } From cf56f7b8ea2e8bf50ee6916863cb915908c93eec Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 14 Aug 2020 12:26:24 -0400 Subject: [PATCH 23/60] Reduce number of blocks grabbed to speed up test Signed-off-by: Thane Thomson --- tendermint/tests/integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index f31c478b8..efb5154c8 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -156,7 +156,7 @@ mod rpc { .subscribe("tm.event='NewBlock'".to_string()) .await .unwrap(); - let mut ev_count = 10_i32; + let mut ev_count = 5_i32; dbg!("Attempting to grab {} new blocks", ev_count); while let Some(ev) = subs.next().await { From ae66f90723c4a5c95845308d6361133525dddbd5 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 14 Aug 2020 12:26:54 -0400 Subject: [PATCH 24/60] Improve documentation for RPC client Signed-off-by: Thane Thomson --- rpc/src/client.rs | 20 +++++++-- rpc/src/client/transport/http_ws.rs | 68 +++++++++++++++++++++++++++++ rpc/src/endpoint/unsubscribe.rs | 2 +- rpc/src/lib.rs | 7 ++- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 6228c837f..67dac5412 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -17,7 +17,11 @@ //! (see [`HttpClient`]), and a WebSocket-based interface for `Event` //! subscription (see [`HttpWebSocketClient`]). //! -//! [`Event`]: event::Event +//! [`MinimalClient`]: trait.MinimalClient.html +//! [`FullClient`]: trait.FullClient.html +//! [`Event`]: event/struct.Event.html +//! [`HttpClient`]: struct.HttpClient.html +//! [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html mod subscription; pub use subscription::{Subscription, SubscriptionId, SubscriptionRouter}; @@ -36,6 +40,9 @@ use tendermint::Genesis; /// The default number of events we buffer in a [`Subscription`] if you do not /// specify the buffer size when creating it. +/// +/// [`Subscription`]: struct.Subscription.html +/// pub const DEFAULT_SUBSCRIPTION_BUF_SIZE: usize = 100; /// A `MinimalClient` provides lightweight access to the Tendermint RPC. It @@ -44,6 +51,9 @@ pub const DEFAULT_SUBSCRIPTION_BUF_SIZE: usize = 100; /// /// To access event subscription capabilities, use a client that implements the /// [`FullClient`] trait. +/// +/// [`FullClient`]: trait.FullClient.html +/// #[async_trait] pub trait MinimalClient { /// `/abci_info`: get information about the ABCI application. @@ -188,6 +198,9 @@ pub trait MinimalClient { /// A `FullClient` is one that augments a [`MinimalClient`] functionality with /// subscription capabilities. +/// +/// [`MinimalClient`]: trait.MinimalClient.html +/// #[async_trait] pub trait FullClient: MinimalClient { /// `/subscribe`: subscribe to receive events produced by the given query. @@ -199,7 +212,7 @@ pub trait FullClient: MinimalClient { /// The slower your application processes events, the larger this buffer /// needs to be. /// - /// [`Subscription`]: client::subscription::Subscription + /// [`Subscription`]: struct.Subscription.html /// async fn subscribe_with_buf_size( &mut self, @@ -212,7 +225,8 @@ pub trait FullClient: MinimalClient { /// Uses [`DEFAULT_SUBSCRIPTION_BUF_SIZE`] as the buffer size for the /// returned [`Subscription`]. /// - /// [`Subscription`]: client::subscription::Subscription + /// [`DEFAULT_SUBSCRIPTION_BUF_SIZE`]: constant.DEFAULT_SUBSCRIPTION_BUF_SIZE.html + /// [`Subscription`]: struct.Subscription.html /// async fn subscribe(&mut self, query: String) -> Result { self.subscribe_with_buf_size(query, DEFAULT_SUBSCRIPTION_BUF_SIZE) diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index d03274bda..81badf003 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -28,6 +28,36 @@ use tokio::task::JoinHandle; const DEFAULT_WEBSOCKET_CMD_BUF_SIZE: usize = 20; /// An HTTP-based Tendermint RPC client (a [`MinimalClient`] implementation). +/// +/// Does not provide [`Event`] subscription facilities (see +/// [`HttpWebSocketClient`] for a client that does provide `Event` subscription +/// facilities). +/// +/// ## Examples +/// +/// We don't test this example automatically at present, but it has and can +/// been tested against a Tendermint node running on `localhost`. +/// +/// ```ignore +/// use tendermint_rpc::{HttpClient, MinimalClient}; +/// +/// #[tokio::main] +/// async fn main() { +/// let client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()) +/// .unwrap(); +/// +/// let abci_info = client.abci_info() +/// .await +/// .unwrap(); +/// +/// println!("Got ABCI info: {:?}", abci_info); +/// } +/// ``` +/// +/// [`MinimalClient`]: trait.MinimalClient.html +/// [`Event`]: ./event/struct.Event.html +/// [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html +/// #[derive(Debug, Clone)] pub struct HttpClient { host: String, @@ -61,6 +91,44 @@ impl HttpClient { /// HTTP is used for all requests except those pertaining to [`Event`] /// subscription. `Event` subscription is facilitated by a WebSocket /// connection, which is opened as this client is created. +/// +/// ## Examples +/// +/// We don't test this example automatically at present, but it has and can +/// been tested against a Tendermint node running on `localhost`. +/// +/// ```ignore +/// use tendermint_rpc::{HttpWebSocketClient, FullClient, MinimalClient}; +/// use futures::StreamExt; +/// +/// #[tokio::main] +/// async fn main() { +/// let mut client = HttpWebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) +/// .await +/// .unwrap(); +/// +/// let mut subs = client.subscribe("tm.event='NewBlock'".to_string()) +/// .await +/// .unwrap(); +/// +/// // Grab 5 NewBlock events +/// let mut ev_count = 5_i32; +/// +/// while let Some(ev) = subs.next().await { +/// println!("Got event: {:?}", ev); +/// ev_count -= 1; +/// if ev_count < 0 { +/// break +/// } +/// } +/// +/// client.unsubscribe(subs).await.unwrap(); +/// client.close().await.unwrap(); +/// } +/// ``` +/// +/// [`Event`]: ./event/struct.Event.html +/// #[derive(Debug)] pub struct HttpWebSocketClient { host: String, diff --git a/rpc/src/endpoint/unsubscribe.rs b/rpc/src/endpoint/unsubscribe.rs index 83afbfc39..9bcc902ed 100644 --- a/rpc/src/endpoint/unsubscribe.rs +++ b/rpc/src/endpoint/unsubscribe.rs @@ -20,7 +20,7 @@ impl crate::Request for Request { type Response = Response; fn method(&self) -> crate::Method { - crate::Method::Subscribe + crate::Method::Unsubscribe } } diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 5ac9fa168..22a467684 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -1,9 +1,12 @@ -//! Tendermint RPC definitons and types. +//! Tendermint RPC definitions and types. #[cfg(feature = "client")] mod client; #[cfg(feature = "client")] -pub use client::{FullClient, MinimalClient, Subscription, SubscriptionId, SubscriptionRouter}; +pub use client::{ + FullClient, MinimalClient, Subscription, SubscriptionId, SubscriptionRouter, + DEFAULT_SUBSCRIPTION_BUF_SIZE, +}; #[cfg(feature = "http_ws")] pub use client::{HttpClient, HttpWebSocketClient}; From d3d2c32777701d2ce6a81edbbe02416c965ccf6a Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 14 Aug 2020 12:42:48 -0400 Subject: [PATCH 25/60] Fix links in method docs Signed-off-by: Thane Thomson --- rpc/src/client/transport/http_ws.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index 81badf003..d9302b8c6 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -156,9 +156,14 @@ impl HttpWebSocketClient { }) } - /// In the absence of an `async` version of [`std::convert::TryFrom`], - /// this constructor provides an `async` way to upgrade an [`HttpClient`] - /// to an [`HttpWebSocketClient`]. + /// In the absence of an `async` version of [`TryFrom`], this constructor + /// provides an `async` way to upgrade an [`HttpClient`] to an + /// [`HttpWebSocketClient`]. + /// + /// [`TryFrom`]: https://doc.rust-lang.org/std/convert/trait.TryFrom.html + /// [`HttpClient`]: struct.HttpClient.html + /// [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html + /// pub async fn try_from(client: HttpClient) -> Result { HttpWebSocketClient::new(net::Address::Tcp { peer_id: None, From f01d13d22f2393c7841c21f054e0c43f7aab6b5b Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 14 Aug 2020 12:52:35 -0400 Subject: [PATCH 26/60] Expose client docs in base RPC package Signed-off-by: Thane Thomson --- rpc/src/client.rs | 23 ----------------------- rpc/src/lib.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 67dac5412..9ec03ff38 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -1,27 +1,4 @@ //! Tendermint RPC client. -//! -//! The RPC client comes in two flavors: a [`MinimalClient`] and a -//! [`FullClient`]. A `MinimalClient` implementation provides access to all -//! RPC endpoints with the exception of the [`Event`] subscription ones, -//! whereas a `FullClient` implementation provides access to all RPC -//! functionality. The reason for this distinction is because `Event` -//! subscription usually requires more resources to manage, and may not be -//! necessary for all applications making use of the Tendermint RPC. -//! -//! This is only available when specifying the `client` feature flag. -//! Transport-specific client support is provided by way of additional feature -//! flags (where right now we only have one transport, but intend on providing -//! more in future): -//! -//! * `http_ws`: Provides an HTTP interface for request/response interactions -//! (see [`HttpClient`]), and a WebSocket-based interface for `Event` -//! subscription (see [`HttpWebSocketClient`]). -//! -//! [`MinimalClient`]: trait.MinimalClient.html -//! [`FullClient`]: trait.FullClient.html -//! [`Event`]: event/struct.Event.html -//! [`HttpClient`]: struct.HttpClient.html -//! [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html mod subscription; pub use subscription::{Subscription, SubscriptionId, SubscriptionRouter}; diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 22a467684..5bed6922a 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -1,4 +1,30 @@ //! Tendermint RPC definitions and types. +//! +//! ## Client +//! +//! Available when specifying the `client` feature flag. +//! +//! The RPC client comes in two flavors: a [`MinimalClient`] and a +//! [`FullClient`]. A `MinimalClient` implementation provides access to all +//! RPC endpoints with the exception of the [`Event`] subscription ones, +//! whereas a `FullClient` implementation provides access to all RPC +//! functionality. The reason for this distinction is because `Event` +//! subscription usually requires more resources to manage, and may not be +//! necessary for all applications making use of the Tendermint RPC. +//! +//! Transport-specific client support is provided by way of additional feature +//! flags (where right now we only have one transport, but intend on providing +//! more in future): +//! +//! * `http_ws`: Provides an HTTP interface for request/response interactions +//! (see [`HttpClient`]), and a WebSocket-based interface for `Event` +//! subscription (see [`HttpWebSocketClient`]). +//! +//! [`MinimalClient`]: trait.MinimalClient.html +//! [`FullClient`]: trait.FullClient.html +//! [`Event`]: event/struct.Event.html +//! [`HttpClient`]: struct.HttpClient.html +//! [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html #[cfg(feature = "client")] mod client; From 279913b0f06b5ebe2f7a19a5e2b36a6434195961 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sun, 16 Aug 2020 12:01:44 -0400 Subject: [PATCH 27/60] Refactor to allow for remote errors The code at the previous commit causes our current integration test on GitHub to hang because it doesn't know how to handle errors from the remote WebSocket endpoint, and also doesn't know how to handle deserialization failures (caused by protocol/serialization format changes). This commit introduces code that allows us to track pending subscribe/unsubscribe requests asynchronously, handling errors better at that stage of the process, as well as handle subscription format problems more effectively without hanging the client process. Signed-off-by: Thane Thomson --- rpc/src/client.rs | 4 +- rpc/src/client/subscription.rs | 263 ++++++++++++++++++++++++---- rpc/src/client/transport/http_ws.rs | 194 ++++++++++++++------ rpc/src/error.rs | 10 ++ rpc/src/lib.rs | 4 +- rpc/src/request.rs | 23 ++- rpc/tests/client.rs | 24 +-- tendermint/tests/integration.rs | 3 +- 8 files changed, 419 insertions(+), 106 deletions(-) diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 9ec03ff38..74b48d042 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -1,7 +1,9 @@ //! Tendermint RPC client. mod subscription; -pub use subscription::{Subscription, SubscriptionId, SubscriptionRouter}; +pub use subscription::{ + EventRx, EventTx, PendingResultTx, Subscription, SubscriptionId, SubscriptionRouter, +}; mod transport; #[cfg(feature = "http_ws")] diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 52c66f020..1f9f495ef 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -1,11 +1,18 @@ //! Subscription- and subscription management-related functionality. use crate::event::Event; +use crate::{Error, Id, Result}; use futures::task::{Context, Poll}; use futures::Stream; +use getrandom::getrandom; use std::collections::HashMap; +use std::convert::TryInto; use std::pin::Pin; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, oneshot}; + +pub type EventRx = mpsc::Receiver>; +pub type EventTx = mpsc::Sender>; +pub type PendingResultTx = oneshot::Sender>; /// An interface that can be used to asynchronously receive events for a /// particular subscription. @@ -13,11 +20,11 @@ use tokio::sync::mpsc; pub struct Subscription { pub query: String, id: SubscriptionId, - event_rx: mpsc::Receiver, + event_rx: EventRx, } impl Stream for Subscription { - type Item = Event; + type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.event_rx.poll_recv(cx) @@ -25,7 +32,7 @@ impl Stream for Subscription { } impl Subscription { - pub fn new(id: SubscriptionId, query: String, event_rx: mpsc::Receiver) -> Self { + pub fn new(id: SubscriptionId, query: String, event_rx: EventRx) -> Self { Self { id, query, @@ -36,42 +43,86 @@ impl Subscription { /// Each new subscription is automatically assigned an ID. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SubscriptionId(usize); +pub struct SubscriptionId(String); impl Default for SubscriptionId { fn default() -> Self { - Self(0) + 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 SubscriptionId { - /// Advances this subscription ID to the next logical ID, returning the - /// previous one. - pub fn advance(&mut self) -> SubscriptionId { - let cur_id = self.clone(); - self.0 += 1; - cur_id +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_error( + "cannot convert an empty JSONRPC ID into a subscription ID", + )), + } } } +#[derive(Debug)] +struct PendingSubscribe { + query: String, + event_tx: EventTx, + result_tx: PendingResultTx, +} + +#[derive(Debug)] +struct PendingUnsubscribe { + subscription: Subscription, + result_tx: PendingResultTx, +} + +#[derive(Debug, Clone)] +pub enum SubscriptionState { + Pending, + Active, + Cancelling, + NotFound, +} + /// Provides a mechanism for tracking subscriptions and routing events to those /// subscriptions. This is useful when implementing your own RPC client /// transport layer. -#[derive(Debug, Clone)] -pub struct SubscriptionRouter(HashMap>>); +#[derive(Debug)] +pub struct SubscriptionRouter { + subscriptions: HashMap>, + pending_subscribe: HashMap, + pending_unsubscribe: HashMap, +} impl SubscriptionRouter { - /// Create a new empty `SubscriptionRouter`. - pub fn new() -> Self { - Self(HashMap::new()) - } - /// 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.0.get_mut(&ev.query) { + let subs_for_query = match self.subscriptions.get_mut(&ev.query) { Some(s) => s, None => return, }; @@ -80,40 +131,190 @@ impl SubscriptionRouter { // TODO(thane): Right now we automatically remove any disconnected // or full channels. We must handle full channels // differently to disconnected ones. - if event_tx.send(ev.clone()).await.is_err() { + if event_tx.send(Ok(ev.clone())).await.is_err() { disconnected.push(id.clone()); } } - let subs_for_query = self.0.get_mut(&ev.query).unwrap(); + let subs_for_query = self.subscriptions.get_mut(&ev.query).unwrap(); for id in disconnected { subs_for_query.remove(&id); } } - /// Keep track of a new subscription for a particular query. - pub fn add(&mut self, id: SubscriptionId, query: String, event_tx: mpsc::Sender) { - let subs_for_query = match self.0.get_mut(&query) { + /// Immediately add a new subscription to the router without waiting for + /// confirmation. + pub fn add(&mut self, id: SubscriptionId, query: String, event_tx: EventTx) { + let subs_for_query = match self.subscriptions.get_mut(&query) { Some(s) => s, None => { - self.0.insert(query.clone(), HashMap::new()); - self.0.get_mut(&query).unwrap() + self.subscriptions.insert(query.clone(), HashMap::new()); + self.subscriptions.get_mut(&query).unwrap() } }; subs_for_query.insert(id, event_tx); } - /// Remove the given subscription and consume it. + /// Keep track of a pending subscription, which can either be confirmed or + /// cancelled. + pub fn add_pending_subscribe( + &mut self, + id: SubscriptionId, + query: String, + event_tx: EventTx, + result_tx: PendingResultTx, + ) { + self.pending_subscribe.insert( + id, + PendingSubscribe { + query, + event_tx, + result_tx, + }, + ); + } + + /// Attempts to confirm the pending subscription with the given ID. + /// + /// Returns an error if it fails to respond (through the internal `oneshot` + /// channel) to the original caller to indicate success. + pub fn confirm_pending_subscribe(&mut self, id: &SubscriptionId) -> Result<()> { + match self.pending_subscribe.remove(id) { + Some(pending_subscribe) => { + self.add( + id.clone(), + pending_subscribe.query.clone(), + pending_subscribe.event_tx, + ); + Ok(pending_subscribe.result_tx.send(Ok(())).map_err(|_| { + Error::client_error(format!( + "failed to communicate result of pending subscription with ID: {}", + id + )) + })?) + } + 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 fn cancel_pending_subscribe( + &mut self, + id: &SubscriptionId, + err: impl Into, + ) -> Result<()> { + match self.pending_subscribe.remove(id) { + Some(pending_subscribe) => Ok(pending_subscribe + .result_tx + .send(Err(err.into())) + .map_err(|_| { + Error::client_error(format!( + "failed to communicate result of pending subscription with ID: {}", + id + )) + })?), + None => Ok(()), + } + } + + /// Immediately remove the given subscription and consume it. pub fn remove(&mut self, subs: Subscription) { - let subs_for_query = match self.0.get_mut(&subs.query) { + let subs_for_query = match self.subscriptions.get_mut(&subs.query) { Some(s) => s, None => return, }; subs_for_query.remove(&subs.id); } + + /// Keeps track of a pending unsubscribe request, which can either be + /// confirmed or cancelled. + pub fn add_pending_unsubscribe(&mut self, subs: Subscription, result_tx: PendingResultTx) { + self.pending_unsubscribe.insert( + subs.id.clone(), + PendingUnsubscribe { + subscription: subs, + result_tx, + }, + ); + } + + /// Confirm the pending unsubscribe request for the subscription with the + /// given ID. + pub fn confirm_pending_unsubscribe(&mut self, id: &SubscriptionId) -> Result<()> { + match self.pending_unsubscribe.remove(id) { + Some(pending_unsubscribe) => { + let (subscription, result_tx) = ( + pending_unsubscribe.subscription, + pending_unsubscribe.result_tx, + ); + self.remove(subscription); + Ok(result_tx.send(Ok(())).map_err(|_| { + Error::client_error(format!( + "failed to communicate result of pending unsubscribe for subscription with ID: {}", + id + )) + })?) + } + None => Ok(()), + } + } + + /// Cancel the pending unsubscribe request for the subscription with the + /// given ID, responding with the given error. + pub fn cancel_pending_unsubscribe( + &mut self, + id: &SubscriptionId, + err: impl Into, + ) -> Result<()> { + match self.pending_unsubscribe.remove(id) { + Some(pending_unsubscribe) => { + Ok(pending_unsubscribe.result_tx.send(Err(err.into())).map_err(|_| { + Error::client_error(format!( + "failed to communicate result of pending unsubscribe for subscription with ID: {}", + id + )) + })?) + } + None => Ok(()), + } + } + + pub fn is_active(&self, id: &SubscriptionId) -> bool { + self.subscriptions + .iter() + .any(|(_query, subs_for_query)| subs_for_query.contains_key(id)) + } + + pub fn get_active_subscription_mut(&mut self, id: &SubscriptionId) -> Option<&mut EventTx> { + self.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, id: &SubscriptionId) -> SubscriptionState { + if self.pending_subscribe.contains_key(id) { + return SubscriptionState::Pending; + } + if self.pending_unsubscribe.contains_key(id) { + return SubscriptionState::Cancelling; + } + if self.is_active(id) { + return SubscriptionState::Active; + } + SubscriptionState::NotFound + } } impl Default for SubscriptionRouter { fn default() -> Self { - Self::new() + Self { + subscriptions: HashMap::new(), + pending_subscribe: HashMap::new(), + pending_unsubscribe: HashMap::new(), + } } } diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index d9302b8c6..b8c39c548 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -3,9 +3,11 @@ //! //! The `client` and `http_ws` features are required to use this module. +use crate::client::subscription::{EventTx, PendingResultTx, SubscriptionState}; use crate::client::{Subscription, SubscriptionId, SubscriptionRouter}; use crate::endpoint::{subscribe, unsubscribe}; use crate::event::Event; +use crate::{request, response}; use crate::{Error, FullClient, MinimalClient, Request, Response, Result}; use async_trait::async_trait; use async_tungstenite::tokio::{connect_async, TokioAdapter}; @@ -16,7 +18,9 @@ use async_tungstenite::WebSocketStream; use bytes::buf::BufExt; use futures::{SinkExt, StreamExt}; use hyper::header; +use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use std::convert::TryInto; use tendermint::net; use tokio::net::TcpStream; use tokio::sync::{mpsc, oneshot}; @@ -28,6 +32,7 @@ use tokio::task::JoinHandle; const DEFAULT_WEBSOCKET_CMD_BUF_SIZE: usize = 20; /// An HTTP-based Tendermint RPC client (a [`MinimalClient`] implementation). +/// Requires features `client` and `http_ws`. /// /// Does not provide [`Event`] subscription facilities (see /// [`HttpWebSocketClient`] for a client that does provide `Event` subscription @@ -86,7 +91,8 @@ impl HttpClient { } } -/// An HTTP- and WebSocket-based Tendermint RPC client. +/// An HTTP- and WebSocket-based Tendermint RPC client. Requires features +/// `client` and `http_ws`. /// /// HTTP is used for all requests except those pertaining to [`Event`] /// subscription. `Event` subscription is facilitated by a WebSocket @@ -197,7 +203,7 @@ impl MinimalClient for HttpWebSocketClient { async fn close(mut self) -> Result<()> { self.send_cmd(WebSocketDriverCmd::Close).await?; self.driver_handle.await.map_err(|e| { - Error::internal_error(format!("failed to join client driver async task: {}", e)) + Error::client_error(format!("failed to join client driver async task: {}", e)) })? } } @@ -210,19 +216,27 @@ impl FullClient for HttpWebSocketClient { buf_size: usize, ) -> Result { let (event_tx, event_rx) = mpsc::channel(buf_size); - let (response_tx, response_rx) = oneshot::channel(); - let id = self.next_subscription_id.advance(); + let (result_tx, result_rx) = oneshot::channel(); + // We use the same ID for our subscription as for the JSONRPC request + // so that we can correlate incoming RPC responses with specific + // subscriptions. We use this to establish whether or not a + // subscription request was successful, as well as whether we support + // the remote endpoint's serialization format. + let id = SubscriptionId::default(); + let req = request::Wrapper::new_with_id( + id.clone().into(), + subscribe::Request::new(query.clone()), + ); self.send_cmd(WebSocketDriverCmd::Subscribe { - id: id.clone(), - query: query.clone(), + req, event_tx, - response_tx, + result_tx, }) .await?; // Wait to make sure our subscription request went through // successfully. - response_rx.await.map_err(|e| { - Error::internal_error(format!( + result_rx.await.map_err(|e| { + Error::client_error(format!( "failed to receive response from client driver for subscription request: {}", e )) @@ -231,14 +245,27 @@ impl FullClient for HttpWebSocketClient { } async fn unsubscribe(&mut self, subscription: Subscription) -> Result<()> { - // TODO(thane): Should we insist on a response here to ensure the - // subscription was actually terminated? Right now this is - // just fire-and-forget. - self.send_cmd(WebSocketDriverCmd::Unsubscribe(subscription)) - .await + let (result_tx, result_rx) = oneshot::channel(); + self.send_cmd(WebSocketDriverCmd::Unsubscribe { + req: request::Wrapper::new(unsubscribe::Request::new(subscription.query.clone())), + subscription, + result_tx, + }) + .await?; + result_rx.await.map_err(|e| { + Error::client_error(format!( + "failed to receive response from client driver for unsubscribe request: {}", + e + )) + })? } } +#[derive(Serialize, Deserialize, Debug, Clone)] +struct GenericJSONResponse(serde_json::Value); + +impl Response for GenericJSONResponse {} + #[derive(Debug)] struct WebSocketSubscriptionDriver { stream: WebSocketStream>, @@ -253,7 +280,7 @@ impl WebSocketSubscriptionDriver { ) -> Self { Self { stream, - router: SubscriptionRouter::new(), + router: SubscriptionRouter::default(), cmd_rx, } } @@ -273,12 +300,13 @@ impl WebSocketSubscriptionDriver { }, Some(cmd) = self.cmd_rx.next() => match cmd { WebSocketDriverCmd::Subscribe { - id, - query, + req, event_tx, - response_tx, - } => self.subscribe(id, query, event_tx, response_tx).await?, - WebSocketDriverCmd::Unsubscribe(subscription) => self.unsubscribe(subscription).await?, + result_tx, + } => self.subscribe(req, event_tx, result_tx).await?, + WebSocketDriverCmd::Unsubscribe { req, subscription, result_tx } => { + self.unsubscribe(req, subscription, result_tx).await? + } WebSocketDriverCmd::Close => return self.close().await, } } @@ -293,43 +321,54 @@ impl WebSocketSubscriptionDriver { async fn subscribe( &mut self, - id: SubscriptionId, - query: String, - event_tx: mpsc::Sender, - response_tx: oneshot::Sender>, + req: request::Wrapper, + event_tx: EventTx, + result_tx: PendingResultTx, ) -> Result<()> { + // We require the outgoing request to have an ID that we can use as the + // subscription ID. + let subs_id = match req.id().clone().try_into() { + Ok(id) => id, + Err(e) => { + let _ = result_tx.send(Err(e)); + return Ok(()); + } + }; if let Err(e) = self - .send(Message::Text( - subscribe::Request::new(query.clone()).into_json(), - )) + .send(Message::Text(serde_json::to_string_pretty(&req).unwrap())) .await { - if response_tx.send(Err(e)).is_err() { - return Err(Error::internal_error( + if result_tx.send(Err(e)).is_err() { + return Err(Error::client_error( "failed to respond internally to subscription request", )); } // One failure shouldn't bring down the entire client. return Ok(()); } - // TODO(thane): Should we wait for a response from the remote endpoint? - self.router.add(id, query, event_tx); - // TODO(thane): How do we deal with the case where the following - // response fails? - if response_tx.send(Ok(())).is_err() { - return Err(Error::internal_error( - "failed to respond internally to subscription request", - )); - } + self.router + .add_pending_subscribe(subs_id, req.params().query.clone(), event_tx, result_tx); Ok(()) } - async fn unsubscribe(&mut self, subs: Subscription) -> Result<()> { - self.send(Message::Text( - unsubscribe::Request::new(subs.query.clone()).into_json(), - )) - .await?; - self.router.remove(subs); + async fn unsubscribe( + &mut self, + req: request::Wrapper, + subscription: Subscription, + result_tx: PendingResultTx, + ) -> Result<()> { + if let Err(e) = self + .send(Message::Text(serde_json::to_string_pretty(&req).unwrap())) + .await + { + if result_tx.send(Err(e)).is_err() { + return Err(Error::client_error( + "failed to respond internally to unsubscribe request", + )); + } + return Ok(()); + } + self.router.add_pending_unsubscribe(subscription, result_tx); Ok(()) } @@ -343,20 +382,60 @@ impl WebSocketSubscriptionDriver { } async fn handle_text_msg(&mut self, msg: String) -> Result<()> { - match Event::from_string(msg) { + match Event::from_string(&msg) { Ok(ev) => { self.router.publish(ev).await; Ok(()) } - // TODO(thane): Should we just ignore messages we can't - // deserialize? There are a number of possible - // messages we may receive from the WebSocket endpoint - // that we'll end up ignoring anyways (like responses - // for subscribe/unsubscribe requests). - Err(_) => Ok(()), + Err(_) => match serde_json::from_str::>(&msg) { + Ok(wrapper) => self.handle_generic_response(wrapper).await, + _ => Ok(()), + }, } } + async fn handle_generic_response( + &mut self, + wrapper: response::Wrapper, + ) -> Result<()> { + let subs_id: SubscriptionId = match wrapper.id().clone().try_into() { + Ok(id) => id, + // Just ignore the message if it doesn't have an intelligible ID. + Err(_) => return Ok(()), + }; + match wrapper.into_result() { + Ok(_) => match self.router.subscription_state(&subs_id) { + SubscriptionState::Pending => { + let _ = self.router.confirm_pending_subscribe(&subs_id); + } + SubscriptionState::Cancelling => { + let _ = self.router.confirm_pending_unsubscribe(&subs_id); + } + _ => (), + }, + Err(e) => match self.router.subscription_state(&subs_id) { + SubscriptionState::Pending => { + let _ = self.router.cancel_pending_subscribe(&subs_id, e); + } + SubscriptionState::Cancelling => { + let _ = self.router.cancel_pending_unsubscribe(&subs_id, e); + } + // 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(&subs_id) { + // TODO(thane): Does an error here warrant terminating the subscription, or the driver? + let _ = event_tx.send(Err(e)).await; + } + } + SubscriptionState::NotFound => (), + }, + } + + Ok(()) + } + async fn pong(&mut self, v: Vec) -> Result<()> { self.send(Message::Pong(v)).await } @@ -380,12 +459,15 @@ impl WebSocketSubscriptionDriver { #[derive(Debug)] enum WebSocketDriverCmd { Subscribe { - id: SubscriptionId, - query: String, - event_tx: mpsc::Sender, - response_tx: oneshot::Sender>, + req: request::Wrapper, + event_tx: mpsc::Sender>, + result_tx: oneshot::Sender>, + }, + Unsubscribe { + req: request::Wrapper, + subscription: Subscription, + result_tx: oneshot::Sender>, }, - Unsubscribe(Subscription), Close, } diff --git a/rpc/src/error.rs b/rpc/src/error.rs index fd01e886b..51ab53438 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -77,6 +77,10 @@ impl Error { Error::new(Code::InternalError, Some(cause.into())) } + pub fn client_error(cause: impl Into) -> Error { + Error::new(Code::ClientError, Some(cause.into())) + } + /// Obtain the `rpc::error::Code` for this error pub fn code(&self) -> Code { self.code @@ -143,6 +147,10 @@ pub enum Code { #[error("Websocket Error")] WebSocketError, + /// The client encountered an error. + #[error("Client error")] + ClientError, + /// Parse error i.e. invalid JSON (-32700) #[error("Parse error. Invalid JSON")] ParseError, @@ -184,6 +192,7 @@ impl From for Code { match value { 0 => Code::HttpError, 1 => Code::WebSocketError, + 2 => Code::ClientError, -32700 => Code::ParseError, -32600 => Code::InvalidRequest, -32601 => Code::MethodNotFound, @@ -200,6 +209,7 @@ impl From for i32 { match code { Code::HttpError => 0, Code::WebSocketError => 1, + Code::ClientError => 2, Code::ParseError => -32700, Code::InvalidRequest => -32600, Code::MethodNotFound => -32601, diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 5bed6922a..80d58cbf5 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -30,8 +30,8 @@ mod client; #[cfg(feature = "client")] pub use client::{ - FullClient, MinimalClient, Subscription, SubscriptionId, SubscriptionRouter, - DEFAULT_SUBSCRIPTION_BUF_SIZE, + EventRx, EventTx, FullClient, MinimalClient, PendingResultTx, Subscription, SubscriptionId, + SubscriptionRouter, DEFAULT_SUBSCRIPTION_BUF_SIZE, }; #[cfg(feature = "http_ws")] pub use client::{HttpClient, HttpWebSocketClient}; diff --git a/rpc/src/request.rs b/rpc/src/request.rs index 01ce5934a..4c5e86c21 100644 --- a/rpc/src/request.rs +++ b/rpc/src/request.rs @@ -20,7 +20,7 @@ pub trait Request: Debug + DeserializeOwned + Serialize + Sized + Send { /// JSONRPC request wrapper (i.e. message envelope) #[derive(Debug, Deserialize, Serialize)] -struct Wrapper { +pub struct Wrapper { /// JSONRPC version jsonrpc: Version, @@ -38,13 +38,30 @@ impl Wrapper where R: Request, { - /// Create a new request wrapper from the given request + /// Create a new request wrapper from the given request. + /// + /// By default this sets the ID of the request to a random [UUIDv4] value. + /// + /// [UUIDv4]: https://en.wikipedia.org/wiki/Universally_unique_identifier pub fn new(request: R) -> Self { + Wrapper::new_with_id(Id::uuid_v4(), request) + } + + /// Create a new request wrapper with a custom JSONRPC request ID. + pub fn new_with_id(id: Id, request: R) -> Self { Self { jsonrpc: Version::current(), - id: Id::uuid_v4(), + id, method: request.method(), params: request, } } + + pub fn id(&self) -> &Id { + &self.id + } + + pub fn params(&self) -> &R { + &self.params + } } diff --git a/rpc/tests/client.rs b/rpc/tests/client.rs index 5abdc6114..2af133edf 100644 --- a/rpc/tests/client.rs +++ b/rpc/tests/client.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use tendermint::block::Height; use tendermint_rpc::event::{Event, EventData, WrappedEvent}; use tendermint_rpc::{ - Error, FullClient, Method, MinimalClient, Request, Response, Result, Subscription, + Error, EventTx, FullClient, Method, MinimalClient, Request, Response, Result, Subscription, SubscriptionId, SubscriptionRouter, }; use tokio::fs; @@ -33,7 +33,6 @@ struct MockClient { driver_handle: JoinHandle>, event_tx: mpsc::Sender, cmd_tx: mpsc::Sender, - next_subs_id: SubscriptionId, } #[async_trait] @@ -68,7 +67,7 @@ impl FullClient for MockClient { ) -> Result { let (event_tx, event_rx) = mpsc::channel(buf_size); let (response_tx, response_rx) = oneshot::channel(); - let id = self.next_subs_id.advance(); + let id = SubscriptionId::default(); self.cmd_tx .send(MockClientCmd::Subscribe { id: id.clone(), @@ -104,7 +103,6 @@ impl MockClient { driver_handle: driver_hdl, event_tx, cmd_tx, - next_subs_id: SubscriptionId::default(), } } @@ -130,7 +128,7 @@ enum MockClientCmd { Subscribe { id: SubscriptionId, query: String, - event_tx: mpsc::Sender, + event_tx: EventTx, response_tx: oneshot::Sender>, }, Unsubscribe(Subscription), @@ -151,7 +149,7 @@ impl MockClientDriver { Self { event_rx, cmd_rx, - router: SubscriptionRouter::new(), + router: SubscriptionRouter::default(), } } @@ -222,9 +220,9 @@ async fn full_client() { .unwrap(); let subs1_events_task = - tokio::spawn(async move { subs1.take(3).collect::>().await }); + tokio::spawn(async move { subs1.take(3).collect::>>().await }); let subs2_events_task = - tokio::spawn(async move { subs2.take(3).collect::>().await }); + tokio::spawn(async move { subs2.take(3).collect::>>().await }); println!("Publishing incoming events..."); for ev in incoming_events { @@ -242,17 +240,19 @@ async fn full_client() { println!("Checking collected events..."); for i in 0..3 { - match &subs1_events[i].data { + let subs1_event = subs1_events[i].as_ref().unwrap(); + let subs2_event = subs2_events[i].as_ref().unwrap(); + match &subs1_event.data { EventData::NewBlock { block, .. } => { assert_eq!(expected_heights[i], block.as_ref().unwrap().header.height); } - _ => panic!("invalid event type for subs1: {:?}", subs1_events[i]), + _ => panic!("invalid event type for subs1: {:?}", subs1_event), } - match &subs2_events[i].data { + match &subs2_event.data { EventData::NewBlock { block, .. } => { assert_eq!(expected_heights[i], block.as_ref().unwrap().header.height); } - _ => panic!("invalid event type for subs2: {:?}", subs2_events[i]), + _ => panic!("invalid event type for subs2: {:?}", subs2_event), } } } diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index efb5154c8..026834ff1 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -159,7 +159,8 @@ mod rpc { let mut ev_count = 5_i32; dbg!("Attempting to grab {} new blocks", ev_count); - while let Some(ev) = subs.next().await { + while let Some(res) = subs.next().await { + let ev = res.unwrap(); dbg!("Got event: {:?}", ev); ev_count -= 1; if ev_count < 0 { From 6d3518097666d7c1092154e7af072f22b8ef7f1e Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sun, 16 Aug 2020 12:12:11 -0400 Subject: [PATCH 28/60] Correctly produce a subscription error for possible RPC protocol mismatch Signed-off-by: Thane Thomson --- rpc/src/client/transport/http_ws.rs | 11 ++++++++++- tendermint/tests/integration.rs | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index b8c39c548..bd3429ff9 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -411,7 +411,16 @@ impl WebSocketSubscriptionDriver { SubscriptionState::Cancelling => { let _ = self.router.confirm_pending_unsubscribe(&subs_id); } - _ => (), + SubscriptionState::Active => { + if let Some(event_tx) = self.router.get_active_subscription_mut(&subs_id) { + 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; + } + } + SubscriptionState::NotFound => (), }, Err(e) => match self.router.subscription_state(&subs_id) { SubscriptionState::Pending => { diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index 026834ff1..199ac51bf 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -158,10 +158,10 @@ mod rpc { .unwrap(); let mut ev_count = 5_i32; - dbg!("Attempting to grab {} new blocks", ev_count); + println!("Attempting to grab {} new blocks", ev_count); while let Some(res) = subs.next().await { let ev = res.unwrap(); - dbg!("Got event: {:?}", ev); + println!("Got event: {:?}", ev); ev_count -= 1; if ev_count < 0 { break; From 2228b7149d838df7f6c18348723141d737168add Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sun, 16 Aug 2020 12:14:36 -0400 Subject: [PATCH 29/60] Remove unnecessary field Signed-off-by: Thane Thomson --- rpc/src/client/transport/http_ws.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index bd3429ff9..0ae4c03b4 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -141,7 +141,6 @@ pub struct HttpWebSocketClient { port: u16, driver_handle: JoinHandle>, cmd_tx: mpsc::Sender, - next_subscription_id: SubscriptionId, } impl HttpWebSocketClient { @@ -158,7 +157,6 @@ impl HttpWebSocketClient { port, driver_handle, cmd_tx, - next_subscription_id: SubscriptionId::default(), }) } From 8bc9f7f39dffd4bcb4b49d3e1dbc1470f5932d25 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sun, 16 Aug 2020 12:28:43 -0400 Subject: [PATCH 30/60] Fix unsubscribe mechanism Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 2 +- rpc/src/client/transport/http_ws.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 1f9f495ef..67cf88977 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -19,7 +19,7 @@ pub type PendingResultTx = oneshot::Sender>; #[derive(Debug)] pub struct Subscription { pub query: String, - id: SubscriptionId, + pub id: SubscriptionId, event_rx: EventRx, } diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index 0ae4c03b4..3b6376bdc 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -120,7 +120,8 @@ impl HttpClient { /// // Grab 5 NewBlock events /// let mut ev_count = 5_i32; /// -/// while let Some(ev) = subs.next().await { +/// while let Some(res) = subs.next().await { +/// let ev = res.unwrap(); /// println!("Got event: {:?}", ev); /// ev_count -= 1; /// if ev_count < 0 { @@ -245,7 +246,10 @@ impl FullClient for HttpWebSocketClient { async fn unsubscribe(&mut self, subscription: Subscription) -> Result<()> { let (result_tx, result_rx) = oneshot::channel(); self.send_cmd(WebSocketDriverCmd::Unsubscribe { - req: request::Wrapper::new(unsubscribe::Request::new(subscription.query.clone())), + req: request::Wrapper::new_with_id( + subscription.id.clone().into(), + unsubscribe::Request::new(subscription.query.clone()), + ), subscription, result_tx, }) From f017d2720c4554565940a31ef4ccb5c6122e3ecb Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sun, 16 Aug 2020 12:32:04 -0400 Subject: [PATCH 31/60] Fix failing doctest Signed-off-by: Thane Thomson --- tendermint/src/serializers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tendermint/src/serializers.rs b/tendermint/src/serializers.rs index 6d98835f5..6ad3e36a0 100644 --- a/tendermint/src/serializers.rs +++ b/tendermint/src/serializers.rs @@ -12,7 +12,7 @@ //! This example shows how to serialize Vec into different types of strings: //! ```ignore //! use serde::{Serialize, Deserialize}; -//! use serializers; +//! use tendermint::serializers; //! //! #[derive(Serialize, Deserialize)] //! struct ByteTypes { From 233fd62ec61aabc4bb41260715e4e3a2985faf89 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sun, 16 Aug 2020 17:30:41 -0400 Subject: [PATCH 32/60] Allow for PartialEq comparisons (to aid in testing) Signed-off-by: Thane Thomson --- tendermint/src/abci/responses.rs | 4 ++-- tendermint/src/abci/tag.rs | 2 +- tendermint/src/abci/transaction.rs | 2 +- tendermint/src/block.rs | 2 +- tendermint/src/evidence.rs | 2 +- tendermint/src/validator.rs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tendermint/src/abci/responses.rs b/tendermint/src/abci/responses.rs index c82414857..24fc79d1a 100644 --- a/tendermint/src/abci/responses.rs +++ b/tendermint/src/abci/responses.rs @@ -91,7 +91,7 @@ pub struct Event { /// /// // TODO(tarcieri): generate this automatically from the proto -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct BeginBlock { /// Tags #[serde(default)] @@ -104,7 +104,7 @@ pub struct BeginBlock { /// /// // TODO(tarcieri): generate this automatically from the proto -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct EndBlock { /// Validator updates #[serde(deserialize_with = "deserialize_validator_updates")] diff --git a/tendermint/src/abci/tag.rs b/tendermint/src/abci/tag.rs index a7b313e5c..347c53e3e 100644 --- a/tendermint/src/abci/tag.rs +++ b/tendermint/src/abci/tag.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use std::{fmt, str::FromStr}; /// Tags -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Tag { /// Key pub key: Key, diff --git a/tendermint/src/abci/transaction.rs b/tendermint/src/abci/transaction.rs index 9add11f71..aaf90a566 100644 --- a/tendermint/src/abci/transaction.rs +++ b/tendermint/src/abci/transaction.rs @@ -61,7 +61,7 @@ impl Serialize for Transaction { /// transactions are arbitrary byte arrays. /// /// -#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Data { txs: Option>, } diff --git a/tendermint/src/block.rs b/tendermint/src/block.rs index e00feeab5..f99922ee9 100644 --- a/tendermint/src/block.rs +++ b/tendermint/src/block.rs @@ -26,7 +26,7 @@ use serde::{Deserialize, Deserializer, Serialize}; /// evidence of malfeasance (i.e. signing conflicting votes). /// /// -#[derive(Deserialize, Serialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] pub struct Block { /// Block header pub header: Header, diff --git a/tendermint/src/evidence.rs b/tendermint/src/evidence.rs index c8c983029..bd9c5adee 100644 --- a/tendermint/src/evidence.rs +++ b/tendermint/src/evidence.rs @@ -53,7 +53,7 @@ impl ConflictingHeadersEvidence { /// Evidence data is a wrapper for a list of `Evidence`. /// /// -#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Data { evidence: Option>, } diff --git a/tendermint/src/validator.rs b/tendermint/src/validator.rs index 30aec3bd0..1e00f78d5 100644 --- a/tendermint/src/validator.rs +++ b/tendermint/src/validator.rs @@ -207,7 +207,7 @@ impl Serialize for ProposerPriority { } /// Updates to the validator set -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Update { /// Validator public key #[serde(deserialize_with = "deserialize_public_key")] From 36e2969cb9985c6e180d5b0021298eae69aeb25a Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sun, 16 Aug 2020 17:31:01 -0400 Subject: [PATCH 33/60] Add tests for SubscriptionRouter Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 168 ++++++++++++++++++++++++++++++++- rpc/src/event.rs | 12 +-- 2 files changed, 173 insertions(+), 7 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 67cf88977..30d78c8a4 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -98,7 +98,7 @@ struct PendingUnsubscribe { result_tx: PendingResultTx, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum SubscriptionState { Pending, Active, @@ -318,3 +318,169 @@ impl Default for SubscriptionRouter { } } } + +#[cfg(test)] +mod test { + use super::*; + use crate::event::{Event, WrappedEvent}; + 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 { + serde_json::from_str::(read_json_fixture(name).await.as_str()) + .unwrap() + .into_result() + .unwrap() + } + + #[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) = mpsc::channel(1); + let (subs2_event_tx, mut subs2_event_rx) = mpsc::channel(1); + let (subs3_event_tx, mut subs3_event_rx) = mpsc::channel(1); + + // Two subscriptions with the same query + router.add(subs1_id.clone(), "query1".into(), subs1_event_tx); + router.add(subs2_id.clone(), "query1".into(), subs2_event_tx); + // Another subscription with a different query + router.add(subs3_id.clone(), "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 = subs1_event_rx.try_recv().unwrap().unwrap(); + let subs2_ev = subs2_event_rx.try_recv().unwrap().unwrap(); + if subs3_event_rx.try_recv().is_ok() { + panic!("should not have received an event here"); + } + assert_eq!(ev, subs1_ev); + assert_eq!(ev, subs2_ev); + + ev.query = "query2".into(); + router.publish(ev.clone()).await; + + if subs1_event_rx.try_recv().is_ok() { + panic!("should not have received an event here"); + } + if subs2_event_rx.try_recv().is_ok() { + panic!("should not have received an event here"); + } + let subs3_ev = subs3_event_rx.try_recv().unwrap().unwrap(); + assert_eq!(ev, subs3_ev); + } + + #[tokio::test] + async fn router_pending_subscription() { + let mut router = SubscriptionRouter::default(); + let subs_id = SubscriptionId::default(); + let (event_tx, mut event_rx) = mpsc::channel(1); + let (result_tx, mut result_rx) = oneshot::channel(); + let query = "query".to_string(); + let mut ev = read_event("event_new_block_1").await; + ev.query = query.clone(); + + assert_eq!( + SubscriptionState::NotFound, + router.subscription_state(&subs_id) + ); + router.add_pending_subscribe(subs_id.clone(), query.clone(), event_tx, result_tx); + assert_eq!( + SubscriptionState::Pending, + router.subscription_state(&subs_id) + ); + router.publish(ev.clone()).await; + if event_rx.try_recv().is_ok() { + panic!("should not have received an event prior to confirming a pending subscription") + } + + router.confirm_pending_subscribe(&subs_id).unwrap(); + assert_eq!( + SubscriptionState::Active, + router.subscription_state(&subs_id) + ); + if event_rx.try_recv().is_ok() { + panic!("should not have received an event here") + } + if result_rx.try_recv().is_err() { + panic!("we should have received successful confirmation of the new subscription") + } + + router.publish(ev.clone()).await; + let received_ev = event_rx.try_recv().unwrap().unwrap(); + assert_eq!(ev, received_ev); + + let (result_tx, mut result_rx) = oneshot::channel(); + router.add_pending_unsubscribe( + Subscription::new(subs_id.clone(), query.clone(), event_rx), + result_tx, + ); + assert_eq!( + SubscriptionState::Cancelling, + router.subscription_state(&subs_id), + ); + + router.confirm_pending_unsubscribe(&subs_id).unwrap(); + assert_eq!( + SubscriptionState::NotFound, + router.subscription_state(&subs_id) + ); + router.publish(ev.clone()).await; + if result_rx.try_recv().is_err() { + panic!("we should have received successful confirmation of the unsubscribe request") + } + } + + #[tokio::test] + async fn router_cancel_pending_subscription() { + let mut router = SubscriptionRouter::default(); + let subs_id = SubscriptionId::default(); + let (event_tx, mut event_rx) = mpsc::channel(1); + let (result_tx, mut result_rx) = oneshot::channel(); + let query = "query".to_string(); + let mut ev = read_event("event_new_block_1").await; + ev.query = query.clone(); + + assert_eq!( + SubscriptionState::NotFound, + router.subscription_state(&subs_id) + ); + router.add_pending_subscribe(subs_id.clone(), query, event_tx, result_tx); + assert_eq!( + SubscriptionState::Pending, + router.subscription_state(&subs_id) + ); + router.publish(ev.clone()).await; + if event_rx.try_recv().is_ok() { + panic!("should not have received an event prior to confirming a pending subscription") + } + + let cancel_error = Error::client_error("cancelled"); + router + .cancel_pending_subscribe(&subs_id, cancel_error.clone()) + .unwrap(); + assert_eq!( + SubscriptionState::NotFound, + router.subscription_state(&subs_id) + ); + assert_eq!(Err(cancel_error), result_rx.try_recv().unwrap()); + + router.publish(ev.clone()).await; + if event_rx.try_recv().is_ok() { + panic!("should not have received an event prior to confirming a pending subscription") + } + } +} diff --git a/rpc/src/event.rs b/rpc/src/event.rs index 9b6311796..2f15ead21 100644 --- a/rpc/src/event.rs +++ b/rpc/src/event.rs @@ -10,7 +10,7 @@ use tendermint::{ use crate::{response::Wrapper, Response}; /// An incoming event produced by a subscription. -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Event { /// The query that produced the event. pub query: String, @@ -24,7 +24,7 @@ impl Response for Event {} /// A JSONRPC-wrapped event. pub type WrappedEvent = Wrapper; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(tag = "type", content = "value")] pub enum EventData { #[serde(alias = "tendermint/event/NewBlock")] @@ -41,7 +41,7 @@ pub enum EventData { } /// Tx Result -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct TxResult { pub height: String, pub index: i64, @@ -50,7 +50,7 @@ pub struct TxResult { } /// TX Results Results -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct TxResultResult { pub log: String, pub gas_wanted: String, @@ -59,7 +59,7 @@ pub struct TxResultResult { } /// Tendermint ABCI Events -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct TmEvent { #[serde(rename = "type")] pub event_type: String, @@ -67,7 +67,7 @@ pub struct TmEvent { } /// Event Attributes -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Attribute { pub key: String, pub value: String, From 83d4835ff03aa0b79b8e9653079c0e334b589650 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sun, 16 Aug 2020 18:53:23 -0400 Subject: [PATCH 34/60] Remove code no longer used Signed-off-by: Thane Thomson --- rpc/src/client/event_listener.rs | 216 ------------------ .../client/test_support/matching_transport.rs | 96 -------- 2 files changed, 312 deletions(-) delete mode 100644 rpc/src/client/event_listener.rs delete mode 100644 rpc/src/client/test_support/matching_transport.rs diff --git a/rpc/src/client/event_listener.rs b/rpc/src/client/event_listener.rs deleted file mode 100644 index 2ad1c19c6..000000000 --- a/rpc/src/client/event_listener.rs +++ /dev/null @@ -1,216 +0,0 @@ -//! Tendermint Websocket event listener client - -// TODO(ismail): document fields or re-use the abci types -#![allow(missing_docs)] - -use async_tungstenite::{tokio::connect_async, tokio::TokioAdapter, tungstenite::Message}; -use futures::prelude::*; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::error::Error as stdError; -use tokio::net::TcpStream; - -use tendermint::block; -use tendermint::net; - -use crate::error::Code; -use crate::response; -use crate::response::Wrapper; -use crate::Request; -use crate::{endpoint::subscribe, Error as RPCError}; - -/// There are only two valid queries to the websocket. A query that subscribes to all transactions -/// and a query that susbscribes to all blocks. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum EventSubscription { - /// Subscribe to all transactions - TransactionSubscription, - ///Subscribe to all blocks - BlockSubscription, -} - -impl EventSubscription { - ///Convert the query enum to a string - pub fn as_str(&self) -> &str { - match self { - EventSubscription::TransactionSubscription => "tm.event='Tx'", - EventSubscription::BlockSubscription => "tm.event='NewBlock'", - } - } -} - -/// Event Listener over websocket. -/// See: -pub struct EventListener { - socket: async_tungstenite::WebSocketStream>, -} - -impl EventListener { - /// Constructor for event listener - pub async fn connect(address: net::Address) -> Result { - let (host, port) = match address { - net::Address::Tcp { host, port, .. } => (host, port), - other => { - return Err(RPCError::invalid_params(&format!( - "invalid RPC address: {:?}", - other - ))); - } - }; - //TODO This doesn't have any way to handle a connection over TLS - let (ws_stream, _unused_tls_stream) = - connect_async(&format!("ws://{}:{}/websocket", host, port)).await?; - Ok(EventListener { socket: ws_stream }) - } - - /// Subscribe to event query stream over the websocket - pub async fn subscribe(&mut self, query: EventSubscription) -> Result<(), Box> { - self.socket - .send(Message::text( - subscribe::Request::new(query.as_str().to_owned()).into_json(), - )) - .await?; - // TODO(ismail): this works if subscriptions are fired sequentially and no event or - // ping message gets in the way: - // Wait for an empty response on subscribe - let msg = self - .socket - .next() - .await - .ok_or_else(|| RPCError::websocket_error("web socket closed"))??; - serde_json::from_str::>(&msg.to_string())?.into_result()?; - - Ok(()) - } - - /// Get the next event from the websocket - pub async fn get_event(&mut self) -> Result, RPCError> { - let msg = self - .socket - .next() - .await - .ok_or_else(|| RPCError::websocket_error("web socket closed"))??; - - if let Ok(result_event) = serde_json::from_str::(&msg.to_string()) { - // if we get an rpc error here, we will bubble it up: - return Ok(Some(result_event.into_result()?)); - } - dbg!("We did not receive a valid JSONRPC wrapped ResultEvent!"); - if serde_json::from_str::(&msg.to_string()).is_ok() { - // FIXME(ismail): Until this is a proper websocket client - // (or the endpoint moved to grpc in tendermint), we accept whatever was read here - // dbg! it out and return None below. - dbg!("Instead of JSONRPC wrapped ResultEvent, we got:"); - dbg!(&msg.to_string()); - return Ok(None); - } - dbg!("received neither event nor generic string message:"); - dbg!(&msg.to_string()); - Err(RPCError::new( - Code::Other(-1), - Some("received neither event nor generic string message".to_string()), - )) - } -} - -// TODO(ismail): this should live somewhere else; these events are also -// published by the event bus independent from RPC. -// We leave it here for now because unsupported types are still -// decodeable via fallthrough variants (GenericJSONEvent). -/// The Event enum is typed events emitted by the Websockets. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(tag = "type", content = "value")] -#[allow(clippy::large_enum_variant)] -pub enum TMEventData { - /// EventDataNewBlock is returned upon subscribing to "tm.event='NewBlock'" - #[serde(alias = "tendermint/event/NewBlock")] - EventDataNewBlock(EventDataNewBlock), - - /// EventDataTx is returned upon subscribing to "tm.event='Tx'" - #[serde(alias = "tendermint/event/Tx")] - EventDataTx(EventDataTx), - - /// Generic event containing json data - GenericJSONEvent( - /// generic event json data - serde_json::Value, - ), -} - -/// Event data from a subscription -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ResultEvent { - /// Query for this result - pub query: String, - /// Tendermint EventData - pub data: TMEventData, - /// Event type and event attributes map - pub events: Option>>, -} -impl response::Response for ResultEvent {} - -/// JSONRPC wrapped ResultEvent -pub type WrappedResultEvent = Wrapper; - -/// TX value -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct EventDataTx { - /// The actual TxResult - #[serde(rename = "TxResult")] - pub tx_result: TxResult, -} - -/// Tx Result -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct TxResult { - pub height: String, - pub index: i64, - pub tx: String, - pub result: TxResultResult, -} - -/// TX Results Results -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct TxResultResult { - pub log: String, - pub gas_wanted: String, - pub gas_used: String, - pub events: Vec, -} -impl response::Response for TxResultResult {} - -/// Tendermint ABCI Events -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct TmEvent { - #[serde(rename = "type")] - pub event_type: String, - pub attributes: Vec, -} -/// Event Attributes -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Attribute { - pub key: String, - pub value: String, -} - -///Block Value -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct EventDataNewBlock { - pub block: Option, - - // TODO(ismail): these should be the same as abci::responses::BeginBlock - // and abci::responses::EndBlock - pub result_begin_block: Option, - pub result_end_block: Option, -} - -/// Begin Block Events -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ResultBeginBlock { - pub events: Option>, -} -///End Block Events -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ResultEndBlock { - pub validator_updates: Option>>, -} diff --git a/rpc/src/client/test_support/matching_transport.rs b/rpc/src/client/test_support/matching_transport.rs deleted file mode 100644 index 031421d01..000000000 --- a/rpc/src/client/test_support/matching_transport.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::client::test_support::Fixture; -use crate::client::transport::{ClosableTransport, Transport}; -use crate::{Error, Method, Request, Response}; -use async_trait::async_trait; - -/// A rudimentary fixture-based transport. -/// -/// Fixtures, if read from the file system, are lazily evaluated. -#[derive(Debug)] -pub struct RequestMatchingTransport { - matchers: Vec, -} - -#[async_trait] -impl Transport for RequestMatchingTransport { - // This transport does not facilitate any subscription mechanism. - type SubscriptionTransport = (); - - async fn request(&self, request: R) -> Result - where - R: Request, - { - for matcher in &self.matchers { - if matcher.matches(&request) { - let response_json = matcher.response()?.read().await; - return R::Response::from_string(response_json); - } - } - Err(Error::internal_error(format!( - "no matcher for request: {:?}", - request - ))) - } - - async fn subscription_transport(&self) -> Result { - unimplemented!() - } -} - -#[async_trait] -impl ClosableTransport for RequestMatchingTransport { - async fn close(self) -> Result<(), Error> { - Ok(()) - } -} - -impl RequestMatchingTransport { - pub fn new(matcher: M) -> Self { - Self { - matchers: vec![matcher], - } - } - - pub fn push(mut self, matcher: M) -> Self { - self.matchers.push(matcher); - self - } -} - -/// Implement this trait to facilitate different kinds of request matching. -pub trait RequestMatcher: Send + Sync + std::fmt::Debug { - /// Does the given request match? - fn matches(&self, request: &R) -> bool - where - R: Request; - - /// The response we need to return if the request matches. - fn response(&self) -> Result; -} - -/// A simple matcher that just returns a specific response every time it gets -/// a request of a particular request method. -#[derive(Debug)] -pub struct MethodMatcher { - method: Method, - response: Result, -} - -impl MethodMatcher { - pub fn new(method: Method, response: Result) -> Self { - Self { method, response } - } -} - -impl RequestMatcher for MethodMatcher { - fn matches(&self, request: &R) -> bool - where - R: Request, - { - return self.method == request.method(); - } - - fn response(&self) -> Result { - self.response.clone() - } -} From ab4ba9b94e0d1fd3dd2afb62df122349dc10d730 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 18 Aug 2020 13:44:48 -0400 Subject: [PATCH 35/60] result module need not be public Signed-off-by: Thane Thomson --- rpc/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 80d58cbf5..c7bb95e86 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -43,7 +43,7 @@ mod id; mod method; pub mod request; pub mod response; -pub mod result; +mod result; mod version; pub use self::{ From b160e656692e8388bfd5e58bda3c0b9a0ea88cf5 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 18 Aug 2020 15:28:42 -0400 Subject: [PATCH 36/60] Clean up docs Signed-off-by: Thane Thomson --- rpc/src/client.rs | 13 ++++----- rpc/src/client/subscription.rs | 45 ++++++++++++++++++++++++++--- rpc/src/client/transport/http_ws.rs | 4 +-- rpc/src/event.rs | 4 ++- rpc/src/lib.rs | 2 ++ rpc/src/request.rs | 2 +- 6 files changed, 54 insertions(+), 16 deletions(-) diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 74b48d042..2081284ae 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -24,12 +24,11 @@ use tendermint::Genesis; /// pub const DEFAULT_SUBSCRIPTION_BUF_SIZE: usize = 100; -/// A `MinimalClient` provides lightweight access to the Tendermint RPC. It -/// gives access to all endpoints with the exception of the event -/// subscription-related ones. +/// Provides lightweight access to the Tendermint RPC. It gives access to all +/// endpoints with the exception of the event subscription-related ones. /// -/// To access event subscription capabilities, use a client that implements the -/// [`FullClient`] trait. +/// To access event subscription capabilities, rather use a client that +/// implements the [`FullClient`] trait. /// /// [`FullClient`]: trait.FullClient.html /// @@ -175,8 +174,8 @@ pub trait MinimalClient { async fn close(self) -> Result<()>; } -/// A `FullClient` is one that augments a [`MinimalClient`] functionality with -/// subscription capabilities. +/// A client that augments a [`MinimalClient`] functionality with subscription +/// capabilities. /// /// [`MinimalClient`]: trait.MinimalClient.html /// diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 30d78c8a4..1b93c0b51 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -14,11 +14,38 @@ pub type EventRx = mpsc::Receiver>; pub type EventTx = mpsc::Sender>; pub type PendingResultTx = oneshot::Sender>; -/// An interface that can be used to asynchronously receive events for a +/// An interface that can be used to asynchronously receive [`Event`]s for a /// particular subscription. +/// +/// ## Examples +/// +/// ``` +/// use tendermint_rpc::{SubscriptionId, Subscription}; +/// use futures::StreamExt; +/// +/// /// Prints `count` events from the given subscription. +/// async fn print_events(subs: &mut Subscription, count: usize) { +/// let mut counter = 0_usize; +/// while let Some(res) = subs.next().await { +/// // Technically, a subscription produces `Result` +/// // instances. Errors can be produced by the remote endpoint at any +/// // time and need to be handled here. +/// let ev = res.unwrap(); +/// println!("Got incoming event: {:?}", ev); +/// counter += 1; +/// if counter >= count { +/// break +/// } +/// } +/// } +/// ``` +/// +/// [`Event`]: ./event/struct.Event.html #[derive(Debug)] pub struct Subscription { + /// The query for which events will be produced. pub query: String, + /// The ID of this subscription (automatically assigned). pub id: SubscriptionId, event_rx: EventRx, } @@ -42,6 +69,11 @@ impl Subscription { } /// 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); @@ -106,9 +138,14 @@ pub enum SubscriptionState { NotFound, } -/// Provides a mechanism for tracking subscriptions and routing events to those -/// subscriptions. This is useful when implementing your own RPC client -/// transport layer. +/// Provides a mechanism for tracking [`Subscription`]s and routing [`Event`]s +/// to those subscriptions. +/// +/// This is useful when implementing your own RPC client transport layer. +/// +/// [`Subscription`]: struct.Subscription.html +/// [`Event`]: ./event/struct.Event.html +/// #[derive(Debug)] pub struct SubscriptionRouter { subscriptions: HashMap>, diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index 3b6376bdc..a1acad671 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -32,7 +32,6 @@ use tokio::task::JoinHandle; const DEFAULT_WEBSOCKET_CMD_BUF_SIZE: usize = 20; /// An HTTP-based Tendermint RPC client (a [`MinimalClient`] implementation). -/// Requires features `client` and `http_ws`. /// /// Does not provide [`Event`] subscription facilities (see /// [`HttpWebSocketClient`] for a client that does provide `Event` subscription @@ -91,8 +90,7 @@ impl HttpClient { } } -/// An HTTP- and WebSocket-based Tendermint RPC client. Requires features -/// `client` and `http_ws`. +/// An HTTP- and WebSocket-based Tendermint RPC client. /// /// HTTP is used for all requests except those pertaining to [`Event`] /// subscription. `Event` subscription is facilitated by a WebSocket diff --git a/rpc/src/event.rs b/rpc/src/event.rs index 2f15ead21..5e43ef4ec 100644 --- a/rpc/src/event.rs +++ b/rpc/src/event.rs @@ -9,7 +9,9 @@ use tendermint::{ use crate::{response::Wrapper, Response}; -/// An incoming event produced by a subscription. +/// An incoming event produced by a [`Subscription`]. +/// +/// [`Subscription`]: ../struct.Subscription.html #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Event { /// The query that produced the event. diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index c7bb95e86..be9174b35 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -26,6 +26,8 @@ //! [`HttpClient`]: struct.HttpClient.html //! [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html +#![cfg_attr(docsrs, feature(doc_cfg))] + #[cfg(feature = "client")] mod client; #[cfg(feature = "client")] diff --git a/rpc/src/request.rs b/rpc/src/request.rs index 4c5e86c21..aa55f88d5 100644 --- a/rpc/src/request.rs +++ b/rpc/src/request.rs @@ -42,7 +42,7 @@ where /// /// By default this sets the ID of the request to a random [UUIDv4] value. /// - /// [UUIDv4]: https://en.wikipedia.org/wiki/Universally_unique_identifier + /// [UUIDv4]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random) pub fn new(request: R) -> Self { Wrapper::new_with_id(Id::uuid_v4(), request) } From 86ed3d9a1408306dc412d8ce3af9c07916feb7f2 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 18 Aug 2020 15:37:34 -0400 Subject: [PATCH 37/60] Rename `ClientError` to `ClientInternalError` and update docs Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 12 ++++++------ rpc/src/client/transport/http_ws.rs | 12 ++++++------ rpc/src/error.rs | 26 ++++++++++++++------------ 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 1b93c0b51..a8502c00d 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -110,7 +110,7 @@ impl TryInto for Id { match self { Id::Str(s) => Ok(SubscriptionId(s)), Id::Num(i) => Ok(SubscriptionId(format!("{}", i))), - Id::None => Err(Error::client_error( + Id::None => Err(Error::client_internal_error( "cannot convert an empty JSONRPC ID into a subscription ID", )), } @@ -223,7 +223,7 @@ impl SubscriptionRouter { pending_subscribe.event_tx, ); Ok(pending_subscribe.result_tx.send(Ok(())).map_err(|_| { - Error::client_error(format!( + Error::client_internal_error(format!( "failed to communicate result of pending subscription with ID: {}", id )) @@ -246,7 +246,7 @@ impl SubscriptionRouter { .result_tx .send(Err(err.into())) .map_err(|_| { - Error::client_error(format!( + Error::client_internal_error(format!( "failed to communicate result of pending subscription with ID: {}", id )) @@ -287,7 +287,7 @@ impl SubscriptionRouter { ); self.remove(subscription); Ok(result_tx.send(Ok(())).map_err(|_| { - Error::client_error(format!( + Error::client_internal_error(format!( "failed to communicate result of pending unsubscribe for subscription with ID: {}", id )) @@ -307,7 +307,7 @@ impl SubscriptionRouter { match self.pending_unsubscribe.remove(id) { Some(pending_unsubscribe) => { Ok(pending_unsubscribe.result_tx.send(Err(err.into())).map_err(|_| { - Error::client_error(format!( + Error::client_internal_error(format!( "failed to communicate result of pending unsubscribe for subscription with ID: {}", id )) @@ -505,7 +505,7 @@ mod test { panic!("should not have received an event prior to confirming a pending subscription") } - let cancel_error = Error::client_error("cancelled"); + let cancel_error = Error::client_internal_error("cancelled"); router .cancel_pending_subscribe(&subs_id, cancel_error.clone()) .unwrap(); diff --git a/rpc/src/client/transport/http_ws.rs b/rpc/src/client/transport/http_ws.rs index a1acad671..af4b27261 100644 --- a/rpc/src/client/transport/http_ws.rs +++ b/rpc/src/client/transport/http_ws.rs @@ -178,7 +178,7 @@ impl HttpWebSocketClient { async fn send_cmd(&mut self, cmd: WebSocketDriverCmd) -> Result<()> { self.cmd_tx.send(cmd).await.map_err(|e| { - Error::internal_error(format!("failed to send command to client driver: {}", e)) + Error::client_internal_error(format!("failed to send command to client driver: {}", e)) }) } } @@ -200,7 +200,7 @@ impl MinimalClient for HttpWebSocketClient { async fn close(mut self) -> Result<()> { self.send_cmd(WebSocketDriverCmd::Close).await?; self.driver_handle.await.map_err(|e| { - Error::client_error(format!("failed to join client driver async task: {}", e)) + Error::client_internal_error(format!("failed to join client driver async task: {}", e)) })? } } @@ -233,7 +233,7 @@ impl FullClient for HttpWebSocketClient { // Wait to make sure our subscription request went through // successfully. result_rx.await.map_err(|e| { - Error::client_error(format!( + Error::client_internal_error(format!( "failed to receive response from client driver for subscription request: {}", e )) @@ -253,7 +253,7 @@ impl FullClient for HttpWebSocketClient { }) .await?; result_rx.await.map_err(|e| { - Error::client_error(format!( + Error::client_internal_error(format!( "failed to receive response from client driver for unsubscribe request: {}", e )) @@ -339,7 +339,7 @@ impl WebSocketSubscriptionDriver { .await { if result_tx.send(Err(e)).is_err() { - return Err(Error::client_error( + return Err(Error::client_internal_error( "failed to respond internally to subscription request", )); } @@ -362,7 +362,7 @@ impl WebSocketSubscriptionDriver { .await { if result_tx.send(Err(e)).is_err() { - return Err(Error::client_error( + return Err(Error::client_internal_error( "failed to respond internally to unsubscribe request", )); } diff --git a/rpc/src/error.rs b/rpc/src/error.rs index 51ab53438..1228fdcda 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -73,12 +73,9 @@ impl Error { Error::new(Code::ServerError, Some(data.to_string())) } - pub fn internal_error(cause: impl Into) -> Error { - Error::new(Code::InternalError, Some(cause.into())) - } - - pub fn client_error(cause: impl Into) -> Error { - Error::new(Code::ClientError, Some(cause.into())) + /// An internal error occurred within the client. + pub fn client_internal_error(cause: impl Into) -> Error { + Error::new(Code::ClientInternalError, Some(cause.into())) } /// Obtain the `rpc::error::Code` for this error @@ -147,9 +144,14 @@ pub enum Code { #[error("Websocket Error")] WebSocketError, - /// The client encountered an error. - #[error("Client error")] - ClientError, + /// An internal error occurred within the client. + /// + /// This is an error unique to this client, and is not available in the + /// [Go client]. + /// + /// [Go client]: https://github.com/tendermint/tendermint/tree/master/rpc/jsonrpc/client + #[error("Client internal error")] + ClientInternalError, /// Parse error i.e. invalid JSON (-32700) #[error("Parse error. Invalid JSON")] @@ -167,7 +169,7 @@ pub enum Code { #[error("Invalid params")] InvalidParams, - /// Internal error (-32603) + /// Internal RPC server error (-32603) #[error("Internal error")] InternalError, @@ -192,7 +194,7 @@ impl From for Code { match value { 0 => Code::HttpError, 1 => Code::WebSocketError, - 2 => Code::ClientError, + 2 => Code::ClientInternalError, -32700 => Code::ParseError, -32600 => Code::InvalidRequest, -32601 => Code::MethodNotFound, @@ -209,7 +211,7 @@ impl From for i32 { match code { Code::HttpError => 0, Code::WebSocketError => 1, - Code::ClientError => 2, + Code::ClientInternalError => 2, Code::ParseError => -32700, Code::InvalidRequest => -32600, Code::MethodNotFound => -32601, From fd012789514bd4d6073832b52057d149cada0f83 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 18 Aug 2020 15:43:02 -0400 Subject: [PATCH 38/60] Fix typo Signed-off-by: Thane Thomson --- rpc/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 18d7ed448..e3cc54c25 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -15,7 +15,7 @@ authors = [ ] description = """ - tenndermint-rpc contains the core types returned by a Tendermint node's RPC endpoint. + tendermint-rpc contains the core types returned by a Tendermint node's RPC endpoint. All networking related features are feature guarded to keep the dependencies small in cases where only the core types are needed. """ From 3eadccdc0168f6eea5c706849bcf4f6f1594454c Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 18 Aug 2020 16:09:58 -0400 Subject: [PATCH 39/60] Not using nightly docsrs features yet Signed-off-by: Thane Thomson --- rpc/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index be9174b35..c7bb95e86 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -26,8 +26,6 @@ //! [`HttpClient`]: struct.HttpClient.html //! [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html -#![cfg_attr(docsrs, feature(doc_cfg))] - #[cfg(feature = "client")] mod client; #[cfg(feature = "client")] From a4c5a4d3893de947d688a51f426f6d10b48de972 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 26 Aug 2020 15:25:38 -0400 Subject: [PATCH 40/60] Refactor and restructure interface As per the comments on #516, I've tried to restructure the public API for the client in a bit of a simpler way. I don't, unfortunately, think it's possible to simplify this API any further at this point in time. Signed-off-by: Thane Thomson --- light-client/Cargo.toml | 2 +- light-client/src/components/io.rs | 2 +- light-client/src/evidence.rs | 2 +- rpc/Cargo.toml | 11 +- rpc/src/client.rs | 87 +-- rpc/src/client/subscription.rs | 291 ++++++---- rpc/src/client/sync.rs | 74 +++ rpc/src/client/transport.rs | 26 +- rpc/src/client/transport/http.rs | 98 ++++ rpc/src/client/transport/http_ws.rs | 518 ------------------ rpc/src/client/transport/mock.rs | 153 ++++++ rpc/src/client/transport/mock/subscription.rs | 245 +++++++++ rpc/src/client/transport/websocket.rs | 360 ++++++++++++ rpc/src/error.rs | 8 +- rpc/src/lib.rs | 62 ++- rpc/tests/client.rs | 258 --------- rpc/tests/endpoint.rs | 292 ---------- rpc/tests/integration.rs | 294 +++++++++- tendermint/Cargo.toml | 2 +- tendermint/tests/integration.rs | 7 +- 20 files changed, 1503 insertions(+), 1289 deletions(-) create mode 100644 rpc/src/client/sync.rs create mode 100644 rpc/src/client/transport/http.rs delete mode 100644 rpc/src/client/transport/http_ws.rs create mode 100644 rpc/src/client/transport/mock.rs create mode 100644 rpc/src/client/transport/mock/subscription.rs create mode 100644 rpc/src/client/transport/websocket.rs delete mode 100644 rpc/tests/client.rs delete mode 100644 rpc/tests/endpoint.rs diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index 5e0d44e47..95f5aa835 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -20,7 +20,7 @@ description = """ [dependencies] tendermint = { version = "0.15.0", path = "../tendermint" } -tendermint-rpc = { version = "0.15.0", path = "../rpc", features = ["client", "http_ws"] } +tendermint-rpc = { version = "0.15.0", path = "../rpc", features = ["client", "transport_http"] } anomaly = { version = "0.2.0", features = ["serializer"] } contracts = "0.4.0" diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index f61e09eb1..c3e876a86 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -12,7 +12,7 @@ use tendermint::{ }; use tendermint_rpc as rpc; -use tendermint_rpc::MinimalClient; +use tendermint_rpc::Client; use crate::{ bail, diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index 342aea9f6..e71e20ab2 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -4,7 +4,7 @@ use crate::{components::io::IoError, types::PeerId}; use tendermint::abci::transaction::Hash; use tendermint_rpc as rpc; -use tendermint_rpc::MinimalClient; +use tendermint_rpc::Client; use contracts::{contract_trait, pre}; use std::collections::HashMap; diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index e3cc54c25..28fccc003 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -25,9 +25,12 @@ all-features = true [features] default = [] -client = [ "async-trait", "async-tungstenite", "futures", "http", "hyper", "tokio" ] -http_ws = [ "async-trait", "async-tungstenite", "futures", "http", "hyper", "tokio" ] -secp256k1 = ["tendermint/secp256k1"] +client = [ "async-trait", "futures" ] +secp256k1 = [ "tendermint/secp256k1" ] +subscription = [ "tokio" ] +transport_http = [ "http", "hyper", "tokio" ] +transport_mock = [ "tokio" ] +transport_websocket = [ "async-tungstenite", "tokio" ] [dependencies] bytes = "0.5" @@ -39,7 +42,7 @@ tendermint = { version = "0.15.0", path = "../tendermint" } thiserror = "1" uuid = { version = "0.8", default-features = false } -async-tungstenite = { version="0.5", features = ["tokio-runtime"], optional = true } +async-tungstenite = { version="0.8", features = ["tokio-runtime"], optional = true } async-trait = { version = "0.1", optional = true } futures = { version = "0.3", optional = true } http = { version = "0.2", optional = true } diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 2081284ae..22554343b 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -1,13 +1,21 @@ //! Tendermint RPC client. +#[cfg(feature = "subscription")] mod subscription; -pub use subscription::{ - EventRx, EventTx, PendingResultTx, Subscription, SubscriptionId, SubscriptionRouter, -}; +#[cfg(feature = "subscription")] +pub use subscription::{Subscription, SubscriptionClient, SubscriptionId, SubscriptionRouter}; +#[cfg(feature = "subscription")] +pub mod sync; mod transport; -#[cfg(feature = "http_ws")] -pub use transport::{HttpClient, HttpWebSocketClient}; +#[cfg(feature = "transport_http")] +pub use transport::http::HttpClient; +#[cfg(all(feature = "subscription", feature = "transport_mock"))] +pub use transport::mock::MockSubscriptionClient; +#[cfg(feature = "transport_mock")] +pub use transport::mock::{MockClient, MockRequestMatcher, MockRequestMethodMatcher}; +#[cfg(all(feature = "subscription", feature = "transport_websocket"))] +pub use transport::websocket::WebSocketSubscriptionClient; use crate::endpoint::*; use crate::{Request, Result}; @@ -17,23 +25,15 @@ use tendermint::block::Height; use tendermint::evidence::Evidence; use tendermint::Genesis; -/// The default number of events we buffer in a [`Subscription`] if you do not -/// specify the buffer size when creating it. -/// -/// [`Subscription`]: struct.Subscription.html -/// -pub const DEFAULT_SUBSCRIPTION_BUF_SIZE: usize = 100; - /// Provides lightweight access to the Tendermint RPC. It gives access to all /// endpoints with the exception of the event subscription-related ones. /// -/// To access event subscription capabilities, rather use a client that -/// implements the [`FullClient`] trait. -/// -/// [`FullClient`]: trait.FullClient.html +/// To access event subscription capabilities, use a client that implements the +/// [`SubscriptionClient`] trait. /// +/// [`SubscriptionClient`]: trait.SubscriptionClient.html #[async_trait] -pub trait MinimalClient { +pub trait Client: ClosableClient { /// `/abci_info`: get information about the ABCI application. async fn abci_info(&self) -> Result { Ok(self.perform(abci_info::Request).await?.response) @@ -168,53 +168,18 @@ pub trait MinimalClient { async fn perform(&self, request: R) -> Result where R: Request; - - /// Gracefully terminate the underlying connection (if relevant - depends - /// on the underlying transport). - async fn close(self) -> Result<()>; } -/// A client that augments a [`MinimalClient`] functionality with subscription -/// capabilities. +/// A client that provides a self-consuming `close` method, to allow for +/// graceful termination. /// -/// [`MinimalClient`]: trait.MinimalClient.html +/// This trait acts as a common trait to both the [`Client`] and +/// [`SubscriptionClient`] traits. /// +/// [`Client`]: trait.Client.html +/// [`SubscriptionClient`]: trait.SubscriptionClient.html #[async_trait] -pub trait FullClient: MinimalClient { - /// `/subscribe`: subscribe to receive events produced by the given query. - /// - /// Allows for specification of the `buf_size` parameter, which determines - /// how many events can be buffered in the resulting [`Subscription`]. The - /// size of this buffer must be tuned according to how quickly your - /// application can process the incoming events from this particular query. - /// The slower your application processes events, the larger this buffer - /// needs to be. - /// - /// [`Subscription`]: struct.Subscription.html - /// - async fn subscribe_with_buf_size( - &mut self, - query: String, - buf_size: usize, - ) -> Result; - - /// `/subscribe`: subscribe to receive events produced by the given query. - /// - /// Uses [`DEFAULT_SUBSCRIPTION_BUF_SIZE`] as the buffer size for the - /// returned [`Subscription`]. - /// - /// [`DEFAULT_SUBSCRIPTION_BUF_SIZE`]: constant.DEFAULT_SUBSCRIPTION_BUF_SIZE.html - /// [`Subscription`]: struct.Subscription.html - /// - async fn subscribe(&mut self, query: String) -> Result { - self.subscribe_with_buf_size(query, DEFAULT_SUBSCRIPTION_BUF_SIZE) - .await - } - - /// `/unsubscribe`: unsubscribe from receiving events for the given - /// subscription. - /// - /// This terminates the given subscription and consumes it, since it is no - /// longer usable. - async fn unsubscribe(&mut self, subscription: Subscription) -> Result<()>; +pub trait ClosableClient { + /// Gracefully close the client. + async fn close(self) -> Result<()>; } diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index a8502c00d..c59992bb9 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -1,18 +1,52 @@ //! Subscription- and subscription management-related functionality. +use crate::client::sync::{unbounded, ChannelRx, ChannelTx}; +use crate::client::ClosableClient; use crate::event::Event; use crate::{Error, Id, 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 tokio::sync::{mpsc, oneshot}; -pub type EventRx = mpsc::Receiver>; -pub type EventTx = mpsc::Sender>; -pub type PendingResultTx = oneshot::Sender>; +/// A client that exclusively provides [`Event`] subscription capabilities, +/// without any other RPC method support. +/// +/// To build a full-featured client, implement both this trait as well as the +/// [`Client`] trait. +/// +/// [`Event`]: ./events/struct.Event.html +/// [`Client`]: trait.Client.html +#[async_trait] +pub trait SubscriptionClient: ClosableClient { + /// `/subscribe`: subscribe to receive events produced by the given query. + /// + /// Allows for specification of the `buf_size` parameter, which determines + /// how many events can be buffered in the resulting [`Subscription`]. Set + /// to 0 to use an unbounded buffer (i.e. the buffer size will only be + /// limited by the amount of memory available to your application). + /// + /// [`Subscription`]: struct.Subscription.html + async fn subscribe_with_buf_size( + &mut self, + query: String, + buf_size: usize, + ) -> Result; + + /// `/subscribe`: subscribe to receive events produced by the given query. + /// + /// Uses an unbounded buffer for the resulting [`Subscription`] (i.e. this + /// is the same as calling `subscribe_with_buf_size` with `buf_size` set to + /// 0). + /// + /// [`Subscription`]: struct.Subscription.html + async fn subscribe(&mut self, query: String) -> Result { + self.subscribe_with_buf_size(query, 0).await + } +} /// An interface that can be used to asynchronously receive [`Event`]s for a /// particular subscription. @@ -47,7 +81,10 @@ pub struct Subscription { pub query: String, /// The ID of this subscription (automatically assigned). pub id: SubscriptionId, - event_rx: EventRx, + // Our internal result event receiver for this subscription. + event_rx: ChannelRx>, + // Allows us to gracefully terminate this subscription. + terminate_tx: ChannelTx, } impl Stream for Subscription { @@ -59,13 +96,51 @@ impl Stream for Subscription { } impl Subscription { - pub fn new(id: SubscriptionId, query: String, event_rx: EventRx) -> Self { + pub(crate) fn new( + id: SubscriptionId, + query: String, + event_rx: ChannelRx>, + terminate_tx: ChannelTx, + ) -> Self { Self { id, query, event_rx, + terminate_tx, } } + + /// Gracefully terminate this subscription. + /// + /// This can be called from any asynchronous context. It only returns once + /// it receives confirmation of termination. + pub async fn terminate(mut self) -> Result<()> { + let (result_tx, mut result_rx) = unbounded(); + self.terminate_tx + .send(SubscriptionTermination { + query: self.query.clone(), + id: self.id.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 message sent to the subscription driver to terminate the subscription +/// with the given parameters. +/// +/// We expect the driver to use the `result_tx` channel to communicate the +/// result of the termination request to the original caller. +#[derive(Debug, Clone)] +pub struct SubscriptionTermination { + pub query: String, + pub id: SubscriptionId, + pub result_tx: ChannelTx>, } /// Each new subscription is automatically assigned an ID. @@ -120,16 +195,17 @@ impl TryInto for Id { #[derive(Debug)] struct PendingSubscribe { query: String, - event_tx: EventTx, - result_tx: PendingResultTx, + event_tx: ChannelTx>, + result_tx: ChannelTx>, } #[derive(Debug)] struct PendingUnsubscribe { - subscription: Subscription, - result_tx: PendingResultTx, + query: String, + result_tx: ChannelTx>, } +/// The current state of a subscription. #[derive(Debug, Clone, PartialEq)] pub enum SubscriptionState { Pending, @@ -141,14 +217,11 @@ pub enum SubscriptionState { /// Provides a mechanism for tracking [`Subscription`]s and routing [`Event`]s /// to those subscriptions. /// -/// This is useful when implementing your own RPC client transport layer. -/// /// [`Subscription`]: struct.Subscription.html /// [`Event`]: ./event/struct.Event.html -/// #[derive(Debug)] pub struct SubscriptionRouter { - subscriptions: HashMap>, + subscriptions: HashMap>>>, pending_subscribe: HashMap, pending_unsubscribe: HashMap, } @@ -180,7 +253,7 @@ impl SubscriptionRouter { /// Immediately add a new subscription to the router without waiting for /// confirmation. - pub fn add(&mut self, id: SubscriptionId, query: String, event_tx: EventTx) { + 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 => { @@ -193,12 +266,12 @@ impl SubscriptionRouter { /// Keep track of a pending subscription, which can either be confirmed or /// cancelled. - pub fn add_pending_subscribe( + pub fn pending_add( &mut self, id: SubscriptionId, query: String, - event_tx: EventTx, - result_tx: PendingResultTx, + event_tx: ChannelTx>, + result_tx: ChannelTx>, ) { self.pending_subscribe.insert( id, @@ -212,22 +285,17 @@ impl SubscriptionRouter { /// Attempts to confirm the pending subscription with the given ID. /// - /// Returns an error if it fails to respond (through the internal `oneshot` - /// channel) to the original caller to indicate success. - pub fn confirm_pending_subscribe(&mut self, id: &SubscriptionId) -> Result<()> { + /// Returns an error if it fails to respond to the original caller to + /// indicate success. + pub async fn confirm_add(&mut self, id: &SubscriptionId) -> Result<()> { match self.pending_subscribe.remove(id) { - Some(pending_subscribe) => { + Some(mut pending_subscribe) => { self.add( id.clone(), pending_subscribe.query.clone(), pending_subscribe.event_tx, ); - Ok(pending_subscribe.result_tx.send(Ok(())).map_err(|_| { - Error::client_internal_error(format!( - "failed to communicate result of pending subscription with ID: {}", - id - )) - })?) + Ok(pending_subscribe.result_tx.send(Ok(())).await?) } None => Ok(()), } @@ -236,15 +304,12 @@ impl SubscriptionRouter { /// Attempts to cancel the pending subscription with the given ID, sending /// the specified error to the original creator of the attempted /// subscription. - pub fn cancel_pending_subscribe( - &mut self, - id: &SubscriptionId, - err: impl Into, - ) -> Result<()> { + pub async fn cancel_add(&mut self, id: &SubscriptionId, err: impl Into) -> Result<()> { match self.pending_subscribe.remove(id) { - Some(pending_subscribe) => Ok(pending_subscribe + 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: {}", @@ -255,43 +320,36 @@ impl SubscriptionRouter { } } - /// Immediately remove the given subscription and consume it. - pub fn remove(&mut self, subs: Subscription) { - let subs_for_query = match self.subscriptions.get_mut(&subs.query) { + /// Immediately remove the subscription with the given query and ID. + pub fn remove(&mut self, query: String, id: SubscriptionId) { + let subs_for_query = match self.subscriptions.get_mut(&query) { Some(s) => s, None => return, }; - subs_for_query.remove(&subs.id); + subs_for_query.remove(&id); } /// Keeps track of a pending unsubscribe request, which can either be /// confirmed or cancelled. - pub fn add_pending_unsubscribe(&mut self, subs: Subscription, result_tx: PendingResultTx) { - self.pending_unsubscribe.insert( - subs.id.clone(), - PendingUnsubscribe { - subscription: subs, - result_tx, - }, - ); + pub fn pending_remove( + &mut self, + query: String, + id: SubscriptionId, + result_tx: ChannelTx>, + ) { + self.pending_unsubscribe + .insert(id, PendingUnsubscribe { query, result_tx }); } /// Confirm the pending unsubscribe request for the subscription with the /// given ID. - pub fn confirm_pending_unsubscribe(&mut self, id: &SubscriptionId) -> Result<()> { + pub async fn confirm_remove(&mut self, id: &SubscriptionId) -> Result<()> { match self.pending_unsubscribe.remove(id) { Some(pending_unsubscribe) => { - let (subscription, result_tx) = ( - pending_unsubscribe.subscription, - pending_unsubscribe.result_tx, - ); - self.remove(subscription); - Ok(result_tx.send(Ok(())).map_err(|_| { - Error::client_internal_error(format!( - "failed to communicate result of pending unsubscribe for subscription with ID: {}", - id - )) - })?) + let (query, mut result_tx) = + (pending_unsubscribe.query, pending_unsubscribe.result_tx); + self.remove(query, id.clone()); + Ok(result_tx.send(Ok(())).await?) } None => Ok(()), } @@ -299,31 +357,33 @@ impl SubscriptionRouter { /// Cancel the pending unsubscribe request for the subscription with the /// given ID, responding with the given error. - pub fn cancel_pending_unsubscribe( + pub async fn cancel_remove( &mut self, id: &SubscriptionId, err: impl Into, ) -> Result<()> { match self.pending_unsubscribe.remove(id) { - Some(pending_unsubscribe) => { - Ok(pending_unsubscribe.result_tx.send(Err(err.into())).map_err(|_| { - Error::client_internal_error(format!( - "failed to communicate result of pending unsubscribe for subscription with ID: {}", - 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.subscriptions .iter() .any(|(_query, subs_for_query)| subs_for_query.contains_key(id)) } - pub fn get_active_subscription_mut(&mut self, id: &SubscriptionId) -> Option<&mut EventTx> { + /// 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.subscriptions .iter_mut() .find(|(_query, subs_for_query)| subs_for_query.contains_key(id)) @@ -359,9 +419,11 @@ impl Default for SubscriptionRouter { #[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")) @@ -376,6 +438,25 @@ mod test { .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(); @@ -385,9 +466,9 @@ mod test { SubscriptionId::default(), SubscriptionId::default(), ); - let (subs1_event_tx, mut subs1_event_rx) = mpsc::channel(1); - let (subs2_event_tx, mut subs2_event_rx) = mpsc::channel(1); - let (subs3_event_tx, mut subs3_event_rx) = mpsc::channel(1); + 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.clone(), "query1".into(), subs1_event_tx); @@ -399,24 +480,18 @@ mod test { ev.query = "query1".into(); router.publish(ev.clone()).await; - let subs1_ev = subs1_event_rx.try_recv().unwrap().unwrap(); - let subs2_ev = subs2_event_rx.try_recv().unwrap().unwrap(); - if subs3_event_rx.try_recv().is_ok() { - panic!("should not have received an event here"); - } + 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; - if subs1_event_rx.try_recv().is_ok() { - panic!("should not have received an event here"); - } - if subs2_event_rx.try_recv().is_ok() { - panic!("should not have received an event here"); - } - let subs3_ev = subs3_event_rx.try_recv().unwrap().unwrap(); + 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); } @@ -424,8 +499,8 @@ mod test { async fn router_pending_subscription() { let mut router = SubscriptionRouter::default(); let subs_id = SubscriptionId::default(); - let (event_tx, mut event_rx) = mpsc::channel(1); - let (result_tx, mut result_rx) = oneshot::channel(); + 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(); @@ -434,49 +509,40 @@ mod test { SubscriptionState::NotFound, router.subscription_state(&subs_id) ); - router.add_pending_subscribe(subs_id.clone(), query.clone(), event_tx, result_tx); + router.pending_add(subs_id.clone(), query.clone(), event_tx, result_tx); assert_eq!( SubscriptionState::Pending, router.subscription_state(&subs_id) ); router.publish(ev.clone()).await; - if event_rx.try_recv().is_ok() { - panic!("should not have received an event prior to confirming a pending subscription") - } + must_not_recv(&mut event_rx, 50).await; - router.confirm_pending_subscribe(&subs_id).unwrap(); + router.confirm_add(&subs_id).await.unwrap(); assert_eq!( SubscriptionState::Active, router.subscription_state(&subs_id) ); - if event_rx.try_recv().is_ok() { - panic!("should not have received an event here") - } - if result_rx.try_recv().is_err() { - panic!("we should have received successful confirmation of the new subscription") - } + must_not_recv(&mut event_rx, 50).await; + let _ = must_recv(&mut result_rx, 500).await; router.publish(ev.clone()).await; - let received_ev = event_rx.try_recv().unwrap().unwrap(); + let received_ev = must_recv(&mut event_rx, 500).await.unwrap(); assert_eq!(ev, received_ev); - let (result_tx, mut result_rx) = oneshot::channel(); - router.add_pending_unsubscribe( - Subscription::new(subs_id.clone(), query.clone(), event_rx), - result_tx, - ); + let (result_tx, mut result_rx) = unbounded(); + router.pending_remove(query.clone(), subs_id.clone(), result_tx); assert_eq!( SubscriptionState::Cancelling, router.subscription_state(&subs_id), ); - router.confirm_pending_unsubscribe(&subs_id).unwrap(); + router.confirm_remove(&subs_id).await.unwrap(); assert_eq!( SubscriptionState::NotFound, router.subscription_state(&subs_id) ); router.publish(ev.clone()).await; - if result_rx.try_recv().is_err() { + if must_recv(&mut result_rx, 500).await.is_err() { panic!("we should have received successful confirmation of the unsubscribe request") } } @@ -485,8 +551,8 @@ mod test { async fn router_cancel_pending_subscription() { let mut router = SubscriptionRouter::default(); let subs_id = SubscriptionId::default(); - let (event_tx, mut event_rx) = mpsc::channel(1); - let (result_tx, mut result_rx) = oneshot::channel(); + 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(); @@ -495,29 +561,26 @@ mod test { SubscriptionState::NotFound, router.subscription_state(&subs_id) ); - router.add_pending_subscribe(subs_id.clone(), query, event_tx, result_tx); + router.pending_add(subs_id.clone(), query, event_tx, result_tx); assert_eq!( SubscriptionState::Pending, router.subscription_state(&subs_id) ); router.publish(ev.clone()).await; - if event_rx.try_recv().is_ok() { - panic!("should not have received an event prior to confirming a pending subscription") - } + must_not_recv(&mut event_rx, 50).await; let cancel_error = Error::client_internal_error("cancelled"); router - .cancel_pending_subscribe(&subs_id, cancel_error.clone()) + .cancel_add(&subs_id, cancel_error.clone()) + .await .unwrap(); assert_eq!( SubscriptionState::NotFound, router.subscription_state(&subs_id) ); - assert_eq!(Err(cancel_error), result_rx.try_recv().unwrap()); + assert_eq!(Err(cancel_error), must_recv(&mut result_rx, 500).await); router.publish(ev.clone()).await; - if event_rx.try_recv().is_ok() { - panic!("should not have received an event prior to confirming a pending subscription") - } + must_not_recv(&mut event_rx, 50).await; } } diff --git a/rpc/src/client/sync.rs b/rpc/src/client/sync.rs new file mode 100644 index 000000000..f8be8d755 --- /dev/null +++ b/rpc/src/client/sync.rs @@ -0,0 +1,74 @@ +//! Synchronization primitives specific to the Tendermint RPC client. +//! +//! At present, this wraps Tokio's synchronization primitives and provides some +//! conveniences, such as an interface to a channel without caring about +//! whether it's bounded or unbounded. + +use crate::{Error, Result}; +use futures::task::{Context, Poll}; +use tokio::sync::mpsc; + +/// Constructor for a bounded channel with maximum capacity of `buf_size` +/// elements. +pub fn bounded(buf_size: usize) -> (ChannelTx, ChannelRx) { + let (tx, rx) = mpsc::channel(buf_size); + (ChannelTx::Bounded(tx), ChannelRx::Bounded(rx)) +} + +/// Constructor for an unbounded channel. +pub fn unbounded() -> (ChannelTx, ChannelRx) { + let (tx, rx) = mpsc::unbounded_channel(); + (ChannelTx::Unbounded(tx), ChannelRx::Unbounded(rx)) +} + +/// Generic sender interface on bounded and unbounded channels for +/// `Result` instances. +/// +/// Can be cloned because the underlying channel used is +/// [`mpsc`](https://docs.rs/tokio/*/tokio/sync/mpsc/index.html). +#[derive(Debug, Clone)] +pub enum ChannelTx { + Bounded(mpsc::Sender), + Unbounded(mpsc::UnboundedSender), +} + +impl ChannelTx { + pub async fn send(&mut self, value: T) -> Result<()> { + match self { + ChannelTx::Bounded(ref mut tx) => tx.send(value).await, + ChannelTx::Unbounded(ref mut tx) => tx.send(value), + } + .map_err(|e| { + Error::client_internal_error(format!( + "failed to send message to internal channel: {}", + e + )) + }) + } +} + +/// Generic receiver interface on bounded and unbounded channels. +#[derive(Debug)] +pub enum ChannelRx { + /// A channel that can contain up to a fixed number of items. + Bounded(mpsc::Receiver), + /// A channel that is unconstrained (except by system resources, of + /// course). + Unbounded(mpsc::UnboundedReceiver), +} + +impl ChannelRx { + pub async fn recv(&mut self) -> Option { + match self { + ChannelRx::Bounded(ref mut rx) => rx.recv().await, + ChannelRx::Unbounded(ref mut rx) => rx.recv().await, + } + } + + pub fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll> { + match self { + ChannelRx::Bounded(ref mut rx) => rx.poll_recv(cx), + ChannelRx::Unbounded(ref mut rx) => rx.poll_recv(cx), + } + } +} diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index ce5348b06..411df7183 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -1,6 +1,24 @@ //! Tendermint RPC client implementations for different transports. -#[cfg(feature = "http_ws")] -mod http_ws; -#[cfg(feature = "http_ws")] -pub use http_ws::{HttpClient, HttpWebSocketClient}; +#[cfg(feature = "transport_http")] +pub mod http; +#[cfg(any(test, feature = "transport_mock"))] +pub mod mock; +#[cfg(feature = "transport_websocket")] +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 new file mode 100644 index 000000000..cda79d474 --- /dev/null +++ b/rpc/src/client/transport/http.rs @@ -0,0 +1,98 @@ +//! HTTP-based transport for Tendermint RPC Client. + +use crate::client::transport::get_tcp_host_port; +use crate::client::ClosableClient; +use crate::{Client, Request, Response, Result}; +use async_trait::async_trait; +use bytes::buf::BufExt; +use hyper::header; +use tendermint::net; + +/// A JSON-RPC/HTTP Tendermint RPC client (implements [`Client`]). +/// +/// Does not provide [`Event`] subscription facilities (see +/// [`WebSocketSubscriptionClient`] for a client that does provide [`Event`] +/// subscription facilities). +/// +/// ## Examples +/// +/// We don't test this example automatically at present, but it has and can +/// been tested against a Tendermint node running on `localhost`. +/// +/// ```ignore +/// use tendermint_rpc::{HttpClient, Client}; +/// +/// #[tokio::main] +/// async fn main() { +/// let client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()) +/// .unwrap(); +/// +/// let abci_info = client.abci_info() +/// .await +/// .unwrap(); +/// +/// println!("Got ABCI info: {:?}", abci_info); +/// } +/// ``` +/// +/// [`Client`]: trait.Client.html +/// [`Event`]: ./event/struct.Event.html +/// [`WebSocketSubscriptionClient`]: struct.WebSocketSubscriptionClient.html +/// +#[derive(Debug, Clone)] +pub struct HttpClient { + host: String, + port: u16, +} + +#[async_trait] +impl Client for HttpClient { + async fn perform(&self, request: R) -> Result + where + R: Request, + { + http_request(&self.host, self.port, request).await + } +} + +#[async_trait] +impl ClosableClient for HttpClient { + async fn close(self) -> Result<()> { + Ok(()) + } +} + +impl HttpClient { + /// Create a new JSON-RPC/HTTP Tendermint RPC client. + pub fn new(address: net::Address) -> Result { + let (host, port) = get_tcp_host_port(address)?; + 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/http_ws.rs b/rpc/src/client/transport/http_ws.rs deleted file mode 100644 index af4b27261..000000000 --- a/rpc/src/client/transport/http_ws.rs +++ /dev/null @@ -1,518 +0,0 @@ -//! HTTP-based transport for Tendermint RPC Client, with WebSockets-based -//! subscription handling mechanism. -//! -//! The `client` and `http_ws` features are required to use this module. - -use crate::client::subscription::{EventTx, PendingResultTx, SubscriptionState}; -use crate::client::{Subscription, SubscriptionId, SubscriptionRouter}; -use crate::endpoint::{subscribe, unsubscribe}; -use crate::event::Event; -use crate::{request, response}; -use crate::{Error, FullClient, MinimalClient, Request, Response, Result}; -use async_trait::async_trait; -use async_tungstenite::tokio::{connect_async, TokioAdapter}; -use async_tungstenite::tungstenite::protocol::frame::coding::CloseCode; -use async_tungstenite::tungstenite::protocol::CloseFrame; -use async_tungstenite::tungstenite::Message; -use async_tungstenite::WebSocketStream; -use bytes::buf::BufExt; -use futures::{SinkExt, StreamExt}; -use hyper::header; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::convert::TryInto; -use tendermint::net; -use tokio::net::TcpStream; -use tokio::sync::{mpsc, oneshot}; -use tokio::task::JoinHandle; - -// We anticipate that this will be a relatively low-traffic command signaling -// mechanism (these commands are limited to subscribe/unsubscribe requests, -// which we assume won't occur very frequently). -const DEFAULT_WEBSOCKET_CMD_BUF_SIZE: usize = 20; - -/// An HTTP-based Tendermint RPC client (a [`MinimalClient`] implementation). -/// -/// Does not provide [`Event`] subscription facilities (see -/// [`HttpWebSocketClient`] for a client that does provide `Event` subscription -/// facilities). -/// -/// ## Examples -/// -/// We don't test this example automatically at present, but it has and can -/// been tested against a Tendermint node running on `localhost`. -/// -/// ```ignore -/// use tendermint_rpc::{HttpClient, MinimalClient}; -/// -/// #[tokio::main] -/// async fn main() { -/// let client = HttpClient::new("tcp://127.0.0.1:26657".parse().unwrap()) -/// .unwrap(); -/// -/// let abci_info = client.abci_info() -/// .await -/// .unwrap(); -/// -/// println!("Got ABCI info: {:?}", abci_info); -/// } -/// ``` -/// -/// [`MinimalClient`]: trait.MinimalClient.html -/// [`Event`]: ./event/struct.Event.html -/// [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html -/// -#[derive(Debug, Clone)] -pub struct HttpClient { - host: String, - port: u16, -} - -#[async_trait] -impl MinimalClient for HttpClient { - async fn perform(&self, request: R) -> Result - where - R: Request, - { - http_request(&self.host, self.port, request).await - } - - async fn close(self) -> Result<()> { - Ok(()) - } -} - -impl HttpClient { - /// Create a new HTTP-based Tendermint RPC client. - pub fn new(address: net::Address) -> Result { - let (host, port) = get_tcp_host_port(address)?; - Ok(HttpClient { host, port }) - } -} - -/// An HTTP- and WebSocket-based Tendermint RPC client. -/// -/// HTTP is used for all requests except those pertaining to [`Event`] -/// subscription. `Event` subscription is facilitated by a WebSocket -/// connection, which is opened as this client is created. -/// -/// ## Examples -/// -/// We don't test this example automatically at present, but it has and can -/// been tested against a Tendermint node running on `localhost`. -/// -/// ```ignore -/// use tendermint_rpc::{HttpWebSocketClient, FullClient, MinimalClient}; -/// use futures::StreamExt; -/// -/// #[tokio::main] -/// async fn main() { -/// let mut client = HttpWebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) -/// .await -/// .unwrap(); -/// -/// let mut subs = client.subscribe("tm.event='NewBlock'".to_string()) -/// .await -/// .unwrap(); -/// -/// // Grab 5 NewBlock events -/// let mut ev_count = 5_i32; -/// -/// while let Some(res) = subs.next().await { -/// let ev = res.unwrap(); -/// println!("Got event: {:?}", ev); -/// ev_count -= 1; -/// if ev_count < 0 { -/// break -/// } -/// } -/// -/// client.unsubscribe(subs).await.unwrap(); -/// client.close().await.unwrap(); -/// } -/// ``` -/// -/// [`Event`]: ./event/struct.Event.html -/// -#[derive(Debug)] -pub struct HttpWebSocketClient { - host: String, - port: u16, - driver_handle: JoinHandle>, - cmd_tx: mpsc::Sender, -} - -impl HttpWebSocketClient { - /// Construct a full HTTP/WebSocket client directly. - pub async fn new(address: net::Address) -> Result { - let (host, port) = get_tcp_host_port(address)?; - let (stream, _response) = - connect_async(&format!("ws://{}:{}/websocket", &host, port)).await?; - let (cmd_tx, cmd_rx) = mpsc::channel(DEFAULT_WEBSOCKET_CMD_BUF_SIZE); - let driver = WebSocketSubscriptionDriver::new(stream, cmd_rx); - let driver_handle = tokio::spawn(async move { driver.run().await }); - Ok(HttpWebSocketClient { - host, - port, - driver_handle, - cmd_tx, - }) - } - - /// In the absence of an `async` version of [`TryFrom`], this constructor - /// provides an `async` way to upgrade an [`HttpClient`] to an - /// [`HttpWebSocketClient`]. - /// - /// [`TryFrom`]: https://doc.rust-lang.org/std/convert/trait.TryFrom.html - /// [`HttpClient`]: struct.HttpClient.html - /// [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html - /// - pub async fn try_from(client: HttpClient) -> Result { - HttpWebSocketClient::new(net::Address::Tcp { - peer_id: None, - host: client.host, - port: client.port, - }) - .await - } - - async fn send_cmd(&mut self, cmd: WebSocketDriverCmd) -> Result<()> { - self.cmd_tx.send(cmd).await.map_err(|e| { - Error::client_internal_error(format!("failed to send command to client driver: {}", e)) - }) - } -} - -#[async_trait] -impl MinimalClient for HttpWebSocketClient { - async fn perform(&self, request: R) -> Result - where - R: Request, - { - http_request(&self.host, self.port, request).await - } - - /// Gracefully terminate the underlying connection. - /// - /// This sends a termination message to the client driver and blocks - /// indefinitely until the driver terminates. If successfully closed, it - /// returns the `Result` of the driver's async task. - async fn close(mut self) -> Result<()> { - self.send_cmd(WebSocketDriverCmd::Close).await?; - self.driver_handle.await.map_err(|e| { - Error::client_internal_error(format!("failed to join client driver async task: {}", e)) - })? - } -} - -#[async_trait] -impl FullClient for HttpWebSocketClient { - async fn subscribe_with_buf_size( - &mut self, - query: String, - buf_size: usize, - ) -> Result { - let (event_tx, event_rx) = mpsc::channel(buf_size); - let (result_tx, result_rx) = oneshot::channel(); - // We use the same ID for our subscription as for the JSONRPC request - // so that we can correlate incoming RPC responses with specific - // subscriptions. We use this to establish whether or not a - // subscription request was successful, as well as whether we support - // the remote endpoint's serialization format. - let id = SubscriptionId::default(); - let req = request::Wrapper::new_with_id( - id.clone().into(), - subscribe::Request::new(query.clone()), - ); - self.send_cmd(WebSocketDriverCmd::Subscribe { - req, - event_tx, - result_tx, - }) - .await?; - // Wait to make sure our subscription request went through - // successfully. - result_rx.await.map_err(|e| { - Error::client_internal_error(format!( - "failed to receive response from client driver for subscription request: {}", - e - )) - })??; - Ok(Subscription::new(id, query, event_rx)) - } - - async fn unsubscribe(&mut self, subscription: Subscription) -> Result<()> { - let (result_tx, result_rx) = oneshot::channel(); - self.send_cmd(WebSocketDriverCmd::Unsubscribe { - req: request::Wrapper::new_with_id( - subscription.id.clone().into(), - unsubscribe::Request::new(subscription.query.clone()), - ), - subscription, - result_tx, - }) - .await?; - result_rx.await.map_err(|e| { - Error::client_internal_error(format!( - "failed to receive response from client driver for unsubscribe request: {}", - e - )) - })? - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -struct GenericJSONResponse(serde_json::Value); - -impl Response for GenericJSONResponse {} - -#[derive(Debug)] -struct WebSocketSubscriptionDriver { - stream: WebSocketStream>, - router: SubscriptionRouter, - cmd_rx: mpsc::Receiver, -} - -impl WebSocketSubscriptionDriver { - fn new( - stream: WebSocketStream>, - cmd_rx: mpsc::Receiver, - ) -> Self { - Self { - stream, - router: SubscriptionRouter::default(), - cmd_rx, - } - } - - async fn run(mut self) -> Result<()> { - // TODO(thane): Should this loop initiate a keepalive (ping) to the - // server on a regular basis? - loop { - tokio::select! { - Some(res) = self.stream.next() => match res { - Ok(msg) => self.handle_incoming_msg(msg).await?, - Err(e) => return Err( - Error::websocket_error( - format!("failed to read from WebSocket connection: {}", e), - ), - ), - }, - Some(cmd) = self.cmd_rx.next() => match cmd { - WebSocketDriverCmd::Subscribe { - req, - event_tx, - result_tx, - } => self.subscribe(req, event_tx, result_tx).await?, - WebSocketDriverCmd::Unsubscribe { req, subscription, result_tx } => { - self.unsubscribe(req, subscription, result_tx).await? - } - WebSocketDriverCmd::Close => return self.close().await, - } - } - } - } - - async fn send(&mut self, msg: Message) -> Result<()> { - self.stream.send(msg).await.map_err(|e| { - Error::websocket_error(format!("failed to write to WebSocket connection: {}", e)) - }) - } - - async fn subscribe( - &mut self, - req: request::Wrapper, - event_tx: EventTx, - result_tx: PendingResultTx, - ) -> Result<()> { - // We require the outgoing request to have an ID that we can use as the - // subscription ID. - let subs_id = match req.id().clone().try_into() { - Ok(id) => id, - Err(e) => { - let _ = result_tx.send(Err(e)); - return Ok(()); - } - }; - if let Err(e) = self - .send(Message::Text(serde_json::to_string_pretty(&req).unwrap())) - .await - { - if result_tx.send(Err(e)).is_err() { - return Err(Error::client_internal_error( - "failed to respond internally to subscription request", - )); - } - // One failure shouldn't bring down the entire client. - return Ok(()); - } - self.router - .add_pending_subscribe(subs_id, req.params().query.clone(), event_tx, result_tx); - Ok(()) - } - - async fn unsubscribe( - &mut self, - req: request::Wrapper, - subscription: Subscription, - result_tx: PendingResultTx, - ) -> Result<()> { - if let Err(e) = self - .send(Message::Text(serde_json::to_string_pretty(&req).unwrap())) - .await - { - if result_tx.send(Err(e)).is_err() { - return Err(Error::client_internal_error( - "failed to respond internally to unsubscribe request", - )); - } - return Ok(()); - } - self.router.add_pending_unsubscribe(subscription, result_tx); - 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::Pong(_) | Message::Binary(_) => Ok(()), - Message::Close(_) => Ok(()), - } - } - - async fn handle_text_msg(&mut self, msg: String) -> Result<()> { - match Event::from_string(&msg) { - Ok(ev) => { - self.router.publish(ev).await; - Ok(()) - } - Err(_) => match serde_json::from_str::>(&msg) { - Ok(wrapper) => self.handle_generic_response(wrapper).await, - _ => Ok(()), - }, - } - } - - async fn handle_generic_response( - &mut self, - wrapper: response::Wrapper, - ) -> Result<()> { - let subs_id: SubscriptionId = match wrapper.id().clone().try_into() { - Ok(id) => id, - // Just ignore the message if it doesn't have an intelligible ID. - Err(_) => return Ok(()), - }; - match wrapper.into_result() { - Ok(_) => match self.router.subscription_state(&subs_id) { - SubscriptionState::Pending => { - let _ = self.router.confirm_pending_subscribe(&subs_id); - } - SubscriptionState::Cancelling => { - let _ = self.router.confirm_pending_unsubscribe(&subs_id); - } - SubscriptionState::Active => { - if let Some(event_tx) = self.router.get_active_subscription_mut(&subs_id) { - 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; - } - } - SubscriptionState::NotFound => (), - }, - Err(e) => match self.router.subscription_state(&subs_id) { - SubscriptionState::Pending => { - let _ = self.router.cancel_pending_subscribe(&subs_id, e); - } - SubscriptionState::Cancelling => { - let _ = self.router.cancel_pending_unsubscribe(&subs_id, e); - } - // 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(&subs_id) { - // TODO(thane): Does an error here warrant terminating the subscription, or the driver? - let _ = event_tx.send(Err(e)).await; - } - } - SubscriptionState::NotFound => (), - }, - } - - Ok(()) - } - - async fn pong(&mut self, v: Vec) -> Result<()> { - self.send(Message::Pong(v)).await - } - - async fn close(mut self) -> Result<()> { - self.send(Message::Close(Some(CloseFrame { - code: CloseCode::Normal, - reason: Cow::from("client closed WebSocket connection"), - }))) - .await?; - - while let Some(res) = self.stream.next().await { - if res.is_err() { - return Ok(()); - } - } - Ok(()) - } -} - -#[derive(Debug)] -enum WebSocketDriverCmd { - Subscribe { - req: request::Wrapper, - event_tx: mpsc::Sender>, - result_tx: oneshot::Sender>, - }, - Unsubscribe { - req: request::Wrapper, - subscription: Subscription, - result_tx: oneshot::Sender>, - }, - Close, -} - -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()) -} - -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/mock.rs b/rpc/src/client/transport/mock.rs new file mode 100644 index 000000000..d6efaa404 --- /dev/null +++ b/rpc/src/client/transport/mock.rs @@ -0,0 +1,153 @@ +//! Mock client implementation for use in testing. + +#[cfg(feature = "subscription")] +mod subscription; +#[cfg(feature = "subscription")] +pub use subscription::MockSubscriptionClient; + +use crate::client::ClosableClient; +use crate::{Client, Error, Method, Request, Response, Result}; +use async_trait::async_trait; +use std::collections::HashMap; + +/// A mock client implementation for use in testing. +/// +/// ## Examples +/// +/// ```rust +/// use tendermint_rpc::{Client, Method, MockClient, MockRequestMatcher, MockRequestMethodMatcher}; +/// +/// const ABCI_INFO_RESPONSE: &str = r#"{ +/// "jsonrpc": "2.0", +/// "id": "", +/// "result": { +/// "response": { +/// "data": "GaiaApp", +/// "last_block_height": "488120", +/// "last_block_app_hash": "2LnCw0fN+Zq/gs5SOuya/GRHUmtWftAqAkTUuoxl4g4=" +/// } +/// } +/// }"#; +/// +/// #[tokio::main] +/// async fn main() { +/// let matcher = MockRequestMethodMatcher::default() +/// .map(Method::AbciInfo, Ok(ABCI_INFO_RESPONSE.to_string())); +/// let client = MockClient::new(matcher); +/// +/// let abci_info = client.abci_info().await.unwrap(); +/// println!("Got mock ABCI info: {:?}", abci_info); +/// assert_eq!("GaiaApp".to_string(), abci_info.data); +/// } +/// ``` +#[derive(Debug)] +pub struct MockClient { + matcher: M, +} + +#[async_trait] +impl Client for MockClient { + async fn perform(&self, request: R) -> Result + where + R: Request, + { + self.matcher.response_for(request).ok_or_else(|| { + Error::client_internal_error("no matching response for incoming request") + })? + } +} + +#[async_trait] +impl ClosableClient for MockClient { + async fn close(self) -> Result<()> { + Ok(()) + } +} + +impl MockClient { + /// Create a new mock RPC client using the given request matcher. + pub fn new(matcher: M) -> Self { + Self { matcher } + } +} + +/// A trait required by the [`MockClient`] that allows for different approaches +/// to mocking responses for specific requests. +pub trait MockRequestMatcher: Send + Sync { + /// Provide the corresponding response for the given request (if any). + fn response_for(&self, request: R) -> Option> + where + R: Request; +} + +/// Provides a simple [`MockRequestMatcher`] implementation that simply maps +/// requests with specific methods to responses. +/// +/// [`MockRequestMatcher`]: trait.MockRequestMatcher.html +#[derive(Debug)] +pub struct MockRequestMethodMatcher { + mappings: HashMap>, +} + +impl MockRequestMatcher for MockRequestMethodMatcher { + fn response_for(&self, request: R) -> Option> + where + R: Request, + { + self.mappings.get(&request.method()).map(|res| match res { + Ok(json) => R::Response::from_string(json), + Err(e) => Err(e.clone()), + }) + } +} + +impl Default for MockRequestMethodMatcher { + fn default() -> Self { + Self { + mappings: HashMap::new(), + } + } +} + +impl MockRequestMethodMatcher { + /// Maps all incoming requests with the given method such that their + /// corresponding response will be `response`. + /// + /// Successful responses must be JSON-encoded. + #[allow(dead_code)] + pub fn map(mut self, method: Method, response: Result) -> Self { + self.mappings.insert(method, response); + self + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::path::PathBuf; + use tendermint::block::Height; + use tendermint::chain::Id; + 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() + } + + #[tokio::test] + async fn mock_client() { + let matcher = MockRequestMethodMatcher::default() + .map(Method::AbciInfo, Ok(read_json_fixture("abci_info").await)) + .map(Method::Block, Ok(read_json_fixture("block").await)); + let client = MockClient::new(matcher); + + let abci_info = client.abci_info().await.unwrap(); + assert_eq!("GaiaApp".to_string(), abci_info.data); + assert_eq!(Height::from(488120), abci_info.last_block_height); + + let block = client.block(Height::from(10)).await.unwrap().block; + assert_eq!(Height::from(10), block.header.height); + assert_eq!("cosmoshub-2".parse::().unwrap(), block.header.chain_id); + } +} diff --git a/rpc/src/client/transport/mock/subscription.rs b/rpc/src/client/transport/mock/subscription.rs new file mode 100644 index 000000000..8cb18fd9c --- /dev/null +++ b/rpc/src/client/transport/mock/subscription.rs @@ -0,0 +1,245 @@ +//! Subscription functionality for the Tendermint RPC mock client. + +use crate::client::subscription::SubscriptionTermination; +use crate::client::sync::{bounded, unbounded, ChannelRx, ChannelTx}; +use crate::client::{ClosableClient, SubscriptionRouter}; +use crate::event::Event; +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, + terminate_tx: ChannelTx, +} + +#[async_trait] +impl SubscriptionClient for MockSubscriptionClient { + async fn subscribe_with_buf_size( + &mut self, + query: String, + buf_size: usize, + ) -> Result { + let (event_tx, event_rx) = if buf_size == 0 { + unbounded() + } else { + bounded(buf_size) + }; + let (result_tx, mut result_rx) = unbounded(); + let id = SubscriptionId::default(); + self.send_cmd(DriverCmd::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.terminate_tx.clone(), + )) + } +} + +#[async_trait] +impl ClosableClient for MockSubscriptionClient { + async fn close(mut self) -> Result<()> { + self.send_cmd(DriverCmd::Close).await?; + self.driver_hdl.await.map_err(|e| { + Error::client_internal_error(format!( + "failed to terminate mock client driver task: {}", + e + )) + })? + } +} + +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: DriverCmd) -> Result<()> { + self.cmd_tx.send(cmd).await + } +} + +impl Default for MockSubscriptionClient { + fn default() -> Self { + let (event_tx, event_rx) = unbounded(); + let (cmd_tx, cmd_rx) = unbounded(); + let (terminate_tx, terminate_rx) = unbounded(); + let driver = MockSubscriptionClientDriver::new(event_rx, cmd_rx, terminate_rx); + let driver_hdl = tokio::spawn(async move { driver.run().await }); + Self { + driver_hdl, + event_tx, + cmd_tx, + terminate_tx, + } + } +} + +#[derive(Debug)] +struct MockSubscriptionClientDriver { + event_rx: ChannelRx, + cmd_rx: ChannelRx, + terminate_rx: ChannelRx, + router: SubscriptionRouter, +} + +impl MockSubscriptionClientDriver { + fn new( + event_rx: ChannelRx, + cmd_rx: ChannelRx, + terminate_rx: ChannelRx, + ) -> Self { + Self { + event_rx, + cmd_rx, + terminate_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 { + DriverCmd::Subscribe { + id, + query, + event_tx, + result_tx, + } => self.subscribe(id, query, event_tx, result_tx).await?, + DriverCmd::Close => return Ok(()), + }, + Some(subs_term) = self.terminate_rx.recv() => self.unsubscribe(subs_term).await?, + } + } + } + + async fn subscribe( + &mut self, + id: SubscriptionId, + query: String, + event_tx: ChannelTx>, + mut result_tx: ChannelTx>, + ) -> Result<()> { + self.router.add(id, query, event_tx); + result_tx.send(Ok(())).await + } + + async fn unsubscribe(&mut self, mut subs_term: SubscriptionTermination) -> Result<()> { + self.router + .remove(subs_term.query.clone(), subs_term.id.clone()); + subs_term.result_tx.send(Ok(())).await + } +} + +#[derive(Debug)] +pub enum DriverCmd { + Subscribe { + id: SubscriptionId, + query: String, + event_tx: ChannelTx>, + result_tx: ChannelTx>, + }, + Close, +} + +#[cfg(test)] +mod test { + use super::*; + 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 events = vec![ + read_event("event_new_block_1").await, + read_event("event_new_block_2").await, + read_event("event_new_block_3").await, + ]; + + let subs1 = client + .subscribe("tm.event='NewBlock'".to_string()) + .await + .unwrap(); + let subs2 = client + .subscribe("tm.event='NewBlock'".to_string()) + .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, subs2_events) = + (subs1_events.await.unwrap(), 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/websocket.rs b/rpc/src/client/transport/websocket.rs new file mode 100644 index 000000000..59ee1e10c --- /dev/null +++ b/rpc/src/client/transport/websocket.rs @@ -0,0 +1,360 @@ +//! WebSocket-based clients for accessing Tendermint RPC functionality. + +use crate::client::subscription::{SubscriptionState, SubscriptionTermination}; +use crate::client::sync::{bounded, unbounded, ChannelRx, ChannelTx}; +use crate::client::transport::get_tcp_host_port; +use crate::client::{ClosableClient, SubscriptionRouter}; +use crate::endpoint::{subscribe, unsubscribe}; +use crate::event::Event; +use crate::request::Wrapper; +use crate::{response, Error, Response, Result, Subscription, SubscriptionClient, SubscriptionId}; +use async_trait::async_trait; +use async_tungstenite::tokio::{connect_async, TokioAdapter}; +use async_tungstenite::tungstenite::protocol::frame::coding::CloseCode; +use async_tungstenite::tungstenite::protocol::CloseFrame; +use async_tungstenite::tungstenite::Message; +use async_tungstenite::WebSocketStream; +use futures::{SinkExt, StreamExt}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::convert::TryInto; +use tendermint::net; +use tokio::net::TcpStream; +use tokio::task::JoinHandle; + +/// WebSocket-based Tendermint RPC client that only provides [`Event`] +/// subscription capabilities. +/// +/// In order to not block the calling task, this client spawns an asynchronous +/// driver that continuously interacts with the actual WebSocket connection. +/// The `WebSocketSubscriptionClient` itself is effectively just a handle to +/// this driver. This driver is spawned as the client is created. +/// +/// To terminate the client and the driver, simply use its [`close`] method. +/// +/// ## Examples +/// +/// We don't test this example automatically at present, but it has and can +/// been tested against a Tendermint node running on `localhost`. +/// +/// ```rust,ignore +/// use tendermint_rpc::{WebSocketSubscriptionClient, SubscriptionClient, ClosableClient}; +/// use futures::StreamExt; +/// +/// #[tokio::main] +/// async fn main() { +/// let mut client = WebSocketSubscriptionClient::new("tcp://127.0.0.1:26657".parse().unwrap()) +/// .await +/// .unwrap(); +/// +/// let mut subs = client.subscribe("tm.event='NewBlock'".to_string()) +/// .await +/// .unwrap(); +/// +/// // Grab 5 NewBlock events +/// let mut ev_count = 5_i32; +/// +/// while let Some(res) = subs.next().await { +/// let ev = res.unwrap(); +/// println!("Got event: {:?}", ev); +/// ev_count -= 1; +/// if ev_count < 0 { +/// break +/// } +/// } +/// +/// // Sends an unsubscribe request via the WebSocket connection, but keeps +/// // the connection open. +/// subs.terminate().await.unwrap(); +/// +/// // Attempt to gracefully terminate the WebSocket connection. +/// client.close().await.unwrap(); +/// } +/// ``` +/// +/// [`Event`]: ./event/struct.Event.html +/// [`close`]: struct.WebSocketSubscriptionClient.html#method.close +#[derive(Debug)] +pub struct WebSocketSubscriptionClient { + host: String, + port: u16, + driver_handle: JoinHandle>, + cmd_tx: ChannelTx, + terminate_tx: ChannelTx, +} + +impl WebSocketSubscriptionClient { + /// Construct a WebSocket client. Immediately attempts to open a WebSocket + /// connection to the node with the given address. + pub async fn new(address: net::Address) -> Result { + let (host, port) = get_tcp_host_port(address)?; + let (stream, _response) = + connect_async(&format!("ws://{}:{}/websocket", &host, port)).await?; + let (cmd_tx, cmd_rx) = unbounded(); + let (terminate_tx, terminate_rx) = unbounded(); + let driver = WebSocketSubscriptionDriver::new(stream, cmd_rx, terminate_rx); + let driver_handle = tokio::spawn(async move { driver.run().await }); + Ok(Self { + host, + port, + driver_handle, + cmd_tx, + terminate_tx, + }) + } + + async fn send_cmd(&mut self, cmd: WebSocketDriverCmd) -> Result<()> { + self.cmd_tx.send(cmd).await.map_err(|e| { + Error::client_internal_error(format!("failed to send command to client driver: {}", e)) + }) + } +} + +#[async_trait] +impl SubscriptionClient for WebSocketSubscriptionClient { + async fn subscribe_with_buf_size( + &mut self, + query: String, + buf_size: usize, + ) -> Result { + let (event_tx, event_rx) = if buf_size == 0 { + unbounded() + } else { + bounded(buf_size) + }; + let (result_tx, mut result_rx) = unbounded::>(); + let id = SubscriptionId::default(); + self.send_cmd(WebSocketDriverCmd::Subscribe { + id: id.clone(), + query: query.clone(), + event_tx, + result_tx, + }) + .await?; + // Wait to make sure our subscription request went through + // successfully. + result_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.terminate_tx.clone(), + )) + } +} + +#[async_trait] +impl ClosableClient for WebSocketSubscriptionClient { + /// Attempt to gracefully close the WebSocket connection. + async fn close(mut self) -> Result<()> { + self.cmd_tx.send(WebSocketDriverCmd::Close).await?; + self.driver_handle.await.map_err(|e| { + Error::client_internal_error(format!( + "failed while waiting for WebSocket driver task to terminate: {}", + e + )) + })? + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct GenericJSONResponse(serde_json::Value); + +impl Response for GenericJSONResponse {} + +#[derive(Debug)] +struct WebSocketSubscriptionDriver { + stream: WebSocketStream>, + router: SubscriptionRouter, + cmd_rx: ChannelRx, + terminate_rx: ChannelRx, +} + +impl WebSocketSubscriptionDriver { + fn new( + stream: WebSocketStream>, + cmd_rx: ChannelRx, + terminate_rx: ChannelRx, + ) -> Self { + Self { + stream, + router: SubscriptionRouter::default(), + cmd_rx, + terminate_rx, + } + } + + async fn run(mut self) -> Result<()> { + // TODO(thane): Should this loop initiate a keepalive (ping) to the + // server on a regular basis? + loop { + tokio::select! { + Some(res) = self.stream.next() => match res { + Ok(msg) => self.handle_incoming_msg(msg).await?, + Err(e) => return Err( + Error::websocket_error( + format!("failed to read from WebSocket connection: {}", e), + ), + ), + }, + Some(cmd) = self.cmd_rx.recv() => match cmd { + WebSocketDriverCmd::Subscribe { + id, + query, + event_tx, + result_tx, + } => self.subscribe(id, query, event_tx, result_tx).await?, + WebSocketDriverCmd::Close => return self.close().await, + }, + Some(term) = self.terminate_rx.recv() => self.unsubscribe(term).await?, + } + } + } + + async fn send(&mut self, msg: Message) -> Result<()> { + self.stream.send(msg).await.map_err(|e| { + Error::websocket_error(format!("failed to write to WebSocket connection: {}", e)) + }) + } + + async fn subscribe( + &mut self, + id: SubscriptionId, + query: String, + event_tx: ChannelTx>, + mut result_tx: ChannelTx>, + ) -> Result<()> { + let req = Wrapper::new_with_id(id.clone().into(), subscribe::Request::new(query.clone())); + if let Err(e) = self + .send(Message::Text(serde_json::to_string_pretty(&req).unwrap())) + .await + { + let _ = result_tx.send(Err(e)).await; + return Ok(()); + } + self.router.pending_add(id, query, event_tx, result_tx); + Ok(()) + } + + async fn unsubscribe(&mut self, mut term: SubscriptionTermination) -> Result<()> { + let req = Wrapper::new_with_id( + term.id.clone().into(), + unsubscribe::Request::new(term.query.clone()), + ); + if let Err(e) = self + .send(Message::Text(serde_json::to_string_pretty(&req).unwrap())) + .await + { + let _ = term.result_tx.send(Err(e)).await; + return Ok(()); + } + self.router + .pending_remove(term.query.clone(), term.id.clone(), term.result_tx); + 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::Pong(_) | Message::Binary(_) => Ok(()), + Message::Close(_) => Ok(()), + } + } + + async fn handle_text_msg(&mut self, msg: String) -> Result<()> { + match Event::from_string(&msg) { + Ok(ev) => { + self.router.publish(ev).await; + Ok(()) + } + Err(_) => match serde_json::from_str::>(&msg) { + Ok(wrapper) => self.handle_generic_response(wrapper).await, + _ => Ok(()), + }, + } + } + + async fn handle_generic_response( + &mut self, + wrapper: response::Wrapper, + ) -> Result<()> { + let subs_id: SubscriptionId = match wrapper.id().clone().try_into() { + Ok(id) => id, + // Just ignore the message if it doesn't have an intelligible ID. + Err(_) => return Ok(()), + }; + match wrapper.into_result() { + Ok(_) => match self.router.subscription_state(&subs_id) { + SubscriptionState::Pending => { + let _ = self.router.confirm_add(&subs_id).await; + } + SubscriptionState::Cancelling => { + let _ = self.router.confirm_remove(&subs_id).await; + } + SubscriptionState::Active => { + if let Some(event_tx) = self.router.get_active_subscription_mut(&subs_id) { + 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; + } + } + SubscriptionState::NotFound => (), + }, + Err(e) => match self.router.subscription_state(&subs_id) { + SubscriptionState::Pending => { + let _ = self.router.cancel_add(&subs_id, e).await; + } + SubscriptionState::Cancelling => { + let _ = self.router.cancel_remove(&subs_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(&subs_id) { + // TODO(thane): Does an error here warrant terminating the subscription, or the driver? + let _ = event_tx.send(Err(e)).await; + } + } + SubscriptionState::NotFound => (), + }, + } + + Ok(()) + } + + async fn pong(&mut self, v: Vec) -> Result<()> { + self.send(Message::Pong(v)).await + } + + async fn close(mut self) -> Result<()> { + self.send(Message::Close(Some(CloseFrame { + code: CloseCode::Normal, + reason: Cow::from("client closed WebSocket connection"), + }))) + .await?; + + while let Some(res) = self.stream.next().await { + if res.is_err() { + return Ok(()); + } + } + Ok(()) + } +} + +#[derive(Debug)] +enum WebSocketDriverCmd { + Subscribe { + id: SubscriptionId, + query: String, + event_tx: ChannelTx>, + result_tx: ChannelTx>, + }, + Close, +} diff --git a/rpc/src/error.rs b/rpc/src/error.rs index 1228fdcda..7cb0a3f37 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -1,6 +1,6 @@ //! JSONRPC error types -#[cfg(feature = "client")] +#[cfg(all(feature = "client", feature = "transport_websocket"))] use async_tungstenite::tungstenite::Error as WSError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -123,7 +123,7 @@ impl From for Error { } } -#[cfg(feature = "client")] +#[cfg(all(feature = "client", feature = "transport_websocket"))] impl From for Error { fn from(websocket_error: WSError) -> Error { Error::websocket_error(websocket_error.to_string()) @@ -140,8 +140,8 @@ pub enum Code { #[error("HTTP error")] HttpError, - /// Low-level Websocket error - #[error("Websocket Error")] + /// Low-level WebSocket error + #[error("WebSocket Error")] WebSocketError, /// An internal error occurred within the client. diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index c7bb95e86..ec6a05c00 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -2,39 +2,51 @@ //! //! ## Client //! -//! Available when specifying the `client` feature flag. +//! This crate optionally provides access to various levels of client +//! functionality and transports. //! -//! The RPC client comes in two flavors: a [`MinimalClient`] and a -//! [`FullClient`]. A `MinimalClient` implementation provides access to all -//! RPC endpoints with the exception of the [`Event`] subscription ones, -//! whereas a `FullClient` implementation provides access to all RPC -//! functionality. The reason for this distinction is because `Event` -//! subscription usually requires more resources to manage, and may not be -//! necessary for all applications making use of the Tendermint RPC. +//! By only specifying the `client` feature flag, all you get access to is the +//! [`Client`] trait. Additional feature flags for access to different aspects +//! of the client and different transports are as follows: //! -//! Transport-specific client support is provided by way of additional feature -//! flags (where right now we only have one transport, but intend on providing -//! more in future): +//! | Features | Description | +//! | -------- | ----------- | +//! | `client`, `transport_http` | 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. | +//! | `client`, `subscription`, `transport_websocket` | Provides [`WebSocketSubscriptionClient`], which provides [`Event`] subscription functionality over a WebSocket connection. See the [`/subscribe` endpoint] in the Tendermint RPC for more details. | +//! | `client`, `transport_mock` | Provides [`MockClient`], which only implements [`Client`] (no subscription functionality) for aiding in testing your application that depends on the Tendermint RPC client. | +//! | `client`, `subscription`, `transport_mock` | In addition to [`MockClient`], this will give you access to [`MockSubscriptionClient`], which helps you simulate subscriptions to events being generated by a Tendermint node. | //! -//! * `http_ws`: Provides an HTTP interface for request/response interactions -//! (see [`HttpClient`]), and a WebSocket-based interface for `Event` -//! subscription (see [`HttpWebSocketClient`]). -//! -//! [`MinimalClient`]: trait.MinimalClient.html -//! [`FullClient`]: trait.FullClient.html -//! [`Event`]: event/struct.Event.html +//! [`Client`]: trait.Client.html //! [`HttpClient`]: struct.HttpClient.html -//! [`HttpWebSocketClient`]: struct.HttpWebSocketClient.html +//! [`Event`]: event/struct.Event.html +//! [`WebSocketSubscriptionClient`]: struct.WebSocketSubscriptionClient.html +//! [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(feature = "client")] mod client; +#[cfg(all(feature = "client", feature = "transport_http"))] +pub use client::HttpClient; +#[cfg(all( + feature = "client", + feature = "subscription", + feature = "transport_mock" +))] +pub use client::MockSubscriptionClient; +#[cfg(all( + feature = "client", + feature = "subscription", + feature = "transport_websocket" +))] +pub use client::WebSocketSubscriptionClient; #[cfg(feature = "client")] -pub use client::{ - EventRx, EventTx, FullClient, MinimalClient, PendingResultTx, Subscription, SubscriptionId, - SubscriptionRouter, DEFAULT_SUBSCRIPTION_BUF_SIZE, -}; -#[cfg(feature = "http_ws")] -pub use client::{HttpClient, HttpWebSocketClient}; +pub use client::{Client, ClosableClient}; +#[cfg(all(feature = "client", feature = "transport_mock"))] +pub use client::{MockClient, MockRequestMatcher, MockRequestMethodMatcher}; +#[cfg(all(feature = "client", feature = "subscription"))] +pub use client::{Subscription, SubscriptionClient, SubscriptionId}; pub mod endpoint; pub mod error; diff --git a/rpc/tests/client.rs b/rpc/tests/client.rs deleted file mode 100644 index 2af133edf..000000000 --- a/rpc/tests/client.rs +++ /dev/null @@ -1,258 +0,0 @@ -//! Tendermint RPC client tests. - -use async_trait::async_trait; -use futures::stream::StreamExt; -use std::collections::HashMap; -use std::path::PathBuf; -use tendermint::block::Height; -use tendermint_rpc::event::{Event, EventData, WrappedEvent}; -use tendermint_rpc::{ - Error, EventTx, FullClient, Method, MinimalClient, Request, Response, Result, Subscription, - SubscriptionId, SubscriptionRouter, -}; -use tokio::fs; -use tokio::sync::{mpsc, oneshot}; -use tokio::task::JoinHandle; - -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() -} - -#[derive(Debug)] -struct MockClient { - responses: HashMap, - driver_handle: JoinHandle>, - event_tx: mpsc::Sender, - cmd_tx: mpsc::Sender, -} - -#[async_trait] -impl MinimalClient for MockClient { - async fn perform(&self, request: R) -> Result - where - R: Request, - { - self.responses - .get(&request.method()) - .and_then(|res| Some(R::Response::from_string(res).unwrap())) - .ok_or_else(|| { - Error::http_error(format!( - "no response mapping for request method: {}", - request.method() - )) - }) - } - - async fn close(mut self) -> Result<()> { - self.cmd_tx.send(MockClientCmd::Close).await.unwrap(); - self.driver_handle.await.unwrap() - } -} - -#[async_trait] -impl FullClient for MockClient { - async fn subscribe_with_buf_size( - &mut self, - query: String, - buf_size: usize, - ) -> Result { - let (event_tx, event_rx) = mpsc::channel(buf_size); - let (response_tx, response_rx) = oneshot::channel(); - let id = SubscriptionId::default(); - self.cmd_tx - .send(MockClientCmd::Subscribe { - id: id.clone(), - query: query.clone(), - event_tx, - response_tx, - }) - .await - .unwrap(); - // We need to wait until the subscription's been created, otherwise we - // risk introducing nondeterminism into the tests. - response_rx.await.unwrap().unwrap(); - Ok(Subscription::new(id, query, event_rx)) - } - - async fn unsubscribe(&mut self, subscription: Subscription) -> Result<()> { - Ok(self - .cmd_tx - .send(MockClientCmd::Unsubscribe(subscription)) - .await - .unwrap()) - } -} - -impl MockClient { - fn new() -> Self { - let (event_tx, event_rx) = mpsc::channel(10); - let (cmd_tx, cmd_rx) = mpsc::channel(10); - let driver = MockClientDriver::new(event_rx, cmd_rx); - let driver_hdl = tokio::spawn(async move { driver.run().await }); - Self { - responses: HashMap::new(), - driver_handle: driver_hdl, - event_tx, - cmd_tx, - } - } - - fn map(mut self, method: Method, response: String) -> Self { - self.responses.insert(method, response); - self - } - - async fn publish(&mut self, ev: Event) { - match &ev.data { - EventData::NewBlock { block, .. } => println!( - "Sending NewBlock event for height {}", - block.as_ref().unwrap().header.height - ), - _ => (), - } - self.event_tx.send(ev).await.unwrap(); - } -} - -#[derive(Debug)] -enum MockClientCmd { - Subscribe { - id: SubscriptionId, - query: String, - event_tx: EventTx, - response_tx: oneshot::Sender>, - }, - Unsubscribe(Subscription), - Close, -} - -#[derive(Debug)] -struct MockClientDriver { - event_rx: mpsc::Receiver, - cmd_rx: mpsc::Receiver, - router: SubscriptionRouter, -} - -impl MockClientDriver { - // `event_rx` simulates an incoming event stream (e.g. by way of the - // WebSocket connection). - fn new(event_rx: mpsc::Receiver, cmd_rx: mpsc::Receiver) -> Self { - Self { - event_rx, - cmd_rx, - router: SubscriptionRouter::default(), - } - } - - async fn run(mut self) -> Result<()> { - loop { - tokio::select! { - Some(ev) = self.event_rx.next() => { - match &ev.data { - EventData::NewBlock { block, .. } => println!( - "Publishing NewBlock event at height {}", - block.as_ref().unwrap().header.height, - ), - _ => (), - } - self.router.publish(ev).await; - () - } - Some(cmd) = self.cmd_rx.next() => match cmd { - MockClientCmd::Subscribe { id, query, event_tx, response_tx } => { - self.router.add(id, query, event_tx); - response_tx.send(Ok(())).unwrap(); - () - }, - MockClientCmd::Unsubscribe(subs) => { - self.router.remove(subs); - () - }, - MockClientCmd::Close => return Ok(()), - } - } - } - } -} - -#[tokio::test] -async fn minimal_client() { - let client = MockClient::new() - .map(Method::AbciInfo, read_json_fixture("abci_info").await) - .map(Method::Block, read_json_fixture("block").await); - - let abci_info = client.abci_info().await.unwrap(); - assert_eq!("GaiaApp".to_string(), abci_info.data); - - let block = client.block(10).await.unwrap().block; - assert_eq!(Height::from(10), block.header.height); - assert_eq!("cosmoshub-2", block.header.chain_id.as_str()); - - client.close().await.unwrap(); -} - -#[tokio::test] -async fn full_client() { - let mut client = MockClient::new(); - let incoming_events = vec![ - read_event("event_new_block_1").await, - read_event("event_new_block_2").await, - read_event("event_new_block_3").await, - ]; - let expected_heights = vec![Height::from(165), Height::from(166), Height::from(167)]; - - let subs1 = client - .subscribe("tm.event='NewBlock'".to_string()) - .await - .unwrap(); - let subs2 = client - .subscribe("tm.event='NewBlock'".to_string()) - .await - .unwrap(); - - let subs1_events_task = - tokio::spawn(async move { subs1.take(3).collect::>>().await }); - let subs2_events_task = - tokio::spawn(async move { subs2.take(3).collect::>>().await }); - - println!("Publishing incoming events..."); - for ev in incoming_events { - client.publish(ev).await; - } - - println!("Collecting incoming events..."); - let subs1_events = subs1_events_task.await.unwrap(); - let subs2_events = subs2_events_task.await.unwrap(); - - client.close().await.unwrap(); - - assert_eq!(3, subs1_events.len()); - assert_eq!(3, subs2_events.len()); - - println!("Checking collected events..."); - for i in 0..3 { - let subs1_event = subs1_events[i].as_ref().unwrap(); - let subs2_event = subs2_events[i].as_ref().unwrap(); - match &subs1_event.data { - EventData::NewBlock { block, .. } => { - assert_eq!(expected_heights[i], block.as_ref().unwrap().header.height); - } - _ => panic!("invalid event type for subs1: {:?}", subs1_event), - } - match &subs2_event.data { - EventData::NewBlock { block, .. } => { - assert_eq!(expected_heights[i], block.as_ref().unwrap().header.height); - } - _ => panic!("invalid event type for subs2: {:?}", subs2_event), - } - } -} diff --git a/rpc/tests/endpoint.rs b/rpc/tests/endpoint.rs deleted file mode 100644 index 9ac9f0bba..000000000 --- a/rpc/tests/endpoint.rs +++ /dev/null @@ -1,292 +0,0 @@ -//! Tendermint RPC endpoint testing. - -use std::{fs, path::PathBuf}; -use tendermint::abci::Code; - -use tendermint_rpc::{self as rpc, endpoint, Response}; - -const EXAMPLE_APP: &str = "GaiaApp"; -const EXAMPLE_CHAIN: &str = "cosmoshub-2"; - -fn read_json_fixture(name: &str) -> String { - fs::read_to_string(PathBuf::from("./tests/support/").join(name.to_owned() + ".json")).unwrap() -} - -#[test] -fn abci_info() { - let response = endpoint::abci_info::Response::from_string(&read_json_fixture("abci_info")) - .unwrap() - .response; - - assert_eq!(response.data.as_str(), EXAMPLE_APP); - assert_eq!(response.last_block_height.value(), 488_120); -} - -#[test] -fn abci_query() { - let response = endpoint::abci_query::Response::from_string(&read_json_fixture("abci_query")) - .unwrap() - .response; - - assert_eq!(response.height.value(), 1); -} - -#[test] -fn block() { - let response = endpoint::block::Response::from_string(&read_json_fixture("block")).unwrap(); - - let tendermint::Block { - header, - data, - evidence, - last_commit, - } = response.block; - - assert_eq!(header.version.block, 10); - assert_eq!(header.chain_id.as_str(), EXAMPLE_CHAIN); - assert_eq!(header.height.value(), 10); - assert_eq!(data.iter().len(), 0); - assert_eq!(evidence.iter().len(), 0); - assert_eq!(last_commit.unwrap().signatures.len(), 1); -} - -#[test] -fn block_with_evidences() { - let response = - endpoint::block::Response::from_string(&read_json_fixture("block_with_evidences")).unwrap(); - - let tendermint::Block { evidence, .. } = response.block; - let evidence = evidence.iter().next().unwrap(); - - match evidence { - tendermint::evidence::Evidence::DuplicateVote(_) => (), - _ => unreachable!(), - } -} - -// TODO: Update this test and its json file -// #[test] -// fn block_empty_block_id() { -// let response = -// endpoint::block::Response::from_string(&read_json_fixture("block_empty_block_id")) -// .unwrap(); -// -// let tendermint::Block { last_commit, .. } = response.block; -// -// assert_eq!(last_commit.as_ref().unwrap().precommits.len(), 2); -// assert!(last_commit.unwrap().precommits[0] -// .as_ref() -// .unwrap() -// .block_id -// .is_none()); -// } - -#[test] -fn first_block() { - let response = - endpoint::block::Response::from_string(&read_json_fixture("first_block")).unwrap(); - - let tendermint::Block { - header, - data, - evidence, - last_commit, - } = response.block; - - assert_eq!(header.version.block, 10); - assert_eq!(header.chain_id.as_str(), EXAMPLE_CHAIN); - assert_eq!(header.height.value(), 1); - assert!(header.last_block_id.is_none()); - - assert_eq!(data.iter().len(), 0); - assert_eq!(evidence.iter().len(), 0); - assert!(last_commit.is_none()); -} -#[test] -fn block_results() { - let response = - endpoint::block_results::Response::from_string(&read_json_fixture("block_results")) - .unwrap(); - assert_eq!(response.height.value(), 1814); - - let validator_updates = response.validator_updates; - let deliver_tx = response.txs_results.unwrap(); - let log_json = &deliver_tx[0].log.parse_json().unwrap(); - let log_json_value = &log_json.as_array().as_ref().unwrap()[0]; - - assert_eq!(log_json_value["msg_index"].as_str().unwrap(), "0"); - assert_eq!(log_json_value["success"].as_bool().unwrap(), true); - - assert_eq!(deliver_tx[0].gas_wanted.value(), 200_000); - assert_eq!(deliver_tx[0].gas_used.value(), 105_662); - - assert_eq!(validator_updates[0].power.value(), 1_233_243); -} - -#[test] -fn blockchain() { - let response = - endpoint::blockchain::Response::from_string(&read_json_fixture("blockchain")).unwrap(); - - assert_eq!(response.last_height.value(), 488_556); - assert_eq!(response.block_metas.len(), 10); - - let block_meta = &response.block_metas[0]; - assert_eq!(block_meta.header.chain_id.as_str(), EXAMPLE_CHAIN) -} - -#[test] -fn broadcast_tx_async() { - let response = endpoint::broadcast::tx_async::Response::from_string(&read_json_fixture( - "broadcast_tx_async", - )) - .unwrap(); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); -} - -#[test] -fn broadcast_tx_sync() { - let response = endpoint::broadcast::tx_sync::Response::from_string(&read_json_fixture( - "broadcast_tx_sync", - )) - .unwrap(); - - assert_eq!(response.code, Code::Ok); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); -} - -#[test] -fn broadcast_tx_sync_int() { - let response = endpoint::broadcast::tx_sync::Response::from_string(&read_json_fixture( - "broadcast_tx_sync_int", - )) - .unwrap(); - - assert_eq!(response.code, Code::Ok); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); -} - -#[test] -fn broadcast_tx_commit() { - let response = endpoint::broadcast::tx_commit::Response::from_string(&read_json_fixture( - "broadcast_tx_commit", - )) - .unwrap(); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); -} - -#[test] -fn broadcast_tx_commit_null_data() { - let response = endpoint::broadcast::tx_commit::Response::from_string(&read_json_fixture( - "broadcast_tx_commit_null_data", - )) - .unwrap(); - - assert_eq!( - &response.hash.to_string(), - "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" - ); -} - -#[test] -fn commit() { - let response = endpoint::commit::Response::from_string(&read_json_fixture("commit")).unwrap(); - let header = response.signed_header.header; - assert_eq!(header.chain_id.as_ref(), EXAMPLE_CHAIN); - // For now we just want to make sure the commit including precommits and a block_id exist - // in SignedHeader; later we should verify some properties: e.g. block_id.hash matches the - // header etc: - let commit = response.signed_header.commit; - let block_id = commit.block_id; - let _signatures = commit.signatures; - assert_eq!(header.hash(), block_id.hash); -} - -#[test] -fn commit_height_1() { - let response = endpoint::commit::Response::from_string(&read_json_fixture("commit_1")).unwrap(); - let header = response.signed_header.header; - let commit = response.signed_header.commit; - let block_id = commit.block_id; - assert_eq!(header.hash(), block_id.hash); -} - -#[test] -fn genesis() { - let response = endpoint::genesis::Response::from_string(&read_json_fixture("genesis")).unwrap(); - - let tendermint::Genesis { - chain_id, - consensus_params, - .. - } = response.genesis; - - assert_eq!(chain_id.as_str(), EXAMPLE_CHAIN); - assert_eq!(consensus_params.block.max_bytes, 200_000); -} - -#[test] -fn health() { - endpoint::health::Response::from_string(&read_json_fixture("health")).unwrap(); -} - -#[test] -fn net_info() { - let response = - endpoint::net_info::Response::from_string(&read_json_fixture("net_info")).unwrap(); - - assert_eq!(response.n_peers, 2); - assert_eq!(response.peers[0].node_info.network.as_str(), EXAMPLE_CHAIN); -} - -#[test] -fn status() { - let response = endpoint::status::Response::from_string(&read_json_fixture("status")).unwrap(); - - assert_eq!(response.node_info.network.as_str(), EXAMPLE_CHAIN); - assert_eq!(response.sync_info.latest_block_height.value(), 410_744); - assert_eq!(response.validator_info.voting_power.value(), 0); -} - -#[test] -fn validators() { - let response = - endpoint::validators::Response::from_string(&read_json_fixture("validators")).unwrap(); - - assert_eq!(response.block_height.value(), 42); - - let validators = response.validators; - assert_eq!(validators.len(), 65); -} - -#[test] -fn jsonrpc_error() { - let result = endpoint::blockchain::Response::from_string(&read_json_fixture("error")); - - if let Err(err) = result { - assert_eq!(err.code(), rpc::error::Code::InternalError); - assert_eq!(err.message(), "Internal error"); - assert_eq!( - err.data().unwrap(), - "min height 321 can't be greater than max height 123" - ); - } else { - panic!("expected error, got {:?}", result) - } -} diff --git a/rpc/tests/integration.rs b/rpc/tests/integration.rs index 04d58b42c..9ac9f0bba 100644 --- a/rpc/tests/integration.rs +++ b/rpc/tests/integration.rs @@ -1,4 +1,292 @@ -//! Tendermint RPC tests +//! Tendermint RPC endpoint testing. -mod client; -mod endpoint; +use std::{fs, path::PathBuf}; +use tendermint::abci::Code; + +use tendermint_rpc::{self as rpc, endpoint, Response}; + +const EXAMPLE_APP: &str = "GaiaApp"; +const EXAMPLE_CHAIN: &str = "cosmoshub-2"; + +fn read_json_fixture(name: &str) -> String { + fs::read_to_string(PathBuf::from("./tests/support/").join(name.to_owned() + ".json")).unwrap() +} + +#[test] +fn abci_info() { + let response = endpoint::abci_info::Response::from_string(&read_json_fixture("abci_info")) + .unwrap() + .response; + + assert_eq!(response.data.as_str(), EXAMPLE_APP); + assert_eq!(response.last_block_height.value(), 488_120); +} + +#[test] +fn abci_query() { + let response = endpoint::abci_query::Response::from_string(&read_json_fixture("abci_query")) + .unwrap() + .response; + + assert_eq!(response.height.value(), 1); +} + +#[test] +fn block() { + let response = endpoint::block::Response::from_string(&read_json_fixture("block")).unwrap(); + + let tendermint::Block { + header, + data, + evidence, + last_commit, + } = response.block; + + assert_eq!(header.version.block, 10); + assert_eq!(header.chain_id.as_str(), EXAMPLE_CHAIN); + assert_eq!(header.height.value(), 10); + assert_eq!(data.iter().len(), 0); + assert_eq!(evidence.iter().len(), 0); + assert_eq!(last_commit.unwrap().signatures.len(), 1); +} + +#[test] +fn block_with_evidences() { + let response = + endpoint::block::Response::from_string(&read_json_fixture("block_with_evidences")).unwrap(); + + let tendermint::Block { evidence, .. } = response.block; + let evidence = evidence.iter().next().unwrap(); + + match evidence { + tendermint::evidence::Evidence::DuplicateVote(_) => (), + _ => unreachable!(), + } +} + +// TODO: Update this test and its json file +// #[test] +// fn block_empty_block_id() { +// let response = +// endpoint::block::Response::from_string(&read_json_fixture("block_empty_block_id")) +// .unwrap(); +// +// let tendermint::Block { last_commit, .. } = response.block; +// +// assert_eq!(last_commit.as_ref().unwrap().precommits.len(), 2); +// assert!(last_commit.unwrap().precommits[0] +// .as_ref() +// .unwrap() +// .block_id +// .is_none()); +// } + +#[test] +fn first_block() { + let response = + endpoint::block::Response::from_string(&read_json_fixture("first_block")).unwrap(); + + let tendermint::Block { + header, + data, + evidence, + last_commit, + } = response.block; + + assert_eq!(header.version.block, 10); + assert_eq!(header.chain_id.as_str(), EXAMPLE_CHAIN); + assert_eq!(header.height.value(), 1); + assert!(header.last_block_id.is_none()); + + assert_eq!(data.iter().len(), 0); + assert_eq!(evidence.iter().len(), 0); + assert!(last_commit.is_none()); +} +#[test] +fn block_results() { + let response = + endpoint::block_results::Response::from_string(&read_json_fixture("block_results")) + .unwrap(); + assert_eq!(response.height.value(), 1814); + + let validator_updates = response.validator_updates; + let deliver_tx = response.txs_results.unwrap(); + let log_json = &deliver_tx[0].log.parse_json().unwrap(); + let log_json_value = &log_json.as_array().as_ref().unwrap()[0]; + + assert_eq!(log_json_value["msg_index"].as_str().unwrap(), "0"); + assert_eq!(log_json_value["success"].as_bool().unwrap(), true); + + assert_eq!(deliver_tx[0].gas_wanted.value(), 200_000); + assert_eq!(deliver_tx[0].gas_used.value(), 105_662); + + assert_eq!(validator_updates[0].power.value(), 1_233_243); +} + +#[test] +fn blockchain() { + let response = + endpoint::blockchain::Response::from_string(&read_json_fixture("blockchain")).unwrap(); + + assert_eq!(response.last_height.value(), 488_556); + assert_eq!(response.block_metas.len(), 10); + + let block_meta = &response.block_metas[0]; + assert_eq!(block_meta.header.chain_id.as_str(), EXAMPLE_CHAIN) +} + +#[test] +fn broadcast_tx_async() { + let response = endpoint::broadcast::tx_async::Response::from_string(&read_json_fixture( + "broadcast_tx_async", + )) + .unwrap(); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn broadcast_tx_sync() { + let response = endpoint::broadcast::tx_sync::Response::from_string(&read_json_fixture( + "broadcast_tx_sync", + )) + .unwrap(); + + assert_eq!(response.code, Code::Ok); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn broadcast_tx_sync_int() { + let response = endpoint::broadcast::tx_sync::Response::from_string(&read_json_fixture( + "broadcast_tx_sync_int", + )) + .unwrap(); + + assert_eq!(response.code, Code::Ok); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn broadcast_tx_commit() { + let response = endpoint::broadcast::tx_commit::Response::from_string(&read_json_fixture( + "broadcast_tx_commit", + )) + .unwrap(); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn broadcast_tx_commit_null_data() { + let response = endpoint::broadcast::tx_commit::Response::from_string(&read_json_fixture( + "broadcast_tx_commit_null_data", + )) + .unwrap(); + + assert_eq!( + &response.hash.to_string(), + "88D4266FD4E6338D13B845FCF289579D209C897823B9217DA3E161936F031589" + ); +} + +#[test] +fn commit() { + let response = endpoint::commit::Response::from_string(&read_json_fixture("commit")).unwrap(); + let header = response.signed_header.header; + assert_eq!(header.chain_id.as_ref(), EXAMPLE_CHAIN); + // For now we just want to make sure the commit including precommits and a block_id exist + // in SignedHeader; later we should verify some properties: e.g. block_id.hash matches the + // header etc: + let commit = response.signed_header.commit; + let block_id = commit.block_id; + let _signatures = commit.signatures; + assert_eq!(header.hash(), block_id.hash); +} + +#[test] +fn commit_height_1() { + let response = endpoint::commit::Response::from_string(&read_json_fixture("commit_1")).unwrap(); + let header = response.signed_header.header; + let commit = response.signed_header.commit; + let block_id = commit.block_id; + assert_eq!(header.hash(), block_id.hash); +} + +#[test] +fn genesis() { + let response = endpoint::genesis::Response::from_string(&read_json_fixture("genesis")).unwrap(); + + let tendermint::Genesis { + chain_id, + consensus_params, + .. + } = response.genesis; + + assert_eq!(chain_id.as_str(), EXAMPLE_CHAIN); + assert_eq!(consensus_params.block.max_bytes, 200_000); +} + +#[test] +fn health() { + endpoint::health::Response::from_string(&read_json_fixture("health")).unwrap(); +} + +#[test] +fn net_info() { + let response = + endpoint::net_info::Response::from_string(&read_json_fixture("net_info")).unwrap(); + + assert_eq!(response.n_peers, 2); + assert_eq!(response.peers[0].node_info.network.as_str(), EXAMPLE_CHAIN); +} + +#[test] +fn status() { + let response = endpoint::status::Response::from_string(&read_json_fixture("status")).unwrap(); + + assert_eq!(response.node_info.network.as_str(), EXAMPLE_CHAIN); + assert_eq!(response.sync_info.latest_block_height.value(), 410_744); + assert_eq!(response.validator_info.voting_power.value(), 0); +} + +#[test] +fn validators() { + let response = + endpoint::validators::Response::from_string(&read_json_fixture("validators")).unwrap(); + + assert_eq!(response.block_height.value(), 42); + + let validators = response.validators; + assert_eq!(validators.len(), 65); +} + +#[test] +fn jsonrpc_error() { + let result = endpoint::blockchain::Response::from_string(&read_json_fixture("error")); + + if let Err(err) = result { + assert_eq!(err.code(), rpc::error::Code::InternalError); + assert_eq!(err.message(), "Internal error"); + assert_eq!( + err.data().unwrap(), + "min height 321 can't be greater than max height 123" + ); + } else { + panic!("expected error, got {:?}", result) + } +} diff --git a/tendermint/Cargo.toml b/tendermint/Cargo.toml index ecc7958f2..796b33d93 100644 --- a/tendermint/Cargo.toml +++ b/tendermint/Cargo.toml @@ -57,7 +57,7 @@ zeroize = { version = "1.1", features = ["zeroize_derive"] } ripemd160 = { version = "0.9", optional = true } [dev-dependencies] -tendermint-rpc = { path = "../rpc", features = [ "client", "http_ws" ] } +tendermint-rpc = { path = "../rpc", features = [ "client", "subscription", "transport_http", "transport_websocket" ] } tokio = { version = "0.2", features = [ "macros" ] } [features] diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index 199ac51bf..6bd1e6cd4 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -11,7 +11,9 @@ mod rpc { use std::cmp::min; - use tendermint_rpc::{FullClient, HttpClient, HttpWebSocketClient, MinimalClient}; + use tendermint_rpc::{ + Client, ClosableClient, HttpClient, SubscriptionClient, WebSocketSubscriptionClient, + }; use futures::StreamExt; use tendermint::abci::Code; @@ -149,7 +151,7 @@ mod rpc { #[tokio::test] #[ignore] async fn subscription_interface() { - let mut client = HttpWebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) + let mut client = WebSocketSubscriptionClient::new("tcp://127.0.0.1:26657".parse().unwrap()) .await .unwrap(); let mut subs = client @@ -168,6 +170,7 @@ mod rpc { } } + subs.terminate().await.unwrap(); client.close().await.unwrap(); } } From 1c826385a92e6c56c3259f527972569be58c3ccd Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 26 Aug 2020 15:39:51 -0400 Subject: [PATCH 41/60] Minor documentation fixes Signed-off-by: Thane Thomson --- rpc/src/client/transport/http.rs | 5 +---- rpc/src/client/transport/mock.rs | 2 ++ rpc/src/client/transport/websocket.rs | 7 ++----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/rpc/src/client/transport/http.rs b/rpc/src/client/transport/http.rs index cda79d474..2fbe58c70 100644 --- a/rpc/src/client/transport/http.rs +++ b/rpc/src/client/transport/http.rs @@ -16,10 +16,7 @@ use tendermint::net; /// /// ## Examples /// -/// We don't test this example automatically at present, but it has and can -/// been tested against a Tendermint node running on `localhost`. -/// -/// ```ignore +/// ```rust,ignore /// use tendermint_rpc::{HttpClient, Client}; /// /// #[tokio::main] diff --git a/rpc/src/client/transport/mock.rs b/rpc/src/client/transport/mock.rs index d6efaa404..6e9f4608a 100644 --- a/rpc/src/client/transport/mock.rs +++ b/rpc/src/client/transport/mock.rs @@ -73,6 +73,8 @@ impl MockClient { /// A trait required by the [`MockClient`] that allows for different approaches /// to mocking responses for specific requests. +/// +/// [`MockClient`]: struct.MockClient.html pub trait MockRequestMatcher: Send + Sync { /// Provide the corresponding response for the given request (if any). fn response_for(&self, request: R) -> Option> diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index 59ee1e10c..e17a05486 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -22,8 +22,8 @@ use tendermint::net; use tokio::net::TcpStream; use tokio::task::JoinHandle; -/// WebSocket-based Tendermint RPC client that only provides [`Event`] -/// subscription capabilities. +/// Tendermint RPC client that provides [`Event`] subscription capabilities +/// over JSON-RPC over a WebSocket connection. /// /// In order to not block the calling task, this client spawns an asynchronous /// driver that continuously interacts with the actual WebSocket connection. @@ -34,9 +34,6 @@ use tokio::task::JoinHandle; /// /// ## Examples /// -/// We don't test this example automatically at present, but it has and can -/// been tested against a Tendermint node running on `localhost`. -/// /// ```rust,ignore /// use tendermint_rpc::{WebSocketSubscriptionClient, SubscriptionClient, ClosableClient}; /// use futures::StreamExt; From 772bcd096258a7e3bc802f7a18ee97be545e0440 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 26 Aug 2020 17:09:20 -0400 Subject: [PATCH 42/60] Add newline at end of JSON fixtures Signed-off-by: Thane Thomson --- rpc/tests/support/event_new_block_1.json | 2 +- rpc/tests/support/event_new_block_2.json | 2 +- rpc/tests/support/event_new_block_3.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rpc/tests/support/event_new_block_1.json b/rpc/tests/support/event_new_block_1.json index 5f711db36..5bc7ce652 100644 --- a/rpc/tests/support/event_new_block_1.json +++ b/rpc/tests/support/event_new_block_1.json @@ -70,4 +70,4 @@ ] } } -} \ No newline at end of file +} diff --git a/rpc/tests/support/event_new_block_2.json b/rpc/tests/support/event_new_block_2.json index d38ffbf4b..30f45f4c0 100644 --- a/rpc/tests/support/event_new_block_2.json +++ b/rpc/tests/support/event_new_block_2.json @@ -70,4 +70,4 @@ ] } } -} \ No newline at end of file +} diff --git a/rpc/tests/support/event_new_block_3.json b/rpc/tests/support/event_new_block_3.json index 5bbed8af3..81b1e3ea0 100644 --- a/rpc/tests/support/event_new_block_3.json +++ b/rpc/tests/support/event_new_block_3.json @@ -70,4 +70,4 @@ ] } } -} \ No newline at end of file +} From 1d08a7dc8488d5cfe87241022d5629b6aca2bfc9 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 26 Aug 2020 18:17:33 -0400 Subject: [PATCH 43/60] Remove request::Wrapper::new_with_id() method Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 149 +++++++++++------- rpc/src/client/transport/mock/subscription.rs | 15 +- rpc/src/client/transport/websocket.rs | 55 ++++--- rpc/src/request.rs | 11 +- 4 files changed, 128 insertions(+), 102 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index c59992bb9..e8b7b6949 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -84,7 +84,7 @@ pub struct Subscription { // Our internal result event receiver for this subscription. event_rx: ChannelRx>, // Allows us to gracefully terminate this subscription. - terminate_tx: ChannelTx, + terminate_tx: ChannelTx, } impl Stream for Subscription { @@ -100,7 +100,7 @@ impl Subscription { id: SubscriptionId, query: String, event_rx: ChannelRx>, - terminate_tx: ChannelTx, + terminate_tx: ChannelTx, ) -> Self { Self { id, @@ -117,9 +117,9 @@ impl Subscription { pub async fn terminate(mut self) -> Result<()> { let (result_tx, mut result_rx) = unbounded(); self.terminate_tx - .send(SubscriptionTermination { - query: self.query.clone(), + .send(TerminateSubscription { id: self.id.clone(), + query: self.query.clone(), result_tx, }) .await?; @@ -137,9 +137,9 @@ impl Subscription { /// We expect the driver to use the `result_tx` channel to communicate the /// result of the termination request to the original caller. #[derive(Debug, Clone)] -pub struct SubscriptionTermination { - pub query: String, +pub struct TerminateSubscription { pub id: SubscriptionId, + pub query: String, pub result_tx: ChannelTx>, } @@ -186,14 +186,27 @@ impl TryInto for Id { Id::Str(s) => Ok(SubscriptionId(s)), Id::Num(i) => Ok(SubscriptionId(format!("{}", i))), Id::None => Err(Error::client_internal_error( - "cannot convert an empty JSONRPC ID into a subscription ID", + "cannot convert an empty JSON-RPC ID into a subscription ID", )), } } } +impl AsRef for SubscriptionId { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl From<&str> for SubscriptionId { + fn from(s: &str) -> Self { + Self(s.to_string()) + } +} + #[derive(Debug)] struct PendingSubscribe { + id: SubscriptionId, query: String, event_tx: ChannelTx>, result_tx: ChannelTx>, @@ -201,6 +214,7 @@ struct PendingSubscribe { #[derive(Debug)] struct PendingUnsubscribe { + id: SubscriptionId, query: String, result_tx: ChannelTx>, } @@ -222,8 +236,12 @@ pub enum SubscriptionState { #[derive(Debug)] pub struct SubscriptionRouter { subscriptions: HashMap>>>, - pending_subscribe: HashMap, - pending_unsubscribe: HashMap, + // 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 SubscriptionRouter { @@ -253,7 +271,7 @@ impl SubscriptionRouter { /// Immediately add a new subscription to the router without waiting for /// confirmation. - pub fn add(&mut self, id: SubscriptionId, query: String, event_tx: ChannelTx>) { + 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 => { @@ -261,21 +279,27 @@ impl SubscriptionRouter { self.subscriptions.get_mut(&query).unwrap() } }; - subs_for_query.insert(id, event_tx); + subs_for_query.insert(id.clone(), event_tx); } /// 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, - id: SubscriptionId, + req_id: &str, + subs_id: &SubscriptionId, query: String, event_tx: ChannelTx>, result_tx: ChannelTx>, ) { self.pending_subscribe.insert( - id, + req_id.to_string(), PendingSubscribe { + id: subs_id.clone(), query, event_tx, result_tx, @@ -283,15 +307,15 @@ impl SubscriptionRouter { ); } - /// Attempts to confirm the pending subscription with the given ID. + /// 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, id: &SubscriptionId) -> Result<()> { - match self.pending_subscribe.remove(id) { + pub async fn confirm_add(&mut self, req_id: &str) -> Result<()> { + match self.pending_subscribe.remove(req_id) { Some(mut pending_subscribe) => { self.add( - id.clone(), + &pending_subscribe.id, pending_subscribe.query.clone(), pending_subscribe.event_tx, ); @@ -304,8 +328,8 @@ impl SubscriptionRouter { /// 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, id: &SubscriptionId, err: impl Into) -> Result<()> { - match self.pending_subscribe.remove(id) { + 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())) @@ -313,7 +337,7 @@ impl SubscriptionRouter { .map_err(|_| { Error::client_internal_error(format!( "failed to communicate result of pending subscription with ID: {}", - id + pending_subscribe.id, )) })?), None => Ok(()), @@ -321,35 +345,40 @@ impl SubscriptionRouter { } /// Immediately remove the subscription with the given query and ID. - pub fn remove(&mut self, query: String, id: SubscriptionId) { + 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); + subs_for_query.remove(id); } /// 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, - id: SubscriptionId, result_tx: ChannelTx>, ) { - self.pending_unsubscribe - .insert(id, PendingUnsubscribe { query, result_tx }); + 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, id: &SubscriptionId) -> Result<()> { - match self.pending_unsubscribe.remove(id) { - Some(pending_unsubscribe) => { - let (query, mut result_tx) = - (pending_unsubscribe.query, pending_unsubscribe.result_tx); - self.remove(query, id.clone()); - Ok(result_tx.send(Ok(())).await?) + pub async fn confirm_remove(&mut self, req_id: &str) -> Result<()> { + match self.pending_unsubscribe.remove(req_id) { + Some(mut pending_unsubscribe) => { + self.remove(&pending_unsubscribe.id, pending_unsubscribe.query.clone()); + Ok(pending_unsubscribe.result_tx.send(Ok(())).await?) } None => Ok(()), } @@ -357,12 +386,8 @@ impl SubscriptionRouter { /// Cancel the pending unsubscribe request for the subscription with the /// given ID, responding with the given error. - pub async fn cancel_remove( - &mut self, - id: &SubscriptionId, - err: impl Into, - ) -> Result<()> { - match self.pending_unsubscribe.remove(id) { + 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?) } @@ -392,14 +417,14 @@ impl SubscriptionRouter { /// Utility method to determine the current state of the subscription with /// the given ID. - pub fn subscription_state(&self, id: &SubscriptionId) -> SubscriptionState { - if self.pending_subscribe.contains_key(id) { + pub fn subscription_state(&self, req_id: &str) -> SubscriptionState { + if self.pending_subscribe.contains_key(req_id) { return SubscriptionState::Pending; } - if self.pending_unsubscribe.contains_key(id) { + if self.pending_unsubscribe.contains_key(req_id) { return SubscriptionState::Cancelling; } - if self.is_active(id) { + if self.is_active(&SubscriptionId::from(req_id)) { return SubscriptionState::Active; } SubscriptionState::NotFound @@ -471,10 +496,10 @@ mod test { let (subs3_event_tx, mut subs3_event_rx) = unbounded(); // Two subscriptions with the same query - router.add(subs1_id.clone(), "query1".into(), subs1_event_tx); - router.add(subs2_id.clone(), "query1".into(), subs2_event_tx); + 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.clone(), "query2".into(), subs3_event_tx); + router.add(&subs3_id, "query2".into(), subs3_event_tx); let mut ev = read_event("event_new_block_1").await; ev.query = "query1".into(); @@ -507,20 +532,26 @@ mod test { assert_eq!( SubscriptionState::NotFound, - router.subscription_state(&subs_id) + router.subscription_state(&subs_id.to_string()) + ); + router.pending_add( + subs_id.as_ref(), + &subs_id, + query.clone(), + event_tx, + result_tx, ); - router.pending_add(subs_id.clone(), query.clone(), event_tx, result_tx); assert_eq!( SubscriptionState::Pending, - router.subscription_state(&subs_id) + router.subscription_state(subs_id.as_ref()) ); router.publish(ev.clone()).await; must_not_recv(&mut event_rx, 50).await; - router.confirm_add(&subs_id).await.unwrap(); + router.confirm_add(subs_id.as_ref()).await.unwrap(); assert_eq!( SubscriptionState::Active, - router.subscription_state(&subs_id) + router.subscription_state(subs_id.as_ref()) ); must_not_recv(&mut event_rx, 50).await; let _ = must_recv(&mut result_rx, 500).await; @@ -530,16 +561,16 @@ mod test { assert_eq!(ev, received_ev); let (result_tx, mut result_rx) = unbounded(); - router.pending_remove(query.clone(), subs_id.clone(), result_tx); + router.pending_remove(subs_id.as_ref(), &subs_id, query.clone(), result_tx); assert_eq!( SubscriptionState::Cancelling, - router.subscription_state(&subs_id), + router.subscription_state(subs_id.as_ref()), ); - router.confirm_remove(&subs_id).await.unwrap(); + router.confirm_remove(subs_id.as_ref()).await.unwrap(); assert_eq!( SubscriptionState::NotFound, - router.subscription_state(&subs_id) + router.subscription_state(subs_id.as_ref()) ); router.publish(ev.clone()).await; if must_recv(&mut result_rx, 500).await.is_err() { @@ -559,24 +590,24 @@ mod test { assert_eq!( SubscriptionState::NotFound, - router.subscription_state(&subs_id) + router.subscription_state(subs_id.as_ref()) ); - router.pending_add(subs_id.clone(), query, event_tx, result_tx); + router.pending_add(subs_id.as_ref(), &subs_id, query, event_tx, result_tx); assert_eq!( SubscriptionState::Pending, - router.subscription_state(&subs_id) + router.subscription_state(subs_id.as_ref()) ); 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, cancel_error.clone()) + .cancel_add(subs_id.as_ref(), cancel_error.clone()) .await .unwrap(); assert_eq!( SubscriptionState::NotFound, - router.subscription_state(&subs_id) + router.subscription_state(subs_id.as_ref()) ); assert_eq!(Err(cancel_error), must_recv(&mut result_rx, 500).await); diff --git a/rpc/src/client/transport/mock/subscription.rs b/rpc/src/client/transport/mock/subscription.rs index 8cb18fd9c..ef21572e1 100644 --- a/rpc/src/client/transport/mock/subscription.rs +++ b/rpc/src/client/transport/mock/subscription.rs @@ -1,6 +1,6 @@ //! Subscription functionality for the Tendermint RPC mock client. -use crate::client::subscription::SubscriptionTermination; +use crate::client::subscription::TerminateSubscription; use crate::client::sync::{bounded, unbounded, ChannelRx, ChannelTx}; use crate::client::{ClosableClient, SubscriptionRouter}; use crate::event::Event; @@ -21,7 +21,7 @@ pub struct MockSubscriptionClient { driver_hdl: JoinHandle>, event_tx: ChannelTx, cmd_tx: ChannelTx, - terminate_tx: ChannelTx, + terminate_tx: ChannelTx, } #[async_trait] @@ -105,7 +105,7 @@ impl Default for MockSubscriptionClient { struct MockSubscriptionClientDriver { event_rx: ChannelRx, cmd_rx: ChannelRx, - terminate_rx: ChannelRx, + terminate_rx: ChannelRx, router: SubscriptionRouter, } @@ -113,7 +113,7 @@ impl MockSubscriptionClientDriver { fn new( event_rx: ChannelRx, cmd_rx: ChannelRx, - terminate_rx: ChannelRx, + terminate_rx: ChannelRx, ) -> Self { Self { event_rx, @@ -148,13 +148,12 @@ impl MockSubscriptionClientDriver { event_tx: ChannelTx>, mut result_tx: ChannelTx>, ) -> Result<()> { - self.router.add(id, query, event_tx); + self.router.add(&id, query, event_tx); result_tx.send(Ok(())).await } - async fn unsubscribe(&mut self, mut subs_term: SubscriptionTermination) -> Result<()> { - self.router - .remove(subs_term.query.clone(), subs_term.id.clone()); + async fn unsubscribe(&mut self, mut subs_term: TerminateSubscription) -> Result<()> { + self.router.remove(&subs_term.id, subs_term.query.clone()); subs_term.result_tx.send(Ok(())).await } } diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index e17a05486..0fe726fdb 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -1,6 +1,6 @@ //! WebSocket-based clients for accessing Tendermint RPC functionality. -use crate::client::subscription::{SubscriptionState, SubscriptionTermination}; +use crate::client::subscription::{SubscriptionState, TerminateSubscription}; use crate::client::sync::{bounded, unbounded, ChannelRx, ChannelTx}; use crate::client::transport::get_tcp_host_port; use crate::client::{ClosableClient, SubscriptionRouter}; @@ -77,7 +77,7 @@ pub struct WebSocketSubscriptionClient { port: u16, driver_handle: JoinHandle>, cmd_tx: ChannelTx, - terminate_tx: ChannelTx, + terminate_tx: ChannelTx, } impl WebSocketSubscriptionClient { @@ -120,10 +120,12 @@ impl SubscriptionClient for WebSocketSubscriptionClient { bounded(buf_size) }; let (result_tx, mut result_rx) = unbounded::>(); - let id = SubscriptionId::default(); + // NB: We assume here that the wrapper generates a unique ID for our + // subscription. + let req = Wrapper::new(subscribe::Request::new(query.clone())); + let id: SubscriptionId = req.id().clone().try_into()?; self.send_cmd(WebSocketDriverCmd::Subscribe { - id: id.clone(), - query: query.clone(), + req, event_tx, result_tx, }) @@ -166,14 +168,14 @@ struct WebSocketSubscriptionDriver { stream: WebSocketStream>, router: SubscriptionRouter, cmd_rx: ChannelRx, - terminate_rx: ChannelRx, + terminate_rx: ChannelRx, } impl WebSocketSubscriptionDriver { fn new( stream: WebSocketStream>, cmd_rx: ChannelRx, - terminate_rx: ChannelRx, + terminate_rx: ChannelRx, ) -> Self { Self { stream, @@ -198,11 +200,10 @@ impl WebSocketSubscriptionDriver { }, Some(cmd) = self.cmd_rx.recv() => match cmd { WebSocketDriverCmd::Subscribe { - id, - query, + req, event_tx, result_tx, - } => self.subscribe(id, query, event_tx, result_tx).await?, + } => self.subscribe(req, event_tx, result_tx).await?, WebSocketDriverCmd::Close => return self.close().await, }, Some(term) = self.terminate_rx.recv() => self.unsubscribe(term).await?, @@ -218,12 +219,12 @@ impl WebSocketSubscriptionDriver { async fn subscribe( &mut self, - id: SubscriptionId, - query: String, + req: Wrapper, event_tx: ChannelTx>, mut result_tx: ChannelTx>, ) -> Result<()> { - let req = Wrapper::new_with_id(id.clone().into(), subscribe::Request::new(query.clone())); + let id: SubscriptionId = req.id().clone().try_into()?; + let query = req.params().query.clone(); if let Err(e) = self .send(Message::Text(serde_json::to_string_pretty(&req).unwrap())) .await @@ -231,15 +232,14 @@ impl WebSocketSubscriptionDriver { let _ = result_tx.send(Err(e)).await; return Ok(()); } - self.router.pending_add(id, query, event_tx, result_tx); + self.router + .pending_add(id.as_ref(), &id, query, event_tx, result_tx); Ok(()) } - async fn unsubscribe(&mut self, mut term: SubscriptionTermination) -> Result<()> { - let req = Wrapper::new_with_id( - term.id.clone().into(), - unsubscribe::Request::new(term.query.clone()), - ); + async fn unsubscribe(&mut self, mut term: TerminateSubscription) -> Result<()> { + let req = Wrapper::new(unsubscribe::Request::new(term.query.clone())); + let id: SubscriptionId = req.id().clone().try_into()?; if let Err(e) = self .send(Message::Text(serde_json::to_string_pretty(&req).unwrap())) .await @@ -248,7 +248,7 @@ impl WebSocketSubscriptionDriver { return Ok(()); } self.router - .pending_remove(term.query.clone(), term.id.clone(), term.result_tx); + .pending_remove(id.as_ref(), &id, term.query.clone(), term.result_tx); Ok(()) } @@ -284,12 +284,12 @@ impl WebSocketSubscriptionDriver { Err(_) => return Ok(()), }; match wrapper.into_result() { - Ok(_) => match self.router.subscription_state(&subs_id) { + Ok(_) => match self.router.subscription_state(subs_id.as_ref()) { SubscriptionState::Pending => { - let _ = self.router.confirm_add(&subs_id).await; + let _ = self.router.confirm_add(subs_id.as_ref()).await; } SubscriptionState::Cancelling => { - let _ = self.router.confirm_remove(&subs_id).await; + let _ = self.router.confirm_remove(subs_id.as_ref()).await; } SubscriptionState::Active => { if let Some(event_tx) = self.router.get_active_subscription_mut(&subs_id) { @@ -302,12 +302,12 @@ impl WebSocketSubscriptionDriver { } SubscriptionState::NotFound => (), }, - Err(e) => match self.router.subscription_state(&subs_id) { + Err(e) => match self.router.subscription_state(subs_id.as_ref()) { SubscriptionState::Pending => { - let _ = self.router.cancel_add(&subs_id, e).await; + let _ = self.router.cancel_add(subs_id.as_ref(), e).await; } SubscriptionState::Cancelling => { - let _ = self.router.cancel_remove(&subs_id, e).await; + let _ = self.router.cancel_remove(subs_id.as_ref(), e).await; } // This is important to allow the remote endpoint to // arbitrarily send error responses back to specific @@ -348,8 +348,7 @@ impl WebSocketSubscriptionDriver { #[derive(Debug)] enum WebSocketDriverCmd { Subscribe { - id: SubscriptionId, - query: String, + req: Wrapper, event_tx: ChannelTx>, result_tx: ChannelTx>, }, diff --git a/rpc/src/request.rs b/rpc/src/request.rs index aa55f88d5..3fbc8463a 100644 --- a/rpc/src/request.rs +++ b/rpc/src/request.rs @@ -40,18 +40,15 @@ where { /// Create a new request wrapper from the given request. /// - /// By default this sets the ID of the request to a random [UUIDv4] value. + /// The ID of the request is set to a random [UUIDv4] value. /// /// [UUIDv4]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random) pub fn new(request: R) -> Self { - Wrapper::new_with_id(Id::uuid_v4(), request) - } - - /// Create a new request wrapper with a custom JSONRPC request ID. - pub fn new_with_id(id: Id, request: R) -> Self { Self { jsonrpc: Version::current(), - id, + // NB: The WebSocket client relies on this being some kind of UUID, + // and will break if it's not. + id: Id::uuid_v4(), method: request.method(), params: request, } From 3048155f99790760408092e1f47bb09f778ada28 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 27 Aug 2020 15:07:30 -0400 Subject: [PATCH 44/60] Refactor interface for subscription client The interface now no longer provides an option to subscribe using a specific buffer size and we assume unbounded buffers everywhere. This may or may not be a safe assumption going forward and we'll need to re-evaluate in future. As such, I could remove all code relating to bounded buffers. Additionally, this commit adds something of an integration test for the WebSocket-based subscription client (with a full WebSocket server, effectively). Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 23 +- rpc/src/client/sync.rs | 49 +-- rpc/src/client/transport/mock/subscription.rs | 14 +- rpc/src/client/transport/websocket.rs | 381 +++++++++++++++++- rpc/src/response.rs | 10 + 5 files changed, 392 insertions(+), 85 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index e8b7b6949..c06b50cf0 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -24,28 +24,11 @@ use std::pin::Pin; pub trait SubscriptionClient: ClosableClient { /// `/subscribe`: subscribe to receive events produced by the given query. /// - /// Allows for specification of the `buf_size` parameter, which determines - /// how many events can be buffered in the resulting [`Subscription`]. Set - /// to 0 to use an unbounded buffer (i.e. the buffer size will only be - /// limited by the amount of memory available to your application). + /// Uses an unbounded buffer for the resulting [`Subscription`]. We do not + /// implement bounded buffers at present. /// /// [`Subscription`]: struct.Subscription.html - async fn subscribe_with_buf_size( - &mut self, - query: String, - buf_size: usize, - ) -> Result; - - /// `/subscribe`: subscribe to receive events produced by the given query. - /// - /// Uses an unbounded buffer for the resulting [`Subscription`] (i.e. this - /// is the same as calling `subscribe_with_buf_size` with `buf_size` set to - /// 0). - /// - /// [`Subscription`]: struct.Subscription.html - async fn subscribe(&mut self, query: String) -> Result { - self.subscribe_with_buf_size(query, 0).await - } + async fn subscribe(&mut self, query: String) -> Result; } /// An interface that can be used to asynchronously receive [`Event`]s for a diff --git a/rpc/src/client/sync.rs b/rpc/src/client/sync.rs index f8be8d755..fa0d7a6a1 100644 --- a/rpc/src/client/sync.rs +++ b/rpc/src/client/sync.rs @@ -1,44 +1,29 @@ //! Synchronization primitives specific to the Tendermint RPC client. //! //! At present, this wraps Tokio's synchronization primitives and provides some -//! conveniences, such as an interface to a channel without caring about -//! whether it's bounded or unbounded. +//! convenience methods. We also only implement unbounded channels at present. +//! In future, if RPC consumers need it, we will implement bounded channels. use crate::{Error, Result}; use futures::task::{Context, Poll}; use tokio::sync::mpsc; -/// Constructor for a bounded channel with maximum capacity of `buf_size` -/// elements. -pub fn bounded(buf_size: usize) -> (ChannelTx, ChannelRx) { - let (tx, rx) = mpsc::channel(buf_size); - (ChannelTx::Bounded(tx), ChannelRx::Bounded(rx)) -} - /// Constructor for an unbounded channel. pub fn unbounded() -> (ChannelTx, ChannelRx) { let (tx, rx) = mpsc::unbounded_channel(); - (ChannelTx::Unbounded(tx), ChannelRx::Unbounded(rx)) + (ChannelTx(tx), ChannelRx(rx)) } -/// Generic sender interface on bounded and unbounded channels for -/// `Result` instances. +/// Sender interface for a channel. /// /// Can be cloned because the underlying channel used is /// [`mpsc`](https://docs.rs/tokio/*/tokio/sync/mpsc/index.html). #[derive(Debug, Clone)] -pub enum ChannelTx { - Bounded(mpsc::Sender), - Unbounded(mpsc::UnboundedSender), -} +pub struct ChannelTx(mpsc::UnboundedSender); impl ChannelTx { pub async fn send(&mut self, value: T) -> Result<()> { - match self { - ChannelTx::Bounded(ref mut tx) => tx.send(value).await, - ChannelTx::Unbounded(ref mut tx) => tx.send(value), - } - .map_err(|e| { + self.0.send(value).map_err(|e| { Error::client_internal_error(format!( "failed to send message to internal channel: {}", e @@ -47,28 +32,18 @@ impl ChannelTx { } } -/// Generic receiver interface on bounded and unbounded channels. +/// Receiver interface for a channel. #[derive(Debug)] -pub enum ChannelRx { - /// A channel that can contain up to a fixed number of items. - Bounded(mpsc::Receiver), - /// A channel that is unconstrained (except by system resources, of - /// course). - Unbounded(mpsc::UnboundedReceiver), -} +pub struct ChannelRx(mpsc::UnboundedReceiver); impl ChannelRx { + /// Wait indefinitely until we receive a value from the channel (or the + /// channel is closed). pub async fn recv(&mut self) -> Option { - match self { - ChannelRx::Bounded(ref mut rx) => rx.recv().await, - ChannelRx::Unbounded(ref mut rx) => rx.recv().await, - } + self.0.recv().await } pub fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll> { - match self { - ChannelRx::Bounded(ref mut rx) => rx.poll_recv(cx), - ChannelRx::Unbounded(ref mut rx) => rx.poll_recv(cx), - } + self.0.poll_recv(cx) } } diff --git a/rpc/src/client/transport/mock/subscription.rs b/rpc/src/client/transport/mock/subscription.rs index ef21572e1..eabb33777 100644 --- a/rpc/src/client/transport/mock/subscription.rs +++ b/rpc/src/client/transport/mock/subscription.rs @@ -1,7 +1,7 @@ //! Subscription functionality for the Tendermint RPC mock client. use crate::client::subscription::TerminateSubscription; -use crate::client::sync::{bounded, unbounded, ChannelRx, ChannelTx}; +use crate::client::sync::{unbounded, ChannelRx, ChannelTx}; use crate::client::{ClosableClient, SubscriptionRouter}; use crate::event::Event; use crate::{Error, Result, Subscription, SubscriptionClient, SubscriptionId}; @@ -26,16 +26,8 @@ pub struct MockSubscriptionClient { #[async_trait] impl SubscriptionClient for MockSubscriptionClient { - async fn subscribe_with_buf_size( - &mut self, - query: String, - buf_size: usize, - ) -> Result { - let (event_tx, event_rx) = if buf_size == 0 { - unbounded() - } else { - bounded(buf_size) - }; + async fn subscribe(&mut self, query: String) -> Result { + let (event_tx, event_rx) = unbounded(); let (result_tx, mut result_rx) = unbounded(); let id = SubscriptionId::default(); self.send_cmd(DriverCmd::Subscribe { diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index 0fe726fdb..b57586ef6 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -1,13 +1,14 @@ //! WebSocket-based clients for accessing Tendermint RPC functionality. use crate::client::subscription::{SubscriptionState, TerminateSubscription}; -use crate::client::sync::{bounded, unbounded, ChannelRx, ChannelTx}; +use crate::client::sync::{unbounded, ChannelRx, ChannelTx}; use crate::client::transport::get_tcp_host_port; use crate::client::{ClosableClient, SubscriptionRouter}; use crate::endpoint::{subscribe, unsubscribe}; use crate::event::Event; -use crate::request::Wrapper; -use crate::{response, Error, Response, Result, Subscription, SubscriptionClient, SubscriptionId}; +use crate::{ + request, response, Error, Response, Result, Subscription, SubscriptionClient, SubscriptionId, +}; use async_trait::async_trait; use async_tungstenite::tokio::{connect_async, TokioAdapter}; use async_tungstenite::tungstenite::protocol::frame::coding::CloseCode; @@ -109,20 +110,12 @@ impl WebSocketSubscriptionClient { #[async_trait] impl SubscriptionClient for WebSocketSubscriptionClient { - async fn subscribe_with_buf_size( - &mut self, - query: String, - buf_size: usize, - ) -> Result { - let (event_tx, event_rx) = if buf_size == 0 { - unbounded() - } else { - bounded(buf_size) - }; + async fn subscribe(&mut self, query: String) -> Result { + let (event_tx, event_rx) = unbounded(); let (result_tx, mut result_rx) = unbounded::>(); // NB: We assume here that the wrapper generates a unique ID for our // subscription. - let req = Wrapper::new(subscribe::Request::new(query.clone())); + let req = request::Wrapper::new(subscribe::Request::new(query.clone())); let id: SubscriptionId = req.id().clone().try_into()?; self.send_cmd(WebSocketDriverCmd::Subscribe { req, @@ -219,7 +212,7 @@ impl WebSocketSubscriptionDriver { async fn subscribe( &mut self, - req: Wrapper, + req: request::Wrapper, event_tx: ChannelTx>, mut result_tx: ChannelTx>, ) -> Result<()> { @@ -238,7 +231,7 @@ impl WebSocketSubscriptionDriver { } async fn unsubscribe(&mut self, mut term: TerminateSubscription) -> Result<()> { - let req = Wrapper::new(unsubscribe::Request::new(term.query.clone())); + let req = request::Wrapper::new(unsubscribe::Request::new(term.query.clone())); let id: SubscriptionId = req.id().clone().try_into()?; if let Err(e) = self .send(Message::Text(serde_json::to_string_pretty(&req).unwrap())) @@ -348,9 +341,363 @@ impl WebSocketSubscriptionDriver { #[derive(Debug)] enum WebSocketDriverCmd { Subscribe { - req: Wrapper, + req: request::Wrapper, event_tx: ChannelTx>, result_tx: ChannelTx>, }, Close, } + +#[cfg(test)] +mod test { + use super::*; + use crate::{Id, Method}; + use async_tungstenite::tokio::accept_async; + use futures::StreamExt; + use std::collections::HashMap; + use std::path::PathBuf; + use std::str::FromStr; + use tokio::fs; + use tokio::net::TcpListener; + + // Interface to a driver that manages all incoming WebSocket connections. + struct TestServer { + node_addr: net::Address, + driver_hdl: JoinHandle>, + terminate_tx: ChannelTx>, + event_tx: ChannelTx, + } + + impl TestServer { + async fn new(addr: &str) -> Self { + let listener = TcpListener::bind(addr).await.unwrap(); + let local_addr = listener.local_addr().unwrap(); + let node_addr = net::Address::Tcp { + peer_id: None, + host: local_addr.ip().to_string(), + port: local_addr.port(), + }; + let (terminate_tx, terminate_rx) = unbounded(); + let (event_tx, event_rx) = unbounded(); + let driver = TestServerDriver::new(listener, event_rx, terminate_rx); + let driver_hdl = tokio::spawn(async move { driver.run().await }); + Self { + node_addr, + driver_hdl, + terminate_tx, + event_tx, + } + } + + async fn publish_event(&mut self, ev: Event) -> Result<()> { + self.event_tx.send(ev).await + } + + async fn terminate(mut self) -> Result<()> { + self.terminate_tx.send(Ok(())).await.unwrap(); + self.driver_hdl.await.unwrap() + } + } + + // Manages all incoming WebSocket connections. + struct TestServerDriver { + listener: TcpListener, + event_rx: ChannelRx, + terminate_rx: ChannelRx>, + handlers: Vec, + } + + impl TestServerDriver { + fn new( + listener: TcpListener, + event_rx: ChannelRx, + terminate_rx: ChannelRx>, + ) -> Self { + Self { + listener, + event_rx, + terminate_rx, + handlers: Vec::new(), + } + } + + async fn run(mut self) -> Result<()> { + loop { + tokio::select! { + Some(ev) = self.event_rx.recv() => self.publish_event(ev).await, + Some(res) = self.listener.next() => self.handle_incoming(res.unwrap()).await, + Some(res) = self.terminate_rx.recv() => { + self.terminate().await; + return res; + }, + } + } + } + + // Publishes the given event to all subscribers for the query relating + // to the event. + async fn publish_event(&mut self, ev: Event) { + for handler in &mut self.handlers { + handler.publish_event(ev.clone()).await; + } + } + + async fn handle_incoming(&mut self, stream: TcpStream) { + self.handlers.push(TestServerHandler::new(stream).await); + } + + async fn terminate(&mut self) { + while self.handlers.len() > 0 { + let handler = match self.handlers.pop() { + Some(h) => h, + None => break, + }; + let _ = handler.terminate().await; + } + } + } + + // Interface to a driver that manages a single incoming WebSocket + // connection. + struct TestServerHandler { + driver_hdl: JoinHandle>, + terminate_tx: ChannelTx>, + event_tx: ChannelTx, + } + + impl TestServerHandler { + async fn new(stream: TcpStream) -> Self { + let conn: WebSocketStream> = + accept_async(stream).await.unwrap(); + let (terminate_tx, terminate_rx) = unbounded(); + let (event_tx, event_rx) = unbounded(); + let driver = TestServerHandlerDriver::new(conn, event_rx, terminate_rx); + let driver_hdl = tokio::spawn(async move { driver.run().await }); + Self { + driver_hdl, + terminate_tx, + event_tx, + } + } + + async fn publish_event(&mut self, ev: Event) { + let _ = self.event_tx.send(ev).await; + } + + async fn terminate(mut self) -> Result<()> { + self.terminate_tx.send(Ok(())).await?; + self.driver_hdl.await.unwrap() + } + } + + // Manages interaction with a single incoming WebSocket connection. + struct TestServerHandlerDriver { + conn: WebSocketStream>, + event_rx: ChannelRx, + terminate_rx: ChannelRx>, + // A mapping of subscription queries to subscription IDs for this + // connection. + subscriptions: HashMap, + } + + impl TestServerHandlerDriver { + fn new( + conn: WebSocketStream>, + event_rx: ChannelRx, + terminate_rx: ChannelRx>, + ) -> Self { + Self { + conn, + event_rx, + terminate_rx, + subscriptions: HashMap::new(), + } + } + + async fn run(mut self) -> Result<()> { + loop { + tokio::select! { + Some(res) = self.conn.next() => { + if let Some(ret) = self.handle_incoming_msg(res.unwrap()).await { + return ret; + } + } + Some(ev) = self.event_rx.recv() => self.publish_event(ev).await, + Some(res) = self.terminate_rx.recv() => { + self.terminate().await; + return res; + }, + } + } + } + + async fn publish_event(&mut self, ev: Event) { + let subs_id = match self.subscriptions.get(&ev.query) { + Some(id) => id.clone(), + None => return, + }; + let _ = self.send(subs_id.into(), ev).await; + } + + async fn handle_incoming_msg(&mut self, msg: Message) -> Option> { + match msg { + Message::Text(s) => self.handle_incoming_text_msg(s).await, + Message::Ping(v) => { + let _ = self.conn.send(Message::Pong(v)).await; + None + } + Message::Close(_) => { + self.terminate().await; + Some(Ok(())) + } + _ => None, + } + } + + async fn handle_incoming_text_msg(&mut self, msg: String) -> Option> { + match serde_json::from_str::(&msg) { + Ok(json_msg) => match json_msg.get("method") { + Some(json_method) => match Method::from_str(json_method.as_str().unwrap()) { + Ok(method) => match method { + Method::Subscribe => { + let req = serde_json::from_str::< + request::Wrapper, + >(&msg) + .unwrap(); + + self.add_subscription( + req.params().query.clone(), + req.id().clone().try_into().unwrap(), + ); + self.send(req.id().clone(), subscribe::Response {}).await; + } + Method::Unsubscribe => { + let req = serde_json::from_str::< + request::Wrapper, + >(&msg) + .unwrap(); + + self.remove_subscription(req.params().query.clone()); + self.send(req.id().clone(), unsubscribe::Response {}).await; + } + _ => { + println!("Unsupported method in incoming request: {}", &method); + } + }, + Err(e) => { + println!( + "Unexpected method in incoming request: {} ({})", + json_method, e + ); + } + }, + None => (), + }, + Err(e) => { + println!("Failed to parse incoming request: {} ({})", &msg, e); + } + } + None + } + + fn add_subscription(&mut self, query: String, id: SubscriptionId) { + println!("Adding subscription with ID {} for query: {}", &id, &query); + self.subscriptions.insert(query, id); + } + + fn remove_subscription(&mut self, query: String) { + if let Some(id) = self.subscriptions.remove(&query) { + println!("Removed subscription {} for query: {}", id, query); + } + } + + async fn send(&mut self, id: Id, res: R) + where + R: Response, + { + self.conn + .send(Message::Text( + serde_json::to_string(&response::Wrapper::new_with_id(id, Some(res), None)) + .unwrap(), + )) + .await + .unwrap(); + } + + async fn terminate(&mut self) { + let _ = self + .conn + .close(Some(CloseFrame { + code: CloseCode::Normal, + reason: Default::default(), + })) + .await; + } + } + + 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() + } + + #[tokio::test] + async fn websocket_client_happy_path() { + let test_events = vec![ + read_event("event_new_block_1").await, + read_event("event_new_block_2").await, + read_event("event_new_block_3").await, + ]; + println!("Starting WebSocket server..."); + let mut server = TestServer::new("127.0.0.1:0").await; + println!("Creating client RPC WebSocket connection..."); + let mut client = WebSocketSubscriptionClient::new(server.node_addr.clone()) + .await + .unwrap(); + + println!("Initiating subscription for new blocks..."); + let mut subs = client + .subscribe("tm.event='NewBlock'".to_string()) + .await + .unwrap(); + + // Collect all the events from the subscription. + let subs_collector_hdl = tokio::spawn(async move { + let mut results = Vec::new(); + while let Some(res) = subs.next().await { + results.push(res); + if results.len() == 3 { + break; + } + } + println!("Terminating subscription..."); + subs.terminate().await.unwrap(); + results + }); + + println!("Publishing events"); + // Publish the events from this context + for ev in &test_events { + server.publish_event(ev.clone()).await.unwrap(); + } + + println!("Collecting results from subscription..."); + let collected_results = subs_collector_hdl.await.unwrap(); + + client.close().await.unwrap(); + server.terminate().await.unwrap(); + println!("Closed client and terminated server"); + + assert_eq!(3, collected_results.len()); + for i in 0..3 { + assert_eq!( + test_events[i], + collected_results[i].as_ref().unwrap().clone() + ); + } + } +} diff --git a/rpc/src/response.rs b/rpc/src/response.rs index e2645e662..e9b664486 100644 --- a/rpc/src/response.rs +++ b/rpc/src/response.rs @@ -66,4 +66,14 @@ where )) } } + + #[cfg(test)] + pub fn new_with_id(id: Id, result: Option, error: Option) -> Self { + Self { + jsonrpc: Version::current(), + id, + result, + error, + } + } } From 5e9e35183d7ecc448990b6a1e85d87eaf76a1ddc Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 27 Aug 2020 15:23:52 -0400 Subject: [PATCH 45/60] Fix broken link in documentation Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index c06b50cf0..735e6ff28 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -18,7 +18,7 @@ use std::pin::Pin; /// To build a full-featured client, implement both this trait as well as the /// [`Client`] trait. /// -/// [`Event`]: ./events/struct.Event.html +/// [`Event`]: event/struct.Event.html /// [`Client`]: trait.Client.html #[async_trait] pub trait SubscriptionClient: ClosableClient { From 99dae2aa00b9bcaef8cb75079b7b130252c2997e Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 27 Aug 2020 15:30:52 -0400 Subject: [PATCH 46/60] Rename all "JSONRPC" references to "JSON-RPC" Signed-off-by: Thane Thomson --- light-node/Cargo.toml | 2 +- light-node/README.md | 4 ++-- light-node/src/lib.rs | 2 +- light-node/src/rpc.rs | 2 +- rpc/README.md | 2 +- rpc/src/endpoint.rs | 2 +- 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.rs | 2 +- rpc/src/endpoint/commit.rs | 2 +- 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 | 2 +- rpc/src/endpoint/unsubscribe.rs | 2 +- rpc/src/endpoint/validators.rs | 2 +- rpc/src/error.rs | 2 +- rpc/src/event.rs | 2 +- rpc/src/id.rs | 6 +++--- rpc/src/method.rs | 6 +++--- rpc/src/request.rs | 8 ++++---- rpc/src/response.rs | 16 ++++++++-------- rpc/src/version.rs | 10 +++++----- tendermint/Cargo.toml | 2 +- tendermint/src/lib.rs | 2 +- 29 files changed, 48 insertions(+), 48 deletions(-) diff --git a/light-node/Cargo.toml b/light-node/Cargo.toml index 54de16f71..43eaa50f7 100644 --- a/light-node/Cargo.toml +++ b/light-node/Cargo.toml @@ -18,7 +18,7 @@ description = """ The Tendermint light-node wraps the light-client crate into a command-line interface tool. It can be used to initialize and start a standalone light client daemon and - exposes a JSONRPC endpoint from which you can query the current state of the + exposes a JSON-RPC endpoint from which you can query the current state of the light node. """ diff --git a/light-node/README.md b/light-node/README.md index 68c6eb15d..f2f2a81dd 100644 --- a/light-node/README.md +++ b/light-node/README.md @@ -6,7 +6,7 @@ See the [repo root] for build status, license, rust version, etc. # Light-Node The [Tendermint] light-node wraps the [light-client] crate into a command-line interface tool. -It can be used as a standalone light client daemon and exposes a JSONRPC endpoint +It can be used as a standalone light client daemon and exposes a JSON-RPC endpoint from which you can query the current state of the light node. ## Getting Started @@ -103,7 +103,7 @@ Or on a specific sub-command, e.g.: $ cargo run -- help start ``` -### JSONRPC Endpoint(s) +### JSON-RPC Endpoint(s) When you have a light-node running you can query its current state via: ``` diff --git a/light-node/src/lib.rs b/light-node/src/lib.rs index 74dc92346..f9fd468ae 100644 --- a/light-node/src/lib.rs +++ b/light-node/src/lib.rs @@ -2,7 +2,7 @@ //! //! The Tendermint light-node wraps the light-client crate into a command-line interface tool. //! -//! It can be used to initialize and start a standalone light client daemon and exposes a JSONRPC +//! It can be used to initialize and start a standalone light client daemon and exposes a JSON-RPC //! endpoint from which you can query the current state of the light node. // Tip: Deny warnings with `RUSTFLAGS="-D warnings"` environment variable in CI diff --git a/light-node/src/rpc.rs b/light-node/src/rpc.rs index b9f113cce..f84f2e350 100644 --- a/light-node/src/rpc.rs +++ b/light-node/src/rpc.rs @@ -1,4 +1,4 @@ -//! JSONRPC Server and Client for the light-node RPC endpoint. +//! JSON-RPC Server and Client for the light-node RPC endpoint. use jsonrpc_core::IoHandler; use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; diff --git a/rpc/README.md b/rpc/README.md index 4bbf013d6..0144dec31 100644 --- a/rpc/README.md +++ b/rpc/README.md @@ -7,7 +7,7 @@ See the [repo root] for build status, license, rust version, etc. A rust implementation of the core types returned by a Tendermint node's RPC endpoint. -These can be used to deserialize JSONRPC responses. +These can be used to deserialize JSON-RPC responses. All networking related features will be feature guarded to keep the dependencies small in cases where only the core types are needed. diff --git a/rpc/src/endpoint.rs b/rpc/src/endpoint.rs index e613c212a..6cd0055ab 100644 --- a/rpc/src/endpoint.rs +++ b/rpc/src/endpoint.rs @@ -1,4 +1,4 @@ -//! Tendermint JSONRPC endpoints +//! Tendermint JSON-RPC endpoints pub mod abci_info; pub mod abci_query; diff --git a/rpc/src/endpoint/abci_info.rs b/rpc/src/endpoint/abci_info.rs index 5ba245874..89cdb1c47 100644 --- a/rpc/src/endpoint/abci_info.rs +++ b/rpc/src/endpoint/abci_info.rs @@ -1,4 +1,4 @@ -//! `/abci_info` endpoint JSONRPC wrapper +//! `/abci_info` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; diff --git a/rpc/src/endpoint/abci_query.rs b/rpc/src/endpoint/abci_query.rs index f5d6484ac..ed19eb884 100644 --- a/rpc/src/endpoint/abci_query.rs +++ b/rpc/src/endpoint/abci_query.rs @@ -1,4 +1,4 @@ -//! `/abci_query` endpoint JSONRPC wrapper +//! `/abci_query` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; diff --git a/rpc/src/endpoint/block.rs b/rpc/src/endpoint/block.rs index 89f3fdd8a..4def03344 100644 --- a/rpc/src/endpoint/block.rs +++ b/rpc/src/endpoint/block.rs @@ -1,4 +1,4 @@ -//! `/block` endpoint JSONRPC wrapper +//! `/block` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; diff --git a/rpc/src/endpoint/block_results.rs b/rpc/src/endpoint/block_results.rs index 9d031c29c..d681293ed 100644 --- a/rpc/src/endpoint/block_results.rs +++ b/rpc/src/endpoint/block_results.rs @@ -1,4 +1,4 @@ -//! `/block_results` endpoint JSONRPC wrapper +//! `/block_results` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; diff --git a/rpc/src/endpoint/blockchain.rs b/rpc/src/endpoint/blockchain.rs index e75dff683..49a83203c 100644 --- a/rpc/src/endpoint/blockchain.rs +++ b/rpc/src/endpoint/blockchain.rs @@ -1,4 +1,4 @@ -//! `/block` endpoint JSONRPC wrapper +//! `/block` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; use std::ops::Range; diff --git a/rpc/src/endpoint/broadcast.rs b/rpc/src/endpoint/broadcast.rs index f45593e82..dafcfdd7a 100644 --- a/rpc/src/endpoint/broadcast.rs +++ b/rpc/src/endpoint/broadcast.rs @@ -1,4 +1,4 @@ -//! `/broadcast_tx_*` endpoint JSONRPC wrappers +//! `/broadcast_tx_*` endpoint JSON-RPC wrappers pub mod tx_async; pub mod tx_commit; diff --git a/rpc/src/endpoint/commit.rs b/rpc/src/endpoint/commit.rs index e2e2acb08..e3dc1c24c 100644 --- a/rpc/src/endpoint/commit.rs +++ b/rpc/src/endpoint/commit.rs @@ -1,4 +1,4 @@ -//! `/commit` endpoint JSONRPC wrapper +//! `/commit` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; diff --git a/rpc/src/endpoint/genesis.rs b/rpc/src/endpoint/genesis.rs index c0b62976f..a09f28f16 100644 --- a/rpc/src/endpoint/genesis.rs +++ b/rpc/src/endpoint/genesis.rs @@ -1,4 +1,4 @@ -//! `/genesis` endpoint JSONRPC wrapper +//! `/genesis` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; diff --git a/rpc/src/endpoint/health.rs b/rpc/src/endpoint/health.rs index bdf45e6ac..a4f24dc94 100644 --- a/rpc/src/endpoint/health.rs +++ b/rpc/src/endpoint/health.rs @@ -1,4 +1,4 @@ -//! `/health` endpoint JSONRPC wrapper +//! `/health` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; diff --git a/rpc/src/endpoint/net_info.rs b/rpc/src/endpoint/net_info.rs index 8a594aafd..a9a90a1ba 100644 --- a/rpc/src/endpoint/net_info.rs +++ b/rpc/src/endpoint/net_info.rs @@ -1,4 +1,4 @@ -//! `/net_info` endpoint JSONRPC wrapper +//! `/net_info` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; diff --git a/rpc/src/endpoint/status.rs b/rpc/src/endpoint/status.rs index 32d3b60ef..375d1ae8a 100644 --- a/rpc/src/endpoint/status.rs +++ b/rpc/src/endpoint/status.rs @@ -1,4 +1,4 @@ -//! `/status` endpoint JSONRPC wrapper +//! `/status` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; diff --git a/rpc/src/endpoint/subscribe.rs b/rpc/src/endpoint/subscribe.rs index 301233939..b2040972e 100644 --- a/rpc/src/endpoint/subscribe.rs +++ b/rpc/src/endpoint/subscribe.rs @@ -1,4 +1,4 @@ -//! `/subscribe` endpoint JSONRPC wrapper +//! `/subscribe` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; use std::io::Read; diff --git a/rpc/src/endpoint/unsubscribe.rs b/rpc/src/endpoint/unsubscribe.rs index 9bcc902ed..c496201f3 100644 --- a/rpc/src/endpoint/unsubscribe.rs +++ b/rpc/src/endpoint/unsubscribe.rs @@ -1,4 +1,4 @@ -//! `/unsubscribe` endpoint JSONRPC wrapper +//! `/unsubscribe` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; use std::io::Read; diff --git a/rpc/src/endpoint/validators.rs b/rpc/src/endpoint/validators.rs index 124abc77e..d3153dbd5 100644 --- a/rpc/src/endpoint/validators.rs +++ b/rpc/src/endpoint/validators.rs @@ -1,4 +1,4 @@ -//! `/validators` endpoint JSONRPC wrapper +//! `/validators` endpoint JSON-RPC wrapper use serde::{Deserialize, Serialize}; diff --git a/rpc/src/error.rs b/rpc/src/error.rs index 7cb0a3f37..a1d90fd9a 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -1,4 +1,4 @@ -//! JSONRPC error types +//! JSON-RPC error types #[cfg(all(feature = "client", feature = "transport_websocket"))] use async_tungstenite::tungstenite::Error as WSError; diff --git a/rpc/src/event.rs b/rpc/src/event.rs index 5e43ef4ec..c6fa4e847 100644 --- a/rpc/src/event.rs +++ b/rpc/src/event.rs @@ -23,7 +23,7 @@ pub struct Event { } impl Response for Event {} -/// A JSONRPC-wrapped event. +/// A JSON-RPC-wrapped event. pub type WrappedEvent = Wrapper; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] diff --git a/rpc/src/id.rs b/rpc/src/id.rs index ff15421bc..2a18abb13 100644 --- a/rpc/src/id.rs +++ b/rpc/src/id.rs @@ -1,9 +1,9 @@ -//! JSONRPC IDs +//! JSON-RPC IDs use getrandom::getrandom; use serde::{Deserialize, Serialize}; -/// JSONRPC ID: request-specific identifier +/// JSON-RPC ID: request-specific identifier #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Ord, PartialOrd)] #[serde(untagged)] pub enum Id { @@ -16,7 +16,7 @@ pub enum Id { } impl Id { - /// Create a JSONRPC ID containing a UUID v4 (i.e. random) + /// 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!"); diff --git a/rpc/src/method.rs b/rpc/src/method.rs index 107c42fda..37e5b42f1 100644 --- a/rpc/src/method.rs +++ b/rpc/src/method.rs @@ -1,4 +1,4 @@ -//! JSONRPC request methods +//! JSON-RPC request methods use super::Error; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; @@ -7,9 +7,9 @@ use std::{ str::FromStr, }; -/// JSONRPC request methods. +/// JSON-RPC request methods. /// -/// Serialized as the "method" field of JSONRPC/HTTP requests. +/// Serialized as the "method" field of JSON-RPC/HTTP requests. #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] pub enum Method { /// Get ABCI info diff --git a/rpc/src/request.rs b/rpc/src/request.rs index 3fbc8463a..3f2d9659b 100644 --- a/rpc/src/request.rs +++ b/rpc/src/request.rs @@ -1,10 +1,10 @@ -//! JSONRPC requests +//! JSON-RPC requests use super::{Id, Method, Version}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::fmt::Debug; -/// JSONRPC requests +/// JSON-RPC requests pub trait Request: Debug + DeserializeOwned + Serialize + Sized + Send { /// Response type for this command type Response: super::response::Response; @@ -18,10 +18,10 @@ pub trait Request: Debug + DeserializeOwned + Serialize + Sized + Send { } } -/// JSONRPC request wrapper (i.e. message envelope) +/// JSON-RPC request wrapper (i.e. message envelope) #[derive(Debug, Deserialize, Serialize)] pub struct Wrapper { - /// JSONRPC version + /// JSON-RPC version jsonrpc: Version, /// Identifier included in request diff --git a/rpc/src/response.rs b/rpc/src/response.rs index e9b664486..a31c5bf58 100644 --- a/rpc/src/response.rs +++ b/rpc/src/response.rs @@ -1,29 +1,29 @@ -//! JSONRPC response types +//! JSON-RPC response types use super::{Error, Id, Version}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::io::Read; -/// JSONRPC responses +/// JSON-RPC responses pub trait Response: Serialize + DeserializeOwned + Sized { - /// Parse a JSONRPC response from a JSON string + /// Parse a JSON-RPC response from a JSON string fn from_string(response: impl AsRef<[u8]>) -> Result { let wrapper: Wrapper = serde_json::from_slice(response.as_ref()).map_err(Error::parse_error)?; wrapper.into_result() } - /// Parse a JSONRPC response from an `io::Reader` + /// Parse a JSON-RPC response from an `io::Reader` fn from_reader(reader: impl Read) -> Result { let wrapper: Wrapper = serde_json::from_reader(reader).map_err(Error::parse_error)?; wrapper.into_result() } } -/// JSONRPC response wrapper (i.e. message envelope) +/// JSON-RPC response wrapper (i.e. message envelope) #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Wrapper { - /// JSONRPC version + /// JSON-RPC version jsonrpc: Version, /// Identifier included in request @@ -40,12 +40,12 @@ impl Wrapper where R: Response, { - /// Get JSONRPC version + /// Get JSON-RPC version pub fn version(&self) -> &Version { &self.jsonrpc } - /// Get JSONRPC ID + /// Get JSON-RPC ID #[allow(dead_code)] pub fn id(&self) -> &Id { &self.id diff --git a/rpc/src/version.rs b/rpc/src/version.rs index 13471dcaf..79c310d66 100644 --- a/rpc/src/version.rs +++ b/rpc/src/version.rs @@ -1,4 +1,4 @@ -//! JSONRPC versions +//! JSON-RPC versions use super::error::Error; use serde::{Deserialize, Serialize}; @@ -7,21 +7,21 @@ use std::{ str::FromStr, }; -/// Supported JSONRPC version +/// Supported JSON-RPC version const SUPPORTED_VERSION: &str = "2.0"; -/// JSONRPC version +/// JSON-RPC version // TODO(tarcieri): add restrictions/validations on these formats? Use an `enum`? #[derive(Clone, Debug, Deserialize, Eq, PartialEq, PartialOrd, Ord, Serialize)] pub struct Version(String); impl Version { - /// Get the currently supported JSONRPC version + /// Get the currently supported JSON-RPC version pub fn current() -> Self { Version(SUPPORTED_VERSION.to_owned()) } - /// Is this JSONRPC version supported? + /// Is this JSON-RPC version supported? pub fn is_supported(&self) -> bool { self.0.eq(SUPPORTED_VERSION) } diff --git a/tendermint/Cargo.toml b/tendermint/Cargo.toml index 796b33d93..8d3d89ee1 100644 --- a/tendermint/Cargo.toml +++ b/tendermint/Cargo.toml @@ -15,7 +15,7 @@ description = """ Byzantine fault tolerant applications written in any programming language. This crate provides core types for representing information about Tendermint blockchain networks, including chain information types, secret connections, - and remote procedure calls (JSONRPC). + and remote procedure calls (JSON-RPC). """ authors = [ diff --git a/tendermint/src/lib.rs b/tendermint/src/lib.rs index 567acf0c6..40200b413 100644 --- a/tendermint/src/lib.rs +++ b/tendermint/src/lib.rs @@ -2,7 +2,7 @@ //! Byzantine fault tolerant applications written in any programming language. //! This crate provides core types for representing information about Tendermint //! blockchain networks, including chain information types, secret connections, -//! and remote procedure calls (JSONRPC). +//! and remote procedure calls (JSON-RPC). #![deny( warnings, From ba2ca31f4ff27a62739630130e65d35f35efbdaa Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 27 Aug 2020 15:36:50 -0400 Subject: [PATCH 47/60] Reword docs for SubscriptionClient Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 735e6ff28..029381398 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -15,19 +15,13 @@ use std::pin::Pin; /// A client that exclusively provides [`Event`] subscription capabilities, /// without any other RPC method support. /// -/// To build a full-featured client, implement both this trait as well as the -/// [`Client`] trait. -/// /// [`Event`]: event/struct.Event.html -/// [`Client`]: trait.Client.html #[async_trait] pub trait SubscriptionClient: ClosableClient { /// `/subscribe`: subscribe to receive events produced by the given query. /// - /// Uses an unbounded buffer for the resulting [`Subscription`]. We do not - /// implement bounded buffers at present. - /// - /// [`Subscription`]: struct.Subscription.html + /// For query syntax details, see the + /// [Tendermint RPC docs](https://docs.tendermint.com/master/rpc/#/Websocket/subscribe). async fn subscribe(&mut self, query: String) -> Result; } From b4253c6604bb35b49570db09d6d0e8db4a3bccad Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 27 Aug 2020 15:38:01 -0400 Subject: [PATCH 48/60] TODO is no longer necessary since we are using unbounded channels Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 029381398..c5c79fe3f 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -233,9 +233,6 @@ impl SubscriptionRouter { }; let mut disconnected = Vec::::new(); for (id, event_tx) in subs_for_query { - // TODO(thane): Right now we automatically remove any disconnected - // or full channels. We must handle full channels - // differently to disconnected ones. if event_tx.send(Ok(ev.clone())).await.is_err() { disconnected.push(id.clone()); } From b9e240e65e1429e3b557c72e58cb2a58ce73e285 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 27 Aug 2020 16:01:15 -0400 Subject: [PATCH 49/60] Update CHANGELOG Signed-off-by: Thane Thomson --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 780ae15a2..eee7fcd76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,34 @@ - Created Rust structs from Tendermint Proto files ([#504]) +### RPC Client (`tendermint-rpc` crate) + +#### Changed +- **BREAKING**: The entire RPC client interface has been refactored. The + `Client` struct has now been replaced by an `HttpClient` struct, which + implements all of the RPC methods except those relating to event + subscription. To access this struct, you now need to enable both the + `client` and `transport_http` features when using the `tendermint-rpc` + crate. + ([#516](https://github.com/informalsystems/tendermint-rs/pull/516)) + +#### Added +- A `WebSocketSubscriptionClient` is now provided to facilitate event + subscription for a limited range of RPC events over a WebSocket connection. + See the [Tendermint `/subscribe` endpoint's](https://docs.tendermint.com/master/rpc/#/Websocket/subscribe) + and the `tendermint-rpc` crate's docs for more details. + To access this struct you need to enable both the `client`, `subscription` + and `transport_websocket` features when using the `tendermint-rpc` crate. + ([#516](https://github.com/informalsystems/tendermint-rs/pull/516)) +- A `MockClient` and `MockSubscriptionClient` struct are available for use in + instances where you may want to interact with the Tendermint RPC from your + tests without integrating with an actual node. To access these structs you + need to enable the `client`, `subscription` and `transport_mock` features + when using the `tendermint-rpc` crate. If you only want to use the + `MockClient` struct, just enable features `client` and `transport_mock`. + See the crate docs for more details. + ([#516](https://github.com/informalsystems/tendermint-rs/pull/516)) + ## [0.15.0] (2020-07-17) This release is mostly about the revamped [light-client] library and the [light-node] command-line interface. From 99a321fb5bcd01df08f1a1a8659dc1c1cc9a2b6c Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 27 Aug 2020 18:41:24 -0400 Subject: [PATCH 50/60] Clarify docs for Subscription::terminate Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index c5c79fe3f..0d3114bac 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -87,10 +87,10 @@ impl Subscription { } } - /// Gracefully terminate this subscription. + /// Gracefully terminate this subscription and consume it. /// - /// This can be called from any asynchronous context. It only returns once - /// it receives confirmation of termination. + /// 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.terminate_tx From 58d52ef0adb2fc222ad3c6836895fce723ba35e1 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Sep 2020 08:39:46 -0400 Subject: [PATCH 51/60] Fix features for tendermint-rpc dependency in light client Signed-off-by: Thane Thomson --- light-client/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index 360c76acd..8f0d1cac8 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -23,12 +23,12 @@ crate-type = ["cdylib", "rlib"] [features] default = ["rpc-client"] -rpc-client = ["tendermint-rpc/client"] +rpc-client = ["tendermint-rpc/client", "tendermint-rpc/transport_http"] secp256k1 = ["tendermint/secp256k1", "tendermint-rpc/secp256k1"] [dependencies] tendermint = { version = "0.16.0", path = "../tendermint" } -tendermint-rpc = { version = "0.16.0", path = "../rpc", features = ["client", "transport_http"] } +tendermint-rpc = { version = "0.16.0", path = "../rpc", default-features = false } anomaly = { version = "0.2.0", features = ["serializer"] } contracts = "0.4.0" From e218dd6dccc78f527d8e7682ce853e5ffc328f55 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Sep 2020 08:39:54 -0400 Subject: [PATCH 52/60] Fix clippy warnings Signed-off-by: Thane Thomson --- rpc/src/client/transport/mock.rs | 6 +- rpc/src/client/transport/mock/subscription.rs | 13 ++- rpc/src/client/transport/websocket.rs | 81 ++++++++++--------- 3 files changed, 51 insertions(+), 49 deletions(-) diff --git a/rpc/src/client/transport/mock.rs b/rpc/src/client/transport/mock.rs index e7f4d08e5..79208e962 100644 --- a/rpc/src/client/transport/mock.rs +++ b/rpc/src/client/transport/mock.rs @@ -139,9 +139,11 @@ mod test { #[tokio::test] async fn mock_client() { + let abci_info_fixture = read_json_fixture("abci_info").await; + let block_fixture = read_json_fixture("block").await; let matcher = MockRequestMethodMatcher::default() - .map(Method::AbciInfo, Ok(read_json_fixture("abci_info").await)) - .map(Method::Block, Ok(read_json_fixture("block").await)); + .map(Method::AbciInfo, Ok(abci_info_fixture)) + .map(Method::Block, Ok(block_fixture)); let client = MockClient::new(matcher); let abci_info = client.abci_info().await.unwrap(); diff --git a/rpc/src/client/transport/mock/subscription.rs b/rpc/src/client/transport/mock/subscription.rs index eabb33777..9ab69a022 100644 --- a/rpc/src/client/transport/mock/subscription.rs +++ b/rpc/src/client/transport/mock/subscription.rs @@ -199,11 +199,10 @@ mod test { #[tokio::test] async fn mock_subscription_client() { let mut client = MockSubscriptionClient::default(); - let events = vec![ - read_event("event_new_block_1").await, - read_event("event_new_block_2").await, - read_event("event_new_block_3").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; + let events = vec![event1, event2, event3]; let subs1 = client .subscribe("tm.event='NewBlock'".to_string()) @@ -221,8 +220,8 @@ mod test { client.publish(ev.clone()).await.unwrap(); } - let (subs1_events, subs2_events) = - (subs1_events.await.unwrap(), subs2_events.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()); diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index b57586ef6..72f24f3e0 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -447,7 +447,7 @@ mod test { } async fn terminate(&mut self) { - while self.handlers.len() > 0 { + while !self.handlers.is_empty() { let handler = match self.handlers.pop() { Some(h) => h, None => break, @@ -556,43 +556,44 @@ mod test { async fn handle_incoming_text_msg(&mut self, msg: String) -> Option> { match serde_json::from_str::(&msg) { - Ok(json_msg) => match json_msg.get("method") { - Some(json_method) => match Method::from_str(json_method.as_str().unwrap()) { - Ok(method) => match method { - Method::Subscribe => { - let req = serde_json::from_str::< - request::Wrapper, - >(&msg) - .unwrap(); - - self.add_subscription( - req.params().query.clone(), - req.id().clone().try_into().unwrap(), + Ok(json_msg) => { + if let Some(json_method) = json_msg.get("method") { + match Method::from_str(json_method.as_str().unwrap()) { + Ok(method) => match method { + Method::Subscribe => { + let req = serde_json::from_str::< + request::Wrapper, + >(&msg) + .unwrap(); + + self.add_subscription( + req.params().query.clone(), + req.id().clone().try_into().unwrap(), + ); + self.send(req.id().clone(), subscribe::Response {}).await; + } + Method::Unsubscribe => { + let req = serde_json::from_str::< + request::Wrapper, + >(&msg) + .unwrap(); + + self.remove_subscription(req.params().query.clone()); + self.send(req.id().clone(), unsubscribe::Response {}).await; + } + _ => { + println!("Unsupported method in incoming request: {}", &method); + } + }, + Err(e) => { + println!( + "Unexpected method in incoming request: {} ({})", + json_method, e ); - self.send(req.id().clone(), subscribe::Response {}).await; } - Method::Unsubscribe => { - let req = serde_json::from_str::< - request::Wrapper, - >(&msg) - .unwrap(); - - self.remove_subscription(req.params().query.clone()); - self.send(req.id().clone(), unsubscribe::Response {}).await; - } - _ => { - println!("Unsupported method in incoming request: {}", &method); - } - }, - Err(e) => { - println!( - "Unexpected method in incoming request: {} ({})", - json_method, e - ); } - }, - None => (), - }, + } + } Err(e) => { println!("Failed to parse incoming request: {} ({})", &msg, e); } @@ -647,11 +648,11 @@ mod test { #[tokio::test] async fn websocket_client_happy_path() { - let test_events = vec![ - read_event("event_new_block_1").await, - read_event("event_new_block_2").await, - read_event("event_new_block_3").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; + let test_events = vec![event1, event2, event3]; + println!("Starting WebSocket server..."); let mut server = TestServer::new("127.0.0.1:0").await; println!("Creating client RPC WebSocket connection..."); From ee70331554dd25b5aa9e56b938c5f0d5aefffc00 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 15 Sep 2020 15:32:03 +0200 Subject: [PATCH 53/60] Do not feature-guard IoError::IoError variant --- light-client/src/components/io.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index 1737a04bf..a7cf359bb 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -2,11 +2,12 @@ use contracts::{contract_trait, post}; use serde::{Deserialize, Serialize}; -#[cfg(feature = "rpc-client")] -use tendermint_rpc as rpc; +use thiserror::Error; + #[cfg(feature = "rpc-client")] use tendermint_rpc::Client; -use thiserror::Error; + +use tendermint_rpc as rpc; use crate::types::{Height, LightBlock, PeerId}; @@ -31,7 +32,6 @@ impl From for AtHeight { /// I/O errors #[derive(Clone, Debug, Error, PartialEq, Serialize, Deserialize)] pub enum IoError { - #[cfg(feature = "rpc-client")] /// Wrapper for a `tendermint::rpc::Error`. #[error(transparent)] IoError(#[from] rpc::Error), From e7bb6a1aac8b49e067549207f350a3c4ce10a536 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 15 Sep 2020 15:32:13 +0200 Subject: [PATCH 54/60] Rename IoError::IoError to IoError::RpcError --- light-client/src/components/io.rs | 6 +++--- light-client/src/evidence.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index a7cf359bb..ead060706 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -34,7 +34,7 @@ impl From for AtHeight { pub enum IoError { /// Wrapper for a `tendermint::rpc::Error`. #[error(transparent)] - IoError(#[from] rpc::Error), + RpcError(#[from] rpc::Error), /// Given height is invalid #[error("invalid height: {0}")] @@ -146,7 +146,7 @@ mod prod { match res { Ok(response) => Ok(response.signed_header), - Err(err) => Err(IoError::IoError(err)), + Err(err) => Err(IoError::RpcError(err)), } } @@ -171,7 +171,7 @@ mod prod { match res { Ok(response) => Ok(TMValidatorSet::new(response.validators)), - Err(err) => Err(IoError::IoError(err)), + Err(err) => Err(IoError::RpcError(err)), } } diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index 1dbed3650..e1a746556 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -43,7 +43,7 @@ mod prod { match res { Ok(response) => Ok(response.hash), - Err(err) => Err(IoError::IoError(err)), + Err(err) => Err(IoError::RpcError(err)), } } } From a891c21cf9d573d767f0c91a08d2e2e057fdd8f7 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Sep 2020 09:52:26 -0400 Subject: [PATCH 55/60] Refactor subscription state modelling Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 46 +++++-------- rpc/src/client/transport/websocket.rs | 99 +++++++++++++-------------- 2 files changed, 64 insertions(+), 81 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 0d3114bac..2de1b15d8 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -202,7 +202,6 @@ pub enum SubscriptionState { Pending, Active, Cancelling, - NotFound, } /// Provides a mechanism for tracking [`Subscription`]s and routing [`Event`]s @@ -391,17 +390,16 @@ impl SubscriptionRouter { /// Utility method to determine the current state of the subscription with /// the given ID. - pub fn subscription_state(&self, req_id: &str) -> SubscriptionState { + pub fn subscription_state(&self, req_id: &str) -> Option { if self.pending_subscribe.contains_key(req_id) { - return SubscriptionState::Pending; + Some(SubscriptionState::Pending) + } else if self.pending_unsubscribe.contains_key(req_id) { + Some(SubscriptionState::Cancelling) + } else if self.is_active(&SubscriptionId::from(req_id)) { + Some(SubscriptionState::Active) + } else { + None } - if self.pending_unsubscribe.contains_key(req_id) { - return SubscriptionState::Cancelling; - } - if self.is_active(&SubscriptionId::from(req_id)) { - return SubscriptionState::Active; - } - SubscriptionState::NotFound } } @@ -504,10 +502,7 @@ mod test { let mut ev = read_event("event_new_block_1").await; ev.query = query.clone(); - assert_eq!( - SubscriptionState::NotFound, - router.subscription_state(&subs_id.to_string()) - ); + assert!(router.subscription_state(&subs_id.to_string()).is_none()); router.pending_add( subs_id.as_ref(), &subs_id, @@ -517,7 +512,7 @@ mod test { ); assert_eq!( SubscriptionState::Pending, - router.subscription_state(subs_id.as_ref()) + router.subscription_state(subs_id.as_ref()).unwrap() ); router.publish(ev.clone()).await; must_not_recv(&mut event_rx, 50).await; @@ -525,7 +520,7 @@ mod test { router.confirm_add(subs_id.as_ref()).await.unwrap(); assert_eq!( SubscriptionState::Active, - router.subscription_state(subs_id.as_ref()) + router.subscription_state(subs_id.as_ref()).unwrap() ); must_not_recv(&mut event_rx, 50).await; let _ = must_recv(&mut result_rx, 500).await; @@ -538,14 +533,11 @@ mod test { router.pending_remove(subs_id.as_ref(), &subs_id, query.clone(), result_tx); assert_eq!( SubscriptionState::Cancelling, - router.subscription_state(subs_id.as_ref()), + router.subscription_state(subs_id.as_ref()).unwrap(), ); router.confirm_remove(subs_id.as_ref()).await.unwrap(); - assert_eq!( - SubscriptionState::NotFound, - router.subscription_state(subs_id.as_ref()) - ); + assert!(router.subscription_state(subs_id.as_ref()).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") @@ -562,14 +554,11 @@ mod test { let mut ev = read_event("event_new_block_1").await; ev.query = query.clone(); - assert_eq!( - SubscriptionState::NotFound, - router.subscription_state(subs_id.as_ref()) - ); + assert!(router.subscription_state(subs_id.as_ref()).is_none()); router.pending_add(subs_id.as_ref(), &subs_id, query, event_tx, result_tx); assert_eq!( SubscriptionState::Pending, - router.subscription_state(subs_id.as_ref()) + router.subscription_state(subs_id.as_ref()).unwrap() ); router.publish(ev.clone()).await; must_not_recv(&mut event_rx, 50).await; @@ -579,10 +568,7 @@ mod test { .cancel_add(subs_id.as_ref(), cancel_error.clone()) .await .unwrap(); - assert_eq!( - SubscriptionState::NotFound, - router.subscription_state(subs_id.as_ref()) - ); + assert!(router.subscription_state(subs_id.as_ref()).is_none()); assert_eq!(Err(cancel_error), must_recv(&mut result_rx, 500).await); router.publish(ev.clone()).await; diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index 72f24f3e0..1828f4dc0 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -248,74 +248,71 @@ impl WebSocketSubscriptionDriver { 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::Pong(_) | Message::Binary(_) => Ok(()), - Message::Close(_) => Ok(()), + Message::Ping(v) => self.pong(v).await?, + _ => (), } + Ok(()) } - async fn handle_text_msg(&mut self, msg: String) -> Result<()> { + async fn handle_text_msg(&mut self, msg: String) { match Event::from_string(&msg) { Ok(ev) => { self.router.publish(ev).await; - Ok(()) } - Err(_) => match serde_json::from_str::>(&msg) { - Ok(wrapper) => self.handle_generic_response(wrapper).await, - _ => Ok(()), - }, + Err(_) => { + if let Ok(wrapper) = + serde_json::from_str::>(&msg) + { + self.handle_generic_response(wrapper).await; + } + } } } - async fn handle_generic_response( - &mut self, - wrapper: response::Wrapper, - ) -> Result<()> { + async fn handle_generic_response(&mut self, wrapper: response::Wrapper) { let subs_id: SubscriptionId = match wrapper.id().clone().try_into() { Ok(id) => id, // Just ignore the message if it doesn't have an intelligible ID. - Err(_) => return Ok(()), + Err(_) => return, }; - match wrapper.into_result() { - Ok(_) => match self.router.subscription_state(subs_id.as_ref()) { - SubscriptionState::Pending => { - let _ = self.router.confirm_add(subs_id.as_ref()).await; - } - SubscriptionState::Cancelling => { - let _ = self.router.confirm_remove(subs_id.as_ref()).await; - } - SubscriptionState::Active => { - if let Some(event_tx) = self.router.get_active_subscription_mut(&subs_id) { - 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; + if let Some(state) = self.router.subscription_state(subs_id.as_ref()) { + match wrapper.into_result() { + Ok(_) => match state { + SubscriptionState::Pending => { + let _ = self.router.confirm_add(subs_id.as_ref()).await; } - } - SubscriptionState::NotFound => (), - }, - Err(e) => match self.router.subscription_state(subs_id.as_ref()) { - SubscriptionState::Pending => { - let _ = self.router.cancel_add(subs_id.as_ref(), e).await; - } - SubscriptionState::Cancelling => { - let _ = self.router.cancel_remove(subs_id.as_ref(), 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(&subs_id) { - // TODO(thane): Does an error here warrant terminating the subscription, or the driver? - let _ = event_tx.send(Err(e)).await; + SubscriptionState::Cancelling => { + let _ = self.router.confirm_remove(subs_id.as_ref()).await; } - } - SubscriptionState::NotFound => (), - }, + SubscriptionState::Active => { + if let Some(event_tx) = self.router.get_active_subscription_mut(&subs_id) { + 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(subs_id.as_ref(), e).await; + } + SubscriptionState::Cancelling => { + let _ = self.router.cancel_remove(subs_id.as_ref(), 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(&subs_id) { + // TODO(thane): Does an error here warrant terminating the subscription, or the driver? + let _ = event_tx.send(Err(e)).await; + } + } + }, + } } - - Ok(()) } async fn pong(&mut self, v: Vec) -> Result<()> { From c22a5dac6363fabe17c9ddafdc7cf8f7b81436b2 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Sep 2020 14:02:46 -0400 Subject: [PATCH 56/60] Replace SubscriptionId AsRef implementation with as_str() method for clarity Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 38 +++++++++++++-------------- rpc/src/client/transport/websocket.rs | 14 +++++----- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 2de1b15d8..48cb05531 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -169,18 +169,18 @@ impl TryInto for Id { } } -impl AsRef for SubscriptionId { - fn as_ref(&self) -> &str { - self.0.as_ref() - } -} - impl From<&str> for SubscriptionId { fn from(s: &str) -> Self { Self(s.to_string()) } } +impl SubscriptionId { + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + #[derive(Debug)] struct PendingSubscribe { id: SubscriptionId, @@ -504,7 +504,7 @@ mod test { assert!(router.subscription_state(&subs_id.to_string()).is_none()); router.pending_add( - subs_id.as_ref(), + subs_id.as_str(), &subs_id, query.clone(), event_tx, @@ -512,15 +512,15 @@ mod test { ); assert_eq!( SubscriptionState::Pending, - router.subscription_state(subs_id.as_ref()).unwrap() + 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_ref()).await.unwrap(); + router.confirm_add(subs_id.as_str()).await.unwrap(); assert_eq!( SubscriptionState::Active, - router.subscription_state(subs_id.as_ref()).unwrap() + router.subscription_state(subs_id.as_str()).unwrap() ); must_not_recv(&mut event_rx, 50).await; let _ = must_recv(&mut result_rx, 500).await; @@ -530,14 +530,14 @@ mod test { assert_eq!(ev, received_ev); let (result_tx, mut result_rx) = unbounded(); - router.pending_remove(subs_id.as_ref(), &subs_id, query.clone(), result_tx); + router.pending_remove(subs_id.as_str(), &subs_id, query.clone(), result_tx); assert_eq!( SubscriptionState::Cancelling, - router.subscription_state(subs_id.as_ref()).unwrap(), + router.subscription_state(subs_id.as_str()).unwrap(), ); - router.confirm_remove(subs_id.as_ref()).await.unwrap(); - assert!(router.subscription_state(subs_id.as_ref()).is_none()); + 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") @@ -554,21 +554,21 @@ mod test { let mut ev = read_event("event_new_block_1").await; ev.query = query.clone(); - assert!(router.subscription_state(subs_id.as_ref()).is_none()); - router.pending_add(subs_id.as_ref(), &subs_id, query, event_tx, result_tx); + 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_ref()).unwrap() + 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_ref(), cancel_error.clone()) + .cancel_add(subs_id.as_str(), cancel_error.clone()) .await .unwrap(); - assert!(router.subscription_state(subs_id.as_ref()).is_none()); + 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; diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index 1828f4dc0..ec8da90fa 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -226,7 +226,7 @@ impl WebSocketSubscriptionDriver { return Ok(()); } self.router - .pending_add(id.as_ref(), &id, query, event_tx, result_tx); + .pending_add(id.as_str(), &id, query, event_tx, result_tx); Ok(()) } @@ -241,7 +241,7 @@ impl WebSocketSubscriptionDriver { return Ok(()); } self.router - .pending_remove(id.as_ref(), &id, term.query.clone(), term.result_tx); + .pending_remove(id.as_str(), &id, term.query.clone(), term.result_tx); Ok(()) } @@ -275,14 +275,14 @@ impl WebSocketSubscriptionDriver { // Just ignore the message if it doesn't have an intelligible ID. Err(_) => return, }; - if let Some(state) = self.router.subscription_state(subs_id.as_ref()) { + if let Some(state) = self.router.subscription_state(subs_id.as_str()) { match wrapper.into_result() { Ok(_) => match state { SubscriptionState::Pending => { - let _ = self.router.confirm_add(subs_id.as_ref()).await; + let _ = self.router.confirm_add(subs_id.as_str()).await; } SubscriptionState::Cancelling => { - let _ = self.router.confirm_remove(subs_id.as_ref()).await; + let _ = self.router.confirm_remove(subs_id.as_str()).await; } SubscriptionState::Active => { if let Some(event_tx) = self.router.get_active_subscription_mut(&subs_id) { @@ -296,10 +296,10 @@ impl WebSocketSubscriptionDriver { }, Err(e) => match state { SubscriptionState::Pending => { - let _ = self.router.cancel_add(subs_id.as_ref(), e).await; + let _ = self.router.cancel_add(subs_id.as_str(), e).await; } SubscriptionState::Cancelling => { - let _ = self.router.cancel_remove(subs_id.as_ref(), e).await; + let _ = self.router.cancel_remove(subs_id.as_str(), e).await; } // This is important to allow the remote endpoint to // arbitrarily send error responses back to specific From 1753566bb1a6e356e7f6590dd0d11d5dbed4b6ff Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Sep 2020 14:18:21 -0400 Subject: [PATCH 57/60] Rename TxResult and TxResultResult for clarity Signed-off-by: Thane Thomson --- rpc/src/event.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rpc/src/event.rs b/rpc/src/event.rs index c6fa4e847..35975b464 100644 --- a/rpc/src/event.rs +++ b/rpc/src/event.rs @@ -37,23 +37,23 @@ pub enum EventData { }, #[serde(alias = "tendermint/event/Tx")] Tx { - tx_result: TxResult, + tx_result: TxInfo, }, GenericJSONEvent(serde_json::Value), } -/// Tx Result +/// Transaction result info. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct TxResult { +pub struct TxInfo { pub height: String, pub index: i64, pub tx: String, - pub result: TxResultResult, + pub result: TxResult, } -/// TX Results Results +/// Transaction result. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct TxResultResult { +pub struct TxResult { pub log: String, pub gas_wanted: String, pub gas_used: String, From 4db8d9e9a34953cb0fb012e5b0937362353e019c Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Sep 2020 14:20:49 -0400 Subject: [PATCH 58/60] Add explanation of SubscriptionRouter subscriptions field Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 48cb05531..c63dba200 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -211,6 +211,9 @@ pub enum SubscriptionState { /// [`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>>>, // A map of JSON-RPC request IDs (for `/subscribe` requests) to pending // subscription requests. From e2936931591e772a30afef244e16463b069613a0 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Sep 2020 14:23:48 -0400 Subject: [PATCH 59/60] Comment on assumption regarding handling of event publishing failure Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index c63dba200..db76964ce 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -233,6 +233,9 @@ impl SubscriptionRouter { 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() { From e7403a62de4253c8a8441244258fd60e773e1d63 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Sep 2020 14:26:29 -0400 Subject: [PATCH 60/60] Add comment explaining redeclaration and obtaining of local var Signed-off-by: Thane Thomson --- rpc/src/client/subscription.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index db76964ce..458ea5611 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -242,6 +242,9 @@ impl SubscriptionRouter { 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);