From b910ffdd728619a9c048995ae24470f226959dae Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Dec 2020 17:59:38 -0500 Subject: [PATCH 01/29] Update proto and tendermint deps to make way for abci crate Signed-off-by: Thane Thomson --- proto/Cargo.toml | 4 ++-- tendermint/Cargo.toml | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 5167690ad..c7c1805a4 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -17,9 +17,9 @@ description = """ all-features = true [dependencies] -prost = { version = "0.6" } +prost = { git = "https://github.com/danburkert/prost", rev = "423f5ec5bd165a7007a388edfb2b485d5bbf40c7" } prost-types = { version = "0.6" } -bytes = "0.5" +bytes = "0.6" anomaly = "0.2" thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } diff --git a/tendermint/Cargo.toml b/tendermint/Cargo.toml index e947d1a72..c9d5aa9c2 100644 --- a/tendermint/Cargo.toml +++ b/tendermint/Cargo.toml @@ -35,14 +35,13 @@ crate-type = ["cdylib", "rlib"] [dependencies] anomaly = "0.2" async-trait = "0.1" -bytes = "0.5" +bytes = "0.6" chrono = { version = "0.4", features = ["serde"] } ed25519 = "1" ed25519-dalek = { version = "1", features = ["serde"] } futures = "0.3" num-traits = "0.2" once_cell = "1.3" -prost = "0.6" prost-types = "0.6" serde = { version = "1", features = ["derive"] } serde_json = "1" From db3e7eacf3b6c6c0f3be7f5f1ccc163f8a5da259 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Dec 2020 18:00:11 -0500 Subject: [PATCH 02/29] Add ABCI echo request Signed-off-by: Thane Thomson --- tendermint/src/abci.rs | 2 + tendermint/src/abci/request.rs | 55 ++++++++++++++++++++++++++++ tendermint/src/abci/request/echo.rs | 41 +++++++++++++++++++++ tendermint/src/abci/response.rs | 55 ++++++++++++++++++++++++++++ tendermint/src/abci/response/echo.rs | 41 +++++++++++++++++++++ tendermint/src/error.rs | 8 ++++ 6 files changed, 202 insertions(+) create mode 100644 tendermint/src/abci/request.rs create mode 100644 tendermint/src/abci/request/echo.rs create mode 100644 tendermint/src/abci/response.rs create mode 100644 tendermint/src/abci/response/echo.rs diff --git a/tendermint/src/abci.rs b/tendermint/src/abci.rs index 9a0f84761..3bf92ab77 100644 --- a/tendermint/src/abci.rs +++ b/tendermint/src/abci.rs @@ -13,6 +13,8 @@ mod gas; mod info; mod log; mod path; +pub mod request; +pub mod response; pub mod responses; pub mod tag; pub mod transaction; diff --git a/tendermint/src/abci/request.rs b/tendermint/src/abci/request.rs new file mode 100644 index 000000000..38080f2ed --- /dev/null +++ b/tendermint/src/abci/request.rs @@ -0,0 +1,55 @@ +//! ABCI requests. + +mod echo; +pub use echo::Echo; + +use crate::{Error, Kind}; +use std::convert::{TryFrom, TryInto}; +use tendermint_proto::abci::request::Value; +use tendermint_proto::abci::Request as RawRequest; +use tendermint_proto::Protobuf; + +/// ABCI request wrapper. +#[derive(Debug, Clone, PartialEq)] +pub enum Request { + /// Request that the ABCI server echo a specific message back to the client. + Echo(Echo), +} + +impl Protobuf for Request {} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(raw: RawRequest) -> Result { + let value = raw.value.ok_or(Kind::MissingAbciRequestValue)?; + Ok(match value { + Value::Echo(raw_req) => Self::Echo(raw_req.try_into()?), + _ => unimplemented!(), + // Value::Flush(_) => {} + // Value::Info(_) => {} + // Value::SetOption(_) => {} + // Value::InitChain(_) => {} + // Value::Query(_) => {} + // Value::BeginBlock(_) => {} + // Value::CheckTx(_) => {} + // Value::DeliverTx(_) => {} + // Value::EndBlock(_) => {} + // Value::Commit(_) => {} + // Value::ListSnapshots(_) => {} + // Value::OfferSnapshot(_) => {} + // Value::LoadSnapshotChunk(_) => {} + // Value::ApplySnapshotChunk(_) => {} + }) + } +} + +impl From for RawRequest { + fn from(request: Request) -> Self { + Self { + value: Some(match request { + Request::Echo(req) => Value::Echo(req.into()), + }), + } + } +} diff --git a/tendermint/src/abci/request/echo.rs b/tendermint/src/abci/request/echo.rs new file mode 100644 index 000000000..e44b856a7 --- /dev/null +++ b/tendermint/src/abci/request/echo.rs @@ -0,0 +1,41 @@ +//! ABCI echo request. + +use crate::Error; +use std::convert::TryFrom; +use tendermint_proto::abci::RequestEcho; +use tendermint_proto::Protobuf; + +/// Request that the ABCI server echo a message back the client. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct Echo { + /// The message to echo back the client. + pub message: String, +} + +impl Echo { + /// Constructor. + pub fn new>(message: S) -> Self { + Self { + message: message.as_ref().to_owned(), + } + } +} + +impl Protobuf for Echo {} + +impl TryFrom for Echo { + type Error = Error; + + fn try_from(raw: RequestEcho) -> Result { + Ok(Self::new(raw.message)) + } +} + +impl From for RequestEcho { + fn from(request: Echo) -> Self { + Self { + message: request.message, + } + } +} diff --git a/tendermint/src/abci/response.rs b/tendermint/src/abci/response.rs new file mode 100644 index 000000000..77c69cc8c --- /dev/null +++ b/tendermint/src/abci/response.rs @@ -0,0 +1,55 @@ +//! ABCI responses. + +mod echo; +pub use echo::Echo; + +use crate::{Error, Kind}; +use std::convert::{TryFrom, TryInto}; +use tendermint_proto::abci::response::Value; +use tendermint_proto::abci::Response as RawResponse; +use tendermint_proto::Protobuf; + +/// ABCI response wrapper. +#[derive(Debug, Clone, PartialEq)] +pub enum Response { + /// Echo response. + Echo(Echo), +} + +impl Protobuf for Response {} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(raw: RawResponse) -> Result { + let value = raw.value.ok_or(Kind::MissingAbciResponseValue)?; + Ok(match value { + Value::Echo(raw_res) => Self::Echo(raw_res.try_into()?), + _ => unimplemented!(), + // Value::Flush(_) => {} + // Value::Info(_) => {} + // Value::SetOption(_) => {} + // Value::InitChain(_) => {} + // Value::Query(_) => {} + // Value::BeginBlock(_) => {} + // Value::CheckTx(_) => {} + // Value::DeliverTx(_) => {} + // Value::EndBlock(_) => {} + // Value::Commit(_) => {} + // Value::ListSnapshots(_) => {} + // Value::OfferSnapshot(_) => {} + // Value::LoadSnapshotChunk(_) => {} + // Value::ApplySnapshotChunk(_) => {} + }) + } +} + +impl From for RawResponse { + fn from(request: Response) -> Self { + Self { + value: Some(match request { + Response::Echo(res) => Value::Echo(res.into()), + }), + } + } +} diff --git a/tendermint/src/abci/response/echo.rs b/tendermint/src/abci/response/echo.rs new file mode 100644 index 000000000..a5b04c91c --- /dev/null +++ b/tendermint/src/abci/response/echo.rs @@ -0,0 +1,41 @@ +//! ABCI echo response. + +use crate::Error; +use std::convert::TryFrom; +use tendermint_proto::abci::ResponseEcho; +use tendermint_proto::Protobuf; + +/// ABCI echo response. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct Echo { + /// The message to be echoed back to the client. + pub message: String, +} + +impl Echo { + /// Constructor. + pub fn new>(message: S) -> Self { + Self { + message: message.as_ref().to_owned(), + } + } +} + +impl Protobuf for Echo {} + +impl TryFrom for Echo { + type Error = Error; + + fn try_from(raw: ResponseEcho) -> Result { + Ok(Self::new(raw.message)) + } +} + +impl From for ResponseEcho { + fn from(response: Echo) -> Self { + Self { + message: response.message, + } + } +} diff --git a/tendermint/src/error.rs b/tendermint/src/error.rs index f5c678a48..66313c47d 100644 --- a/tendermint/src/error.rs +++ b/tendermint/src/error.rs @@ -196,6 +196,14 @@ pub enum Kind { /// Proposer not found in validator set #[error("proposer with address '{}' not found in validator set", _0)] ProposerNotFound(account::Id), + + /// ABCI request is missing its inner value property. + #[error("malformed ABCI request: request is missing its inner value")] + MissingAbciRequestValue, + + /// ABCI response is missing its inner value property. + #[error("malformed ABCI response: response is missing its inner value")] + MissingAbciResponseValue, } impl Kind { From 241ddac61bc0a077aae52082fd3c28e9ed3bf6bf Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Dec 2020 18:00:44 -0500 Subject: [PATCH 03/29] Re-export encode_varint and decode_varint for use in ABCI codec Signed-off-by: Thane Thomson --- proto/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 001a65601..4128c9204 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -22,6 +22,7 @@ use anomaly::BoxError; use bytes::{Buf, BufMut}; pub use error::{Error, Kind}; use prost::encoding::encoded_len_varint; +pub use prost::encoding::{decode_varint, encode_varint}; use prost::Message; use std::convert::{TryFrom, TryInto}; From 5c347b770b4c8fc7441f2e871c4dc11d16d5d389 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 15 Dec 2020 18:01:03 -0500 Subject: [PATCH 04/29] Add rudimentary Tokio-based ABCI echo server Signed-off-by: Thane Thomson --- Cargo.toml | 1 + abci/Cargo.toml | 24 ++++++ abci/src/application.rs | 29 +++++++ abci/src/codec.rs | 137 ++++++++++++++++++++++++++++++++ abci/src/lib.rs | 13 +++ abci/src/protocol.rs | 1 + abci/src/result.rs | 21 +++++ abci/src/server.rs | 4 + abci/src/server/tokio.rs | 166 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 396 insertions(+) create mode 100644 abci/Cargo.toml create mode 100644 abci/src/application.rs create mode 100644 abci/src/codec.rs create mode 100644 abci/src/lib.rs create mode 100644 abci/src/protocol.rs create mode 100644 abci/src/result.rs create mode 100644 abci/src/server.rs create mode 100644 abci/src/server/tokio.rs diff --git a/Cargo.toml b/Cargo.toml index 3a0569528..5ed9b776d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ + "abci", "light-client", "light-node", "p2p", diff --git a/abci/Cargo.toml b/abci/Cargo.toml new file mode 100644 index 000000000..6479d70b2 --- /dev/null +++ b/abci/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "tendermint-abci" +version = "0.17.0-rc3" +authors = ["Thane Thomson "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +server = [] +with-tokio = [ "tokio", "tokio-util" ] + +[dependencies] +async-trait = "0.1" +bytes = "0.6" +eyre = "0.6" +futures = "0.3" +log = "0.4" +tendermint = { version = "0.17.0-rc3", path = "../tendermint" } +tendermint-proto = { version = "0.17.0-rc3", path = "../proto" } +thiserror = "1.0" + +tokio = { version = "0.3", features = [ "macros", "net", "rt", "sync" ], optional = true } +tokio-util = { version = "0.5", features = [ "codec" ], optional = true } diff --git a/abci/src/application.rs b/abci/src/application.rs new file mode 100644 index 000000000..2429db001 --- /dev/null +++ b/abci/src/application.rs @@ -0,0 +1,29 @@ +//! `async` ABCI server application interface. + +use async_trait::async_trait; +use tendermint::abci::{request, response}; + +#[async_trait] +pub trait Application: Send + Clone { + /// Request that the ABCI server echo back the same message sent to it. + fn echo(&self, request: request::Echo) -> response::Echo { + response::Echo::new(request.message) + } +} + +#[cfg(test)] +pub mod test { + use super::*; + + /// Simple echo application for use in testing. + #[derive(Clone)] + pub struct EchoApp {} + + impl Default for EchoApp { + fn default() -> Self { + Self {} + } + } + + impl Application for EchoApp {} +} diff --git a/abci/src/codec.rs b/abci/src/codec.rs new file mode 100644 index 000000000..7315ea275 --- /dev/null +++ b/abci/src/codec.rs @@ -0,0 +1,137 @@ +//! ABCI codec. + +use crate::{Error, Result}; +use bytes::{Buf, BufMut, BytesMut}; +use tendermint::abci::request::Request; +use tendermint::abci::response::Response; +use tendermint_proto::Protobuf; + +// The maximum number of bytes we expect in a varint. We use this to check if +// we're encountering a decoding error for a varint. +const MAX_VARINT_LENGTH: usize = 16; + +/// Tendermint Socket Protocol encoder. +pub struct TspEncoder {} + +impl TspEncoder { + /// Encode the given request to its raw wire-level representation and store + /// this in the given buffer. + pub fn encode_request(request: Request, mut dst: &mut BytesMut) -> Result<()> { + encode_length_delimited(|mut b| Ok(request.encode(&mut b)?), &mut dst) + } + + /// Encode the given response to its raw wire-level representation and + /// store this in the given buffer. + pub fn encode_response(response: Response, mut dst: &mut BytesMut) -> Result<()> { + encode_length_delimited(|mut b| Ok(response.encode(&mut b)?), &mut dst) + } +} + +/// Tendermint Socket Protocol decoder. +pub struct TspDecoder { + read_buf: BytesMut, +} + +impl TspDecoder { + /// Constructor. + pub fn new() -> Self { + Self { + read_buf: BytesMut::new(), + } + } + + /// Attempt to decode a request from the given buffer. + /// + /// Returns `Ok(None)` if we don't yet have enough data to decode a full + /// request. + pub fn decode_request(&mut self, buf: &mut BytesMut) -> Result> { + self.read_buf.put(buf); + decode_length_delimited(&mut self.read_buf, |mut b| Ok(Request::decode(&mut b)?)) + } + + /// Attempt to decode a response from the given buffer. + /// + /// Returns `Ok(None)` if we don't yet have enough data to decode a full + /// response. + pub fn decode_response(&mut self, buf: &mut BytesMut) -> Result> { + self.read_buf.put(buf); + decode_length_delimited(&mut self.read_buf, |mut b| Ok(Response::decode(&mut b)?)) + } +} + +// encode_varint and decode_varint will be removed once +// https://github.com/tendermint/tendermint/issues/5783 lands in Tendermint. +fn encode_varint(val: u64, mut buf: &mut B) { + tendermint_proto::encode_varint(val << 1, &mut buf); +} + +fn decode_varint(mut buf: &mut B) -> Result { + let len = tendermint_proto::decode_varint(&mut buf) + .map_err(|e| Error::Protobuf(tendermint_proto::Kind::DecodeMessage.into()))?; + Ok(len >> 1) +} + +// Allows us to avoid having to re-export `prost::Message`. +// TODO(thane): Investigate a better approach here. +fn encode_length_delimited(mut encode_fn: F, mut dst: &mut B) -> Result<()> +where + F: FnMut(&mut BytesMut) -> Result<()>, + B: BufMut, +{ + let mut buf = BytesMut::new(); + encode_fn(&mut buf)?; + let buf = buf.freeze(); + encode_varint(buf.len() as u64, &mut dst); + dst.put(buf); + Ok(()) +} + +fn decode_length_delimited(src: &mut BytesMut, mut decode_fn: F) -> Result> +where + F: FnMut(&mut BytesMut) -> Result, +{ + let src_len = src.len(); + let mut tmp = src.clone().freeze(); + let encoded_len = match decode_varint(&mut tmp) { + Ok(len) => len, + Err(e) => { + return if src_len <= MAX_VARINT_LENGTH { + // We've potentially only received a partial length delimiter + Ok(None) + } else { + Err(e) + }; + } + }; + let remaining = tmp.remaining() as u64; + if remaining < encoded_len { + // We don't have enough data yet to decode the entire message + Ok(None) + } else { + let delim_len = src_len - tmp.remaining(); + // We only advance the source buffer once we're sure we have enough + // data to try to decode the result. + src.advance(delim_len + (encoded_len as usize)); + + let mut result_bytes = BytesMut::from(tmp.split_to(encoded_len as usize).as_ref()); + Ok(Some(decode_fn(&mut result_bytes)?)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use tendermint::abci::request::Echo; + + #[test] + fn single_request() { + let request = Request::Echo(Echo::new("Hello TSP!")); + let mut buf = BytesMut::new(); + TspEncoder::encode_request(request.clone(), &mut buf).unwrap(); + + let mut decoder = TspDecoder::new(); + let decoded_request = decoder.decode_request(&mut buf).unwrap().unwrap(); + + assert_eq!(request, decoded_request); + } +} diff --git a/abci/src/lib.rs b/abci/src/lib.rs new file mode 100644 index 000000000..4365a1adc --- /dev/null +++ b/abci/src/lib.rs @@ -0,0 +1,13 @@ +//! ABCI framework for building applications with Tendermint. + +mod application; +mod codec; +mod protocol; +mod result; + +#[cfg(feature = "server")] +pub use application::Application; +#[cfg(feature = "server")] +pub mod server; + +pub use result::{Error, Result}; diff --git a/abci/src/protocol.rs b/abci/src/protocol.rs new file mode 100644 index 000000000..8107efc62 --- /dev/null +++ b/abci/src/protocol.rs @@ -0,0 +1 @@ +//! ABCI protocol. diff --git a/abci/src/result.rs b/abci/src/result.rs new file mode 100644 index 000000000..2e8ca6079 --- /dev/null +++ b/abci/src/result.rs @@ -0,0 +1,21 @@ +//! Results and errors relating to ABCI client/server operations. + +use thiserror::Error; + +/// Convenience type for results produced by the ABCI crate. +pub type Result = std::result::Result; + +/// The various errors produced by the ABCI crate. +#[derive(Debug, Error)] +pub enum Error { + #[error("protocol buffers error")] + Protobuf(#[from] tendermint_proto::Error), + + #[cfg(feature = "with-tokio")] + #[error("network I/O error")] + TokioIo(#[from] tokio::io::Error), + + #[cfg(feature = "with-tokio")] + #[error("channel send error: {0}")] + TokioChannelSend(String), +} diff --git a/abci/src/server.rs b/abci/src/server.rs new file mode 100644 index 000000000..9b5da586f --- /dev/null +++ b/abci/src/server.rs @@ -0,0 +1,4 @@ +//! ABCI servers. + +#[cfg(feature = "with-tokio")] +pub mod tokio; diff --git a/abci/src/server/tokio.rs b/abci/src/server/tokio.rs new file mode 100644 index 000000000..9b51cecf7 --- /dev/null +++ b/abci/src/server/tokio.rs @@ -0,0 +1,166 @@ +//! Tokio-based ABCI server. + +use crate::codec::{TspDecoder, TspEncoder}; +use crate::{Application, Error, Result}; +use bytes::BytesMut; +use futures::{SinkExt, StreamExt}; +use log::info; +use tendermint::abci::request::Request; +use tendermint::abci::response::Response; +use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; +use tokio::sync::mpsc; +use tokio_util::codec::{Decoder, Encoder, Framed}; + +/// Attempts to bind to the given address and immediately start serving the +/// given ABCI application in the current task. +pub async fn serve( + addr: S, + app: A, + mut term: mpsc::Receiver<()>, + ready: mpsc::Sender, +) -> Result<()> +where + S: ToSocketAddrs, + A: Application + 'static, +{ + let listener = TcpListener::bind(addr).await?; + let local_addr = listener.local_addr()?; + info!( + "ABCI server bound to {}, listening for incoming connections", + local_addr, + ); + ready + .send(local_addr.to_string()) + .await + .map_err(|e| Error::TokioChannelSend(e.to_string()))?; + loop { + tokio::select! { + result = listener.accept() => { + let (stream, addr) = result?; + info!("Incoming connection from {}", addr); + let conn_app = app.clone(); + tokio::spawn(async move { handle_client(stream, conn_app).await }); + }, + Some(_) = term.recv() => { + info!("Server terminated"); + return Ok(()) + } + } + } +} + +async fn handle_client(stream: TcpStream, app: A) -> Result<()> { + let codec = ServerCodec::new(); + let mut stream = Framed::new(stream, codec); + loop { + let request = match stream.next().await { + Some(res) => res?, + None => return Ok(()), + }; + let response = match request { + Request::Echo(echo) => Response::Echo(app.echo(echo)), + }; + stream.send(response).await?; + } +} + +// The server decodes requests and encodes responses. +struct ServerCodec { + decoder: TspDecoder, +} + +impl ServerCodec { + fn new() -> Self { + Self { + decoder: TspDecoder::new(), + } + } +} + +impl Encoder for ServerCodec { + type Error = Error; + + fn encode(&mut self, item: Response, mut dst: &mut BytesMut) -> Result<()> { + TspEncoder::encode_response(item, &mut dst) + } +} + +impl Decoder for ServerCodec { + type Item = Request; + type Error = Error; + + fn decode(&mut self, mut src: &mut BytesMut) -> Result> { + self.decoder.decode_request(&mut src) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::application::test::EchoApp; + use tendermint::abci::request::Echo; + + struct ClientCodec { + decoder: TspDecoder, + } + + impl ClientCodec { + fn new() -> Self { + Self { + decoder: TspDecoder::new(), + } + } + } + + impl Encoder for ClientCodec { + type Error = Error; + + fn encode(&mut self, item: Request, mut dst: &mut BytesMut) -> Result<()> { + TspEncoder::encode_request(item, &mut dst) + } + } + + impl Decoder for ClientCodec { + type Item = Response; + type Error = Error; + + fn decode(&mut self, mut src: &mut BytesMut) -> Result> { + self.decoder.decode_response(&mut src) + } + } + + #[tokio::test] + async fn echo() { + let app = EchoApp::default(); + let (term_tx, term_rx) = mpsc::channel(1); + let (ready_tx, mut ready_rx) = mpsc::channel(1); + let server_handle = + tokio::spawn(async move { serve("127.0.0.1:0", app, term_rx, ready_tx).await }); + + // Wait for the server to become available + let server_addr = ready_rx.recv().await.unwrap(); + + let mut client = Framed::new( + TcpStream::connect(server_addr).await.unwrap(), + ClientCodec::new(), + ); + client + .send(Request::Echo(Echo::new("Hello ABCI :-)"))) + .await + .unwrap(); + match client.next().await { + Some(result) => { + let response = result.unwrap(); + match response { + Response::Echo(res) => { + assert_eq!(res.message, "Hello ABCI :-)"); + } + } + } + None => panic!("No response from server"), + } + + term_tx.send(()).await.unwrap(); + server_handle.await.unwrap().unwrap(); + } +} From 362f201eb4fbc7fb9561d8fb3ccb568d08bcfcce Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 16 Dec 2020 14:33:53 -0500 Subject: [PATCH 05/29] Update p2p prost dependency to be compatible with tendermint-proto Signed-off-by: Thane Thomson --- p2p/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/Cargo.toml b/p2p/Cargo.toml index 91843018d..c495cce49 100644 --- a/p2p/Cargo.toml +++ b/p2p/Cargo.toml @@ -21,7 +21,7 @@ ed25519-dalek = "1" eyre = "0.6" hkdf = "0.10.0" merlin = "2" -prost = "0.6" +prost = { git = "https://github.com/danburkert/prost", rev = "423f5ec5bd165a7007a388edfb2b485d5bbf40c7" } rand_core = { version = "0.5", features = ["std"] } sha2 = "0.9" subtle = "2" From dd9768d6bf13594bbbb7e8a82e9811e075c9f045 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 23 Dec 2020 10:47:11 -0500 Subject: [PATCH 06/29] Bump to v0.17.0 Signed-off-by: Thane Thomson --- abci/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/abci/Cargo.toml b/abci/Cargo.toml index 6479d70b2..087f1a029 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tendermint-abci" -version = "0.17.0-rc3" +version = "0.17.0" authors = ["Thane Thomson "] edition = "2018" @@ -16,8 +16,8 @@ bytes = "0.6" eyre = "0.6" futures = "0.3" log = "0.4" -tendermint = { version = "0.17.0-rc3", path = "../tendermint" } -tendermint-proto = { version = "0.17.0-rc3", path = "../proto" } +tendermint = { version = "0.17.0", path = "../tendermint" } +tendermint-proto = { version = "0.17.0", path = "../proto" } thiserror = "1.0" tokio = { version = "0.3", features = [ "macros", "net", "rt", "sync" ], optional = true } From 21ca9aed9d7c8c9944b9c2d1ad63f519f81ac6ce Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 11 Jan 2021 11:18:39 -0500 Subject: [PATCH 07/29] Bump Tendermint crate dependency versions to v0.17.1 Signed-off-by: Thane Thomson --- abci/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/abci/Cargo.toml b/abci/Cargo.toml index 087f1a029..95043a6fd 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tendermint-abci" -version = "0.17.0" +version = "0.17.1" authors = ["Thane Thomson "] edition = "2018" @@ -16,8 +16,8 @@ bytes = "0.6" eyre = "0.6" futures = "0.3" log = "0.4" -tendermint = { version = "0.17.0", path = "../tendermint" } -tendermint-proto = { version = "0.17.0", path = "../proto" } +tendermint = { version = "0.17.1", path = "../tendermint" } +tendermint-proto = { version = "0.17.1", path = "../proto" } thiserror = "1.0" tokio = { version = "0.3", features = [ "macros", "net", "rt", "sync" ], optional = true } From dc6cc1af88f977b9fad79ccc4e622446362f27ef Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 12 Jan 2021 10:36:59 -0500 Subject: [PATCH 08/29] Facilitate conversion of ABCI request/response inner types to the wrapper Signed-off-by: Thane Thomson --- tendermint/src/abci/request.rs | 9 ++++++++- tendermint/src/abci/request/echo.rs | 25 ++++++++++++++++++++++++- tendermint/src/abci/response.rs | 5 ++++- tendermint/src/abci/response/echo.rs | 22 +++++++++++++++++++++- tendermint/src/error.rs | 8 ++++++++ 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/tendermint/src/abci/request.rs b/tendermint/src/abci/request.rs index 38080f2ed..03763dae8 100644 --- a/tendermint/src/abci/request.rs +++ b/tendermint/src/abci/request.rs @@ -3,6 +3,7 @@ mod echo; pub use echo::Echo; +use crate::abci::response::ResponseInner; use crate::{Error, Kind}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::abci::request::Value; @@ -10,7 +11,7 @@ use tendermint_proto::abci::Request as RawRequest; use tendermint_proto::Protobuf; /// ABCI request wrapper. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Request { /// Request that the ABCI server echo a specific message back to the client. Echo(Echo), @@ -53,3 +54,9 @@ impl From for RawRequest { } } } + +/// The inner type of a [`Request`]. +pub trait RequestInner: TryFrom + Into + Send { + /// The corresponding response type for this request. + type Response: ResponseInner; +} diff --git a/tendermint/src/abci/request/echo.rs b/tendermint/src/abci/request/echo.rs index e44b856a7..b390502b7 100644 --- a/tendermint/src/abci/request/echo.rs +++ b/tendermint/src/abci/request/echo.rs @@ -1,12 +1,14 @@ //! ABCI echo request. +use crate::abci::request::{Request, RequestInner}; +use crate::abci::response; use crate::Error; use std::convert::TryFrom; use tendermint_proto::abci::RequestEcho; use tendermint_proto::Protobuf; /// Request that the ABCI server echo a message back the client. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub struct Echo { /// The message to echo back the client. @@ -39,3 +41,24 @@ impl From for RequestEcho { } } } + +impl RequestInner for Echo { + type Response = response::Echo; +} + +impl TryFrom for Echo { + type Error = Error; + + fn try_from(value: Request) -> Result { + match value { + Request::Echo(r) => Ok(r), + // _ => Err(Kind::UnexpectedAbciRequestType("Echo".to_owned(), value).into()), + } + } +} + +impl From for Request { + fn from(req: Echo) -> Self { + Self::Echo(req) + } +} diff --git a/tendermint/src/abci/response.rs b/tendermint/src/abci/response.rs index 77c69cc8c..7df73fc97 100644 --- a/tendermint/src/abci/response.rs +++ b/tendermint/src/abci/response.rs @@ -10,7 +10,7 @@ use tendermint_proto::abci::Response as RawResponse; use tendermint_proto::Protobuf; /// ABCI response wrapper. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Response { /// Echo response. Echo(Echo), @@ -53,3 +53,6 @@ impl From for RawResponse { } } } + +/// The inner type of a [`Response`]. +pub trait ResponseInner: TryFrom + Into + Send {} diff --git a/tendermint/src/abci/response/echo.rs b/tendermint/src/abci/response/echo.rs index a5b04c91c..f6bf358d8 100644 --- a/tendermint/src/abci/response/echo.rs +++ b/tendermint/src/abci/response/echo.rs @@ -1,12 +1,13 @@ //! ABCI echo response. +use crate::abci::response::{Response, ResponseInner}; use crate::Error; use std::convert::TryFrom; use tendermint_proto::abci::ResponseEcho; use tendermint_proto::Protobuf; /// ABCI echo response. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub struct Echo { /// The message to be echoed back to the client. @@ -39,3 +40,22 @@ impl From for ResponseEcho { } } } + +impl ResponseInner for Echo {} + +impl TryFrom for Echo { + type Error = Error; + + fn try_from(value: Response) -> Result { + match value { + Response::Echo(res) => Ok(res), + // _ => Err(Kind::UnexpectedAbciResponseType("Echo".to_string(), value).into()), + } + } +} + +impl From for Response { + fn from(res: Echo) -> Self { + Self::Echo(res) + } +} diff --git a/tendermint/src/error.rs b/tendermint/src/error.rs index 66313c47d..a061c9cb2 100644 --- a/tendermint/src/error.rs +++ b/tendermint/src/error.rs @@ -204,6 +204,14 @@ pub enum Kind { /// ABCI response is missing its inner value property. #[error("malformed ABCI response: response is missing its inner value")] MissingAbciResponseValue, + + /// The ABCI request type doesn't match our expectations. + #[error("expected ABCI request type {0}, but got {1:?}")] + UnexpectedAbciRequestType(String, crate::abci::request::Request), + + /// The ABCI response type doesn't match our expectations. + #[error("expected ABCI response type {0}, but got {1:?}")] + UnexpectedAbciResponseType(String, crate::abci::response::Response), } impl Kind { From 37650c7bd17dafdaa35b208300422bec6a4df1a5 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 12 Jan 2021 10:37:23 -0500 Subject: [PATCH 09/29] Refactor to clean up interface and add client functionality Signed-off-by: Thane Thomson --- abci/Cargo.toml | 3 +- abci/src/application.rs | 21 +---- abci/src/client.rs | 22 +++++ abci/src/client/tokio.rs | 79 ++++++++++++++++ abci/src/codec.rs | 19 +++- abci/src/lib.rs | 6 +- abci/src/protocol.rs | 1 - abci/src/result.rs | 10 ++ abci/src/server.rs | 3 + abci/src/server/async_std.rs | 1 + abci/src/server/tokio.rs | 172 +++++++++++++---------------------- abci/tests/tokio.rs | 40 ++++++++ 12 files changed, 244 insertions(+), 133 deletions(-) create mode 100644 abci/src/client.rs create mode 100644 abci/src/client/tokio.rs delete mode 100644 abci/src/protocol.rs create mode 100644 abci/src/server/async_std.rs create mode 100644 abci/tests/tokio.rs diff --git a/abci/Cargo.toml b/abci/Cargo.toml index 95043a6fd..fb06dde0d 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -7,8 +7,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -server = [] with-tokio = [ "tokio", "tokio-util" ] +with-async-std = [ "async-std" ] [dependencies] async-trait = "0.1" @@ -20,5 +20,6 @@ tendermint = { version = "0.17.1", path = "../tendermint" } tendermint-proto = { version = "0.17.1", path = "../proto" } thiserror = "1.0" +async-std = { version = "1.7", optional = true } tokio = { version = "0.3", features = [ "macros", "net", "rt", "sync" ], optional = true } tokio-util = { version = "0.5", features = [ "codec" ], optional = true } diff --git a/abci/src/application.rs b/abci/src/application.rs index 2429db001..681082608 100644 --- a/abci/src/application.rs +++ b/abci/src/application.rs @@ -6,24 +6,7 @@ use tendermint::abci::{request, response}; #[async_trait] pub trait Application: Send + Clone { /// Request that the ABCI server echo back the same message sent to it. - fn echo(&self, request: request::Echo) -> response::Echo { - response::Echo::new(request.message) + fn echo(&self, req: request::Echo) -> response::Echo { + response::Echo::new(req.message) } } - -#[cfg(test)] -pub mod test { - use super::*; - - /// Simple echo application for use in testing. - #[derive(Clone)] - pub struct EchoApp {} - - impl Default for EchoApp { - fn default() -> Self { - Self {} - } - } - - impl Application for EchoApp {} -} diff --git a/abci/src/client.rs b/abci/src/client.rs new file mode 100644 index 000000000..dca382262 --- /dev/null +++ b/abci/src/client.rs @@ -0,0 +1,22 @@ +//! ABCI clients for interacting with ABCI servers. + +#[cfg(feature = "with-tokio")] +pub mod tokio; + +use crate::Result; +use async_trait::async_trait; +use tendermint::abci::request::RequestInner; +use tendermint::abci::{request, response}; + +/// An asynchronous ABCI client. +#[async_trait] +pub trait Client { + /// Request that the ABCI server echo back the message in the given + /// request. + async fn echo(&mut self, req: request::Echo) -> Result { + self.perform(req).await + } + + /// Generic method to perform the given request. + async fn perform(&mut self, req: R) -> Result; +} diff --git a/abci/src/client/tokio.rs b/abci/src/client/tokio.rs new file mode 100644 index 000000000..3e7802f97 --- /dev/null +++ b/abci/src/client/tokio.rs @@ -0,0 +1,79 @@ +//! Tokio-based ABCI client. + +use crate::client::Client; +use crate::codec::{TspDecoder, TspEncoder}; +use crate::{Error, Result}; +use async_trait::async_trait; +use bytes::BytesMut; +use futures::{SinkExt, StreamExt}; +use std::convert::TryInto; +use tendermint::abci::request::{Request, RequestInner}; +use tendermint::abci::response::Response; +use tokio::net::{TcpStream, ToSocketAddrs}; +use tokio_util::codec::{Decoder, Encoder, Framed}; + +/// Tokio-based ABCI client for interacting with an ABCI server via a TCP +/// socket. +/// +/// Not thread-safe, because it wraps a single outgoing TCP connection and the +/// underlying protocol doesn't support multiplexing. To submit requests in +/// parallel, create multiple TCP connections. +pub struct TokioClient { + stream: Framed, +} + +#[async_trait] +impl Client for TokioClient { + async fn perform(&mut self, req: R) -> Result { + self.stream.send(req.into()).await?; + let res: std::result::Result = self + .stream + .next() + .await + .ok_or(Error::ServerStreamTerminated)?? + .try_into(); + Ok(res?) + } +} + +impl TokioClient { + /// Connect to the given ABCI server address. + pub async fn connect(addr: A) -> Result { + let stream = TcpStream::connect(addr).await?; + Ok(Self { + stream: Framed::new(stream, ClientCodec::default()), + }) + } +} + +/// Codec for the ABCI client. +/// +/// Implements [`Encode`] for [`Request`]s and [`Decode`] for [`Response`]s. +pub struct ClientCodec { + decoder: TspDecoder, +} + +impl Default for ClientCodec { + fn default() -> Self { + Self { + decoder: TspDecoder::new(), + } + } +} + +impl Encoder for ClientCodec { + type Error = Error; + + fn encode(&mut self, item: Request, mut dst: &mut BytesMut) -> Result<()> { + TspEncoder::encode_request(item, &mut dst) + } +} + +impl Decoder for ClientCodec { + type Item = Response; + type Error = Error; + + fn decode(&mut self, mut src: &mut BytesMut) -> Result> { + self.decoder.decode_response(&mut src) + } +} diff --git a/abci/src/codec.rs b/abci/src/codec.rs index 7315ea275..0479d7a80 100644 --- a/abci/src/codec.rs +++ b/abci/src/codec.rs @@ -67,7 +67,7 @@ fn encode_varint(val: u64, mut buf: &mut B) { fn decode_varint(mut buf: &mut B) -> Result { let len = tendermint_proto::decode_varint(&mut buf) - .map_err(|e| Error::Protobuf(tendermint_proto::Kind::DecodeMessage.into()))?; + .map_err(|_| Error::Protobuf(tendermint_proto::Kind::DecodeMessage.into()))?; Ok(len >> 1) } @@ -134,4 +134,21 @@ mod test { assert_eq!(request, decoded_request); } + + #[test] + fn multiple_requests() { + let requests = (0..5) + .map(|r| Request::Echo(Echo::new(format!("Request {}", r)))) + .collect::>(); + let mut buf = BytesMut::new(); + requests + .iter() + .for_each(|request| TspEncoder::encode_request(request.clone(), &mut buf).unwrap()); + + let mut decoder = TspDecoder::new(); + for request in requests { + let decoded = decoder.decode_request(&mut buf).unwrap().unwrap(); + assert_eq!(decoded, request); + } + } } diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 4365a1adc..37ba3811e 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -2,12 +2,10 @@ mod application; mod codec; -mod protocol; mod result; -#[cfg(feature = "server")] -pub use application::Application; -#[cfg(feature = "server")] +pub mod client; pub mod server; +pub use application::Application; pub use result::{Error, Result}; diff --git a/abci/src/protocol.rs b/abci/src/protocol.rs deleted file mode 100644 index 8107efc62..000000000 --- a/abci/src/protocol.rs +++ /dev/null @@ -1 +0,0 @@ -//! ABCI protocol. diff --git a/abci/src/result.rs b/abci/src/result.rs index 2e8ca6079..d4732dd5a 100644 --- a/abci/src/result.rs +++ b/abci/src/result.rs @@ -18,4 +18,14 @@ pub enum Error { #[cfg(feature = "with-tokio")] #[error("channel send error: {0}")] TokioChannelSend(String), + + #[cfg(feature = "with-tokio")] + #[error("failed to obtain UNIX stream path")] + CannotObtainUnixStreamPath, + + #[error("Tendermint error")] + TendermintError(#[from] tendermint::Error), + + #[error("server stream terminated unexpectedly")] + ServerStreamTerminated, } diff --git a/abci/src/server.rs b/abci/src/server.rs index 9b5da586f..bb864f115 100644 --- a/abci/src/server.rs +++ b/abci/src/server.rs @@ -2,3 +2,6 @@ #[cfg(feature = "with-tokio")] pub mod tokio; + +#[cfg(feature = "with-async-std")] +pub mod async_std; diff --git a/abci/src/server/async_std.rs b/abci/src/server/async_std.rs new file mode 100644 index 000000000..aad061b4f --- /dev/null +++ b/abci/src/server/async_std.rs @@ -0,0 +1 @@ +//! `async-std`-based ABCI server. diff --git a/abci/src/server/tokio.rs b/abci/src/server/tokio.rs index 9b51cecf7..dbd6ffe7a 100644 --- a/abci/src/server/tokio.rs +++ b/abci/src/server/tokio.rs @@ -11,46 +11,73 @@ use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; use tokio::sync::mpsc; use tokio_util::codec::{Decoder, Encoder, Framed}; -/// Attempts to bind to the given address and immediately start serving the -/// given ABCI application in the current task. -pub async fn serve( - addr: S, +/// Tokio-based ABCI server for a specific application. +/// +/// Listens for incoming TCP connections. +pub struct TokioServer { app: A, - mut term: mpsc::Receiver<()>, - ready: mpsc::Sender, -) -> Result<()> -where - S: ToSocketAddrs, - A: Application + 'static, -{ - let listener = TcpListener::bind(addr).await?; - let local_addr = listener.local_addr()?; - info!( - "ABCI server bound to {}, listening for incoming connections", - local_addr, - ); - ready - .send(local_addr.to_string()) - .await - .map_err(|e| Error::TokioChannelSend(e.to_string()))?; - loop { - tokio::select! { - result = listener.accept() => { - let (stream, addr) = result?; - info!("Incoming connection from {}", addr); - let conn_app = app.clone(); - tokio::spawn(async move { handle_client(stream, conn_app).await }); + listener: TcpListener, + local_addr: String, + term_rx: mpsc::Receiver<()>, +} + +impl TokioServer { + /// Bind the application server to the given socket address using TCP. + /// + /// On success, returns the server and a channel through which the server + /// can be asynchronously signaled to terminate. + pub async fn bind(addr: S, app: A) -> Result<(Self, mpsc::Sender<()>)> + where + S: ToSocketAddrs, + { + let listener = TcpListener::bind(addr).await?; + let local_addr = listener.local_addr()?; + info!( + "ABCI server bound to {}, listening for incoming connections", + local_addr, + ); + let (term_tx, term_rx) = mpsc::channel(1); + Ok(( + Self { + app, + listener, + local_addr: local_addr.to_string(), + term_rx, }, - Some(_) = term.recv() => { - info!("Server terminated"); - return Ok(()) + term_tx, + )) + } + + /// Getter for the server's local address. + pub fn local_addr(&self) -> String { + self.local_addr.clone() + } + + /// Block the current task while listening for incoming connections. + /// + /// Each incoming connection is spawned in a separate task. + pub async fn listen(mut self) -> Result<()> { + loop { + tokio::select! { + result = self.listener.accept() => { + let (stream, addr) = result?; + info!("Incoming connection from {}", addr); + let conn_app = self.app.clone(); + tokio::spawn(async move { handle_client(stream, conn_app).await }); + }, + Some(_) = self.term_rx.recv() => { + // TODO(thane): Terminate client tasks + info!("Server terminated"); + return Ok(()) + } } } } } +// Each incoming request is processed sequentially in a single connection. async fn handle_client(stream: TcpStream, app: A) -> Result<()> { - let codec = ServerCodec::new(); + let codec = ServerCodec::default(); let mut stream = Framed::new(stream, codec); loop { let request = match stream.next().await { @@ -64,13 +91,15 @@ async fn handle_client(stream: TcpStream, app: A) -> Result<()> } } -// The server decodes requests and encodes responses. -struct ServerCodec { +/// Codec for the ABCI server. +/// +/// Implements [`Decode`] for [`Request`]s and [`Encode`] for [`Response`]s. +pub struct ServerCodec { decoder: TspDecoder, } -impl ServerCodec { - fn new() -> Self { +impl Default for ServerCodec { + fn default() -> Self { Self { decoder: TspDecoder::new(), } @@ -93,74 +122,3 @@ impl Decoder for ServerCodec { self.decoder.decode_request(&mut src) } } - -#[cfg(test)] -mod test { - use super::*; - use crate::application::test::EchoApp; - use tendermint::abci::request::Echo; - - struct ClientCodec { - decoder: TspDecoder, - } - - impl ClientCodec { - fn new() -> Self { - Self { - decoder: TspDecoder::new(), - } - } - } - - impl Encoder for ClientCodec { - type Error = Error; - - fn encode(&mut self, item: Request, mut dst: &mut BytesMut) -> Result<()> { - TspEncoder::encode_request(item, &mut dst) - } - } - - impl Decoder for ClientCodec { - type Item = Response; - type Error = Error; - - fn decode(&mut self, mut src: &mut BytesMut) -> Result> { - self.decoder.decode_response(&mut src) - } - } - - #[tokio::test] - async fn echo() { - let app = EchoApp::default(); - let (term_tx, term_rx) = mpsc::channel(1); - let (ready_tx, mut ready_rx) = mpsc::channel(1); - let server_handle = - tokio::spawn(async move { serve("127.0.0.1:0", app, term_rx, ready_tx).await }); - - // Wait for the server to become available - let server_addr = ready_rx.recv().await.unwrap(); - - let mut client = Framed::new( - TcpStream::connect(server_addr).await.unwrap(), - ClientCodec::new(), - ); - client - .send(Request::Echo(Echo::new("Hello ABCI :-)"))) - .await - .unwrap(); - match client.next().await { - Some(result) => { - let response = result.unwrap(); - match response { - Response::Echo(res) => { - assert_eq!(res.message, "Hello ABCI :-)"); - } - } - } - None => panic!("No response from server"), - } - - term_tx.send(()).await.unwrap(); - server_handle.await.unwrap().unwrap(); - } -} diff --git a/abci/tests/tokio.rs b/abci/tests/tokio.rs new file mode 100644 index 000000000..ddbd61f7a --- /dev/null +++ b/abci/tests/tokio.rs @@ -0,0 +1,40 @@ +//! Tokio-based ABCI client/server interaction. + +#[cfg(feature = "with-tokio")] +mod integration { + use tendermint::abci::request::Echo; + use tendermint_abci::client::tokio::TokioClient; + use tendermint_abci::client::Client; + use tendermint_abci::server::tokio::TokioServer; + use tendermint_abci::Application; + + /// Simple echo application for use in testing. + #[derive(Clone)] + pub struct EchoApp {} + + impl Default for EchoApp { + fn default() -> Self { + Self {} + } + } + + impl Application for EchoApp {} + + #[tokio::test] + async fn echo() { + let app = EchoApp::default(); + let (server, term_tx) = TokioServer::bind("127.0.0.1:0", app).await.unwrap(); + let server_addr = server.local_addr(); + let server_handle = tokio::spawn(async move { server.listen().await }); + + let mut client = TokioClient::connect(server_addr).await.unwrap(); + let res = client + .echo(Echo::new("Hello ABCI!".to_owned())) + .await + .unwrap(); + assert_eq!(res.message, "Hello ABCI!"); + + term_tx.send(()).await.unwrap(); + server_handle.await.unwrap().unwrap(); + } +} From 1630370cf0c5c9778d5e730d52c9df058c9dcd1c Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 12 Jan 2021 11:57:42 -0500 Subject: [PATCH 10/29] Allow developer to disable client code Signed-off-by: Thane Thomson --- abci/Cargo.toml | 4 ++++ abci/src/codec.rs | 3 +++ abci/src/lib.rs | 16 ++++++++++++++-- abci/tests/tokio.rs | 9 +++------ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/abci/Cargo.toml b/abci/Cargo.toml index fb06dde0d..64ff7f1b8 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -7,6 +7,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] +# By default we assume users of the library will be implementing an ABCI +# application using Tokio. +default = [ "with-tokio" ] +client = [] with-tokio = [ "tokio", "tokio-util" ] with-async-std = [ "async-std" ] diff --git a/abci/src/codec.rs b/abci/src/codec.rs index 0479d7a80..c7be963d0 100644 --- a/abci/src/codec.rs +++ b/abci/src/codec.rs @@ -16,6 +16,7 @@ pub struct TspEncoder {} impl TspEncoder { /// Encode the given request to its raw wire-level representation and store /// this in the given buffer. + #[cfg(feature = "client")] pub fn encode_request(request: Request, mut dst: &mut BytesMut) -> Result<()> { encode_length_delimited(|mut b| Ok(request.encode(&mut b)?), &mut dst) } @@ -53,6 +54,7 @@ impl TspDecoder { /// /// Returns `Ok(None)` if we don't yet have enough data to decode a full /// response. + #[cfg(feature = "client")] pub fn decode_response(&mut self, buf: &mut BytesMut) -> Result> { self.read_buf.put(buf); decode_length_delimited(&mut self.read_buf, |mut b| Ok(Response::decode(&mut b)?)) @@ -118,6 +120,7 @@ where } } +#[cfg(feature = "client")] #[cfg(test)] mod test { use super::*; diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 37ba3811e..4847552af 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -3,9 +3,21 @@ mod application; mod codec; mod result; +mod server; -pub mod client; -pub mod server; +// Client exports +#[cfg(feature = "client")] +mod client; +#[cfg(feature = "client")] +pub use client::Client; +#[cfg(all(feature = "client", feature = "with-tokio"))] +pub use client::tokio::TokioClient; + +// Server exports +#[cfg(feature = "with-tokio")] +pub use server::tokio::TokioServer; + +// Common exports pub use application::Application; pub use result::{Error, Result}; diff --git a/abci/tests/tokio.rs b/abci/tests/tokio.rs index ddbd61f7a..3251f75e5 100644 --- a/abci/tests/tokio.rs +++ b/abci/tests/tokio.rs @@ -1,12 +1,9 @@ //! Tokio-based ABCI client/server interaction. -#[cfg(feature = "with-tokio")] -mod integration { +#[cfg(all(feature = "with-tokio", feature = "client"))] +mod tokio_integration { use tendermint::abci::request::Echo; - use tendermint_abci::client::tokio::TokioClient; - use tendermint_abci::client::Client; - use tendermint_abci::server::tokio::TokioServer; - use tendermint_abci::Application; + use tendermint_abci::{Application, Client, TokioClient, TokioServer}; /// Simple echo application for use in testing. #[derive(Clone)] From 28d0f713590e851a5fe476202484e3b14e011ce2 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 12 Jan 2021 14:42:29 -0500 Subject: [PATCH 11/29] Add async-std client/server Signed-off-by: Thane Thomson --- abci/Cargo.toml | 9 ++-- abci/src/application.rs | 3 ++ abci/src/application/echo.rs | 22 ++++++++ abci/src/client.rs | 2 + abci/src/client/async_std.rs | 58 ++++++++++++++++++++ abci/src/client/tokio.rs | 2 +- abci/src/lib.rs | 9 ++++ abci/src/result.rs | 3 +- abci/src/server/async_std.rs | 101 +++++++++++++++++++++++++++++++++++ abci/tests/async_std.rs | 25 +++++++++ abci/tests/tokio.rs | 18 ++----- 11 files changed, 229 insertions(+), 23 deletions(-) create mode 100644 abci/src/application/echo.rs create mode 100644 abci/src/client/async_std.rs create mode 100644 abci/tests/async_std.rs diff --git a/abci/Cargo.toml b/abci/Cargo.toml index 64ff7f1b8..d8e4fa973 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -7,12 +7,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -# By default we assume users of the library will be implementing an ABCI -# application using Tokio. -default = [ "with-tokio" ] client = [] +echo-app = [] with-tokio = [ "tokio", "tokio-util" ] -with-async-std = [ "async-std" ] +with-async-std = [ "async-channel", "async-std" ] [dependencies] async-trait = "0.1" @@ -24,6 +22,7 @@ tendermint = { version = "0.17.1", path = "../tendermint" } tendermint-proto = { version = "0.17.1", path = "../proto" } thiserror = "1.0" -async-std = { version = "1.7", optional = true } +async-channel = { version = "1.5", optional = true } +async-std = { version = "1.8", features = [ "attributes" ], optional = true } tokio = { version = "0.3", features = [ "macros", "net", "rt", "sync" ], optional = true } tokio-util = { version = "0.5", features = [ "codec" ], optional = true } diff --git a/abci/src/application.rs b/abci/src/application.rs index 681082608..861f070b4 100644 --- a/abci/src/application.rs +++ b/abci/src/application.rs @@ -1,5 +1,8 @@ //! `async` ABCI server application interface. +#[cfg(feature = "echo-app")] +pub mod echo; + use async_trait::async_trait; use tendermint::abci::{request, response}; diff --git a/abci/src/application/echo.rs b/abci/src/application/echo.rs new file mode 100644 index 000000000..7a6957431 --- /dev/null +++ b/abci/src/application/echo.rs @@ -0,0 +1,22 @@ +//! Trivial ABCI application that just implements echo functionality. + +use crate::Application; + +/// Trivial ABCI application that just implements echo functionality. +#[derive(Clone)] +pub struct EchoApp {} + +impl EchoApp { + /// Constructor. + pub fn new() -> Self { + Self {} + } +} + +impl Default for EchoApp { + fn default() -> Self { + EchoApp::new() + } +} + +impl Application for EchoApp {} diff --git a/abci/src/client.rs b/abci/src/client.rs index dca382262..aff01cd48 100644 --- a/abci/src/client.rs +++ b/abci/src/client.rs @@ -1,5 +1,7 @@ //! ABCI clients for interacting with ABCI servers. +#[cfg(feature = "with-async-std")] +pub mod async_std; #[cfg(feature = "with-tokio")] pub mod tokio; diff --git a/abci/src/client/async_std.rs b/abci/src/client/async_std.rs new file mode 100644 index 000000000..a91bfde3e --- /dev/null +++ b/abci/src/client/async_std.rs @@ -0,0 +1,58 @@ +//! `async-std`-based ABCI client. + +use crate::codec::{TspDecoder, TspEncoder}; +use crate::{Client, Result}; +use async_std::net::{TcpStream, ToSocketAddrs}; +use async_trait::async_trait; +use bytes::BytesMut; +use futures::{AsyncReadExt, AsyncWriteExt}; +use std::convert::TryInto; +use tendermint::abci::request::RequestInner; + +const CLIENT_READ_BUF_SIZE: usize = 4096; + +/// `async-std`-based ABCI client for interacting with an ABCI server via a TCP +/// socket. +/// +/// Not thread-safe, because it wraps a single outgoing TCP connection and the +/// underlying protocol doesn't support multiplexing. To submit requests in +/// parallel, create multiple client instances. +pub struct AsyncStdClient { + stream: TcpStream, + read_buf: BytesMut, + write_buf: BytesMut, + decoder: TspDecoder, +} + +#[async_trait] +impl Client for AsyncStdClient { + async fn perform(&mut self, req: R) -> Result { + TspEncoder::encode_request(req.into(), &mut self.write_buf)?; + self.stream.write(self.write_buf.as_ref()).await?; + self.write_buf.clear(); + + let mut read_buf = [0u8; CLIENT_READ_BUF_SIZE]; + loop { + let bytes_read = self.stream.read(&mut read_buf).await?; + self.read_buf.extend_from_slice(&read_buf[..bytes_read]); + // Try to read a full response + if let Some(response) = self.decoder.decode_response(&mut self.read_buf)? { + return Ok(response.try_into()?); + } + // Otherwise continue reading into our read buffer + } + } +} + +impl AsyncStdClient { + /// Connect to the given ABCI server address. + pub async fn connect(addr: A) -> Result { + let stream = TcpStream::connect(addr).await?; + Ok(Self { + stream, + read_buf: BytesMut::new(), + write_buf: BytesMut::new(), + decoder: TspDecoder::new(), + }) + } +} diff --git a/abci/src/client/tokio.rs b/abci/src/client/tokio.rs index 3e7802f97..7e80ce82a 100644 --- a/abci/src/client/tokio.rs +++ b/abci/src/client/tokio.rs @@ -17,7 +17,7 @@ use tokio_util::codec::{Decoder, Encoder, Framed}; /// /// Not thread-safe, because it wraps a single outgoing TCP connection and the /// underlying protocol doesn't support multiplexing. To submit requests in -/// parallel, create multiple TCP connections. +/// parallel, create multiple client instances. pub struct TokioClient { stream: Framed, } diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 4847552af..6b692c728 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -1,6 +1,7 @@ //! ABCI framework for building applications with Tendermint. mod application; +#[cfg(any(feature = "client", feature = "with-tokio", feature = "with-async-std"))] mod codec; mod result; mod server; @@ -11,13 +12,21 @@ mod client; #[cfg(feature = "client")] pub use client::Client; +#[cfg(all(feature = "client", feature = "with-async-std"))] +pub use client::async_std::AsyncStdClient; #[cfg(all(feature = "client", feature = "with-tokio"))] pub use client::tokio::TokioClient; // Server exports +#[cfg(feature = "with-async-std")] +pub use server::async_std::AsyncStdServer; #[cfg(feature = "with-tokio")] pub use server::tokio::TokioServer; +// Example applications +#[cfg(feature = "echo-app")] +pub use application::echo::EchoApp; + // Common exports pub use application::Application; pub use result::{Error, Result}; diff --git a/abci/src/result.rs b/abci/src/result.rs index d4732dd5a..596274bf9 100644 --- a/abci/src/result.rs +++ b/abci/src/result.rs @@ -11,9 +11,8 @@ pub enum Error { #[error("protocol buffers error")] Protobuf(#[from] tendermint_proto::Error), - #[cfg(feature = "with-tokio")] #[error("network I/O error")] - TokioIo(#[from] tokio::io::Error), + NetworkIo(#[from] std::io::Error), #[cfg(feature = "with-tokio")] #[error("channel send error: {0}")] diff --git a/abci/src/server/async_std.rs b/abci/src/server/async_std.rs index aad061b4f..f7645a8ef 100644 --- a/abci/src/server/async_std.rs +++ b/abci/src/server/async_std.rs @@ -1 +1,102 @@ //! `async-std`-based ABCI server. + +use crate::codec::{TspDecoder, TspEncoder}; +use crate::{Application, Result}; +use async_channel::{bounded, Receiver, Sender}; +use async_std::net::{TcpListener, TcpStream, ToSocketAddrs}; +use bytes::BytesMut; +use futures::{select, AsyncReadExt, AsyncWriteExt, FutureExt}; +use log::info; +use tendermint::abci::request::Request; +use tendermint::abci::response::Response; + +const SERVER_READ_BUF_SIZE: usize = 4096; + +/// `async-std`-based ABCI server for a specific ABCI application. +/// +/// Listens for incoming TCP connections. +pub struct AsyncStdServer { + app: A, + listener: TcpListener, + local_addr: String, + term_rx: Receiver<()>, +} + +impl AsyncStdServer { + /// Bind the application server to the given socket address using TCP. + /// + /// On success, returns the server and a channel through which the server + /// can be asynchronously signaled to terminate. + pub async fn bind(addr: S, app: A) -> Result<(Self, Sender<()>)> + where + S: ToSocketAddrs, + { + let listener = TcpListener::bind(addr).await?; + let local_addr = listener.local_addr()?; + info!( + "ABCI server bound to {}, listening for incoming connections", + local_addr, + ); + let (term_tx, term_rx) = bounded(1); + Ok(( + Self { + app, + listener, + local_addr: local_addr.to_string(), + term_rx, + }, + term_tx, + )) + } + + /// Getter for the server's local address. + pub fn local_addr(&self) -> String { + self.local_addr.clone() + } + + /// Block the current task while listening for incoming connections. + /// + /// Each incoming connection is spawned in a separate task. + pub async fn listen(self) -> Result<()> { + loop { + select! { + result = self.listener.accept().fuse() => { + let (stream, addr) = result?; + info!("Incoming connection from {}", addr); + let conn_app = self.app.clone(); + async_std::task::spawn(async move { handle_client(stream, conn_app).await }); + }, + _ = self.term_rx.recv().fuse() => { + // TODO(thane): Terminate client tasks + info!("Server terminated"); + return Ok(()) + } + } + } + } +} + +// Each incoming request is processed sequentially in a single connection. +async fn handle_client(mut stream: TcpStream, app: A) -> Result<()> { + let mut decoder = TspDecoder::new(); + let mut stream_buf = BytesMut::new(); + let mut read_buf = [0u8; SERVER_READ_BUF_SIZE]; + let mut write_buf = BytesMut::new(); + loop { + let bytes_read = stream.read(&mut read_buf).await?; + stream_buf.extend_from_slice(&read_buf[..bytes_read]); + // Try to process as many requests as we can from the stream buffer + 'request_loop: loop { + let request = match decoder.decode_request(&mut stream_buf)? { + Some(req) => req, + None => break 'request_loop, + }; + let response = match request { + Request::Echo(echo) => Response::Echo(app.echo(echo)), + }; + TspEncoder::encode_response(response, &mut write_buf)?; + stream.write(&write_buf).await?; + write_buf.clear(); + } + } +} diff --git a/abci/tests/async_std.rs b/abci/tests/async_std.rs new file mode 100644 index 000000000..35c8f7e1a --- /dev/null +++ b/abci/tests/async_std.rs @@ -0,0 +1,25 @@ +/// `async-std`-based ABCI client/server interaction. + +#[cfg(all(feature = "with-async-std", feature = "client", feature = "echo-app"))] +mod async_std_integration { + use tendermint::abci::request::Echo; + use tendermint_abci::{AsyncStdClient, AsyncStdServer, Client, EchoApp}; + + #[async_std::test] + async fn echo() { + let app = EchoApp::new(); + let (server, term_tx) = AsyncStdServer::bind("127.0.0.1:0", app).await.unwrap(); + let server_addr = server.local_addr(); + let server_handle = async_std::task::spawn(async move { server.listen().await }); + + let mut client = AsyncStdClient::connect(server_addr).await.unwrap(); + let res = client + .echo(Echo::new("Hello ABCI!".to_owned())) + .await + .unwrap(); + assert_eq!(res.message, "Hello ABCI!"); + + term_tx.send(()).await.unwrap(); + server_handle.await.unwrap(); + } +} diff --git a/abci/tests/tokio.rs b/abci/tests/tokio.rs index 3251f75e5..935b00680 100644 --- a/abci/tests/tokio.rs +++ b/abci/tests/tokio.rs @@ -1,25 +1,13 @@ //! Tokio-based ABCI client/server interaction. -#[cfg(all(feature = "with-tokio", feature = "client"))] +#[cfg(all(feature = "with-tokio", feature = "client", feature = "echo-app"))] mod tokio_integration { use tendermint::abci::request::Echo; - use tendermint_abci::{Application, Client, TokioClient, TokioServer}; - - /// Simple echo application for use in testing. - #[derive(Clone)] - pub struct EchoApp {} - - impl Default for EchoApp { - fn default() -> Self { - Self {} - } - } - - impl Application for EchoApp {} + use tendermint_abci::{Client, EchoApp, TokioClient, TokioServer}; #[tokio::test] async fn echo() { - let app = EchoApp::default(); + let app = EchoApp::new(); let (server, term_tx) = TokioServer::bind("127.0.0.1:0", app).await.unwrap(); let server_addr = server.local_addr(); let server_handle = tokio::spawn(async move { server.listen().await }); From 375317875e4c4a6fcc47a8bb20eb61547d43f224 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 12 Jan 2021 14:46:06 -0500 Subject: [PATCH 12/29] Test multiple echo requests/responses with client/server Signed-off-by: Thane Thomson --- abci/tests/async_std.rs | 12 +++++++----- abci/tests/tokio.rs | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/abci/tests/async_std.rs b/abci/tests/async_std.rs index 35c8f7e1a..7d98be8ef 100644 --- a/abci/tests/async_std.rs +++ b/abci/tests/async_std.rs @@ -13,11 +13,13 @@ mod async_std_integration { let server_handle = async_std::task::spawn(async move { server.listen().await }); let mut client = AsyncStdClient::connect(server_addr).await.unwrap(); - let res = client - .echo(Echo::new("Hello ABCI!".to_owned())) - .await - .unwrap(); - assert_eq!(res.message, "Hello ABCI!"); + let requests = (0..5) + .map(|r| Echo::new(format!("Request {}", r))) + .collect::>(); + for request in &requests { + let res = client.echo(request.clone()).await.unwrap(); + assert_eq!(res.message, request.message); + } term_tx.send(()).await.unwrap(); server_handle.await.unwrap(); diff --git a/abci/tests/tokio.rs b/abci/tests/tokio.rs index 935b00680..2b00fc5df 100644 --- a/abci/tests/tokio.rs +++ b/abci/tests/tokio.rs @@ -13,11 +13,13 @@ mod tokio_integration { let server_handle = tokio::spawn(async move { server.listen().await }); let mut client = TokioClient::connect(server_addr).await.unwrap(); - let res = client - .echo(Echo::new("Hello ABCI!".to_owned())) - .await - .unwrap(); - assert_eq!(res.message, "Hello ABCI!"); + let requests = (0..5) + .map(|r| Echo::new(format!("Request {}", r))) + .collect::>(); + for request in &requests { + let res = client.echo(request.clone()).await.unwrap(); + assert_eq!(res.message, request.message); + } term_tx.send(()).await.unwrap(); server_handle.await.unwrap().unwrap(); From 5a44bd1db1d7f9e1450b2d7fe08e62d893dbb623 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 12 Jan 2021 19:14:17 -0500 Subject: [PATCH 13/29] Add ABCI info request/response Signed-off-by: Thane Thomson --- tendermint/src/abci/request.rs | 8 ++- tendermint/src/abci/request/echo.rs | 4 +- tendermint/src/abci/request/info.rs | 92 ++++++++++++++++++++++++ tendermint/src/abci/response.rs | 8 ++- tendermint/src/abci/response/echo.rs | 4 +- tendermint/src/abci/response/info.rs | 100 +++++++++++++++++++++++++++ 6 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 tendermint/src/abci/request/info.rs create mode 100644 tendermint/src/abci/response/info.rs diff --git a/tendermint/src/abci/request.rs b/tendermint/src/abci/request.rs index 03763dae8..e557eb9f7 100644 --- a/tendermint/src/abci/request.rs +++ b/tendermint/src/abci/request.rs @@ -1,7 +1,10 @@ //! ABCI requests. mod echo; +mod info; + pub use echo::Echo; +pub use info::Info; use crate::abci::response::ResponseInner; use crate::{Error, Kind}; @@ -15,6 +18,8 @@ use tendermint_proto::Protobuf; pub enum Request { /// Request that the ABCI server echo a specific message back to the client. Echo(Echo), + /// Return application info. + Info(Info), } impl Protobuf for Request {} @@ -26,9 +31,9 @@ impl TryFrom for Request { let value = raw.value.ok_or(Kind::MissingAbciRequestValue)?; Ok(match value { Value::Echo(raw_req) => Self::Echo(raw_req.try_into()?), + Value::Info(raw_req) => Self::Info(raw_req.try_into()?), _ => unimplemented!(), // Value::Flush(_) => {} - // Value::Info(_) => {} // Value::SetOption(_) => {} // Value::InitChain(_) => {} // Value::Query(_) => {} @@ -50,6 +55,7 @@ impl From for RawRequest { Self { value: Some(match request { Request::Echo(req) => Value::Echo(req.into()), + Request::Info(req) => Value::Info(req.into()), }), } } diff --git a/tendermint/src/abci/request/echo.rs b/tendermint/src/abci/request/echo.rs index b390502b7..58dbc66bd 100644 --- a/tendermint/src/abci/request/echo.rs +++ b/tendermint/src/abci/request/echo.rs @@ -2,7 +2,7 @@ use crate::abci::request::{Request, RequestInner}; use crate::abci::response; -use crate::Error; +use crate::{Error, Kind}; use std::convert::TryFrom; use tendermint_proto::abci::RequestEcho; use tendermint_proto::Protobuf; @@ -52,7 +52,7 @@ impl TryFrom for Echo { fn try_from(value: Request) -> Result { match value { Request::Echo(r) => Ok(r), - // _ => Err(Kind::UnexpectedAbciRequestType("Echo".to_owned(), value).into()), + _ => Err(Kind::UnexpectedAbciRequestType("Echo".to_owned(), value).into()), } } } diff --git a/tendermint/src/abci/request/info.rs b/tendermint/src/abci/request/info.rs new file mode 100644 index 000000000..34ec2f52b --- /dev/null +++ b/tendermint/src/abci/request/info.rs @@ -0,0 +1,92 @@ +//! ABCI info request. + +use crate::abci::request::{Request, RequestInner}; +use crate::abci::response; +use crate::{Error, Kind}; +use std::convert::TryFrom; +use tendermint_proto::abci::RequestInfo; +use tendermint_proto::Protobuf; + +/// Allows a Tendermint node to provide information about itself to the ABCI +/// server, in exchange for information about the server. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct Info { + /// Tendermint software semantic version. + pub version: String, + /// Tendermint block protocol version. + pub block_version: u64, + /// Tendermint P2P protocol version. + pub p2p_version: u64, + /// Tendermint ABCI version. + pub abci_version: u64, +} + +impl Info { + /// Constructor. + pub fn new>( + version: S, + block_version: u64, + p2p_version: u64, + abci_version: u64, + ) -> Self { + Self { + version: version.as_ref().to_owned(), + block_version, + p2p_version, + abci_version, + } + } +} + +impl Default for Info { + fn default() -> Self { + Self::new("", 0, 0, 0) + } +} + +impl Protobuf for Info {} + +impl TryFrom for Info { + type Error = Error; + + fn try_from(value: RequestInfo) -> Result { + Ok(Self::new( + value.version, + value.block_version, + value.p2p_version, + 0, + )) + } +} + +impl From for RequestInfo { + fn from(value: Info) -> Self { + Self { + version: value.version, + block_version: value.block_version, + p2p_version: value.p2p_version, + } + } +} + +impl RequestInner for Info { + type Response = response::Info; +} + +impl TryFrom for Info { + type Error = Error; + + fn try_from(value: Request) -> Result { + match value { + Request::Info(r) => Ok(r), + _ => Err(Kind::UnexpectedAbciRequestType("Info".to_owned(), value).into()), + } + } +} + +impl From for Request { + fn from(value: Info) -> Self { + Self::Info(value) + } +} diff --git a/tendermint/src/abci/response.rs b/tendermint/src/abci/response.rs index 7df73fc97..994a38b84 100644 --- a/tendermint/src/abci/response.rs +++ b/tendermint/src/abci/response.rs @@ -1,7 +1,10 @@ //! ABCI responses. mod echo; +mod info; + pub use echo::Echo; +pub use info::Info; use crate::{Error, Kind}; use std::convert::{TryFrom, TryInto}; @@ -14,6 +17,8 @@ use tendermint_proto::Protobuf; pub enum Response { /// Echo response. Echo(Echo), + /// Application info. + Info(Info), } impl Protobuf for Response {} @@ -25,9 +30,9 @@ impl TryFrom for Response { let value = raw.value.ok_or(Kind::MissingAbciResponseValue)?; Ok(match value { Value::Echo(raw_res) => Self::Echo(raw_res.try_into()?), + Value::Info(raw_res) => Self::Info(raw_res.try_into()?), _ => unimplemented!(), // Value::Flush(_) => {} - // Value::Info(_) => {} // Value::SetOption(_) => {} // Value::InitChain(_) => {} // Value::Query(_) => {} @@ -49,6 +54,7 @@ impl From for RawResponse { Self { value: Some(match request { Response::Echo(res) => Value::Echo(res.into()), + Response::Info(res) => Value::Info(res.into()), }), } } diff --git a/tendermint/src/abci/response/echo.rs b/tendermint/src/abci/response/echo.rs index f6bf358d8..9074b274e 100644 --- a/tendermint/src/abci/response/echo.rs +++ b/tendermint/src/abci/response/echo.rs @@ -1,7 +1,7 @@ //! ABCI echo response. use crate::abci::response::{Response, ResponseInner}; -use crate::Error; +use crate::{Error, Kind}; use std::convert::TryFrom; use tendermint_proto::abci::ResponseEcho; use tendermint_proto::Protobuf; @@ -49,7 +49,7 @@ impl TryFrom for Echo { fn try_from(value: Response) -> Result { match value { Response::Echo(res) => Ok(res), - // _ => Err(Kind::UnexpectedAbciResponseType("Echo".to_string(), value).into()), + _ => Err(Kind::UnexpectedAbciResponseType("Echo".to_owned(), value).into()), } } } diff --git a/tendermint/src/abci/response/info.rs b/tendermint/src/abci/response/info.rs new file mode 100644 index 000000000..598818ae0 --- /dev/null +++ b/tendermint/src/abci/response/info.rs @@ -0,0 +1,100 @@ +//! ABCI info response. + +use crate::abci::response::{Response, ResponseInner}; +use crate::{Error, Kind}; +use std::convert::TryFrom; +use tendermint_proto::abci::ResponseInfo; +use tendermint_proto::Protobuf; + +/// Allows the ABCI app to provide information about itself back to the +/// Tendermint node. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct Info { + /// Arbitrary (application-specific) information. + pub data: String, + /// Application software semantic version. + pub version: String, + /// Application protocol version. + pub app_version: u64, + /// Latest block for which the application has called Commit. + pub last_block_height: i64, + /// Latest result of Commit. + pub last_block_app_hash: Vec, +} + +impl Info { + /// Constructor. + pub fn new( + data: S, + version: S, + app_version: u64, + last_block_height: i64, + last_block_app_hash: V, + ) -> Self + where + S: AsRef, + V: AsRef<[u8]>, + { + Self { + data: data.as_ref().to_owned(), + version: version.as_ref().to_owned(), + app_version, + last_block_height, + last_block_app_hash: last_block_app_hash.as_ref().to_vec(), + } + } +} + +impl Default for Info { + fn default() -> Self { + Self::new("", "", 0, 0, []) + } +} + +impl Protobuf for Info {} + +impl TryFrom for Info { + type Error = Error; + + fn try_from(value: ResponseInfo) -> Result { + Ok(Self::new( + value.data, + value.version, + value.app_version, + value.last_block_height, + value.last_block_app_hash, + )) + } +} + +impl From for ResponseInfo { + fn from(value: Info) -> Self { + Self { + data: value.data, + version: value.version, + app_version: value.app_version, + last_block_height: value.last_block_height, + last_block_app_hash: value.last_block_app_hash, + } + } +} + +impl ResponseInner for Info {} + +impl TryFrom for Info { + type Error = Error; + + fn try_from(value: Response) -> Result { + match value { + Response::Info(res) => Ok(res), + _ => Err(Kind::UnexpectedAbciResponseType("Info".to_owned(), value).into()), + } + } +} + +impl From for Response { + fn from(value: Info) -> Self { + Self::Info(value) + } +} From febdf3e039868640c2c38f2ad5a29e8bf843bd52 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 12 Jan 2021 19:14:30 -0500 Subject: [PATCH 14/29] Add support for ABCI info request/response Signed-off-by: Thane Thomson --- abci/src/application.rs | 6 ++++++ abci/src/application/echo.rs | 23 ++++++++++++++++++----- abci/src/client.rs | 8 +++++++- abci/src/server/async_std.rs | 1 + abci/src/server/tokio.rs | 1 + abci/tests/async_std.rs | 25 +++++++++++++++++++++---- abci/tests/tokio.rs | 25 +++++++++++++++++++++---- 7 files changed, 75 insertions(+), 14 deletions(-) diff --git a/abci/src/application.rs b/abci/src/application.rs index 861f070b4..c78315e5b 100644 --- a/abci/src/application.rs +++ b/abci/src/application.rs @@ -12,4 +12,10 @@ pub trait Application: Send + Clone { fn echo(&self, req: request::Echo) -> response::Echo { response::Echo::new(req.message) } + + /// Receive information about the Tendermint node and respond with + /// information about the ABCI application. + fn info(&self, _req: request::Info) -> response::Info { + Default::default() + } } diff --git a/abci/src/application/echo.rs b/abci/src/application/echo.rs index 7a6957431..5326f0825 100644 --- a/abci/src/application/echo.rs +++ b/abci/src/application/echo.rs @@ -1,22 +1,35 @@ //! Trivial ABCI application that just implements echo functionality. use crate::Application; +use tendermint::abci::{request, response}; /// Trivial ABCI application that just implements echo functionality. #[derive(Clone)] -pub struct EchoApp {} +pub struct EchoApp { + data: String, + version: String, + app_version: u64, +} impl EchoApp { /// Constructor. - pub fn new() -> Self { - Self {} + pub fn new>(data: S, version: S, app_version: u64) -> Self { + Self { + data: data.as_ref().to_owned(), + version: version.as_ref().to_owned(), + app_version, + } } } impl Default for EchoApp { fn default() -> Self { - EchoApp::new() + EchoApp::new("Echo App", "0.0.1", 1) } } -impl Application for EchoApp {} +impl Application for EchoApp { + fn info(&self, _req: request::Info) -> response::Info { + response::Info::new(&self.data, &self.version, self.app_version, 1, []) + } +} diff --git a/abci/src/client.rs b/abci/src/client.rs index aff01cd48..3ada88419 100644 --- a/abci/src/client.rs +++ b/abci/src/client.rs @@ -19,6 +19,12 @@ pub trait Client { self.perform(req).await } - /// Generic method to perform the given request. + /// Provide information to the ABCI server about the Tendermint node in + /// exchange for information about the application. + async fn info(&mut self, req: request::Info) -> Result { + self.perform(req).await + } + + /// Generic method to perform the given [`Request`]. async fn perform(&mut self, req: R) -> Result; } diff --git a/abci/src/server/async_std.rs b/abci/src/server/async_std.rs index f7645a8ef..becd234a6 100644 --- a/abci/src/server/async_std.rs +++ b/abci/src/server/async_std.rs @@ -93,6 +93,7 @@ async fn handle_client(mut stream: TcpStream, app: A) -> Result< }; let response = match request { Request::Echo(echo) => Response::Echo(app.echo(echo)), + Request::Info(info) => Response::Info(app.info(info)), }; TspEncoder::encode_response(response, &mut write_buf)?; stream.write(&write_buf).await?; diff --git a/abci/src/server/tokio.rs b/abci/src/server/tokio.rs index dbd6ffe7a..9729cf661 100644 --- a/abci/src/server/tokio.rs +++ b/abci/src/server/tokio.rs @@ -86,6 +86,7 @@ async fn handle_client(stream: TcpStream, app: A) -> Result<()> }; let response = match request { Request::Echo(echo) => Response::Echo(app.echo(echo)), + Request::Info(info) => Response::Info(app.info(info)), }; stream.send(response).await?; } diff --git a/abci/tests/async_std.rs b/abci/tests/async_std.rs index 7d98be8ef..43c2c627a 100644 --- a/abci/tests/async_std.rs +++ b/abci/tests/async_std.rs @@ -2,20 +2,20 @@ #[cfg(all(feature = "with-async-std", feature = "client", feature = "echo-app"))] mod async_std_integration { - use tendermint::abci::request::Echo; + use tendermint::abci::request; use tendermint_abci::{AsyncStdClient, AsyncStdServer, Client, EchoApp}; #[async_std::test] async fn echo() { - let app = EchoApp::new(); + let app = EchoApp::default(); let (server, term_tx) = AsyncStdServer::bind("127.0.0.1:0", app).await.unwrap(); let server_addr = server.local_addr(); let server_handle = async_std::task::spawn(async move { server.listen().await }); let mut client = AsyncStdClient::connect(server_addr).await.unwrap(); let requests = (0..5) - .map(|r| Echo::new(format!("Request {}", r))) - .collect::>(); + .map(|r| request::Echo::new(format!("Request {}", r))) + .collect::>(); for request in &requests { let res = client.echo(request.clone()).await.unwrap(); assert_eq!(res.message, request.message); @@ -24,4 +24,21 @@ mod async_std_integration { term_tx.send(()).await.unwrap(); server_handle.await.unwrap(); } + + #[async_std::test] + async fn info() { + let app = EchoApp::new("Echo App", "0.0.1", 1); + let (server, term_tx) = AsyncStdServer::bind("127.0.0.1:0", app).await.unwrap(); + let server_addr = server.local_addr(); + let server_handle = async_std::task::spawn(async move { server.listen().await }); + + let mut client = AsyncStdClient::connect(server_addr).await.unwrap(); + let response = client.info(request::Info::default()).await.unwrap(); + assert_eq!(response.data, "Echo App"); + assert_eq!(response.version, "0.0.1"); + assert_eq!(response.app_version, 1); + + term_tx.send(()).await.unwrap(); + server_handle.await.unwrap(); + } } diff --git a/abci/tests/tokio.rs b/abci/tests/tokio.rs index 2b00fc5df..c9d91c1ca 100644 --- a/abci/tests/tokio.rs +++ b/abci/tests/tokio.rs @@ -2,20 +2,20 @@ #[cfg(all(feature = "with-tokio", feature = "client", feature = "echo-app"))] mod tokio_integration { - use tendermint::abci::request::Echo; + use tendermint::abci::request; use tendermint_abci::{Client, EchoApp, TokioClient, TokioServer}; #[tokio::test] async fn echo() { - let app = EchoApp::new(); + let app = EchoApp::default(); let (server, term_tx) = TokioServer::bind("127.0.0.1:0", app).await.unwrap(); let server_addr = server.local_addr(); let server_handle = tokio::spawn(async move { server.listen().await }); let mut client = TokioClient::connect(server_addr).await.unwrap(); let requests = (0..5) - .map(|r| Echo::new(format!("Request {}", r))) - .collect::>(); + .map(|r| request::Echo::new(format!("Request {}", r))) + .collect::>(); for request in &requests { let res = client.echo(request.clone()).await.unwrap(); assert_eq!(res.message, request.message); @@ -24,4 +24,21 @@ mod tokio_integration { term_tx.send(()).await.unwrap(); server_handle.await.unwrap().unwrap(); } + + #[tokio::test] + async fn info() { + let app = EchoApp::new("Echo App", "0.0.1", 1); + let (server, term_tx) = TokioServer::bind("127.0.0.1:0", app).await.unwrap(); + let server_addr = server.local_addr(); + let server_handle = tokio::spawn(async move { server.listen().await }); + + let mut client = TokioClient::connect(server_addr).await.unwrap(); + let response = client.info(request::Info::default()).await.unwrap(); + assert_eq!(response.data, "Echo App"); + assert_eq!(response.version, "0.0.1"); + assert_eq!(response.app_version, 1); + + term_tx.send(()).await.unwrap(); + server_handle.await.unwrap().unwrap(); + } } From cba4c45d8bcfcdd9ffda8f9e53933c75042440d3 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 21 Jan 2021 18:27:20 -0500 Subject: [PATCH 15/29] Allow for direct construction of ABCI structs Signed-off-by: Thane Thomson --- abci/src/application.rs | 4 ++- abci/src/application/echo.rs | 8 ++++- abci/src/codec.rs | 10 ++++-- abci/tests/async_std.rs | 4 ++- abci/tests/tokio.rs | 4 ++- tendermint/src/abci/request/echo.rs | 14 ++------- tendermint/src/abci/request/info.rs | 37 ++++++++-------------- tendermint/src/abci/response/echo.rs | 14 ++------- tendermint/src/abci/response/info.rs | 46 +++++++++------------------- 9 files changed, 56 insertions(+), 85 deletions(-) diff --git a/abci/src/application.rs b/abci/src/application.rs index c78315e5b..0f784fd5e 100644 --- a/abci/src/application.rs +++ b/abci/src/application.rs @@ -10,7 +10,9 @@ use tendermint::abci::{request, response}; pub trait Application: Send + Clone { /// Request that the ABCI server echo back the same message sent to it. fn echo(&self, req: request::Echo) -> response::Echo { - response::Echo::new(req.message) + response::Echo { + message: req.message, + } } /// Receive information about the Tendermint node and respond with diff --git a/abci/src/application/echo.rs b/abci/src/application/echo.rs index 5326f0825..f5a5e5274 100644 --- a/abci/src/application/echo.rs +++ b/abci/src/application/echo.rs @@ -30,6 +30,12 @@ impl Default for EchoApp { impl Application for EchoApp { fn info(&self, _req: request::Info) -> response::Info { - response::Info::new(&self.data, &self.version, self.app_version, 1, []) + response::Info { + data: self.data.clone(), + version: self.version.clone(), + app_version: self.app_version, + last_block_height: 1, + last_block_app_hash: vec![], + } } } diff --git a/abci/src/codec.rs b/abci/src/codec.rs index c7be963d0..edd40d1e4 100644 --- a/abci/src/codec.rs +++ b/abci/src/codec.rs @@ -128,7 +128,9 @@ mod test { #[test] fn single_request() { - let request = Request::Echo(Echo::new("Hello TSP!")); + let request = Request::Echo(Echo { + message: "Hello TSP!".to_owned(), + }); let mut buf = BytesMut::new(); TspEncoder::encode_request(request.clone(), &mut buf).unwrap(); @@ -141,7 +143,11 @@ mod test { #[test] fn multiple_requests() { let requests = (0..5) - .map(|r| Request::Echo(Echo::new(format!("Request {}", r)))) + .map(|r| { + Request::Echo(Echo { + message: format!("Request {}", r), + }) + }) .collect::>(); let mut buf = BytesMut::new(); requests diff --git a/abci/tests/async_std.rs b/abci/tests/async_std.rs index 43c2c627a..be3b45d72 100644 --- a/abci/tests/async_std.rs +++ b/abci/tests/async_std.rs @@ -14,7 +14,9 @@ mod async_std_integration { let mut client = AsyncStdClient::connect(server_addr).await.unwrap(); let requests = (0..5) - .map(|r| request::Echo::new(format!("Request {}", r))) + .map(|r| request::Echo { + message: format!("Request {}", r), + }) .collect::>(); for request in &requests { let res = client.echo(request.clone()).await.unwrap(); diff --git a/abci/tests/tokio.rs b/abci/tests/tokio.rs index c9d91c1ca..7d4ca2136 100644 --- a/abci/tests/tokio.rs +++ b/abci/tests/tokio.rs @@ -14,7 +14,9 @@ mod tokio_integration { let mut client = TokioClient::connect(server_addr).await.unwrap(); let requests = (0..5) - .map(|r| request::Echo::new(format!("Request {}", r))) + .map(|r| request::Echo { + message: format!("Request {}", r), + }) .collect::>(); for request in &requests { let res = client.echo(request.clone()).await.unwrap(); diff --git a/tendermint/src/abci/request/echo.rs b/tendermint/src/abci/request/echo.rs index 58dbc66bd..cf021107f 100644 --- a/tendermint/src/abci/request/echo.rs +++ b/tendermint/src/abci/request/echo.rs @@ -9,28 +9,20 @@ use tendermint_proto::Protobuf; /// Request that the ABCI server echo a message back the client. #[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] pub struct Echo { /// The message to echo back the client. pub message: String, } -impl Echo { - /// Constructor. - pub fn new>(message: S) -> Self { - Self { - message: message.as_ref().to_owned(), - } - } -} - impl Protobuf for Echo {} impl TryFrom for Echo { type Error = Error; fn try_from(raw: RequestEcho) -> Result { - Ok(Self::new(raw.message)) + Ok(Self { + message: raw.message, + }) } } diff --git a/tendermint/src/abci/request/info.rs b/tendermint/src/abci/request/info.rs index 34ec2f52b..c179aab7d 100644 --- a/tendermint/src/abci/request/info.rs +++ b/tendermint/src/abci/request/info.rs @@ -10,7 +10,6 @@ use tendermint_proto::Protobuf; /// Allows a Tendermint node to provide information about itself to the ABCI /// server, in exchange for information about the server. #[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] pub struct Info { /// Tendermint software semantic version. pub version: String, @@ -22,26 +21,14 @@ pub struct Info { pub abci_version: u64, } -impl Info { - /// Constructor. - pub fn new>( - version: S, - block_version: u64, - p2p_version: u64, - abci_version: u64, - ) -> Self { - Self { - version: version.as_ref().to_owned(), - block_version, - p2p_version, - abci_version, - } - } -} - impl Default for Info { fn default() -> Self { - Self::new("", 0, 0, 0) + Self { + version: "".to_string(), + block_version: 0, + p2p_version: 0, + abci_version: 0, + } } } @@ -51,12 +38,12 @@ impl TryFrom for Info { type Error = Error; fn try_from(value: RequestInfo) -> Result { - Ok(Self::new( - value.version, - value.block_version, - value.p2p_version, - 0, - )) + Ok(Self { + version: value.version, + block_version: value.block_version, + p2p_version: value.p2p_version, + abci_version: 0, + }) } } diff --git a/tendermint/src/abci/response/echo.rs b/tendermint/src/abci/response/echo.rs index 9074b274e..42787c820 100644 --- a/tendermint/src/abci/response/echo.rs +++ b/tendermint/src/abci/response/echo.rs @@ -8,28 +8,20 @@ use tendermint_proto::Protobuf; /// ABCI echo response. #[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] pub struct Echo { /// The message to be echoed back to the client. pub message: String, } -impl Echo { - /// Constructor. - pub fn new>(message: S) -> Self { - Self { - message: message.as_ref().to_owned(), - } - } -} - impl Protobuf for Echo {} impl TryFrom for Echo { type Error = Error; fn try_from(raw: ResponseEcho) -> Result { - Ok(Self::new(raw.message)) + Ok(Self { + message: raw.message, + }) } } diff --git a/tendermint/src/abci/response/info.rs b/tendermint/src/abci/response/info.rs index 598818ae0..a1fbe8a02 100644 --- a/tendermint/src/abci/response/info.rs +++ b/tendermint/src/abci/response/info.rs @@ -9,7 +9,6 @@ use tendermint_proto::Protobuf; /// Allows the ABCI app to provide information about itself back to the /// Tendermint node. #[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] pub struct Info { /// Arbitrary (application-specific) information. pub data: String, @@ -23,32 +22,15 @@ pub struct Info { pub last_block_app_hash: Vec, } -impl Info { - /// Constructor. - pub fn new( - data: S, - version: S, - app_version: u64, - last_block_height: i64, - last_block_app_hash: V, - ) -> Self - where - S: AsRef, - V: AsRef<[u8]>, - { - Self { - data: data.as_ref().to_owned(), - version: version.as_ref().to_owned(), - app_version, - last_block_height, - last_block_app_hash: last_block_app_hash.as_ref().to_vec(), - } - } -} - impl Default for Info { fn default() -> Self { - Self::new("", "", 0, 0, []) + Self { + data: "".to_string(), + version: "".to_string(), + app_version: 0, + last_block_height: 0, + last_block_app_hash: vec![], + } } } @@ -58,13 +40,13 @@ impl TryFrom for Info { type Error = Error; fn try_from(value: ResponseInfo) -> Result { - Ok(Self::new( - value.data, - value.version, - value.app_version, - value.last_block_height, - value.last_block_app_hash, - )) + Ok(Self { + data: value.data, + version: value.version, + app_version: value.app_version, + last_block_height: value.last_block_height, + last_block_app_hash: value.last_block_app_hash, + }) } } From 80773b5c4aa69eefb61930442a1d50d0e19a7b56 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 21 Jan 2021 18:30:06 -0500 Subject: [PATCH 16/29] Rename runtime-related features Signed-off-by: Thane Thomson --- abci/Cargo.toml | 5 +++-- abci/src/application.rs | 5 ++--- abci/src/client.rs | 4 ++-- abci/src/lib.rs | 16 +++++++++++----- abci/src/result.rs | 4 ++-- abci/src/runtime.rs | 3 +++ abci/src/server.rs | 4 ++-- abci/tests/async_std.rs | 6 +++++- abci/tests/tokio.rs | 2 +- 9 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 abci/src/runtime.rs diff --git a/abci/Cargo.toml b/abci/Cargo.toml index d8e4fa973..0e884a514 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -9,8 +9,9 @@ edition = "2018" [features] client = [] echo-app = [] -with-tokio = [ "tokio", "tokio-util" ] -with-async-std = [ "async-channel", "async-std" ] +runtime-async-std = [ "async-channel", "async-std" ] +runtime-std = [] +runtime-tokio = [ "tokio", "tokio-util" ] [dependencies] async-trait = "0.1" diff --git a/abci/src/application.rs b/abci/src/application.rs index 0f784fd5e..f89d01c8c 100644 --- a/abci/src/application.rs +++ b/abci/src/application.rs @@ -1,12 +1,11 @@ -//! `async` ABCI server application interface. +//! ABCI application-related types. #[cfg(feature = "echo-app")] pub mod echo; -use async_trait::async_trait; use tendermint::abci::{request, response}; -#[async_trait] +/// ABCI server application interface. pub trait Application: Send + Clone { /// Request that the ABCI server echo back the same message sent to it. fn echo(&self, req: request::Echo) -> response::Echo { diff --git a/abci/src/client.rs b/abci/src/client.rs index 3ada88419..468f5fbaa 100644 --- a/abci/src/client.rs +++ b/abci/src/client.rs @@ -1,8 +1,8 @@ //! ABCI clients for interacting with ABCI servers. -#[cfg(feature = "with-async-std")] +#[cfg(feature = "runtime-async-std")] pub mod async_std; -#[cfg(feature = "with-tokio")] +#[cfg(feature = "runtime-tokio")] pub mod tokio; use crate::Result; diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 6b692c728..89f79d1eb 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -1,26 +1,32 @@ //! ABCI framework for building applications with Tendermint. mod application; -#[cfg(any(feature = "client", feature = "with-tokio", feature = "with-async-std"))] +#[cfg(any( + feature = "client", + feature = "runtime-tokio", + feature = "runtime-async-std" +))] mod codec; mod result; +pub mod runtime; mod server; // Client exports #[cfg(feature = "client")] mod client; + #[cfg(feature = "client")] pub use client::Client; -#[cfg(all(feature = "client", feature = "with-async-std"))] +#[cfg(all(feature = "client", feature = "runtime-async-std"))] pub use client::async_std::AsyncStdClient; -#[cfg(all(feature = "client", feature = "with-tokio"))] +#[cfg(all(feature = "client", feature = "runtime-tokio"))] pub use client::tokio::TokioClient; // Server exports -#[cfg(feature = "with-async-std")] +#[cfg(feature = "runtime-async-std")] pub use server::async_std::AsyncStdServer; -#[cfg(feature = "with-tokio")] +#[cfg(feature = "runtime-tokio")] pub use server::tokio::TokioServer; // Example applications diff --git a/abci/src/result.rs b/abci/src/result.rs index 596274bf9..cf95dee87 100644 --- a/abci/src/result.rs +++ b/abci/src/result.rs @@ -14,11 +14,11 @@ pub enum Error { #[error("network I/O error")] NetworkIo(#[from] std::io::Error), - #[cfg(feature = "with-tokio")] + #[cfg(feature = "runtime-tokio")] #[error("channel send error: {0}")] TokioChannelSend(String), - #[cfg(feature = "with-tokio")] + #[cfg(feature = "runtime-tokio")] #[error("failed to obtain UNIX stream path")] CannotObtainUnixStreamPath, diff --git a/abci/src/runtime.rs b/abci/src/runtime.rs new file mode 100644 index 000000000..9bfafeaf8 --- /dev/null +++ b/abci/src/runtime.rs @@ -0,0 +1,3 @@ +//! Abstractions for facilitating runtime-independent code. + +pub trait Runtime {} diff --git a/abci/src/server.rs b/abci/src/server.rs index bb864f115..2d34561e6 100644 --- a/abci/src/server.rs +++ b/abci/src/server.rs @@ -1,7 +1,7 @@ //! ABCI servers. -#[cfg(feature = "with-tokio")] +#[cfg(feature = "runtime-tokio")] pub mod tokio; -#[cfg(feature = "with-async-std")] +#[cfg(feature = "runtime-async-std")] pub mod async_std; diff --git a/abci/tests/async_std.rs b/abci/tests/async_std.rs index be3b45d72..a9458da30 100644 --- a/abci/tests/async_std.rs +++ b/abci/tests/async_std.rs @@ -1,6 +1,10 @@ /// `async-std`-based ABCI client/server interaction. -#[cfg(all(feature = "with-async-std", feature = "client", feature = "echo-app"))] +#[cfg(all( + feature = "runtime-async-std", + feature = "client", + feature = "echo-app" +))] mod async_std_integration { use tendermint::abci::request; use tendermint_abci::{AsyncStdClient, AsyncStdServer, Client, EchoApp}; diff --git a/abci/tests/tokio.rs b/abci/tests/tokio.rs index 7d4ca2136..1148d2dd8 100644 --- a/abci/tests/tokio.rs +++ b/abci/tests/tokio.rs @@ -1,6 +1,6 @@ //! Tokio-based ABCI client/server interaction. -#[cfg(all(feature = "with-tokio", feature = "client", feature = "echo-app"))] +#[cfg(all(feature = "runtime-tokio", feature = "client", feature = "echo-app"))] mod tokio_integration { use tendermint::abci::request; use tendermint_abci::{Client, EchoApp, TokioClient, TokioServer}; From 4942a816092f28d30c4f02e84f1caa6fafe30c6f Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sat, 23 Jan 2021 09:34:22 -0500 Subject: [PATCH 17/29] Refactor over generic async runtime Signed-off-by: Thane Thomson --- abci/Cargo.toml | 6 +- abci/src/application.rs | 10 +- abci/src/client.rs | 46 ++-- abci/src/client/async_std.rs | 58 ----- abci/src/client/tokio.rs | 79 ------- abci/src/lib.rs | 18 +- abci/src/result.rs | 11 +- abci/src/runtime.rs | 100 ++++++++- abci/src/runtime/async_std.rs | 383 ++++++++++++++++++++++++++++++++++ abci/src/runtime/tokio.rs | 232 ++++++++++++++++++++ abci/src/server.rs | 105 +++++++++- abci/src/server/async_std.rs | 103 --------- abci/src/server/tokio.rs | 125 ----------- abci/tests/async_std.rs | 38 ++-- abci/tests/tokio.rs | 44 ++-- 15 files changed, 907 insertions(+), 451 deletions(-) delete mode 100644 abci/src/client/async_std.rs delete mode 100644 abci/src/client/tokio.rs create mode 100644 abci/src/runtime/async_std.rs create mode 100644 abci/src/runtime/tokio.rs delete mode 100644 abci/src/server/async_std.rs delete mode 100644 abci/src/server/tokio.rs diff --git a/abci/Cargo.toml b/abci/Cargo.toml index 0e884a514..d29b5d06e 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -9,21 +9,23 @@ edition = "2018" [features] client = [] echo-app = [] +async = [ "async-trait", "futures", "pin-project" ] runtime-async-std = [ "async-channel", "async-std" ] runtime-std = [] runtime-tokio = [ "tokio", "tokio-util" ] [dependencies] -async-trait = "0.1" bytes = "0.6" eyre = "0.6" -futures = "0.3" log = "0.4" tendermint = { version = "0.17.1", path = "../tendermint" } tendermint-proto = { version = "0.17.1", path = "../proto" } thiserror = "1.0" +async-trait = { version = "0.1", optional = true } async-channel = { version = "1.5", optional = true } async-std = { version = "1.8", features = [ "attributes" ], optional = true } +futures = { version = "0.3", optional = true } +pin-project = { version = "1.0", optional = true } tokio = { version = "0.3", features = [ "macros", "net", "rt", "sync" ], optional = true } tokio-util = { version = "0.5", features = [ "codec" ], optional = true } diff --git a/abci/src/application.rs b/abci/src/application.rs index f89d01c8c..1fa4860e2 100644 --- a/abci/src/application.rs +++ b/abci/src/application.rs @@ -6,7 +6,7 @@ pub mod echo; use tendermint::abci::{request, response}; /// ABCI server application interface. -pub trait Application: Send + Clone { +pub trait Application: Send + Clone + 'static { /// Request that the ABCI server echo back the same message sent to it. fn echo(&self, req: request::Echo) -> response::Echo { response::Echo { @@ -19,4 +19,12 @@ pub trait Application: Send + Clone { fn info(&self, _req: request::Info) -> response::Info { Default::default() } + + /// Generic handler for mapping incoming requests to responses. + fn handle(&self, req: request::Request) -> response::Response { + match req { + request::Request::Echo(echo) => response::Response::Echo(self.echo(echo)), + request::Request::Info(info) => response::Response::Info(self.info(info)), + } + } } diff --git a/abci/src/client.rs b/abci/src/client.rs index 468f5fbaa..50a1f2b6b 100644 --- a/abci/src/client.rs +++ b/abci/src/client.rs @@ -1,30 +1,48 @@ //! ABCI clients for interacting with ABCI servers. -#[cfg(feature = "runtime-async-std")] -pub mod async_std; -#[cfg(feature = "runtime-tokio")] -pub mod tokio; - -use crate::Result; -use async_trait::async_trait; +use crate::runtime::{ClientCodec, Runtime, TcpStream}; +use crate::{Error, Result}; +use std::convert::TryInto; use tendermint::abci::request::RequestInner; use tendermint::abci::{request, response}; -/// An asynchronous ABCI client. -#[async_trait] -pub trait Client { +/// A runtime-dependent ABCI client. +pub struct Client { + codec: Rt::ClientCodec, +} + +#[cfg(feature = "async")] +impl Client { + /// Connect to the ABCI server at the given network address. + pub async fn connect>(addr: S) -> Result { + let stream = Rt::TcpStream::connect(addr.as_ref()).await?; + Ok(Self { + codec: Rt::ClientCodec::from_tcp_stream(stream), + }) + } + /// Request that the ABCI server echo back the message in the given /// request. - async fn echo(&mut self, req: request::Echo) -> Result { + pub async fn echo(&mut self, req: request::Echo) -> Result { self.perform(req).await } /// Provide information to the ABCI server about the Tendermint node in /// exchange for information about the application. - async fn info(&mut self, req: request::Info) -> Result { + pub async fn info(&mut self, req: request::Info) -> Result { self.perform(req).await } - /// Generic method to perform the given [`Request`]. - async fn perform(&mut self, req: R) -> Result; + async fn perform(&mut self, req: Req) -> Result { + use futures::{SinkExt, StreamExt}; + + self.codec.send(req.into()).await?; + let res: std::result::Result = self + .codec + .next() + .await + .ok_or(Error::ServerStreamTerminated)?? + .try_into(); + Ok(res?) + } } diff --git a/abci/src/client/async_std.rs b/abci/src/client/async_std.rs deleted file mode 100644 index a91bfde3e..000000000 --- a/abci/src/client/async_std.rs +++ /dev/null @@ -1,58 +0,0 @@ -//! `async-std`-based ABCI client. - -use crate::codec::{TspDecoder, TspEncoder}; -use crate::{Client, Result}; -use async_std::net::{TcpStream, ToSocketAddrs}; -use async_trait::async_trait; -use bytes::BytesMut; -use futures::{AsyncReadExt, AsyncWriteExt}; -use std::convert::TryInto; -use tendermint::abci::request::RequestInner; - -const CLIENT_READ_BUF_SIZE: usize = 4096; - -/// `async-std`-based ABCI client for interacting with an ABCI server via a TCP -/// socket. -/// -/// Not thread-safe, because it wraps a single outgoing TCP connection and the -/// underlying protocol doesn't support multiplexing. To submit requests in -/// parallel, create multiple client instances. -pub struct AsyncStdClient { - stream: TcpStream, - read_buf: BytesMut, - write_buf: BytesMut, - decoder: TspDecoder, -} - -#[async_trait] -impl Client for AsyncStdClient { - async fn perform(&mut self, req: R) -> Result { - TspEncoder::encode_request(req.into(), &mut self.write_buf)?; - self.stream.write(self.write_buf.as_ref()).await?; - self.write_buf.clear(); - - let mut read_buf = [0u8; CLIENT_READ_BUF_SIZE]; - loop { - let bytes_read = self.stream.read(&mut read_buf).await?; - self.read_buf.extend_from_slice(&read_buf[..bytes_read]); - // Try to read a full response - if let Some(response) = self.decoder.decode_response(&mut self.read_buf)? { - return Ok(response.try_into()?); - } - // Otherwise continue reading into our read buffer - } - } -} - -impl AsyncStdClient { - /// Connect to the given ABCI server address. - pub async fn connect(addr: A) -> Result { - let stream = TcpStream::connect(addr).await?; - Ok(Self { - stream, - read_buf: BytesMut::new(), - write_buf: BytesMut::new(), - decoder: TspDecoder::new(), - }) - } -} diff --git a/abci/src/client/tokio.rs b/abci/src/client/tokio.rs deleted file mode 100644 index 7e80ce82a..000000000 --- a/abci/src/client/tokio.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Tokio-based ABCI client. - -use crate::client::Client; -use crate::codec::{TspDecoder, TspEncoder}; -use crate::{Error, Result}; -use async_trait::async_trait; -use bytes::BytesMut; -use futures::{SinkExt, StreamExt}; -use std::convert::TryInto; -use tendermint::abci::request::{Request, RequestInner}; -use tendermint::abci::response::Response; -use tokio::net::{TcpStream, ToSocketAddrs}; -use tokio_util::codec::{Decoder, Encoder, Framed}; - -/// Tokio-based ABCI client for interacting with an ABCI server via a TCP -/// socket. -/// -/// Not thread-safe, because it wraps a single outgoing TCP connection and the -/// underlying protocol doesn't support multiplexing. To submit requests in -/// parallel, create multiple client instances. -pub struct TokioClient { - stream: Framed, -} - -#[async_trait] -impl Client for TokioClient { - async fn perform(&mut self, req: R) -> Result { - self.stream.send(req.into()).await?; - let res: std::result::Result = self - .stream - .next() - .await - .ok_or(Error::ServerStreamTerminated)?? - .try_into(); - Ok(res?) - } -} - -impl TokioClient { - /// Connect to the given ABCI server address. - pub async fn connect(addr: A) -> Result { - let stream = TcpStream::connect(addr).await?; - Ok(Self { - stream: Framed::new(stream, ClientCodec::default()), - }) - } -} - -/// Codec for the ABCI client. -/// -/// Implements [`Encode`] for [`Request`]s and [`Decode`] for [`Response`]s. -pub struct ClientCodec { - decoder: TspDecoder, -} - -impl Default for ClientCodec { - fn default() -> Self { - Self { - decoder: TspDecoder::new(), - } - } -} - -impl Encoder for ClientCodec { - type Error = Error; - - fn encode(&mut self, item: Request, mut dst: &mut BytesMut) -> Result<()> { - TspEncoder::encode_request(item, &mut dst) - } -} - -impl Decoder for ClientCodec { - type Item = Response; - type Error = Error; - - fn decode(&mut self, mut src: &mut BytesMut) -> Result> { - self.decoder.decode_response(&mut src) - } -} diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 89f79d1eb..4c298a391 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -18,16 +18,10 @@ mod client; #[cfg(feature = "client")] pub use client::Client; -#[cfg(all(feature = "client", feature = "runtime-async-std"))] -pub use client::async_std::AsyncStdClient; #[cfg(all(feature = "client", feature = "runtime-tokio"))] -pub use client::tokio::TokioClient; - -// Server exports -#[cfg(feature = "runtime-async-std")] -pub use server::async_std::AsyncStdServer; -#[cfg(feature = "runtime-tokio")] -pub use server::tokio::TokioServer; +pub type TokioClient = Client; +#[cfg(all(feature = "client", feature = "runtime-async-std"))] +pub type AsyncStdClient = Client; // Example applications #[cfg(feature = "echo-app")] @@ -36,3 +30,9 @@ pub use application::echo::EchoApp; // Common exports pub use application::Application; pub use result::{Error, Result}; +pub use server::Server; + +#[cfg(feature = "runtime-tokio")] +pub type TokioServer = Server; +#[cfg(feature = "runtime-async-std")] +pub type AsyncStdServer = Server; diff --git a/abci/src/result.rs b/abci/src/result.rs index cf95dee87..eb3699943 100644 --- a/abci/src/result.rs +++ b/abci/src/result.rs @@ -14,9 +14,9 @@ pub enum Error { #[error("network I/O error")] NetworkIo(#[from] std::io::Error), - #[cfg(feature = "runtime-tokio")] + #[cfg(any(feature = "runtime-tokio", feature = "runtime-async-std"))] #[error("channel send error: {0}")] - TokioChannelSend(String), + ChannelSend(String), #[cfg(feature = "runtime-tokio")] #[error("failed to obtain UNIX stream path")] @@ -27,4 +27,11 @@ pub enum Error { #[error("server stream terminated unexpectedly")] ServerStreamTerminated, + + #[error("sending end of channel closed unexpectedly")] + ChannelSenderClosed, + + #[cfg(feature = "runtime-async-std")] + #[error("failed to receive message from channel: {0}")] + ChannelRecv(String), } diff --git a/abci/src/runtime.rs b/abci/src/runtime.rs index 9bfafeaf8..98e35c96e 100644 --- a/abci/src/runtime.rs +++ b/abci/src/runtime.rs @@ -1,3 +1,101 @@ //! Abstractions for facilitating runtime-independent code. -pub trait Runtime {} +#[cfg(feature = "runtime-async-std")] +pub mod async_std; +#[cfg(feature = "runtime-tokio")] +pub mod tokio; + +pub use crate::runtime::interface::{ + ChannelNotify, ClientCodec, Receiver, Sender, ServerCodec, TaskSpawner, TcpListener, TcpStream, +}; + +/// Implemented by each runtime we support. +pub trait Runtime: 'static { + type TcpStream: TcpStream; + type TcpListener: TcpListener; + type TaskSpawner: TaskSpawner; + type ServerCodec: ServerCodec; + type ClientCodec: ClientCodec; + + // TODO(thane): Make this generic once GATs are stable (see + // https://rust-lang.github.io/rfcs/1598-generic_associated_types.html) + type ChannelNotify: ChannelNotify; +} + +#[cfg(feature = "async")] +mod interface { + use crate::{Error, Result}; + use async_trait::async_trait; + use futures::{Future, Sink, Stream}; + use std::net::SocketAddr; + use tendermint::abci::{request, response}; + + #[async_trait] + pub trait TcpListener: Sized { + /// Bind this listener to the given address. + async fn bind(addr: &str) -> Result; + + /// Returns the string representation of this listener's local address. + fn local_addr(&self) -> Result; + + /// Attempt to accept an incoming connection. + async fn accept(&self) -> Result<(T, SocketAddr)>; + } + + #[async_trait] + pub trait TcpStream: Sized + Send { + async fn connect(addr: &str) -> Result; + } + + pub trait TaskSpawner { + /// Spawn an asynchronous task without caring about its result. + fn spawn_and_forget(task: T) + where + T: Future + Send + 'static, + T::Output: Send + 'static; + } + + pub trait ServerCodec: + Stream> + + Sink + + Unpin + + Send + { + fn from_tcp_stream(stream: S) -> Self; + } + + pub trait ClientCodec: + Sink + + Stream> + + Unpin + + Send + { + fn from_tcp_stream(stream: S) -> Self; + } + + /// The sending end of a channel. + #[async_trait] + pub trait Sender { + async fn send(&self, value: T) -> Result<()>; + } + + /// The receiving end of a channel. + #[async_trait] + pub trait Receiver { + async fn recv(&mut self) -> Result; + } + + /// A simple notification channel. + pub trait ChannelNotify { + type Sender: Sender<()>; + type Receiver: Receiver<()>; + + /// Construct an unbounded channel. + fn unbounded() -> (Self::Sender, Self::Receiver); + } +} + +#[cfg(not(feature = "async"))] +mod interface { + pub trait Server {} +} diff --git a/abci/src/runtime/async_std.rs b/abci/src/runtime/async_std.rs new file mode 100644 index 000000000..ba538362b --- /dev/null +++ b/abci/src/runtime/async_std.rs @@ -0,0 +1,383 @@ +//! `async-std` runtime-specific types. + +// TODO(thane): Implement something like tokio-util's `Framed` for async-std to +// reduce code duplication in AsyncStdServerCodec and +// AsyncStdServerCodec. + +use crate::codec::{TspDecoder, TspEncoder}; +use crate::runtime::{ + ChannelNotify, ClientCodec, Receiver, Runtime, Sender, ServerCodec, TaskSpawner, TcpListener, + TcpStream, +}; +use crate::{Error, Result}; +use async_trait::async_trait; +use bytes::{Buf, BytesMut}; +use futures::ready; +use futures::task::{Context, Poll}; +use futures::{AsyncRead, AsyncWrite, Future, Sink, Stream}; +use pin_project::pin_project; +use std::net::SocketAddr; +use std::pin::Pin; +use tendermint::abci::{request, response}; + +// The size of the read buffer we use when reading from a TCP stream. This is +// allocated each time a stream is polled for readiness to be read, so it's +// important that it's relatively small. Too small, however, and it'll increase +// CPU load due to increased decode/poll attempts. +// +// Tokio seems to get around this by using `unsafe` code in their buffer +// polling routine in `tokio-util`: https://github.com/tokio-rs/tokio/blob/198363f4f1a71cb98ffc0e9eaac335f669a5e1de/tokio-util/src/lib.rs#L106 +// but we don't want to write our own `unsafe` code. +// +// As for a good number for general ABCI apps, that's something we should +// benchmark to determine. +// TODO(thane): Benchmark options here. +const CODEC_READ_BUF_SIZE: usize = 128; + +/// `async-std` runtime. +pub struct AsyncStd; + +impl Runtime for AsyncStd { + type TcpStream = AsyncStdTcpStream; + type TcpListener = AsyncStdTcpListener; + type TaskSpawner = AsyncStdTaskSpawner; + type ServerCodec = AsyncStdServerCodec; + type ClientCodec = AsyncStdClientCodec; + type ChannelNotify = AsyncStdChannelNotify; +} + +pub struct AsyncStdTcpStream(async_std::net::TcpStream); + +#[async_trait] +impl TcpStream for AsyncStdTcpStream { + async fn connect(addr: &str) -> Result { + Ok(Self(async_std::net::TcpStream::connect(addr).await?)) + } +} + +pub struct AsyncStdTcpListener(async_std::net::TcpListener); + +#[async_trait] +impl TcpListener for AsyncStdTcpListener { + async fn bind(addr: &str) -> Result { + Ok(Self(async_std::net::TcpListener::bind(addr).await?)) + } + + fn local_addr(&self) -> Result { + Ok(self.0.local_addr()?.to_string()) + } + + async fn accept(&self) -> Result<(AsyncStdTcpStream, SocketAddr)> { + let (stream, addr) = self.0.accept().await?; + Ok((AsyncStdTcpStream(stream), addr)) + } +} + +pub struct AsyncStdTaskSpawner; + +impl TaskSpawner for AsyncStdTaskSpawner { + fn spawn_and_forget(task: T) + where + T: Future + Send + 'static, + T::Output: Send + 'static, + { + let _ = async_std::task::spawn(task); + } +} + +#[pin_project] +pub struct AsyncStdServerCodec { + #[pin] + stream: async_std::net::TcpStream, + read_buf: BytesMut, + write_buf: BytesMut, + decoder: TspDecoder, +} + +impl ServerCodec for AsyncStdServerCodec { + fn from_tcp_stream(stream: AsyncStdTcpStream) -> Self { + Self { + stream: stream.0, + read_buf: BytesMut::new(), + write_buf: BytesMut::new(), + decoder: TspDecoder::new(), + } + } +} + +impl Stream for AsyncStdServerCodec { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + let mut stream: Pin<&mut async_std::net::TcpStream> = this.stream; + let read_buf: &mut BytesMut = this.read_buf; + let decoder: &mut TspDecoder = this.decoder; + let mut tmp_read_buf = [0_u8; CODEC_READ_BUF_SIZE]; + + loop { + // Try to decode a request from our existing buffer + match decoder.decode_request(read_buf) { + Ok(res) => { + if let Some(req) = res { + return Poll::Ready(Some(Ok(req))); + } + } + Err(e) => return Poll::Ready(Some(Err(e))), + } + + // If we couldn't decode another request from the buffer, try to + // fill up the buffer as much as we can + let bytes_read = match ready!(stream.as_mut().poll_read(cx, &mut tmp_read_buf)) { + Ok(br) => br, + Err(e) => return Poll::Ready(Some(Err(e.into()))), + }; + if bytes_read == 0 { + // The stream terminated + return Poll::Ready(None); + } + read_buf.extend_from_slice(&tmp_read_buf[..bytes_read]); + } + } +} + +impl Sink for AsyncStdServerCodec { + type Error = Error; + + fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send(self: Pin<&mut Self>, item: response::Response) -> Result<()> { + let write_buf: &mut BytesMut = self.project().write_buf; + TspEncoder::encode_response(item, write_buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + let mut stream: Pin<&mut async_std::net::TcpStream> = this.stream; + let write_buf: &mut BytesMut = this.write_buf; + + while !write_buf.is_empty() { + let bytes_written = match ready!(stream.as_mut().poll_write(cx, write_buf.as_ref())) { + Ok(bw) => bw, + Err(e) => return Poll::Ready(Err(e.into())), + }; + if bytes_written == 0 { + return Poll::Ready(Err(async_std::io::Error::new( + async_std::io::ErrorKind::WriteZero, + "failed to write to transport", + ) + .into())); + } + write_buf.advance(bytes_written); + } + // Try to flush the underlying stream + ready!(stream.poll_flush(cx))?; + Poll::Ready(Ok(())) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + ready!(self.as_mut().poll_flush(cx))?; + let stream: Pin<&mut async_std::net::TcpStream> = self.project().stream; + ready!(stream.poll_close(cx))?; + + Poll::Ready(Ok(())) + } +} + +#[pin_project] +pub struct AsyncStdClientCodec { + #[pin] + stream: async_std::net::TcpStream, + read_buf: BytesMut, + write_buf: BytesMut, + decoder: TspDecoder, +} + +impl ClientCodec for AsyncStdClientCodec { + fn from_tcp_stream(stream: AsyncStdTcpStream) -> Self { + Self { + stream: stream.0, + read_buf: BytesMut::new(), + write_buf: BytesMut::new(), + decoder: TspDecoder::new(), + } + } +} + +impl Stream for AsyncStdClientCodec { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + let mut stream: Pin<&mut async_std::net::TcpStream> = this.stream; + let read_buf: &mut BytesMut = this.read_buf; + let decoder: &mut TspDecoder = this.decoder; + let mut tmp_read_buf = [0_u8; CODEC_READ_BUF_SIZE]; + + loop { + // Try to decode a response from our existing buffer + match decoder.decode_response(read_buf) { + Ok(res_opt) => { + if let Some(res) = res_opt { + return Poll::Ready(Some(Ok(res))); + } + } + Err(e) => return Poll::Ready(Some(Err(e))), + } + + // If we couldn't decode another request from the buffer, try to + // fill up the buffer as much as we can + let bytes_read = match ready!(stream.as_mut().poll_read(cx, &mut tmp_read_buf)) { + Ok(br) => br, + Err(e) => return Poll::Ready(Some(Err(e.into()))), + }; + if bytes_read == 0 { + // The stream terminated + return Poll::Ready(None); + } + read_buf.extend_from_slice(&tmp_read_buf[..bytes_read]); + } + } +} + +impl Sink for AsyncStdClientCodec { + type Error = Error; + + fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send(self: Pin<&mut Self>, item: request::Request) -> Result<()> { + let write_buf: &mut BytesMut = self.project().write_buf; + TspEncoder::encode_request(item, write_buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + let mut stream: Pin<&mut async_std::net::TcpStream> = this.stream; + let write_buf: &mut BytesMut = this.write_buf; + + while !write_buf.is_empty() { + let bytes_written = match ready!(stream.as_mut().poll_write(cx, write_buf.as_ref())) { + Ok(bw) => bw, + Err(e) => return Poll::Ready(Err(e.into())), + }; + if bytes_written == 0 { + return Poll::Ready(Err(async_std::io::Error::new( + async_std::io::ErrorKind::WriteZero, + "failed to write to transport", + ) + .into())); + } + write_buf.advance(bytes_written); + } + // Try to flush the underlying stream + ready!(stream.poll_flush(cx))?; + Poll::Ready(Ok(())) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + ready!(self.as_mut().poll_flush(cx))?; + let stream: Pin<&mut async_std::net::TcpStream> = self.project().stream; + ready!(stream.poll_close(cx))?; + + Poll::Ready(Ok(())) + } +} + +pub struct AsyncStdChannelNotify; + +impl ChannelNotify for AsyncStdChannelNotify { + type Sender = AsyncStdSender<()>; + type Receiver = AsyncStdReceiver<()>; + + fn unbounded() -> (Self::Sender, Self::Receiver) { + let (tx, rx) = async_channel::unbounded(); + (AsyncStdSender(tx), AsyncStdReceiver(rx)) + } +} + +pub struct AsyncStdSender(async_channel::Sender); + +#[async_trait] +impl Sender for AsyncStdSender { + async fn send(&self, value: T) -> Result<()> { + self.0 + .send(value) + .await + .map_err(|e| Error::ChannelSend(e.to_string())) + } +} + +pub struct AsyncStdReceiver(async_channel::Receiver); + +#[async_trait] +impl Receiver for AsyncStdReceiver { + async fn recv(&mut self) -> Result { + self.0 + .recv() + .await + .map_err(|e| Error::ChannelRecv(e.to_string())) + } +} + +#[cfg(test)] +mod test { + use super::*; + use async_std::task::JoinHandle; + use futures::{SinkExt, StreamExt}; + use std::convert::TryInto; + + #[async_std::test] + async fn codec() { + let requests = (0..5) + .map(|r| request::Echo { + message: format!("echo {}", r), + }) + .collect::>(); + let listener = async_std::net::TcpListener::bind("127.0.0.1:0") + .await + .unwrap(); + let local_addr = listener.local_addr().unwrap().to_string(); + let client_requests = requests.clone(); + let client_handle: JoinHandle> = async_std::task::spawn(async move { + let client_stream = async_std::net::TcpStream::connect(local_addr) + .await + .unwrap(); + let mut codec = AsyncStdClientCodec::from_tcp_stream(AsyncStdTcpStream(client_stream)); + let mut received_responses = Vec::new(); + + for req in client_requests { + codec.send(req.into()).await.unwrap(); + let res: response::Echo = codec.next().await.unwrap().unwrap().try_into().unwrap(); + received_responses.push(res); + } + + received_responses + }); + + let (server_stream, _) = listener.accept().await.unwrap(); + let mut codec = AsyncStdServerCodec::from_tcp_stream(AsyncStdTcpStream(server_stream)); + + let mut received_requests = Vec::new(); + while let Some(result) = codec.next().await { + let request: request::Echo = result.unwrap().try_into().unwrap(); + codec + .send( + response::Echo { + message: request.message.clone(), + } + .into(), + ) + .await + .unwrap(); + received_requests.push(request); + } + let received_responses = client_handle.await; + assert_eq!(received_requests.len(), requests.len()); + assert_eq!(received_requests, requests); + assert_eq!(received_responses.len(), requests.len()); + } +} diff --git a/abci/src/runtime/tokio.rs b/abci/src/runtime/tokio.rs new file mode 100644 index 000000000..3862d9af5 --- /dev/null +++ b/abci/src/runtime/tokio.rs @@ -0,0 +1,232 @@ +//! Tokio runtime-specific types. + +use crate::codec::{TspDecoder, TspEncoder}; +use crate::runtime::{ + ChannelNotify, ClientCodec, Receiver, Runtime, Sender, ServerCodec, TaskSpawner, TcpListener, + TcpStream, +}; +use crate::{Error, Result}; +use async_trait::async_trait; +use bytes::BytesMut; +use futures::task::{Context, Poll}; +use futures::{Future, Sink, Stream}; +use pin_project::pin_project; +use std::net::SocketAddr; +use std::pin::Pin; +use tendermint::abci::{request, response}; +use tokio_util::codec::{Decoder, Encoder, Framed}; + +/// Tokio runtime. +pub struct Tokio; + +impl Runtime for Tokio { + type TcpStream = TokioTcpStream; + type TcpListener = TokioTcpListener; + type TaskSpawner = TokioTaskSpawner; + type ServerCodec = TokioServerCodec; + type ClientCodec = TokioClientCodec; + type ChannelNotify = TokioChannelNotify; +} + +pub struct TokioTcpStream(tokio::net::TcpStream); + +#[async_trait] +impl TcpStream for TokioTcpStream { + async fn connect(addr: &str) -> Result { + Ok(Self(tokio::net::TcpStream::connect(addr).await?)) + } +} + +pub struct TokioTcpListener(tokio::net::TcpListener); + +#[async_trait] +impl TcpListener for TokioTcpListener { + async fn bind(addr: &str) -> Result { + Ok(Self(tokio::net::TcpListener::bind(addr).await?)) + } + + fn local_addr(&self) -> Result { + Ok(self.0.local_addr()?.to_string()) + } + + async fn accept(&self) -> Result<(TokioTcpStream, SocketAddr)> { + let (stream, addr) = self.0.accept().await?; + Ok((TokioTcpStream(stream), addr)) + } +} + +pub struct TokioTaskSpawner; + +impl TaskSpawner for TokioTaskSpawner { + fn spawn_and_forget(task: T) + where + T: Future + Send + 'static, + T::Output: Send + 'static, + { + let _ = tokio::spawn(task); + } +} + +#[pin_project] +pub struct TokioServerCodec(#[pin] Framed); + +impl ServerCodec for TokioServerCodec { + fn from_tcp_stream(stream: TokioTcpStream) -> Self { + Self(Framed::new(stream.0, TspServerCodec::default())) + } +} + +impl Stream for TokioServerCodec { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_next(cx) + } +} + +impl Sink for TokioServerCodec { + type Error = Error; + + fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_ready(cx) + } + + fn start_send(self: Pin<&mut Self>, item: response::Response) -> Result<()> { + self.project().0.start_send(item) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_close(cx) + } +} + +pub struct TspServerCodec { + decoder: TspDecoder, +} + +impl Default for TspServerCodec { + fn default() -> Self { + Self { + decoder: TspDecoder::new(), + } + } +} + +impl Encoder for TspServerCodec { + type Error = Error; + + fn encode(&mut self, item: response::Response, dst: &mut BytesMut) -> Result<()> { + TspEncoder::encode_response(item, dst) + } +} + +impl Decoder for TspServerCodec { + type Item = request::Request; + type Error = Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result> { + self.decoder.decode_request(src) + } +} + +#[pin_project] +pub struct TokioClientCodec(#[pin] Framed); + +impl ClientCodec for TokioClientCodec { + fn from_tcp_stream(stream: TokioTcpStream) -> Self { + Self(Framed::new(stream.0, TspClientCodec::default())) + } +} + +impl Stream for TokioClientCodec { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_next(cx) + } +} + +impl Sink for TokioClientCodec { + type Error = Error; + + fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_ready(cx) + } + + fn start_send(self: Pin<&mut Self>, item: request::Request) -> Result<()> { + self.project().0.start_send(item) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_close(cx) + } +} + +pub struct TspClientCodec { + decoder: TspDecoder, +} + +impl Default for TspClientCodec { + fn default() -> Self { + Self { + decoder: TspDecoder::new(), + } + } +} + +impl Encoder for TspClientCodec { + type Error = Error; + + fn encode(&mut self, item: request::Request, dst: &mut BytesMut) -> Result<()> { + TspEncoder::encode_request(item, dst) + } +} + +impl Decoder for TspClientCodec { + type Item = response::Response; + type Error = Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result> { + self.decoder.decode_response(src) + } +} + +pub struct TokioChannelNotify; + +impl ChannelNotify for TokioChannelNotify { + type Sender = TokioSender<()>; + type Receiver = TokioReceiver<()>; + + fn unbounded() -> (Self::Sender, Self::Receiver) { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + (TokioSender(tx), TokioReceiver(rx)) + } +} + +pub struct TokioSender(tokio::sync::mpsc::UnboundedSender); + +#[async_trait] +impl Sender for TokioSender { + async fn send(&self, value: T) -> Result<()> { + self.0 + .send(value) + .map_err(|e| Error::ChannelSend(e.to_string())) + } +} + +pub struct TokioReceiver(tokio::sync::mpsc::UnboundedReceiver); + +#[async_trait] +impl Receiver for TokioReceiver { + async fn recv(&mut self) -> Result { + self.0.recv().await.ok_or(Error::ChannelSenderClosed) + } +} diff --git a/abci/src/server.rs b/abci/src/server.rs index 2d34561e6..8e9affde3 100644 --- a/abci/src/server.rs +++ b/abci/src/server.rs @@ -1,7 +1,104 @@ //! ABCI servers. -#[cfg(feature = "runtime-tokio")] -pub mod tokio; +use crate::runtime::{ChannelNotify, Receiver, Runtime, ServerCodec, TaskSpawner, TcpListener}; +use crate::{Application, Result}; +use log::{debug, error, info}; +use tendermint::abci::request; -#[cfg(feature = "runtime-async-std")] -pub mod async_std; +/// ABCI server for a specific application and runtime. +pub struct Server { + app: App, + listener: Rt::TcpListener, + local_addr: String, + term_rx: ::Receiver, +} + +#[cfg(feature = "async")] +impl Server +where + App: Application, + Rt: Runtime, +{ + /// Bind our ABCI application server to the given address. + /// + /// On success, returns our server and the sending end of a channel we can + /// use to terminate the server while it's listening. + pub async fn bind>( + addr: S, + app: App, + ) -> Result<(Self, ::Sender)> { + let listener = Rt::TcpListener::bind(addr.as_ref()).await?; + let (term_tx, term_rx) = ::unbounded(); + let local_addr = listener.local_addr()?; + Ok(( + Self { + app, + listener, + local_addr, + term_rx, + }, + term_tx, + )) + } + + /// Get the local address for the server, once bound. + pub fn local_addr(&self) -> String { + self.local_addr.clone() + } + + /// Start listening for incoming connections. + pub async fn listen(mut self) -> Result<()> { + use futures::FutureExt; + + loop { + futures::select! { + result = self.listener.accept().fuse() => match result { + Ok(r) => { + let (stream, addr) = r; + info!("Incoming connection from: {}", addr.to_string()); + self.spawn_client_handler(stream).await; + }, + Err(e) => { + error!("Failed to accept incoming connection: {:?}", e); + } + }, + _ = self.term_rx.recv().fuse() => { + info!("Server terminated"); + return Ok(()) + } + } + } + } + + async fn spawn_client_handler(&self, stream: Rt::TcpStream) { + Rt::TaskSpawner::spawn_and_forget(Self::handle_client(stream, self.app.clone())); + } + + async fn handle_client(stream: Rt::TcpStream, app: App) { + use futures::{SinkExt, StreamExt}; + + let mut codec = Rt::ServerCodec::from_tcp_stream(stream); + loop { + let req: request::Request = match codec.next().await { + Some(result) => match result { + Ok(r) => r, + Err(e) => { + error!("Failed to read request from client: {}", e); + return; + } + }, + None => { + info!("Client terminated connection"); + return; + } + }; + debug!("Got incoming request from client: {:?}", req); + let res = app.handle(req); + debug!("Sending outgoing response: {:?}", res); + if let Err(e) = codec.send(res).await { + error!("Failed to write outgoing response to client: {}", e); + return; + } + } + } +} diff --git a/abci/src/server/async_std.rs b/abci/src/server/async_std.rs deleted file mode 100644 index becd234a6..000000000 --- a/abci/src/server/async_std.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! `async-std`-based ABCI server. - -use crate::codec::{TspDecoder, TspEncoder}; -use crate::{Application, Result}; -use async_channel::{bounded, Receiver, Sender}; -use async_std::net::{TcpListener, TcpStream, ToSocketAddrs}; -use bytes::BytesMut; -use futures::{select, AsyncReadExt, AsyncWriteExt, FutureExt}; -use log::info; -use tendermint::abci::request::Request; -use tendermint::abci::response::Response; - -const SERVER_READ_BUF_SIZE: usize = 4096; - -/// `async-std`-based ABCI server for a specific ABCI application. -/// -/// Listens for incoming TCP connections. -pub struct AsyncStdServer { - app: A, - listener: TcpListener, - local_addr: String, - term_rx: Receiver<()>, -} - -impl AsyncStdServer { - /// Bind the application server to the given socket address using TCP. - /// - /// On success, returns the server and a channel through which the server - /// can be asynchronously signaled to terminate. - pub async fn bind(addr: S, app: A) -> Result<(Self, Sender<()>)> - where - S: ToSocketAddrs, - { - let listener = TcpListener::bind(addr).await?; - let local_addr = listener.local_addr()?; - info!( - "ABCI server bound to {}, listening for incoming connections", - local_addr, - ); - let (term_tx, term_rx) = bounded(1); - Ok(( - Self { - app, - listener, - local_addr: local_addr.to_string(), - term_rx, - }, - term_tx, - )) - } - - /// Getter for the server's local address. - pub fn local_addr(&self) -> String { - self.local_addr.clone() - } - - /// Block the current task while listening for incoming connections. - /// - /// Each incoming connection is spawned in a separate task. - pub async fn listen(self) -> Result<()> { - loop { - select! { - result = self.listener.accept().fuse() => { - let (stream, addr) = result?; - info!("Incoming connection from {}", addr); - let conn_app = self.app.clone(); - async_std::task::spawn(async move { handle_client(stream, conn_app).await }); - }, - _ = self.term_rx.recv().fuse() => { - // TODO(thane): Terminate client tasks - info!("Server terminated"); - return Ok(()) - } - } - } - } -} - -// Each incoming request is processed sequentially in a single connection. -async fn handle_client(mut stream: TcpStream, app: A) -> Result<()> { - let mut decoder = TspDecoder::new(); - let mut stream_buf = BytesMut::new(); - let mut read_buf = [0u8; SERVER_READ_BUF_SIZE]; - let mut write_buf = BytesMut::new(); - loop { - let bytes_read = stream.read(&mut read_buf).await?; - stream_buf.extend_from_slice(&read_buf[..bytes_read]); - // Try to process as many requests as we can from the stream buffer - 'request_loop: loop { - let request = match decoder.decode_request(&mut stream_buf)? { - Some(req) => req, - None => break 'request_loop, - }; - let response = match request { - Request::Echo(echo) => Response::Echo(app.echo(echo)), - Request::Info(info) => Response::Info(app.info(info)), - }; - TspEncoder::encode_response(response, &mut write_buf)?; - stream.write(&write_buf).await?; - write_buf.clear(); - } - } -} diff --git a/abci/src/server/tokio.rs b/abci/src/server/tokio.rs deleted file mode 100644 index 9729cf661..000000000 --- a/abci/src/server/tokio.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Tokio-based ABCI server. - -use crate::codec::{TspDecoder, TspEncoder}; -use crate::{Application, Error, Result}; -use bytes::BytesMut; -use futures::{SinkExt, StreamExt}; -use log::info; -use tendermint::abci::request::Request; -use tendermint::abci::response::Response; -use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; -use tokio::sync::mpsc; -use tokio_util::codec::{Decoder, Encoder, Framed}; - -/// Tokio-based ABCI server for a specific application. -/// -/// Listens for incoming TCP connections. -pub struct TokioServer { - app: A, - listener: TcpListener, - local_addr: String, - term_rx: mpsc::Receiver<()>, -} - -impl TokioServer { - /// Bind the application server to the given socket address using TCP. - /// - /// On success, returns the server and a channel through which the server - /// can be asynchronously signaled to terminate. - pub async fn bind(addr: S, app: A) -> Result<(Self, mpsc::Sender<()>)> - where - S: ToSocketAddrs, - { - let listener = TcpListener::bind(addr).await?; - let local_addr = listener.local_addr()?; - info!( - "ABCI server bound to {}, listening for incoming connections", - local_addr, - ); - let (term_tx, term_rx) = mpsc::channel(1); - Ok(( - Self { - app, - listener, - local_addr: local_addr.to_string(), - term_rx, - }, - term_tx, - )) - } - - /// Getter for the server's local address. - pub fn local_addr(&self) -> String { - self.local_addr.clone() - } - - /// Block the current task while listening for incoming connections. - /// - /// Each incoming connection is spawned in a separate task. - pub async fn listen(mut self) -> Result<()> { - loop { - tokio::select! { - result = self.listener.accept() => { - let (stream, addr) = result?; - info!("Incoming connection from {}", addr); - let conn_app = self.app.clone(); - tokio::spawn(async move { handle_client(stream, conn_app).await }); - }, - Some(_) = self.term_rx.recv() => { - // TODO(thane): Terminate client tasks - info!("Server terminated"); - return Ok(()) - } - } - } - } -} - -// Each incoming request is processed sequentially in a single connection. -async fn handle_client(stream: TcpStream, app: A) -> Result<()> { - let codec = ServerCodec::default(); - let mut stream = Framed::new(stream, codec); - loop { - let request = match stream.next().await { - Some(res) => res?, - None => return Ok(()), - }; - let response = match request { - Request::Echo(echo) => Response::Echo(app.echo(echo)), - Request::Info(info) => Response::Info(app.info(info)), - }; - stream.send(response).await?; - } -} - -/// Codec for the ABCI server. -/// -/// Implements [`Decode`] for [`Request`]s and [`Encode`] for [`Response`]s. -pub struct ServerCodec { - decoder: TspDecoder, -} - -impl Default for ServerCodec { - fn default() -> Self { - Self { - decoder: TspDecoder::new(), - } - } -} - -impl Encoder for ServerCodec { - type Error = Error; - - fn encode(&mut self, item: Response, mut dst: &mut BytesMut) -> Result<()> { - TspEncoder::encode_response(item, &mut dst) - } -} - -impl Decoder for ServerCodec { - type Item = Request; - type Error = Error; - - fn decode(&mut self, mut src: &mut BytesMut) -> Result> { - self.decoder.decode_request(&mut src) - } -} diff --git a/abci/tests/async_std.rs b/abci/tests/async_std.rs index a9458da30..e21cf153e 100644 --- a/abci/tests/async_std.rs +++ b/abci/tests/async_std.rs @@ -1,48 +1,34 @@ -/// `async-std`-based ABCI client/server interaction. +//! `async-std`-based ABCI client/server integration tests. #[cfg(all( + feature = "async", feature = "runtime-async-std", feature = "client", feature = "echo-app" ))] mod async_std_integration { use tendermint::abci::request; - use tendermint_abci::{AsyncStdClient, AsyncStdServer, Client, EchoApp}; + use tendermint_abci::runtime::Sender; + use tendermint_abci::{AsyncStdClient, AsyncStdServer, EchoApp}; #[async_std::test] async fn echo() { - let app = EchoApp::default(); - let (server, term_tx) = AsyncStdServer::bind("127.0.0.1:0", app).await.unwrap(); - let server_addr = server.local_addr(); - let server_handle = async_std::task::spawn(async move { server.listen().await }); - - let mut client = AsyncStdClient::connect(server_addr).await.unwrap(); let requests = (0..5) .map(|r| request::Echo { - message: format!("Request {}", r), + message: format!("echo {}", r), }) .collect::>(); - for request in &requests { - let res = client.echo(request.clone()).await.unwrap(); - assert_eq!(res.message, request.message); - } - - term_tx.send(()).await.unwrap(); - server_handle.await.unwrap(); - } - - #[async_std::test] - async fn info() { - let app = EchoApp::new("Echo App", "0.0.1", 1); - let (server, term_tx) = AsyncStdServer::bind("127.0.0.1:0", app).await.unwrap(); + let (server, term_tx) = AsyncStdServer::bind("127.0.0.1:0", EchoApp::default()) + .await + .unwrap(); let server_addr = server.local_addr(); let server_handle = async_std::task::spawn(async move { server.listen().await }); let mut client = AsyncStdClient::connect(server_addr).await.unwrap(); - let response = client.info(request::Info::default()).await.unwrap(); - assert_eq!(response.data, "Echo App"); - assert_eq!(response.version, "0.0.1"); - assert_eq!(response.app_version, 1); + for req in requests { + let res = client.echo(req.clone()).await.unwrap(); + assert_eq!(res.message, req.message); + } term_tx.send(()).await.unwrap(); server_handle.await.unwrap(); diff --git a/abci/tests/tokio.rs b/abci/tests/tokio.rs index 1148d2dd8..8ef03bc8b 100644 --- a/abci/tests/tokio.rs +++ b/abci/tests/tokio.rs @@ -1,44 +1,34 @@ -//! Tokio-based ABCI client/server interaction. +//! Tokio-based ABCI client/server integration tests. -#[cfg(all(feature = "runtime-tokio", feature = "client", feature = "echo-app"))] +#[cfg(all( + feature = "async", + feature = "runtime-tokio", + feature = "client", + feature = "echo-app" +))] mod tokio_integration { use tendermint::abci::request; - use tendermint_abci::{Client, EchoApp, TokioClient, TokioServer}; + use tendermint_abci::runtime::Sender; + use tendermint_abci::{EchoApp, TokioClient, TokioServer}; #[tokio::test] async fn echo() { - let app = EchoApp::default(); - let (server, term_tx) = TokioServer::bind("127.0.0.1:0", app).await.unwrap(); - let server_addr = server.local_addr(); - let server_handle = tokio::spawn(async move { server.listen().await }); - - let mut client = TokioClient::connect(server_addr).await.unwrap(); let requests = (0..5) .map(|r| request::Echo { - message: format!("Request {}", r), + message: format!("echo {}", r), }) .collect::>(); - for request in &requests { - let res = client.echo(request.clone()).await.unwrap(); - assert_eq!(res.message, request.message); - } - - term_tx.send(()).await.unwrap(); - server_handle.await.unwrap().unwrap(); - } - - #[tokio::test] - async fn info() { - let app = EchoApp::new("Echo App", "0.0.1", 1); - let (server, term_tx) = TokioServer::bind("127.0.0.1:0", app).await.unwrap(); + let (server, term_tx) = TokioServer::bind("127.0.0.1:0", EchoApp::default()) + .await + .unwrap(); let server_addr = server.local_addr(); let server_handle = tokio::spawn(async move { server.listen().await }); let mut client = TokioClient::connect(server_addr).await.unwrap(); - let response = client.info(request::Info::default()).await.unwrap(); - assert_eq!(response.data, "Echo App"); - assert_eq!(response.version, "0.0.1"); - assert_eq!(response.app_version, 1); + for req in requests { + let res = client.echo(req.clone()).await.unwrap(); + assert_eq!(res.message, req.message); + } term_tx.send(()).await.unwrap(); server_handle.await.unwrap().unwrap(); From 4383f267503e44d8b5805309a6069b9ffa6269ab Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sat, 23 Jan 2021 17:42:42 -0500 Subject: [PATCH 18/29] Restructure errors and remove redundant ones Signed-off-by: Thane Thomson --- abci/src/result.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/abci/src/result.rs b/abci/src/result.rs index eb3699943..745e87e1c 100644 --- a/abci/src/result.rs +++ b/abci/src/result.rs @@ -8,30 +8,24 @@ pub type Result = std::result::Result; /// The various errors produced by the ABCI crate. #[derive(Debug, Error)] pub enum Error { + #[error("Tendermint error")] + TendermintError(#[from] tendermint::Error), + #[error("protocol buffers error")] Protobuf(#[from] tendermint_proto::Error), #[error("network I/O error")] NetworkIo(#[from] std::io::Error), - #[cfg(any(feature = "runtime-tokio", feature = "runtime-async-std"))] #[error("channel send error: {0}")] ChannelSend(String), - #[cfg(feature = "runtime-tokio")] - #[error("failed to obtain UNIX stream path")] - CannotObtainUnixStreamPath, - - #[error("Tendermint error")] - TendermintError(#[from] tendermint::Error), - - #[error("server stream terminated unexpectedly")] - ServerStreamTerminated, + #[error("failed to receive message from channel: {0}")] + ChannelRecv(String), #[error("sending end of channel closed unexpectedly")] ChannelSenderClosed, - #[cfg(feature = "runtime-async-std")] - #[error("failed to receive message from channel: {0}")] - ChannelRecv(String), + #[error("server stream terminated unexpectedly")] + ServerStreamTerminated, } From 6b6d7d5a309626d607c0ed4acf6d1e046df35b8a Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sat, 23 Jan 2021 17:43:03 -0500 Subject: [PATCH 19/29] Make async feature compulsory for Tokio/async-std-related features Signed-off-by: Thane Thomson --- abci/src/lib.rs | 8 ++++---- abci/src/runtime.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 4c298a391..5bc0af4a8 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -18,9 +18,9 @@ mod client; #[cfg(feature = "client")] pub use client::Client; -#[cfg(all(feature = "client", feature = "runtime-tokio"))] +#[cfg(all(feature = "async", feature = "client", feature = "runtime-tokio"))] pub type TokioClient = Client; -#[cfg(all(feature = "client", feature = "runtime-async-std"))] +#[cfg(all(feature = "async", feature = "client", feature = "runtime-async-std"))] pub type AsyncStdClient = Client; // Example applications @@ -32,7 +32,7 @@ pub use application::Application; pub use result::{Error, Result}; pub use server::Server; -#[cfg(feature = "runtime-tokio")] +#[cfg(all(feature = "async", feature = "runtime-tokio"))] pub type TokioServer = Server; -#[cfg(feature = "runtime-async-std")] +#[cfg(all(feature = "async", feature = "runtime-async-std"))] pub type AsyncStdServer = Server; diff --git a/abci/src/runtime.rs b/abci/src/runtime.rs index 98e35c96e..c0753bdc7 100644 --- a/abci/src/runtime.rs +++ b/abci/src/runtime.rs @@ -1,8 +1,8 @@ //! Abstractions for facilitating runtime-independent code. -#[cfg(feature = "runtime-async-std")] +#[cfg(all(feature = "async", feature = "runtime-async-std"))] pub mod async_std; -#[cfg(feature = "runtime-tokio")] +#[cfg(all(feature = "async", feature = "runtime-tokio"))] pub mod tokio; pub use crate::runtime::interface::{ From 814ebd364a76e9f7a4f8c269687611938f0bf974 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sat, 23 Jan 2021 17:43:46 -0500 Subject: [PATCH 20/29] Reorganize runtime-specific exports Signed-off-by: Thane Thomson --- abci/src/lib.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 5bc0af4a8..e36c4f22c 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -18,11 +18,6 @@ mod client; #[cfg(feature = "client")] pub use client::Client; -#[cfg(all(feature = "async", feature = "client", feature = "runtime-tokio"))] -pub type TokioClient = Client; -#[cfg(all(feature = "async", feature = "client", feature = "runtime-async-std"))] -pub type AsyncStdClient = Client; - // Example applications #[cfg(feature = "echo-app")] pub use application::echo::EchoApp; @@ -32,6 +27,12 @@ pub use application::Application; pub use result::{Error, Result}; pub use server::Server; +// Runtime-specific exports +#[cfg(all(feature = "async", feature = "client", feature = "runtime-tokio"))] +pub type TokioClient = Client; +#[cfg(all(feature = "async", feature = "client", feature = "runtime-async-std"))] +pub type AsyncStdClient = Client; + #[cfg(all(feature = "async", feature = "runtime-tokio"))] pub type TokioServer = Server; #[cfg(all(feature = "async", feature = "runtime-async-std"))] From 104bae7c12fe27398253e66cf463eb68d5ef403b Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sat, 23 Jan 2021 19:02:26 -0500 Subject: [PATCH 21/29] Structure appropriately for simple std/multi-threaded runtime Signed-off-by: Thane Thomson --- abci/src/client.rs | 33 ++++++ abci/src/codec.rs | 3 - abci/src/lib.rs | 7 +- abci/src/runtime.rs | 70 +++++++++++-- abci/src/runtime/std.rs | 225 ++++++++++++++++++++++++++++++++++++++++ abci/src/server.rs | 75 +++++++++++++- abci/tests/std.rs | 30 ++++++ 7 files changed, 432 insertions(+), 11 deletions(-) create mode 100644 abci/src/runtime/std.rs create mode 100644 abci/tests/std.rs diff --git a/abci/src/client.rs b/abci/src/client.rs index 50a1f2b6b..f2e3d6ef6 100644 --- a/abci/src/client.rs +++ b/abci/src/client.rs @@ -46,3 +46,36 @@ impl Client { Ok(res?) } } + +#[cfg(not(feature = "async"))] +impl Client { + pub fn connect>(addr: S) -> Result { + let stream = Rt::TcpStream::connect(addr.as_ref())?; + Ok(Self { + codec: Rt::ClientCodec::from_tcp_stream(stream), + }) + } + + /// Request that the ABCI server echo back the message in the given + /// request. + pub fn echo(&mut self, req: request::Echo) -> Result { + self.perform(req) + } + + /// Provide information to the ABCI server about the Tendermint node in + /// exchange for information about the application. + pub fn info(&mut self, req: request::Info) -> Result { + self.perform(req) + } + + fn perform(&mut self, req: Req) -> Result { + self.codec.send(req.into())?; + match self.codec.next() { + Some(result) => { + let res = result?; + Ok(res.try_into()?) + } + None => Err(Error::ServerStreamTerminated), + } + } +} diff --git a/abci/src/codec.rs b/abci/src/codec.rs index edd40d1e4..748f7e591 100644 --- a/abci/src/codec.rs +++ b/abci/src/codec.rs @@ -16,7 +16,6 @@ pub struct TspEncoder {} impl TspEncoder { /// Encode the given request to its raw wire-level representation and store /// this in the given buffer. - #[cfg(feature = "client")] pub fn encode_request(request: Request, mut dst: &mut BytesMut) -> Result<()> { encode_length_delimited(|mut b| Ok(request.encode(&mut b)?), &mut dst) } @@ -54,7 +53,6 @@ impl TspDecoder { /// /// Returns `Ok(None)` if we don't yet have enough data to decode a full /// response. - #[cfg(feature = "client")] pub fn decode_response(&mut self, buf: &mut BytesMut) -> Result> { self.read_buf.put(buf); decode_length_delimited(&mut self.read_buf, |mut b| Ok(Response::decode(&mut b)?)) @@ -120,7 +118,6 @@ where } } -#[cfg(feature = "client")] #[cfg(test)] mod test { use super::*; diff --git a/abci/src/lib.rs b/abci/src/lib.rs index e36c4f22c..284b77183 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -4,7 +4,8 @@ mod application; #[cfg(any( feature = "client", feature = "runtime-tokio", - feature = "runtime-async-std" + feature = "runtime-async-std", + feature = "runtime-std", ))] mod codec; mod result; @@ -32,8 +33,12 @@ pub use server::Server; pub type TokioClient = Client; #[cfg(all(feature = "async", feature = "client", feature = "runtime-async-std"))] pub type AsyncStdClient = Client; +#[cfg(all(not(feature = "async"), feature = "client", feature = "runtime-std"))] +pub type StdClient = Client; #[cfg(all(feature = "async", feature = "runtime-tokio"))] pub type TokioServer = Server; #[cfg(all(feature = "async", feature = "runtime-async-std"))] pub type AsyncStdServer = Server; +#[cfg(all(not(feature = "async"), feature = "runtime-std"))] +pub type StdServer = Server; diff --git a/abci/src/runtime.rs b/abci/src/runtime.rs index c0753bdc7..63f3bbd9c 100644 --- a/abci/src/runtime.rs +++ b/abci/src/runtime.rs @@ -2,6 +2,8 @@ #[cfg(all(feature = "async", feature = "runtime-async-std"))] pub mod async_std; +#[cfg(all(not(feature = "async"), feature = "runtime-std"))] +pub mod std; #[cfg(all(feature = "async", feature = "runtime-tokio"))] pub mod tokio; @@ -30,6 +32,11 @@ mod interface { use std::net::SocketAddr; use tendermint::abci::{request, response}; + #[async_trait] + pub trait TcpStream: Sized + Send { + async fn connect(addr: &str) -> Result; + } + #[async_trait] pub trait TcpListener: Sized { /// Bind this listener to the given address. @@ -42,11 +49,6 @@ mod interface { async fn accept(&self) -> Result<(T, SocketAddr)>; } - #[async_trait] - pub trait TcpStream: Sized + Send { - async fn connect(addr: &str) -> Result; - } - pub trait TaskSpawner { /// Spawn an asynchronous task without caring about its result. fn spawn_and_forget(task: T) @@ -97,5 +99,61 @@ mod interface { #[cfg(not(feature = "async"))] mod interface { - pub trait Server {} + use crate::Result; + use std::net::SocketAddr; + use tendermint::abci::{request, response}; + + pub trait TcpStream: Sized + Send { + fn connect(addr: &str) -> Result; + } + + pub trait TcpListener: Sized { + /// Bind this listener to the given address. + fn bind(addr: &str) -> Result; + + /// Returns the string representation of this listener's local address. + fn local_addr(&self) -> Result; + + /// Attempt to accept an incoming connection. + fn accept(&self) -> Result<(T, SocketAddr)>; + } + + pub trait TaskSpawner { + /// Spawn a task in a separate thread without caring about its result. + fn spawn_and_forget(task: T) + where + T: FnOnce() + Send + 'static, + T::Output: Send; + } + + pub trait ServerCodec: Iterator> { + fn from_tcp_stream(stream: S) -> Self; + + fn send(&mut self, res: response::Response) -> Result<()>; + } + + pub trait ClientCodec: Iterator> { + fn from_tcp_stream(stream: S) -> Self; + + fn send(&mut self, req: request::Request) -> Result<()>; + } + + /// The sending end of a channel. + pub trait Sender { + fn send(&self, value: T) -> Result<()>; + } + + /// The receiving end of a channel. + pub trait Receiver { + fn recv(&self) -> Result; + } + + /// A simple notification channel. + pub trait ChannelNotify { + type Sender: Sender<()>; + type Receiver: Receiver<()>; + + /// Construct an unbounded channel. + fn unbounded() -> (Self::Sender, Self::Receiver); + } } diff --git a/abci/src/runtime/std.rs b/abci/src/runtime/std.rs new file mode 100644 index 000000000..0dff1fc4a --- /dev/null +++ b/abci/src/runtime/std.rs @@ -0,0 +1,225 @@ +//! `std` runtime-specific types. + +use crate::codec::{TspDecoder, TspEncoder}; +use crate::runtime::{ + ChannelNotify, ClientCodec, Receiver, Runtime, Sender, ServerCodec, TaskSpawner, TcpListener, + TcpStream, +}; +use crate::{Error, Result}; +use bytes::{Buf, BytesMut}; +use std::io::{Read, Write}; +use std::net::SocketAddr; +use tendermint::abci::request::Request; +use tendermint::abci::response::Response; +use tendermint::abci::{request, response}; + +const CODEC_READ_BUF_SIZE: usize = 128; + +/// The built-in Rust standard library is our runtime. +pub struct Std; + +impl Runtime for Std { + type TcpStream = StdTcpStream; + type TcpListener = StdTcpListener; + type TaskSpawner = StdTaskSpawner; + type ServerCodec = StdServerCodec; + type ClientCodec = StdClientCodec; + type ChannelNotify = StdChannelNotify; +} + +pub struct StdTcpStream(std::net::TcpStream); + +impl TcpStream for StdTcpStream { + fn connect(addr: &str) -> Result { + Ok(Self(std::net::TcpStream::connect(addr)?)) + } +} + +pub struct StdTcpListener(std::net::TcpListener); + +impl TcpListener for StdTcpListener { + fn bind(addr: &str) -> Result { + Ok(Self(std::net::TcpListener::bind(addr)?)) + } + + fn local_addr(&self) -> Result { + Ok(self.0.local_addr()?.to_string()) + } + + fn accept(&self) -> Result<(StdTcpStream, SocketAddr)> { + let (stream, addr) = self.0.accept()?; + Ok((StdTcpStream(stream), addr)) + } +} + +pub struct StdTaskSpawner; + +impl TaskSpawner for StdTaskSpawner { + fn spawn_and_forget(task: T) + where + T: FnOnce() + Send + 'static, + T::Output: Send, + { + let _ = std::thread::spawn(move || { + task(); + }); + } +} + +pub struct StdServerCodec { + stream: std::net::TcpStream, + read_buf: BytesMut, + write_buf: BytesMut, + decoder: TspDecoder, +} + +impl ServerCodec for StdServerCodec { + fn from_tcp_stream(stream: StdTcpStream) -> Self { + Self { + stream: stream.0, + read_buf: BytesMut::new(), + write_buf: BytesMut::new(), + decoder: TspDecoder::new(), + } + } + + fn send(&mut self, res: Response) -> Result<()> { + TspEncoder::encode_response(res, &mut self.write_buf)?; + let bytes_written = self.stream.write(self.write_buf.as_ref())?; + if bytes_written == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "failed to write response", + ) + .into()); + } + self.write_buf.advance(bytes_written); + Ok(self.stream.flush()?) + } +} + +impl Iterator for StdServerCodec { + type Item = Result; + + fn next(&mut self) -> Option { + let mut tmp_read_buf = [0_u8; CODEC_READ_BUF_SIZE]; + + loop { + // Try to decode a request from our internal read buffer first + match self.decoder.decode_request(&mut self.read_buf) { + Ok(req_opt) => { + if let Some(req) = req_opt { + return Some(Ok(req)); + } + } + Err(e) => return Some(Err(e)), + } + + // If we don't have enough data to decode a message, try to read + // more + let bytes_read = match self.stream.read(&mut tmp_read_buf) { + Ok(br) => br, + Err(e) => return Some(Err(e.into())), + }; + if bytes_read == 0 { + // The stream terminated + return None; + } + self.read_buf.extend_from_slice(&tmp_read_buf[..bytes_read]); + } + } +} + +pub struct StdClientCodec { + stream: std::net::TcpStream, + read_buf: BytesMut, + write_buf: BytesMut, + decoder: TspDecoder, +} + +impl ClientCodec for StdClientCodec { + fn from_tcp_stream(stream: StdTcpStream) -> Self { + Self { + stream: stream.0, + read_buf: BytesMut::new(), + write_buf: BytesMut::new(), + decoder: TspDecoder::new(), + } + } + + fn send(&mut self, req: Request) -> Result<()> { + TspEncoder::encode_request(req, &mut self.write_buf)?; + let bytes_written = self.stream.write(self.write_buf.as_ref())?; + if bytes_written == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "failed to write request", + ) + .into()); + } + self.write_buf.advance(bytes_written); + Ok(self.stream.flush()?) + } +} + +impl Iterator for StdClientCodec { + type Item = Result; + + fn next(&mut self) -> Option { + let mut tmp_read_buf = [0_u8; CODEC_READ_BUF_SIZE]; + + loop { + // Try to decode a response from our internal read buffer first + match self.decoder.decode_response(&mut self.read_buf) { + Ok(res_opt) => { + if let Some(res) = res_opt { + return Some(Ok(res)); + } + } + Err(e) => return Some(Err(e)), + } + + // If we don't have enough data to decode a message, try to read + // more + let bytes_read = match self.stream.read(&mut tmp_read_buf) { + Ok(br) => br, + Err(e) => return Some(Err(e.into())), + }; + if bytes_read == 0 { + // The stream terminated + return None; + } + self.read_buf.extend_from_slice(&tmp_read_buf[..bytes_read]); + } + } +} + +pub struct StdChannelNotify; + +impl ChannelNotify for StdChannelNotify { + type Sender = StdSender<()>; + type Receiver = StdReceiver<()>; + + fn unbounded() -> (Self::Sender, Self::Receiver) { + let (tx, rx) = std::sync::mpsc::channel(); + (StdSender(tx), StdReceiver(rx)) + } +} + +pub struct StdSender(std::sync::mpsc::Sender); + +impl Sender for StdSender { + fn send(&self, value: T) -> Result<()> { + self.0 + .send(value) + .map_err(|e| Error::ChannelSend(e.to_string())) + } +} + +pub struct StdReceiver(std::sync::mpsc::Receiver); + +impl Receiver for StdReceiver { + fn recv(&self) -> Result { + self.0.recv().map_err(|e| Error::ChannelRecv(e.to_string())) + } +} diff --git a/abci/src/server.rs b/abci/src/server.rs index 8e9affde3..80cd22f51 100644 --- a/abci/src/server.rs +++ b/abci/src/server.rs @@ -1,6 +1,8 @@ //! ABCI servers. -use crate::runtime::{ChannelNotify, Receiver, Runtime, ServerCodec, TaskSpawner, TcpListener}; +#[cfg(feature = "async")] +use crate::runtime::{ChannelNotify, Receiver}; +use crate::runtime::{Runtime, ServerCodec, TaskSpawner, TcpListener}; use crate::{Application, Result}; use log::{debug, error, info}; use tendermint::abci::request; @@ -10,6 +12,7 @@ pub struct Server { app: App, listener: Rt::TcpListener, local_addr: String, + #[cfg(feature = "async")] term_rx: ::Receiver, } @@ -102,3 +105,73 @@ where } } } + +#[cfg(not(feature = "async"))] +impl Server +where + App: Application, + Rt: Runtime, +{ + /// Bind our ABCI application server to the given address. + pub fn bind>(addr: S, app: App) -> Result { + let listener = Rt::TcpListener::bind(addr.as_ref())?; + let local_addr = listener.local_addr()?; + Ok(Self { + app, + listener, + local_addr, + }) + } + + /// Get the local address for the server, once bound. + pub fn local_addr(&self) -> String { + self.local_addr.clone() + } + + /// Start listening for incoming connections. + pub fn listen(self) -> Result<()> { + loop { + match self.listener.accept() { + Ok(r) => { + let (stream, addr) = r; + info!("Incoming connection from: {}", addr.to_string()); + self.spawn_client_handler(stream); + } + Err(e) => { + error!("Failed to accept incoming connection: {:?}", e); + } + } + } + } + + fn spawn_client_handler(&self, stream: Rt::TcpStream) { + let app_clone = self.app.clone(); + Rt::TaskSpawner::spawn_and_forget(move || Self::handle_client(stream, app_clone)); + } + + fn handle_client(stream: Rt::TcpStream, app: App) { + let mut codec = Rt::ServerCodec::from_tcp_stream(stream); + loop { + let req: request::Request = match codec.next() { + Some(result) => match result { + Ok(r) => r, + Err(e) => { + error!("Failed to read request from client: {}", e); + return; + } + }, + None => { + info!("Client terminated connection"); + return; + } + }; + debug!("Got incoming request from client: {:?}", req); + let res = app.handle(req); + debug!("Sending outgoing response: {:?}", res); + if let Err(e) = codec.send(res) { + error!("Failed to write outgoing response to client: {:?}", e); + return; + } + } + } +} diff --git a/abci/tests/std.rs b/abci/tests/std.rs new file mode 100644 index 000000000..a28201c06 --- /dev/null +++ b/abci/tests/std.rs @@ -0,0 +1,30 @@ +//! Tokio-based ABCI client/server integration tests. + +#[cfg(all( + not(feature = "async"), + feature = "runtime-std", + feature = "client", + feature = "echo-app" +))] +mod std_integration { + use tendermint::abci::request; + use tendermint_abci::{EchoApp, StdClient, StdServer}; + + #[test] + fn echo() { + let requests = (0..5) + .map(|r| request::Echo { + message: format!("echo {}", r), + }) + .collect::>(); + let server = StdServer::bind("127.0.0.1:0", EchoApp::default()).unwrap(); + let server_addr = server.local_addr(); + let _ = std::thread::spawn(move || server.listen()); + + let mut client = StdClient::connect(server_addr).unwrap(); + for req in requests { + let res = client.echo(req.clone()).unwrap(); + assert_eq!(res.message, req.message); + } + } +} From 1d8624694eb76757fa6093a2ff6c353bcd3990e3 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sat, 23 Jan 2021 19:34:10 -0500 Subject: [PATCH 22/29] Ignore tests if async-std runtime is not enabled Signed-off-by: Thane Thomson --- abci/src/runtime/async_std.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/abci/src/runtime/async_std.rs b/abci/src/runtime/async_std.rs index ba538362b..8d56ef8b1 100644 --- a/abci/src/runtime/async_std.rs +++ b/abci/src/runtime/async_std.rs @@ -323,6 +323,7 @@ impl Receiver for AsyncStdReceiver { } } +#[cfg(all(feature = "async", feature = "runtime-async-std"))] #[cfg(test)] mod test { use super::*; From f3adbbe44879db70343c0c5f16ac8ab8bd124db6 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sat, 23 Jan 2021 19:34:20 -0500 Subject: [PATCH 23/29] Add rudimentary README Signed-off-by: Thane Thomson --- abci/README.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 abci/README.md diff --git a/abci/README.md b/abci/README.md new file mode 100644 index 000000000..835909a27 --- /dev/null +++ b/abci/README.md @@ -0,0 +1,90 @@ +## tendermint-abci + +[![Crate][crate-image]][crate-link] +[![Docs][docs-image]][docs-link] +[![Build Status][build-image]][build-link] +[![Audit Status][audit-image]][audit-link] +[![Apache 2.0 Licensed][license-image]][license-link] +![Rust Stable][rustc-image] + +**NB: Currently heavily under construction** + +Crate for creating ABCI applications for use with Tendermint. See the +[Tendermint ABCI docs][abci-docs] for more details on ABCI. + +## Requirements + +- The latest stable version of Rust + +## Features + +This crate exposes an interface that allows for the construction of ABCI +applications that run using the Rust standard library (i.e. simple +multi-threaded applications), as well as using various `async` runtimes. + +Currently, in terms of `async` runtimes, we support [Tokio] and [`async-std`]. + +These are all enabled/disabled by way of feature flags. + +## Testing + +To run integration tests for the Rust standard library (non-`async`, +multi-threaded), run: + +```bash +cargo test --features client,runtime-std,echo-app +``` + +To run all integration tests for all supported `async` runtimes, run: + +```bash +cargo test --all-features +``` + +To run Tokio-specific integration tests only, run: + +```bash +cargo test --features async,client,runtime-tokio,echo-app +``` + +To run `async-std`-specific integration tests only, run: + +```bash +cargo test --features async,client,runtime-async-std,echo-app +``` + +## License + +Copyright © 2020 Informal Systems + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use the files in this repository except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +[//]: # (badges) + +[crate-image]: https://img.shields.io/crates/v/tendermint-abci.svg +[crate-link]: https://crates.io/crates/tendermint-abci +[docs-image]: https://docs.rs/tendermint-abci/badge.svg +[docs-link]: https://docs.rs/tendermint-abci/ +[build-image]: https://github.com/informalsystems/tendermint-rs/workflows/Rust/badge.svg +[build-link]: https://github.com/informalsystems/tendermint-rs/actions?query=workflow%3ARust +[audit-image]: https://github.com/informalsystems/tendermint-rs/workflows/Audit-Check/badge.svg +[audit-link]: https://github.com/informalsystems/tendermint-rs/actions?query=workflow%3AAudit-Check +[license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg +[license-link]: https://github.com/informalsystems/tendermint-rs/blob/master/LICENSE +[rustc-image]: https://img.shields.io/badge/rustc-stable-blue.svg + +[//]: # (general links) + +[abci-docs]: https://docs.tendermint.com/master/spec/abci/ +[Tokio]: https://tokio.rs/ +[`async-std`]: https://async.rs/ From 1713d47c26b7565b293084291753e20c28d53a23 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Sat, 23 Jan 2021 19:45:29 -0500 Subject: [PATCH 24/29] Fix comment in integration test Signed-off-by: Thane Thomson --- abci/tests/std.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abci/tests/std.rs b/abci/tests/std.rs index a28201c06..97c12255c 100644 --- a/abci/tests/std.rs +++ b/abci/tests/std.rs @@ -1,4 +1,4 @@ -//! Tokio-based ABCI client/server integration tests. +//! Rust standard library-based ABCI client/server integration tests. #[cfg(all( not(feature = "async"), From e7b2f9cf439cff3c8396e91f575d86a635888028 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 25 Jan 2021 12:15:28 -0500 Subject: [PATCH 25/29] Separate out runtime-dependent from runtime-independent code Signed-off-by: Thane Thomson --- abci/src/server.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/abci/src/server.rs b/abci/src/server.rs index 80cd22f51..abf39766c 100644 --- a/abci/src/server.rs +++ b/abci/src/server.rs @@ -16,6 +16,14 @@ pub struct Server { term_rx: ::Receiver, } +// Code common to both the async and non-async runtimes. +impl Server { + /// Get the local address for the server, once bound. + pub fn local_addr(&self) -> String { + self.local_addr.clone() + } +} + #[cfg(feature = "async")] impl Server where @@ -44,11 +52,6 @@ where )) } - /// Get the local address for the server, once bound. - pub fn local_addr(&self) -> String { - self.local_addr.clone() - } - /// Start listening for incoming connections. pub async fn listen(mut self) -> Result<()> { use futures::FutureExt; @@ -123,11 +126,6 @@ where }) } - /// Get the local address for the server, once bound. - pub fn local_addr(&self) -> String { - self.local_addr.clone() - } - /// Start listening for incoming connections. pub fn listen(self) -> Result<()> { loop { From c67c718914df9c86229154fee041bb5e43c8fcee Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 25 Jan 2021 12:15:39 -0500 Subject: [PATCH 26/29] Add cargo make Makefile Signed-off-by: Thane Thomson --- abci/Makefile.toml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 abci/Makefile.toml diff --git a/abci/Makefile.toml b/abci/Makefile.toml new file mode 100644 index 000000000..3b00c6654 --- /dev/null +++ b/abci/Makefile.toml @@ -0,0 +1,22 @@ +# tendermint-abci makefile +# +# For use with https://github.com/sagiegurari/cargo-make + +[tasks.test-std] +command = "cargo" +args = ["test", "--features", "client,runtime-std,echo-app"] + +[tasks.test-async-std] +command = "cargo" +args = ["test", "--features", "async,client,runtime-async-std,echo-app"] + +[tasks.test-tokio] +command = "cargo" +args = ["test", "--features", "async,client,runtime-tokio,echo-app"] + +[tasks.test] +dependencies = [ + "test-std", + "test-async-std", + "test-tokio" +] From d7c154068f6264cad67e192e7de5012c2834df95 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 25 Jan 2021 15:34:02 -0500 Subject: [PATCH 27/29] Add crate description Signed-off-by: Thane Thomson --- abci/Cargo.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/abci/Cargo.toml b/abci/Cargo.toml index 030293982..db2f84a79 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -3,8 +3,10 @@ name = "tendermint-abci" version = "0.17.1" authors = ["Thane Thomson "] edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +description = """ + tendermint-abci provides a framework for building Tendermint applications + in Rust. + """ [features] async = [ "async-trait", "futures", "pin-project" ] From 746fdbf13c401159a72c11be138877e576ac8468 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 25 Jan 2021 15:34:25 -0500 Subject: [PATCH 28/29] Add comment about which interface is exposed through the async feature flag Signed-off-by: Thane Thomson --- abci/src/runtime.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/abci/src/runtime.rs b/abci/src/runtime.rs index 63f3bbd9c..f00cb8fd5 100644 --- a/abci/src/runtime.rs +++ b/abci/src/runtime.rs @@ -7,6 +7,8 @@ pub mod std; #[cfg(all(feature = "async", feature = "runtime-tokio"))] pub mod tokio; +// The runtime interface included here depends on whether the `async` feature +// flag is enabled or not. pub use crate::runtime::interface::{ ChannelNotify, ClientCodec, Receiver, Sender, ServerCodec, TaskSpawner, TcpListener, TcpStream, }; From beddc043d77bc2cf2e80c3872a763527ecbd26db Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 27 Jan 2021 20:16:00 -0500 Subject: [PATCH 29/29] Refactor to allow for simultaneous inclusion of both blocking and non-blocking code Signed-off-by: Thane Thomson --- abci/Cargo.toml | 3 +- abci/src/client.rs | 87 +--- abci/src/client/blocking.rs | 81 ++++ abci/src/client/non_blocking.rs | 85 ++++ abci/src/codec.rs | 108 +++-- abci/src/codec/blocking.rs | 120 ++++++ abci/src/codec/non_blocking.rs | 161 ++++++++ abci/src/lib.rs | 51 +-- abci/src/runtime.rs | 163 +------- abci/src/runtime/async_std.rs | 384 ------------------ abci/src/runtime/blocking.rs | 64 +++ abci/src/runtime/blocking/runtime_std.rs | 59 +++ abci/src/runtime/non_blocking.rs | 79 ++++ .../runtime/non_blocking/runtime_async_std.rs | 97 +++++ .../src/runtime/non_blocking/runtime_tokio.rs | 135 ++++++ abci/src/runtime/std.rs | 225 ---------- abci/src/runtime/tokio.rs | 232 ----------- abci/src/server.rs | 181 +-------- abci/src/server/blocking.rs | 127 ++++++ abci/src/server/non_blocking.rs | 152 +++++++ abci/tests/async_std.rs | 14 +- abci/tests/std.rs | 10 +- abci/tests/tokio.rs | 14 +- 23 files changed, 1306 insertions(+), 1326 deletions(-) create mode 100644 abci/src/client/blocking.rs create mode 100644 abci/src/client/non_blocking.rs create mode 100644 abci/src/codec/blocking.rs create mode 100644 abci/src/codec/non_blocking.rs delete mode 100644 abci/src/runtime/async_std.rs create mode 100644 abci/src/runtime/blocking.rs create mode 100644 abci/src/runtime/blocking/runtime_std.rs create mode 100644 abci/src/runtime/non_blocking.rs create mode 100644 abci/src/runtime/non_blocking/runtime_async_std.rs create mode 100644 abci/src/runtime/non_blocking/runtime_tokio.rs delete mode 100644 abci/src/runtime/std.rs delete mode 100644 abci/src/runtime/tokio.rs create mode 100644 abci/src/server/blocking.rs create mode 100644 abci/src/server/non_blocking.rs diff --git a/abci/Cargo.toml b/abci/Cargo.toml index db2f84a79..afb05590e 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -9,9 +9,10 @@ description = """ """ [features] -async = [ "async-trait", "futures", "pin-project" ] +blocking = [] client = [] echo-app = [] +non-blocking = [ "async-trait", "futures", "pin-project" ] runtime-async-std = [ "async-channel", "async-std" ] runtime-std = [] runtime-tokio = [ "tokio", "tokio-util" ] diff --git a/abci/src/client.rs b/abci/src/client.rs index f2e3d6ef6..00e47652d 100644 --- a/abci/src/client.rs +++ b/abci/src/client.rs @@ -1,81 +1,10 @@ //! ABCI clients for interacting with ABCI servers. -use crate::runtime::{ClientCodec, Runtime, TcpStream}; -use crate::{Error, Result}; -use std::convert::TryInto; -use tendermint::abci::request::RequestInner; -use tendermint::abci::{request, response}; - -/// A runtime-dependent ABCI client. -pub struct Client { - codec: Rt::ClientCodec, -} - -#[cfg(feature = "async")] -impl Client { - /// Connect to the ABCI server at the given network address. - pub async fn connect>(addr: S) -> Result { - let stream = Rt::TcpStream::connect(addr.as_ref()).await?; - Ok(Self { - codec: Rt::ClientCodec::from_tcp_stream(stream), - }) - } - - /// Request that the ABCI server echo back the message in the given - /// request. - pub async fn echo(&mut self, req: request::Echo) -> Result { - self.perform(req).await - } - - /// Provide information to the ABCI server about the Tendermint node in - /// exchange for information about the application. - pub async fn info(&mut self, req: request::Info) -> Result { - self.perform(req).await - } - - async fn perform(&mut self, req: Req) -> Result { - use futures::{SinkExt, StreamExt}; - - self.codec.send(req.into()).await?; - let res: std::result::Result = self - .codec - .next() - .await - .ok_or(Error::ServerStreamTerminated)?? - .try_into(); - Ok(res?) - } -} - -#[cfg(not(feature = "async"))] -impl Client { - pub fn connect>(addr: S) -> Result { - let stream = Rt::TcpStream::connect(addr.as_ref())?; - Ok(Self { - codec: Rt::ClientCodec::from_tcp_stream(stream), - }) - } - - /// Request that the ABCI server echo back the message in the given - /// request. - pub fn echo(&mut self, req: request::Echo) -> Result { - self.perform(req) - } - - /// Provide information to the ABCI server about the Tendermint node in - /// exchange for information about the application. - pub fn info(&mut self, req: request::Info) -> Result { - self.perform(req) - } - - fn perform(&mut self, req: Req) -> Result { - self.codec.send(req.into())?; - match self.codec.next() { - Some(result) => { - let res = result?; - Ok(res.try_into()?) - } - None => Err(Error::ServerStreamTerminated), - } - } -} +#[cfg(feature = "blocking")] +pub mod blocking; +#[cfg(feature = "non-blocking")] +pub mod non_blocking; + +/// The default size of the ABCI client's read buffer when reading responses +/// from the server. +pub const DEFAULT_CLIENT_READ_BUF_SIZE: usize = 1024; diff --git a/abci/src/client/blocking.rs b/abci/src/client/blocking.rs new file mode 100644 index 000000000..71884336e --- /dev/null +++ b/abci/src/client/blocking.rs @@ -0,0 +1,81 @@ +//! Blocking ABCI client. + +use crate::client::DEFAULT_CLIENT_READ_BUF_SIZE; +use crate::codec::blocking::Codec; +use crate::runtime::blocking::{Runtime, TcpStream}; +use crate::{Error, Result}; +use std::convert::TryInto; +use tendermint::abci::request::RequestInner; +use tendermint::abci::{request, response}; + +/// A runtime-dependent blocking ABCI client. +pub struct Client { + codec: Rt::ClientCodec, +} + +impl Client { + /// Request that the ABCI server echo back the message in the given + /// request. + pub fn echo(&mut self, req: request::Echo) -> Result { + self.perform(req) + } + + /// Provide information to the ABCI server about the Tendermint node in + /// exchange for information about the application. + pub fn info(&mut self, req: request::Info) -> Result { + self.perform(req) + } + + fn perform(&mut self, req: Req) -> Result { + self.codec.send(req.into())?; + match self.codec.next() { + Some(result) => { + let res = result?; + Ok(res.try_into()?) + } + None => Err(Error::ServerStreamTerminated), + } + } +} + +/// Builder for a blocking ABCI client. +pub struct ClientBuilder { + read_buf_size: usize, + _runtime: std::marker::PhantomData, +} + +impl ClientBuilder { + /// Constructor allowing customization of the client's read buffer size. + pub fn new(read_buf_size: usize) -> Self { + Self { + read_buf_size, + _runtime: Default::default(), + } + } + + /// Constructor for our [`Client`] instance, which attempts to connect to + /// the given address. + pub fn connect(self, addr: S) -> Result> + where + S: AsRef, + { + let stream = Rt::TcpStream::connect(addr.as_ref())?; + Ok(Client { + codec: Rt::ClientCodec::new(stream.into_inner(), self.read_buf_size), + }) + } +} + +impl Default for ClientBuilder { + fn default() -> Self { + Self { + read_buf_size: DEFAULT_CLIENT_READ_BUF_SIZE, + _runtime: Default::default(), + } + } +} + +#[cfg(feature = "runtime-std")] +/// Blocking ABCI client builder when using Rust's standard library as your +/// runtime. +pub type StdClientBuilder = ClientBuilder; diff --git a/abci/src/client/non_blocking.rs b/abci/src/client/non_blocking.rs new file mode 100644 index 000000000..318953a11 --- /dev/null +++ b/abci/src/client/non_blocking.rs @@ -0,0 +1,85 @@ +//! Non-blocking ABCI client. + +use crate::client::DEFAULT_CLIENT_READ_BUF_SIZE; +use crate::codec::non_blocking::Codec; +use crate::runtime::non_blocking::{Runtime, TcpStream}; +use crate::{Error, Result}; +use futures::{SinkExt, StreamExt}; +use std::convert::TryInto; +use tendermint::abci::request::RequestInner; +use tendermint::abci::{request, response}; + +/// A runtime-dependent non-blocking ABCI client. +pub struct Client { + codec: Rt::ClientCodec, +} + +impl Client { + /// Request that the ABCI server echo back the message in the given + /// request. + pub async fn echo(&mut self, req: request::Echo) -> Result { + self.perform(req).await + } + + /// Provide information to the ABCI server about the Tendermint node in + /// exchange for information about the application. + pub async fn info(&mut self, req: request::Info) -> Result { + self.perform(req).await + } + + async fn perform(&mut self, req: Req) -> Result { + self.codec.send(req.into()).await?; + let res: std::result::Result = self + .codec + .next() + .await + .ok_or(Error::ServerStreamTerminated)?? + .try_into(); + Ok(res?) + } +} + +/// Builder for a non-blocking ABCI client. +pub struct ClientBuilder { + read_buf_size: usize, + _runtime: std::marker::PhantomData, +} + +impl ClientBuilder { + /// Constructor allowing configuration of read buffer size. + pub fn new(read_buf_size: usize) -> Self { + Self { + read_buf_size, + _runtime: Default::default(), + } + } + + /// Connect to the ABCI server at the given network address. + pub async fn connect(self, addr: S) -> Result> + where + S: AsRef, + { + let stream = Rt::TcpStream::connect(addr.as_ref()).await?; + Ok(Client { + codec: Rt::ClientCodec::new(stream.into_inner(), self.read_buf_size), + }) + } +} + +impl Default for ClientBuilder { + fn default() -> Self { + Self { + read_buf_size: DEFAULT_CLIENT_READ_BUF_SIZE, + _runtime: Default::default(), + } + } +} + +#[cfg(feature = "runtime-tokio")] +/// Non-blocking ABCI client builder when using Tokio as your runtime. +pub type TokioClientBuilder = ClientBuilder; + +#[cfg(feature = "runtime-async-std")] +/// Non-blocking ABCI client builder when using `async-std` as your runtime. +pub type AsyncStdClientBuilder = + ClientBuilder; diff --git a/abci/src/codec.rs b/abci/src/codec.rs index 748f7e591..bb00993f6 100644 --- a/abci/src/codec.rs +++ b/abci/src/codec.rs @@ -1,64 +1,112 @@ //! ABCI codec. +#[cfg(feature = "blocking")] +pub mod blocking; +#[cfg(feature = "non-blocking")] +pub mod non_blocking; + use crate::{Error, Result}; use bytes::{Buf, BufMut, BytesMut}; use tendermint::abci::request::Request; use tendermint::abci::response::Response; use tendermint_proto::Protobuf; +/// The size of the server's read buffer for incoming messages. +pub const SERVER_READ_BUF_SIZE: usize = 1024 * 1024; + +/// The size of the client's read buffer for incoming messages. +pub const CLIENT_READ_BUF_SIZE: usize = 1024; + // The maximum number of bytes we expect in a varint. We use this to check if // we're encountering a decoding error for a varint. const MAX_VARINT_LENGTH: usize = 16; -/// Tendermint Socket Protocol encoder. -pub struct TspEncoder {} +/// A stateless encoder of `T` into its wire-level representation. +pub trait Encoder { + fn encode(value: T, dst: &mut BytesMut) -> Result<()>; +} -impl TspEncoder { - /// Encode the given request to its raw wire-level representation and store - /// this in the given buffer. - pub fn encode_request(request: Request, mut dst: &mut BytesMut) -> Result<()> { - encode_length_delimited(|mut b| Ok(request.encode(&mut b)?), &mut dst) +/// Encodes [`tendermint::abci::Request`]s into their wire-level +/// representation as per the Tendermint Socket Protocol. +pub struct RequestEncoder; + +impl Encoder for RequestEncoder { + fn encode(value: Request, mut dst: &mut BytesMut) -> Result<()> { + encode_length_delimited(|mut b| Ok(value.encode(&mut b)?), &mut dst) } +} + +/// Encodes [`tendermint::abci::Response`]s into their wire-level +/// representation as per the Tendermint Socket Protocol. +pub struct ResponseEncoder; - /// Encode the given response to its raw wire-level representation and - /// store this in the given buffer. - pub fn encode_response(response: Response, mut dst: &mut BytesMut) -> Result<()> { - encode_length_delimited(|mut b| Ok(response.encode(&mut b)?), &mut dst) +impl Encoder for ResponseEncoder { + fn encode(value: Response, mut dst: &mut BytesMut) -> Result<()> { + encode_length_delimited(|mut b| Ok(value.encode(&mut b)?), &mut dst) } } -/// Tendermint Socket Protocol decoder. -pub struct TspDecoder { +/// A potentially stateful decoder of `T` from its wire-level representation. +pub trait Decoder { + fn decode(&mut self, buf: &mut BytesMut) -> Result>; +} + +/// Decodes [`tendermint::abci::Request`]s from their wire-level +/// representation as per the Tendermint Socket Protocol. +pub struct RequestDecoder { read_buf: BytesMut, } -impl TspDecoder { +impl RequestDecoder { /// Constructor. pub fn new() -> Self { Self { read_buf: BytesMut::new(), } } +} - /// Attempt to decode a request from the given buffer. - /// - /// Returns `Ok(None)` if we don't yet have enough data to decode a full - /// request. - pub fn decode_request(&mut self, buf: &mut BytesMut) -> Result> { +impl Decoder for RequestDecoder { + fn decode(&mut self, buf: &mut BytesMut) -> Result> { self.read_buf.put(buf); decode_length_delimited(&mut self.read_buf, |mut b| Ok(Request::decode(&mut b)?)) } +} + +impl Default for RequestDecoder { + fn default() -> Self { + Self::new() + } +} + +/// Decodes [`tendermint::abci::Response`]s from their wire-level +/// representation as per the Tendermint Socket Protocol. +pub struct ResponseDecoder { + read_buf: BytesMut, +} + +impl ResponseDecoder { + /// Constructor. + pub fn new() -> Self { + Self { + read_buf: BytesMut::new(), + } + } +} - /// Attempt to decode a response from the given buffer. - /// - /// Returns `Ok(None)` if we don't yet have enough data to decode a full - /// response. - pub fn decode_response(&mut self, buf: &mut BytesMut) -> Result> { +impl Decoder for ResponseDecoder { + fn decode(&mut self, buf: &mut BytesMut) -> Result> { self.read_buf.put(buf); decode_length_delimited(&mut self.read_buf, |mut b| Ok(Response::decode(&mut b)?)) } } +impl Default for ResponseDecoder { + fn default() -> Self { + Self::new() + } +} + // encode_varint and decode_varint will be removed once // https://github.com/tendermint/tendermint/issues/5783 lands in Tendermint. fn encode_varint(val: u64, mut buf: &mut B) { @@ -129,10 +177,10 @@ mod test { message: "Hello TSP!".to_owned(), }); let mut buf = BytesMut::new(); - TspEncoder::encode_request(request.clone(), &mut buf).unwrap(); + RequestEncoder::encode(request.clone(), &mut buf).unwrap(); - let mut decoder = TspDecoder::new(); - let decoded_request = decoder.decode_request(&mut buf).unwrap().unwrap(); + let mut decoder = RequestDecoder::new(); + let decoded_request = decoder.decode(&mut buf).unwrap().unwrap(); assert_eq!(request, decoded_request); } @@ -149,11 +197,11 @@ mod test { let mut buf = BytesMut::new(); requests .iter() - .for_each(|request| TspEncoder::encode_request(request.clone(), &mut buf).unwrap()); + .for_each(|request| RequestEncoder::encode(request.clone(), &mut buf).unwrap()); - let mut decoder = TspDecoder::new(); + let mut decoder = RequestDecoder::new(); for request in requests { - let decoded = decoder.decode_request(&mut buf).unwrap().unwrap(); + let decoded = decoder.decode(&mut buf).unwrap().unwrap(); assert_eq!(decoded, request); } } diff --git a/abci/src/codec/blocking.rs b/abci/src/codec/blocking.rs new file mode 100644 index 000000000..2c501fe7d --- /dev/null +++ b/abci/src/codec/blocking.rs @@ -0,0 +1,120 @@ +//! Blocking API for frame en/decoding. + +use crate::codec::{ + Decoder, Encoder, RequestDecoder, RequestEncoder, ResponseDecoder, ResponseEncoder, +}; +use crate::Result; +use bytes::{Buf, BytesMut}; +use std::io::{Read, Write}; +use std::marker::PhantomData; +use tendermint::abci::request::Request; +use tendermint::abci::response::Response; + +/// An ABCI server decodes incoming [`tendermint::abci::request::Request`]s +/// and encodes outgoing [`tendermint::abci::response::Response`]s. The stream +/// `S` must implement [`std::io::Read`] and [`std::io::Write`]. +pub type ServerCodec = CodecImpl; + +/// An ABCI client encodes outgoing [`tendermint::abci::request::Request`]s +/// and decodes incoming [`tendermint::abci::response::Response`]s. The stream +/// `S` must implement [`std::io::Read`] and [`std::io::Write`]. +pub type ClientCodec = CodecImpl; + +/// A blocking codec that allows us to iterate over `S`, producing values of +/// type `Result`, and send values of type `O`. +pub trait Codec: Iterator> { + /// Constructor. + fn new(inner: S, read_buf_size: usize) -> Self; + + /// Send the given value out using this codec. + fn send(&mut self, value: O) -> Result<()>; +} + +/// Blocking adapter to convert an underlying I/O stream, which implements +/// [`std::io::Read`] and [`std::io::Write`], into an [`Iterator`] producing +/// entities of type `I` and allowing for sending of entities of type `O`. +/// +/// The blocking iterator terminates once the underlying reader terminates. +pub struct CodecImpl { + inner: S, + _encoder: PhantomData, + _outgoing: PhantomData, + decoder: D, + _incoming: PhantomData, + growable_read_buf: BytesMut, + growable_write_buf: BytesMut, + read_buf: Vec, +} + +impl Iterator for CodecImpl +where + D: Decoder, + S: Read, +{ + type Item = Result; + + fn next(&mut self) -> Option { + loop { + // Try to decode a request from our internal read buffer first + match self.decoder.decode(&mut self.growable_read_buf) { + Ok(req_opt) => { + if let Some(req) = req_opt { + return Some(Ok(req)); + } + } + Err(e) => return Some(Err(e)), + } + + // If we don't have enough data to decode a message, try to read + // more + let bytes_read = match self.inner.read(self.read_buf.as_mut()) { + Ok(br) => br, + Err(e) => return Some(Err(e.into())), + }; + if bytes_read == 0 { + // The stream terminated + return None; + } + self.growable_read_buf + .extend_from_slice(&self.read_buf[..bytes_read]); + } + } +} + +impl Codec for CodecImpl +where + E: Encoder, + D: Decoder + Default, + S: Read + Write, +{ + fn new(inner: S, read_buf_size: usize) -> Self { + Self { + inner, + _encoder: Default::default(), + _outgoing: Default::default(), + decoder: Default::default(), + _incoming: Default::default(), + growable_read_buf: BytesMut::new(), + growable_write_buf: BytesMut::new(), + read_buf: vec![0_u8; read_buf_size], + } + } + + fn send(&mut self, value: O) -> Result<()> { + E::encode(value, &mut self.growable_write_buf)?; + loop { + let bytes_written = self.inner.write(self.growable_write_buf.as_ref())?; + if bytes_written == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "failed to write to underlying writer", + ) + .into()); + } + self.growable_write_buf.advance(bytes_written); + if self.growable_write_buf.is_empty() { + return Ok(self.inner.flush()?); + } + } + } +} diff --git a/abci/src/codec/non_blocking.rs b/abci/src/codec/non_blocking.rs new file mode 100644 index 000000000..534a5a204 --- /dev/null +++ b/abci/src/codec/non_blocking.rs @@ -0,0 +1,161 @@ +//! `async` (futures)-compatible API for frame en/decoding. + +use crate::codec::{ + Decoder, Encoder, RequestDecoder, RequestEncoder, ResponseDecoder, ResponseEncoder, +}; +use crate::{Error, Result}; +use bytes::{Buf, BytesMut}; +use futures::task::{Context, Poll}; +use futures::{ready, AsyncRead, AsyncWrite, Sink, Stream}; +use pin_project::pin_project; +use std::marker::PhantomData; +use std::pin::Pin; +use tendermint::abci::request::Request; +use tendermint::abci::response::Response; + +/// An ABCI server decodes incoming [`tendermint::abci::request::Request`]s +/// and encodes outgoing [`tendermint::abci::response::Response`]s. The stream +/// `S` must implement [`futures::AsyncRead`] and [`futures::AsyncWrite`]. +pub type ServerCodec = CodecImpl; + +/// An ABCI client encodes outgoing [`tendermint::abci::request::Request`]s +/// and decodes incoming [`tendermint::abci::response::Response`]s. The stream +/// `S` must implement [`futures::AsyncRead`] and [`futures::AsyncWrite`]. +pub type ClientCodec = CodecImpl; + +/// A non-blocking codec that allows us to iterate over a [`futures::Stream`] +/// producing values of type `Result`. It also implements [`futures::Sink`], +/// allowing us to send values of type `O`. +pub trait Codec: Stream> + Sink + Send + Unpin { + /// Constructor. + fn new(inner: S, read_buf_size: usize) -> Self; +} + +/// Non-blocking adapter to convert an underlying I/O stream, which implements +/// [`futures::AsyncRead`] and [`futures::AsyncWrite`], into a +/// [`futures::Stream`] producing entities of type `I` and a [`futures::Sink`] +/// allowing for sending of entities of type `O`. +#[pin_project] +pub struct CodecImpl { + #[pin] + inner: S, + _encoder: PhantomData, + _output: PhantomData, + decoder: D, + _input: PhantomData, + growable_read_buf: BytesMut, + growable_write_buf: BytesMut, + read_buf: Vec, +} + +impl Stream for CodecImpl +where + D: Decoder, + S: AsyncRead, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + let mut inner: Pin<&mut S> = this.inner; + let growable_read_buf: &mut BytesMut = this.growable_read_buf; + let read_buf: &mut Vec = this.read_buf; + let decoder: &mut D = this.decoder; + + loop { + // Try to decode another input value from our existing read buffer + // if we can without resorting to I/O. + match decoder.decode(growable_read_buf) { + Ok(res) => { + if let Some(val) = res { + return Poll::Ready(Some(Ok(val))); + } + } + Err(e) => return Poll::Ready(Some(Err(e))), + } + + // If we don't have enough data to decode an input value, try to + // read some more from the underlying reader. + let bytes_read = match ready!(inner.as_mut().poll_read(cx, read_buf.as_mut())) { + Ok(br) => br, + Err(e) => return Poll::Ready(Some(Err(e.into()))), + }; + if bytes_read == 0 { + // The underlying stream terminated + return Poll::Ready(None); + } + growable_read_buf.extend_from_slice(&read_buf[..bytes_read]); + } + } +} + +impl Sink for CodecImpl +where + E: Encoder, + S: AsyncWrite, +{ + type Error = Error; + + fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send(self: Pin<&mut Self>, item: O) -> Result<()> { + let write_buf: &mut BytesMut = self.project().growable_write_buf; + E::encode(item, write_buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + let mut inner: Pin<&mut S> = this.inner; + let write_buf: &mut BytesMut = this.growable_write_buf; + + while !write_buf.is_empty() { + let bytes_written = match ready!(inner.as_mut().poll_write(cx, write_buf.as_ref())) { + Ok(bw) => bw, + Err(e) => return Poll::Ready(Err(e.into())), + }; + if bytes_written == 0 { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "failed to write to underlying stream", + ) + .into())); + } + write_buf.advance(bytes_written); + } + // Try to flush the underlying stream + ready!(inner.poll_flush(cx))?; + Poll::Ready(Ok(())) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + ready!(self.as_mut().poll_flush(cx))?; + let inner: Pin<&mut S> = self.project().inner; + ready!(inner.poll_close(cx))?; + + Poll::Ready(Ok(())) + } +} + +impl Codec for CodecImpl +where + E: Encoder + Send, + D: Decoder + Default + Send, + O: Send, + I: Send, + S: AsyncRead + AsyncWrite + Send + Unpin, +{ + fn new(inner: S, read_buf_size: usize) -> Self { + Self { + inner, + _encoder: Default::default(), + _output: Default::default(), + decoder: Default::default(), + _input: Default::default(), + growable_read_buf: BytesMut::new(), + growable_write_buf: BytesMut::new(), + read_buf: vec![0_u8; read_buf_size], + } + } +} diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 284b77183..a7586f268 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -1,23 +1,12 @@ //! ABCI framework for building applications with Tendermint. mod application; -#[cfg(any( - feature = "client", - feature = "runtime-tokio", - feature = "runtime-async-std", - feature = "runtime-std", -))] -mod codec; +#[cfg(feature = "client")] +pub mod client; +pub mod codec; mod result; pub mod runtime; -mod server; - -// Client exports -#[cfg(feature = "client")] -mod client; - -#[cfg(feature = "client")] -pub use client::Client; +pub mod server; // Example applications #[cfg(feature = "echo-app")] @@ -26,19 +15,23 @@ pub use application::echo::EchoApp; // Common exports pub use application::Application; pub use result::{Error, Result}; -pub use server::Server; -// Runtime-specific exports -#[cfg(all(feature = "async", feature = "client", feature = "runtime-tokio"))] -pub type TokioClient = Client; -#[cfg(all(feature = "async", feature = "client", feature = "runtime-async-std"))] -pub type AsyncStdClient = Client; -#[cfg(all(not(feature = "async"), feature = "client", feature = "runtime-std"))] -pub type StdClient = Client; +// Runtime-specific convenience exports +#[cfg(all(feature = "blocking", feature = "runtime-std"))] +pub use server::blocking::StdServerBuilder; +#[cfg(all(feature = "non-blocking", feature = "runtime-async-std"))] +pub use server::non_blocking::AsyncStdServerBuilder; +#[cfg(all(feature = "non-blocking", feature = "runtime-tokio"))] +pub use server::non_blocking::TokioServerBuilder; -#[cfg(all(feature = "async", feature = "runtime-tokio"))] -pub type TokioServer = Server; -#[cfg(all(feature = "async", feature = "runtime-async-std"))] -pub type AsyncStdServer = Server; -#[cfg(all(not(feature = "async"), feature = "runtime-std"))] -pub type StdServer = Server; +#[cfg(feature = "client")] +mod client_exports { + #[cfg(all(feature = "blocking", feature = "runtime-std"))] + pub use super::client::blocking::StdClientBuilder; + #[cfg(all(feature = "non-blocking", feature = "runtime-async-std"))] + pub use super::client::non_blocking::AsyncStdClientBuilder; + #[cfg(all(feature = "non-blocking", feature = "runtime-tokio"))] + pub use super::client::non_blocking::TokioClientBuilder; +} +#[cfg(feature = "client")] +pub use client_exports::*; diff --git a/abci/src/runtime.rs b/abci/src/runtime.rs index f00cb8fd5..c1453fe1c 100644 --- a/abci/src/runtime.rs +++ b/abci/src/runtime.rs @@ -1,161 +1,6 @@ //! Abstractions for facilitating runtime-independent code. -#[cfg(all(feature = "async", feature = "runtime-async-std"))] -pub mod async_std; -#[cfg(all(not(feature = "async"), feature = "runtime-std"))] -pub mod std; -#[cfg(all(feature = "async", feature = "runtime-tokio"))] -pub mod tokio; - -// The runtime interface included here depends on whether the `async` feature -// flag is enabled or not. -pub use crate::runtime::interface::{ - ChannelNotify, ClientCodec, Receiver, Sender, ServerCodec, TaskSpawner, TcpListener, TcpStream, -}; - -/// Implemented by each runtime we support. -pub trait Runtime: 'static { - type TcpStream: TcpStream; - type TcpListener: TcpListener; - type TaskSpawner: TaskSpawner; - type ServerCodec: ServerCodec; - type ClientCodec: ClientCodec; - - // TODO(thane): Make this generic once GATs are stable (see - // https://rust-lang.github.io/rfcs/1598-generic_associated_types.html) - type ChannelNotify: ChannelNotify; -} - -#[cfg(feature = "async")] -mod interface { - use crate::{Error, Result}; - use async_trait::async_trait; - use futures::{Future, Sink, Stream}; - use std::net::SocketAddr; - use tendermint::abci::{request, response}; - - #[async_trait] - pub trait TcpStream: Sized + Send { - async fn connect(addr: &str) -> Result; - } - - #[async_trait] - pub trait TcpListener: Sized { - /// Bind this listener to the given address. - async fn bind(addr: &str) -> Result; - - /// Returns the string representation of this listener's local address. - fn local_addr(&self) -> Result; - - /// Attempt to accept an incoming connection. - async fn accept(&self) -> Result<(T, SocketAddr)>; - } - - pub trait TaskSpawner { - /// Spawn an asynchronous task without caring about its result. - fn spawn_and_forget(task: T) - where - T: Future + Send + 'static, - T::Output: Send + 'static; - } - - pub trait ServerCodec: - Stream> - + Sink - + Unpin - + Send - { - fn from_tcp_stream(stream: S) -> Self; - } - - pub trait ClientCodec: - Sink - + Stream> - + Unpin - + Send - { - fn from_tcp_stream(stream: S) -> Self; - } - - /// The sending end of a channel. - #[async_trait] - pub trait Sender { - async fn send(&self, value: T) -> Result<()>; - } - - /// The receiving end of a channel. - #[async_trait] - pub trait Receiver { - async fn recv(&mut self) -> Result; - } - - /// A simple notification channel. - pub trait ChannelNotify { - type Sender: Sender<()>; - type Receiver: Receiver<()>; - - /// Construct an unbounded channel. - fn unbounded() -> (Self::Sender, Self::Receiver); - } -} - -#[cfg(not(feature = "async"))] -mod interface { - use crate::Result; - use std::net::SocketAddr; - use tendermint::abci::{request, response}; - - pub trait TcpStream: Sized + Send { - fn connect(addr: &str) -> Result; - } - - pub trait TcpListener: Sized { - /// Bind this listener to the given address. - fn bind(addr: &str) -> Result; - - /// Returns the string representation of this listener's local address. - fn local_addr(&self) -> Result; - - /// Attempt to accept an incoming connection. - fn accept(&self) -> Result<(T, SocketAddr)>; - } - - pub trait TaskSpawner { - /// Spawn a task in a separate thread without caring about its result. - fn spawn_and_forget(task: T) - where - T: FnOnce() + Send + 'static, - T::Output: Send; - } - - pub trait ServerCodec: Iterator> { - fn from_tcp_stream(stream: S) -> Self; - - fn send(&mut self, res: response::Response) -> Result<()>; - } - - pub trait ClientCodec: Iterator> { - fn from_tcp_stream(stream: S) -> Self; - - fn send(&mut self, req: request::Request) -> Result<()>; - } - - /// The sending end of a channel. - pub trait Sender { - fn send(&self, value: T) -> Result<()>; - } - - /// The receiving end of a channel. - pub trait Receiver { - fn recv(&self) -> Result; - } - - /// A simple notification channel. - pub trait ChannelNotify { - type Sender: Sender<()>; - type Receiver: Receiver<()>; - - /// Construct an unbounded channel. - fn unbounded() -> (Self::Sender, Self::Receiver); - } -} +#[cfg(feature = "blocking")] +pub mod blocking; +#[cfg(feature = "non-blocking")] +pub mod non_blocking; diff --git a/abci/src/runtime/async_std.rs b/abci/src/runtime/async_std.rs deleted file mode 100644 index 8d56ef8b1..000000000 --- a/abci/src/runtime/async_std.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! `async-std` runtime-specific types. - -// TODO(thane): Implement something like tokio-util's `Framed` for async-std to -// reduce code duplication in AsyncStdServerCodec and -// AsyncStdServerCodec. - -use crate::codec::{TspDecoder, TspEncoder}; -use crate::runtime::{ - ChannelNotify, ClientCodec, Receiver, Runtime, Sender, ServerCodec, TaskSpawner, TcpListener, - TcpStream, -}; -use crate::{Error, Result}; -use async_trait::async_trait; -use bytes::{Buf, BytesMut}; -use futures::ready; -use futures::task::{Context, Poll}; -use futures::{AsyncRead, AsyncWrite, Future, Sink, Stream}; -use pin_project::pin_project; -use std::net::SocketAddr; -use std::pin::Pin; -use tendermint::abci::{request, response}; - -// The size of the read buffer we use when reading from a TCP stream. This is -// allocated each time a stream is polled for readiness to be read, so it's -// important that it's relatively small. Too small, however, and it'll increase -// CPU load due to increased decode/poll attempts. -// -// Tokio seems to get around this by using `unsafe` code in their buffer -// polling routine in `tokio-util`: https://github.com/tokio-rs/tokio/blob/198363f4f1a71cb98ffc0e9eaac335f669a5e1de/tokio-util/src/lib.rs#L106 -// but we don't want to write our own `unsafe` code. -// -// As for a good number for general ABCI apps, that's something we should -// benchmark to determine. -// TODO(thane): Benchmark options here. -const CODEC_READ_BUF_SIZE: usize = 128; - -/// `async-std` runtime. -pub struct AsyncStd; - -impl Runtime for AsyncStd { - type TcpStream = AsyncStdTcpStream; - type TcpListener = AsyncStdTcpListener; - type TaskSpawner = AsyncStdTaskSpawner; - type ServerCodec = AsyncStdServerCodec; - type ClientCodec = AsyncStdClientCodec; - type ChannelNotify = AsyncStdChannelNotify; -} - -pub struct AsyncStdTcpStream(async_std::net::TcpStream); - -#[async_trait] -impl TcpStream for AsyncStdTcpStream { - async fn connect(addr: &str) -> Result { - Ok(Self(async_std::net::TcpStream::connect(addr).await?)) - } -} - -pub struct AsyncStdTcpListener(async_std::net::TcpListener); - -#[async_trait] -impl TcpListener for AsyncStdTcpListener { - async fn bind(addr: &str) -> Result { - Ok(Self(async_std::net::TcpListener::bind(addr).await?)) - } - - fn local_addr(&self) -> Result { - Ok(self.0.local_addr()?.to_string()) - } - - async fn accept(&self) -> Result<(AsyncStdTcpStream, SocketAddr)> { - let (stream, addr) = self.0.accept().await?; - Ok((AsyncStdTcpStream(stream), addr)) - } -} - -pub struct AsyncStdTaskSpawner; - -impl TaskSpawner for AsyncStdTaskSpawner { - fn spawn_and_forget(task: T) - where - T: Future + Send + 'static, - T::Output: Send + 'static, - { - let _ = async_std::task::spawn(task); - } -} - -#[pin_project] -pub struct AsyncStdServerCodec { - #[pin] - stream: async_std::net::TcpStream, - read_buf: BytesMut, - write_buf: BytesMut, - decoder: TspDecoder, -} - -impl ServerCodec for AsyncStdServerCodec { - fn from_tcp_stream(stream: AsyncStdTcpStream) -> Self { - Self { - stream: stream.0, - read_buf: BytesMut::new(), - write_buf: BytesMut::new(), - decoder: TspDecoder::new(), - } - } -} - -impl Stream for AsyncStdServerCodec { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.project(); - let mut stream: Pin<&mut async_std::net::TcpStream> = this.stream; - let read_buf: &mut BytesMut = this.read_buf; - let decoder: &mut TspDecoder = this.decoder; - let mut tmp_read_buf = [0_u8; CODEC_READ_BUF_SIZE]; - - loop { - // Try to decode a request from our existing buffer - match decoder.decode_request(read_buf) { - Ok(res) => { - if let Some(req) = res { - return Poll::Ready(Some(Ok(req))); - } - } - Err(e) => return Poll::Ready(Some(Err(e))), - } - - // If we couldn't decode another request from the buffer, try to - // fill up the buffer as much as we can - let bytes_read = match ready!(stream.as_mut().poll_read(cx, &mut tmp_read_buf)) { - Ok(br) => br, - Err(e) => return Poll::Ready(Some(Err(e.into()))), - }; - if bytes_read == 0 { - // The stream terminated - return Poll::Ready(None); - } - read_buf.extend_from_slice(&tmp_read_buf[..bytes_read]); - } - } -} - -impl Sink for AsyncStdServerCodec { - type Error = Error; - - fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn start_send(self: Pin<&mut Self>, item: response::Response) -> Result<()> { - let write_buf: &mut BytesMut = self.project().write_buf; - TspEncoder::encode_response(item, write_buf) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.project(); - let mut stream: Pin<&mut async_std::net::TcpStream> = this.stream; - let write_buf: &mut BytesMut = this.write_buf; - - while !write_buf.is_empty() { - let bytes_written = match ready!(stream.as_mut().poll_write(cx, write_buf.as_ref())) { - Ok(bw) => bw, - Err(e) => return Poll::Ready(Err(e.into())), - }; - if bytes_written == 0 { - return Poll::Ready(Err(async_std::io::Error::new( - async_std::io::ErrorKind::WriteZero, - "failed to write to transport", - ) - .into())); - } - write_buf.advance(bytes_written); - } - // Try to flush the underlying stream - ready!(stream.poll_flush(cx))?; - Poll::Ready(Ok(())) - } - - fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - ready!(self.as_mut().poll_flush(cx))?; - let stream: Pin<&mut async_std::net::TcpStream> = self.project().stream; - ready!(stream.poll_close(cx))?; - - Poll::Ready(Ok(())) - } -} - -#[pin_project] -pub struct AsyncStdClientCodec { - #[pin] - stream: async_std::net::TcpStream, - read_buf: BytesMut, - write_buf: BytesMut, - decoder: TspDecoder, -} - -impl ClientCodec for AsyncStdClientCodec { - fn from_tcp_stream(stream: AsyncStdTcpStream) -> Self { - Self { - stream: stream.0, - read_buf: BytesMut::new(), - write_buf: BytesMut::new(), - decoder: TspDecoder::new(), - } - } -} - -impl Stream for AsyncStdClientCodec { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.project(); - let mut stream: Pin<&mut async_std::net::TcpStream> = this.stream; - let read_buf: &mut BytesMut = this.read_buf; - let decoder: &mut TspDecoder = this.decoder; - let mut tmp_read_buf = [0_u8; CODEC_READ_BUF_SIZE]; - - loop { - // Try to decode a response from our existing buffer - match decoder.decode_response(read_buf) { - Ok(res_opt) => { - if let Some(res) = res_opt { - return Poll::Ready(Some(Ok(res))); - } - } - Err(e) => return Poll::Ready(Some(Err(e))), - } - - // If we couldn't decode another request from the buffer, try to - // fill up the buffer as much as we can - let bytes_read = match ready!(stream.as_mut().poll_read(cx, &mut tmp_read_buf)) { - Ok(br) => br, - Err(e) => return Poll::Ready(Some(Err(e.into()))), - }; - if bytes_read == 0 { - // The stream terminated - return Poll::Ready(None); - } - read_buf.extend_from_slice(&tmp_read_buf[..bytes_read]); - } - } -} - -impl Sink for AsyncStdClientCodec { - type Error = Error; - - fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn start_send(self: Pin<&mut Self>, item: request::Request) -> Result<()> { - let write_buf: &mut BytesMut = self.project().write_buf; - TspEncoder::encode_request(item, write_buf) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.project(); - let mut stream: Pin<&mut async_std::net::TcpStream> = this.stream; - let write_buf: &mut BytesMut = this.write_buf; - - while !write_buf.is_empty() { - let bytes_written = match ready!(stream.as_mut().poll_write(cx, write_buf.as_ref())) { - Ok(bw) => bw, - Err(e) => return Poll::Ready(Err(e.into())), - }; - if bytes_written == 0 { - return Poll::Ready(Err(async_std::io::Error::new( - async_std::io::ErrorKind::WriteZero, - "failed to write to transport", - ) - .into())); - } - write_buf.advance(bytes_written); - } - // Try to flush the underlying stream - ready!(stream.poll_flush(cx))?; - Poll::Ready(Ok(())) - } - - fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - ready!(self.as_mut().poll_flush(cx))?; - let stream: Pin<&mut async_std::net::TcpStream> = self.project().stream; - ready!(stream.poll_close(cx))?; - - Poll::Ready(Ok(())) - } -} - -pub struct AsyncStdChannelNotify; - -impl ChannelNotify for AsyncStdChannelNotify { - type Sender = AsyncStdSender<()>; - type Receiver = AsyncStdReceiver<()>; - - fn unbounded() -> (Self::Sender, Self::Receiver) { - let (tx, rx) = async_channel::unbounded(); - (AsyncStdSender(tx), AsyncStdReceiver(rx)) - } -} - -pub struct AsyncStdSender(async_channel::Sender); - -#[async_trait] -impl Sender for AsyncStdSender { - async fn send(&self, value: T) -> Result<()> { - self.0 - .send(value) - .await - .map_err(|e| Error::ChannelSend(e.to_string())) - } -} - -pub struct AsyncStdReceiver(async_channel::Receiver); - -#[async_trait] -impl Receiver for AsyncStdReceiver { - async fn recv(&mut self) -> Result { - self.0 - .recv() - .await - .map_err(|e| Error::ChannelRecv(e.to_string())) - } -} - -#[cfg(all(feature = "async", feature = "runtime-async-std"))] -#[cfg(test)] -mod test { - use super::*; - use async_std::task::JoinHandle; - use futures::{SinkExt, StreamExt}; - use std::convert::TryInto; - - #[async_std::test] - async fn codec() { - let requests = (0..5) - .map(|r| request::Echo { - message: format!("echo {}", r), - }) - .collect::>(); - let listener = async_std::net::TcpListener::bind("127.0.0.1:0") - .await - .unwrap(); - let local_addr = listener.local_addr().unwrap().to_string(); - let client_requests = requests.clone(); - let client_handle: JoinHandle> = async_std::task::spawn(async move { - let client_stream = async_std::net::TcpStream::connect(local_addr) - .await - .unwrap(); - let mut codec = AsyncStdClientCodec::from_tcp_stream(AsyncStdTcpStream(client_stream)); - let mut received_responses = Vec::new(); - - for req in client_requests { - codec.send(req.into()).await.unwrap(); - let res: response::Echo = codec.next().await.unwrap().unwrap().try_into().unwrap(); - received_responses.push(res); - } - - received_responses - }); - - let (server_stream, _) = listener.accept().await.unwrap(); - let mut codec = AsyncStdServerCodec::from_tcp_stream(AsyncStdTcpStream(server_stream)); - - let mut received_requests = Vec::new(); - while let Some(result) = codec.next().await { - let request: request::Echo = result.unwrap().try_into().unwrap(); - codec - .send( - response::Echo { - message: request.message.clone(), - } - .into(), - ) - .await - .unwrap(); - received_requests.push(request); - } - let received_responses = client_handle.await; - assert_eq!(received_requests.len(), requests.len()); - assert_eq!(received_requests, requests); - assert_eq!(received_responses.len(), requests.len()); - } -} diff --git a/abci/src/runtime/blocking.rs b/abci/src/runtime/blocking.rs new file mode 100644 index 000000000..619fbd409 --- /dev/null +++ b/abci/src/runtime/blocking.rs @@ -0,0 +1,64 @@ +//! Blocking API for blocking runtimes. + +#[cfg(feature = "runtime-std")] +pub mod runtime_std; + +use crate::codec::blocking::Codec; +use crate::Result; +use std::io::{Read, Write}; +use tendermint::abci::{request, response}; + +/// Implemented by each blocking runtime we support. +pub trait Runtime: 'static { + type TcpStream: TcpStream; + type TcpListener: TcpListener; + + // Crate-specific types + type ServerCodec: Codec< + <::TcpStream as TcpStream>::Inner, + request::Request, + response::Response, + >; + type ClientCodec: Codec< + <::TcpStream as TcpStream>::Inner, + response::Response, + request::Request, + >; + + /// Spawn a task in a separate thread without caring about its result. + fn spawn_and_forget(task: T) + where + T: FnOnce() + Send + 'static, + T::Output: Send; +} + +pub trait TcpStream: Sized + Send { + type Inner: Read + Write; + + fn connect(addr: &str) -> Result; + + fn into_inner(self) -> Self::Inner; +} + +pub trait TcpListener: Sized { + /// Bind this listener to the given address. + fn bind(addr: &str) -> Result; + + /// Returns the string representation of this listener's local address. + fn local_addr(&self) -> Result; + + /// Attempt to accept an incoming connection. + /// + /// On success, returns a TCP stream and a string representation of its address. + fn accept(&self) -> Result<(T, String)>; +} + +/// The sending end of a channel. +pub trait Sender { + fn send(&self, value: T) -> Result<()>; +} + +/// The receiving end of a channel. +pub trait Receiver { + fn recv(&self) -> Result; +} diff --git a/abci/src/runtime/blocking/runtime_std.rs b/abci/src/runtime/blocking/runtime_std.rs new file mode 100644 index 000000000..277dbbf26 --- /dev/null +++ b/abci/src/runtime/blocking/runtime_std.rs @@ -0,0 +1,59 @@ +//! Rust standard library-based runtime. + +use crate::codec::blocking::{ClientCodec, ServerCodec}; +use crate::runtime::blocking::{Runtime, TcpListener, TcpStream}; +use crate::Result; + +/// Rust standard library-based runtime. +pub struct Std; + +impl Runtime for Std { + type TcpStream = StdTcpStream; + type TcpListener = StdTcpListener; + + type ServerCodec = ServerCodec; + type ClientCodec = ClientCodec; + + fn spawn_and_forget(task: T) + where + T: FnOnce() + Send + 'static, + T::Output: Send, + { + let _ = std::thread::spawn(move || { + task(); + }); + } +} + +/// Rust standard library TCP stream ([`std::net::TcpStream`]). +pub struct StdTcpStream(std::net::TcpStream); + +impl TcpStream for StdTcpStream { + type Inner = std::net::TcpStream; + + fn connect(addr: &str) -> Result { + Ok(Self(std::net::TcpStream::connect(addr)?)) + } + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +/// Rust standard library TCP listener ([`std::net::TcpListener`]). +pub struct StdTcpListener(std::net::TcpListener); + +impl TcpListener for StdTcpListener { + fn bind(addr: &str) -> Result { + Ok(Self(std::net::TcpListener::bind(addr)?)) + } + + fn local_addr(&self) -> Result { + Ok(self.0.local_addr()?.to_string()) + } + + fn accept(&self) -> Result<(StdTcpStream, String)> { + let (stream, addr) = self.0.accept()?; + Ok((StdTcpStream(stream), addr.to_string())) + } +} diff --git a/abci/src/runtime/non_blocking.rs b/abci/src/runtime/non_blocking.rs new file mode 100644 index 000000000..5c6c7cf14 --- /dev/null +++ b/abci/src/runtime/non_blocking.rs @@ -0,0 +1,79 @@ +//! Non-blocking runtime interface. + +#[cfg(feature = "runtime-async-std")] +pub mod runtime_async_std; +#[cfg(feature = "runtime-tokio")] +pub mod runtime_tokio; + +use crate::codec::non_blocking::Codec; +use crate::Result; +use async_trait::async_trait; +use futures::{AsyncRead, AsyncWrite, Future}; +use tendermint::abci::{request, response}; + +/// Implemented by each non-blocking runtime we support. +pub trait Runtime: 'static { + type TcpStream: TcpStream; + type TcpListener: TcpListener; + + // Crate-specific types + type ServerCodec: Codec< + <::TcpStream as TcpStream>::Inner, + request::Request, + response::Response, + >; + type ClientCodec: Codec< + <::TcpStream as TcpStream>::Inner, + response::Response, + request::Request, + >; + type ChannelNotify: ChannelNotify; + + /// Spawn an asynchronous task without caring about its result. + fn spawn_and_forget(task: T) + where + T: Future + Send + 'static, + T::Output: Send + 'static; +} + +#[async_trait] +pub trait TcpStream: Sized + Send { + type Inner: AsyncRead + AsyncWrite; + + async fn connect(addr: &str) -> Result; + + fn into_inner(self) -> Self::Inner; +} + +#[async_trait] +pub trait TcpListener: Sized { + /// Bind this listener to the given address. + async fn bind(addr: &str) -> Result; + + /// Returns the string representation of this listener's local address. + fn local_addr(&self) -> Result; + + /// Attempt to accept an incoming connection. + async fn accept(&self) -> Result<(T, String)>; +} + +/// The sending end of a channel. +#[async_trait] +pub trait Sender { + async fn send(&self, value: T) -> Result<()>; +} + +/// The receiving end of a channel. +#[async_trait] +pub trait Receiver { + async fn recv(&mut self) -> Result; +} + +/// A simple notification channel. +pub trait ChannelNotify { + type Sender: Sender<()>; + type Receiver: Receiver<()>; + + /// Construct an unbounded channel. + fn unbounded() -> (Self::Sender, Self::Receiver); +} diff --git a/abci/src/runtime/non_blocking/runtime_async_std.rs b/abci/src/runtime/non_blocking/runtime_async_std.rs new file mode 100644 index 000000000..86087720c --- /dev/null +++ b/abci/src/runtime/non_blocking/runtime_async_std.rs @@ -0,0 +1,97 @@ +//! `async-std`-based non-blocking runtime. + +use crate::codec::non_blocking::{ClientCodec, ServerCodec}; +use crate::runtime::non_blocking::{ + ChannelNotify, Receiver, Runtime, Sender, TcpListener, TcpStream, +}; +use crate::{Error, Result}; +use async_trait::async_trait; +use futures::Future; + +pub struct AsyncStd; + +impl Runtime for AsyncStd { + type TcpStream = AsyncStdTcpStream; + type TcpListener = AsyncStdTcpListener; + + type ServerCodec = ServerCodec; + type ClientCodec = ClientCodec; + type ChannelNotify = AsyncStdChannelNotify; + + fn spawn_and_forget(task: T) + where + T: Future + Send + 'static, + T::Output: Send + 'static, + { + let _ = async_std::task::spawn(task); + } +} + +pub struct AsyncStdTcpStream(async_std::net::TcpStream); + +#[async_trait] +impl TcpStream for AsyncStdTcpStream { + type Inner = async_std::net::TcpStream; + + async fn connect(addr: &str) -> Result { + Ok(Self(async_std::net::TcpStream::connect(addr).await?)) + } + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +pub struct AsyncStdTcpListener(async_std::net::TcpListener); + +#[async_trait] +impl TcpListener for AsyncStdTcpListener { + async fn bind(addr: &str) -> Result { + Ok(Self(async_std::net::TcpListener::bind(addr).await?)) + } + + fn local_addr(&self) -> Result { + Ok(self.0.local_addr()?.to_string()) + } + + async fn accept(&self) -> Result<(AsyncStdTcpStream, String)> { + let (stream, addr) = self.0.accept().await?; + Ok((AsyncStdTcpStream(stream), addr.to_string())) + } +} + +pub struct AsyncStdSender(async_channel::Sender); + +#[async_trait] +impl Sender for AsyncStdSender { + async fn send(&self, value: T) -> Result<()> { + self.0 + .send(value) + .await + .map_err(|e| Error::ChannelSend(e.to_string())) + } +} + +pub struct AsyncStdReceiver(async_channel::Receiver); + +#[async_trait] +impl Receiver for AsyncStdReceiver { + async fn recv(&mut self) -> Result { + self.0 + .recv() + .await + .map_err(|e| Error::ChannelRecv(e.to_string())) + } +} + +pub struct AsyncStdChannelNotify; + +impl ChannelNotify for AsyncStdChannelNotify { + type Sender = AsyncStdSender<()>; + type Receiver = AsyncStdReceiver<()>; + + fn unbounded() -> (Self::Sender, Self::Receiver) { + let (tx, rx) = async_channel::unbounded(); + (AsyncStdSender(tx), AsyncStdReceiver(rx)) + } +} diff --git a/abci/src/runtime/non_blocking/runtime_tokio.rs b/abci/src/runtime/non_blocking/runtime_tokio.rs new file mode 100644 index 000000000..11b2e156a --- /dev/null +++ b/abci/src/runtime/non_blocking/runtime_tokio.rs @@ -0,0 +1,135 @@ +//! Tokio-based non-blocking runtime. + +use crate::codec::non_blocking::{ClientCodec, ServerCodec}; +use crate::runtime::non_blocking::{ + ChannelNotify, Receiver, Runtime, Sender, TcpListener, TcpStream, +}; +use crate::{Error, Result}; +use async_trait::async_trait; +use futures::task::{Context, Poll}; +use futures::{ready, Future}; +use pin_project::pin_project; +use std::pin::Pin; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +pub struct Tokio; + +impl Runtime for Tokio { + type TcpStream = TokioTcpStream; + type TcpListener = TokioTcpListener; + + type ServerCodec = ServerCodec; + type ClientCodec = ClientCodec; + type ChannelNotify = TokioChannelNotify; + + fn spawn_and_forget(task: T) + where + T: Future + Send + 'static, + T::Output: Send + 'static, + { + let _ = tokio::spawn(task); + } +} + +/// A wrapper for [`tokio::net::TcpStream`] that implements +/// [`futures::AsyncRead`] and [`futures::AsyncWrite`] to ensure compatibility +/// with our non-blocking (futures-based) interfaces. +#[pin_project] +pub struct FuturesTcpStream(#[pin] tokio::net::TcpStream); + +impl futures::AsyncRead for FuturesTcpStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let mut buf = ReadBuf::new(buf); + ready!(self.project().0.poll_read(cx, &mut buf)?); + Poll::Ready(Ok(buf.filled().len())) + } +} + +impl futures::AsyncWrite for FuturesTcpStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.project().0.poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().0.poll_shutdown(cx) + } +} + +pub struct TokioTcpStream(FuturesTcpStream); + +#[async_trait] +impl TcpStream for TokioTcpStream { + type Inner = FuturesTcpStream; + + async fn connect(addr: &str) -> Result { + Ok(Self(FuturesTcpStream( + tokio::net::TcpStream::connect(addr).await?, + ))) + } + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +pub struct TokioTcpListener(tokio::net::TcpListener); + +#[async_trait] +impl TcpListener for TokioTcpListener { + async fn bind(addr: &str) -> Result { + Ok(Self(tokio::net::TcpListener::bind(addr).await?)) + } + + fn local_addr(&self) -> Result { + Ok(self.0.local_addr()?.to_string()) + } + + async fn accept(&self) -> Result<(TokioTcpStream, String)> { + let (stream, addr) = self.0.accept().await?; + Ok((TokioTcpStream(FuturesTcpStream(stream)), addr.to_string())) + } +} + +pub struct TokioChannelNotify; + +impl ChannelNotify for TokioChannelNotify { + type Sender = TokioSender<()>; + type Receiver = TokioReceiver<()>; + + fn unbounded() -> (Self::Sender, Self::Receiver) { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + (TokioSender(tx), TokioReceiver(rx)) + } +} + +pub struct TokioSender(tokio::sync::mpsc::UnboundedSender); + +#[async_trait] +impl Sender for TokioSender { + async fn send(&self, value: T) -> Result<()> { + self.0 + .send(value) + .map_err(|e| Error::ChannelSend(e.to_string())) + } +} + +pub struct TokioReceiver(tokio::sync::mpsc::UnboundedReceiver); + +#[async_trait] +impl Receiver for TokioReceiver { + async fn recv(&mut self) -> Result { + self.0.recv().await.ok_or(Error::ChannelSenderClosed) + } +} diff --git a/abci/src/runtime/std.rs b/abci/src/runtime/std.rs deleted file mode 100644 index 0dff1fc4a..000000000 --- a/abci/src/runtime/std.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! `std` runtime-specific types. - -use crate::codec::{TspDecoder, TspEncoder}; -use crate::runtime::{ - ChannelNotify, ClientCodec, Receiver, Runtime, Sender, ServerCodec, TaskSpawner, TcpListener, - TcpStream, -}; -use crate::{Error, Result}; -use bytes::{Buf, BytesMut}; -use std::io::{Read, Write}; -use std::net::SocketAddr; -use tendermint::abci::request::Request; -use tendermint::abci::response::Response; -use tendermint::abci::{request, response}; - -const CODEC_READ_BUF_SIZE: usize = 128; - -/// The built-in Rust standard library is our runtime. -pub struct Std; - -impl Runtime for Std { - type TcpStream = StdTcpStream; - type TcpListener = StdTcpListener; - type TaskSpawner = StdTaskSpawner; - type ServerCodec = StdServerCodec; - type ClientCodec = StdClientCodec; - type ChannelNotify = StdChannelNotify; -} - -pub struct StdTcpStream(std::net::TcpStream); - -impl TcpStream for StdTcpStream { - fn connect(addr: &str) -> Result { - Ok(Self(std::net::TcpStream::connect(addr)?)) - } -} - -pub struct StdTcpListener(std::net::TcpListener); - -impl TcpListener for StdTcpListener { - fn bind(addr: &str) -> Result { - Ok(Self(std::net::TcpListener::bind(addr)?)) - } - - fn local_addr(&self) -> Result { - Ok(self.0.local_addr()?.to_string()) - } - - fn accept(&self) -> Result<(StdTcpStream, SocketAddr)> { - let (stream, addr) = self.0.accept()?; - Ok((StdTcpStream(stream), addr)) - } -} - -pub struct StdTaskSpawner; - -impl TaskSpawner for StdTaskSpawner { - fn spawn_and_forget(task: T) - where - T: FnOnce() + Send + 'static, - T::Output: Send, - { - let _ = std::thread::spawn(move || { - task(); - }); - } -} - -pub struct StdServerCodec { - stream: std::net::TcpStream, - read_buf: BytesMut, - write_buf: BytesMut, - decoder: TspDecoder, -} - -impl ServerCodec for StdServerCodec { - fn from_tcp_stream(stream: StdTcpStream) -> Self { - Self { - stream: stream.0, - read_buf: BytesMut::new(), - write_buf: BytesMut::new(), - decoder: TspDecoder::new(), - } - } - - fn send(&mut self, res: Response) -> Result<()> { - TspEncoder::encode_response(res, &mut self.write_buf)?; - let bytes_written = self.stream.write(self.write_buf.as_ref())?; - if bytes_written == 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::WriteZero, - "failed to write response", - ) - .into()); - } - self.write_buf.advance(bytes_written); - Ok(self.stream.flush()?) - } -} - -impl Iterator for StdServerCodec { - type Item = Result; - - fn next(&mut self) -> Option { - let mut tmp_read_buf = [0_u8; CODEC_READ_BUF_SIZE]; - - loop { - // Try to decode a request from our internal read buffer first - match self.decoder.decode_request(&mut self.read_buf) { - Ok(req_opt) => { - if let Some(req) = req_opt { - return Some(Ok(req)); - } - } - Err(e) => return Some(Err(e)), - } - - // If we don't have enough data to decode a message, try to read - // more - let bytes_read = match self.stream.read(&mut tmp_read_buf) { - Ok(br) => br, - Err(e) => return Some(Err(e.into())), - }; - if bytes_read == 0 { - // The stream terminated - return None; - } - self.read_buf.extend_from_slice(&tmp_read_buf[..bytes_read]); - } - } -} - -pub struct StdClientCodec { - stream: std::net::TcpStream, - read_buf: BytesMut, - write_buf: BytesMut, - decoder: TspDecoder, -} - -impl ClientCodec for StdClientCodec { - fn from_tcp_stream(stream: StdTcpStream) -> Self { - Self { - stream: stream.0, - read_buf: BytesMut::new(), - write_buf: BytesMut::new(), - decoder: TspDecoder::new(), - } - } - - fn send(&mut self, req: Request) -> Result<()> { - TspEncoder::encode_request(req, &mut self.write_buf)?; - let bytes_written = self.stream.write(self.write_buf.as_ref())?; - if bytes_written == 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::WriteZero, - "failed to write request", - ) - .into()); - } - self.write_buf.advance(bytes_written); - Ok(self.stream.flush()?) - } -} - -impl Iterator for StdClientCodec { - type Item = Result; - - fn next(&mut self) -> Option { - let mut tmp_read_buf = [0_u8; CODEC_READ_BUF_SIZE]; - - loop { - // Try to decode a response from our internal read buffer first - match self.decoder.decode_response(&mut self.read_buf) { - Ok(res_opt) => { - if let Some(res) = res_opt { - return Some(Ok(res)); - } - } - Err(e) => return Some(Err(e)), - } - - // If we don't have enough data to decode a message, try to read - // more - let bytes_read = match self.stream.read(&mut tmp_read_buf) { - Ok(br) => br, - Err(e) => return Some(Err(e.into())), - }; - if bytes_read == 0 { - // The stream terminated - return None; - } - self.read_buf.extend_from_slice(&tmp_read_buf[..bytes_read]); - } - } -} - -pub struct StdChannelNotify; - -impl ChannelNotify for StdChannelNotify { - type Sender = StdSender<()>; - type Receiver = StdReceiver<()>; - - fn unbounded() -> (Self::Sender, Self::Receiver) { - let (tx, rx) = std::sync::mpsc::channel(); - (StdSender(tx), StdReceiver(rx)) - } -} - -pub struct StdSender(std::sync::mpsc::Sender); - -impl Sender for StdSender { - fn send(&self, value: T) -> Result<()> { - self.0 - .send(value) - .map_err(|e| Error::ChannelSend(e.to_string())) - } -} - -pub struct StdReceiver(std::sync::mpsc::Receiver); - -impl Receiver for StdReceiver { - fn recv(&self) -> Result { - self.0.recv().map_err(|e| Error::ChannelRecv(e.to_string())) - } -} diff --git a/abci/src/runtime/tokio.rs b/abci/src/runtime/tokio.rs deleted file mode 100644 index 3862d9af5..000000000 --- a/abci/src/runtime/tokio.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Tokio runtime-specific types. - -use crate::codec::{TspDecoder, TspEncoder}; -use crate::runtime::{ - ChannelNotify, ClientCodec, Receiver, Runtime, Sender, ServerCodec, TaskSpawner, TcpListener, - TcpStream, -}; -use crate::{Error, Result}; -use async_trait::async_trait; -use bytes::BytesMut; -use futures::task::{Context, Poll}; -use futures::{Future, Sink, Stream}; -use pin_project::pin_project; -use std::net::SocketAddr; -use std::pin::Pin; -use tendermint::abci::{request, response}; -use tokio_util::codec::{Decoder, Encoder, Framed}; - -/// Tokio runtime. -pub struct Tokio; - -impl Runtime for Tokio { - type TcpStream = TokioTcpStream; - type TcpListener = TokioTcpListener; - type TaskSpawner = TokioTaskSpawner; - type ServerCodec = TokioServerCodec; - type ClientCodec = TokioClientCodec; - type ChannelNotify = TokioChannelNotify; -} - -pub struct TokioTcpStream(tokio::net::TcpStream); - -#[async_trait] -impl TcpStream for TokioTcpStream { - async fn connect(addr: &str) -> Result { - Ok(Self(tokio::net::TcpStream::connect(addr).await?)) - } -} - -pub struct TokioTcpListener(tokio::net::TcpListener); - -#[async_trait] -impl TcpListener for TokioTcpListener { - async fn bind(addr: &str) -> Result { - Ok(Self(tokio::net::TcpListener::bind(addr).await?)) - } - - fn local_addr(&self) -> Result { - Ok(self.0.local_addr()?.to_string()) - } - - async fn accept(&self) -> Result<(TokioTcpStream, SocketAddr)> { - let (stream, addr) = self.0.accept().await?; - Ok((TokioTcpStream(stream), addr)) - } -} - -pub struct TokioTaskSpawner; - -impl TaskSpawner for TokioTaskSpawner { - fn spawn_and_forget(task: T) - where - T: Future + Send + 'static, - T::Output: Send + 'static, - { - let _ = tokio::spawn(task); - } -} - -#[pin_project] -pub struct TokioServerCodec(#[pin] Framed); - -impl ServerCodec for TokioServerCodec { - fn from_tcp_stream(stream: TokioTcpStream) -> Self { - Self(Framed::new(stream.0, TspServerCodec::default())) - } -} - -impl Stream for TokioServerCodec { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().0.poll_next(cx) - } -} - -impl Sink for TokioServerCodec { - type Error = Error; - - fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().0.poll_ready(cx) - } - - fn start_send(self: Pin<&mut Self>, item: response::Response) -> Result<()> { - self.project().0.start_send(item) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().0.poll_flush(cx) - } - - fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().0.poll_close(cx) - } -} - -pub struct TspServerCodec { - decoder: TspDecoder, -} - -impl Default for TspServerCodec { - fn default() -> Self { - Self { - decoder: TspDecoder::new(), - } - } -} - -impl Encoder for TspServerCodec { - type Error = Error; - - fn encode(&mut self, item: response::Response, dst: &mut BytesMut) -> Result<()> { - TspEncoder::encode_response(item, dst) - } -} - -impl Decoder for TspServerCodec { - type Item = request::Request; - type Error = Error; - - fn decode(&mut self, src: &mut BytesMut) -> Result> { - self.decoder.decode_request(src) - } -} - -#[pin_project] -pub struct TokioClientCodec(#[pin] Framed); - -impl ClientCodec for TokioClientCodec { - fn from_tcp_stream(stream: TokioTcpStream) -> Self { - Self(Framed::new(stream.0, TspClientCodec::default())) - } -} - -impl Stream for TokioClientCodec { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().0.poll_next(cx) - } -} - -impl Sink for TokioClientCodec { - type Error = Error; - - fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().0.poll_ready(cx) - } - - fn start_send(self: Pin<&mut Self>, item: request::Request) -> Result<()> { - self.project().0.start_send(item) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().0.poll_flush(cx) - } - - fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().0.poll_close(cx) - } -} - -pub struct TspClientCodec { - decoder: TspDecoder, -} - -impl Default for TspClientCodec { - fn default() -> Self { - Self { - decoder: TspDecoder::new(), - } - } -} - -impl Encoder for TspClientCodec { - type Error = Error; - - fn encode(&mut self, item: request::Request, dst: &mut BytesMut) -> Result<()> { - TspEncoder::encode_request(item, dst) - } -} - -impl Decoder for TspClientCodec { - type Item = response::Response; - type Error = Error; - - fn decode(&mut self, src: &mut BytesMut) -> Result> { - self.decoder.decode_response(src) - } -} - -pub struct TokioChannelNotify; - -impl ChannelNotify for TokioChannelNotify { - type Sender = TokioSender<()>; - type Receiver = TokioReceiver<()>; - - fn unbounded() -> (Self::Sender, Self::Receiver) { - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - (TokioSender(tx), TokioReceiver(rx)) - } -} - -pub struct TokioSender(tokio::sync::mpsc::UnboundedSender); - -#[async_trait] -impl Sender for TokioSender { - async fn send(&self, value: T) -> Result<()> { - self.0 - .send(value) - .map_err(|e| Error::ChannelSend(e.to_string())) - } -} - -pub struct TokioReceiver(tokio::sync::mpsc::UnboundedReceiver); - -#[async_trait] -impl Receiver for TokioReceiver { - async fn recv(&mut self) -> Result { - self.0.recv().await.ok_or(Error::ChannelSenderClosed) - } -} diff --git a/abci/src/server.rs b/abci/src/server.rs index abf39766c..617c9cf06 100644 --- a/abci/src/server.rs +++ b/abci/src/server.rs @@ -1,175 +1,10 @@ //! ABCI servers. -#[cfg(feature = "async")] -use crate::runtime::{ChannelNotify, Receiver}; -use crate::runtime::{Runtime, ServerCodec, TaskSpawner, TcpListener}; -use crate::{Application, Result}; -use log::{debug, error, info}; -use tendermint::abci::request; - -/// ABCI server for a specific application and runtime. -pub struct Server { - app: App, - listener: Rt::TcpListener, - local_addr: String, - #[cfg(feature = "async")] - term_rx: ::Receiver, -} - -// Code common to both the async and non-async runtimes. -impl Server { - /// Get the local address for the server, once bound. - pub fn local_addr(&self) -> String { - self.local_addr.clone() - } -} - -#[cfg(feature = "async")] -impl Server -where - App: Application, - Rt: Runtime, -{ - /// Bind our ABCI application server to the given address. - /// - /// On success, returns our server and the sending end of a channel we can - /// use to terminate the server while it's listening. - pub async fn bind>( - addr: S, - app: App, - ) -> Result<(Self, ::Sender)> { - let listener = Rt::TcpListener::bind(addr.as_ref()).await?; - let (term_tx, term_rx) = ::unbounded(); - let local_addr = listener.local_addr()?; - Ok(( - Self { - app, - listener, - local_addr, - term_rx, - }, - term_tx, - )) - } - - /// Start listening for incoming connections. - pub async fn listen(mut self) -> Result<()> { - use futures::FutureExt; - - loop { - futures::select! { - result = self.listener.accept().fuse() => match result { - Ok(r) => { - let (stream, addr) = r; - info!("Incoming connection from: {}", addr.to_string()); - self.spawn_client_handler(stream).await; - }, - Err(e) => { - error!("Failed to accept incoming connection: {:?}", e); - } - }, - _ = self.term_rx.recv().fuse() => { - info!("Server terminated"); - return Ok(()) - } - } - } - } - - async fn spawn_client_handler(&self, stream: Rt::TcpStream) { - Rt::TaskSpawner::spawn_and_forget(Self::handle_client(stream, self.app.clone())); - } - - async fn handle_client(stream: Rt::TcpStream, app: App) { - use futures::{SinkExt, StreamExt}; - - let mut codec = Rt::ServerCodec::from_tcp_stream(stream); - loop { - let req: request::Request = match codec.next().await { - Some(result) => match result { - Ok(r) => r, - Err(e) => { - error!("Failed to read request from client: {}", e); - return; - } - }, - None => { - info!("Client terminated connection"); - return; - } - }; - debug!("Got incoming request from client: {:?}", req); - let res = app.handle(req); - debug!("Sending outgoing response: {:?}", res); - if let Err(e) = codec.send(res).await { - error!("Failed to write outgoing response to client: {}", e); - return; - } - } - } -} - -#[cfg(not(feature = "async"))] -impl Server -where - App: Application, - Rt: Runtime, -{ - /// Bind our ABCI application server to the given address. - pub fn bind>(addr: S, app: App) -> Result { - let listener = Rt::TcpListener::bind(addr.as_ref())?; - let local_addr = listener.local_addr()?; - Ok(Self { - app, - listener, - local_addr, - }) - } - - /// Start listening for incoming connections. - pub fn listen(self) -> Result<()> { - loop { - match self.listener.accept() { - Ok(r) => { - let (stream, addr) = r; - info!("Incoming connection from: {}", addr.to_string()); - self.spawn_client_handler(stream); - } - Err(e) => { - error!("Failed to accept incoming connection: {:?}", e); - } - } - } - } - - fn spawn_client_handler(&self, stream: Rt::TcpStream) { - let app_clone = self.app.clone(); - Rt::TaskSpawner::spawn_and_forget(move || Self::handle_client(stream, app_clone)); - } - - fn handle_client(stream: Rt::TcpStream, app: App) { - let mut codec = Rt::ServerCodec::from_tcp_stream(stream); - loop { - let req: request::Request = match codec.next() { - Some(result) => match result { - Ok(r) => r, - Err(e) => { - error!("Failed to read request from client: {}", e); - return; - } - }, - None => { - info!("Client terminated connection"); - return; - } - }; - debug!("Got incoming request from client: {:?}", req); - let res = app.handle(req); - debug!("Sending outgoing response: {:?}", res); - if let Err(e) = codec.send(res) { - error!("Failed to write outgoing response to client: {:?}", e); - return; - } - } - } -} +#[cfg(feature = "blocking")] +pub mod blocking; +#[cfg(feature = "non-blocking")] +pub mod non_blocking; + +/// The default read buffer size for the server when reading incoming requests +/// from client connections. +pub const DEFAULT_SERVER_READ_BUF_SIZE: usize = 1024 * 1024; diff --git a/abci/src/server/blocking.rs b/abci/src/server/blocking.rs new file mode 100644 index 000000000..89577dde6 --- /dev/null +++ b/abci/src/server/blocking.rs @@ -0,0 +1,127 @@ +//! Blocking ABCI server. + +use crate::codec::blocking::Codec; +use crate::runtime::blocking::{Runtime, TcpListener, TcpStream}; +use crate::server::DEFAULT_SERVER_READ_BUF_SIZE; +use crate::{Application, Result}; +use log::{debug, error, info}; +use tendermint::abci::request; + +/// Runtime-dependent blocking ABCI server. +/// +/// Blocking servers, unfortunately, cannot be terminated gracefully since they +/// block on their listener. +pub struct Server { + app: App, + listener: Rt::TcpListener, + local_addr: String, + read_buf_size: usize, +} + +impl Server +where + App: Application, + Rt: Runtime, +{ + /// Start listening for incoming connections. + pub fn listen(self) -> Result<()> { + loop { + match self.listener.accept() { + Ok(r) => { + let (stream, addr) = r; + info!("Incoming connection from: {}", addr.to_string()); + self.spawn_client_handler(stream); + } + Err(e) => { + error!("Failed to accept incoming connection: {:?}", e); + } + } + } + } + + fn spawn_client_handler(&self, stream: Rt::TcpStream) { + let app_clone = self.app.clone(); + let read_buf_size = self.read_buf_size; + Rt::spawn_and_forget(move || Self::handle_client(stream, app_clone, read_buf_size)); + } + + fn handle_client(stream: Rt::TcpStream, app: App, read_buf_size: usize) { + let mut codec = Rt::ServerCodec::new(stream.into_inner(), read_buf_size); + loop { + let req: request::Request = match codec.next() { + Some(result) => match result { + Ok(r) => r, + Err(e) => { + error!("Failed to read request from client: {}", e); + return; + } + }, + None => { + info!("Client terminated connection"); + return; + } + }; + debug!("Got incoming request from client: {:?}", req); + let res = app.handle(req); + debug!("Sending outgoing response: {:?}", res); + if let Err(e) = codec.send(res) { + error!("Failed to write outgoing response to client: {:?}", e); + return; + } + } + } + + /// Get the local address for the server, once bound. + pub fn local_addr(&self) -> String { + self.local_addr.clone() + } +} + +/// Allows for construction and configuration of a blocking ABCI server. +pub struct ServerBuilder { + read_buf_size: usize, + _runtime: std::marker::PhantomData, +} + +impl ServerBuilder { + /// Constructor for a server builder that allows for customization of the + /// read buffer size. + pub fn new(read_buf_size: usize) -> Self { + Self { + read_buf_size, + _runtime: Default::default(), + } + } + + /// Constructor for a blocking ABCI server. + /// + /// Attempts to bind the specified application to the given network + /// address. + pub fn bind(self, addr: S, app: App) -> Result> + where + S: AsRef, + App: Application, + { + let listener = Rt::TcpListener::bind(addr.as_ref())?; + let local_addr = listener.local_addr()?; + Ok(Server { + app, + listener, + local_addr, + read_buf_size: self.read_buf_size, + }) + } +} + +impl Default for ServerBuilder { + fn default() -> Self { + Self { + read_buf_size: DEFAULT_SERVER_READ_BUF_SIZE, + _runtime: Default::default(), + } + } +} + +#[cfg(feature = "runtime-std")] +/// Convenience export for when using Rust's standard library as your runtime. +pub type StdServerBuilder = ServerBuilder; diff --git a/abci/src/server/non_blocking.rs b/abci/src/server/non_blocking.rs new file mode 100644 index 000000000..4ab0ac98c --- /dev/null +++ b/abci/src/server/non_blocking.rs @@ -0,0 +1,152 @@ +//! Non-blocking ABCI server. + +use crate::codec::non_blocking::Codec; +use crate::runtime::non_blocking::{ChannelNotify, Receiver, Runtime, TcpListener, TcpStream}; +use crate::server::DEFAULT_SERVER_READ_BUF_SIZE; +use crate::{Application, Result}; +use futures::{SinkExt, StreamExt}; +use log::{debug, error, info}; +use tendermint::abci::request; + +/// Non-blocking ABCI server for a specific application and runtime. +pub struct Server { + app: App, + listener: Rt::TcpListener, + local_addr: String, + term_rx: ::Receiver, + read_buf_size: usize, +} + +impl Server +where + App: Application, + Rt: Runtime, +{ + /// Start listening for incoming connections. + pub async fn listen(mut self) -> Result<()> { + use futures::FutureExt; + + loop { + futures::select! { + result = self.listener.accept().fuse() => match result { + Ok(r) => { + let (stream, addr) = r; + info!("Incoming connection from: {}", addr.to_string()); + self.spawn_client_handler(stream).await; + }, + Err(e) => { + error!("Failed to accept incoming connection: {:?}", e); + } + }, + _ = self.term_rx.recv().fuse() => { + info!("Server terminated"); + return Ok(()) + } + } + } + } + + async fn spawn_client_handler(&self, stream: Rt::TcpStream) { + Rt::spawn_and_forget(Self::handle_client( + stream, + self.app.clone(), + self.read_buf_size, + )); + } + + async fn handle_client(stream: Rt::TcpStream, app: App, read_buf_size: usize) { + let mut codec: Rt::ServerCodec = Rt::ServerCodec::new(stream.into_inner(), read_buf_size); + loop { + let req: request::Request = match codec.next().await { + Some(result) => match result { + Ok(r) => r, + Err(e) => { + error!("Failed to read request from client: {}", e); + return; + } + }, + None => { + info!("Client terminated connection"); + return; + } + }; + debug!("Got incoming request from client: {:?}", req); + let res = app.handle(req); + debug!("Sending outgoing response: {:?}", res); + if let Err(e) = codec.send(res).await { + error!("Failed to write outgoing response to client: {}", e); + return; + } + } + } + + /// Get the local address for the server, once bound. + pub fn local_addr(&self) -> String { + self.local_addr.clone() + } +} + +/// Allows for construction and configuration of a non-blocking ABCI server. +pub struct ServerBuilder { + read_buf_size: usize, + _runtime: std::marker::PhantomData, +} + +impl ServerBuilder { + /// Constructor allowing for customization of server parameters. + pub fn new(read_buf_size: usize) -> Self { + Self { + read_buf_size, + _runtime: Default::default(), + } + } + + /// Bind our ABCI application server to the given address. + /// + /// On success, returns our server and the sending end of a channel we can + /// use to terminate the server while it's listening. + pub async fn bind( + self, + addr: S, + app: App, + ) -> Result<( + Server, + ::Sender, + )> + where + S: AsRef, + App: Application, + { + let listener = Rt::TcpListener::bind(addr.as_ref()).await?; + let (term_tx, term_rx) = ::unbounded(); + let local_addr = listener.local_addr()?; + Ok(( + Server { + app, + listener, + local_addr, + term_rx, + read_buf_size: self.read_buf_size, + }, + term_tx, + )) + } +} + +impl Default for ServerBuilder { + fn default() -> Self { + Self { + read_buf_size: DEFAULT_SERVER_READ_BUF_SIZE, + _runtime: Default::default(), + } + } +} + +#[cfg(feature = "runtime-tokio")] +/// Convenience export for when using Tokio's runtime. +pub type TokioServerBuilder = ServerBuilder; + +#[cfg(feature = "runtime-async-std")] +/// Convenience export for when using `async-std`'s runtime. +pub type AsyncStdServerBuilder = + ServerBuilder; diff --git a/abci/tests/async_std.rs b/abci/tests/async_std.rs index e21cf153e..e89d2cfff 100644 --- a/abci/tests/async_std.rs +++ b/abci/tests/async_std.rs @@ -1,15 +1,15 @@ //! `async-std`-based ABCI client/server integration tests. #[cfg(all( - feature = "async", + feature = "non-blocking", feature = "runtime-async-std", feature = "client", feature = "echo-app" ))] mod async_std_integration { use tendermint::abci::request; - use tendermint_abci::runtime::Sender; - use tendermint_abci::{AsyncStdClient, AsyncStdServer, EchoApp}; + use tendermint_abci::runtime::non_blocking::Sender; + use tendermint_abci::{AsyncStdClientBuilder, AsyncStdServerBuilder, EchoApp}; #[async_std::test] async fn echo() { @@ -18,13 +18,17 @@ mod async_std_integration { message: format!("echo {}", r), }) .collect::>(); - let (server, term_tx) = AsyncStdServer::bind("127.0.0.1:0", EchoApp::default()) + let (server, term_tx) = AsyncStdServerBuilder::default() + .bind("127.0.0.1:0", EchoApp::default()) .await .unwrap(); let server_addr = server.local_addr(); let server_handle = async_std::task::spawn(async move { server.listen().await }); - let mut client = AsyncStdClient::connect(server_addr).await.unwrap(); + let mut client = AsyncStdClientBuilder::default() + .connect(server_addr) + .await + .unwrap(); for req in requests { let res = client.echo(req.clone()).await.unwrap(); assert_eq!(res.message, req.message); diff --git a/abci/tests/std.rs b/abci/tests/std.rs index 97c12255c..6efd2bfd4 100644 --- a/abci/tests/std.rs +++ b/abci/tests/std.rs @@ -1,14 +1,14 @@ //! Rust standard library-based ABCI client/server integration tests. #[cfg(all( - not(feature = "async"), + feature = "blocking", feature = "runtime-std", feature = "client", feature = "echo-app" ))] mod std_integration { use tendermint::abci::request; - use tendermint_abci::{EchoApp, StdClient, StdServer}; + use tendermint_abci::{EchoApp, StdClientBuilder, StdServerBuilder}; #[test] fn echo() { @@ -17,11 +17,13 @@ mod std_integration { message: format!("echo {}", r), }) .collect::>(); - let server = StdServer::bind("127.0.0.1:0", EchoApp::default()).unwrap(); + let server = StdServerBuilder::default() + .bind("127.0.0.1:0", EchoApp::default()) + .unwrap(); let server_addr = server.local_addr(); let _ = std::thread::spawn(move || server.listen()); - let mut client = StdClient::connect(server_addr).unwrap(); + let mut client = StdClientBuilder::default().connect(server_addr).unwrap(); for req in requests { let res = client.echo(req.clone()).unwrap(); assert_eq!(res.message, req.message); diff --git a/abci/tests/tokio.rs b/abci/tests/tokio.rs index 8ef03bc8b..9e692dff0 100644 --- a/abci/tests/tokio.rs +++ b/abci/tests/tokio.rs @@ -1,15 +1,15 @@ //! Tokio-based ABCI client/server integration tests. #[cfg(all( - feature = "async", + feature = "non-blocking", feature = "runtime-tokio", feature = "client", feature = "echo-app" ))] mod tokio_integration { use tendermint::abci::request; - use tendermint_abci::runtime::Sender; - use tendermint_abci::{EchoApp, TokioClient, TokioServer}; + use tendermint_abci::runtime::non_blocking::Sender; + use tendermint_abci::{EchoApp, TokioClientBuilder, TokioServerBuilder}; #[tokio::test] async fn echo() { @@ -18,13 +18,17 @@ mod tokio_integration { message: format!("echo {}", r), }) .collect::>(); - let (server, term_tx) = TokioServer::bind("127.0.0.1:0", EchoApp::default()) + let (server, term_tx) = TokioServerBuilder::default() + .bind("127.0.0.1:0", EchoApp::default()) .await .unwrap(); let server_addr = server.local_addr(); let server_handle = tokio::spawn(async move { server.listen().await }); - let mut client = TokioClient::connect(server_addr).await.unwrap(); + let mut client = TokioClientBuilder::default() + .connect(server_addr) + .await + .unwrap(); for req in requests { let res = client.echo(req.clone()).await.unwrap(); assert_eq!(res.message, req.message);