254 Commits

Author SHA1 Message Date
Tim Kuehn
00751d2518 external_doc doesn't work with crates.io yet :( 2018-10-29 11:05:09 -07:00
Tim Kuehn
4394a52b65 Add doc tests to .travis.yml 2018-10-29 10:55:12 -07:00
Tim Kuehn
70938501d7 Use eternal_doc for tarpc package. This will ensure our README is always up-to-date. 2018-10-29 10:53:34 -07:00
Tim Kuehn
d5f5cf4300 Bump versions. 2018-10-29 10:43:41 -07:00
Tim Kuehn
e2c4164d8c Remove unused feature enablements from tarpc 2018-10-25 11:44:38 -07:00
Tim Kuehn
78124ef7a8 Cargo fmt 2018-10-25 11:44:18 -07:00
Tim Kuehn
096d354b7e Remove unused features 2018-10-25 11:41:08 -07:00
Tim
7ad0e4b070 Service registry (#204)
# Changes

## Client is now a trait
And `Channel<Req, Resp>` implements `Client<Req, Resp>`. Previously, `Client<Req, Resp>` was a thin wrapper around `Channel<Req, Resp>`.

This was changed to allow for mapping the request and response types. For example, you can take a `channel: Channel<Req, Resp>` and do:

```rust
channel
    .with_request(|req: Req2| -> Req { ... })
    .map_response(|resp: Resp| -> Resp2 { ... })
```

...which returns a type that implements `Client<Req2, Resp2>`.

### Why would you want to map request and response types?

The main benefit of this is that it enables creating different client types backed by the same channel. For example, you could run multiple clients multiplexing requests over a single `TcpStream`. I have a demo in `tarpc/examples/service_registry.rs` showing how you might do this with a bincode transport. I am considering factoring out the service registry portion of that to an actual library, because it's doing pretty cool stuff. For this PR, though, it'll just be part of the example.

## Client::new is now client::new

This is pretty minor, but necessary because async fns can't currently exist on traits. I changed `Server::new` to match this as well.

## Macro-generated Clients are generic over the backing Client.

This is a natural consequence of the above change. However, it is transparent to the user by keeping `Channel<Req, Resp>` as the default type for the `<C: Client>` type parameter. `new_stub` returns `Client<Channel<Req, Resp>>`, and other clients can be created via the `From` trait.

## example-service/ now has two binaries, one for client and one for server.

This serves as a "realistic" example of how one might set up a service. The other examples all run the client and server in the same binary, which isn't realistic in distributed systems use cases.

## `service!` trait fns take self by value.

Services are already cloned per request, so this just passes on that flexibility to the trait implementers.

# Open Questions

In the service registry example, multiple services are running on a single port, and thus multiple clients are sending requests over a single `TcpStream`. This has implications for throttling: [`max_in_flight_requests_per_connection`](https://github.com/google/tarpc/blob/master/rpc/src/server/mod.rs#L57-L60) will set a maximum for the sum of requests for all clients sharing a single connection. I think this is reasonable behavior, but users may expect this setting to act like `max_in_flight_requests_per_client`.

Fixes #103 #153 #205
2018-10-25 11:22:55 -07:00
Tim
64755d5329 Update futures 2018-10-19 11:19:25 -07:00
Tim Kuehn
3071422132 Helper fn to create transports 2018-10-18 00:24:26 -07:00
Tim Kuehn
8847330dbe impl From<S> for bincode::Transport<S> 2018-10-18 00:24:08 -07:00
Tim Kuehn
6d396520f4 Don't allow empty service invocations 2018-10-18 00:23:34 -07:00
Tim Kuehn
79a2f7fe2f Replace tokio-serde-bincode with async-bincode 2018-10-17 20:24:31 -07:00
Tim Kuehn
af66841f68 Remove keyword 2018-10-17 11:59:09 -07:00
Tim
1ab4cfdff9 Make Request and Resonse enums' docs public, because they show up in the serve fn. 2018-10-16 23:02:52 -07:00
Tim
f7e03eeeb7 Fix up readme 2018-10-16 22:28:57 -07:00
Tim
29067b7773 Prepare for release 2018-10-16 22:19:16 -07:00
Tim
905e5be8bb 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
2018-10-16 11:26:27 -07:00
Henrique Nogara
5e4b97e589 Closes #197 (#198) 2018-08-27 14:59:16 -07:00
Tim
9bd66b7e49 Bump version (#195) 2018-08-13 15:49:17 -07:00
Yechan Bae
0ecc7a80c1 Do not ignore payload size in sync client (#194) 2018-08-12 10:39:20 -07:00
Tim
92f157206d Minor change rollup
* Head off imminent breakage due to https://github.com/rust-lang/rust/pull/51285.
* Fix examples and documentation to use a recently-gated feature, `proc_macro_path_invoc`.
* Update dependency versions.
2018-07-11 17:04:54 -07:00
Tim
b093db63a3 Remove usage of Cursor (#186)
* Add intellij files to .gitignore

* Remove Cursor usage when deserializing

* Don't use methods that copy into another temp buffer
2018-04-14 21:16:35 -07:00
Tim
8c3e3df47f Bump tarpc-plugins version. (#185) 2018-04-09 18:45:28 -07:00
Tim
6907c6e0a3 Merge pull request #183 from tikue/fix-breakage
Fix breakage on most recent nightly.
2018-04-09 12:28:06 -07:00
Tim Kuehn
4b5273127d Fix breakage on most recent nightly. 2018-04-08 22:16:00 -07:00
Tim
4b763e9f52 Merge pull request #180 from tikue/update-deps
Update deps and fix deprecation warnings.
2018-03-27 11:25:18 -07:00
Tim Kuehn
848eb00bea Bump version 2018-03-26 00:53:28 -07:00
Tim Kuehn
44ec68c002 Update deps 2018-03-26 00:32:40 -07:00
Tim Kuehn
b2282f9d7a Clean up code warnings 2018-03-25 23:52:36 -07:00
Tim
326f0270b9 Merge pull request #175 from imp/updates
Update dependency versions, and apply clippy suggestions.
2017-12-05 10:31:31 -08:00
Cyril Plisko
fd47a6c038 Drop more 'static lifetime 2017-12-05 07:14:34 -05:00
Cyril Plisko
77cfffaaed Drop more 'static lifetimes 2017-12-04 19:24:03 -05:00
Cyril Plisko
118893678b Update dependencies
lazy_static (0.2 -> 1.0)
bincode (0.8 -> 0.9)
2017-12-04 18:02:09 -05:00
Cyril Plisko
ae3985de46 clippy: using '.clone()' on a ref-counted pointer 2017-12-04 17:52:09 -05:00
Cyril Plisko
49f36e0b2b clippy: simplify else arm 2017-12-04 17:46:04 -05:00
Cyril Plisko
4a7082b27c clippy: unneeded dereference 2017-12-04 17:45:24 -05:00
Cyril Plisko
3aa53a06fb clippy: use .is_err() instead of destructuring Result 2017-11-23 10:04:39 +02:00
Cyril Plisko
a0afbefef4 &'static str -> &str
'static lifetime in const is default
2017-11-23 09:29:23 +02:00
Cyril Plisko
5b554f7062 itertools: update to 0.7 2017-11-23 09:18:24 +02:00
Tim
0411a90be9 Fix README link to documentation (#171)
Fixes #170
2017-11-03 13:37:26 -05:00
Tim
9ce7938fdc Merge pull request #167 from tikue/master
* Fix breakage from nightly compiler changes.

* Remove unused imports

* Update TLS code to not use deprecated method

* Bump versions

Fixes #166
2017-09-17 17:58:50 -07:00
Tim Kuehn
650dc88da5 Bump versions 2017-09-17 16:02:28 -07:00
Tim Kuehn
3601763442 Update TLS code to not use deprecated method 2017-09-17 15:56:07 -07:00
Tim Kuehn
4aaaea1e04 Remove unused imports 2017-09-17 15:56:07 -07:00
Tim Kuehn
2e214c85d3 Fix breakage from nightly compiler changes.
Fixes #166
2017-09-17 15:56:07 -07:00
Cyril Plisko
0676ab67df Refresh dependencies (#164) 2017-08-14 14:31:47 -07:00
Tim
0b843512dd Use #[allow(unreachable_patterns)] to simplify some macro code. (#163)
By allowing unreachable patterns, we don't have to have 'NotIrrefutable' variants in the Request, Response, and Error enums.

This commit also removes an unused macro in an example.
2017-08-02 13:54:06 -07:00
Jesper Håkansson
85d9416750 doc: Update tarpc version in readme (#162) 2017-07-21 08:37:07 -07:00
Tim
5e3cf3c807 Run the new nightly cargo fmt (#156) 2017-07-18 14:54:46 -07:00
Tim
4dfb3a48c3 Derive Deserialize, Serialize in macros. Requires feature(use_extern_macros). (#151) 2017-07-18 12:04:56 -07:00
Tim
21e8883877 Update to serde 1.0 and bincode 0.8 (#149) 2017-05-05 22:16:44 -07:00
Tim Kuehn
7dbfe07c97 Bump to version 0.7.3 and update release notes. 2017-04-26 12:33:58 -07:00
Jon Gjengset
8bc01a993b format with rustfmt 0.8.3 (#148) 2017-04-26 12:25:49 -07:00
Jon Gjengset
e2728d84f3 Remove unnecessary Sync bound on clients (#147) 2017-04-26 12:11:30 -07:00
Adam Wright
85a8f8fce7 Merge pull request #145 from tikue/master
Bump version
2017-04-22 16:55:29 -07:00
Tim Kuehn
9267daa409 Bump version 2017-04-22 16:28:27 -07:00
Adam Wright
a01ecd3314 Merge pull request #144 from tikue/master
Track rustc master
2017-04-22 16:23:18 -07:00
Tim Kuehn
dd662beb9b Track rustc master 2017-04-22 16:07:31 -07:00
Adam Wright
5857de5164 Merge pull request #142 from google/readme-typo
Fix typo in readme
2017-04-22 16:05:09 -07:00
Tim
8cb6ff89cc Fix typo in readme 2017-04-11 20:30:14 -07:00
Tim
a441fcb771 Bump to 0.7.1 for some doc fixes (#141) 2017-03-31 15:18:35 -07:00
Tim
f0ad99b900 Prepare for Crates.io release (#140)
* Prepare tarpc-plugins for Crates.io release.

* Add a 0.7 bullet to RELEASES.md.

* Fix some outdated doc comments.

* Reexport ShutdownFuture.
2017-03-31 14:15:20 -07:00
Tim
5add81b5f3 Feature rollup (#129)
* Create a directory for the `future::server` module, which has become quite large. server.rs => server/mod.rs. Server submodules for shutdown and connection logic are added.

* Add fn thread_pool(...) to sync::server::Options

* Configure idle threads to expire after one minute

* Add tarpc::util::lazy for lazily executing functions. Similar to `futures::lazy` but useful in different circumstances. Specifically, `futures::lazy` typically requires a closure, whereas `util::lazy` kind of deconstructs a closure into its function and args.

* Remove some unstable features, and `cfg(plugin)` only in tests. Features `unboxed_closures` and `fn_traits` are removed by replacing manual Fn impls with Stream impls. This actually leads to slightly more performant code, as well, because some `Rc`s could be removed.

* Fix tokio deprecation warnings. Update to use tokio-io in lieu of deprecated tokio-core items. impl AsyncRead's optional `unsafe fn prepare_uninitialized_buffer` for huge perf wins

* Add debug impls to all public items and add `deny(missing_debug_implementations)` to the crate.

* Bump tokio core version.
2017-03-31 12:16:40 -07:00
Tim
15080b2889 Set a default max packet size. (#128)
The default max packet size of 2 << 20
2017-03-23 00:08:08 -07:00
Jon Gjengset
8598d4beaf Fix following removal of parse_ty_path (#138)
The merge of rust-lang/rust#40043 removes parse_ty_path in the latest
nightly, which we depended on. This patch rewrites that code path using
parse_path, and in the process eliminates an unreachable!() if let arm.
2017-03-22 21:20:03 -07:00
Tim
40faf25d99 Fix vulnerability. (#126)
0 is a sentinel value used to make all enums refutable. This is a hack around issues in maros
where you're unknowingly treating irrefutable patterns as refutable, which is unfortunately
a hard error.

The server panics if it ever encountered the 0-variant, which before this patch was possible. Now,
it's not possible, because 0-variants are now not able to be deserialized.
2017-03-22 18:17:37 -07:00
Jon Gjengset
c8be9b690b Fix following removal of Attribute.value (#136)
Since rust-lang/rust#40346 has now been merged, Attribute no longer has
a .value field. Instead, we must follow the token stream and modify the
tokens directly. For Docstring attributes, there should only be one
token, the docstring value.
2017-03-21 11:00:52 -07:00
Malte Schwarzkopf
f4018a431e bincode 1.0.0-alpha6 changed SizeLimit to a trait (#134)
Unfortunately, cargo's semantic versioning gets confused by the
"-alpha" suffix in current bincode versions (I think): even though
tarpc's Cargo.toml specified version "1.0.0-alpha4", cargo will
download the more recent "1.0.0-alpha6", which has breaking changes
to the `SizeLimit` enum.

This change makes tarpc work with bincode 1.0.0-alpha6 and updates the
dependency.
2017-03-19 11:12:17 -07:00
Tim
79aee18d17 Change module structure. (#122)
* Change `client::{future, sync}, server::{future, sync}` to `future::{client, server}, sync::{client, server}`

This reflects the most common usage pattern and allows for some types to not need to be fully qualified when used together (e.g. the previously-named `client::future::Options` and `server::future::Options` can now be `client::Options` and `server::Options`).

* sync::client: create a RequestHandler struct to encapsulate logic of processing client requests.

The largest benefit is that unit testing becomes easier, e.g. testing that the request processing stops when all request senders are dropped.

* Rename Serialize error variants to make sense.

* Rename tarpc_service_ConnectFuture__ => Connect (because it's public)

* Use tokio proto's ClientProxy.

Rather than reimplement the same logic. Curiously, this commit
isn't a net loss in LOC. Oh well.

* Factor out os-specific functionality in listener() into their own fns

* Remove service-fn dep
2017-03-07 18:20:46 -08:00
Tim
f9ff2c4e50 Merge pull request #121 from tikue/send-client
Make SyncClient Send again (and Clone too!).
2017-03-06 22:12:33 -08:00
Tim Kuehn
073bc25e18 Derive debug rather than manually impl 2017-03-06 21:12:44 -08:00
Tim Kuehn
d0d65c413a Make Client Send again (and Clone too!).
The basic strategy is to start a reactor on a dedicated thread running a request stream.
Requests are spawned onto the reactor, allowing multiple requests to be
processed concurrently. For example, if you clone the client to make requests
from multiple threads, they won't have to wait for each others'
requests to complete before theirs start being sent out.

Also, client rpcs only take &self now, which was also required for
clients to be usable in a service.

Also added a test to prevent regressions.
2017-03-06 21:12:44 -08:00
Tim
e59116fb48 Add server::Handle::shutdown (#117)
* Add server::Handle::shutdown
* Hybrid approach: lameduck + total shutdown when all clients disconnect.
* The future handle has addr() and shutdown(), but not run().
2017-03-06 20:57:12 -08:00
Tim
daa96a69a2 Latency benchmark: drive the client with the same reactor as server (so it's entirely single-threaded) (#116) 2017-02-22 21:46:28 -08:00
Tim
85ae614983 Add a test that dropping a client doesn't break the server (#115) 2017-02-22 21:12:52 -08:00
Adam Wright
e17549a1f7 Merge pull request #114 from shaladdle/fix-throughput
Fix throughput example
2017-02-21 22:39:51 -08:00
Adam Wright
0b8a845ec1 Actually run the server future 2017-02-21 22:24:05 -08:00
Tim
2b8f3db1fd Return a concrete type from server::listen (#113)
* Return a concrete type from `server::listen`.
* Change `FutureServiceExt::listen` to return `(SocketAddr, Listen)`, where `Listen` is a struct created by the `service!` macro that `impls Future<Item=(),  Error=()>` and represents server execution.
* Disable `conservative_impl_trait` as it's no longer used.
* Update `FutureServiceExt` doc comment.
* Update `SyncServiceExt` doc comment. Also annotate `server::Handle` with `#[must_use]`.
* `cargo fmt`
2017-02-21 22:01:59 -08:00
Adam Wright
44792347b1 Change sync listen to return a handle which runs the server (#112) 2017-02-21 09:50:01 -08:00
Adam Wright
f7c371930f Merge pull request #110 from shaladdle/prepush
Prepush script improvement/simplification
2017-02-20 18:43:51 -08:00
Adam Wright
3eec8fe1dd Actually fail the prepush in try_run 2017-02-20 18:10:23 -08:00
Adam Wright
9c973eb80b Switch back to verb_ing_ 2017-02-20 18:07:36 -08:00
Adam Wright
37763f25e4 Make whitespace consistent 2017-02-20 17:46:13 -08:00
Adam Wright
515ab90299 Various simplifications/improvements to pre-push 2017-02-20 17:44:30 -08:00
Adam Wright
9987eae290 --color=always has to be after the command 2017-02-20 17:11:05 -08:00
Adam Wright
22545c653c Fix prepush script? 2017-02-20 17:11:05 -08:00
Cyril Plisko
8b9847e347 Update chrono (#111) 2017-02-20 13:05:47 -08:00
Adam Wright
49bc4d0bce Merge pull request #109 from shaladdle/formatting
Run cargo fmt
2017-02-18 22:37:27 -08:00
Adam Wright
0780af9e05 Run cargo fmt 2017-02-18 15:37:31 -08:00
Adam Wright
bc16ffd2d0 Merge pull request #108 from tikue/doc-fix
Fix readme, but for real this time.
2017-02-16 21:08:01 -08:00
Tim Kuehn
c7831e8aa6 More spacing stuff 2017-02-16 20:56:04 -08:00
Tim Kuehn
8faba59d66 Fix tabs in pre-push, and remove dead code in readme 2017-02-16 20:51:57 -08:00
Tim Kuehn
bceaea1206 .travis.yml: Do tls tests, then rustdoc tests, then regular tests 2017-02-16 20:18:46 -08:00
Tim Kuehn
fea8d5eb1d I hate READMEs. 2017-02-16 17:57:17 -08:00
Tim Kuehn
db9c23058d Dumb thing 2017-02-16 17:53:59 -08:00
Tim Kuehn
77638b388d Have travis test README with rustdoc. Yay! 2017-02-16 17:53:06 -08:00
Tim Kuehn
eac6b64aeb Merge branch 'master' of github.com:google/tarpc into doc-fix 2017-02-16 17:52:21 -08:00
Tim Kuehn
7ae107cf2b Fix readme for real this time. 2017-02-16 17:46:55 -08:00
Adam Wright
12efcae80c Merge pull request #107 from tikue/master
Fix README future service examples
2017-02-16 16:22:48 -08:00
Adam Wright
5fbd45ea49 Merge branch 'master' into master 2017-02-16 14:43:20 -08:00
Tim Kuehn
e651838f19 remove unused thing 2017-02-16 13:41:13 -08:00
Adam Wright
cdb44090bd Merge pull request #106 from tikue/pre-push-bench
Add bench to pre-push and .travis.yml
2017-02-16 12:43:19 -08:00
Tim Kuehn
fc2edc89af Fix README future service examples 2017-02-16 12:07:49 -08:00
Tim Kuehn
30eed41c40 Unused imports 2017-02-16 01:04:40 -08:00
Tim Kuehn
517477129c Remove dead code 2017-02-16 01:00:40 -08:00
Tim Kuehn
1325d1a2d7 Merge branch 'master' of github.com:google/tarpc into pre-push-bench 2017-02-16 00:59:34 -08:00
Adam Wright
15f83961e9 Merge pull request #98 from tikue/into-future
Change bound on FutureService's associated types
2017-02-16 00:58:06 -08:00
Tim Kuehn
4fc37fe707 Add bench to pre-push and .travis.yml 2017-02-16 00:56:38 -08:00
Tim Kuehn
8ee6ce0307 Update README 2017-02-16 00:51:55 -08:00
Tim Kuehn
07f9d5a34d Merge branch 'master' of github.com:google/tarpc into into-future 2017-02-16 00:50:08 -08:00
Adam Wright
a0a11f8704 Merge pull request #97 from tikue/crates-io-categories
Add categories to Cargo.toml.
2017-02-16 00:46:55 -08:00
Tim Kuehn
6673869721 Merge branch 'master' of github.com:google/tarpc into tikue/crates-io-categories 2017-02-16 00:40:11 -08:00
Tim Kuehn
63caacf0c1 Merge branch master into tikue/into-future. 2017-02-16 00:37:19 -08:00
Tim
2c09a35705 Remove the Send bound from FutureService (#96)
* Make a reactor handle mandatory for server.

This removes the Send bound from FutureService. The Send bound
is still required for SyncService, since clones are sent to
new threads for each request. (This is more fodder for the argument
that there should be a distinct Options struct for each combination of
async/sync and client/server.)

This commit also makes FutureService::listen return an io::Result
rather than a Future; the future was never really necessary and
had the unintended consequence of making SyncService::listen
deadlock when the options specified a handle (because that means
the reactor driving the service lives on the same thread that
SyncService is waiting on).

`SyncClient` is no longer `Clone` because it needs to create
a new `reactor::Core` when cloning. Tokio Clients are `Clone` but
they don't allow moving the cloned client onto a new reactor.

* Change pubsub to use Rc<Refcell<>> instead of Arc<Mutex<>>.

This is possible since services no longer need to be Send.

* Remove some unnecessary unstable features.

There 3 remaining unstable features. The hardest to remove is plugin, because
we rely on compiler plugins to rewrite types from snake case to camel. It's
possible this can be removed before the proc macros rewrite lands if
impl Trait is extended to work with traits.

* Clean up example

* Sync servers now spawn a reactor on a thread. It's decided that
   sync users should not have to know about tokio at all.

* Don't allow specifying a reactor::Core on client options.

* Fail fast in server::listen if local_addr() returns Err.
2017-02-15 23:47:35 -08:00
Adam Wright
6bf4d171c1 Merge pull request #99 from tikue/deps-cleanup
Remove unused deps: bytes, take, and scoped-pool
2017-02-15 20:57:08 -08:00
Adam Wright
0f52b08426 Merge branch 'master' into deps-cleanup 2017-02-15 20:49:53 -08:00
Cyril Plisko
acdf03c8ca Fix compilation error for benches (#101) 2017-02-15 09:26:57 -08:00
Adam Wright
b2f69faa13 Merge pull request #92 from tikue/sync-reactor
Change SyncClient to drive its own execution with an internal reactor::Core.
2017-02-13 21:37:49 -08:00
Tim Kuehn
2749d33f88 Merge master into sync-reactor. 2017-02-13 21:29:39 -08:00
compressed
338c91d393 fix(bincode): updates to support bincode 1.0.0-alpha2 (#100)
Removed:
- `Error::ClientDeserialize` variant
- `Error::ServerDeserialize` variant
- `WireError::ServerDeserialize` variant
2017-02-12 14:52:38 -08:00
Tim Kuehn
fa2df184e9 Remove unused deps: bytes, take, and scoped-pool 2017-02-07 20:34:52 -08:00
Tim Kuehn
fe4eab38f1 Change FutureService's associated types to be bounded by IntoFuture rather than Future.
It's strictly more flexible, because everything that impls Future impls IntoFuture, and it
additionally allows returning types like Result. Which is nice.
2017-02-07 19:58:29 -08:00
Tim Kuehn
06beec3e5a Add keywords that aren't terrible 2017-02-06 19:24:35 -08:00
Tim Kuehn
d39a382012 Add travis-ci repository to Cargo.toml 2017-02-06 19:24:35 -08:00
Tim Kuehn
efcc914e65 Add categories to Cargo.toml.
Also remove a keyword because 5's the limit.
2017-02-06 19:24:34 -08:00
compressed
a1072c8c06 Upgrade serde to 0.9 (#94)
* feat(serde): upgrade serde to 0.9

If you are using `#[derive(Serialize, Deserialize)]` or implementing
your own (de)serialization behavior for your RPC types you will need to
ensure you are using serde 0.9.x.

[breaking-change]

* chore(byteorder): upgrade byteorder to 1.0
2017-02-02 13:36:19 -08:00
Tim Kuehn
ed90f4ecea Merge master into sync-reactor 2017-02-01 22:48:49 -08:00
Tim Kuehn
b5bf696017 Merge master into sync-reactor 2017-02-01 22:39:19 -08:00
Tim Kuehn
fe20c8af14 Add a reactor::Core field to SyncClient.
This allows the client to drive its own execution, as one would expect.
Previously, the reactor had to be driven on a separate thread, which was confusing.
This has a couple notable side effects:
  1. SyncClient is no longer `Clone`.  This is because `reactor::Core`
     is not `Clone`, and creating one is not infallible
     (`Core::new` returns a `Result`).
  2. SyncClient does not use the user-specified `client::Options::handle` or
     `client::Options::remote`, because it constructs its own reactor.
2017-02-01 22:32:25 -08:00
Adam Wright
2ffa1138dd Merge pull request #91 from tikue/master
Temporary workaround for compiler bugs in impl Trait feature.
2017-02-01 21:05:33 -08:00
Tim Kuehn
9d552e48a4 Temporary workaround for compiler bugs in impl Trait feature. 2017-02-01 15:52:30 -08:00
compressed
fafe569ebc Add TLS support (#81)
When `-- features tls` is specified for tarpc, RPC communication can
also occur over a `TlsStream<TcpStream>` instead of a `TcpStream`.

* The functional tests have been refactored to use a common set of
functions for constructing the client and server structs so that all
the tests are shared across non-tls and tls test runs.

* Update pre-push to test TLS

* The `cfg_attr` logic caused many false warnings from clippy, so for now the crate docs for TLS are not tested.
2017-01-31 10:21:13 -08:00
Adam Wright
c286c596bd Merge pull request #85 from tikue/master
Make port reusable
2017-01-30 22:46:29 -08:00
Tim Kuehn
348111a423 Add test for reusing addr:port 2017-01-30 17:11:31 -08:00
Tim Kuehn
984c1c29c5 Make port reusable 2017-01-30 15:51:22 -08:00
Adam Wright
cc1290636d Merge pull request #82 from tikue/master
Make sure synchronous RPCs are wrapped in a lazy future.
2017-01-23 00:48:10 -08:00
Tim Kuehn
41683eee1d Change variable obfuscation style from prefix underscores to suffix underscores. 2017-01-22 22:41:06 -08:00
Tim Kuehn
6c2239d6f2 Cargo clippy 2017-01-22 21:16:11 -08:00
Tim Kuehn
3196fd91ff Cargo fmt 2017-01-22 20:08:38 -08:00
Tim Kuehn
45fa4c7bf1 Make sure synchronous RPCs are wrapped in a lazy future.
Some future-returning fns implicitly require the presence of an execution task. Wrapping in a lazy future ensures that by the time the future is polled, there is a task present.
2017-01-22 19:39:19 -08:00
Adam Wright
c7c18cbaaa Merge pull request #78 from tikue/master
Simplify Never impls: return the inner uninhabited type for fns taking &self.
2017-01-22 15:31:43 -08:00
Adam Wright
1c0cf2a67f Merge branch 'master' into master 2017-01-22 15:27:45 -08:00
Adam Wright
15a3900f3d Merge pull request #79 from tikue/private-items
Make private a couple items that no longer need to be public.
2017-01-22 15:23:59 -08:00
Adam Wright
f0ecd7008d Merge branch 'master' into private-items 2017-01-22 15:13:32 -08:00
Adam Wright
3567202aa3 Merge pull request #80 from tikue/client-reexports
Don't reexport client implementation details from crate root.
2017-01-22 15:13:22 -08:00
Tim Kuehn
802ee838ca Don't reexport client implementation details from crate root. 2017-01-17 16:38:30 -08:00
Tim Kuehn
c976ca710a Make private a couple items that no longer need to be public. 2017-01-17 15:36:08 -08:00
Tim Kuehn
9d2d69b4f4 Simplify Never impls: return the inner uninhabited type for fns taking &self. 2017-01-17 15:25:48 -08:00
Adam Wright
558dda28ad Merge pull request #75 from tikue/master
Add Options to all connect and listen fns
2017-01-15 17:21:30 -08:00
Tim
4a4ffab611 Merge branch 'master' into master 2017-01-15 17:07:18 -08:00
Tim Kuehn
cabbbb2a0b Fix some doc comments and remove unused impl. 2017-01-15 17:00:04 -08:00
compressed
3dc1b6381d fix(futures): Either was only added in 0.1.7 (#76) 2017-01-13 15:21:07 -08:00
Adam Wright
c96ab77dcf Merge pull request #74 from shaladdle/docs
First pass at some more detailed documentation
2017-01-13 15:15:09 -08:00
Adam Wright
865712f36e First pass at some more detailed documentation 2017-01-12 20:11:03 -08:00
Tim Kuehn
a8e5bc45a1 Minor refactor 2017-01-12 00:31:46 -08:00
Tim Kuehn
95c57a4b2d Add Options to all connect and listen fns 2017-01-12 00:06:17 -08:00
Adam Wright
ab3e73812c Merge pull request #73 from tikue/master
Remove readme_expanded
2017-01-11 23:32:51 -08:00
Tim Kuehn
d34ca2acda Remove readme_expanded 2017-01-11 23:29:19 -08:00
Adam Wright
9e92666932 Merge pull request #72 from tikue/master
Fix readme examples
2017-01-11 23:29:06 -08:00
Tim Kuehn
a6b25dc268 Fix readme examples 2017-01-11 23:14:52 -08:00
Adam Wright
77e12f56cc Merge pull request #71 from tikue/master
Rework the future Connect trait to only have one method, which takes …
2017-01-11 23:03:58 -08:00
Tim Kuehn
05c6be192d Rework the future Connect trait to only have one method, which takes an Options arg. 2017-01-11 22:55:04 -08:00
Adam Wright
568484f14f Merge pull request #70 from tikue/master
Make spawn_core private
2017-01-11 21:00:15 -08:00
Tim Kuehn
918b6b3b75 Make spawn_core private 2017-01-11 20:44:59 -08:00
Adam Wright
d5854fd049 Merge pull request #69 from tikue/remove-error-trait
Remove unnecessary trait
2017-01-11 20:41:48 -08:00
Adam Wright
9646d92cae Merge pull request #68 from tikue/master
Remove unused macro code
2017-01-11 20:27:46 -08:00
Tim Kuehn
a660ed7f1a Remove unnecessary trait 2017-01-11 20:27:17 -08:00
Tim Kuehn
3eb2292841 Remove unused code 2017-01-11 20:22:52 -08:00
Adam Wright
5a525d9fb7 Merge pull request #67 from tikue/real-crates
Update Cargo.toml to use crates.io releases of tokio deps.
2017-01-11 18:01:39 -08:00
Tim Kuehn
e4ef0881e6 Update Cargo.toml to use crates.io releases of tokio deps. 2017-01-11 14:23:52 -08:00
Adam Wright
91e9ad3001 Merge pull request #63 from tikue/tokio-tracking
Track more changes to tokio
2017-01-09 13:05:15 -08:00
Tim Kuehn
b3c187cdac Remove commented out code 2017-01-08 22:14:26 -08:00
Tim Kuehn
ffea090726 Make SyncClient only require &self for RPCs. 2017-01-08 22:13:49 -08:00
Tim Kuehn
e8f942f463 Merge master into tokio-tracking. 2017-01-08 22:05:10 -08:00
Adam Wright
5e9527e583 Merge pull request #61 from tikue/proto-changes
Track the changes to tokio-proto/master
2017-01-08 21:45:20 -07:00
Adam Wright
48452c04a6 Remove unecessary unreachable! calls (#8) 2017-01-08 20:36:28 -08:00
Tim Kuehn
626254e836 Update tokio-core replacement to 0.1.3 2017-01-08 17:49:41 -08:00
Tim Kuehn
3719564efc Fix stale comment 2017-01-08 17:38:05 -08:00
Tim Kuehn
ef41d4349c Make connection backlog arg to listen a const 2017-01-08 17:34:44 -08:00
Tim Kuehn
200407a4c9 Make connection backlog arg to listen a const 2017-01-08 17:30:31 -08:00
Tim Kuehn
388c4be69a Fully commit to futures in hello world. 2016-12-29 12:08:31 +01:00
Tim Kuehn
77653282b6 Fix latency bench 2016-12-28 18:29:56 +01:00
Tim Kuehn
5dbfa99d0b Shuffle around the sync/future modules 2016-12-26 16:02:44 -05:00
Tim Kuehn
18cbbe5b15 impl Clone (again) for the clients 2016-12-26 15:30:36 -05:00
Tim Kuehn
e22210bfd8 Rename module framed -> protocol, and clarify some type parameters. 2016-12-26 14:54:31 -05:00
Tim Kuehn
d242bdbb82 Track latest tokio changes 2016-12-26 00:27:42 -05:00
Tim Kuehn
bdd6737914 Add a test for concurrent requests 2016-12-16 14:22:25 -08:00
Tim Kuehn
f2bf1adf8b Fix bug wherein the Codec was clearing the buf after decoding a message. Don't do thatgit stash pop! 2016-12-16 14:15:31 -08:00
Tim Kuehn
be156f4d6b Cut down size of readme_expanded. 2016-12-11 23:32:44 -08:00
Tim Kuehn
35f8aefb30 Small refactor 2016-12-11 16:43:41 -08:00
Tim Kuehn
8a29aa29b2 Add an version of the readme where all macros are expanded (with slight modifications to make it prettier). 2016-12-09 22:26:04 -08:00
Tim Kuehn
b5750365ca Change readme example to exhibit deadlock... 2016-12-06 15:15:42 -08:00
Tim Kuehn
f6b1660092 Count requests in concurrency example 2016-12-05 16:06:58 -08:00
Tim Kuehn
5c17ffacae Add listen_with fns. 2016-12-05 15:23:43 -08:00
Tim Kuehn
13e56481bb Track latest changes to tokio-proto. 2016-12-04 20:26:32 -08:00
Tim Kuehn
608be5372b Try to fix concurrency example 2016-11-05 19:31:45 -07:00
Adam Wright
583f2cd92f Merge pull request #58 from tikue/framed
Fixes to readme
2016-11-05 17:14:06 -07:00
Tim Kuehn
ef8deb2059 Merge branch 'master' of github.com:google/tarpc into framed 2016-11-05 16:51:24 -07:00
Tim Kuehn
c437d66603 Fixes to readme 2016-11-05 16:47:46 -07:00
Adam Wright
662a627e4e Merge pull request #55 from tikue/framed
Replace handrolled transport with higher-level `Framed` construct
2016-11-05 16:42:55 -07:00
Tim Kuehn
d47a931f9f Elaborate in the doc comment for Framed 2016-11-05 16:33:41 -07:00
Tim Kuehn
a30e929b63 Clarify some docs and fix some dumb code. 2016-11-05 15:43:31 -07:00
Tim Kuehn
b638f45d27 Add a method to util::FirstSocketAddr that returns a Result rather than panicking 2016-11-05 15:43:09 -07:00
Tim Kuehn
68ba7505ac Update naming of publisher client and server in pubsub example 2016-11-05 15:22:17 -07:00
Tim Kuehn
3afcfe6274 Track latest git deps and rust compiler 2016-11-05 15:06:39 -07:00
Tim Kuehn
85fbe411e6 Run clients in parallel (that's the whole point!). 2016-10-29 10:16:21 -07:00
Tim Kuehn
aaaaf942d6 Add future::Connect::connect_remotely. 2016-10-29 10:15:39 -07:00
Tim Kuehn
29b6425fb5 Add a util fn for running a reactor forever on a new thread. 2016-10-29 10:15:15 -07:00
Tim Kuehn
539776eb27 Make future::Connect take a lifetime param so that connect_with doesn't have to clone Handle. 2016-10-29 09:38:43 -07:00
Tim Kuehn
61e7d9aab8 Remove some logging and thread yielding. 2016-10-29 02:03:35 -07:00
Tim Kuehn
59988c6ee1 Initialize env_logger in example server_calling_server. 2016-10-29 02:02:33 -07:00
Tim Kuehn
a1de4c1b05 Re-work concurrency example to not use wait() everywhere. 2016-10-29 02:02:11 -07:00
Tim Kuehn
b6e9d61286 Add futures::Connect::connect_with, allowing the user to specify the reactor core to use. 2016-10-29 02:01:40 -07:00
Tim Kuehn
67ad2b90be Track nightly compiler 2016-10-28 16:13:59 -07:00
Tim Kuehn
3506397150 Track latest tokio changes. 2016-10-28 11:33:20 -07:00
Tim Kuehn
db665ebb60 Bump itertools from 0.4 => 0.5 2016-10-27 19:07:54 -07:00
Tim Kuehn
cff8782e18 Replace try! with ? 2016-10-16 13:49:53 -07:00
Tim Kuehn
531dc20d66 Track nightly 2016-10-16 13:19:35 -07:00
Adam Wright
d8d240ec12 Add docs to play nicer with deny(misisng_docs) (#6) 2016-10-10 12:25:28 -04:00
Adam Wright
eb49c30fdb Add an example to the readme for futures (#4)
And add the example under examples/ so we know when it breaks.
2016-10-04 16:25:57 -04:00
Tim Kuehn
b661ff0175 Complete ClientFuture on error. 2016-09-30 15:36:55 -04:00
Tim Kuehn
b880d65f44 Merge branch 'framed' of github.com:tikue/tarpc into framed 2016-09-30 15:19:16 -04:00
Tim Kuehn
451b99b92a Remove all remaining #[inline]s. 2016-09-30 15:16:29 -04:00
Tim Kuehn
99b13ae6fc Remove an unnecessary Box 2016-09-28 00:04:58 -07:00
Tim Kuehn
5bace01f2b Finish the multiplex implementation 2016-09-26 23:44:22 -07:00
Tim Kuehn
4a63064cbd Remove some panics, and don't use ToSocketAddrs in async methods. 2016-09-19 00:16:47 -07:00
Tim Kuehn
3eb57d4009 Move the static remote to the default reactor core to the crate root.
It didn't really make sense in the framed module, which doesn't care about such things.
2016-09-17 18:31:08 -07:00
Tim Kuehn
14c97b61f9 Rename things to align with tokio changes. 2016-09-17 18:23:35 -07:00
Tim Kuehn
20d1a019ae WIP multiplex Parse/Serialize/FramedIo impls 2016-09-17 12:50:48 -07:00
Tim Kuehn
8c0181633d Track crates.io deps 2016-09-14 10:11:28 -07:00
Tim Kuehn
987e445935 Update benches 2016-09-14 01:57:03 -07:00
Tim Kuehn
1c318182c4 Don't serialize on a thread pool 2016-09-14 01:54:03 -07:00
Tim Kuehn
ac7e2eedb2 Update readme 2016-09-14 01:47:21 -07:00
Tim Kuehn
6d1fbab73c Fix readme 2016-09-14 01:36:57 -07:00
Tim
e8902c21a2 Mangle a lot of names in macro expansion. (#53)
* Mangle a lot of names in macro expansion.

To lower the chance of any issues, prefix idents in service expansion with __tarpc_service.
In future_enum, prefix with __future_enum. The pattern is basically __macro_name_ident.

Any imported enum variant will conflict with a let binding or a function arg, so we basically
can't use any generic idents at all. Example:

    enum Req { request(..) }
    use self::Req::request;

    fn make_request(request: Request) { ... }

                    ^^^^^^^ conflict here

Additionally, suffix generated associated types with Fut to avoid conflicts with camelcased rpcs.
Why someone would do that, I don't know, but we shouldn't allow that wart.

* Trim long macro lines
2016-09-14 01:34:35 -07:00
Tim
be5f55c5f6 Extend snake_to_camel plugin to replace {} in the doc string with the original snake-cased ident. (#50)
* Extend snake_to_camel plugin to replace {} in the doc string with the origin snake-cased ident.

Also, track tokio-rs master.

This is really ad-hoc, undiscoverable, and unintuitive, but there's no way to programmatically create doc strings
in regular code, and I want to produce better doc strings for the associated types.

Given `fn foo_bar`:

Before: `/// The type of future returned by the function of the same name.`
After: ``/// The type of future returned by `{}`.``
    => `/// The type of future returned by foo_bar.`

* Fix some docs

* Use a helper fn on pipeline::Frame instead of handrolled match.

* Don't hide docs for ClientFuture.

It's exposed in the Connect impl of FutureService -- the tradeoff for not generating *another* item -- and hiding it breaks doc links.

* Formatting

* Rename snake_to_camel plugin => tarpc-plugins

* Update README

* Mangle a lot of names in macro expansion.

To lower the chance of any issues, prefix idents in service expansion with __tarpc_service.
In future_enum, prefix with __future_enum. The pattern is basically __macro_name_ident.

Any imported enum variant will conflict with a let binding or a function arg, so we basically
can't use any generic idents at all. Example:

    enum Req { request(..) }
    use self::Req::request;

    fn make_request(request: Request) { ... }

                    ^^^^^^^ conflict here

Additionally, suffix generated associated types with Fut to avoid conflicts with camelcased rpcs.
Why someone would do that, I don't know, but we shouldn't allow that wart.
2016-09-14 01:19:24 -07:00
Tim
54017839d1 Remove an unused trait and a few unnecessary Send bounds (#54) 2016-09-13 21:35:05 -07:00
Adam Wright
b5f2fe5f4f Merge pull request #51 from tikue/client-side-request
Use cleverness to declare the client-side request enum inside a fn.
2016-09-13 18:02:04 -07:00
Adam Wright
437997b2d1 Merge pull request #52 from tikue/serde_derive
Use the new serde_derive crate.
2016-09-13 18:00:45 -07:00
Tim Kuehn
b5e472b374 Use the new serde_derive crate. 2016-09-06 16:11:32 -07:00
Tim Kuehn
f76030ecd7 Correct typos 2016-09-06 14:05:32 -07:00
Tim Kuehn
b7952d3f47 Fix README Cargo.toml 2016-09-06 14:00:57 -07:00
Tim Kuehn
8d561d21b2 Use cleverness to declare the client-side request enum inside a fn.
Previously, the enum couldn't be declared in the fn it was used in, because
they were part of the same repeating pattern in the macro syntax, and you can't
nest the same repeating pattern. I fixed this by expanding it early,
as a macro argument separate from the repeating fn pattern.
2016-09-06 03:54:02 -07:00
Adam Wright
8968b1fbd2 Merge pull request #49 from tikue/master
Remove two unused error variants of tarpc::Error.
2016-09-04 19:15:06 -07:00
Tim
ab6e829ddd Remove two unused errors. 2016-09-04 19:03:52 -07:00
Tim
246c09c876 Remove outdated potential improvement 2016-09-04 16:34:01 -07:00
Tim
e7cfbc1085 Update readme 2016-09-04 16:14:59 -07:00
Tim
7aabfb3c14 Rewrite using tokio (#44)
* Rewrite tarpc on top of tokio.

* Add examples

* Move error types to their own module.

Also, cull unused error variants.

* Remove unused fn

* Remove CanonicalRpcError* types. They're 100% useless.

* Track tokio master (WIP)

* The great error revamp.

Removed the canonical rpc error type. Instead, the user declares
the error type for each rpc:

In the above example, the error type is Baz. Declaring an error is
optional; if none is specified, it defaults to Never, a convenience
struct that wraps the never type (exclamation mark) to impl Serialize, Deserialize,
Error, etc. Also adds the convenience type StringError for easily
using a String as an error type.

* Add missing license header

* Minor cleanup

* Rename StringError => Message

* Create a sync::Connect trait.

Along with this, the existing Connect trait moves to future::Connect. The future
and sync modules are reexported from the crate root.

Additionally, the utility errors Never and Message are no longer reexported from
the crate root.

* Update readme

* Track tokio/futures master. Add a Spawn utility trait to replace the removed forget.

* Fix pre-push hook

* Add doc comment to SyncServiceExt.

* Fix up some documentation

* Track tokio-proto master

* Don't set tcp nodelay

* Make future::Connect take an associated type for the future.

* Unbox FutureClient::connect return type

* Use type alias instead of newtype struct for ClientFuture

* Fix benches/latency.rs

* Write a plugin to convert lower_snake_case idents/types to UpperCamelCase.

Use it to add associated types to FutureService instead of boxing the return futures.

* Specify plugin = true in snake_to_camel/Cargo.toml. Weird things happen otherwise.

* Add clippy.toml
2016-09-04 16:09:50 -07:00
Morton Fox
64ca851209 Fix the license link in the top strip (#48) 2016-08-26 21:42:40 -07:00
Dustin H
1738864d32 make documentation link go to latest version (#47) 2016-08-26 16:48:42 -07:00
Tim
683617674c Point documentation to docs.rs 2016-08-26 16:43:51 -07:00
Adam Wright
3c4932f55a Add gitter badge 2016-08-24 12:37:32 -07:00
Tim Kuehn
3cf8e440f7 Bump minor version. 2016-08-07 13:02:04 -07:00
56 changed files with 5603 additions and 1929 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
target
Cargo.lock
.cargo
*.swp
*.bk
tarpc.iml
.idea

View File

@@ -1,32 +1,13 @@
language: rust
sudo: false
rust:
- stable
- beta
- nightly
sudo: false
cache: cargo
addons:
apt:
packages:
- libcurl4-openssl-dev
- libelf-dev
- libdw-dev
before_script:
- |
pip install 'travis-cargo<0.2' --user &&
export PATH=$HOME/.local/bin:$PATH
os:
- osx
- linux
script:
- |
(cd tarpc && travis-cargo build) &&
(cd tarpc && travis-cargo test)
after_success:
- (cd tarpc && travis-cargo coveralls --no-sudo)
env:
global:
# override the default `--features unstable` used for the nightly branch
- TRAVIS_CARGO_NIGHTLY_FEATURE=""
- cargo test --all-targets --all-features
- cargo test --doc --all-features

10
Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[workspace]
members = [
"example-service",
"rpc",
"trace",
"bincode-transport",
"tarpc",
"plugins",
]

167
README.md
View File

@@ -1,92 +1,137 @@
## tarpc: Tim & Adam's RPC lib
[![Travis-CI Status](https://travis-ci.org/google/tarpc.png?branch=master)](https://travis-ci.org/google/tarpc)
[![Coverage Status](https://coveralls.io/repos/github/google/tarpc/badge.svg?branch=master)](https://coveralls.io/github/google/tarpc?branch=master)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.txt)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)
[![Latest Version](https://img.shields.io/crates/v/tarpc.svg)](https://crates.io/crates/tarpc)
[![Join the chat at https://gitter.im/tarpc/Lobby](https://badges.gitter.im/tarpc/Lobby.svg)](https://gitter.im/tarpc/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
*Disclaimer*: This is not an official Google product.
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.
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.
[Documentation](https://google.github.io/tarpc)
[Documentation](https://docs.rs/crate/tarpc/)
## 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 compute the function instead. The original
function then returns the value produced by that other server.
"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.
[More information](https://www.cs.cf.ac.uk/Dave/C/node33.html)
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.
## Usage
Add to your `Cargo.toml` dependencies:
```toml
tarpc = "0.5.1"
tarpc = "0.14.0"
```
## Example
```rust
#[macro_use]
extern crate tarpc;
The `service!` macro expands to a collection of items that form an
rpc service. In the above example, the macro is called within the
`hello_service` module. This module will contain a `Client` stub and `Service` trait. There is
These generated types make it easy and ergonomic to write servers without dealing with serialization
directly. Simply implement one of the generated traits, and you're off to the
races!
mod hello_service {
service! {
rpc hello(name: String) -> String;
## 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},
};
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))
}
}
use hello_service::Service as HelloService;
struct HelloServer;
impl HelloService for HelloServer {
fn hello(&self, name: String) -> String {
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() {
let addr = "localhost:10000";
let server_handle = HelloServer.spawn(addr).unwrap();
let client = hello_service::Client::new(addr).unwrap();
assert_eq!("Hello, Mom!", client.hello("Mom".into()).unwrap());
drop(client);
server_handle.shutdown();
tarpc::init(TokioDefaultSpawner);
tokio::run(run()
.map_err(|e| eprintln!("Oh no: {}", e))
.boxed()
.compat(),
);
}
```
The `service!` macro expands to a collection of items that collectively form an rpc service. In the
above example, the macro is called within the `hello_service` module. This module will contain a
`Client` (and `AsyncClient`) type, and a `Service` trait. The trait provides default `fn`s for
starting the service: `spawn` and `spawn_with_config`, which start the service listening over an
arbitrary transport. A `Client` (or `AsyncClient`) can connect to such a service. These generated
types make it easy and ergonomic to write servers without dealing with sockets or serialization
directly. See the tarpc_examples package for more sophisticated examples.
## Service Documentation
## Documentation
Use `cargo doc` as you normally would to see the documentation created for all
items expanded by a `service!` invocation.
## Additional Features
- Connect over any transport that `impl`s the `Transport` trait.
- Concurrent requests from a single client.
- Any type that `impl`s `serde`'s `Serialize` and `Deserialize` can be used in the rpc signatures.
- Attributes can be specified on rpc methods. These will be included on both the `Service` trait
methods as well as on the `Client`'s stub methods.
- Just like regular fns, the return type can be left off when it's `-> ()`.
- Arg-less rpc's are also allowed.
## Planned Improvements (actively being worked on)
- Automatically reconnect on the client side when the connection cuts out.
- Support asynchronous server implementations (currently thread per connection).
- Support generic serialization protocols.
## Contributing
To contribute to tarpc, please see [CONTRIBUTING](CONTRIBUTING.md).
## License
tarpc is distributed under the terms of the MIT license.
See [LICENSE](LICENSE) for details.

View File

@@ -1,3 +1,92 @@
## 0.13.0 (2018-10-16)
### Breaking Changes
Version 0.13 marks a significant departure from previous versions of tarpc. The
API has changed significantly. The tokio-proto crate has been torn out and
replaced with a homegrown rpc framework. Additionally, the crate has been
modularized, so that the tarpc crate itself contains only the macro code.
### 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!
## 0.10.0 (2018-04-08)
### Breaking Changes
Fixed rustc breakage in tarpc-plugins. These changes require a recent version of rustc.
## 0.10.0 (2018-03-26)
### Breaking Changes
Updates bincode to version 1.0.
## 0.9.0 (2017-09-17)
### Breaking Changes
Updates tarpc to use tarpc-plugins 0.2.
## 0.8.0 (2017-05-05)
### Breaking Changes
This release updates tarpc to use serde 1.0.
As such, users must also update to use serde 1.0.
The serde 1.0 [release notes](https://github.com/serde-rs/serde/releases/tag/v1.0.0)
detail migration paths.
## 0.7.3 (2017-04-26)
This release removes the `Sync` bound on RPC args for both sync and future
clients. No breaking changes.
## 0.7.2 (2017-04-22)
### Breaking Changes
This release updates tarpc-plugins to work with rustc master. Thus, older
versions of rustc are no longer supported. We chose a minor version bump
because it is still source-compatible with existing code using tarpc.
## 0.7.1 (2017-03-31)
This release was purely doc fixes. No breaking changes.
## 0.7 (2017-03-31)
### Breaking Changes
This release is a complete overhaul to build tarpc on top of the tokio stack.
It's safe to assume that everything broke with this release.
Two traits are now generated by the macro, `FutureService` and `SyncService`.
`SyncService` is the successor to the original `Service` trait. It uses a configurable
thread pool to serve requests. `FutureService`, as the name implies, uses futures
to serve requests and is single-threaded by default.
The easiest way to upgrade from a 0.6 service impl is to `impl SyncService for MyService`.
For more complete information, see the readme and the examples directory.
## 0.6 (2016-08-07)
### Breaking Changes
* Updated serde to 0.8. Requires dependents to update as well.
## 0.5 (2016-04-24)
### Breaking Changes

View File

@@ -0,0 +1,40 @@
cargo-features = ["rename-dependency"]
[package]
name = "tarpc-bincode-transport"
version = "0.3.0"
authors = ["Tim Kuehn <tikue@google.com>"]
edition = '2018'
license = "MIT"
documentation = "https://docs.rs/tarpc-bincode-transport"
homepage = "https://github.com/google/tarpc"
repository = "https://github.com/google/tarpc"
keywords = ["rpc", "network", "bincode", "serde", "tarpc"]
categories = ["asynchronous", "network-programming"]
readme = "../README.md"
description = "A bincode-based transport for tarpc services."
[dependencies]
bincode = { version = "1.0", features = ["i128"] }
futures_legacy = { version = "0.1", package = "futures" }
pin-utils = "0.1.0-alpha.3"
rpc = { package = "tarpc-lib", version = "0.2", path = "../rpc", features = ["serde1"] }
serde = "1.0"
tokio-io = "0.1"
async-bincode = "0.4"
tokio-tcp = "0.1"
[target.'cfg(not(test))'.dependencies]
futures-preview = { version = "0.3.0-alpha.9", features = ["compat"] }
[dev-dependencies]
futures-preview = { version = "0.3.0-alpha.9", features = ["compat", "tokio-compat"] }
env_logger = "0.5"
humantime = "1.0"
log = "0.4"
rand = "0.5"
tokio = "0.1"
tokio-executor = "0.1"
tokio-reactor = "0.1"
tokio-serde = "0.2"
tokio-timer = "0.2"

View File

@@ -0,0 +1 @@
edition = "2018"

View File

@@ -0,0 +1,150 @@
use futures::{compat::Stream01CompatExt, prelude::*, ready};
use futures_legacy::{
executor::{
self as executor01, Notify as Notify01, NotifyHandle as NotifyHandle01,
UnsafeNotify as UnsafeNotify01,
},
Async as Async01, AsyncSink as AsyncSink01, Sink as Sink01, Stream as Stream01,
};
use std::{
pin::Pin,
task::{self, LocalWaker, Poll},
};
/// A shim to convert a 0.1 Sink + Stream to a 0.3 Sink + Stream.
#[derive(Debug)]
pub struct Compat<S, SinkItem> {
staged_item: Option<SinkItem>,
inner: S,
}
impl<S, SinkItem> Compat<S, SinkItem> {
/// Returns a new Compat.
pub fn new(inner: S) -> Self {
Compat {
inner,
staged_item: None,
}
}
/// Unwraps Compat, returning the inner value.
pub fn into_inner(self) -> S {
self.inner
}
/// Returns a reference to the value wrapped by Compat.
pub fn get_ref(&self) -> &S {
&self.inner
}
}
impl<S, SinkItem> Stream for Compat<S, SinkItem>
where
S: Stream01,
{
type Item = Result<S::Item, S::Error>;
fn poll_next(self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Option<Self::Item>> {
unsafe {
let inner = &mut Pin::get_mut_unchecked(self).inner;
let mut compat = inner.compat();
let compat = Pin::new_unchecked(&mut compat);
match ready!(compat.poll_next(waker)) {
None => Poll::Ready(None),
Some(Ok(next)) => Poll::Ready(Some(Ok(next))),
Some(Err(e)) => Poll::Ready(Some(Err(e))),
}
}
}
}
impl<S, SinkItem> Sink for Compat<S, SinkItem>
where
S: Sink01<SinkItem = SinkItem>,
{
type SinkItem = SinkItem;
type SinkError = S::SinkError;
fn start_send(self: Pin<&mut Self>, item: SinkItem) -> Result<(), S::SinkError> {
let me = unsafe { Pin::get_mut_unchecked(self) };
assert!(me.staged_item.is_none());
me.staged_item = Some(item);
Ok(())
}
fn poll_ready(self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Result<(), S::SinkError>> {
let notify = &WakerToHandle(waker);
executor01::with_notify(notify, 0, move || {
let me = unsafe { Pin::get_mut_unchecked(self) };
match me.staged_item.take() {
Some(staged_item) => match me.inner.start_send(staged_item) {
Ok(AsyncSink01::Ready) => Poll::Ready(Ok(())),
Ok(AsyncSink01::NotReady(item)) => {
me.staged_item = Some(item);
Poll::Pending
}
Err(e) => Poll::Ready(Err(e)),
},
None => Poll::Ready(Ok(())),
}
})
}
fn poll_flush(self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Result<(), S::SinkError>> {
let notify = &WakerToHandle(waker);
executor01::with_notify(notify, 0, move || {
let me = unsafe { Pin::get_mut_unchecked(self) };
match me.inner.poll_complete() {
Ok(Async01::Ready(())) => Poll::Ready(Ok(())),
Ok(Async01::NotReady) => Poll::Pending,
Err(e) => Poll::Ready(Err(e)),
}
})
}
fn poll_close(self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Result<(), S::SinkError>> {
let notify = &WakerToHandle(waker);
executor01::with_notify(notify, 0, move || {
let me = unsafe { Pin::get_mut_unchecked(self) };
match me.inner.close() {
Ok(Async01::Ready(())) => Poll::Ready(Ok(())),
Ok(Async01::NotReady) => Poll::Pending,
Err(e) => Poll::Ready(Err(e)),
}
})
}
}
#[derive(Clone, Debug)]
struct WakerToHandle<'a>(&'a LocalWaker);
#[derive(Debug)]
struct NotifyWaker(task::Waker);
impl Notify01 for NotifyWaker {
fn notify(&self, _: usize) {
self.0.wake();
}
}
unsafe impl UnsafeNotify01 for NotifyWaker {
unsafe fn clone_raw(&self) -> NotifyHandle01 {
let ptr = Box::new(NotifyWaker(self.0.clone()));
NotifyHandle01::new(Box::into_raw(ptr))
}
unsafe fn drop_raw(&self) {
let ptr: *const dyn UnsafeNotify01 = self;
drop(Box::from_raw(ptr as *mut dyn UnsafeNotify01));
}
}
impl<'a> From<WakerToHandle<'a>> for NotifyHandle01 {
fn from(handle: WakerToHandle<'a>) -> NotifyHandle01 {
unsafe { NotifyWaker(handle.0.clone().into_waker()).clone_raw() }
}
}

View File

@@ -0,0 +1,202 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! A TCP [`Transport`] that serializes as bincode.
#![feature(
futures_api,
pin,
arbitrary_self_types,
await_macro,
async_await
)]
#![deny(missing_docs, missing_debug_implementations)]
use self::compat::Compat;
use async_bincode::{AsyncBincodeStream, AsyncDestination};
use futures::{
compat::{Compat01As03, Future01CompatExt, Stream01CompatExt},
prelude::*,
ready,
};
use pin_utils::unsafe_pinned;
use serde::{Deserialize, Serialize};
use std::{
error::Error,
io,
marker::PhantomData,
net::SocketAddr,
pin::Pin,
task::{LocalWaker, Poll},
};
use tokio_io::{AsyncRead, AsyncWrite};
use tokio_tcp::{TcpListener, TcpStream};
mod compat;
/// A transport that serializes to, and deserializes from, a [`TcpStream`].
#[derive(Debug)]
pub struct Transport<S, Item, SinkItem> {
inner: Compat<AsyncBincodeStream<S, Item, SinkItem, AsyncDestination>, SinkItem>,
}
impl<S, Item, SinkItem> Transport<S, Item, SinkItem> {
/// Returns the transport underlying the bincode transport.
pub fn into_inner(self) -> S {
self.inner.into_inner().into_inner()
}
}
impl<S, Item, SinkItem> Transport<S, Item, SinkItem> {
unsafe_pinned!(
inner: Compat<AsyncBincodeStream<S, Item, SinkItem, AsyncDestination>, SinkItem>
);
}
impl<S, Item, SinkItem> Stream for Transport<S, Item, SinkItem>
where
S: AsyncRead,
Item: for<'a> Deserialize<'a>,
{
type Item = io::Result<Item>;
fn poll_next(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Option<io::Result<Item>>> {
match self.inner().poll_next(waker) {
Poll::Pending => Poll::Pending,
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some(Ok(next))) => Poll::Ready(Some(Ok(next))),
Poll::Ready(Some(Err(e))) => {
Poll::Ready(Some(Err(io::Error::new(io::ErrorKind::Other, e))))
}
}
}
}
impl<S, Item, SinkItem> Sink for Transport<S, Item, SinkItem>
where
S: AsyncWrite,
SinkItem: Serialize,
{
type SinkItem = SinkItem;
type SinkError = io::Error;
fn start_send(mut self: Pin<&mut Self>, item: SinkItem) -> io::Result<()> {
self.inner()
.start_send(item)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}
fn poll_ready(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<io::Result<()>> {
convert(self.inner().poll_ready(waker))
}
fn poll_flush(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<io::Result<()>> {
convert(self.inner().poll_flush(waker))
}
fn poll_close(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<io::Result<()>> {
convert(self.inner().poll_close(waker))
}
}
fn convert<E: Into<Box<Error + Send + Sync>>>(poll: Poll<Result<(), E>>) -> Poll<io::Result<()>> {
match poll {
Poll::Pending => Poll::Pending,
Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
Poll::Ready(Err(e)) => Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))),
}
}
impl<Item, SinkItem> rpc::Transport for Transport<TcpStream, Item, SinkItem>
where
Item: for<'de> Deserialize<'de>,
SinkItem: Serialize,
{
type Item = Item;
type SinkItem = SinkItem;
fn peer_addr(&self) -> io::Result<SocketAddr> {
self.inner.get_ref().get_ref().peer_addr()
}
fn local_addr(&self) -> io::Result<SocketAddr> {
self.inner.get_ref().get_ref().local_addr()
}
}
/// Returns a new bincode transport that reads from and writes to `io`.
pub fn new<Item, SinkItem>(io: TcpStream) -> Transport<TcpStream, Item, SinkItem>
where
Item: for<'de> Deserialize<'de>,
SinkItem: Serialize,
{
Transport::from(io)
}
impl<S, Item, SinkItem> From<S> for Transport<S, Item, SinkItem> {
fn from(inner: S) -> Self {
Transport {
inner: Compat::new(AsyncBincodeStream::from(inner).for_async()),
}
}
}
/// Connects to `addr`, wrapping the connection in a bincode transport.
pub async fn connect<Item, SinkItem>(
addr: &SocketAddr,
) -> io::Result<Transport<TcpStream, Item, SinkItem>>
where
Item: for<'de> Deserialize<'de>,
SinkItem: Serialize,
{
Ok(new(await!(TcpStream::connect(addr).compat())?))
}
/// Listens on `addr`, wrapping accepted connections in bincode transports.
pub fn listen<Item, SinkItem>(addr: &SocketAddr) -> io::Result<Incoming<Item, SinkItem>>
where
Item: for<'de> Deserialize<'de>,
SinkItem: Serialize,
{
let listener = TcpListener::bind(addr)?;
let local_addr = listener.local_addr()?;
let incoming = listener.incoming().compat();
Ok(Incoming {
incoming,
local_addr,
ghost: PhantomData,
})
}
/// A [`TcpListener`] that wraps connections in bincode transports.
#[derive(Debug)]
pub struct Incoming<Item, SinkItem> {
incoming: Compat01As03<tokio_tcp::Incoming>,
local_addr: SocketAddr,
ghost: PhantomData<(Item, SinkItem)>,
}
impl<Item, SinkItem> Incoming<Item, SinkItem> {
unsafe_pinned!(incoming: Compat01As03<tokio_tcp::Incoming>);
/// Returns the address being listened on.
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
}
impl<Item, SinkItem> Stream for Incoming<Item, SinkItem>
where
Item: for<'a> Deserialize<'a>,
SinkItem: Serialize,
{
type Item = io::Result<Transport<TcpStream, Item, SinkItem>>;
fn poll_next(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Option<Self::Item>> {
let next = ready!(self.incoming().poll_next(waker)?);
Poll::Ready(next.map(|conn| Ok(new(conn))))
}
}

View File

@@ -0,0 +1,110 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! Tests client/server control flow.
#![feature(
test,
integer_atomics,
futures_api,
generators,
await_macro,
async_await
)]
extern crate test;
use self::test::stats::Stats;
use futures::{compat::TokioDefaultSpawner, prelude::*};
use rpc::{
client, context,
server::{Handler, Server},
};
use std::{
io,
time::{Duration, Instant},
};
async fn bench() -> io::Result<()> {
let listener = tarpc_bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
let addr = listener.local_addr();
tokio_executor::spawn(
Server::<u32, u32>::default()
.incoming(listener)
.take(1)
.respond_with(|_ctx, request| futures::future::ready(Ok(request)))
.unit_error()
.boxed()
.compat(),
);
let conn = await!(tarpc_bincode_transport::connect(&addr))?;
let client = &mut await!(client::new::<u32, u32, _>(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.call(context::current(), 0u32));
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)
);
Ok(())
}
#[test]
fn bench_small_packet() -> io::Result<()> {
env_logger::init();
rpc::init(TokioDefaultSpawner);
tokio::run(bench().map_err(|e| panic!(e.to_string())).boxed().compat());
println!("done");
Ok(())
}

View File

@@ -0,0 +1,143 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! Tests client/server control flow.
#![feature(generators, await_macro, async_await, futures_api)]
use futures::{
compat::{Future01CompatExt, TokioDefaultSpawner},
prelude::*,
stream,
};
use log::{info, trace};
use rand::distributions::{Distribution, Normal};
use rpc::{client, context, server::Server};
use std::{
io,
time::{Duration, Instant, SystemTime},
};
use tokio::timer::Delay;
pub trait AsDuration {
/// Delay of 0 if self is in the past
fn as_duration(&self) -> Duration;
}
impl AsDuration for SystemTime {
fn as_duration(&self) -> Duration {
self.duration_since(SystemTime::now()).unwrap_or_default()
}
}
async fn run() -> io::Result<()> {
let listener = tarpc_bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
let addr = listener.local_addr();
let server = Server::<String, String>::default()
.incoming(listener)
.take(1)
.for_each(async move |channel| {
let channel = if let Ok(channel) = channel {
channel
} else {
return;
};
let client_addr = *channel.client_addr();
let handler = channel.respond_with(move |ctx, request| {
// Sleep for a time sampled from a normal distribution with:
// - mean: 1/2 the deadline.
// - std dev: 1/2 the deadline.
let deadline: Duration = ctx.deadline.as_duration();
let deadline_millis = deadline.as_secs() * 1000 + deadline.subsec_millis() as u64;
let distribution =
Normal::new(deadline_millis as f64 / 2., deadline_millis as f64 / 2.);
let delay_millis = distribution.sample(&mut rand::thread_rng()).max(0.);
let delay = Duration::from_millis(delay_millis as u64);
trace!(
"[{}/{}] Responding to request in {:?}.",
ctx.trace_id(),
client_addr,
delay,
);
let wait = Delay::new(Instant::now() + delay).compat();
async move {
await!(wait).unwrap();
Ok(request)
}
});
tokio_executor::spawn(handler.unit_error().boxed().compat());
});
tokio_executor::spawn(server.unit_error().boxed().compat());
let conn = await!(tarpc_bincode_transport::connect(&addr))?;
let client = await!(client::new::<String, String, _>(
client::Config::default(),
conn
))?;
// Proxy service
let listener = tarpc_bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
let addr = listener.local_addr();
let proxy_server = Server::<String, String>::default()
.incoming(listener)
.take(1)
.for_each(move |channel| {
let client = client.clone();
async move {
let channel = if let Ok(channel) = channel {
channel
} else {
return;
};
let client_addr = *channel.client_addr();
let handler = channel.respond_with(move |ctx, request| {
trace!("[{}/{}] Proxying request.", ctx.trace_id(), client_addr);
let mut client = client.clone();
async move { await!(client.call(ctx, request)) }
});
tokio_executor::spawn(handler.unit_error().boxed().compat());
}
});
tokio_executor::spawn(proxy_server.unit_error().boxed().compat());
let mut config = client::Config::default();
config.max_in_flight_requests = 10;
config.pending_request_buffer = 10;
let client = await!(client::new::<String, String, _>(
config,
await!(tarpc_bincode_transport::connect(&addr))?
))?;
// Make 3 speculative requests, returning only the quickest.
let mut clients: Vec<_> = (1..=3u32).map(|_| client.clone()).collect();
let mut requests = vec![];
for client in &mut clients {
let mut ctx = context::current();
ctx.deadline = SystemTime::now() + Duration::from_millis(200);
let trace_id = *ctx.trace_id();
let response = client.call(ctx, "ping".into());
requests.push(response.map(move |r| (trace_id, r)));
}
let (fastest_response, _) = await!(stream::futures_unordered(requests).into_future());
let (trace_id, resp) = fastest_response.unwrap();
info!("[{}] fastest_response = {:?}", trace_id, resp);
Ok::<_, io::Error>(())
}
#[test]
fn cancel_slower() -> io::Result<()> {
env_logger::init();
rpc::init(TokioDefaultSpawner);
tokio::run(run().boxed().map_err(|e| panic!(e)).compat());
Ok(())
}

View File

@@ -0,0 +1,119 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! Tests client/server control flow.
#![feature(generators, await_macro, async_await, futures_api)]
use futures::{
compat::{Future01CompatExt, TokioDefaultSpawner},
prelude::*,
};
use log::{error, info, trace};
use rand::distributions::{Distribution, Normal};
use rpc::{client, context, server::Server};
use std::{
io,
time::{Duration, Instant, SystemTime},
};
use tokio::timer::Delay;
pub trait AsDuration {
/// Delay of 0 if self is in the past
fn as_duration(&self) -> Duration;
}
impl AsDuration for SystemTime {
fn as_duration(&self) -> Duration {
self.duration_since(SystemTime::now()).unwrap_or_default()
}
}
async fn run() -> io::Result<()> {
let listener = tarpc_bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
let addr = listener.local_addr();
let server = Server::<String, String>::default()
.incoming(listener)
.take(1)
.for_each(async move |channel| {
let channel = if let Ok(channel) = channel {
channel
} else {
return;
};
let client_addr = *channel.client_addr();
let handler = channel.respond_with(move |ctx, request| {
// Sleep for a time sampled from a normal distribution with:
// - mean: 1/2 the deadline.
// - std dev: 1/2 the deadline.
let deadline: Duration = ctx.deadline.as_duration();
let deadline_millis = deadline.as_secs() * 1000 + deadline.subsec_millis() as u64;
let distribution =
Normal::new(deadline_millis as f64 / 2., deadline_millis as f64 / 2.);
let delay_millis = distribution.sample(&mut rand::thread_rng()).max(0.);
let delay = Duration::from_millis(delay_millis as u64);
trace!(
"[{}/{}] Responding to request in {:?}.",
ctx.trace_id(),
client_addr,
delay,
);
let sleep = Delay::new(Instant::now() + delay).compat();
async {
await!(sleep).unwrap();
Ok(request)
}
});
tokio_executor::spawn(handler.unit_error().boxed().compat());
});
tokio_executor::spawn(server.unit_error().boxed().compat());
let mut config = client::Config::default();
config.max_in_flight_requests = 10;
config.pending_request_buffer = 10;
let conn = await!(tarpc_bincode_transport::connect(&addr))?;
let client = await!(client::new::<String, String, _>(config, conn))?;
let clients = (1..=100u32).map(|_| client.clone()).collect::<Vec<_>>();
for mut client in clients {
let ctx = context::current();
tokio_executor::spawn(
async move {
let trace_id = *ctx.trace_id();
let response = client.call(ctx, "ping".into());
match await!(response) {
Ok(response) => info!("[{}] response: {}", trace_id, response),
Err(e) => error!("[{}] request error: {:?}: {}", trace_id, e.kind(), e),
}
}
.unit_error()
.boxed()
.compat(),
);
}
Ok(())
}
#[test]
fn ping_pong() -> io::Result<()> {
env_logger::init();
rpc::init(TokioDefaultSpawner);
tokio::run(
run()
.map_ok(|_| println!("done"))
.map_err(|e| panic!(e.to_string()))
.boxed()
.compat(),
);
Ok(())
}

View File

@@ -0,0 +1,36 @@
cargo-features = ["rename-dependency"]
[package]
name = "tarpc-example-service"
version = "0.2.0"
authors = ["Tim Kuehn <tikue@google.com>"]
edition = "2018"
license = "MIT"
documentation = "https://docs.rs/tarpc-example-service"
homepage = "https://github.com/google/tarpc"
repository = "https://github.com/google/tarpc"
keywords = ["rpc", "network", "server", "microservices", "example"]
categories = ["asynchronous", "network-programming"]
readme = "../README.md"
description = "An example server built on tarpc."
[dependencies]
bincode-transport = { package = "tarpc-bincode-transport", version = "0.3", path = "../bincode-transport" }
clap = "2.0"
futures-preview = { version = "0.3.0-alpha.9", features = ["compat", "tokio-compat"] }
serde = { version = "1.0" }
tarpc = { version = "0.14", path = "../tarpc", features = ["serde1"] }
tokio = "0.1"
tokio-executor = "0.1"
[lib]
name = "service"
path = "src/lib.rs"
[[bin]]
name = "server"
path = "src/server.rs"
[[bin]]
name = "client"
path = "src/client.rs"

View File

@@ -0,0 +1,79 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#![feature(
futures_api,
pin,
arbitrary_self_types,
await_macro,
async_await
)]
use clap::{App, Arg};
use futures::{compat::TokioDefaultSpawner, prelude::*};
use std::{io, net::SocketAddr};
use tarpc::{client, context};
async fn run(server_addr: SocketAddr, name: String) -> io::Result<()> {
let transport = await!(bincode_transport::connect(&server_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!(service::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(), name))?;
println!("{}", hello);
Ok(())
}
fn main() {
let flags = App::new("Hello Client")
.version("0.1")
.author("Tim <tikue@google.com>")
.about("Say hello!")
.arg(
Arg::with_name("server_addr")
.long("server_addr")
.value_name("ADDRESS")
.help("Sets the server address to connect to.")
.required(true)
.takes_value(true),
)
.arg(
Arg::with_name("name")
.short("n")
.long("name")
.value_name("STRING")
.help("Sets the name to say hello to.")
.required(true)
.takes_value(true),
)
.get_matches();
tarpc::init(TokioDefaultSpawner);
let server_addr = flags.value_of("server_addr").unwrap();
let server_addr = server_addr
.parse()
.unwrap_or_else(|e| panic!(r#"--server_addr value "{}" invalid: {}"#, server_addr, e));
let name = flags.value_of("name").unwrap();
tarpc::init(TokioDefaultSpawner);
tokio::run(
run(server_addr, name.into())
.map_err(|e| eprintln!("Oh no: {}", e))
.boxed()
.compat(),
);
}

View File

@@ -0,0 +1,21 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#![feature(
futures_api,
pin,
arbitrary_self_types,
await_macro,
async_await,
proc_macro_hygiene
)]
// 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;
}

View File

@@ -0,0 +1,90 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#![feature(
futures_api,
pin,
arbitrary_self_types,
await_macro,
async_await
)]
use clap::{App, Arg};
use futures::{
compat::TokioDefaultSpawner,
future::{self, Ready},
prelude::*,
};
use std::{io, net::SocketAddr};
use tarpc::{
context,
server::{Handler, Server},
};
// 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::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(server_addr: SocketAddr) -> 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(&server_addr)?;
// The server is configured with the defaults.
let server = Server::default()
// Server can listen on any type that implements the Transport trait.
.incoming(transport)
// serve is generated by the service! macro. It takes as input any type implementing
// the generated Service trait.
.respond_with(service::serve(HelloServer));
await!(server);
Ok(())
}
fn main() {
let flags = App::new("Hello Server")
.version("0.1")
.author("Tim <tikue@google.com>")
.about("Say hello!")
.arg(
Arg::with_name("port")
.short("p")
.long("port")
.value_name("NUMBER")
.help("Sets the port number to listen on")
.required(true)
.takes_value(true),
)
.get_matches();
let port = flags.value_of("port").unwrap();
let port = port
.parse()
.unwrap_or_else(|e| panic!(r#"--port value "{}" invalid: {}"#, port, e));
tarpc::init(TokioDefaultSpawner);
tokio::run(
run(([0, 0, 0, 0], port).into())
.map_err(|e| eprintln!("Oh no: {}", e))
.boxed()
.compat(),
);
}

View File

@@ -67,7 +67,7 @@ else
fi
printf "${PREFIX} Checking for rustfmt ... "
command -v rustfmt &>/dev/null
command -v cargo fmt &>/dev/null
if [ $? == 0 ]; then
printf "${SUCCESS}\n"
else
@@ -89,22 +89,23 @@ fi
# not only contain discrete files.
printf "${PREFIX} Checking formatting ... "
FMTRESULT=0
diff=""
for file in $(git diff --name-only --cached);
do
if [ ${file: -3} == ".rs" ]; then
diff=$(rustfmt --skip-children --write-mode=diff $file)
if grep --quiet "^Diff at line" <<< "$diff"; then
FMTRESULT=1
fi
diff="$diff$(cargo fmt -- --skip-children --write-mode=diff $file)"
fi
done
if grep --quiet "^Diff at line" <<< "$diff"; then
FMTRESULT=1
fi
if [ "${TARPC_SKIP_RUSTFMT}" == 1 ]; then
printf "${SKIPPED}\n"$?
elif [ ${FMTRESULT} != 0 ]; then
FAILED=1
printf "${FAILURE}\n"
echo "$diff" | sed '/Using rustfmt.*$/d'
echo "$diff" | sed 's/Using rustfmt config file.*$/d/'
else
printf "${SUCCESS}\n"
fi

View File

@@ -20,11 +20,6 @@
# copy. Set to 0 by default, since the intent is to test the code that's being pushed, not changes
# still in the working copy.
#
# - TARPC_USE_CURRENT_TOOLCHAIN, default = 0
#
# Setting this variable to 1 will just run cargo build and cargo test, rather than running
# stable/beta/nightly.
#
# Note that these options are most useful for testing the hooks themselves. Use git push --no-verify
# to skip the pre-push hook altogether.
@@ -42,86 +37,62 @@ SUCCESS="${GREEN}ok${NC}"
printf "${PREFIX} Clean working copy ... "
git diff --exit-code &>/dev/null
if [ "$?" == 0 ]; then
printf "${SUCCESS}\n"
printf "${SUCCESS}\n"
else
if [ "${TARPC_ALLOW_DIRTY}" == "1" ]
then
printf "${SKIPPED}\n"
else
printf "${FAILURE}\n"
exit 1
fi
if [ "${TARPC_ALLOW_DIRTY}" == "1" ]
then
printf "${SKIPPED}\n"
else
printf "${FAILURE}\n"
exit 1
fi
fi
PREPUSH_RESULT=0
# args:
# 1 - cargo command to run (build/test)
# 2 - directory name of crate to build
# 3 - rust toolchain (nightly/stable/beta)
run_cargo() {
if [ "$1" == "build" ]; then
VERB=Building
else
VERB=Testing
fi
if [ "$3" != "" ]; then
printf "${PREFIX} $VERB $2 on $3 ... "
rustup run $3 cargo $1 --manifest-path $2/Cargo.toml &>/dev/null
else
printf "${PREFIX} $VERB $2 ... "
cargo $1 --manifest-path $2/Cargo.toml &>/dev/null
fi
if [ "$?" != "0" ]; then
printf "${FAILURE}\n"
try_run() {
TEXT=$1
shift
printf "${PREFIX} ${TEXT}"
OUTPUT=$($@ 2>&1)
if [ "$?" != "0" ]; then
printf "${FAILURE}, output shown below\n"
printf "\n\n"
printf "$OUTPUT"
printf "\n\n"
PREPUSH_RESULT=1
else
printf "${SUCCESS}\n"
fi
return 1
else
printf "${SUCCESS}\n"
fi
}
TOOLCHAIN_RESULT=0
check_toolchain() {
printf "${PREFIX} Checking for $1 toolchain ... "
if [[ $(rustup toolchain list) =~ $1 ]]; then
printf "${SUCCESS}\n"
else
TOOLCHAIN_RESULT=1
PREPUSH_RESULT=1
printf "${FAILURE}\n"
fi
printf "${PREFIX} Checking for $1 toolchain ... "
if [[ $(rustup toolchain list) =~ $1 ]]; then
printf "${SUCCESS}\n"
else
TOOLCHAIN_RESULT=1
PREPUSH_RESULT=1
printf "${FAILURE}\n"
fi
}
printf "${PREFIX} Checking for rustup ... "
printf "${PREFIX} Checking for rustup or current toolchain directive... "
command -v rustup &>/dev/null
if [ "$?" == 0 ] && [ "${TARPC_USE_CURRENT_TOOLCHAIN}" == "" ]; then
printf "${SUCCESS}\n"
if [ "$?" == 0 ]; then
printf "${SUCCESS}\n"
check_toolchain stable
check_toolchain beta
check_toolchain nightly
if [ ${TOOLCHAIN_RESULT} == 1 ]; then
exit 1
fi
check_toolchain nightly
if [ ${TOOLCHAIN_RESULT} == 1 ]; then
exit 1
fi
run_cargo build tarpc stable
run_cargo build tarpc_examples stable
try_run "Building ... " cargo build --color=always
try_run "Testing ... " cargo test --color=always
try_run "Doc Test ... " cargo clean && cargo build --tests && rustdoc --test README.md --edition 2018 -L target/debug/deps -Z unstable-options
run_cargo build tarpc beta
run_cargo build tarpc_examples beta
run_cargo build tarpc nightly
run_cargo build tarpc_examples nightly
# We still rely on some nightly stuff for tests
run_cargo test tarpc nightly
run_cargo test tarpc_examples nightly
else
printf "${YELLOW}NOT FOUND${NC}\n"
printf "${WARNING} Falling back to current toolchain: $(rustc -V)\n"
run_cargo test tarpc
run_cargo test tarpc_examples
fi
exit $PREPUSH_RESULT

24
plugins/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "tarpc-plugins"
version = "0.5.0"
authors = ["Adam Wright <adam.austin.wright@gmail.com>", "Tim Kuehn <timothy.j.kuehn@gmail.com>"]
license = "MIT"
documentation = "https://docs.rs/tarpc-plugins"
homepage = "https://github.com/google/tarpc"
repository = "https://github.com/google/tarpc"
keywords = ["rpc", "network", "server", "api", "microservices"]
categories = ["asynchronous", "network-programming"]
readme = "../README.md"
description = "Proc macros for tarpc."
[badges]
travis-ci = { repository = "google/tarpc" }
[dependencies]
itertools = "0.7"
syn = { version = "0.15", features = ["full", "extra-traits"] }
quote = "0.6"
proc-macro2 = "0.4"
[lib]
proc-macro = true

1
plugins/rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
edition = "2018"

90
plugins/src/lib.rs Normal file
View File

@@ -0,0 +1,90 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
extern crate itertools;
extern crate proc_macro;
extern crate proc_macro2;
extern crate quote;
extern crate syn;
use proc_macro::TokenStream;
use itertools::Itertools;
use proc_macro2::Span;
use quote::ToTokens;
use std::str::FromStr;
use syn::{parse, Ident, TraitItemType, TypePath};
#[proc_macro]
pub fn snake_to_camel(input: TokenStream) -> TokenStream {
let i = input.clone();
let mut assoc_type = parse::<TraitItemType>(input)
.unwrap_or_else(|_| panic!("Could not parse trait item from:\n{}", i));
let old_ident = convert(&mut assoc_type.ident);
for mut attr in &mut assoc_type.attrs {
if let Some(pair) = attr.path.segments.first() {
if pair.value().ident == "doc" {
attr.tts = proc_macro2::TokenStream::from_str(
&attr.tts.to_string().replace("{}", &old_ident),
)
.unwrap();
}
}
}
assoc_type.into_token_stream().into()
}
#[proc_macro]
pub fn ty_snake_to_camel(input: TokenStream) -> TokenStream {
let mut path = parse::<TypePath>(input).unwrap();
// Only capitalize the final segment
convert(&mut path.path.segments.last_mut().unwrap().into_value().ident);
path.into_token_stream().into()
}
/// Converts an ident in-place to CamelCase and returns the previous ident.
fn convert(ident: &mut Ident) -> String {
let ident_str = ident.to_string();
let mut camel_ty = String::new();
{
// Find the first non-underscore and add it capitalized.
let mut chars = ident_str.chars();
// Find the first non-underscore char, uppercase it, and append it.
// Guaranteed to succeed because all idents must have at least one non-underscore char.
camel_ty.extend(chars.find(|&c| c != '_').unwrap().to_uppercase());
// When we find an underscore, we remove it and capitalize the next char. To do this,
// we need to ensure the next char is not another underscore.
let mut chars = chars.coalesce(|c1, c2| {
if c1 == '_' && c2 == '_' {
Ok(c1)
} else {
Err((c1, c2))
}
});
while let Some(c) = chars.next() {
if c != '_' {
camel_ty.push(c);
} else if let Some(c) = chars.next() {
camel_ty.extend(c.to_uppercase());
}
}
}
// The Fut suffix is hardcoded right now; this macro isn't really meant to be general-purpose.
camel_ty.push_str("Fut");
*ident = Ident::new(&camel_ty, Span::call_site());
ident_str
}

38
rpc/Cargo.toml Normal file
View File

@@ -0,0 +1,38 @@
cargo-features = ["rename-dependency"]
[package]
name = "tarpc-lib"
version = "0.2.0"
authors = ["Tim Kuehn <tikue@google.com>"]
edition = '2018'
license = "MIT"
documentation = "https://docs.rs/tarpc-lib"
homepage = "https://github.com/google/tarpc"
repository = "https://github.com/google/tarpc"
keywords = ["rpc", "network", "server", "api", "microservices"]
categories = ["asynchronous", "network-programming"]
readme = "../README.md"
description = "An RPC framework for Rust with a focus on ease of use."
[features]
default = []
serde1 = ["trace/serde", "serde", "serde/derive"]
[dependencies]
fnv = "1.0"
humantime = "1.0"
log = "0.4"
pin-utils = "0.1.0-alpha.3"
rand = "0.5"
tokio-timer = "0.2"
trace = { package = "tarpc-trace", version = "0.1", path = "../trace" }
serde = { optional = true, version = "1.0" }
[target.'cfg(not(test))'.dependencies]
futures-preview = { version = "0.3.0-alpha.9", features = ["compat"] }
[dev-dependencies]
futures-preview = { version = "0.3.0-alpha.9", features = ["compat", "tokio-compat"] }
futures-test-preview = { version = "0.3.0-alpha.9" }
env_logger = "0.5"
tokio = "0.1"

1
rpc/rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
edition = "2018"

945
rpc/src/client/channel.rs Normal file
View File

@@ -0,0 +1,945 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use crate::{
context,
util::{deadline_compat, AsDuration, Compact},
ClientMessage, ClientMessageKind, Request, Response, Transport,
};
use fnv::FnvHashMap;
use futures::{
channel::{mpsc, oneshot},
prelude::*,
ready,
stream::Fuse,
task::LocalWaker,
Poll,
};
use humantime::format_rfc3339;
use log::{debug, error, info, trace};
use pin_utils::{unsafe_pinned, unsafe_unpinned};
use std::{
io,
marker::{self, Unpin},
net::SocketAddr,
pin::Pin,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Instant,
};
use trace::SpanId;
use super::Config;
/// Handles communication from the client to request dispatch.
#[derive(Debug)]
pub struct Channel<Req, Resp> {
to_dispatch: mpsc::Sender<DispatchRequest<Req, Resp>>,
/// Channel to send a cancel message to the dispatcher.
cancellation: RequestCancellation,
/// The ID to use for the next request to stage.
next_request_id: Arc<AtomicU64>,
server_addr: SocketAddr,
}
impl<Req, Resp> Clone for Channel<Req, Resp> {
fn clone(&self) -> Self {
Self {
to_dispatch: self.to_dispatch.clone(),
cancellation: self.cancellation.clone(),
next_request_id: self.next_request_id.clone(),
server_addr: self.server_addr,
}
}
}
/// A future returned by [`Channel::send`] that resolves to a server response.
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
struct Send<'a, Req, Resp> {
fut: MapOkDispatchResponse<
MapErrConnectionReset<futures::sink::Send<'a, mpsc::Sender<DispatchRequest<Req, Resp>>>>,
Resp,
>,
}
impl<'a, Req, Resp> Send<'a, Req, Resp> {
unsafe_pinned!(
fut: MapOkDispatchResponse<
MapErrConnectionReset<
futures::sink::Send<'a, mpsc::Sender<DispatchRequest<Req, Resp>>>,
>,
Resp,
>
);
}
impl<'a, Req, Resp> Future for Send<'a, Req, Resp> {
type Output = io::Result<DispatchResponse<Resp>>;
fn poll(mut self: Pin<&mut Self>, lw: &LocalWaker) -> Poll<Self::Output> {
self.fut().poll(lw)
}
}
/// A future returned by [`Channel::call`] that resolves to a server response.
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
pub struct Call<'a, Req, Resp> {
fut: AndThenIdent<Send<'a, Req, Resp>, DispatchResponse<Resp>>,
}
impl<'a, Req, Resp> Call<'a, Req, Resp> {
unsafe_pinned!(fut: AndThenIdent<Send<'a, Req, Resp>, DispatchResponse<Resp>>);
}
impl<'a, Req, Resp> Future for Call<'a, Req, Resp> {
type Output = io::Result<Resp>;
fn poll(mut self: Pin<&mut Self>, lw: &LocalWaker) -> Poll<Self::Output> {
self.fut().poll(lw)
}
}
impl<Req, Resp> Channel<Req, Resp> {
/// Sends a request to the dispatch task to forward to the server, returning a [`Future`] that
/// resolves when the request is sent (not when the response is received).
fn send<'a>(&'a mut self, mut ctx: context::Context, request: Req) -> Send<'a, Req, Resp> {
// Convert the context to the call context.
ctx.trace_context.parent_id = Some(ctx.trace_context.span_id);
ctx.trace_context.span_id = SpanId::random(&mut rand::thread_rng());
let timeout = ctx.deadline.as_duration();
let deadline = Instant::now() + timeout;
trace!(
"[{}/{}] Queuing request with deadline {} (timeout {:?}).",
ctx.trace_id(),
self.server_addr,
format_rfc3339(ctx.deadline),
timeout,
);
let (response_completion, response) = oneshot::channel();
let cancellation = self.cancellation.clone();
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
let server_addr = self.server_addr;
Send {
fut: MapOkDispatchResponse::new(
MapErrConnectionReset::new(self.to_dispatch.send(DispatchRequest {
ctx,
request_id,
request,
response_completion,
})),
DispatchResponse {
response: deadline_compat::Deadline::new(response, deadline),
complete: false,
request_id,
cancellation,
ctx,
server_addr,
},
),
}
}
/// Sends a request to the dispatch task to forward to the server, returning a [`Future`] that
/// resolves to the response.
pub fn call<'a>(&'a mut self, context: context::Context, request: Req) -> Call<'a, Req, Resp> {
Call {
fut: AndThenIdent::new(self.send(context, request)),
}
}
}
/// A server response that is completed by request dispatch when the corresponding response
/// arrives off the wire.
#[derive(Debug)]
struct DispatchResponse<Resp> {
response: deadline_compat::Deadline<oneshot::Receiver<Response<Resp>>>,
ctx: context::Context,
complete: bool,
cancellation: RequestCancellation,
request_id: u64,
server_addr: SocketAddr,
}
impl<Resp> DispatchResponse<Resp> {
unsafe_pinned!(server_addr: SocketAddr);
unsafe_pinned!(ctx: context::Context);
}
impl<Resp> Future for DispatchResponse<Resp> {
type Output = io::Result<Resp>;
fn poll(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<io::Result<Resp>> {
let resp = ready!(self.response.poll_unpin(waker));
self.complete = true;
Poll::Ready(match resp {
Ok(resp) => Ok(resp.message?),
Err(e) => Err({
let trace_id = *self.ctx().trace_id();
let server_addr = *self.server_addr();
if e.is_elapsed() {
io::Error::new(
io::ErrorKind::TimedOut,
"Client dropped expired request.".to_string(),
)
} else if e.is_timer() {
let e = e.into_timer().unwrap();
if e.is_at_capacity() {
io::Error::new(
io::ErrorKind::Other,
"Cancelling request because an expiration could not be set \
due to the timer being at capacity."
.to_string(),
)
} else if e.is_shutdown() {
panic!("[{}/{}] Timer was shutdown", trace_id, server_addr)
} else {
panic!(
"[{}/{}] Unrecognized timer error: {}",
trace_id, server_addr, e
)
}
} else if e.is_inner() {
// The oneshot is Canceled when the dispatch task ends.
io::Error::from(io::ErrorKind::ConnectionReset)
} else {
panic!(
"[{}/{}] Unrecognized deadline error: {}",
trace_id, server_addr, e
)
}
}),
})
}
}
// Cancels the request when dropped, if not already complete.
impl<Resp> Drop for DispatchResponse<Resp> {
fn drop(&mut self) {
if !self.complete {
// The receiver needs to be closed to handle the edge case that the request has not
// yet been received by the dispatch task. It is possible for the cancel message to
// arrive before the request itself, in which case the request could get stuck in the
// dispatch map forever if the server never responds (e.g. if the server dies while
// responding). Even if the server does respond, it will have unnecessarily done work
// for a client no longer waiting for a response. To avoid this, the dispatch task
// checks if the receiver is closed before inserting the request in the map. By
// closing the receiver before sending the cancel message, it is guaranteed that if the
// dispatch task misses an early-arriving cancellation message, then it will see the
// receiver as closed.
self.response.get_mut().close();
self.cancellation.cancel(self.request_id);
}
}
}
/// Spawns a dispatch task on the default executor that manages the lifecycle of requests initiated
/// by the returned [`Channel`].
pub async fn spawn<Req, Resp, C>(
config: Config,
transport: C,
server_addr: SocketAddr,
) -> io::Result<Channel<Req, Resp>>
where
Req: marker::Send + 'static,
Resp: marker::Send + 'static,
C: Transport<Item = Response<Resp>, SinkItem = ClientMessage<Req>> + marker::Send + 'static,
{
let (to_dispatch, pending_requests) = mpsc::channel(config.pending_request_buffer);
let (cancellation, canceled_requests) = cancellations();
crate::spawn(
RequestDispatch {
config,
server_addr,
canceled_requests,
transport: transport.fuse(),
in_flight_requests: FnvHashMap::default(),
pending_requests: pending_requests.fuse(),
}
.unwrap_or_else(move |e| error!("[{}] Connection broken: {}", server_addr, e)),
)
.map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!(
"Could not spawn client dispatch task. Is shutdown: {}",
e.is_shutdown()
),
)
})?;
Ok(Channel {
to_dispatch,
cancellation,
server_addr,
next_request_id: Arc::new(AtomicU64::new(0)),
})
}
/// Handles the lifecycle of requests, writing requests to the wire, managing cancellations,
/// and dispatching responses to the appropriate channel.
struct RequestDispatch<Req, Resp, C> {
/// Writes requests to the wire and reads responses off the wire.
transport: Fuse<C>,
/// Requests waiting to be written to the wire.
pending_requests: Fuse<mpsc::Receiver<DispatchRequest<Req, Resp>>>,
/// Requests that were dropped.
canceled_requests: CanceledRequests,
/// Requests already written to the wire that haven't yet received responses.
in_flight_requests: FnvHashMap<u64, InFlightData<Resp>>,
/// Configures limits to prevent unlimited resource usage.
config: Config,
/// The address of the server connected to.
server_addr: SocketAddr,
}
impl<Req, Resp, C> RequestDispatch<Req, Resp, C>
where
Req: marker::Send,
Resp: marker::Send,
C: Transport<Item = Response<Resp>, SinkItem = ClientMessage<Req>>,
{
unsafe_pinned!(server_addr: SocketAddr);
unsafe_pinned!(in_flight_requests: FnvHashMap<u64, InFlightData<Resp>>);
unsafe_pinned!(canceled_requests: CanceledRequests);
unsafe_pinned!(pending_requests: Fuse<mpsc::Receiver<DispatchRequest<Req, Resp>>>);
unsafe_pinned!(transport: Fuse<C>);
fn pump_read(self: &mut Pin<&mut Self>, waker: &LocalWaker) -> Poll<Option<io::Result<()>>> {
Poll::Ready(match ready!(self.transport().poll_next(waker)?) {
Some(response) => {
self.complete(response);
Some(Ok(()))
}
None => {
trace!("[{}] read half closed", self.server_addr());
None
}
})
}
fn pump_write(self: &mut Pin<&mut Self>, waker: &LocalWaker) -> Poll<Option<io::Result<()>>> {
enum ReceiverStatus {
NotReady,
Closed,
}
let pending_requests_status = match self.poll_next_request(waker)? {
Poll::Ready(Some(dispatch_request)) => {
self.write_request(dispatch_request)?;
return Poll::Ready(Some(Ok(())));
}
Poll::Ready(None) => ReceiverStatus::Closed,
Poll::Pending => ReceiverStatus::NotReady,
};
let canceled_requests_status = match self.poll_next_cancellation(waker)? {
Poll::Ready(Some((context, request_id))) => {
self.write_cancel(context, request_id)?;
return Poll::Ready(Some(Ok(())));
}
Poll::Ready(None) => ReceiverStatus::Closed,
Poll::Pending => ReceiverStatus::NotReady,
};
match (pending_requests_status, canceled_requests_status) {
(ReceiverStatus::Closed, ReceiverStatus::Closed) => {
ready!(self.transport().poll_flush(waker)?);
Poll::Ready(None)
}
(ReceiverStatus::NotReady, _) | (_, ReceiverStatus::NotReady) => {
// No more messages to process, so flush any messages buffered in the transport.
ready!(self.transport().poll_flush(waker)?);
// Even if we fully-flush, we return Pending, because we have no more requests
// or cancellations right now.
Poll::Pending
}
}
}
/// Yields the next pending request, if one is ready to be sent.
fn poll_next_request(
self: &mut Pin<&mut Self>,
waker: &LocalWaker,
) -> Poll<Option<io::Result<DispatchRequest<Req, Resp>>>> {
if self.in_flight_requests().len() >= self.config.max_in_flight_requests {
info!(
"At in-flight request capacity ({}/{}).",
self.in_flight_requests().len(),
self.config.max_in_flight_requests
);
// No need to schedule a wakeup, because timers and responses are responsible
// for clearing out in-flight requests.
return Poll::Pending;
}
while let Poll::Pending = self.transport().poll_ready(waker)? {
// We can't yield a request-to-be-sent before the transport is capable of buffering it.
ready!(self.transport().poll_flush(waker)?);
}
loop {
match ready!(self.pending_requests().poll_next_unpin(waker)) {
Some(request) => {
if request.response_completion.is_canceled() {
trace!(
"[{}] Request canceled before being sent.",
request.ctx.trace_id()
);
continue;
}
return Poll::Ready(Some(Ok(request)));
}
None => {
trace!("[{}] pending_requests closed", self.server_addr());
return Poll::Ready(None);
}
}
}
}
/// Yields the next pending cancellation, and, if one is ready, cancels the associated request.
fn poll_next_cancellation(
self: &mut Pin<&mut Self>,
waker: &LocalWaker,
) -> Poll<Option<io::Result<(context::Context, u64)>>> {
while let Poll::Pending = self.transport().poll_ready(waker)? {
ready!(self.transport().poll_flush(waker)?);
}
loop {
match ready!(self.canceled_requests().poll_next_unpin(waker)) {
Some(request_id) => {
if let Some(in_flight_data) = self.in_flight_requests().remove(&request_id) {
self.in_flight_requests().compact(0.1);
debug!(
"[{}/{}] Removed request.",
in_flight_data.ctx.trace_id(),
self.server_addr()
);
return Poll::Ready(Some(Ok((in_flight_data.ctx, request_id))));
}
}
None => {
trace!("[{}] canceled_requests closed.", self.server_addr());
return Poll::Ready(None);
}
}
}
}
fn write_request(
self: &mut Pin<&mut Self>,
dispatch_request: DispatchRequest<Req, Resp>,
) -> io::Result<()> {
let request_id = dispatch_request.request_id;
let request = ClientMessage {
trace_context: dispatch_request.ctx.trace_context,
message: ClientMessageKind::Request(Request {
id: request_id,
message: dispatch_request.request,
deadline: dispatch_request.ctx.deadline,
}),
};
self.transport().start_send(request)?;
self.in_flight_requests().insert(
request_id,
InFlightData {
ctx: dispatch_request.ctx,
response_completion: dispatch_request.response_completion,
},
);
Ok(())
}
fn write_cancel(
self: &mut Pin<&mut Self>,
context: context::Context,
request_id: u64,
) -> io::Result<()> {
let trace_id = *context.trace_id();
let cancel = ClientMessage {
trace_context: context.trace_context,
message: ClientMessageKind::Cancel { request_id },
};
self.transport().start_send(cancel)?;
trace!("[{}/{}] Cancel message sent.", trace_id, self.server_addr());
return Ok(());
}
/// Sends a server response to the client task that initiated the associated request.
fn complete(self: &mut Pin<&mut Self>, response: Response<Resp>) -> bool {
if let Some(in_flight_data) = self.in_flight_requests().remove(&response.request_id) {
self.in_flight_requests().compact(0.1);
trace!(
"[{}/{}] Received response.",
in_flight_data.ctx.trace_id(),
self.server_addr()
);
let _ = in_flight_data.response_completion.send(response);
return true;
}
debug!(
"[{}] No in-flight request found for request_id = {}.",
self.server_addr(),
response.request_id
);
// If the response completion was absent, then the request was already canceled.
false
}
}
impl<Req, Resp, C> Future for RequestDispatch<Req, Resp, C>
where
Req: marker::Send,
Resp: marker::Send,
C: Transport<Item = Response<Resp>, SinkItem = ClientMessage<Req>>,
{
type Output = io::Result<()>;
fn poll(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<io::Result<()>> {
trace!("[{}] RequestDispatch::poll", self.server_addr());
loop {
match (self.pump_read(waker)?, self.pump_write(waker)?) {
(read, write @ Poll::Ready(None)) => {
if self.in_flight_requests().is_empty() {
info!(
"[{}] Shutdown: write half closed, and no requests in flight.",
self.server_addr()
);
return Poll::Ready(Ok(()));
}
match read {
Poll::Ready(Some(())) => continue,
_ => {
trace!(
"[{}] read: {:?}, write: {:?}, (not ready)",
self.server_addr(),
read,
write,
);
return Poll::Pending;
}
}
}
(read @ Poll::Ready(Some(())), write) | (read, write @ Poll::Ready(Some(()))) => {
trace!(
"[{}] read: {:?}, write: {:?}",
self.server_addr(),
read,
write,
)
}
(read, write) => {
trace!(
"[{}] read: {:?}, write: {:?} (not ready)",
self.server_addr(),
read,
write,
);
return Poll::Pending;
}
}
}
}
}
/// A server-bound request sent from a [`Channel`] to request dispatch, which will then manage
/// the lifecycle of the request.
#[derive(Debug)]
struct DispatchRequest<Req, Resp> {
ctx: context::Context,
request_id: u64,
request: Req,
response_completion: oneshot::Sender<Response<Resp>>,
}
struct InFlightData<Resp> {
ctx: context::Context,
response_completion: oneshot::Sender<Response<Resp>>,
}
/// Sends request cancellation signals.
#[derive(Debug, Clone)]
struct RequestCancellation(mpsc::UnboundedSender<u64>);
/// A stream of IDs of requests that have been canceled.
#[derive(Debug)]
struct CanceledRequests(mpsc::UnboundedReceiver<u64>);
/// Returns a channel to send request cancellation messages.
fn cancellations() -> (RequestCancellation, CanceledRequests) {
// Unbounded because messages are sent in the drop fn. This is fine, because it's still
// bounded by the number of in-flight requests. Additionally, each request has a clone
// of the sender, so the bounded channel would have the same behavior,
// since it guarantees a slot.
let (tx, rx) = mpsc::unbounded();
(RequestCancellation(tx), CanceledRequests(rx))
}
impl RequestCancellation {
/// Cancels the request with ID `request_id`.
fn cancel(&mut self, request_id: u64) {
let _ = self.0.unbounded_send(request_id);
}
}
impl Stream for CanceledRequests {
type Item = u64;
fn poll_next(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Option<u64>> {
self.0.poll_next_unpin(waker)
}
}
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
struct MapErrConnectionReset<Fut> {
future: Fut,
finished: Option<()>,
}
impl<Fut> MapErrConnectionReset<Fut> {
unsafe_pinned!(future: Fut);
unsafe_unpinned!(finished: Option<()>);
fn new(future: Fut) -> MapErrConnectionReset<Fut> {
MapErrConnectionReset {
future,
finished: Some(()),
}
}
}
impl<Fut: Unpin> Unpin for MapErrConnectionReset<Fut> {}
impl<Fut> Future for MapErrConnectionReset<Fut>
where
Fut: TryFuture,
{
type Output = io::Result<Fut::Ok>;
fn poll(mut self: Pin<&mut Self>, lw: &LocalWaker) -> Poll<Self::Output> {
match self.future().try_poll(lw) {
Poll::Pending => Poll::Pending,
Poll::Ready(result) => {
self.finished().take().expect(
"MapErrConnectionReset must not be polled after it returned `Poll::Ready`",
);
Poll::Ready(result.map_err(|_| io::Error::from(io::ErrorKind::ConnectionReset)))
}
}
}
}
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
struct MapOkDispatchResponse<Fut, Resp> {
future: Fut,
response: Option<DispatchResponse<Resp>>,
}
impl<Fut, Resp> MapOkDispatchResponse<Fut, Resp> {
unsafe_pinned!(future: Fut);
unsafe_unpinned!(response: Option<DispatchResponse<Resp>>);
fn new(future: Fut, response: DispatchResponse<Resp>) -> MapOkDispatchResponse<Fut, Resp> {
MapOkDispatchResponse {
future,
response: Some(response),
}
}
}
impl<Fut: Unpin, Resp> Unpin for MapOkDispatchResponse<Fut, Resp> {}
impl<Fut, Resp> Future for MapOkDispatchResponse<Fut, Resp>
where
Fut: TryFuture,
{
type Output = Result<DispatchResponse<Resp>, Fut::Error>;
fn poll(mut self: Pin<&mut Self>, lw: &LocalWaker) -> Poll<Self::Output> {
match self.future().try_poll(lw) {
Poll::Pending => Poll::Pending,
Poll::Ready(result) => {
let response = self
.response()
.take()
.expect("MapOk must not be polled after it returned `Poll::Ready`");
Poll::Ready(result.map(|_| response))
}
}
}
}
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
struct AndThenIdent<Fut1, Fut2> {
try_chain: TryChain<Fut1, Fut2>,
}
impl<Fut1, Fut2> AndThenIdent<Fut1, Fut2>
where
Fut1: TryFuture<Ok = Fut2>,
Fut2: TryFuture,
{
unsafe_pinned!(try_chain: TryChain<Fut1, Fut2>);
/// Creates a new `Then`.
fn new(future: Fut1) -> AndThenIdent<Fut1, Fut2> {
AndThenIdent {
try_chain: TryChain::new(future),
}
}
}
impl<Fut1, Fut2> Future for AndThenIdent<Fut1, Fut2>
where
Fut1: TryFuture<Ok = Fut2>,
Fut2: TryFuture<Error = Fut1::Error>,
{
type Output = Result<Fut2::Ok, Fut2::Error>;
fn poll(mut self: Pin<&mut Self>, lw: &LocalWaker) -> Poll<Self::Output> {
self.try_chain().poll(lw, |result| match result {
Ok(ok) => TryChainAction::Future(ok),
Err(err) => TryChainAction::Output(Err(err)),
})
}
}
#[must_use = "futures do nothing unless polled"]
#[derive(Debug)]
enum TryChain<Fut1, Fut2> {
First(Fut1),
Second(Fut2),
Empty,
}
enum TryChainAction<Fut2>
where
Fut2: TryFuture,
{
Future(Fut2),
Output(Result<Fut2::Ok, Fut2::Error>),
}
impl<Fut1, Fut2> TryChain<Fut1, Fut2>
where
Fut1: TryFuture<Ok = Fut2>,
Fut2: TryFuture,
{
fn new(fut1: Fut1) -> TryChain<Fut1, Fut2> {
TryChain::First(fut1)
}
fn poll<F>(self: Pin<&mut Self>, lw: &LocalWaker, f: F) -> Poll<Result<Fut2::Ok, Fut2::Error>>
where
F: FnOnce(Result<Fut1::Ok, Fut1::Error>) -> TryChainAction<Fut2>,
{
let mut f = Some(f);
// Safe to call `get_mut_unchecked` because we won't move the futures.
let this = unsafe { Pin::get_mut_unchecked(self) };
loop {
let output = match this {
TryChain::First(fut1) => {
// Poll the first future
match unsafe { Pin::new_unchecked(fut1) }.try_poll(lw) {
Poll::Pending => return Poll::Pending,
Poll::Ready(output) => output,
}
}
TryChain::Second(fut2) => {
// Poll the second future
return unsafe { Pin::new_unchecked(fut2) }.try_poll(lw);
}
TryChain::Empty => {
panic!("future must not be polled after it returned `Poll::Ready`");
}
};
*this = TryChain::Empty; // Drop fut1
let f = f.take().unwrap();
match f(output) {
TryChainAction::Future(fut2) => *this = TryChain::Second(fut2),
TryChainAction::Output(output) => return Poll::Ready(output),
}
}
}
}
#[cfg(test)]
mod tests {
use super::{CanceledRequests, Channel, RequestCancellation, RequestDispatch};
use crate::{
client::Config,
context,
transport::{self, channel::UnboundedChannel},
ClientMessage, Response,
};
use fnv::FnvHashMap;
use futures::{channel::mpsc, prelude::*, Poll};
use futures_test::task::noop_local_waker_ref;
use std::{
marker,
net::{IpAddr, Ipv4Addr, SocketAddr},
pin::Pin,
sync::atomic::AtomicU64,
sync::Arc,
};
#[test]
fn stage_request() {
let (mut dispatch, mut channel, _server_channel) = set_up();
// Test that a request future dropped before it's processed by dispatch will cause the request
// to not be added to the in-flight request map.
let _resp = tokio::runtime::current_thread::block_on_all(
channel
.send(context::current(), "hi".to_string())
.boxed()
.compat(),
);
let mut dispatch = Pin::new(&mut dispatch);
let waker = &noop_local_waker_ref();
let req = dispatch.poll_next_request(waker).ready();
assert!(req.is_some());
let req = req.unwrap();
assert_eq!(req.request_id, 0);
assert_eq!(req.request, "hi".to_string());
}
#[test]
fn stage_request_response_future_dropped() {
let (mut dispatch, mut channel, _server_channel) = set_up();
// Test that a request future dropped before it's processed by dispatch will cause the request
// to not be added to the in-flight request map.
let resp = tokio::runtime::current_thread::block_on_all(
channel
.send(context::current(), "hi".into())
.boxed()
.compat(),
)
.unwrap();
drop(resp);
drop(channel);
let mut dispatch = Pin::new(&mut dispatch);
let waker = &noop_local_waker_ref();
dispatch.poll_next_cancellation(waker).unwrap();
assert!(dispatch.poll_next_request(waker).ready().is_none());
}
#[test]
fn stage_request_response_future_closed() {
let (mut dispatch, mut channel, _server_channel) = set_up();
// Test that a request future that's closed its receiver but not yet canceled its request --
// i.e. still in `drop fn` -- will cause the request to not be added to the in-flight request
// map.
let resp = tokio::runtime::current_thread::block_on_all(
channel
.send(context::current(), "hi".into())
.boxed()
.compat(),
)
.unwrap();
drop(resp);
drop(channel);
let mut dispatch = Pin::new(&mut dispatch);
let waker = &noop_local_waker_ref();
assert!(dispatch.poll_next_request(waker).ready().is_none());
}
fn set_up() -> (
RequestDispatch<String, String, UnboundedChannel<Response<String>, ClientMessage<String>>>,
Channel<String, String>,
UnboundedChannel<ClientMessage<String>, Response<String>>,
) {
let _ = env_logger::try_init();
let (to_dispatch, pending_requests) = mpsc::channel(1);
let (cancel_tx, canceled_requests) = mpsc::unbounded();
let (client_channel, server_channel) = transport::channel::unbounded();
let dispatch = RequestDispatch::<String, String, _> {
transport: client_channel.fuse(),
pending_requests: pending_requests.fuse(),
canceled_requests: CanceledRequests(canceled_requests),
in_flight_requests: FnvHashMap::default(),
config: Config::default(),
server_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
};
let cancellation = RequestCancellation(cancel_tx);
let channel = Channel {
to_dispatch,
cancellation,
next_request_id: Arc::new(AtomicU64::new(0)),
server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0),
};
(dispatch, channel, server_channel)
}
trait PollTest {
type T;
fn unwrap(self) -> Poll<Self::T>;
fn ready(self) -> Self::T;
}
impl<T, E> PollTest for Poll<Option<Result<T, E>>>
where
E: ::std::fmt::Display + marker::Send + 'static,
{
type T = Option<T>;
fn unwrap(self) -> Poll<Option<T>> {
match self {
Poll::Ready(Some(Ok(t))) => Poll::Ready(Some(t)),
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some(Err(e))) => panic!(e.to_string()),
Poll::Pending => Poll::Pending,
}
}
fn ready(self) -> Option<T> {
match self {
Poll::Ready(Some(Ok(t))) => Some(t),
Poll::Ready(None) => None,
Poll::Ready(Some(Err(e))) => panic!(e.to_string()),
Poll::Pending => panic!("Pending"),
}
}
}
}

151
rpc/src/client/mod.rs Normal file
View File

@@ -0,0 +1,151 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! Provides a client that connects to a server and sends multiplexed requests.
use crate::{context, ClientMessage, Response, Transport};
use futures::prelude::*;
use log::warn;
use std::{
io,
net::{Ipv4Addr, SocketAddr},
};
/// Provides a [`Client`] backed by a transport.
pub mod channel;
pub use self::channel::Channel;
/// Sends multiplexed requests to, and receives responses from, a server.
pub trait Client<'a, Req> {
/// The response type.
type Response;
/// The future response.
type Future: Future<Output = io::Result<Self::Response>> + 'a;
/// Initiates a request, sending it to the dispatch task.
///
/// Returns a [`Future`] that resolves to this client and the future response
/// once the request is successfully enqueued.
///
/// [`Future`]: futures::Future
fn call(&'a mut self, ctx: context::Context, request: Req) -> Self::Future;
/// Returns a Client that applies a post-processing function to the returned response.
fn map_response<F, R>(self, f: F) -> MapResponse<Self, F>
where
F: FnMut(Self::Response) -> R,
Self: Sized,
{
MapResponse { inner: self, f }
}
/// Returns a Client that applies a pre-processing function to the request.
fn with_request<F, Req2>(self, f: F) -> WithRequest<Self, F>
where
F: FnMut(Req2) -> Req,
Self: Sized,
{
WithRequest { inner: self, f }
}
}
/// A Client that applies a function to the returned response.
#[derive(Clone, Debug)]
pub struct MapResponse<C, F> {
inner: C,
f: F,
}
impl<'a, C, F, Req, Resp, Resp2> Client<'a, Req> for MapResponse<C, F>
where
C: Client<'a, Req, Response = Resp>,
F: FnMut(Resp) -> Resp2 + 'a,
{
type Response = Resp2;
type Future = futures::future::MapOk<<C as Client<'a, Req>>::Future, &'a mut F>;
fn call(&'a mut self, ctx: context::Context, request: Req) -> Self::Future {
self.inner.call(ctx, request).map_ok(&mut self.f)
}
}
/// A Client that applies a pre-processing function to the request.
#[derive(Clone, Debug)]
pub struct WithRequest<C, F> {
inner: C,
f: F,
}
impl<'a, C, F, Req, Req2, Resp> Client<'a, Req2> for WithRequest<C, F>
where
C: Client<'a, Req, Response = Resp>,
F: FnMut(Req2) -> Req,
{
type Response = Resp;
type Future = <C as Client<'a, Req>>::Future;
fn call(&'a mut self, ctx: context::Context, request: Req2) -> Self::Future {
self.inner.call(ctx, (self.f)(request))
}
}
impl<'a, Req, Resp> Client<'a, Req> for Channel<Req, Resp>
where
Req: 'a,
Resp: 'a,
{
type Response = Resp;
type Future = channel::Call<'a, Req, Resp>;
fn call(&'a mut self, ctx: context::Context, request: Req) -> channel::Call<'a, Req, Resp> {
self.call(ctx, request)
}
}
/// Settings that control the behavior of the client.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Config {
/// The number of requests that can be in flight at once.
/// `max_in_flight_requests` controls the size of the map used by the client
/// for storing pending requests.
pub max_in_flight_requests: usize,
/// The number of requests that can be buffered client-side before being sent.
/// `pending_requests_buffer` controls the size of the channel clients use
/// to communicate with the request dispatch task.
pub pending_request_buffer: usize,
}
impl Default for Config {
fn default() -> Self {
Config {
max_in_flight_requests: 1_000,
pending_request_buffer: 100,
}
}
}
/// Creates a new Client by wrapping a [`Transport`] and spawning a dispatch task
/// that manages the lifecycle of requests.
///
/// Must only be called from on an executor.
pub async fn new<Req, Resp, T>(config: Config, transport: T) -> io::Result<Channel<Req, Resp>>
where
Req: Send + 'static,
Resp: Send + 'static,
T: Transport<Item = Response<Resp>, SinkItem = ClientMessage<Req>> + Send + 'static,
{
let server_addr = transport.peer_addr().unwrap_or_else(|e| {
warn!(
"Setting peer to unspecified because peer could not be determined: {}",
e
);
SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0)
});
Ok(await!(channel::spawn(config, transport, server_addr))?)
}

45
rpc/src/context.rs Normal file
View File

@@ -0,0 +1,45 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! Provides a request context that carries a deadline and trace context. This context is sent from
//! client to server and is used by the server to enforce response deadlines.
use std::time::{Duration, SystemTime};
use trace::{self, TraceId};
/// A request context that carries request-scoped information like deadlines and trace information.
/// It is sent from client to server and is used by the server to enforce response deadlines.
///
/// The context should not be stored directly in a server implementation, because the context will
/// be different for each request in scope.
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct Context {
/// When the client expects the request to be complete by. The server should cancel the request
/// if it is not complete by this time.
pub deadline: SystemTime,
/// Uniquely identifies requests originating from the same source.
/// When a service handles a request by making requests itself, those requests should
/// include the same `trace_id` as that included on the original request. This way,
/// users can trace related actions across a distributed system.
pub trace_context: trace::Context,
}
/// Returns the context for the current request, or a default Context if no request is active.
// TODO: populate Context with request-scoped data, with default fallbacks.
pub fn current() -> Context {
Context {
deadline: SystemTime::now() + Duration::from_secs(10),
trace_context: trace::Context::new_root(),
}
}
impl Context {
/// Returns the ID of the request-scoped trace.
pub fn trace_id(&self) -> &TraceId {
&self.trace_context.trace_id
}
}

209
rpc/src/lib.rs Normal file
View File

@@ -0,0 +1,209 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#![feature(
non_exhaustive,
integer_atomics,
try_trait,
futures_api,
pin,
arbitrary_self_types,
await_macro,
async_await,
)]
#![deny(missing_docs, missing_debug_implementations)]
//! An RPC framework providing client and server.
//!
//! Features:
//! * RPC deadlines, both client- and server-side.
//! * Cascading cancellation (works with multiple hops).
//! * Configurable limits
//! * In-flight requests, both client and server-side.
//! * Server-side limit is per-connection.
//! * When the server reaches the in-flight request maximum, it returns a throttled error
//! to the client.
//! * When the client reaches the in-flight request max, messages are buffered up to a
//! configurable maximum, beyond which the requests are back-pressured.
//! * Server connections.
//! * Total and per-IP limits.
//! * When an incoming connection is accepted, if already at maximum, the connection is
//! dropped.
//! * Transport agnostic.
pub mod client;
pub mod context;
pub mod server;
pub mod transport;
pub(crate) mod util;
pub use crate::{client::Client, server::Server, transport::Transport};
use futures::{
task::{Spawn, SpawnError, SpawnExt},
Future,
};
use std::{cell::RefCell, io, sync::Once, time::SystemTime};
/// A message from a client to a server.
#[derive(Debug)]
#[cfg_attr(
feature = "serde1",
derive(serde::Serialize, serde::Deserialize)
)]
#[non_exhaustive]
pub struct ClientMessage<T> {
/// The trace context associates the message with a specific chain of causally-related actions,
/// possibly orchestrated across many distributed systems.
pub trace_context: trace::Context,
/// The message payload.
pub message: ClientMessageKind<T>,
}
/// Different messages that can be sent from a client to a server.
#[derive(Debug)]
#[cfg_attr(
feature = "serde1",
derive(serde::Serialize, serde::Deserialize)
)]
#[non_exhaustive]
pub enum ClientMessageKind<T> {
/// A request initiated by a user. The server responds to a request by invoking a
/// service-provided request handler. The handler completes with a [`response`](Response), which
/// the server sends back to the client.
Request(Request<T>),
/// A command to cancel an in-flight request, automatically sent by the client when a response
/// future is dropped.
///
/// When received, the server will immediately cancel the main task (top-level future) of the
/// request handler for the associated request. Any tasks spawned by the request handler will
/// not be canceled, because the framework layer does not
/// know about them.
Cancel {
/// The ID of the request to cancel.
request_id: u64,
},
}
/// A request from a client to a server.
#[derive(Debug)]
#[cfg_attr(
feature = "serde1",
derive(serde::Serialize, serde::Deserialize)
)]
#[non_exhaustive]
pub struct Request<T> {
/// Uniquely identifies the request across all requests sent over a single channel.
pub id: u64,
/// The request body.
pub message: T,
/// When the client expects the request to be complete by. The server will cancel the request
/// if it is not complete by this time.
#[cfg_attr(
feature = "serde1",
serde(serialize_with = "util::serde::serialize_epoch_secs")
)]
#[cfg_attr(
feature = "serde1",
serde(deserialize_with = "util::serde::deserialize_epoch_secs")
)]
pub deadline: SystemTime,
}
/// A response from a server to a client.
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serde1",
derive(serde::Serialize, serde::Deserialize)
)]
#[non_exhaustive]
pub struct Response<T> {
/// The ID of the request being responded to.
pub request_id: u64,
/// The response body, or an error if the request failed.
pub message: Result<T, ServerError>,
}
/// An error response from a server to a client.
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serde1",
derive(serde::Serialize, serde::Deserialize)
)]
#[non_exhaustive]
pub struct ServerError {
#[cfg_attr(
feature = "serde1",
serde(serialize_with = "util::serde::serialize_io_error_kind_as_u32")
)]
#[cfg_attr(
feature = "serde1",
serde(deserialize_with = "util::serde::deserialize_io_error_kind_from_u32")
)]
/// The type of error that occurred to fail the request.
pub kind: io::ErrorKind,
/// A message describing more detail about the error that occurred.
pub detail: Option<String>,
}
impl From<ServerError> for io::Error {
fn from(e: ServerError) -> io::Error {
io::Error::new(e.kind, e.detail.unwrap_or_default())
}
}
impl<T> Request<T> {
/// Returns the deadline for this request.
pub fn deadline(&self) -> &SystemTime {
&self.deadline
}
}
static INIT: Once = Once::new();
static mut SEED_SPAWN: Option<Box<dyn CloneSpawn>> = None;
thread_local! {
static SPAWN: RefCell<Box<dyn CloneSpawn>> = {
unsafe {
// INIT must always be called before accessing SPAWN.
// Otherwise, accessing SPAWN can trigger undefined behavior due to race conditions.
INIT.call_once(|| {});
RefCell::new(SEED_SPAWN.clone().expect("init() must be called."))
}
};
}
/// Initializes the RPC library with a mechanism to spawn futures on the user's runtime.
/// Client stubs and servers both use the initialized spawn.
///
/// Init only has an effect the first time it is called. If called previously, successive calls to
/// init are noops.
pub fn init(spawn: impl Spawn + Clone + 'static) {
unsafe {
INIT.call_once(|| {
SEED_SPAWN = Some(Box::new(spawn));
});
}
}
pub(crate) fn spawn(future: impl Future<Output = ()> + Send + 'static) -> Result<(), SpawnError> {
SPAWN.with(|spawn| spawn.borrow_mut().spawn(future))
}
trait CloneSpawn: Spawn {
fn box_clone(&self) -> Box<dyn CloneSpawn>;
}
impl Clone for Box<dyn CloneSpawn> {
fn clone(&self) -> Self {
self.box_clone()
}
}
impl<S: Spawn + Clone + 'static> CloneSpawn for S {
fn box_clone(&self) -> Box<dyn CloneSpawn> {
Box::new(self.clone())
}
}

260
rpc/src/server/filter.rs Normal file
View File

@@ -0,0 +1,260 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use crate::{
server::{Channel, Config},
util::Compact,
ClientMessage, Response, Transport,
};
use fnv::FnvHashMap;
use futures::{
channel::mpsc,
prelude::*,
ready,
stream::Fuse,
task::{LocalWaker, Poll},
};
use log::{debug, error, info, trace, warn};
use pin_utils::unsafe_pinned;
use std::{
collections::hash_map::Entry,
io,
marker::PhantomData,
net::{IpAddr, SocketAddr},
ops::Try,
option::NoneError,
pin::Pin,
};
/// Drops connections under configurable conditions:
///
/// 1. If the max number of connections is reached.
/// 2. If the max number of connections for a single IP is reached.
#[derive(Debug)]
pub struct ConnectionFilter<S, Req, Resp> {
listener: Fuse<S>,
closed_connections: mpsc::UnboundedSender<SocketAddr>,
closed_connections_rx: mpsc::UnboundedReceiver<SocketAddr>,
config: Config,
connections_per_ip: FnvHashMap<IpAddr, usize>,
open_connections: usize,
ghost: PhantomData<(Req, Resp)>,
}
enum NewConnection<Req, Resp, C> {
Filtered,
Accepted(Channel<Req, Resp, C>),
}
impl<Req, Resp, C> Try for NewConnection<Req, Resp, C> {
type Ok = Channel<Req, Resp, C>;
type Error = NoneError;
fn into_result(self) -> Result<Channel<Req, Resp, C>, NoneError> {
match self {
NewConnection::Filtered => Err(NoneError),
NewConnection::Accepted(channel) => Ok(channel),
}
}
fn from_error(_: NoneError) -> Self {
NewConnection::Filtered
}
fn from_ok(channel: Channel<Req, Resp, C>) -> Self {
NewConnection::Accepted(channel)
}
}
impl<S, Req, Resp> ConnectionFilter<S, Req, Resp> {
unsafe_pinned!(open_connections: usize);
unsafe_pinned!(config: Config);
unsafe_pinned!(connections_per_ip: FnvHashMap<IpAddr, usize>);
unsafe_pinned!(closed_connections_rx: mpsc::UnboundedReceiver<SocketAddr>);
unsafe_pinned!(listener: Fuse<S>);
/// Sheds new connections to stay under configured limits.
pub fn filter<C>(listener: S, config: Config) -> Self
where
S: Stream<Item = Result<C, io::Error>>,
C: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
{
let (closed_connections, closed_connections_rx) = mpsc::unbounded();
ConnectionFilter {
listener: listener.fuse(),
closed_connections,
closed_connections_rx,
config,
connections_per_ip: FnvHashMap::default(),
open_connections: 0,
ghost: PhantomData,
}
}
fn handle_new_connection<C>(self: &mut Pin<&mut Self>, stream: C) -> NewConnection<Req, Resp, C>
where
C: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
{
let peer = match stream.peer_addr() {
Ok(peer) => peer,
Err(e) => {
warn!("Could not get peer_addr of new connection: {}", e);
return NewConnection::Filtered;
}
};
let open_connections = *self.open_connections();
if open_connections >= self.config().max_connections {
warn!(
"[{}] Shedding connection because the maximum open connections \
limit is reached ({}/{}).",
peer,
open_connections,
self.config().max_connections
);
return NewConnection::Filtered;
}
let config = self.config.clone();
let open_connections_for_ip = self.increment_connections_for_ip(&peer)?;
*self.open_connections() += 1;
debug!(
"[{}] Opening channel ({}/{} connections for IP, {} total).",
peer,
open_connections_for_ip,
config.max_connections_per_ip,
self.open_connections(),
);
NewConnection::Accepted(Channel {
client_addr: peer,
closed_connections: self.closed_connections.clone(),
transport: stream.fuse(),
config,
ghost: PhantomData,
})
}
fn handle_closed_connection(self: &mut Pin<&mut Self>, addr: &SocketAddr) {
*self.open_connections() -= 1;
debug!(
"[{}] Closing channel. {} open connections remaining.",
addr, self.open_connections
);
self.decrement_connections_for_ip(&addr);
self.connections_per_ip().compact(0.1);
}
fn increment_connections_for_ip(self: &mut Pin<&mut Self>, peer: &SocketAddr) -> Option<usize> {
let max_connections_per_ip = self.config().max_connections_per_ip;
let mut occupied;
let mut connections_per_ip = self.connections_per_ip();
let occupied = match connections_per_ip.entry(peer.ip()) {
Entry::Vacant(vacant) => vacant.insert(0),
Entry::Occupied(o) => {
if *o.get() < max_connections_per_ip {
// Store the reference outside the block to extend the lifetime.
occupied = o;
occupied.get_mut()
} else {
info!(
"[{}] Opened max connections from IP ({}/{}).",
peer,
o.get(),
max_connections_per_ip
);
return None;
}
}
};
*occupied += 1;
Some(*occupied)
}
fn decrement_connections_for_ip(self: &mut Pin<&mut Self>, addr: &SocketAddr) {
let should_compact = match self.connections_per_ip().entry(addr.ip()) {
Entry::Vacant(_) => {
error!("[{}] Got vacant entry when closing connection.", addr);
return;
}
Entry::Occupied(mut occupied) => {
*occupied.get_mut() -= 1;
if *occupied.get() == 0 {
occupied.remove();
true
} else {
false
}
}
};
if should_compact {
self.connections_per_ip().compact(0.1);
}
}
fn poll_listener<C>(
self: &mut Pin<&mut Self>,
cx: &LocalWaker,
) -> Poll<Option<io::Result<NewConnection<Req, Resp, C>>>>
where
S: Stream<Item = Result<C, io::Error>>,
C: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
{
match ready!(self.listener().poll_next_unpin(cx)?) {
Some(codec) => Poll::Ready(Some(Ok(self.handle_new_connection(codec)))),
None => Poll::Ready(None),
}
}
fn poll_closed_connections(self: &mut Pin<&mut Self>, cx: &LocalWaker) -> Poll<io::Result<()>> {
match ready!(self.closed_connections_rx().poll_next_unpin(cx)) {
Some(addr) => {
self.handle_closed_connection(&addr);
Poll::Ready(Ok(()))
}
None => unreachable!("Holding a copy of closed_connections and didn't close it."),
}
}
}
impl<S, Req, Resp, T> Stream for ConnectionFilter<S, Req, Resp>
where
S: Stream<Item = Result<T, io::Error>>,
T: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
{
type Item = io::Result<Channel<Req, Resp, T>>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &LocalWaker,
) -> Poll<Option<io::Result<Channel<Req, Resp, T>>>> {
loop {
match (self.poll_listener(cx)?, self.poll_closed_connections(cx)?) {
(Poll::Ready(Some(NewConnection::Accepted(channel))), _) => {
return Poll::Ready(Some(Ok(channel)))
}
(Poll::Ready(Some(NewConnection::Filtered)), _) | (_, Poll::Ready(())) => {
trace!("Filtered a connection; {} open.", self.open_connections());
continue;
}
(Poll::Pending, Poll::Pending) => return Poll::Pending,
(Poll::Ready(None), Poll::Pending) => {
if *self.open_connections() > 0 {
trace!(
"Listener closed; {} open connections.",
self.open_connections()
);
return Poll::Pending;
}
trace!("Shutting down listener: all connections closed, and no more coming.");
return Poll::Ready(None);
}
}
}
}
}

609
rpc/src/server/mod.rs Normal file
View File

@@ -0,0 +1,609 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! Provides a server that concurrently handles many connections sending multiplexed requests.
use crate::{
context::Context, util::deadline_compat, util::AsDuration, util::Compact, ClientMessage,
ClientMessageKind, Request, Response, ServerError, Transport,
};
use fnv::FnvHashMap;
use futures::{
channel::mpsc,
future::{abortable, AbortHandle},
prelude::*,
ready,
stream::Fuse,
task::{LocalWaker, Poll},
try_ready,
};
use humantime::format_rfc3339;
use log::{debug, error, info, trace, warn};
use pin_utils::{unsafe_pinned, unsafe_unpinned};
use std::{
error::Error as StdError,
io,
marker::PhantomData,
net::SocketAddr,
pin::Pin,
time::{Instant, SystemTime},
};
use tokio_timer::timeout;
use trace::{self, TraceId};
mod filter;
/// Manages clients, serving multiplexed requests over each connection.
#[derive(Debug)]
pub struct Server<Req, Resp> {
config: Config,
ghost: PhantomData<(Req, Resp)>,
}
impl<Req, Resp> Default for Server<Req, Resp> {
fn default() -> Self {
new(Config::default())
}
}
/// Settings that control the behavior of the server.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Config {
/// The maximum number of clients that can be connected to the server at once. When at the
/// limit, existing connections are honored and new connections are rejected.
pub max_connections: usize,
/// The maximum number of clients per IP address that can be connected to the server at once.
/// When an IP is at the limit, existing connections are honored and new connections on that IP
/// address are rejected.
pub max_connections_per_ip: usize,
/// The maximum number of requests that can be in flight for each client. When a client is at
/// the in-flight request limit, existing requests are fulfilled and new requests are rejected.
/// Rejected requests are sent a response error.
pub max_in_flight_requests_per_connection: usize,
/// The number of responses per client that can be buffered server-side before being sent.
/// `pending_response_buffer` controls the buffer size of the channel that a server's
/// response tasks use to send responses to the client handler task.
pub pending_response_buffer: usize,
}
impl Default for Config {
fn default() -> Self {
Config {
max_connections: 1_000_000,
max_connections_per_ip: 1_000,
max_in_flight_requests_per_connection: 1_000,
pending_response_buffer: 100,
}
}
}
/// Returns a new server with configuration specified `config`.
pub fn new<Req, Resp>(config: Config) -> Server<Req, Resp> {
Server {
config,
ghost: PhantomData,
}
}
impl<Req, Resp> Server<Req, Resp> {
/// Returns the config for this server.
pub fn config(&self) -> &Config {
&self.config
}
/// Returns a stream of the incoming connections to the server.
pub fn incoming<S, T>(
self,
listener: S,
) -> impl Stream<Item = io::Result<Channel<Req, Resp, T>>>
where
Req: Send,
Resp: Send,
S: Stream<Item = io::Result<T>>,
T: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
{
self::filter::ConnectionFilter::filter(listener, self.config.clone())
}
}
/// The future driving the server.
#[derive(Debug)]
pub struct Running<S, F> {
incoming: S,
request_handler: F,
}
impl<S, F> Running<S, F> {
unsafe_pinned!(incoming: S);
unsafe_unpinned!(request_handler: F);
}
impl<S, T, Req, Resp, F, Fut> Future for Running<S, F>
where
S: Sized + Stream<Item = io::Result<Channel<Req, Resp, T>>>,
Req: Send + 'static,
Resp: Send + 'static,
T: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send + 'static,
F: FnOnce(Context, Req) -> Fut + Send + 'static + Clone,
Fut: Future<Output = io::Result<Resp>> + Send + 'static,
{
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &LocalWaker) -> Poll<()> {
while let Some(channel) = ready!(self.incoming().poll_next(cx)) {
match channel {
Ok(channel) => {
let peer = channel.client_addr;
if let Err(e) =
crate::spawn(channel.respond_with(self.request_handler().clone()))
{
warn!("[{}] Failed to spawn connection handler: {:?}", peer, e);
}
}
Err(e) => {
warn!("Incoming connection error: {}", e);
}
}
}
info!("Server shutting down.");
return Poll::Ready(());
}
}
/// A utility trait enabling a stream to fluently chain a request handler.
pub trait Handler<T, Req, Resp>
where
Self: Sized + Stream<Item = io::Result<Channel<Req, Resp, T>>>,
Req: Send,
Resp: Send,
T: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
{
/// Responds to all requests with `request_handler`.
fn respond_with<F, Fut>(self, request_handler: F) -> Running<Self, F>
where
F: FnOnce(Context, Req) -> Fut + Send + 'static + Clone,
Fut: Future<Output = io::Result<Resp>> + Send + 'static,
{
Running {
incoming: self,
request_handler,
}
}
}
impl<T, Req, Resp, S> Handler<T, Req, Resp> for S
where
S: Sized + Stream<Item = io::Result<Channel<Req, Resp, T>>>,
Req: Send,
Resp: Send,
T: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
{}
/// Responds to all requests with `request_handler`.
/// The server end of an open connection with a client.
#[derive(Debug)]
pub struct Channel<Req, Resp, T> {
/// Writes responses to the wire and reads requests off the wire.
transport: Fuse<T>,
/// Signals the connection is closed when `Channel` is dropped.
closed_connections: mpsc::UnboundedSender<SocketAddr>,
/// Channel limits to prevent unlimited resource usage.
config: Config,
/// The address of the server connected to.
client_addr: SocketAddr,
/// Types the request and response.
ghost: PhantomData<(Req, Resp)>,
}
impl<Req, Resp, T> Drop for Channel<Req, Resp, T> {
fn drop(&mut self) {
trace!("[{}] Closing channel.", self.client_addr);
// Even in a bounded channel, each connection would have a guaranteed slot, so using
// an unbounded sender is actually no different. And, the bound is on the maximum number
// of open connections.
if self
.closed_connections
.unbounded_send(self.client_addr)
.is_err()
{
warn!(
"[{}] Failed to send closed connection message.",
self.client_addr
);
}
}
}
impl<Req, Resp, T> Channel<Req, Resp, T> {
unsafe_pinned!(transport: Fuse<T>);
}
impl<Req, Resp, T> Channel<Req, Resp, T>
where
T: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
Req: Send,
Resp: Send,
{
pub(crate) fn start_send(
self: &mut Pin<&mut Self>,
response: Response<Resp>,
) -> io::Result<()> {
self.transport().start_send(response)
}
pub(crate) fn poll_ready(self: &mut Pin<&mut Self>, cx: &LocalWaker) -> Poll<io::Result<()>> {
self.transport().poll_ready(cx)
}
pub(crate) fn poll_flush(self: &mut Pin<&mut Self>, cx: &LocalWaker) -> Poll<io::Result<()>> {
self.transport().poll_flush(cx)
}
pub(crate) fn poll_next(
self: &mut Pin<&mut Self>,
cx: &LocalWaker,
) -> Poll<Option<io::Result<ClientMessage<Req>>>> {
self.transport().poll_next(cx)
}
/// Returns the address of the client connected to the channel.
pub fn client_addr(&self) -> &SocketAddr {
&self.client_addr
}
/// Respond to requests coming over the channel with `f`. Returns a future that drives the
/// responses and resolves when the connection is closed.
pub fn respond_with<F, Fut>(self, f: F) -> impl Future<Output = ()>
where
F: FnOnce(Context, Req) -> Fut + Send + 'static + Clone,
Fut: Future<Output = io::Result<Resp>> + Send + 'static,
Req: 'static,
Resp: 'static,
{
let (responses_tx, responses) = mpsc::channel(self.config.pending_response_buffer);
let responses = responses.fuse();
let peer = self.client_addr;
ClientHandler {
channel: self,
f,
pending_responses: responses,
responses_tx,
in_flight_requests: FnvHashMap::default(),
}
.unwrap_or_else(move |e| {
info!("[{}] ClientHandler errored out: {}", peer, e);
})
}
}
#[derive(Debug)]
struct ClientHandler<Req, Resp, T, F> {
channel: Channel<Req, Resp, T>,
/// Responses waiting to be written to the wire.
pending_responses: Fuse<mpsc::Receiver<(Context, Response<Resp>)>>,
/// Handed out to request handlers to fan in responses.
responses_tx: mpsc::Sender<(Context, Response<Resp>)>,
/// Number of requests currently being responded to.
in_flight_requests: FnvHashMap<u64, AbortHandle>,
/// Request handler.
f: F,
}
impl<Req, Resp, T, F> ClientHandler<Req, Resp, T, F> {
unsafe_pinned!(channel: Channel<Req, Resp, T>);
unsafe_pinned!(in_flight_requests: FnvHashMap<u64, AbortHandle>);
unsafe_pinned!(pending_responses: Fuse<mpsc::Receiver<(Context, Response<Resp>)>>);
unsafe_pinned!(responses_tx: mpsc::Sender<(Context, Response<Resp>)>);
// For this to be safe, field f must be private, and code in this module must never
// construct PinMut<F>.
unsafe_unpinned!(f: F);
}
impl<Req, Resp, T, F, Fut> ClientHandler<Req, Resp, T, F>
where
Req: Send + 'static,
Resp: Send + 'static,
T: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
F: FnOnce(Context, Req) -> Fut + Send + 'static + Clone,
Fut: Future<Output = io::Result<Resp>> + Send + 'static,
{
/// If at max in-flight requests, check that there's room to immediately write a throttled
/// response.
fn poll_ready_if_throttling(
self: &mut Pin<&mut Self>,
cx: &LocalWaker,
) -> Poll<io::Result<()>> {
if self.in_flight_requests.len()
>= self.channel.config.max_in_flight_requests_per_connection
{
let peer = self.channel().client_addr;
while let Poll::Pending = self.channel().poll_ready(cx)? {
info!(
"[{}] In-flight requests at max ({}), and transport is not ready.",
peer,
self.in_flight_requests().len(),
);
try_ready!(self.channel().poll_flush(cx));
}
}
Poll::Ready(Ok(()))
}
fn pump_read(self: &mut Pin<&mut Self>, cx: &LocalWaker) -> Poll<Option<io::Result<()>>> {
ready!(self.poll_ready_if_throttling(cx)?);
Poll::Ready(match ready!(self.channel().poll_next(cx)?) {
Some(message) => {
match message.message {
ClientMessageKind::Request(request) => {
self.handle_request(message.trace_context, request)?;
}
ClientMessageKind::Cancel { request_id } => {
self.cancel_request(&message.trace_context, request_id);
}
}
Some(Ok(()))
}
None => {
trace!("[{}] Read half closed", self.channel.client_addr);
None
}
})
}
fn pump_write(
self: &mut Pin<&mut Self>,
cx: &LocalWaker,
read_half_closed: bool,
) -> Poll<Option<io::Result<()>>> {
match self.poll_next_response(cx)? {
Poll::Ready(Some((_, response))) => {
self.channel().start_send(response)?;
Poll::Ready(Some(Ok(())))
}
Poll::Ready(None) => {
// Shutdown can't be done before we finish pumping out remaining responses.
ready!(self.channel().poll_flush(cx)?);
Poll::Ready(None)
}
Poll::Pending => {
// No more requests to process, so flush any requests buffered in the transport.
ready!(self.channel().poll_flush(cx)?);
// Being here means there are no staged requests and all written responses are
// fully flushed. So, if the read half is closed and there are no in-flight
// requests, then we can close the write half.
if read_half_closed && self.in_flight_requests().is_empty() {
Poll::Ready(None)
} else {
Poll::Pending
}
}
}
}
fn poll_next_response(
self: &mut Pin<&mut Self>,
cx: &LocalWaker,
) -> Poll<Option<io::Result<(Context, Response<Resp>)>>> {
// Ensure there's room to write a response.
while let Poll::Pending = self.channel().poll_ready(cx)? {
ready!(self.channel().poll_flush(cx)?);
}
let peer = self.channel().client_addr;
match ready!(self.pending_responses().poll_next(cx)) {
Some((ctx, response)) => {
if let Some(_) = self.in_flight_requests().remove(&response.request_id) {
self.in_flight_requests().compact(0.1);
}
trace!(
"[{}/{}] Staging response. In-flight requests = {}.",
ctx.trace_id(),
peer,
self.in_flight_requests().len(),
);
return Poll::Ready(Some(Ok((ctx, response))));
}
None => {
// This branch likely won't happen, since the ClientHandler is holding a Sender.
trace!("[{}] No new responses.", peer);
Poll::Ready(None)
}
}
}
fn handle_request(
self: &mut Pin<&mut Self>,
trace_context: trace::Context,
request: Request<Req>,
) -> io::Result<()> {
let request_id = request.id;
let peer = self.channel().client_addr;
let ctx = Context {
deadline: request.deadline,
trace_context,
};
let request = request.message;
if self.in_flight_requests().len()
>= self.channel().config.max_in_flight_requests_per_connection
{
debug!(
"[{}/{}] Client has reached in-flight request limit ({}/{}).",
ctx.trace_id(),
peer,
self.in_flight_requests().len(),
self.channel().config.max_in_flight_requests_per_connection
);
self.channel().start_send(Response {
request_id,
message: Err(ServerError {
kind: io::ErrorKind::WouldBlock,
detail: Some("Server throttled the request.".into()),
}),
})?;
return Ok(());
}
let deadline = ctx.deadline;
let timeout = deadline.as_duration();
trace!(
"[{}/{}] Received request with deadline {} (timeout {:?}).",
ctx.trace_id(),
peer,
format_rfc3339(deadline),
timeout,
);
let mut response_tx = self.responses_tx().clone();
let trace_id = *ctx.trace_id();
let response = self.f().clone()(ctx.clone(), request);
let response = deadline_compat::Deadline::new(response, Instant::now() + timeout).then(
async move |result| {
let response = Response {
request_id,
message: match result {
Ok(message) => Ok(message),
Err(e) => Err(make_server_error(e, trace_id, peer, deadline)),
},
};
trace!("[{}/{}] Sending response.", trace_id, peer);
await!(response_tx.send((ctx, response)).unwrap_or_else(|_| ()));
},
);
let (abortable_response, abort_handle) = abortable(response);
crate::spawn(abortable_response.map(|_| ())).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!(
"Could not spawn response task. Is shutdown: {}",
e.is_shutdown()
),
)
})?;
self.in_flight_requests().insert(request_id, abort_handle);
Ok(())
}
fn cancel_request(self: &mut Pin<&mut Self>, trace_context: &trace::Context, request_id: u64) {
// It's possible the request was already completed, so it's fine
// if this is None.
if let Some(cancel_handle) = self.in_flight_requests().remove(&request_id) {
self.in_flight_requests().compact(0.1);
cancel_handle.abort();
let remaining = self.in_flight_requests().len();
trace!(
"[{}/{}] Request canceled. In-flight requests = {}",
trace_context.trace_id,
self.channel.client_addr,
remaining,
);
} else {
trace!(
"[{}/{}] Received cancellation, but response handler \
is already complete.",
trace_context.trace_id,
self.channel.client_addr
);
}
}
}
impl<Req, Resp, T, F, Fut> Future for ClientHandler<Req, Resp, T, F>
where
Req: Send + 'static,
Resp: Send + 'static,
T: Transport<Item = ClientMessage<Req>, SinkItem = Response<Resp>> + Send,
F: FnOnce(Context, Req) -> Fut + Send + 'static + Clone,
Fut: Future<Output = io::Result<Resp>> + Send + 'static,
{
type Output = io::Result<()>;
fn poll(mut self: Pin<&mut Self>, cx: &LocalWaker) -> Poll<io::Result<()>> {
trace!("[{}] ClientHandler::poll", self.channel.client_addr);
loop {
let read = self.pump_read(cx)?;
match (read, self.pump_write(cx, read == Poll::Ready(None))?) {
(Poll::Ready(None), Poll::Ready(None)) => {
info!("[{}] Client disconnected.", self.channel.client_addr);
return Poll::Ready(Ok(()));
}
(read @ Poll::Ready(Some(())), write) | (read, write @ Poll::Ready(Some(()))) => {
trace!(
"[{}] read: {:?}, write: {:?}.",
self.channel.client_addr,
read,
write
)
}
(read, write) => {
trace!(
"[{}] read: {:?}, write: {:?} (not ready).",
self.channel.client_addr,
read,
write,
);
return Poll::Pending;
}
}
}
}
}
fn make_server_error(
e: timeout::Error<io::Error>,
trace_id: TraceId,
peer: SocketAddr,
deadline: SystemTime,
) -> ServerError {
if e.is_elapsed() {
debug!(
"[{}/{}] Response did not complete before deadline of {}s.",
trace_id,
peer,
format_rfc3339(deadline)
);
// No point in responding, since the client will have dropped the request.
ServerError {
kind: io::ErrorKind::TimedOut,
detail: Some(format!(
"Response did not complete before deadline of {}s.",
format_rfc3339(deadline)
)),
}
} else if e.is_timer() {
error!(
"[{}/{}] Response failed because of an issue with a timer: {}",
trace_id, peer, e
);
ServerError {
kind: io::ErrorKind::Other,
detail: Some(format!("{}", e)),
}
} else if e.is_inner() {
let e = e.into_inner().unwrap();
ServerError {
kind: e.kind(),
detail: Some(e.description().into()),
}
} else {
error!("[{}/{}] Unexpected response failure: {}", trace_id, peer, e);
ServerError {
kind: io::ErrorKind::Other,
detail: Some(format!("Server unexpectedly failed to respond: {}", e)),
}
}
}

View File

@@ -0,0 +1,158 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! Transports backed by in-memory channels.
use crate::Transport;
use futures::{channel::mpsc, task::LocalWaker, Poll, Sink, Stream};
use pin_utils::unsafe_pinned;
use std::pin::Pin;
use std::{
io,
net::{IpAddr, Ipv4Addr, SocketAddr},
};
/// Returns two unbounded channel peers. Each [`Stream`] yields items sent through the other's
/// [`Sink`].
pub fn unbounded<SinkItem, Item>() -> (
UnboundedChannel<SinkItem, Item>,
UnboundedChannel<Item, SinkItem>,
) {
let (tx1, rx2) = mpsc::unbounded();
let (tx2, rx1) = mpsc::unbounded();
(
UnboundedChannel { tx: tx1, rx: rx1 },
UnboundedChannel { tx: tx2, rx: rx2 },
)
}
/// A bi-directional channel backed by an [`UnboundedSender`](mpsc::UnboundedSender)
/// and [`UnboundedReceiver`](mpsc::UnboundedReceiver).
#[derive(Debug)]
pub struct UnboundedChannel<Item, SinkItem> {
rx: mpsc::UnboundedReceiver<Item>,
tx: mpsc::UnboundedSender<SinkItem>,
}
impl<Item, SinkItem> UnboundedChannel<Item, SinkItem> {
unsafe_pinned!(rx: mpsc::UnboundedReceiver<Item>);
unsafe_pinned!(tx: mpsc::UnboundedSender<SinkItem>);
}
impl<Item, SinkItem> Stream for UnboundedChannel<Item, SinkItem> {
type Item = Result<Item, io::Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &LocalWaker) -> Poll<Option<io::Result<Item>>> {
self.rx().poll_next(cx).map(|option| option.map(Ok))
}
}
impl<Item, SinkItem> Sink for UnboundedChannel<Item, SinkItem> {
type SinkItem = SinkItem;
type SinkError = io::Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &LocalWaker) -> Poll<io::Result<()>> {
self.tx()
.poll_ready(cx)
.map_err(|_| io::Error::from(io::ErrorKind::NotConnected))
}
fn start_send(mut self: Pin<&mut Self>, item: SinkItem) -> io::Result<()> {
self.tx()
.start_send(item)
.map_err(|_| io::Error::from(io::ErrorKind::NotConnected))
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &LocalWaker) -> Poll<Result<(), Self::SinkError>> {
self.tx()
.poll_flush(cx)
.map_err(|_| io::Error::from(io::ErrorKind::NotConnected))
}
fn poll_close(mut self: Pin<&mut Self>, cx: &LocalWaker) -> Poll<io::Result<()>> {
self.tx()
.poll_close(cx)
.map_err(|_| io::Error::from(io::ErrorKind::NotConnected))
}
}
impl<Item, SinkItem> Transport for UnboundedChannel<Item, SinkItem> {
type Item = Item;
type SinkItem = SinkItem;
fn peer_addr(&self) -> io::Result<SocketAddr> {
Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
}
fn local_addr(&self) -> io::Result<SocketAddr> {
Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
}
}
#[cfg(test)]
mod tests {
use crate::{
client, context,
server::{Handler, Server},
transport,
};
use futures::{compat::TokioDefaultSpawner, prelude::*, stream};
use log::trace;
use std::io;
#[test]
fn integration() {
let _ = env_logger::try_init();
crate::init(TokioDefaultSpawner);
let (client_channel, server_channel) = transport::channel::unbounded();
let server = Server::<String, u64>::default()
.incoming(stream::once(future::ready(Ok(server_channel))))
.respond_with(|_ctx, request| {
future::ready(request.parse::<u64>().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{:?} is not an int", request),
)
}))
});
let responses = async {
let mut client = await!(client::new(client::Config::default(), client_channel))?;
let response1 = await!(client.call(context::current(), "123".into()));
let response2 = await!(client.call(context::current(), "abc".into()));
Ok::<_, io::Error>((response1, response2))
};
let (response1, response2) =
run_future(server.join(responses.unwrap_or_else(|e| panic!(e)))).1;
trace!("response1: {:?}, response2: {:?}", response1, response2);
assert!(response1.is_ok());
assert_eq!(response1.ok().unwrap(), 123);
assert!(response2.is_err());
assert_eq!(response2.err().unwrap().kind(), io::ErrorKind::InvalidInput);
}
fn run_future<F>(f: F) -> F::Output
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let (tx, rx) = futures::channel::oneshot::channel();
tokio::run(
f.map(|result| tx.send(result).unwrap_or_else(|_| unreachable!()))
.boxed()
.unit_error()
.compat(),
);
futures::executor::block_on(rx).unwrap()
}
}

121
rpc/src/transport/mod.rs Normal file
View File

@@ -0,0 +1,121 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! Provides a [`Transport`] trait as well as implementations.
//!
//! The rpc crate is transport- and protocol-agnostic. Any transport that impls [`Transport`]
//! can be plugged in, using whatever protocol it wants.
use futures::prelude::*;
use std::{
io,
net::SocketAddr,
pin::Pin,
task::{LocalWaker, Poll},
};
pub mod channel;
/// A bidirectional stream ([`Sink`] + [`Stream`]) of messages.
pub trait Transport
where
Self: Stream<Item = io::Result<<Self as Transport>::Item>>,
Self: Sink<SinkItem = <Self as Transport>::SinkItem, SinkError = io::Error>,
{
/// The type read off the transport.
type Item;
/// The type written to the transport.
type SinkItem;
/// The address of the remote peer this transport is in communication with.
fn peer_addr(&self) -> io::Result<SocketAddr>;
/// The address of the local half of this transport.
fn local_addr(&self) -> io::Result<SocketAddr>;
}
/// Returns a new Transport backed by the given Stream + Sink and connecting addresses.
pub fn new<S, Item>(
inner: S,
peer_addr: SocketAddr,
local_addr: SocketAddr,
) -> impl Transport<Item = Item, SinkItem = S::SinkItem>
where
S: Stream<Item = io::Result<Item>>,
S: Sink<SinkError = io::Error>,
{
TransportShim {
inner,
peer_addr,
local_addr,
}
}
/// A transport created by adding peers to a Stream + Sink.
#[derive(Debug)]
struct TransportShim<S> {
peer_addr: SocketAddr,
local_addr: SocketAddr,
inner: S,
}
impl<S> TransportShim<S> {
pin_utils::unsafe_pinned!(inner: S);
}
impl<S> Stream for TransportShim<S>
where
S: Stream,
{
type Item = S::Item;
fn poll_next(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Option<S::Item>> {
self.inner().poll_next(waker)
}
}
impl<S> Sink for TransportShim<S>
where
S: Sink,
{
type SinkItem = S::SinkItem;
type SinkError = S::SinkError;
fn start_send(mut self: Pin<&mut Self>, item: S::SinkItem) -> Result<(), S::SinkError> {
self.inner().start_send(item)
}
fn poll_ready(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Result<(), S::SinkError>> {
self.inner().poll_ready(waker)
}
fn poll_flush(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Result<(), S::SinkError>> {
self.inner().poll_flush(waker)
}
fn poll_close(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Result<(), S::SinkError>> {
self.inner().poll_close(waker)
}
}
impl<S, Item> Transport for TransportShim<S>
where
S: Stream + Sink,
Self: Stream<Item = io::Result<Item>>,
Self: Sink<SinkItem = S::SinkItem, SinkError = io::Error>,
{
type Item = Item;
type SinkItem = S::SinkItem;
/// The address of the remote peer this transport is in communication with.
fn peer_addr(&self) -> io::Result<SocketAddr> {
Ok(self.peer_addr)
}
/// The address of the local half of this transport.
fn local_addr(&self) -> io::Result<SocketAddr> {
Ok(self.local_addr)
}
}

View File

@@ -0,0 +1,69 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use futures::{
compat::{Compat01As03, Future01CompatExt},
prelude::*,
ready,
task::{LocalWaker, Poll},
};
use pin_utils::unsafe_pinned;
use std::pin::Pin;
use std::time::Instant;
use tokio_timer::{timeout, Delay};
#[must_use = "futures do nothing unless polled"]
#[derive(Debug)]
pub struct Deadline<T> {
future: T,
delay: Compat01As03<Delay>,
}
impl<T> Deadline<T> {
unsafe_pinned!(future: T);
unsafe_pinned!(delay: Compat01As03<Delay>);
/// Create a new `Deadline` that completes when `future` completes or when
/// `deadline` is reached.
pub fn new(future: T, deadline: Instant) -> Deadline<T> {
Deadline::new_with_delay(future, Delay::new(deadline))
}
pub(crate) fn new_with_delay(future: T, delay: Delay) -> Deadline<T> {
Deadline {
future,
delay: delay.compat(),
}
}
/// Gets a mutable reference to the underlying future in this deadline.
pub fn get_mut(&mut self) -> &mut T {
&mut self.future
}
}
impl<T> Future for Deadline<T>
where
T: TryFuture,
{
type Output = Result<T::Ok, timeout::Error<T::Error>>;
fn poll(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Self::Output> {
// First, try polling the future
match self.future().try_poll(waker) {
Poll::Ready(Ok(v)) => return Poll::Ready(Ok(v)),
Poll::Pending => {}
Poll::Ready(Err(e)) => return Poll::Ready(Err(timeout::Error::inner(e))),
}
let delay = self.delay().poll_unpin(waker);
// Now check the timer
match ready!(delay) {
Ok(_) => Poll::Ready(Err(timeout::Error::elapsed())),
Err(e) => Poll::Ready(Err(timeout::Error::timer(e))),
}
}
}

46
rpc/src/util/mod.rs Normal file
View File

@@ -0,0 +1,46 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use std::{
collections::HashMap,
hash::{BuildHasher, Hash},
time::{Duration, SystemTime},
};
pub mod deadline_compat;
#[cfg(feature = "serde")]
pub mod serde;
/// Types that can be represented by a [`Duration`].
pub trait AsDuration {
fn as_duration(&self) -> Duration;
}
impl AsDuration for SystemTime {
/// Duration of 0 if self is earlier than [`SystemTime::now`].
fn as_duration(&self) -> Duration {
self.duration_since(SystemTime::now()).unwrap_or_default()
}
}
/// Collection compaction; configurable `shrink_to_fit`.
pub trait Compact {
/// Compacts space if the ratio of length : capacity is less than `usage_ratio_threshold`.
fn compact(&mut self, usage_ratio_threshold: f64);
}
impl<K, V, H> Compact for HashMap<K, V, H>
where
K: Eq + Hash,
H: BuildHasher,
{
fn compact(&mut self, usage_ratio_threshold: f64) {
let usage_ratio = self.len() as f64 / self.capacity() as f64;
if usage_ratio < usage_ratio_threshold {
self.shrink_to_fit();
}
}
}

95
rpc/src/util/serde.rs Normal file
View File

@@ -0,0 +1,95 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{
io,
time::{Duration, SystemTime},
};
/// Serializes `system_time` as a `u64` equal to the number of seconds since the epoch.
pub fn serialize_epoch_secs<S>(system_time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
system_time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs() // Only care about second precision
.serialize(serializer)
}
/// Deserializes [`SystemTime`] from a `u64` equal to the number of seconds since the epoch.
pub fn deserialize_epoch_secs<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
where
D: Deserializer<'de>,
{
Ok(SystemTime::UNIX_EPOCH + Duration::from_secs(u64::deserialize(deserializer)?))
}
/// Serializes [`io::ErrorKind`] as a `u32`.
pub fn serialize_io_error_kind_as_u32<S>(
kind: &io::ErrorKind,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use std::io::ErrorKind::*;
match *kind {
NotFound => 0,
PermissionDenied => 1,
ConnectionRefused => 2,
ConnectionReset => 3,
ConnectionAborted => 4,
NotConnected => 5,
AddrInUse => 6,
AddrNotAvailable => 7,
BrokenPipe => 8,
AlreadyExists => 9,
WouldBlock => 10,
InvalidInput => 11,
InvalidData => 12,
TimedOut => 13,
WriteZero => 14,
Interrupted => 15,
Other => 16,
UnexpectedEof => 17,
_ => 16,
}
.serialize(serializer)
}
/// Deserializes [`io::ErrorKind`] from a `u32`.
pub fn deserialize_io_error_kind_from_u32<'de, D>(
deserializer: D,
) -> Result<io::ErrorKind, D::Error>
where
D: Deserializer<'de>,
{
use std::io::ErrorKind::*;
Ok(match u32::deserialize(deserializer)? {
0 => NotFound,
1 => PermissionDenied,
2 => ConnectionRefused,
3 => ConnectionReset,
4 => ConnectionAborted,
5 => NotConnected,
6 => AddrInUse,
7 => AddrNotAvailable,
8 => BrokenPipe,
9 => AlreadyExists,
10 => WouldBlock,
11 => InvalidInput,
12 => InvalidData,
13 => TimedOut,
14 => WriteZero,
15 => Interrupted,
16 => Other,
17 => UnexpectedEof,
_ => Other,
})
}

View File

@@ -1,23 +1,42 @@
cargo-features = ["rename-dependency"]
[package]
name = "tarpc"
version = "0.5.1"
version = "0.14.0"
authors = ["Adam Wright <adam.austin.wright@gmail.com>", "Tim Kuehn <timothy.j.kuehn@gmail.com>"]
edition = "2018"
license = "MIT"
documentation = "https://google.github.io/tarpc"
documentation = "https://docs.rs/tarpc"
homepage = "https://github.com/google/tarpc"
repository = "https://github.com/google/tarpc"
keywords = ["rpc", "protocol", "remote", "procedure", "serialize"]
keywords = ["rpc", "network", "server", "api", "microservices"]
categories = ["asynchronous", "network-programming"]
readme = "../README.md"
description = "An RPC framework for Rust with a focus on ease of use."
[features]
serde1 = ["rpc/serde1", "serde", "serde/derive"]
[badges]
travis-ci = { repository = "google/tarpc" }
[dependencies]
bincode = "0.6"
log = "0.3"
scoped-pool = "1.0"
serde = "0.8"
unix_socket = "0.5"
log = "0.4"
serde = { optional = true, version = "1.0" }
tarpc-plugins = { path = "../plugins", version = "0.5.0" }
rpc = { package = "tarpc-lib", path = "../rpc", version = "0.2" }
[target.'cfg(not(test))'.dependencies]
futures-preview = "0.3.0-alpha.9"
[dev-dependencies]
lazy_static = "0.2"
env_logger = "0.3"
tempdir = "0.3"
bincode = "1.0"
bytes = { version = "0.4", features = ["serde"] }
humantime = "1.0"
futures-preview = { version = "0.3.0-alpha.9", features = ["compat", "tokio-compat"] }
bincode-transport = { package = "tarpc-bincode-transport", version = "0.3", path = "../bincode-transport" }
env_logger = "0.5"
tokio = "0.1"
tokio-executor = "0.1"
tokio-tcp = "0.1"
pin-utils = "0.1.0-alpha.3"

1
tarpc/clippy.toml Normal file
View File

@@ -0,0 +1 @@
doc-valid-idents = ["gRPC"]

185
tarpc/examples/pubsub.rs Normal file
View File

@@ -0,0 +1,185 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#![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::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));
}

94
tarpc/examples/readme.rs Normal file
View File

@@ -0,0 +1,94 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#![feature(
futures_api,
pin,
arbitrary_self_types,
await_macro,
async_await,
proc_macro_hygiene
)]
use futures::{
compat::TokioDefaultSpawner,
future::{self, Ready},
prelude::*,
};
use rpc::{
client, context,
server::{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::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() {
tarpc::init(TokioDefaultSpawner);
tokio::run(
run()
.map_err(|e| eprintln!("Oh no: {}", e))
.boxed()
.compat(),
);
}

View File

@@ -0,0 +1,108 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#![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::{
compat::TokioDefaultSpawner,
future::{self, Ready},
prelude::*,
};
use rpc::{
client, context,
server::{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::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::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();
tarpc::init(TokioDefaultSpawner);
tokio::run(run().map_err(|e| panic!(e)).boxed().compat());
}

View File

@@ -0,0 +1,411 @@
#![feature(
pin,
async_await,
await_macro,
futures_api,
arbitrary_self_types,
proc_macro_hygiene,
impl_trait_in_bindings
)]
mod registry {
use bytes::Bytes;
use futures::{
future::{ready, Ready},
prelude::*,
};
use serde::{Deserialize, Serialize};
use std::{
io,
pin::Pin,
sync::Arc,
task::{LocalWaker, Poll},
};
use tarpc::{
client::{self, Client},
context,
};
/// A request to a named service.
#[derive(Serialize, Deserialize)]
pub struct ServiceRequest {
service_name: String,
request: Bytes,
}
/// A response from a named service.
#[derive(Serialize, Deserialize)]
pub struct ServiceResponse {
response: Bytes,
}
/// A list of registered services.
pub struct Registry<Services> {
registrations: Services,
}
impl Default for Registry<Nil> {
fn default() -> Self {
Registry { registrations: Nil }
}
}
impl<Services: MaybeServe + Sync> Registry<Services> {
/// Returns a function that serves requests for the registered services.
pub fn serve(
self,
) -> impl FnOnce(context::Context, ServiceRequest)
-> Either<Services::Future, Ready<io::Result<ServiceResponse>>>
+ Clone {
let registrations = Arc::new(self.registrations);
move |cx, req: ServiceRequest| match registrations.serve(cx, &req) {
Some(serve) => Either::Left(serve),
None => Either::Right(ready(Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Service '{}' not registered", req.service_name),
)))),
}
}
/// Registers `serve` with the given `name` using the given serialization scheme.
pub fn register<S, Req, Resp, RespFut, Ser, De>(
self,
name: String,
serve: S,
deserialize: De,
serialize: Ser,
) -> Registry<Registration<impl Serve + Send + 'static, Services>>
where
Req: Send,
S: FnOnce(context::Context, Req) -> RespFut + Send + 'static + Clone,
RespFut: Future<Output = io::Result<Resp>> + Send + 'static,
De: FnOnce(Bytes) -> io::Result<Req> + Send + 'static + Clone,
Ser: FnOnce(Resp) -> io::Result<Bytes> + Send + 'static + Clone,
{
let registrations = Registration {
name: name,
serve: move |cx, req: Bytes| {
async move {
let req = deserialize.clone()(req)?;
let response = await!(serve.clone()(cx, req))?;
let response = serialize.clone()(response)?;
Ok(ServiceResponse { response })
}
},
rest: self.registrations,
};
Registry { registrations }
}
}
/// Creates a client that sends requests to a service
/// named `service_name`, over the given channel, using
/// the specified serialization scheme.
pub fn new_client<Req, Resp, Ser, De>(
service_name: String,
channel: &client::Channel<ServiceRequest, ServiceResponse>,
mut serialize: Ser,
mut deserialize: De,
) -> client::MapResponse<
client::WithRequest<
client::Channel<ServiceRequest, ServiceResponse>,
impl FnMut(Req) -> ServiceRequest,
>,
impl FnMut(ServiceResponse) -> Resp,
>
where
Req: Send + 'static,
Resp: Send + 'static,
De: FnMut(Bytes) -> io::Result<Resp> + Clone + Send + 'static,
Ser: FnMut(Req) -> io::Result<Bytes> + Clone + Send + 'static,
{
channel
.clone()
.with_request(move |req| {
ServiceRequest {
service_name: service_name.clone(),
// TODO: shouldn't need to unwrap here. Maybe with_request should allow for
// returning Result.
request: serialize(req).unwrap(),
}
})
// TODO: same thing. Maybe this should be more like and_then rather than map.
.map_response(move |resp| deserialize(resp.response).unwrap())
}
/// Serves a request.
///
/// This trait is mostly an implementation detail that isn't used outside of the registry
/// internals.
pub trait Serve: Clone + Send + 'static {
type Response: Future<Output = io::Result<ServiceResponse>> + Send + 'static;
fn serve(self, cx: context::Context, request: Bytes) -> Self::Response;
}
/// Serves a request if the request is for a registered service.
///
/// This trait is mostly an implementation detail that isn't used outside of the registry
/// internals.
pub trait MaybeServe: Send + 'static {
type Future: Future<Output = io::Result<ServiceResponse>> + Send + 'static;
fn serve(&self, cx: context::Context, request: &ServiceRequest) -> Option<Self::Future>;
}
/// A registry starting with service S, followed by Rest.
///
/// This type is mostly an implementation detail that is not used directly
/// outside of the registry internals.
pub struct Registration<S, Rest> {
/// The registered service's name. Must be unique across all registered services.
name: String,
/// The registered service.
serve: S,
/// Any remaining registered services.
rest: Rest,
}
/// An empty registry.
///
/// This type is mostly an implementation detail that is not used directly
/// outside of the registry internals.
pub struct Nil;
impl MaybeServe for Nil {
type Future = futures::future::Ready<io::Result<ServiceResponse>>;
fn serve(&self, _: context::Context, _: &ServiceRequest) -> Option<Self::Future> {
None
}
}
impl<S, Rest> MaybeServe for Registration<S, Rest>
where
S: Serve,
Rest: MaybeServe,
{
type Future = Either<S::Response, Rest::Future>;
fn serve(&self, cx: context::Context, request: &ServiceRequest) -> Option<Self::Future> {
if self.name == request.service_name {
Some(Either::Left(
self.serve.clone().serve(cx, request.request.clone()),
))
} else {
self.rest.serve(cx, request).map(Either::Right)
}
}
}
/// Wraps either of two future types that both resolve to the same output type.
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
pub enum Either<Left, Right> {
Left(Left),
Right(Right),
}
impl<Output, Left, Right> Future for Either<Left, Right>
where
Left: Future<Output = Output>,
Right: Future<Output = Output>,
{
type Output = Output;
fn poll(self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Output> {
unsafe {
match Pin::get_mut_unchecked(self) {
Either::Left(car) => Pin::new_unchecked(car).poll(waker),
Either::Right(cdr) => Pin::new_unchecked(cdr).poll(waker),
}
}
}
}
impl<Resp, F> Serve for F
where
F: FnOnce(context::Context, Bytes) -> Resp + Clone + Send + 'static,
Resp: Future<Output = io::Result<ServiceResponse>> + Send + 'static,
{
type Response = Resp;
fn serve(self, cx: context::Context, request: Bytes) -> Resp {
self(cx, request)
}
}
}
// Example
use bytes::Bytes;
use futures::{
future::{ready, Ready},
prelude::*,
};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
io,
sync::{Arc, RwLock},
};
use tarpc::{client, context, server::Handler};
fn deserialize<Req>(req: Bytes) -> io::Result<Req>
where
Req: for<'a> Deserialize<'a> + Send,
{
bincode::deserialize(req.as_ref()).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}
fn serialize<Resp>(resp: Resp) -> io::Result<Bytes>
where
Resp: Serialize,
{
Ok(bincode::serialize(&resp)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?
.into())
}
mod write_service {
tarpc::service! {
rpc write(key: String, value: String);
}
}
mod read_service {
tarpc::service! {
rpc read(key: String) -> Option<String>;
}
}
#[derive(Debug, Default, Clone)]
struct Server {
data: Arc<RwLock<HashMap<String, String>>>,
}
impl write_service::Service for Server {
type WriteFut = Ready<()>;
fn write(self, _: context::Context, key: String, value: String) -> Self::WriteFut {
self.data.write().unwrap().insert(key, value);
ready(())
}
}
impl read_service::Service for Server {
type ReadFut = Ready<Option<String>>;
fn read(self, _: context::Context, key: String) -> Self::ReadFut {
ready(self.data.read().unwrap().get(&key).cloned())
}
}
trait DefaultSpawn {
fn spawn(self);
}
impl<F> DefaultSpawn for F
where
F: Future<Output = ()> + Send + 'static,
{
fn spawn(self) {
tokio_executor::spawn(self.unit_error().boxed().compat())
}
}
struct BincodeRegistry<Services> {
registry: registry::Registry<Services>,
}
impl Default for BincodeRegistry<registry::Nil> {
fn default() -> Self {
BincodeRegistry {
registry: registry::Registry::default(),
}
}
}
impl<Services: registry::MaybeServe + Sync> BincodeRegistry<Services> {
fn serve(
self,
) -> impl FnOnce(
context::Context, registry::ServiceRequest
) -> registry::Either<
Services::Future,
Ready<io::Result<registry::ServiceResponse>>,
> + Clone {
self.registry.serve()
}
fn register<S, Req, Resp, RespFut>(
self,
name: String,
serve: S,
) -> BincodeRegistry<registry::Registration<impl registry::Serve + Send + 'static, Services>>
where
Req: for<'a> Deserialize<'a> + Send + 'static,
Resp: Serialize + 'static,
S: FnOnce(context::Context, Req) -> RespFut + Send + 'static + Clone,
RespFut: Future<Output = io::Result<Resp>> + Send + 'static,
{
let registry = self.registry.register(name, serve, deserialize, serialize);
BincodeRegistry { registry }
}
}
pub fn new_client<Req, Resp>(
service_name: String,
channel: &client::Channel<registry::ServiceRequest, registry::ServiceResponse>,
) -> client::MapResponse<
client::WithRequest<
client::Channel<registry::ServiceRequest, registry::ServiceResponse>,
impl FnMut(Req) -> registry::ServiceRequest,
>,
impl FnMut(registry::ServiceResponse) -> Resp,
>
where
Req: Serialize + Send + 'static,
Resp: for<'a> Deserialize<'a> + Send + 'static,
{
registry::new_client(service_name, channel, serialize, deserialize)
}
async fn run() -> io::Result<()> {
let server = Server::default();
let registry = BincodeRegistry::default()
.register(
"WriteService".to_string(),
write_service::serve(server.clone()),
)
.register(
"ReadService".to_string(),
read_service::serve(server.clone()),
);
let listener = bincode_transport::listen(&"0.0.0.0:0".parse().unwrap())?;
let server_addr = listener.local_addr();
let server = tarpc::Server::default()
.incoming(listener)
.take(1)
.respond_with(registry.serve());
tokio_executor::spawn(server.unit_error().boxed().compat());
let transport = await!(bincode_transport::connect(&server_addr))?;
let channel = await!(client::new(client::Config::default(), transport))?;
let write_client = new_client("WriteService".to_string(), &channel);
let mut write_client = write_service::Client::from(write_client);
let read_client = new_client("ReadService".to_string(), &channel);
let mut read_client = read_service::Client::from(read_client);
await!(write_client.write(context::current(), "key".to_string(), "val".to_string()))?;
let val = await!(read_client.read(context::current(), "key".to_string()))?;
println!("{:?}", val);
Ok(())
}
fn main() {
tarpc::init(futures::compat::TokioDefaultSpawner);
tokio::run(run().boxed().map_err(|e| panic!(e)).boxed().compat());
}

View File

@@ -1,2 +1 @@
ideal_width = 100
reorder_imports = true
edition = "2018"

View File

@@ -1,66 +1,169 @@
// Copyright 2016 Google Inc. All Rights Reserved.
// Copyright 2018 Google LLC
//
// 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.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! An RPC library for Rust.
//! ## tarpc: Tim & Adam's RPC lib
//! [![Travis-CI Status](https://travis-ci.org/google/tarpc.png?branch=master)](https://travis-ci.org/google/tarpc)
//! [![Coverage Status](https://coveralls.io/repos/github/google/tarpc/badge.svg?branch=master)](https://coveralls.io/github/google/tarpc?branch=master)
//! [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)
//! [![Latest Version](https://img.shields.io/crates/v/tarpc.svg)](https://crates.io/crates/tarpc)
//! [![Join the chat at https://gitter.im/tarpc/Lobby](https://badges.gitter.im/tarpc/Lobby.svg)](https://gitter.im/tarpc/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
//!
//! Example usage:
//! *Disclaimer*: This is not an official Google product.
//!
//! 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.
//!
//! [Documentation](https://docs.rs/crate/tarpc/)
//!
//! ## 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.
//!
//! ## Usage
//! Add to your `Cargo.toml` dependencies:
//!
//! ```toml
//! tarpc = "0.14.0"
//! ```
//! #[macro_use] extern crate tarpc;
//! mod my_server {
//! service! {
//! rpc hello(name: String) -> String;
//! rpc add(x: i32, y: i32) -> i32;
//!
//! The `service!` macro expands to a collection of items that form an
//! rpc service. In the above example, the macro is called within the
//! `hello_service` module. This module will contain a `Client` stub and `Service` trait. There is
//! These generated types make it easy and ergonomic to write servers without dealing with serialization
//! directly. Simply implement one of the generated traits, and you're off to the
//! races!
//!
//! ## 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},
//! };
//! 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))
//! }
//! }
//!
//! use self::my_server::*;
//! use std::time::Duration;
//! 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();
//!
//! struct Server;
//! impl my_server::Service for Server {
//! fn hello(&self, s: String) -> String {
//! format!("Hello, {}!", s)
//! }
//! fn add(&self, x: i32, y: i32) -> i32 {
//! x + y
//! }
//! // 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() {
//! let serve_handle = Server.spawn("localhost:0").unwrap();
//! let client = Client::new(serve_handle.dialer()).unwrap();
//! assert_eq!(3, client.add(1, 2).unwrap());
//! assert_eq!("Hello, Mom!".to_string(),
//! client.hello("Mom".to_string()).unwrap());
//! drop(client);
//! serve_handle.shutdown();
//! tarpc::init(TokioDefaultSpawner);
//! tokio::run(run()
//! .map_err(|e| eprintln!("Oh no: {}", e))
//! .boxed()
//! .compat(),
//! );
//! }
//! ```
//!
//! ## Service Documentation
//!
//! Use `cargo doc` as you normally would to see the documentation created for all
//! items expanded by a `service!` invocation.
#![deny(missing_docs)]
#![deny(missing_docs, missing_debug_implementations)]
#![feature(async_await)]
#![cfg_attr(
test,
feature(
pin,
futures_api,
await_macro,
proc_macro_hygiene,
arbitrary_self_types
)
)]
extern crate serde;
extern crate bincode;
#[macro_use]
extern crate log;
extern crate scoped_pool;
extern crate unix_socket;
macro_rules! pos {
() => (concat!(file!(), ":", line!()))
}
/// Provides the tarpc client and server, which implements the tarpc protocol.
/// The protocol is defined by the implementation.
pub mod protocol;
#[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.
pub mod macros;
/// Provides transport traits and implementations.
pub mod transport;
pub use protocol::{Config, Error, Result, ServeHandle};
#[macro_use]
mod macros;

View File

@@ -1,213 +1,28 @@
// Copyright 2016 Google Inc. All Rights Reserved.
// Copyright 2018 Google LLC
//
// 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.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
/// Serde re-exports required by macros. Not for general use.
pub mod serde {
pub use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Deserialization re-exports required by macros. Not for general use.
pub mod de {
pub use serde::de::{EnumVisitor, Error, VariantVisitor, Visitor};
#[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
}
}
// Required because if-let can't be used with irrefutable patterns, so it needs
// to be special cased.
#[cfg(not(feature = "serde"))]
#[doc(hidden)]
#[macro_export]
macro_rules! client_methods {
(
{ $(#[$attr:meta])* }
$fn_name:ident( ($($arg:ident,)*) : ($($in_:ty,)*) ) -> $out:ty
) => (
#[allow(unused)]
macro_rules! add_serde_if_enabled {
($(#[$attr:meta])* -- $i:item) => {
$(#[$attr])*
pub fn $fn_name(&self, $($arg: $in_),*) -> $crate::Result<$out> {
let reply = try!((self.0).rpc(__Request::$fn_name(($($arg,)*))));
let __Reply::$fn_name(reply) = reply;
::std::result::Result::Ok(reply)
}
);
($(
{ $(#[$attr:meta])* }
$fn_name:ident( ($( $arg:ident,)*) : ($($in_:ty, )*) ) -> $out:ty
)*) => ( $(
#[allow(unused)]
$(#[$attr])*
pub fn $fn_name(&self, $($arg: $in_),*) -> $crate::Result<$out> {
let reply = try!((self.0).rpc(__Request::$fn_name(($($arg,)*))));
if let __Reply::$fn_name(reply) = reply {
::std::result::Result::Ok(reply)
} else {
panic!("Incorrect reply variant returned from rpc; expected `{}`, \
but got {:?}",
stringify!($fn_name),
reply);
}
}
)*);
}
// Required because if-let can't be used with irrefutable patterns, so it needs
// to be special cased.
#[doc(hidden)]
#[macro_export]
macro_rules! async_client_methods {
(
{ $(#[$attr:meta])* }
$fn_name:ident( ($( $arg:ident, )*) : ($( $in_:ty, )*) ) -> $out:ty
) => (
#[allow(unused)]
$(#[$attr])*
pub fn $fn_name(&self, $($arg: $in_),*) -> Future<$out> {
fn mapper(reply: __Reply) -> $out {
let __Reply::$fn_name(reply) = reply;
reply
}
let reply = (self.0).rpc_async(__Request::$fn_name(($($arg,)*)));
Future {
future: reply,
mapper: mapper,
}
}
);
($(
{ $(#[$attr:meta])* }
$fn_name:ident( ($( $arg:ident, )*) : ($( $in_:ty, )*) ) -> $out:ty
)*) => ( $(
#[allow(unused)]
$(#[$attr])*
pub fn $fn_name(&self, $($arg: $in_),*) -> Future<$out> {
fn mapper(reply: __Reply) -> $out {
if let __Reply::$fn_name(reply) = reply {
reply
} else {
panic!("Incorrect reply variant returned from rpc; expected `{}`, but got \
{:?}",
stringify!($fn_name),
reply);
}
}
let reply = (self.0).rpc_async(__Request::$fn_name(($($arg,)*)));
Future {
future: reply,
mapper: mapper,
}
}
)*);
}
#[doc(hidden)]
#[macro_export]
macro_rules! impl_serialize {
($impler:ident, $(@($name:ident $n:expr))* -- #($_n:expr) ) => (
impl $crate::macros::serde::Serialize for $impler {
#[inline]
fn serialize<S>(&self, serializer: &mut S) -> ::std::result::Result<(), S::Error>
where S: $crate::macros::serde::Serializer
{
match *self {
$(
$impler::$name(ref field) =>
$crate::macros::serde::Serializer::serialize_newtype_variant(
serializer,
stringify!($impler),
$n,
stringify!($name),
field,
)
),*
}
}
}
);
// All args are wrapped in a tuple so we can use the newtype variant for each one.
($impler:ident, $(@$finished:tt)* -- #($n:expr) $name:ident($field:ty) $($req:tt)*) => (
impl_serialize!($impler, $(@$finished)* @($name $n) -- #($n + 1) $($req)*);
);
// Entry
($impler:ident, $($started:tt)*) => (impl_serialize!($impler, -- #(0) $($started)*););
}
#[doc(hidden)]
#[macro_export]
macro_rules! impl_deserialize {
($impler:ident, $(@($name:ident $n:expr))* -- #($_n:expr) ) => (
impl $crate::macros::serde::Deserialize for $impler {
#[inline]
fn deserialize<D>(deserializer: &mut D)
-> ::std::result::Result<$impler, D::Error>
where D: $crate::macros::serde::Deserializer
{
#[allow(non_camel_case_types, unused)]
enum __Field {
$($name),*
}
impl $crate::macros::serde::Deserialize for __Field {
#[inline]
fn deserialize<D>(deserializer: &mut D)
-> ::std::result::Result<__Field, D::Error>
where D: $crate::macros::serde::Deserializer
{
struct __FieldVisitor;
impl $crate::macros::serde::de::Visitor for __FieldVisitor {
type Value = __Field;
#[inline]
fn visit_usize<E>(&mut self, value: usize)
-> ::std::result::Result<__Field, E>
where E: $crate::macros::serde::de::Error,
{
$(
if value == $n {
return ::std::result::Result::Ok(__Field::$name);
}
)*
return ::std::result::Result::Err(
$crate::macros::serde::de::Error::custom(
format!("No variants have a value of {}!", value))
);
}
}
deserializer.deserialize_struct_field(__FieldVisitor)
}
}
struct __Visitor;
impl $crate::macros::serde::de::EnumVisitor for __Visitor {
type Value = $impler;
#[inline]
fn visit<__V>(&mut self, mut visitor: __V)
-> ::std::result::Result<$impler, __V::Error>
where __V: $crate::macros::serde::de::VariantVisitor
{
match try!(visitor.visit_variant()) {
$(
__Field::$name => {
let val = try!(visitor.visit_newtype());
Ok($impler::$name(val))
}
),*
}
}
}
const VARIANTS: &'static [&'static str] = &[
$(
stringify!($name)
),*
];
deserializer.deserialize_enum(stringify!($impler), VARIANTS, __Visitor)
}
}
);
// All args are wrapped in a tuple so we can use the newtype variant for each one.
($impler:ident, $(@$finished:tt)* -- #($n:expr) $name:ident($field:ty) $($req:tt)*) => (
impl_deserialize!($impler, $(@$finished)* @($name $n) -- #($n + 1) $($req)*);
);
// Entry
($impler:ident, $($started:tt)*) => (impl_deserialize!($impler, -- #(0) $($started)*););
$i
}
}
/// The main macro that creates RPC services.
@@ -215,41 +30,30 @@ macro_rules! impl_deserialize {
/// Rpc methods are specified, mirroring trait syntax:
///
/// ```
/// # #[macro_use] extern crate tarpc;
/// # #![feature(await_macro, pin, arbitrary_self_types, async_await, futures_api, proc_macro_hygiene)]
/// # fn main() {}
/// # service! {
/// #[doc="Say hello"]
/// # tarpc::service! {
/// /// Say hello
/// rpc hello(name: String) -> String;
/// # }
/// ```
///
/// There are two rpc names reserved for the default fns `spawn` and `spawn_with_config`.
///
/// Attributes can be attached to each rpc. These attributes
/// will then be attached to the generated `Service` trait's
/// corresponding method, as well as to the `Client` stub's rpcs methods.
/// 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:
///
/// * `Service` -- the trait defining the RPC service. It comes with two default methods for
/// starting the server:
/// 1. `spawn` starts the service in another thread using default configuration.
/// 2. `spawn_with_config` starts the service in another thread using the specified
/// `Config`.
/// * `Client` -- a client that makes synchronous requests to the RPC server
/// * `AsyncClient` -- a client that makes asynchronous requests to the RPC server
/// * `Future` -- a handle for asynchronously retrieving the result of an RPC
/// * `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.
///
/// **Warning**: In addition to the above items, there are a few expanded items that
/// are considered implementation details. As with the above items, shadowing
/// these item names in the enclosing module is likely to break things in confusing
/// ways:
///
/// * `__Server` -- an implementation detail
/// * `__Request` -- an implementation detail
/// * `__Reply` -- an implementation detail
#[macro_export]
macro_rules! service {
() => {
compile_error!("Must define at least one RPC method.");
};
// Entry point
(
$(
@@ -257,14 +61,14 @@ macro_rules! service {
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* ) $(-> $out:ty)*;
)*
) => {
service! {{
$crate::service! {{
$(
$(#[$attr])*
rpc $fn_name( $( $arg : $in_ ),* ) $(-> $out)*;
)*
}}
};
// Pattern for when the next rpc has an implicit unit return type
// Pattern for when the next rpc has an implicit unit return type.
(
{
$(#[$attr:meta])*
@@ -274,7 +78,7 @@ macro_rules! service {
}
$( $expanded:tt )*
) => {
service! {
$crate::service! {
{ $( $unexpanded )* }
$( $expanded )*
@@ -283,7 +87,7 @@ macro_rules! service {
rpc $fn_name( $( $arg : $in_ ),* ) -> ();
}
};
// Pattern for when the next rpc has an explicit return type
// Pattern for when the next rpc has an explicit return type.
(
{
$(#[$attr:meta])*
@@ -293,7 +97,7 @@ macro_rules! service {
}
$( $expanded:tt )*
) => {
service! {
$crate::service! {
{ $( $unexpanded )* }
$( $expanded )*
@@ -310,323 +114,264 @@ macro_rules! service {
rpc $fn_name:ident ( $( $arg:ident : $in_:ty ),* ) -> $out:ty;
)*
) => {
#[doc="Defines the RPC service"]
pub trait Service: Send + Sync + Sized {
$crate::add_serde_if_enabled! {
/// The request sent over the wire from the client to the server.
#[derive(Debug)]
#[allow(non_camel_case_types, unused)]
--
pub enum Request {
$(
$(#[$attr])*
$fn_name{ $($arg: $in_,)* }
),*
}
}
$crate::add_serde_if_enabled! {
/// The response sent over the wire from the server to the client.
#[derive(Debug)]
#[allow(non_camel_case_types, unused)]
--
pub enum Response {
$(
$(#[$attr])*
$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.
/// A future resolving to a server [`Response`].
#[allow(non_camel_case_types)]
pub enum ResponseFut<S: Service> {
$(
$(#[$attr])*
fn $fn_name(&self, $($arg:$in_),*) -> $out;
$fn_name($crate::ty_snake_to_camel!(<S as Service>::$fn_name)),
)*
}
#[doc="Spawn a running service."]
fn spawn<T>(self,
transport: T)
-> $crate::Result<
$crate::protocol::ServeHandle<
<T::Listener as $crate::transport::Listener>::Dialer>>
where T: $crate::transport::Transport,
Self: 'static,
{
self.spawn_with_config(transport, $crate::Config::default())
}
#[doc="Spawn a running service."]
fn spawn_with_config<T>(self,
transport: T,
config: $crate::Config)
-> $crate::Result<
$crate::protocol::ServeHandle<
<T::Listener as $crate::transport::Listener>::Dialer>>
where T: $crate::transport::Transport,
Self: 'static,
{
let server = __Server(self);
let result = $crate::protocol::Serve::spawn_with_config(server, transport, config);
let handle = try!(result);
::std::result::Result::Ok(handle)
impl<S: Service> ::std::fmt::Debug for ResponseFut<S> {
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
fmt.debug_struct("Response").finish()
}
}
impl<P, S> Service for P
where P: Send + Sync + Sized + 'static + ::std::ops::Deref<Target=S>,
S: Service
impl<S: Service> ::std::future::Future for ResponseFut<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) {
$(
ResponseFut::$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 FnOnce($crate::context::Context, Request) -> ResponseFut<S> + Send + 'static + Clone {
move |ctx, req| {
match req {
$(
Request::$fn_name{ $($arg,)* } => {
let resp = Service::$fn_name(service.clone(), ctx, $($arg),*);
ResponseFut::$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<C = $crate::client::Channel<Request, Response>>(C);
/// 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 + 'static,
{
Ok(Client(await!($crate::client::new(config, transport))?))
}
impl<C> From<C> for Client<C>
where for <'a> C: $crate::Client<'a, Request, Response = Response>
{
fn from(client: C) -> Self {
Client(client)
}
}
impl<C> Client<C>
where for<'a> C: $crate::Client<'a, Request, Response = Response>
{
$(
#[allow(unused)]
$(#[$attr])*
fn $fn_name(&self, $($arg:$in_),*) -> $out {
Service::$fn_name(&**self, $($arg),*)
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 = $crate::Client::call(&mut self.0, ctx, request__);
async move {
match await!(resp)? {
Response::$fn_name(msg__) => ::std::result::Result::Ok(msg__),
_ => unreachable!(),
}
}
}
)*
}
#[allow(non_camel_case_types, unused)]
#[derive(Debug)]
enum __Request {
$(
$fn_name(( $($in_,)* ))
),*
}
impl_serialize!(__Request, $($fn_name(($($in_),*)))*);
impl_deserialize!(__Request, $($fn_name(($($in_),*)))*);
#[allow(non_camel_case_types, unused)]
#[derive(Debug)]
enum __Reply {
$(
$fn_name($out),
)*
}
impl_serialize!(__Reply, $($fn_name($out))*);
impl_deserialize!(__Reply, $($fn_name($out))*);
#[allow(unused)]
#[doc="An asynchronous RPC call"]
pub struct Future<T> {
future: $crate::protocol::Future<__Reply>,
mapper: fn(__Reply) -> T,
}
impl<T> Future<T> {
#[allow(unused)]
#[doc="Block until the result of the RPC call is available"]
pub fn get(self) -> $crate::Result<T> {
self.future.get().map(self.mapper)
}
}
#[allow(unused)]
#[doc="The client stub that makes RPC calls to the server."]
pub struct Client<S = ::std::net::TcpStream>(
$crate::protocol::Client<__Request, __Reply, S>
) where S: $crate::transport::Stream;
impl<S> Client<S>
where S: $crate::transport::Stream
{
#[allow(unused)]
#[doc="Create a new client with default configuration that connects to the given \
address."]
pub fn new<D>(dialer: D) -> $crate::Result<Self>
where D: $crate::transport::Dialer<Stream=S>,
{
Self::with_config(dialer, $crate::Config::default())
}
#[allow(unused)]
#[doc="Create a new client with the specified configuration that connects to the \
given address."]
pub fn with_config<D>(dialer: D, config: $crate::Config) -> $crate::Result<Self>
where D: $crate::transport::Dialer<Stream=S>,
{
let inner = try!($crate::protocol::Client::with_config(dialer, config));
::std::result::Result::Ok(Client(inner))
}
client_methods!(
$(
{ $(#[$attr])* }
$fn_name(($($arg,)*) : ($($in_,)*)) -> $out
)*
);
#[allow(unused)]
#[doc="Attempt to clone the client object. This might fail if the underlying TcpStream \
clone fails."]
pub fn try_clone(&self) -> ::std::io::Result<Self> {
::std::result::Result::Ok(Client(try!(self.0.try_clone())))
}
}
#[allow(unused)]
#[doc="The client stub that makes asynchronous RPC calls to the server."]
pub struct AsyncClient<S = ::std::net::TcpStream>(
$crate::protocol::Client<__Request, __Reply, S>
) where S: $crate::transport::Stream;
impl<S> AsyncClient<S>
where S: $crate::transport::Stream {
#[allow(unused)]
#[doc="Create a new asynchronous client with default configuration that connects to \
the given address."]
pub fn new<D>(dialer: D) -> $crate::Result<Self>
where D: $crate::transport::Dialer<Stream=S>,
{
Self::with_config(dialer, $crate::Config::default())
}
#[allow(unused)]
#[doc="Create a new asynchronous client that connects to the given address."]
pub fn with_config<D>(dialer: D, config: $crate::Config) -> $crate::Result<Self>
where D: $crate::transport::Dialer<Stream=S>,
{
let inner = try!($crate::protocol::Client::with_config(dialer, config));
::std::result::Result::Ok(AsyncClient(inner))
}
async_client_methods!(
$(
{ $(#[$attr])* }
$fn_name(($($arg,)*): ($($in_,)*)) -> $out
)*
);
#[allow(unused)]
#[doc="Attempt to clone the client object. This might fail if the underlying TcpStream \
clone fails."]
pub fn try_clone(&self) -> ::std::io::Result<Self> {
::std::result::Result::Ok(AsyncClient(try!(self.0.try_clone())))
}
}
#[allow(unused)]
struct __Server<S>(S)
where S: 'static + Service;
impl<S> $crate::protocol::Serve for __Server<S>
where S: 'static + Service
{
type Request = __Request;
type Reply = __Reply;
fn serve(&self, request: __Request) -> __Reply {
match request {
$(
__Request::$fn_name(( $($arg,)* )) =>
__Reply::$fn_name((self.0).$fn_name($($arg),*)),
)*
}
}
}
}
}
// allow dead code; we're just testing that the macro expansion compiles
#[allow(dead_code)]
// because we're just testing that the macro expansion compiles
#[cfg(test)]
mod syntax_test {
// Tests a service definition with a fn that takes no args
mod qux {
service! {
rpc hello() -> String;
}
}
// Tests a service definition with an attribute.
mod bar {
service! {
#[doc="Hello bob"]
rpc baz(s: String) -> String;
}
}
// Tests a service with implicit return types.
mod no_return {
service! {
rpc ack();
rpc apply(foo: String) -> i32;
rpc bi_consume(bar: String, baz: u64);
rpc bi_fn(bar: String, baz: u64) -> String;
}
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 {
extern crate env_logger;
extern crate tempdir;
use transport::unix::UnixTransport;
use futures::{
compat::TokioDefaultSpawner,
future::{ready, Ready},
prelude::*,
};
use rpc::{client, context, server::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 {
fn add(&self, x: i32, y: i32) -> i32 {
x + y
type AddFut = Ready<i32>;
fn add(self, _: context::Context, x: i32, y: i32) -> Self::AddFut {
ready(x + y)
}
fn hey(&self, name: String) -> String {
format!("Hey, {}.", name)
type HeyFut = Ready<String>;
fn hey(self, _: context::Context, name: String) -> Self::HeyFut {
ready(format!("Hey, {}.", name))
}
}
#[test]
fn simple() {
let _ = env_logger::init();
let handle = Server.spawn("localhost:0").unwrap();
let client = Client::new(handle.dialer()).unwrap();
assert_eq!(3, client.add(1, 2).unwrap());
assert_eq!("Hey, Tim.", client.hey("Tim".into()).unwrap());
drop(client);
handle.shutdown();
}
fn sequential() {
let _ = env_logger::try_init();
rpc::init(TokioDefaultSpawner);
#[test]
fn simple_async() {
let _ = env_logger::init();
let handle = Server.spawn("localhost:0").unwrap();
let client = AsyncClient::new(handle.dialer()).unwrap();
assert_eq!(3, client.add(1, 2).get().unwrap());
assert_eq!("Hey, Adam.", client.hey("Adam".into()).get().unwrap());
drop(client);
handle.shutdown();
}
let test = async {
let (tx, rx) = channel::unbounded();
tokio_executor::spawn(
crate::Server::default()
.incoming(stream::once(ready(Ok(rx))))
.respond_with(serve(Server))
.unit_error()
.boxed()
.compat(),
);
#[test]
fn try_clone() {
let handle = Server.spawn("localhost:0").unwrap();
let client1 = Client::new(handle.dialer()).unwrap();
let client2 = client1.try_clone().unwrap();
assert_eq!(3, client1.add(1, 2).unwrap());
assert_eq!(3, client2.add(1, 2).unwrap());
}
#[test]
fn async_try_clone() {
let handle = Server.spawn("localhost:0").unwrap();
let client1 = AsyncClient::new(handle.dialer()).unwrap();
let client2 = client1.try_clone().unwrap();
assert_eq!(3, client1.add(1, 2).get().unwrap());
assert_eq!(3, client2.add(1, 2).get().unwrap());
}
#[test]
fn async_try_clone_unix() {
let temp_dir = tempdir::TempDir::new("tarpc").unwrap();
let temp_file = temp_dir.path()
.join("async_try_clone_unix.tmp");
let handle = Server.spawn(UnixTransport(temp_file)).unwrap();
let client1 = AsyncClient::new(handle.dialer()).unwrap();
let client2 = client1.try_clone().unwrap();
assert_eq!(3, client1.add(1, 2).get().unwrap());
assert_eq!(3, client2.add(1, 2).get().unwrap());
}
// Tests that a server can be wrapped in an Arc; no need to run, just compile
#[allow(dead_code)]
fn serve_arc_server() {
let _ = ::std::sync::Arc::new(Server).spawn("localhost:0");
}
// Tests that a tcp client can be created from &str
#[allow(dead_code)]
fn test_client_str() {
let _ = Client::new("localhost:0");
}
#[test]
fn serde() {
use bincode;
let _ = env_logger::init();
let request = __Request::add((1, 2));
let ser = bincode::serde::serialize(&request, bincode::SizeLimit::Infinite).unwrap();
let de = bincode::serde::deserialize(&ser).unwrap();
if let __Request::add((1, 2)) = de {
// success
} else {
panic!("Expected __Request::add, got {:?}", de);
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::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();
}
}

View File

@@ -1,269 +0,0 @@
// 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.
use serde;
use std::fmt;
use std::io::{self, BufReader, BufWriter};
use std::collections::HashMap;
use std::mem;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread;
use super::{Config, Deserialize, Error, Packet, Result, Serialize};
use transport::{Dialer, Stream};
/// A client stub that connects to a server to run rpcs.
pub struct Client<Request, Reply, S>
where Request: serde::ser::Serialize,
S: Stream
{
// The guard is in an option so it can be joined in the drop fn
reader_guard: Arc<Option<thread::JoinHandle<()>>>,
outbound: Sender<(Request, Sender<Result<Reply>>)>,
requests: Arc<Mutex<RpcFutures<Reply>>>,
shutdown: S,
}
impl<Request, Reply, S> Client<Request, Reply, S>
where Request: serde::ser::Serialize + Send + 'static,
Reply: serde::de::Deserialize + Send + 'static,
S: Stream
{
/// Create a new client that connects to `addr`. The client uses the given timeout
/// for both reads and writes.
pub fn new<D>(dialer: D) -> io::Result<Self>
where D: Dialer<Stream = S>
{
Self::with_config(dialer, Config::default())
}
/// Create a new client that connects to `addr`. The client uses the given timeout
/// for both reads and writes.
pub fn with_config<D>(dialer: D, config: Config) -> io::Result<Self>
where D: Dialer<Stream = S>
{
let stream = try!(dialer.dial());
try!(stream.set_read_timeout(config.timeout));
try!(stream.set_write_timeout(config.timeout));
let reader_stream = try!(stream.try_clone());
let writer_stream = try!(stream.try_clone());
let requests = Arc::new(Mutex::new(RpcFutures::new()));
let reader_requests = requests.clone();
let writer_requests = requests.clone();
let (tx, rx) = channel();
let reader_guard = thread::spawn(move || read(reader_requests, reader_stream));
thread::spawn(move || write(rx, writer_requests, writer_stream));
Ok(Client {
reader_guard: Arc::new(Some(reader_guard)),
outbound: tx,
requests: requests,
shutdown: stream,
})
}
/// Clones the Client so that it can be shared across threads.
pub fn try_clone(&self) -> io::Result<Self> {
Ok(Client {
reader_guard: self.reader_guard.clone(),
outbound: self.outbound.clone(),
requests: self.requests.clone(),
shutdown: try!(self.shutdown.try_clone()),
})
}
fn rpc_internal(&self, request: Request) -> Receiver<Result<Reply>>
where Request: serde::ser::Serialize + fmt::Debug + Send + 'static
{
let (tx, rx) = channel();
self.outbound.send((request, tx)).expect(pos!());
rx
}
/// Run the specified rpc method on the server this client is connected to
pub fn rpc(&self, request: Request) -> Result<Reply>
where Request: serde::ser::Serialize + fmt::Debug + Send + 'static
{
self.rpc_internal(request)
.recv()
.map_err(|_| self.requests.lock().expect(pos!()).get_error())
.and_then(|reply| reply)
}
/// Asynchronously run the specified rpc method on the server this client is connected to
pub fn rpc_async(&self, request: Request) -> Future<Reply>
where Request: serde::ser::Serialize + fmt::Debug + Send + 'static
{
Future {
rx: self.rpc_internal(request),
requests: self.requests.clone(),
}
}
}
impl<Request, Reply, S> Drop for Client<Request, Reply, S>
where Request: serde::ser::Serialize,
S: Stream
{
fn drop(&mut self) {
debug!("Dropping Client.");
if let Some(reader_guard) = Arc::get_mut(&mut self.reader_guard) {
debug!("Attempting to shut down writer and reader threads.");
if let Err(e) = self.shutdown.shutdown() {
warn!("Client: couldn't shutdown writer and reader threads: {:?}",
e);
} else {
// We only join if we know the TcpStream was shut down. Otherwise we might never
// finish.
debug!("Joining writer and reader.");
reader_guard.take()
.expect(pos!())
.join()
.expect(pos!());
debug!("Successfully joined writer and reader.");
}
}
}
}
/// An asynchronous RPC call
pub struct Future<T> {
rx: Receiver<Result<T>>,
requests: Arc<Mutex<RpcFutures<T>>>,
}
impl<T> Future<T> {
/// Block until the result of the RPC call is available
pub fn get(self) -> Result<T> {
let requests = self.requests;
self.rx
.recv()
.map_err(|_| requests.lock().expect(pos!()).get_error())
.and_then(|reply| reply)
}
}
struct RpcFutures<Reply>(Result<HashMap<u64, Sender<Result<Reply>>>>);
impl<Reply> RpcFutures<Reply> {
fn new() -> RpcFutures<Reply> {
RpcFutures(Ok(HashMap::new()))
}
fn insert_tx(&mut self, id: u64, tx: Sender<Result<Reply>>) -> Result<()> {
match self.0 {
Ok(ref mut requests) => {
requests.insert(id, tx);
Ok(())
}
Err(ref e) => Err(e.clone()),
}
}
fn remove_tx(&mut self, id: u64) -> Result<()> {
match self.0 {
Ok(ref mut requests) => {
requests.remove(&id);
Ok(())
}
Err(ref e) => Err(e.clone()),
}
}
fn complete_reply(&mut self, packet: Packet<Reply>) {
if let Some(tx) = self.0.as_mut().expect(pos!()).remove(&packet.rpc_id) {
if let Err(e) = tx.send(Ok(packet.message)) {
info!("Reader: could not complete reply: {:?}", e);
}
} else {
warn!("RpcFutures: expected sender for id {} but got None!",
packet.rpc_id);
}
}
fn set_error(&mut self, err: Error) {
let _ = mem::replace(&mut self.0, Err(err));
}
fn get_error(&self) -> Error {
self.0.as_ref().err().expect(pos!()).clone()
}
}
fn write<Request, Reply, S>(outbound: Receiver<(Request, Sender<Result<Reply>>)>,
requests: Arc<Mutex<RpcFutures<Reply>>>,
stream: S)
where Request: serde::Serialize,
Reply: serde::Deserialize,
S: Stream
{
let mut next_id = 0;
let mut stream = BufWriter::new(stream);
loop {
let (request, tx) = match outbound.recv() {
Err(e) => {
debug!("Writer: all senders have exited ({:?}). Returning.", e);
return;
}
Ok(request) => request,
};
if let Err(e) = requests.lock().expect(pos!()).insert_tx(next_id, tx.clone()) {
report_error(&tx, e);
// Once insert_tx returns Err, it will continue to do so. However, continue here so
// that any other clients who sent requests will also recv the Err.
continue;
}
let id = next_id;
next_id += 1;
let packet = Packet {
rpc_id: id,
message: request,
};
debug!("Writer: writing rpc, id={:?}", id);
if let Err(e) = stream.serialize(&packet) {
report_error(&tx, e.into());
// Typically we'd want to notify the client of any Err returned by remove_tx, but in
// this case the client already hit an Err, and doesn't need to know about this one, as
// well.
let _ = requests.lock().expect(pos!()).remove_tx(id);
continue;
}
}
fn report_error<Reply>(tx: &Sender<Result<Reply>>, e: Error)
where Reply: serde::Deserialize
{
// Clone the err so we can log it if sending fails
if let Err(e2) = tx.send(Err(e.clone())) {
debug!("Error encountered while trying to send an error. Initial error: {:?}; Send \
error: {:?}",
e,
e2);
}
}
}
fn read<Reply, S>(requests: Arc<Mutex<RpcFutures<Reply>>>, stream: S)
where Reply: serde::Deserialize,
S: Stream
{
let mut stream = BufReader::new(stream);
loop {
match stream.deserialize::<Packet<Reply>>() {
Ok(packet) => {
debug!("Client: received message, id={}", packet.rpc_id);
requests.lock().expect(pos!()).complete_reply(packet);
}
Err(err) => {
warn!("Client: reader thread encountered an unexpected error while parsing; \
returning now. Error: {:?}",
err);
requests.lock().expect(pos!()).set_error(err);
break;
}
}
}
}

View File

@@ -1,249 +0,0 @@
// 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.
use bincode::{self, SizeLimit};
use bincode::serde::{deserialize_from, serialize_into};
use serde;
use serde::de::value::Error::EndOfStream;
use std::io::{self, Read, Write};
use std::convert;
use std::sync::Arc;
use std::time::Duration;
mod client;
mod server;
mod packet;
pub use self::packet::Packet;
pub use self::client::{Client, Future};
pub use self::server::{Serve, ServeHandle};
/// Client errors that can occur during rpc calls
#[derive(Debug, Clone)]
pub enum Error {
/// An IO-related error
Io(Arc<io::Error>),
/// The server hung up.
ConnectionBroken,
}
impl convert::From<bincode::serde::SerializeError> for Error {
fn from(err: bincode::serde::SerializeError) -> Error {
match err {
bincode::serde::SerializeError::IoError(err) => Error::Io(Arc::new(err)),
err => panic!("Unexpected error during serialization: {:?}", err),
}
}
}
impl convert::From<bincode::serde::DeserializeError> for Error {
fn from(err: bincode::serde::DeserializeError) -> Error {
match err {
bincode::serde::DeserializeError::Serde(EndOfStream) => Error::ConnectionBroken,
bincode::serde::DeserializeError::IoError(err) => {
match err.kind() {
io::ErrorKind::ConnectionReset |
io::ErrorKind::UnexpectedEof => Error::ConnectionBroken,
_ => Error::Io(Arc::new(err)),
}
}
err => panic!("Unexpected error during deserialization: {:?}", err),
}
}
}
impl convert::From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(Arc::new(err))
}
}
/// Configuration for client and server.
#[derive(Debug, Default)]
pub struct Config {
/// Request/Response timeout between packet delivery.
pub timeout: Option<Duration>,
}
/// Return type of rpc calls: either the successful return value, or a client error.
pub type Result<T> = ::std::result::Result<T, Error>;
trait Deserialize: Read + Sized {
fn deserialize<T: serde::Deserialize>(&mut self) -> Result<T> {
deserialize_from(self, SizeLimit::Infinite).map_err(Error::from)
}
}
impl<R: Read> Deserialize for R {}
trait Serialize: Write + Sized {
fn serialize<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
try!(serialize_into(self, value, SizeLimit::Infinite));
try!(self.flush());
Ok(())
}
}
impl<W: Write> Serialize for W {}
#[cfg(test)]
mod test {
extern crate env_logger;
use super::{Client, Config, Serve};
use scoped_pool::Pool;
use std::net::TcpStream;
use std::sync::{Arc, Barrier, Mutex};
use std::thread;
use std::time::Duration;
fn test_timeout() -> Option<Duration> {
Some(Duration::from_secs(1))
}
struct Server {
counter: Mutex<u64>,
}
impl Serve for Server {
type Request = ();
type Reply = u64;
fn serve(&self, _: ()) -> u64 {
let mut counter = self.counter.lock().unwrap();
let reply = *counter;
*counter += 1;
reply
}
}
impl Server {
fn new() -> Server {
Server { counter: Mutex::new(0) }
}
fn count(&self) -> u64 {
*self.counter.lock().unwrap()
}
}
#[test]
fn handle() {
let _ = env_logger::init();
let server = Arc::new(Server::new());
let serve_handle = server.spawn("localhost:0").unwrap();
let client: Client<(), u64, TcpStream> = Client::new(serve_handle.dialer()).unwrap();
drop(client);
serve_handle.shutdown();
}
#[test]
fn simple() {
let _ = env_logger::init();
let server = Arc::new(Server::new());
let serve_handle = server.clone().spawn("localhost:0").unwrap();
// The explicit type is required so that it doesn't deserialize a u32 instead of u64
let client: Client<(), u64, _> = Client::new(serve_handle.dialer()).unwrap();
assert_eq!(0, client.rpc(()).unwrap());
assert_eq!(1, server.count());
assert_eq!(1, client.rpc(()).unwrap());
assert_eq!(2, server.count());
drop(client);
serve_handle.shutdown();
}
struct BarrierServer {
barrier: Barrier,
inner: Server,
}
impl Serve for BarrierServer {
type Request = ();
type Reply = u64;
fn serve(&self, request: ()) -> u64 {
self.barrier.wait();
self.inner.serve(request)
}
}
impl BarrierServer {
fn new(n: usize) -> BarrierServer {
BarrierServer {
barrier: Barrier::new(n),
inner: Server::new(),
}
}
fn count(&self) -> u64 {
self.inner.count()
}
}
#[test]
fn force_shutdown() {
let _ = env_logger::init();
let server = Arc::new(Server::new());
let serve_handle = server.spawn_with_config("localhost:0",
Config { timeout: Some(Duration::new(0, 10)) })
.unwrap();
let client: Client<(), u64, _> = Client::new(serve_handle.dialer()).unwrap();
let thread = thread::spawn(move || serve_handle.shutdown());
info!("force_shutdown:: rpc1: {:?}", client.rpc(()));
thread.join().unwrap();
}
#[test]
fn client_failed_rpc() {
let _ = env_logger::init();
let server = Arc::new(Server::new());
let serve_handle =
server.spawn_with_config("localhost:0", Config { timeout: test_timeout() })
.unwrap();
let client: Arc<Client<(), u64, _>> = Arc::new(Client::new(serve_handle.dialer()).unwrap());
client.rpc(()).unwrap();
serve_handle.shutdown();
match client.rpc(()) {
Err(super::Error::ConnectionBroken) => {} // success
otherwise => panic!("Expected Err(ConnectionBroken), got {:?}", otherwise),
}
let _ = client.rpc(()); // Test whether second failure hangs
}
#[test]
fn concurrent() {
let _ = env_logger::init();
let concurrency = 10;
let pool = Pool::new(concurrency);
let server = Arc::new(BarrierServer::new(concurrency));
let serve_handle = server.clone().spawn("localhost:0").unwrap();
let client: Client<(), u64, _> = Client::new(serve_handle.dialer()).unwrap();
pool.scoped(|scope| {
for _ in 0..concurrency {
let client = client.try_clone().unwrap();
scope.execute(move || {
client.rpc(()).unwrap();
});
}
});
assert_eq!(concurrency as u64, server.count());
drop(client);
serve_handle.shutdown();
}
#[test]
fn async() {
let _ = env_logger::init();
let server = Arc::new(Server::new());
let serve_handle = server.spawn("localhost:0").unwrap();
let client: Client<(), u64, _> = Client::new(serve_handle.dialer()).unwrap();
// Drop future immediately; does the reader channel panic when sending?
client.rpc_async(());
// If the reader panicked, this won't succeed
client.rpc_async(());
drop(client);
serve_handle.shutdown();
}
}

View File

@@ -1,78 +0,0 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::marker::PhantomData;
/// Packet shared between client and server.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Packet<T> {
/// Packet id to map response to request.
pub rpc_id: u64,
/// Packet payload.
pub message: T,
}
const PACKET: &'static str = "Packet";
const RPC_ID: &'static str = "rpc_id";
const MESSAGE: &'static str = "message";
impl<T: Serialize> Serialize for Packet<T> {
#[inline]
fn serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error>
where S: Serializer
{
let mut state = try!(serializer.serialize_struct(PACKET, 2));
try!(serializer.serialize_struct_elt(&mut state, RPC_ID, &self.rpc_id));
try!(serializer.serialize_struct_elt(&mut state, MESSAGE, &self.message));
serializer.serialize_struct_end(state)
}
}
impl<T: Deserialize> Deserialize for Packet<T> {
#[inline]
fn deserialize<D>(deserializer: &mut D) -> Result<Self, D::Error>
where D: Deserializer
{
const FIELDS: &'static [&'static str] = &[RPC_ID, MESSAGE];
deserializer.deserialize_struct(PACKET, FIELDS, Visitor(PhantomData))
}
}
struct Visitor<T>(PhantomData<T>);
impl<T: Deserialize> de::Visitor for Visitor<T> {
type Value = Packet<T>;
#[inline]
fn visit_seq<V>(&mut self, mut visitor: V) -> Result<Packet<T>, V::Error>
where V: de::SeqVisitor
{
let packet = Packet {
rpc_id: match try!(visitor.visit()) {
Some(rpc_id) => rpc_id,
None => return Err(de::Error::end_of_stream()),
},
message: match try!(visitor.visit()) {
Some(message) => message,
None => return Err(de::Error::end_of_stream()),
},
};
try!(visitor.end());
Ok(packet)
}
}
#[cfg(test)]
extern crate env_logger;
#[test]
fn serde() {
use bincode;
let _ = env_logger::init();
let packet = Packet {
rpc_id: 1,
message: (),
};
let ser = bincode::serde::serialize(&packet, bincode::SizeLimit::Infinite).unwrap();
let de = bincode::serde::deserialize(&ser);
assert_eq!(packet, de.unwrap());
}

View File

@@ -1,280 +0,0 @@
// 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.
use serde;
use scoped_pool::{Pool, Scope};
use std::fmt;
use std::io::{self, BufReader, BufWriter};
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use std::thread::{self, JoinHandle};
use super::{Config, Deserialize, Error, Packet, Result, Serialize};
use transport::{Dialer, Listener, Stream, Transport};
use transport::tcp::TcpDialer;
struct ConnectionHandler<'a, S, St>
where S: Serve,
St: Stream
{
read_stream: BufReader<St>,
write_stream: BufWriter<St>,
server: S,
shutdown: &'a AtomicBool,
}
impl<'a, S, St> ConnectionHandler<'a, S, St>
where S: Serve,
St: Stream
{
fn handle_conn<'b>(&'b mut self, scope: &Scope<'b>) -> Result<()> {
let ConnectionHandler {
ref mut read_stream,
ref mut write_stream,
ref server,
shutdown,
} = *self;
trace!("ConnectionHandler: serving client...");
let (tx, rx) = channel();
scope.execute(move || Self::write(rx, write_stream));
loop {
match read_stream.deserialize() {
Ok(Packet { rpc_id, message }) => {
let tx = tx.clone();
scope.execute(move || {
let reply = server.serve(message);
let reply_packet = Packet {
rpc_id: rpc_id,
message: reply,
};
tx.send(reply_packet).expect(pos!());
});
if shutdown.load(Ordering::SeqCst) {
info!("ConnectionHandler: server shutdown, so closing connection.");
break;
}
}
Err(Error::Io(ref err)) if Self::timed_out(err.kind()) => {
if !shutdown.load(Ordering::SeqCst) {
info!("ConnectionHandler: read timed out ({:?}). Server not shutdown, so \
retrying read.",
err);
continue;
} else {
info!("ConnectionHandler: read timed out ({:?}). Server shutdown, so \
closing connection.",
err);
break;
}
}
Err(e) => {
warn!("ConnectionHandler: closing client connection due to {:?}",
e);
return Err(e.into());
}
}
}
Ok(())
}
fn timed_out(error_kind: io::ErrorKind) -> bool {
match error_kind {
io::ErrorKind::TimedOut |
io::ErrorKind::WouldBlock => true,
_ => false,
}
}
fn write(rx: Receiver<Packet<<S as Serve>::Reply>>, stream: &mut BufWriter<St>) {
loop {
match rx.recv() {
Err(e) => {
debug!("Write thread: returning due to {:?}", e);
return;
}
Ok(reply_packet) => {
if let Err(e) = stream.serialize(&reply_packet) {
warn!("Writer: failed to write reply to Client: {:?}", e);
}
}
}
}
}
}
/// Provides methods for blocking until the server completes,
pub struct ServeHandle<D = TcpDialer>
where D: Dialer
{
tx: Sender<()>,
join_handle: JoinHandle<()>,
dialer: D,
}
impl<D> ServeHandle<D>
where D: Dialer
{
/// Block until the server completes
pub fn wait(self) {
self.join_handle.join().expect(pos!());
}
/// Returns the dialer to the server.
pub fn dialer(&self) -> &D {
&self.dialer
}
/// Shutdown the server. Gracefully shuts down the serve thread but currently does not
/// gracefully close open connections.
pub fn shutdown(self) {
info!("ServeHandle: attempting to shut down the server.");
self.tx.send(()).expect(pos!());
if let Ok(_) = self.dialer.dial() {
self.join_handle.join().expect(pos!());
} else {
warn!("ServeHandle: best effort shutdown of serve thread failed");
}
}
}
struct Server<'a, S: 'a, L>
where L: Listener
{
server: &'a S,
listener: L,
read_timeout: Option<Duration>,
die_rx: Receiver<()>,
shutdown: &'a AtomicBool,
}
impl<'a, S, L> Server<'a, S, L>
where S: Serve + 'static,
L: Listener
{
fn serve<'b>(self, scope: &Scope<'b>)
where 'a: 'b
{
for conn in self.listener.incoming() {
match self.die_rx.try_recv() {
Ok(_) => {
info!("serve: shutdown received.");
return;
}
Err(TryRecvError::Disconnected) => {
info!("serve: shutdown sender disconnected.");
return;
}
_ => (),
}
let conn = match conn {
Err(err) => {
error!("serve: failed to accept connection: {:?}", err);
return;
}
Ok(c) => c,
};
if let Err(err) = conn.set_read_timeout(self.read_timeout) {
info!("serve: could not set read timeout: {:?}", err);
continue;
}
let read_conn = match conn.try_clone() {
Err(err) => {
error!("serve: could not clone tcp stream; possibly out of file descriptors? \
Err: {:?}",
err);
continue;
}
Ok(conn) => conn,
};
let mut handler = ConnectionHandler {
read_stream: BufReader::new(read_conn),
write_stream: BufWriter::new(conn),
server: self.server,
shutdown: self.shutdown,
};
scope.recurse(move |scope| {
scope.zoom(|scope| {
if let Err(err) = handler.handle_conn(scope) {
info!("ConnectionHandler: err in connection handling: {:?}", err);
}
});
});
}
}
}
impl<'a, S, L> Drop for Server<'a, S, L>
where L: Listener
{
fn drop(&mut self) {
debug!("Shutting down connection handlers.");
self.shutdown.store(true, Ordering::SeqCst);
}
}
/// A service provided by a server
pub trait Serve: Send + Sync + Sized {
/// The type of request received by the server
type Request: 'static + fmt::Debug + serde::ser::Serialize + serde::de::Deserialize + Send;
/// The type of reply sent by the server
type Reply: 'static + fmt::Debug + serde::ser::Serialize + serde::de::Deserialize + Send;
/// Return a reply for a given request
fn serve(&self, request: Self::Request) -> Self::Reply;
/// spawn
fn spawn<T>(self, transport: T) -> io::Result<ServeHandle<<T::Listener as Listener>::Dialer>>
where T: Transport,
Self: 'static
{
self.spawn_with_config(transport, Config::default())
}
/// spawn
fn spawn_with_config<T>(self,
transport: T,
config: Config)
-> io::Result<ServeHandle<<T::Listener as Listener>::Dialer>>
where T: Transport,
Self: 'static
{
let listener = try!(transport.bind());
let dialer = try!(listener.dialer());
info!("spawn_with_config: spinning up server.");
let (die_tx, die_rx) = channel();
let timeout = config.timeout;
let join_handle = thread::spawn(move || {
let pool = Pool::new(100); // TODO(tjk): make this configurable, and expire idle threads
let shutdown = AtomicBool::new(false);
let server = Server {
server: &self,
listener: listener,
read_timeout: timeout,
die_rx: die_rx,
shutdown: &shutdown,
};
pool.scoped(|scope| {
server.serve(scope);
});
});
Ok(ServeHandle {
tx: die_tx,
join_handle: join_handle,
dialer: dialer,
})
}
}
impl<P, S> Serve for P
where P: Send + Sync + ::std::ops::Deref<Target = S>,
S: Serve
{
type Request = S::Request;
type Reply = S::Reply;
fn serve(&self, request: S::Request) -> S::Reply {
S::serve(self, request)
}
}

View File

@@ -1,91 +0,0 @@
use std::io::{self, Read, Write};
use std::time::Duration;
/// A factory for creating a listener on a given address.
/// For TCP, an address might be an IPv4 address; for Unix sockets, it
/// is just a file name.
pub trait Transport {
/// The type of listener that binds to the given address.
type Listener: Listener;
/// Return a listener on the given address, and a dialer to that address.
fn bind(&self) -> io::Result<Self::Listener>;
}
/// Accepts incoming connections from dialers.
pub trait Listener: Send + 'static {
/// The type of address being listened on.
type Dialer: Dialer;
/// The type of stream this listener accepts.
type Stream: Stream;
/// Accept an incoming stream.
fn accept(&self) -> io::Result<Self::Stream>;
/// Returns the local address being listened on.
fn dialer(&self) -> io::Result<Self::Dialer>;
/// Iterate over incoming connections.
fn incoming(&self) -> Incoming<Self> {
Incoming { listener: self }
}
}
/// A cloneable Reader/Writer.
pub trait Stream: Read + Write + Send + Sized + 'static {
/// Creates a new independently owned handle to the Stream.
///
/// The returned Stream should reference the same stream that this
/// object references. Both handles should read and write the same
/// stream of data, and options set on one stream should be propagated
/// to the other stream.
fn try_clone(&self) -> io::Result<Self>;
/// Sets a read timeout.
///
/// If the value specified is `None`, then read calls will block indefinitely.
/// It is an error to pass the zero `Duration` to this method.
fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()>;
/// Sets a write timeout.
///
/// If the value specified is `None`, then write calls will block indefinitely.
/// It is an error to pass the zero `Duration` to this method.
fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()>;
/// Shuts down both ends of the stream.
///
/// Implementations should cause all pending and future I/O on the specified
/// portions to return immediately with an appropriate value.
fn shutdown(&self) -> io::Result<()>;
}
/// A `Stream` factory.
pub trait Dialer {
/// The type of `Stream` this can create.
type Stream: Stream;
/// Open a stream.
fn dial(&self) -> io::Result<Self::Stream>;
}
impl<P, D: ?Sized> Dialer for P
where P: ::std::ops::Deref<Target = D>,
D: Dialer + 'static
{
type Stream = D::Stream;
fn dial(&self) -> io::Result<Self::Stream> {
(**self).dial()
}
}
/// Iterates over incoming connections.
pub struct Incoming<'a, L: Listener + ?Sized + 'a> {
listener: &'a L,
}
impl<'a, L: Listener> Iterator for Incoming<'a, L> {
type Item = io::Result<L::Stream>;
fn next(&mut self) -> Option<Self::Item> {
Some(self.listener.accept())
}
}
/// Provides a TCP transport.
pub mod tcp;
/// Provides a unix socket transport.
pub mod unix;

View File

@@ -1,77 +0,0 @@
use std::io;
use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
use std::time::Duration;
/// A transport for TCP.
#[derive(Debug)]
pub struct TcpTransport<A: ToSocketAddrs>(pub A);
impl<A: ToSocketAddrs> super::Transport for TcpTransport<A> {
type Listener = TcpListener;
fn bind(&self) -> io::Result<TcpListener> {
TcpListener::bind(&self.0)
}
}
impl<A: ToSocketAddrs> super::Transport for A {
type Listener = TcpListener;
fn bind(&self) -> io::Result<TcpListener> {
TcpListener::bind(self)
}
}
impl super::Listener for TcpListener {
type Dialer = TcpDialer<SocketAddr>;
type Stream = TcpStream;
fn accept(&self) -> io::Result<TcpStream> {
self.accept().map(|(stream, _)| stream)
}
fn dialer(&self) -> io::Result<TcpDialer<SocketAddr>> {
self.local_addr().map(|addr| TcpDialer(addr))
}
}
impl super::Stream for TcpStream {
fn try_clone(&self) -> io::Result<Self> {
self.try_clone()
}
fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
self.set_read_timeout(dur)
}
fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
self.set_write_timeout(dur)
}
fn shutdown(&self) -> io::Result<()> {
self.shutdown(::std::net::Shutdown::Both)
}
}
/// Connects to a socket address.
#[derive(Debug)]
pub struct TcpDialer<A = SocketAddr>(pub A) where A: ToSocketAddrs;
impl<A> super::Dialer for TcpDialer<A>
where A: ToSocketAddrs
{
type Stream = TcpStream;
fn dial(&self) -> io::Result<TcpStream> {
TcpStream::connect(&self.0)
}
}
impl super::Dialer for str {
type Stream = TcpStream;
fn dial(&self) -> io::Result<TcpStream> {
TcpStream::connect(self)
}
}

View File

@@ -1,72 +0,0 @@
use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;
use unix_socket::{UnixListener, UnixStream};
/// A transport for unix sockets.
#[derive(Debug)]
pub struct UnixTransport<P>(pub P) where P: AsRef<Path>;
impl<P> super::Transport for UnixTransport<P>
where P: AsRef<Path>
{
type Listener = UnixListener;
fn bind(&self) -> io::Result<UnixListener> {
UnixListener::bind(&self.0)
}
}
/// Connects to a unix socket address.
#[derive(Debug)]
pub struct UnixDialer<P>(pub P) where P: AsRef<Path>;
impl<P> super::Dialer for UnixDialer<P>
where P: AsRef<Path>
{
type Stream = UnixStream;
fn dial(&self) -> io::Result<UnixStream> {
UnixStream::connect(&self.0)
}
}
impl super::Listener for UnixListener {
type Stream = UnixStream;
type Dialer = UnixDialer<PathBuf>;
fn accept(&self) -> io::Result<UnixStream> {
self.accept().map(|(stream, _)| stream)
}
fn dialer(&self) -> io::Result<UnixDialer<PathBuf>> {
self.local_addr().and_then(|addr| {
match addr.as_pathname() {
Some(path) => Ok(UnixDialer(path.to_owned())),
None => {
Err(io::Error::new(io::ErrorKind::AddrNotAvailable,
"Couldn't get a path to bound unix socket"))
}
}
})
}
}
impl super::Stream for UnixStream {
fn try_clone(&self) -> io::Result<Self> {
self.try_clone()
}
fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
self.set_read_timeout(timeout)
}
fn set_write_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
self.set_write_timeout(timeout)
}
fn shutdown(&self) -> io::Result<()> {
self.shutdown(::std::net::Shutdown::Both)
}
}

126
tarpc/tests/latency.rs Normal file
View File

@@ -0,0 +1,126 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#![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::{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::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())
}

View File

@@ -1,9 +0,0 @@
[package]
name = "tarpc_examples"
version = "0.1.0"
authors = ["Adam Wright <adam.austin.wright@gmail.com>", "Tim Kuehn <timothy.j.kuehn@gmail.com>"]
[dev-dependencies]
tarpc = { path = "../tarpc" }
lazy_static = "0.2"
env_logger = "0.3"

View File

@@ -1,74 +0,0 @@
// 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_attr(test, feature(test))]
#[cfg(test)]
#[macro_use]
extern crate lazy_static;
#[cfg(test)]
#[macro_use]
extern crate tarpc;
#[cfg(test)]
#[allow(dead_code)] // generated Client isn't used in this benchmark
mod benchmark {
extern crate env_logger;
extern crate test;
use tarpc::ServeHandle;
use self::test::Bencher;
use std::sync::{Arc, Mutex};
service! {
rpc hello(s: String) -> String;
}
struct HelloServer;
impl Service for HelloServer {
fn hello(&self, s: String) -> String {
format!("Hello, {}!", s)
}
}
// Prevents resource exhaustion when benching
lazy_static! {
static ref HANDLE: Arc<Mutex<ServeHandle>> = {
let handle = HelloServer.spawn("localhost:0").unwrap();
Arc::new(Mutex::new(handle))
};
static ref CLIENT: Arc<Mutex<AsyncClient>> = {
let lock = HANDLE.lock().unwrap();
let dialer = lock.dialer();
let client = AsyncClient::new(dialer).unwrap();
Arc::new(Mutex::new(client))
};
}
#[bench]
fn hello(bencher: &mut Bencher) {
let _ = env_logger::init();
let client = CLIENT.lock().unwrap();
let concurrency = 100;
let mut futures = Vec::with_capacity(concurrency);
let mut count = 0;
bencher.iter(|| {
futures.push(client.hello("Bob".into()));
count += 1;
if count % concurrency == 0 {
// We can't block on each rpc call, otherwise we'd be
// benchmarking latency instead of throughput. It's also
// not ideal to call more than one rpc per iteration, because
// it makes the output of the bencher harder to parse (you have
// to mentally divide the number by `concurrency` to get
// the ns / iter for one rpc
for f in futures.drain(..) {
f.get().unwrap();
}
}
});
}
}

21
trace/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "tarpc-trace"
version = "0.1.0"
authors = ["tikue <tikue@google.com>"]
edition = '2018'
license = "MIT"
documentation = "https://docs.rs/tarpc-trace"
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 = "foundations for tracing in tarpc"
[dependencies]
rand = "0.5"
[dependencies.serde]
version = "1.0"
optional = true
features = ["derive"]

1
trace/rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
edition = "2018"

106
trace/src/lib.rs Normal file
View File

@@ -0,0 +1,106 @@
// Copyright 2018 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#![deny(missing_docs, missing_debug_implementations)]
//! Provides building blocks for tracing distributed programs.
//!
//! A trace is logically a tree of causally-related events called spans. Traces are tracked via a
//! [context](Context) that identifies the current trace, span, and parent of the current span. In
//! distributed systems, a context can be sent from client to server to connect events occurring on
//! either side.
//!
//! This crate's design is based on [opencensus
//! tracing](https://opencensus.io/core-concepts/tracing/).
use rand::Rng;
use std::{
fmt::{self, Formatter},
mem,
};
/// A context for tracing the execution of processes, distributed or otherwise.
///
/// Consists of a span identifying an event, an optional parent span identifying a causal event
/// that triggered the current span, and a trace with which all related spans are associated.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct Context {
/// An identifier of the trace associated with the current context. A trace ID is typically
/// created at a root span and passed along through all causal events.
pub trace_id: TraceId,
/// An identifier of the current span. In typical RPC usage, a span is created by a client
/// before making an RPC, and the span ID is sent to the server. The server is free to create
/// its own spans, for which it sets the client's span as the parent span.
pub span_id: SpanId,
/// An identifier of the span that originated the current span. For example, if a server sends
/// an RPC in response to a client request that included a span, the server would create a span
/// for the RPC and set its parent to the span_id in the incoming request's context.
///
/// If `parent_id` is `None`, then this is a root context.
pub parent_id: Option<SpanId>,
}
/// A 128-bit UUID identifying a trace. All spans caused by the same originating span share the
/// same trace ID.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct TraceId(u128);
/// A 64-bit identifier of a span within a trace. The identifier is unique within the span's trace.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct SpanId(u64);
impl Context {
/// Constructs a new root context. A root context is one with no parent span.
pub fn new_root() -> Self {
let rng = &mut rand::thread_rng();
Context {
trace_id: TraceId::random(rng),
span_id: SpanId::random(rng),
parent_id: None,
}
}
}
impl TraceId {
/// Returns a random trace ID that can be assumed to be globally unique if `rng` generates
/// actually-random numbers.
pub fn random<R: Rng>(rng: &mut R) -> Self {
TraceId((rng.next_u64() as u128) << mem::size_of::<u64>() | rng.next_u64() as u128)
}
}
impl SpanId {
/// Returns a random span ID that can be assumed to be unique within a single trace.
pub fn random<R: Rng>(rng: &mut R) -> Self {
SpanId(rng.next_u64())
}
}
impl fmt::Display for TraceId {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
write!(f, "{:02x}", self.0)?;
Ok(())
}
}
impl fmt::Display for SpanId {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
write!(f, "{:02x}", self.0)?;
Ok(())
}
}