mirror of
https://github.com/OMGeeky/tarpc.git
synced 2026-01-22 19:15:22 +01:00
Remove deprecated tokio-proto and replace with homegrown rpc framework (#199)
# New Crates - crate rpc contains the core client/server request-response framework, as well as a transport trait. - crate bincode-transport implements a transport that works almost exactly as tarpc works today (not to say it's wire-compatible). - crate trace has some foundational types for tracing. This isn't really fleshed out yet, but it's useful for in-process log tracing, at least. All crates are now at the top level. e.g. tarpc-plugins is now tarpc/plugins rather than tarpc/src/plugins. tarpc itself is now a *very* small code surface, as most functionality has been moved into the other more granular crates. # New Features - deadlines: all requests specify a deadline, and a server will stop processing a response when past its deadline. - client cancellation propagation: when a client drops a request, the client sends a message to the server informing it to cancel its response. This means cancellations can propagate across multiple server hops. - trace context stuff as mentioned above - more server configuration for total connection limits, per-connection request limits, etc. # Removals - no more shutdown handle. I left it out for now because of time and not being sure what the right solution is. - all async now, no blocking stub or server interface. This helps with maintainability, and async/await makes async code much more usable. The service trait is thusly renamed Service, and the client is renamed Client. - no built-in transport. Tarpc is now transport agnostic (see bincode-transport for transitioning existing uses). - going along with the previous bullet, no preferred transport means no TLS support at this time. We could make a tls transport or make bincode-transport compatible with TLS. - a lot of examples were removed because I couldn't keep up with maintaining all of them. Hopefully the ones I kept are still illustrative. - no more plugins! # Open Questions 1. Should client.send() return `Future<Response>` or `Future<Future<Response>>`? The former appears more ergonomic but it doesn’t allow concurrent requests with a single client handle. The latter is less ergonomic but yields back control of the client once it’s successfully sent out the request. Should we offer fns for both? 2. Should rpc service! Fns take &mut self or &self or self? The service needs to impl Clone anyway, technically we only need to clone it once per connection, and then leave it up to the user to decide if they want to clone it per RPC. In practice, everyone doing nontrivial stuff will need to clone it per RPC, I think. 3. Do the request/response structs look ok? 4. Is supporting server shutdown/lameduck important? Fixes #178 #155 #124 #104 #83 #38
This commit is contained in:
39
tarpc/Cargo.toml
Normal file
39
tarpc/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
cargo-features = ["namespaced-features"]
|
||||
|
||||
[package]
|
||||
name = "tarpc"
|
||||
version = "0.12.1"
|
||||
authors = ["Adam Wright <adam.austin.wright@gmail.com>", "Tim Kuehn <timothy.j.kuehn@gmail.com>"]
|
||||
edition = "2018"
|
||||
namespaced-features = true
|
||||
license = "MIT"
|
||||
documentation = "https://docs.rs/tarpc"
|
||||
homepage = "https://github.com/google/tarpc"
|
||||
repository = "https://github.com/google/tarpc"
|
||||
keywords = ["rpc", "network", "server", "api", "tls"]
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
readme = "README.md"
|
||||
description = "An RPC framework for Rust with a focus on ease of use."
|
||||
|
||||
[features]
|
||||
serde = ["rpc/serde", "crate:serde", "serde/derive"]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "google/tarpc" }
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
serde = { optional = true, version = "1.0" }
|
||||
tarpc-plugins = { path = "../plugins", version = "0.4.0" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
||||
[target.'cfg(not(test))'.dependencies]
|
||||
futures-preview = { git = "https://github.com/rust-lang-nursery/futures-rs" }
|
||||
|
||||
[dev-dependencies]
|
||||
humantime = "1.0"
|
||||
futures-preview = { git = "https://github.com/rust-lang-nursery/futures-rs", features = ["compat", "tokio-compat"] }
|
||||
bincode-transport = { path = "../bincode-transport" }
|
||||
env_logger = "0.5"
|
||||
tokio = "0.1"
|
||||
tokio-executor = "0.1"
|
||||
1
tarpc/clippy.toml
Normal file
1
tarpc/clippy.toml
Normal file
@@ -0,0 +1 @@
|
||||
doc-valid-idents = ["gRPC"]
|
||||
190
tarpc/examples/pubsub.rs
Normal file
190
tarpc/examples/pubsub.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
#![feature(
|
||||
arbitrary_self_types,
|
||||
pin,
|
||||
futures_api,
|
||||
await_macro,
|
||||
async_await,
|
||||
existential_type,
|
||||
proc_macro_hygiene,
|
||||
)]
|
||||
|
||||
use futures::{
|
||||
future::{self, Ready},
|
||||
prelude::*,
|
||||
Future,
|
||||
};
|
||||
use rpc::{
|
||||
client, context,
|
||||
server::{self, Handler, Server},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
net::SocketAddr,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub mod subscriber {
|
||||
tarpc::service! {
|
||||
rpc receive(message: String);
|
||||
}
|
||||
}
|
||||
|
||||
pub mod publisher {
|
||||
use std::net::SocketAddr;
|
||||
tarpc::service! {
|
||||
rpc broadcast(message: String);
|
||||
rpc subscribe(id: u32, address: SocketAddr) -> Result<(), String>;
|
||||
rpc unsubscribe(id: u32);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Subscriber {
|
||||
id: u32,
|
||||
}
|
||||
|
||||
impl subscriber::Service for Subscriber {
|
||||
type ReceiveFut = Ready<()>;
|
||||
|
||||
fn receive(&self, _: context::Context, message: String) -> Self::ReceiveFut {
|
||||
println!("{} received message: {}", self.id, message);
|
||||
future::ready(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscriber {
|
||||
async fn listen(id: u32, config: server::Config) -> io::Result<SocketAddr> {
|
||||
let incoming = bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
|
||||
let addr = incoming.local_addr();
|
||||
tokio_executor::spawn(
|
||||
Server::new(config)
|
||||
.incoming(incoming)
|
||||
.take(1)
|
||||
.respond_with(subscriber::serve(Subscriber { id }))
|
||||
.unit_error()
|
||||
.boxed()
|
||||
.compat()
|
||||
);
|
||||
Ok(addr)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Publisher {
|
||||
clients: Arc<Mutex<HashMap<u32, subscriber::Client>>>,
|
||||
}
|
||||
|
||||
impl Publisher {
|
||||
fn new() -> Publisher {
|
||||
Publisher {
|
||||
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl publisher::Service for Publisher {
|
||||
existential type BroadcastFut: Future<Output = ()>;
|
||||
|
||||
fn broadcast(&self, _: context::Context, message: String) -> Self::BroadcastFut {
|
||||
async fn broadcast(clients: Arc<Mutex<HashMap<u32, subscriber::Client>>>, message: String) {
|
||||
let mut clients = clients.lock().unwrap().clone();
|
||||
for client in clients.values_mut() {
|
||||
// Ignore failing subscribers. In a real pubsub,
|
||||
// you'd want to continually retry until subscribers
|
||||
// ack.
|
||||
let _ = await!(client.receive(context::current(), message.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
broadcast(self.clients.clone(), message)
|
||||
}
|
||||
|
||||
existential type SubscribeFut: Future<Output = Result<(), String>>;
|
||||
|
||||
fn subscribe(&self, _: context::Context, id: u32, addr: SocketAddr) -> Self::SubscribeFut {
|
||||
async fn subscribe(
|
||||
clients: Arc<Mutex<HashMap<u32, subscriber::Client>>>,
|
||||
id: u32,
|
||||
addr: SocketAddr,
|
||||
) -> io::Result<()> {
|
||||
let conn = await!(bincode_transport::connect(&addr))?;
|
||||
let subscriber = await!(subscriber::new_stub(client::Config::default(), conn))?;
|
||||
println!("Subscribing {}.", id);
|
||||
clients.lock().unwrap().insert(id, subscriber);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
subscribe(Arc::clone(&self.clients), id, addr).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
existential type UnsubscribeFut: Future<Output = ()>;
|
||||
|
||||
fn unsubscribe(&self, _: context::Context, id: u32) -> Self::UnsubscribeFut {
|
||||
println!("Unsubscribing {}", id);
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let None = clients.remove(&id) {
|
||||
eprintln!(
|
||||
"Client {} not found. Existings clients: {:?}",
|
||||
id, &*clients
|
||||
);
|
||||
}
|
||||
future::ready(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> io::Result<()> {
|
||||
env_logger::init();
|
||||
let transport = bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
|
||||
let publisher_addr = transport.local_addr();
|
||||
tokio_executor::spawn(
|
||||
Server::new(server::Config::default())
|
||||
.incoming(transport)
|
||||
.take(1)
|
||||
.respond_with(publisher::serve(Publisher::new()))
|
||||
.unit_error()
|
||||
.boxed()
|
||||
.compat()
|
||||
);
|
||||
|
||||
let subscriber1 = await!(Subscriber::listen(0, server::Config::default()))?;
|
||||
let subscriber2 = await!(Subscriber::listen(1, server::Config::default()))?;
|
||||
|
||||
let publisher_conn = bincode_transport::connect(&publisher_addr);
|
||||
let publisher_conn = await!(publisher_conn)?;
|
||||
let mut publisher = await!(publisher::new_stub(
|
||||
client::Config::default(),
|
||||
publisher_conn
|
||||
))?;
|
||||
|
||||
if let Err(e) = await!(publisher.subscribe(context::current(), 0, subscriber1))? {
|
||||
eprintln!("Couldn't subscribe subscriber 0: {}", e);
|
||||
}
|
||||
if let Err(e) = await!(publisher.subscribe(context::current(), 1, subscriber2))? {
|
||||
eprintln!("Couldn't subscribe subscriber 1: {}", e);
|
||||
}
|
||||
|
||||
println!("Broadcasting...");
|
||||
await!(publisher.broadcast(context::current(), "hello to all".to_string()))?;
|
||||
await!(publisher.unsubscribe(context::current(), 1))?;
|
||||
await!(publisher.broadcast(context::current(), "hi again".to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tokio::run(
|
||||
run()
|
||||
.boxed()
|
||||
.map_err(|e| panic!(e))
|
||||
.boxed()
|
||||
.compat(),
|
||||
);
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
90
tarpc/examples/readme.rs
Normal file
90
tarpc/examples/readme.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
#![feature(
|
||||
futures_api,
|
||||
pin,
|
||||
arbitrary_self_types,
|
||||
await_macro,
|
||||
async_await,
|
||||
proc_macro_hygiene,
|
||||
)]
|
||||
|
||||
use futures::{
|
||||
future::{self, Ready},
|
||||
prelude::*,
|
||||
};
|
||||
use rpc::{
|
||||
client, context,
|
||||
server::{self, Handler, Server},
|
||||
};
|
||||
use std::io;
|
||||
|
||||
// This is the service definition. It looks a lot like a trait definition.
|
||||
// It defines one RPC, hello, which takes one arg, name, and returns a String.
|
||||
|
||||
tarpc::service! {
|
||||
rpc hello(name: String) -> String;
|
||||
}
|
||||
|
||||
// This is the type that implements the generated Service trait. It is the business logic
|
||||
// and is used to start the server.
|
||||
#[derive(Clone)]
|
||||
struct HelloServer;
|
||||
|
||||
impl Service for HelloServer {
|
||||
// Each defined rpc generates two items in the trait, a fn that serves the RPC, and
|
||||
// an associated type representing the future output by the fn.
|
||||
|
||||
type HelloFut = Ready<String>;
|
||||
|
||||
fn hello(&self, _: context::Context, name: String) -> Self::HelloFut {
|
||||
future::ready(format!("Hello, {}!", name))
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> io::Result<()> {
|
||||
// bincode_transport is provided by the associated crate bincode-transport. It makes it easy
|
||||
// to start up a serde-powered bincode serialization strategy over TCP.
|
||||
let transport = bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
|
||||
let addr = transport.local_addr();
|
||||
|
||||
// The server is configured with the defaults.
|
||||
let server = Server::new(server::Config::default())
|
||||
// Server can listen on any type that implements the Transport trait.
|
||||
.incoming(transport)
|
||||
// Close the stream after the client connects
|
||||
.take(1)
|
||||
// serve is generated by the tarpc::service! macro. It takes as input any type implementing
|
||||
// the generated Service trait.
|
||||
.respond_with(serve(HelloServer));
|
||||
|
||||
tokio_executor::spawn(server.unit_error().boxed().compat());
|
||||
|
||||
let transport = await!(bincode_transport::connect(&addr))?;
|
||||
|
||||
// new_stub is generated by the tarpc::service! macro. Like Server, it takes a config and any
|
||||
// Transport as input, and returns a Client, also generated by the macro.
|
||||
// by the service mcro.
|
||||
let mut client = await!(new_stub(client::Config::default(), transport))?;
|
||||
|
||||
// The client has an RPC method for each RPC defined in tarpc::service!. It takes the same args
|
||||
// as defined, with the addition of a Context, which is always the first arg. The Context
|
||||
// specifies a deadline and trace information which can be helpful in debugging requests.
|
||||
let hello = await!(client.hello(context::current(), "Stim".to_string()))?;
|
||||
|
||||
println!("{}", hello);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tokio::run(
|
||||
run()
|
||||
.map_err(|e| eprintln!("Oh no: {}", e))
|
||||
.boxed()
|
||||
.compat(),
|
||||
);
|
||||
}
|
||||
110
tarpc/examples/server_calling_server.rs
Normal file
110
tarpc/examples/server_calling_server.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
#![feature(
|
||||
existential_type,
|
||||
arbitrary_self_types,
|
||||
pin,
|
||||
futures_api,
|
||||
await_macro,
|
||||
async_await,
|
||||
proc_macro_hygiene,
|
||||
)]
|
||||
|
||||
use crate::{add::Service as AddService, double::Service as DoubleService};
|
||||
use futures::{
|
||||
future::{self, Ready},
|
||||
prelude::*,
|
||||
};
|
||||
use rpc::{
|
||||
client, context,
|
||||
server::{self, Handler, Server},
|
||||
};
|
||||
use std::io;
|
||||
|
||||
pub mod add {
|
||||
tarpc::service! {
|
||||
/// Add two ints together.
|
||||
rpc add(x: i32, y: i32) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod double {
|
||||
tarpc::service! {
|
||||
/// 2 * x
|
||||
rpc double(x: i32) -> Result<i32, String>;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AddServer;
|
||||
|
||||
impl AddService for AddServer {
|
||||
type AddFut = Ready<i32>;
|
||||
|
||||
fn add(&self, _: context::Context, x: i32, y: i32) -> Self::AddFut {
|
||||
future::ready(x + y)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DoubleServer {
|
||||
add_client: add::Client,
|
||||
}
|
||||
|
||||
impl DoubleService for DoubleServer {
|
||||
existential type DoubleFut: Future<Output = Result<i32, String>> + Send;
|
||||
|
||||
fn double(&self, _: context::Context, x: i32) -> Self::DoubleFut {
|
||||
async fn double(mut client: add::Client, x: i32) -> Result<i32, String> {
|
||||
let result = await!(client.add(context::current(), x, x));
|
||||
result.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
double(self.add_client.clone(), x)
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> io::Result<()> {
|
||||
let add_listener = bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
|
||||
let addr = add_listener.local_addr();
|
||||
let add_server = Server::new(server::Config::default())
|
||||
.incoming(add_listener)
|
||||
.take(1)
|
||||
.respond_with(add::serve(AddServer));
|
||||
tokio_executor::spawn(add_server.unit_error().boxed().compat());
|
||||
|
||||
let to_add_server = await!(bincode_transport::connect(&addr))?;
|
||||
let add_client = await!(add::new_stub(client::Config::default(), to_add_server))?;
|
||||
|
||||
let double_listener = bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
|
||||
let addr = double_listener.local_addr();
|
||||
let double_server = rpc::Server::new(server::Config::default())
|
||||
.incoming(double_listener)
|
||||
.take(1)
|
||||
.respond_with(double::serve(DoubleServer { add_client }));
|
||||
tokio_executor::spawn(double_server.unit_error().boxed().compat());
|
||||
|
||||
let to_double_server = await!(bincode_transport::connect(&addr))?;
|
||||
let mut double_client = await!(double::new_stub(
|
||||
client::Config::default(),
|
||||
to_double_server
|
||||
))?;
|
||||
|
||||
for i in 1..=5 {
|
||||
println!("{:?}", await!(double_client.double(context::current(), i))?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
tokio::run(
|
||||
run()
|
||||
.map_err(|e| panic!(e))
|
||||
.boxed()
|
||||
.compat(),
|
||||
);
|
||||
}
|
||||
1
tarpc/rustfmt.toml
Normal file
1
tarpc/rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
edition = "Edition2018"
|
||||
135
tarpc/src/lib.rs
Normal file
135
tarpc/src/lib.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! tarpc is an RPC framework for rust with a focus on ease of use. Defining a
|
||||
//! service can be done in just a few lines of code, and most of the boilerplate of
|
||||
//! writing a server is taken care of for you.
|
||||
//!
|
||||
//! ## What is an RPC framework?
|
||||
//! "RPC" stands for "Remote Procedure Call," a function call where the work of
|
||||
//! producing the return value is being done somewhere else. When an rpc function is
|
||||
//! invoked, behind the scenes the function contacts some other process somewhere
|
||||
//! and asks them to evaluate the function instead. The original function then
|
||||
//! returns the value produced by the other process.
|
||||
//!
|
||||
//! RPC frameworks are a fundamental building block of most microservices-oriented
|
||||
//! architectures. Two well-known ones are [gRPC](http://www.grpc.io) and
|
||||
//! [Cap'n Proto](https://capnproto.org/).
|
||||
//!
|
||||
//! tarpc differentiates itself from other RPC frameworks by defining the schema in code,
|
||||
//! rather than in a separate language such as .proto. This means there's no separate compilation
|
||||
//! process, and no cognitive context switching between different languages. Additionally, it
|
||||
//! works with the community-backed library serde: any serde-serializable type can be used as
|
||||
//! arguments to tarpc fns.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! Here's a small service.
|
||||
//!
|
||||
//! ```rust
|
||||
//! #![feature(futures_api, pin, arbitrary_self_types, await_macro, async_await, proc_macro_hygiene)]
|
||||
//!
|
||||
//!
|
||||
//! use futures::{
|
||||
//! compat::TokioDefaultSpawner,
|
||||
//! future::{self, Ready},
|
||||
//! prelude::*,
|
||||
//! };
|
||||
//! use tarpc::{
|
||||
//! client, context,
|
||||
//! server::{self, Handler, Server},
|
||||
//! };
|
||||
//! use std::io;
|
||||
//!
|
||||
//! // This is the service definition. It looks a lot like a trait definition.
|
||||
//! // It defines one RPC, hello, which takes one arg, name, and returns a String.
|
||||
//! tarpc::service! {
|
||||
//! /// Returns a greeting for name.
|
||||
//! rpc hello(name: String) -> String;
|
||||
//! }
|
||||
//!
|
||||
//! // This is the type that implements the generated Service trait. It is the business logic
|
||||
//! // and is used to start the server.
|
||||
//! #[derive(Clone)]
|
||||
//! struct HelloServer;
|
||||
//!
|
||||
//! impl Service for HelloServer {
|
||||
//! // Each defined rpc generates two items in the trait, a fn that serves the RPC, and
|
||||
//! // an associated type representing the future output by the fn.
|
||||
//!
|
||||
//! type HelloFut = Ready<String>;
|
||||
//!
|
||||
//! fn hello(&self, _: context::Context, name: String) -> Self::HelloFut {
|
||||
//! future::ready(format!("Hello, {}!", name))
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! async fn run() -> io::Result<()> {
|
||||
//! // bincode_transport is provided by the associated crate bincode-transport. It makes it easy
|
||||
//! // to start up a serde-powered bincode serialization strategy over TCP.
|
||||
//! let transport = bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
|
||||
//! let addr = transport.local_addr();
|
||||
//!
|
||||
//! // The server is configured with the defaults.
|
||||
//! let server = Server::new(server::Config::default())
|
||||
//! // Server can listen on any type that implements the Transport trait.
|
||||
//! .incoming(transport)
|
||||
//! // Close the stream after the client connects
|
||||
//! .take(1)
|
||||
//! // serve is generated by the service! macro. It takes as input any type implementing
|
||||
//! // the generated Service trait.
|
||||
//! .respond_with(serve(HelloServer));
|
||||
//!
|
||||
//! tokio_executor::spawn(server.unit_error().boxed().compat());
|
||||
//!
|
||||
//! let transport = await!(bincode_transport::connect(&addr))?;
|
||||
//!
|
||||
//! // new_stub is generated by the service! macro. Like Server, it takes a config and any
|
||||
//! // Transport as input, and returns a Client, also generated by the macro.
|
||||
//! // by the service mcro.
|
||||
//! let mut client = await!(new_stub(client::Config::default(), transport))?;
|
||||
//!
|
||||
//! // The client has an RPC method for each RPC defined in service!. It takes the same args
|
||||
//! // as defined, with the addition of a Context, which is always the first arg. The Context
|
||||
//! // specifies a deadline and trace information which can be helpful in debugging requests.
|
||||
//! let hello = await!(client.hello(context::current(), "Stim".to_string()))?;
|
||||
//!
|
||||
//! println!("{}", hello);
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! fn main() {
|
||||
//! tarpc::init(TokioDefaultSpawner);
|
||||
//! tokio::run(run()
|
||||
//! .map_err(|e| eprintln!("Oh no: {}", e))
|
||||
//! .boxed()
|
||||
//! .compat(),
|
||||
//! );
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![deny(missing_docs, missing_debug_implementations)]
|
||||
#![feature(
|
||||
futures_api,
|
||||
pin,
|
||||
await_macro,
|
||||
async_await,
|
||||
decl_macro,
|
||||
)]
|
||||
#![cfg_attr(test, feature(proc_macro_hygiene, arbitrary_self_types))]
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use futures;
|
||||
pub use rpc::*;
|
||||
#[cfg(feature = "serde")]
|
||||
#[doc(hidden)]
|
||||
pub use serde;
|
||||
#[doc(hidden)]
|
||||
pub use tarpc_plugins::*;
|
||||
|
||||
/// Provides the macro used for constructing rpc services and client stubs.
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
363
tarpc/src/macros.rs
Normal file
363
tarpc/src/macros.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! add_serde_if_enabled {
|
||||
($(#[$attr:meta])* -- $i:item) => {
|
||||
$(#[$attr])*
|
||||
#[derive($crate::serde::Serialize, $crate::serde::Deserialize)]
|
||||
$i
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "serde"))]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! add_serde_if_enabled {
|
||||
($(#[$attr:meta])* -- $i:item) => {
|
||||
$(#[$attr])*
|
||||
$i
|
||||
}
|
||||
}
|
||||
|
||||
/// The main macro that creates RPC services.
|
||||
///
|
||||
/// Rpc methods are specified, mirroring trait syntax:
|
||||
///
|
||||
/// ```
|
||||
/// # #![feature(await_macro, pin, arbitrary_self_types, async_await, futures_api, proc_macro_hygiene)]
|
||||
/// # fn main() {}
|
||||
/// # tarpc::service! {
|
||||
/// /// Say hello
|
||||
/// rpc hello(name: String) -> String;
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Attributes can be attached to each rpc. These attributes
|
||||
/// will then be attached to the generated service traits'
|
||||
/// corresponding `fn`s, as well as to the client stubs' RPCs.
|
||||
///
|
||||
/// The following items are expanded in the enclosing module:
|
||||
///
|
||||
/// * `trait Service` -- defines the RPC service.
|
||||
/// * `fn serve` -- turns a service impl into a request handler.
|
||||
/// * `Client` -- a client stub with a fn for each RPC.
|
||||
/// * `fn new_stub` -- creates a new Client stub.
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! service {
|
||||
// Entry point
|
||||
(
|
||||
$(
|
||||
$(#[$attr:meta])*
|
||||
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* ) $(-> $out:ty)*;
|
||||
)*
|
||||
) => {
|
||||
$crate::service! {{
|
||||
$(
|
||||
$(#[$attr])*
|
||||
rpc $fn_name( $( $arg : $in_ ),* ) $(-> $out)*;
|
||||
)*
|
||||
}}
|
||||
};
|
||||
// Pattern for when the next rpc has an implicit unit return type.
|
||||
(
|
||||
{
|
||||
$(#[$attr:meta])*
|
||||
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* );
|
||||
|
||||
$( $unexpanded:tt )*
|
||||
}
|
||||
$( $expanded:tt )*
|
||||
) => {
|
||||
$crate::service! {
|
||||
{ $( $unexpanded )* }
|
||||
|
||||
$( $expanded )*
|
||||
|
||||
$(#[$attr])*
|
||||
rpc $fn_name( $( $arg : $in_ ),* ) -> ();
|
||||
}
|
||||
};
|
||||
// Pattern for when the next rpc has an explicit return type.
|
||||
(
|
||||
{
|
||||
$(#[$attr:meta])*
|
||||
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* ) -> $out:ty;
|
||||
|
||||
$( $unexpanded:tt )*
|
||||
}
|
||||
$( $expanded:tt )*
|
||||
) => {
|
||||
$crate::service! {
|
||||
{ $( $unexpanded )* }
|
||||
|
||||
$( $expanded )*
|
||||
|
||||
$(#[$attr])*
|
||||
rpc $fn_name( $( $arg : $in_ ),* ) -> $out;
|
||||
}
|
||||
};
|
||||
// Pattern for when all return types have been expanded
|
||||
(
|
||||
{ } // none left to expand
|
||||
$(
|
||||
$(#[$attr:meta])*
|
||||
rpc $fn_name:ident ( $( $arg:ident : $in_:ty ),* ) -> $out:ty;
|
||||
)*
|
||||
) => {
|
||||
$crate::add_serde_if_enabled! {
|
||||
#[derive(Debug)]
|
||||
#[doc(hidden)]
|
||||
#[allow(non_camel_case_types, unused)]
|
||||
--
|
||||
pub enum Request__ {
|
||||
$(
|
||||
$fn_name{ $($arg: $in_,)* }
|
||||
),*
|
||||
}
|
||||
}
|
||||
|
||||
$crate::add_serde_if_enabled! {
|
||||
#[derive(Debug)]
|
||||
#[doc(hidden)]
|
||||
#[allow(non_camel_case_types, unused)]
|
||||
--
|
||||
pub enum Response__ {
|
||||
$(
|
||||
$fn_name($out)
|
||||
),*
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: proc_macro can't currently parse $crate, so this needs to be imported for the
|
||||
// usage of snake_to_camel! to work.
|
||||
use $crate::futures::Future as Future__;
|
||||
|
||||
/// Defines the RPC service. The additional trait bounds are required so that services can
|
||||
/// multiplex requests across multiple tasks, potentially on multiple threads.
|
||||
pub trait Service: Clone + Send + 'static {
|
||||
$(
|
||||
$crate::snake_to_camel! {
|
||||
/// The type of future returned by `{}`.
|
||||
type $fn_name: Future__<Output = $out> + Send;
|
||||
}
|
||||
|
||||
$(#[$attr])*
|
||||
fn $fn_name(&self, ctx: $crate::context::Context, $($arg:$in_),*) -> $crate::ty_snake_to_camel!(Self::$fn_name);
|
||||
)*
|
||||
}
|
||||
|
||||
// TODO: use an existential type instead of this when existential types work.
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum Response<S: Service> {
|
||||
$(
|
||||
$fn_name($crate::ty_snake_to_camel!(<S as Service>::$fn_name)),
|
||||
)*
|
||||
}
|
||||
|
||||
impl<S: Service> ::std::fmt::Debug for Response<S> {
|
||||
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
|
||||
fmt.debug_struct("Response").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Service> ::std::future::Future for Response<S> {
|
||||
type Output = ::std::io::Result<Response__>;
|
||||
|
||||
fn poll(self: ::std::pin::Pin<&mut Self>, waker: &::std::task::LocalWaker)
|
||||
-> ::std::task::Poll<::std::io::Result<Response__>>
|
||||
{
|
||||
unsafe {
|
||||
match ::std::pin::Pin::get_mut_unchecked(self) {
|
||||
$(
|
||||
Response::$fn_name(resp) =>
|
||||
::std::pin::Pin::new_unchecked(resp)
|
||||
.poll(waker)
|
||||
.map(Response__::$fn_name)
|
||||
.map(Ok),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a serving function to use with rpc::server::Server.
|
||||
pub fn serve<S: Service>(service: S)
|
||||
-> impl FnMut($crate::context::Context, Request__) -> Response<S> + Send + 'static + Clone {
|
||||
move |ctx, req| {
|
||||
match req {
|
||||
$(
|
||||
Request__::$fn_name{ $($arg,)* } => {
|
||||
let resp = Service::$fn_name(&mut service.clone(), ctx, $($arg),*);
|
||||
Response::$fn_name(resp)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Clone, Debug)]
|
||||
/// The client stub that makes RPC calls to the server. Exposes a Future interface.
|
||||
pub struct Client($crate::client::Client<Request__, Response__>);
|
||||
|
||||
/// Returns a new client stub that sends requests over the given transport.
|
||||
pub async fn new_stub<T>(config: $crate::client::Config, transport: T)
|
||||
-> ::std::io::Result<Client>
|
||||
where
|
||||
T: $crate::Transport<
|
||||
Item = $crate::Response<Response__>,
|
||||
SinkItem = $crate::ClientMessage<Request__>> + Send,
|
||||
{
|
||||
Ok(Client(await!($crate::client::Client::new(config, transport))?))
|
||||
}
|
||||
|
||||
impl Client {
|
||||
$(
|
||||
#[allow(unused)]
|
||||
$(#[$attr])*
|
||||
pub fn $fn_name(&mut self, ctx: $crate::context::Context, $($arg: $in_),*)
|
||||
-> impl ::std::future::Future<Output = ::std::io::Result<$out>> + '_ {
|
||||
let request__ = Request__::$fn_name { $($arg,)* };
|
||||
let resp = self.0.call(ctx, request__);
|
||||
async move {
|
||||
match await!(resp)? {
|
||||
Response__::$fn_name(msg__) => ::std::result::Result::Ok(msg__),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allow dead code; we're just testing that the macro expansion compiles
|
||||
#[allow(dead_code)]
|
||||
#[cfg(test)]
|
||||
mod syntax_test {
|
||||
service! {
|
||||
#[deny(warnings)]
|
||||
#[allow(non_snake_case)]
|
||||
rpc TestCamelCaseDoesntConflict();
|
||||
rpc hello() -> String;
|
||||
#[doc="attr"]
|
||||
rpc attr(s: String) -> String;
|
||||
rpc no_args_no_return();
|
||||
rpc no_args() -> ();
|
||||
rpc one_arg(foo: String) -> i32;
|
||||
rpc two_args_no_return(bar: String, baz: u64);
|
||||
rpc two_args(bar: String, baz: u64) -> String;
|
||||
rpc no_args_ret_error() -> i32;
|
||||
rpc one_arg_ret_error(foo: String) -> String;
|
||||
rpc no_arg_implicit_return_error();
|
||||
#[doc="attr"]
|
||||
rpc one_arg_implicit_return_error(foo: String);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod functional_test {
|
||||
use futures::{
|
||||
compat::TokioDefaultSpawner,
|
||||
future::{ready, Ready},
|
||||
prelude::*,
|
||||
};
|
||||
use rpc::{
|
||||
client, context,
|
||||
server::{self, Handler},
|
||||
transport::channel,
|
||||
};
|
||||
use std::io;
|
||||
use tokio::runtime::current_thread;
|
||||
|
||||
service! {
|
||||
rpc add(x: i32, y: i32) -> i32;
|
||||
rpc hey(name: String) -> String;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Server;
|
||||
|
||||
impl Service for Server {
|
||||
type AddFut = Ready<i32>;
|
||||
|
||||
fn add(&self, _: context::Context, x: i32, y: i32) -> Self::AddFut {
|
||||
ready(x + y)
|
||||
}
|
||||
|
||||
type HeyFut = Ready<String>;
|
||||
|
||||
fn hey(&self, _: context::Context, name: String) -> Self::HeyFut {
|
||||
ready(format!("Hey, {}.", name))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequential() {
|
||||
let _ = env_logger::try_init();
|
||||
rpc::init(TokioDefaultSpawner);
|
||||
|
||||
let test = async {
|
||||
let (tx, rx) = channel::unbounded();
|
||||
tokio_executor::spawn(
|
||||
rpc::Server::new(server::Config::default())
|
||||
.incoming(stream::once(ready(Ok(rx))))
|
||||
.respond_with(serve(Server))
|
||||
.unit_error()
|
||||
.boxed()
|
||||
.compat()
|
||||
);
|
||||
|
||||
let mut client = await!(new_stub(client::Config::default(), tx))?;
|
||||
assert_eq!(3, await!(client.add(context::current(), 1, 2))?);
|
||||
assert_eq!(
|
||||
"Hey, Tim.",
|
||||
await!(client.hey(context::current(), "Tim".to_string()))?
|
||||
);
|
||||
Ok::<_, io::Error>(())
|
||||
}
|
||||
.map_err(|e| panic!(e.to_string()));
|
||||
|
||||
current_thread::block_on_all(test.boxed().compat()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent() {
|
||||
let _ = env_logger::try_init();
|
||||
rpc::init(TokioDefaultSpawner);
|
||||
|
||||
let test = async {
|
||||
let (tx, rx) = channel::unbounded();
|
||||
tokio_executor::spawn(
|
||||
rpc::Server::new(server::Config::default())
|
||||
.incoming(stream::once(ready(Ok(rx))))
|
||||
.respond_with(serve(Server))
|
||||
.unit_error()
|
||||
.boxed()
|
||||
.compat()
|
||||
);
|
||||
|
||||
let client = await!(new_stub(client::Config::default(), tx))?;
|
||||
let mut c = client.clone();
|
||||
let req1 = c.add(context::current(), 1, 2);
|
||||
let mut c = client.clone();
|
||||
let req2 = c.add(context::current(), 3, 4);
|
||||
let mut c = client.clone();
|
||||
let req3 = c.hey(context::current(), "Tim".to_string());
|
||||
|
||||
assert_eq!(3, await!(req1)?);
|
||||
assert_eq!(7, await!(req2)?);
|
||||
assert_eq!("Hey, Tim.", await!(req3)?);
|
||||
Ok::<_, io::Error>(())
|
||||
}
|
||||
.map_err(|e| panic!("test failed: {}", e));
|
||||
|
||||
current_thread::block_on_all(test.boxed().compat()).unwrap();
|
||||
}
|
||||
}
|
||||
130
tarpc/tests/latency.rs
Normal file
130
tarpc/tests/latency.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
#![feature(
|
||||
test,
|
||||
arbitrary_self_types,
|
||||
pin,
|
||||
integer_atomics,
|
||||
futures_api,
|
||||
generators,
|
||||
await_macro,
|
||||
async_await,
|
||||
proc_macro_hygiene,
|
||||
)]
|
||||
|
||||
extern crate test;
|
||||
|
||||
use self::test::stats::Stats;
|
||||
use futures::{compat::TokioDefaultSpawner, future, prelude::*};
|
||||
use rpc::{
|
||||
client, context,
|
||||
server::{self, Handler, Server},
|
||||
};
|
||||
use std::{
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
mod ack {
|
||||
tarpc::service! {
|
||||
rpc ack();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Serve;
|
||||
|
||||
impl ack::Service for Serve {
|
||||
type AckFut = future::Ready<()>;
|
||||
|
||||
fn ack(&self, _: context::Context) -> Self::AckFut {
|
||||
future::ready(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn bench() -> io::Result<()> {
|
||||
let listener = bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
|
||||
let addr = listener.local_addr();
|
||||
|
||||
tokio_executor::spawn(
|
||||
Server::new(server::Config::default())
|
||||
.incoming(listener)
|
||||
.take(1)
|
||||
.respond_with(ack::serve(Serve))
|
||||
.unit_error()
|
||||
.boxed()
|
||||
.compat()
|
||||
);
|
||||
|
||||
let conn = await!(bincode_transport::connect(&addr))?;
|
||||
let mut client = await!(ack::new_stub(client::Config::default(), conn))?;
|
||||
|
||||
let total = 10_000usize;
|
||||
let mut successful = 0u32;
|
||||
let mut unsuccessful = 0u32;
|
||||
let mut durations = vec![];
|
||||
for _ in 1..=total {
|
||||
let now = Instant::now();
|
||||
let response = await!(client.ack(context::current()));
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
match response {
|
||||
Ok(_) => successful += 1,
|
||||
Err(_) => unsuccessful += 1,
|
||||
};
|
||||
durations.push(elapsed);
|
||||
}
|
||||
|
||||
let durations_nanos = durations
|
||||
.iter()
|
||||
.map(|duration| duration.as_secs() as f64 * 1E9 + duration.subsec_nanos() as f64)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (lower, median, upper) = durations_nanos.quartiles();
|
||||
|
||||
println!("Of {:?} runs:", durations_nanos.len());
|
||||
println!("\tSuccessful: {:?}", successful);
|
||||
println!("\tUnsuccessful: {:?}", unsuccessful);
|
||||
println!(
|
||||
"\tMean: {:?}",
|
||||
Duration::from_nanos(durations_nanos.mean() as u64)
|
||||
);
|
||||
println!("\tMedian: {:?}", Duration::from_nanos(median as u64));
|
||||
println!(
|
||||
"\tStd Dev: {:?}",
|
||||
Duration::from_nanos(durations_nanos.std_dev() as u64)
|
||||
);
|
||||
println!(
|
||||
"\tMin: {:?}",
|
||||
Duration::from_nanos(durations_nanos.min() as u64)
|
||||
);
|
||||
println!(
|
||||
"\tMax: {:?}",
|
||||
Duration::from_nanos(durations_nanos.max() as u64)
|
||||
);
|
||||
println!(
|
||||
"\tQuartiles: ({:?}, {:?}, {:?})",
|
||||
Duration::from_nanos(lower as u64),
|
||||
Duration::from_nanos(median as u64),
|
||||
Duration::from_nanos(upper as u64)
|
||||
);
|
||||
|
||||
println!("done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_small_packet() {
|
||||
env_logger::init();
|
||||
tarpc::init(TokioDefaultSpawner);
|
||||
|
||||
tokio::run(
|
||||
bench()
|
||||
.map_err(|e| panic!(e.to_string()))
|
||||
.boxed()
|
||||
.compat(),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user