409 Commits

Author SHA1 Message Date
Tim Kuehn
d27f341bde Prepare release of v0.22.0 2020-08-19 18:35:36 -07:00
Tim Kuehn
2264ebecfc Remove serde_transport::tcp::connect_with.
Instead, serde_transport::tcp::connect returns a future named Connect
that has methods to directly access the framing config. This is
consistent with how serde_transport::tcp::listen returns a future with
methods to access the framing config. In addition to this consistency,
it reduces the API surface and provides a simpler user transition from
"zero config" to "some config".
2020-08-19 17:51:53 -07:00
Tim Kuehn
3207affb4a Update pre-commit for changes to cargo fmt.
--write-mode is now --check.
2020-08-19 17:51:20 -07:00
Andre B. Reis
0602afd50c Make connect() and connect_with() take a FnOnce for the codec (#315) 2020-08-19 16:15:26 -07:00
Tim Kuehn
4343e12217 Fix incorrect documentation 2020-08-18 02:58:11 -07:00
Tim Kuehn
7fda862fb8 Run cargo fmt 2020-08-18 02:55:24 -07:00
Tim Kuehn
aa7b875b1a Expose framing config in serde_transport. 2020-08-18 02:47:41 -07:00
Tim Kuehn
54d6e0e3b6 Add license headers 2020-08-04 17:33:41 -07:00
Tim Kuehn
bea3b442aa Move mod.rs files up one directory.
It's easier in IDEs if the files aren't all named the same.
2020-08-04 17:25:53 -07:00
Tim Kuehn
954a2502e7 Remove duplicate rustdoc 2020-08-02 22:24:09 -07:00
Tim Kuehn
e3f34917c5 Prepare v0.21.1 2020-08-02 21:34:13 -07:00
Tim Kuehn
f65dd05949 Enable documentation for optional features on docs.rs 2020-08-02 20:57:21 -07:00
Tim Kuehn
240c436b34 Ensure Context is Sync. 2020-08-01 14:01:07 -07:00
Tim Kuehn
c9803688cc Ensure Context is Send. 2020-08-01 13:49:25 -07:00
Tim Kuehn
4987094483 Compression example.
Follow-up work: some extension points would be useful allow enabling compression on a per-request basis.

Fixes https://github.com/google/tarpc/issues/200
2020-08-01 13:45:16 -07:00
Tim Kuehn
ff55080193 Minor refactor 2020-07-30 13:11:13 -07:00
Tim Kuehn
258193c932 PubSub example needs to populate the subscription topics. 2020-07-30 11:14:13 -07:00
Tim Kuehn
67823ef5de Get rid of sleeps in PubSub example. 2020-07-30 01:27:31 -07:00
Tim Kuehn
a671457243 Add topics to PubSub example 2020-07-29 22:51:04 -07:00
Tim Kuehn
cf654549da Add documentation to PubSub example. 2020-07-29 18:05:35 -07:00
Tim Kuehn
6a01e32a2d Shut down client dispatch immediately when read half of transport is closed.
Clients can't receive any responses when the read half is closed, which means they can't verify if their requests were served. Therefore, there is no point in writing further requests after the read half is closed.
2020-07-29 13:50:42 -07:00
Tim Kuehn
e6597fab03 Add some error context to client dispatch.
I'm taking this opportunity to experiment with anyhow. So far, results are promising. It was a bit hard to use with Poll<Option<Result<T, E>>> types, so I added a crate-internal helper trait for that.
2020-07-29 12:07:07 -07:00
Tim Kuehn
ebd245a93d Rewrite pubsub example to have the subscriber connect to the publisher.
Fixes https://github.com/google/tarpc/issues/313
2020-07-28 22:10:17 -07:00
Tim Kuehn
3ebc3b5845 Add accessor fns.
- ClientHandler::get_pin_channel
- BaseChannel::get_pin_ref
- serde_transport::Transport::get_ref
2020-07-28 21:27:36 -07:00
Tim Kuehn
0e5973109d Make docs.rs document feature-gated public items. 2020-07-28 19:43:43 -07:00
Tim Kuehn
5f02d7383a Add tests for correct diagnostic output from proc macro-generated compiler errors. 2020-07-27 01:17:06 -07:00
Tim Kuehn
2bae148529 Address clippy lints 2020-07-27 00:04:45 -07:00
Tim Kuehn
42a2e03aab Add better diagnostics for missing 'async' in impls using #[tarpc::server] 2020-07-26 23:47:48 -07:00
Tim Kuehn
b566d0c646 Use #[tarpc::server] in example-service 2020-07-26 18:26:41 -07:00
Jon Cinque
b359f16767 Add concurrent tests using join and join_all
These tests are essentially copies of the `concurrent` test,
specifically using `join` and `join_all`.  Note that for the `join_all`
example to work, all of the `Client` clones must be created before *any*
requests are added, otherwise there will be a lifetime problem with the
second request, saying that second client, `c2`, is still borrowed when
`req1` is dropped.  It would require a larger redesign to fix this
issue.
2020-07-24 09:51:05 -07:00
Greg Fitzgerald
f8681ab134 Migrate examples to tarpc::server 2020-07-22 14:03:23 -07:00
Tim Kuehn
7e521768ab Prepare for v0.21.0 release. 2020-06-26 20:05:02 -07:00
Tim Kuehn
e9b1e7d101 Use #[non_exhaustive] in lieu of _NonExhaustive enum variant. 2020-06-26 19:47:20 -07:00
Taiki Endo
f0322fb892 Remove uses of pin_project::project attribute
pin-project will deprecate the project attribute due to some unfixable
limitations.

Refs: https://github.com/taiki-e/pin-project/issues/225
2020-06-05 20:34:44 -07:00
Patrick Elsen
617daebb88 Add tarpc::server proc-macro as syntactic sugar for async methods. (#302)
The tarpc::server proc-macro can be used to annotate implementations of
services to turn async functions into the proper declarations needed
for tarpc to be able to call them.

This uses the assert_type_eq crate to check that the transformations
applied by the tarpc::server proc macro are correct and lead to code
that compiles.
2020-05-16 10:25:25 -07:00
Tim Kuehn
a11d4fff58 Remove raii_counter 2020-04-22 02:13:02 -07:00
Tim
bf42a04d83 Move the request timeout so that it surrounds the entire call, not just the response future. (#295)
* Move the request timeout so that it surrounds the entire call, not just the response future.

This will enable the timeout earlier, so that a backlog in the outbound request buffer can not cause requests to stall indefinitely.

* Run cargo fmt
2020-02-25 14:42:40 -08:00
Tim Kuehn
06528d6953 Fix clippy lint. 2019-12-19 12:28:26 -08:00
Tim Kuehn
9f00395746 Replace _non_exhaustive fields with #[non_exhaustive] attribute.
The attribute landed on stable rust (1.40.0) today.

Fixes https://github.com/google/tarpc/issues/275
2019-12-19 12:14:34 -08:00
Tim Kuehn
e0674cd57f Make pre-push run on rust stable. 2019-12-19 12:06:06 -08:00
Tim Kuehn
7e49bd9ee7 Clean up badges a bit. 2019-12-16 13:21:00 -08:00
Tim Kuehn
8a1baa9c4e Remove usage of unsafe in rpc::client::channel.
pin_project is actually able to handle the complexities of enum Futures.
2019-12-16 11:10:57 -08:00
Oleg Nosov
31c713d188 Allow raw identifiers + fixed naming + place all code generation methods in impl (#291)
Allows defining services using raw identifiers like:

```rust
pub mod service {
    #[tarpc::service]
    pub trait r#trait {
        async fn r#fn(x: i32) -> Result<u8, String>;
    }
}
```

Also:

- Refactored names (ident -> type)
- All code generation methods placed in impl
2019-12-12 10:13:57 -08:00
Tim Kuehn
d905bc1591 Prepare for tarpc release v0.20.0 2019-12-11 20:47:56 -08:00
Tim Kuehn
7f946c7f83 Make tokio a hard dependency.
Fixes #289
2019-12-11 20:08:36 -08:00
Tim Kuehn
36cfdb6c6f Fix tokio dependency for example-service 2019-12-11 20:01:06 -08:00
Tim Kuehn
dbabe9774f Clean up proc macro code to make clippy happy.
I made a separate TokenStream-returning fn for each item in the previously-huge quote block.
The service fn now primarily performs the duty of creating idents and joining all the TokenStreams.
2019-12-11 17:20:03 -08:00
Tim Kuehn
deb041b8d3 Replace travis-ci badge with github CI workflow badge 2019-12-11 12:54:56 -08:00
Oleg Nosov
85d49477f5 Updated and simplified macros (#290)
* syn updated to latest version
* quote updated to latest version
* proc-macro-2 updated to latest version
* Performance improvements
* Don't create unnecessary TokenStreams for output types
2019-12-11 12:28:24 -08:00
Tim Kuehn
45af6ccdeb Workaround for pubsub example hanging.
The publisher client isn't being dropped when the async fn returns. It
could potentially be something strange in the ThreadPool executor.
2019-12-07 22:01:41 -08:00
Tim Kuehn
917c0c5e2d Use tokio::time::delay_for in lieu of thread::sleep. 2019-12-07 21:28:45 -08:00
Artem Vorotnikov
bbbd43e282 Unify serde transports.
This PR obsoletes the JSON and Bincode transports and instead introduces a unified transport that
is generic over any tokio-serde serialization format as well as AsyncRead + AsyncWrite medium.
This comes with a slight hit for usability (having to manually specify the underlying transport
and codec), but it can be alleviated by making custom freestanding connect and listen fns.
2019-12-07 20:58:08 -08:00
Artem Vorotnikov
f945392b5a Use tokio/stream feature for json-transport 2019-12-07 09:54:33 -08:00
Artem Vorotnikov
f4060779e4 Add GitHub workflow 2019-12-05 20:13:14 -08:00
Artem Vorotnikov
7cc8d9640b Fix clippy warnings 2019-12-05 17:39:53 -08:00
Artem Vorotnikov
7f871f03ef Improve Travis configuration (#282)
* Improve Travis configuration

* Replace 0.0.0.0 with localhost in tests
2019-11-28 14:06:35 -08:00
Artem Vorotnikov
709b966150 Update to Tokio 0.2 and futures 0.3 (#277) 2019-11-27 19:53:44 -08:00
Artem Vorotnikov
5e19b79aa4 Unite most of tarpc into a single crate 2019-11-26 13:08:18 -08:00
Tim Kuehn
6eb806907a Replace Gitter badge with Discord badge. 2019-11-22 14:28:24 -08:00
Tim Kuehn
8250ca31ff Remove --no-default-features from pre-push hook.
It seemingly doesn't work at the root of a virtual workspace. Not sure if this is new behavior or just a new explicit error message.
2019-11-15 17:19:08 -08:00
Tim Kuehn
7cd776143b Fix typo 2019-11-15 17:12:00 -08:00
Artem Vorotnikov
5f6c3d7d98 Port to pin-project 2019-10-09 14:12:24 -07:00
Artem Vorotnikov
915fe3ed4e Use the JSON transport in examples 2019-10-08 19:18:49 -07:00
Artem Vorotnikov
d8c7b9feb2 JSON transport: use Tokio resolver for connect() 2019-10-08 18:03:25 -07:00
Artem Vorotnikov
5ab3866d96 Add Unpin note 2019-10-08 17:15:17 -07:00
Artem Vorotnikov
184ea42033 Upgrade json-transport to Tokio 0.2 2019-10-08 17:15:17 -07:00
Artem Vorotnikov
014c209b8e Do not serialize _non_exhaustive field 2019-10-03 13:09:26 -07:00
Artem Vorotnikov
e91005855c Remove remaining feature flags 2019-10-02 13:07:37 -07:00
Artem Vorotnikov
46bcc0f559 tokio 0.2.0-alpha.4 2019-08-30 09:29:18 -07:00
Artem Vorotnikov
61322ebf41 Clippy fixes 2019-08-29 11:34:38 -07:00
Artem Vorotnikov
db0c9c4182 Cut type_alias_impl_trait feature flag 2019-08-29 11:34:38 -07:00
Artem Vorotnikov
9ee3011687 Update to Tokio 0.3.0-alpha.3 2019-08-29 11:34:38 -07:00
Artem Vorotnikov
5aa4a2cef6 tokio 0.2.0-alpha.2 2019-08-19 23:13:06 -07:00
Artem Vorotnikov
f38a172523 Format code with rustfmt 2019-08-19 13:20:21 -07:00
Tim Kuehn
66dbca80b2 Add missing feature, "compat", back to json-transport dependency on futures-preview. 2019-08-14 09:16:44 -07:00
Tim
61377dd4ff Fix comment in example service
It referred to bincode instead of json.
2019-08-14 08:32:49 -07:00
Tim
cd03f3ff8c Don't mention 'static optional in readme
This isn't supported by the service attribute.
2019-08-13 08:49:11 -07:00
Tim Kuehn
9479963773 Don't enable serde1 by default. I forgot it gives bad compile errors to people who don't have serde in their Cargo.toml. 2019-08-09 01:21:31 -07:00
Tim Kuehn
f974533bf7 Use real crate names rather than internal aliases. It's less confusing for people reading examples. 2019-08-09 01:16:06 -07:00
Tim Kuehn
d560ac6197 Update to the latest rustc nightly. 2019-08-09 01:08:20 -07:00
Tim Kuehn
1cdff15412 Fix needless verbosity in readme 2019-08-09 00:50:06 -07:00
Tim Kuehn
f8ba7d9f4e Make tokio1 serde1 default features 2019-08-08 22:06:09 -07:00
Tim Kuehn
41c1aafaf7 Update tokio to v0.2.0-alpha.1
As part of this, I made an optional tokio feature which, when enabled,
adds utility functions that spawn on the default tokio executor. This
allows for the removal of the runtime crate.

On the one hand, this makes the spawning utils slightly less generic. On
the other hand:

- The fns are just helpers and are easily rewritten by the user.
- Tokio is the clear dominant futures executor, so most people will just
  use these versions.
2019-08-08 21:53:36 -07:00
Tim Kuehn
75d1e877be Update README to talk about deadlines a bit more precisely. 2019-08-08 20:45:37 -07:00
Tim Kuehn
88e1cf558b Generate README.md from cargo readme 2019-08-08 20:31:04 -07:00
Tim Kuehn
50879d2acb Don't bake in Send + 'static.
Send + 'static was baked in to make it possible to spawn futures onto
the default executor. We can accomplish the same thing by offering
helper fns that do the spawning while not requiring it for the rest of
the functionality.

Fixes https://github.com/google/tarpc/issues/212
2019-08-07 13:39:48 -07:00
Tim
13cb14a119 Merge pull request #248 from tikue/service-idents
With this change, the service definitions don't need to be isolated in their own modules.

Given:

```rust
#[tarpc::service]
trait World { ... }
```

Before this would generate the following items
------
- `trait World`
- `fn serve`
- `struct Client`
- `fn new_stub`

`// Implementation details below`
- `enum Request`
- `enum Response`
- `enum ResponseFut`

And now these items
------
- `trait World {    ...    fn serve }`
- `struct WorldClient ... impl WorldClient {    ...    async fn new }`

`// Implementation details below`
- `enum WorldRequest`
- `enum WorldResponse`
- `enum WorldResponseFut`
- `struct ServeWorld` (new manual closure impl because you can't use impl Trait in trait fns)
```
2019-08-05 12:23:35 -07:00
Tim Kuehn
22ef6b7800 Choose a slightly less obvious name for Serve impl.
To hopefully avoid most collisions.
2019-07-30 21:46:16 -07:00
Tim Kuehn
e48e6dfe67 Add nice error message for ident collisions 2019-07-30 21:31:22 -07:00
Tim Kuehn
1b58914d59 Move generated functions under their corresponding items.
- fn serve -> Service::serve
- fn new_stub -> Client::new

This allows the generated function names to remain consistent across
service definitions while preventing collisions.
2019-07-30 20:45:58 -07:00
Tim Kuehn
2f24842b2d Add service name to generated items.
With this change, the service definitions don't need to be isolated in their own modules.
2019-07-30 00:52:30 -07:00
Tim Kuehn
5c485fe608 Add some tests for snake to camel case conversion. 2019-07-30 00:52:30 -07:00
Tim Kuehn
b0319e7db9 Remove macros.rs 2019-07-30 00:51:29 -07:00
Tim Kuehn
a4d9581888 Remove service_registry example 2019-07-29 23:17:08 -07:00
Tim Kuehn
fb5022b1c0 cargo fmt 2019-07-29 22:08:53 -07:00
Tim Kuehn
abb0b5b3ac Rewrite to use proc_macro_attribute 2019-07-29 22:04:04 -07:00
Artem Vorotnikov
49f2641e3c Port to runtime crate 2019-07-29 08:36:06 -07:00
Tim
650c60fe44 Merge pull request #246 from google/rustfmt
Reformat all code using rustfmt
2019-07-22 17:53:48 -07:00
Artem Vorotnikov
1d0bbcb36c Reformat all code using rustfmt 2019-07-23 03:44:16 +03:00
Tim Kuehn
c456ad7fa5 Fix typo 2019-07-22 14:15:27 -07:00
Tim Kuehn
537446a5c9 Remove use of unstable feature 'arbitrary_self_types'.
Turns out, this actually wasn't needed, with some minor refactoring.
2019-07-19 00:48:59 -07:00
Tim Kuehn
94b5b2c431 Add tests for rpc/server/filter.rs 2019-07-16 21:48:11 -07:00
Tim Kuehn
9863433fea Remove unstable feature 'async_closure' 2019-07-16 11:17:18 -07:00
Tim Kuehn
9a27465a25 Remove use of unstable feature 'try_trait' 2019-07-16 11:08:53 -07:00
Tim Kuehn
263cfe1435 Remove unused unstable feature 'integer_atomics' 2019-07-16 10:27:59 -07:00
Tim
6ae5302a70 Merge pull request #240 from tikue/filter-refactor 2019-07-15 23:04:20 -07:00
Tim Kuehn
c67b7283e7 Move bench outside crate. 2019-07-15 22:43:58 -07:00
Tim Kuehn
7b6e98da7b Replace transport integration tests with unit tests.
I want 'cargo test' to run faster.
2019-07-15 22:40:58 -07:00
Tim Kuehn
15b65fa20f Replace usage of Once and unsafe code with once_cell crate. 2019-07-15 20:05:10 -07:00
Tim Kuehn
372900173a Merge origin/master => tikue/filter-refactor 2019-07-15 19:04:56 -07:00
Tim Kuehn
1089415451 Make server methods more composable.
-- Connection Limits

The problem with having ConnectionFilter default-enabled is elaborated on in https://github.com/google/tarpc/issues/217. The gist of it is not all servers want a policy based on `SocketAddr`. This PR allows customizing the behavior of ConnectionFilter, at the cost of not having it enabled by default. However, enabling it is as simple as one line:

incoming.max_channels_per_key(10, ip_addr)

The second argument is a key function that takes the user-chosen transport and returns some hashable, equatable, cloneable key. In the above example, it returns an `IpAddr`.

This also allows the `Transport` trait to have the addr fns removed, which means it has become simply an alias for `Stream + Sink`.

-- Per-Channel Request Throttling

With respect to Channel's throttling behavior, the same argument applies. There isn't a one size fits all solution to throttling requests, and the policy applied by tarpc is just one of potentially many solutions. As such, `Channel` is now a trait that offers a few combinators, one of which is throttling:

channel.max_concurrent_requests(10).respond_with(serve(Server))

This functionality is also available on the existing `Handler` trait, which applies it to all incoming channels and can be used in tandem with connection limits:

incoming
    .max_channels_per_key(10, ip_addr)
    .max_concurrent_requests_per_channel(10).respond_with(serve(Server))

-- Global Request Throttling

I've entirely removed the overall request limit enforced across all channels. This functionality is easily gotten back via [`StreamExt::buffer_unordered`](https://rust-lang-nursery.github.io/futures-api-docs/0.3.0-alpha.1/futures/stream/trait.StreamExt.html#method.buffer_unordered), with the difference being that the previous behavior allowed you to spawn channels onto different threads, whereas `buffer_unordered ` means the `Channels` are handled on a single thread (the per-request handlers are still spawned). Considering the existing options, I don't believe that the benefit provided by this functionality held its own.
2019-07-15 19:01:46 -07:00
Tim Kuehn
8dbeeff0eb Fix some lint warnings 2019-07-15 18:21:11 -07:00
iovxw
85312d430c Update to futures-preview 0.3.0-alpha.17 (#238)
* Update to futures-preview 0.3.0-alpha.17

* Update feature gate

async_closure was moved out from async_await
2019-07-15 18:20:19 -07:00
Adam Wright
9843af9e00 Reflow some text in the readme (#239) 2019-07-15 17:53:56 -07:00
Tim Kuehn
a6bd423ef0 Remove use of external crate 'libtest'. 2019-07-15 17:52:27 -07:00
Kevin Ji
146496d08c README: Use the SVG Travis badge (#236) 2019-06-08 10:31:08 -07:00
Tim Kuehn
b562051c38 Bump tarpc-lib to 0.6.1 to fix request cancellation issue. 2019-05-22 01:33:00 -07:00
Tim Kuehn
fe164ca368 Fix bug where expired request wasn't propagating cancellation.
DispatchResponse was incorrectly marking itself as complete even when
expiring without receiving a response. This can cause a chain of
deleterious effects:

- Request cancellation won't propagate when request timers expire.
- Which causes client dispatch to have an inconsistent in-flight request
  map containing stale IDs.
- Which can cause clients to hang rather than exiting.
2019-05-22 01:29:01 -07:00
Artem Vorotnikov
950ad5187c Add JSON transport (#219) 2019-05-20 18:45:41 -07:00
Tim Kuehn
e6ab69c314 Keep commented-out code in each block so that rustdoc is happy. 2019-05-15 16:31:11 -07:00
Tim Kuehn
373dcbed57 Clarify dependencies required for README example
Fixes https://github.com/google/tarpc/issues/232
2019-05-15 15:40:25 -07:00
Tim Kuehn
ce9c057b1b Remove await!() macro from readme 2019-05-13 10:16:25 -07:00
Tim Kuehn
6745cee72c Bump tarpc to v0.18.0 2019-05-11 13:00:35 -07:00
Artem Vorotnikov
31abea18b3 Update to futures-preview 0.3.0-alpha.16 (#230) 2019-05-11 15:18:52 -04:00
Tim Kuehn
593ac135ce Remove stable features from doc examples 2019-04-30 13:18:39 -07:00
Tim Kuehn
05a924d27f Bump tarpc version to 0.17.0 2019-04-30 13:01:45 -07:00
Artem Vorotnikov
af9d71ed0d Bump futures to 0.3.0-alpha.15 (#226) 2019-04-28 20:13:06 -07:00
Tim Kuehn
9b90f6ae51 Bump to v0.16.0 2019-04-16 10:46:53 -07:00
Tim
bbfc8ac352 Merge pull request #216 from vorot93/futures-master
* Use upstream sink compat shims
* Port to new Sink trait introduced in e101c891f04aba34ee29c6a8cd8321563c7e0161
* rustfmt
* Port to std::task::Context
* Add Google license header to bincode-transport/src/compat.rs
* Remove compat for it is no longer needed
* future::join as freestanding function
* Simplify dependencies
* Depend on futures-preview 0.3.0-alpha.14
* Fix infinite recursion
2019-04-16 08:43:10 -07:00
Tim
ad86a967ba Fix infinite recursion 2019-04-16 18:27:42 +03:00
Artem Vorotnikov
58a0eced19 Depend on futures-preview 0.3.0-alpha.14 2019-04-15 21:16:20 +03:00
Artem Vorotnikov
46fffd13e7 Simplify dependencies 2019-04-15 21:14:25 +03:00
Artem Vorotnikov
6c8d4be462 future::join as freestanding function 2019-04-15 20:30:04 +03:00
Artem Vorotnikov
e3a517bf0d Remove compat and transmute for they are no longer needed 2019-04-15 20:24:09 +03:00
Artem Vorotnikov
f4e22bdc2e Port to std::task::Context 2019-04-15 20:22:15 +03:00
Artem Vorotnikov
46f56fbdc0 Add Google license header to bincode-transport/src/compat.rs 2019-04-15 20:22:15 +03:00
Artem Vorotnikov
8665655592 Fix test client breakage by 9100ea46f997f24d4bc8c1764d0fe3ff8226ad2a 2019-04-15 20:22:15 +03:00
Artem Vorotnikov
4569d26d81 rustfmt 2019-04-15 20:22:15 +03:00
Artem Vorotnikov
b8b92ddb5f Workaround for stack overflow caused by 2a95710db0e2d85094938776ebb4f270bc389c41 2019-04-15 20:16:48 +03:00
Artem Vorotnikov
8dd3390876 Port to new Sink trait introduced in e101c891f04aba34ee29c6a8cd8321563c7e0161 2019-04-15 20:16:48 +03:00
Artem Vorotnikov
06c420b60c Use upstream sink compat shims 2019-04-15 20:16:48 +03:00
Artem Vorotnikov
a7fb4d22cc Switch to master branch of futures-preview 2019-04-15 20:16:48 +03:00
Tim
b1cd5f34e5 Don't panic in pump_write when a client is dropped and there are more calls to poll. (#221)
This can happen in cases where a response is being read and the client isn't around.

Fixes #220
2019-04-15 09:42:53 -07:00
Artem Vorotnikov
088e5f8f2c Remove deprecated feature from bincode dependency (#218) 2019-04-04 10:34:11 -07:00
Tim Kuehn
4e0be5b626 Publish tarpc v0.15.0 2019-03-26 21:13:41 -07:00
Artem Vorotnikov
5516034bbc Use libtest crate (#213) 2019-03-24 22:29:01 -07:00
Artem Vorotnikov
06544faa5a Update to futures 0.3.0-alpha.13 (#211) 2019-02-26 09:32:41 -08:00
Tim Kuehn
39737b720a Cargo fmt 2019-01-17 10:37:16 -08:00
Tim Kuehn
0f36985440 Update for latest changes to futures.
Fixes #209.
2019-01-17 10:37:03 -08:00
Tyler Bindon
959bb691cd Update regex to match diffs output by cargo fmt. (#208)
It appears the header of the diffs output by cargo fmt have changed. It now says "Diff in /blah/blah/blah.rs at line 99:" Matching on lines starting with + or - should be more future-proof against changes to the surroundings.
2018-12-09 01:59:35 -08:00
Tim
2a3162c5fa Cargo feature 'rename-dependency' is stabilized 2018-11-21 11:03:41 -08:00
Tim Kuehn
0cc976b729 cargo fmt 2018-11-06 17:01:27 -08:00
Tim Kuehn
4d2d3f24c6 Address Clippy lints 2018-11-06 17:00:15 -08:00
Tim Kuehn
2c7c64841f Add symlink tarpc/README.md -> README.md 2018-10-29 16:11:01 -07:00
Tim Kuehn
4ea142d0f3 Remove coverage badge.
It hasn't been updated in over 2 years.
2018-10-29 11:40:09 -07:00
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
60 changed files with 6642 additions and 2067 deletions

66
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
on: [push, pull_request]
name: Continuous integration
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
args: --all-features
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-features -- -D warnings

4
.gitignore vendored
View File

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

View File

@@ -1,32 +0,0 @@
language: rust
sudo: false
rust:
- stable
- beta
- nightly
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
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=""

7
Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[workspace]
members = [
"example-service",
"tarpc",
"plugins",
]

207
README.md
View File

@@ -1,92 +1,167 @@
## 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)
[![Latest Version](https://img.shields.io/crates/v/tarpc.svg)](https://crates.io/crates/tarpc)
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build status][gh-actions-badge]][gh-actions-url]
[![Discord chat][discord-badge]][discord-url]
[crates-badge]: https://img.shields.io/crates/v/tarpc.svg
[crates-url]: https://crates.io/crates/tarpc
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: LICENSE
[gh-actions-badge]: https://github.com/google/tarpc/workflows/Continuous%20integration/badge.svg
[gh-actions-url]: https://github.com/google/tarpc/actions?query=workflow%3A%22Continuous+integration%22
[discord-badge]: https://img.shields.io/discord/647529123996237854.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/gXwpdSt
# tarpc
<!-- cargo-sync-readme start -->
*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 context switching between different languages.
Some other features of tarpc:
- Pluggable transport: any type impling `Stream<Item = Request> + Sink<Response>` can be
used as a transport to connect the client and server.
- `Send + 'static` optional: if the transport doesn't require it, neither does tarpc!
- Cascading cancellation: dropping a request will send a cancellation message to the server.
The server will cease any unfinished work on the request, subsequently cancelling any of its
own requests, repeating for the entire chain of transitive dependencies.
- Configurable deadlines and deadline propagation: request deadlines default to 10s if
unspecified. The server will automatically cease work when the deadline has passed. Any
requests sent by the server that use the request context will propagate the request deadline.
For example, if a server is handling a request with a 10s deadline, does 2s of work, then
sends a request to another server, that server will see an 8s deadline.
- Serde serialization: enabling the `serde1` Cargo feature will make service requests and
responses `Serialize + Deserialize`. It's entirely optional, though: in-memory transports can
be used, as well, so the price of serialization doesn't have to be paid when it's not needed.
## Usage
Add to your `Cargo.toml` dependencies:
```toml
tarpc = "0.5.1"
tarpc = "0.22.0"
```
The `tarpc::service` attribute expands to a collection of items that form an rpc service.
These generated types make it easy and ergonomic to write servers with less boilerplate.
Simply implement the generated service trait, and you're off to the races!
## Example
For this example, in addition to tarpc, also add two other dependencies to
your `Cargo.toml`:
```toml
futures = "0.3"
tokio = "0.2"
```
In the following example, we use an in-process channel for communication between
client and server. In real code, you will likely communicate over the network.
For a more real-world example, see [example-service](example-service).
First, let's set up the dependencies and service definition.
```rust
#[macro_use]
extern crate tarpc;
mod hello_service {
service! {
rpc hello(name: String) -> String;
}
}
use hello_service::Service as HelloService;
use futures::{
future::{self, Ready},
prelude::*,
};
use tarpc::{
client, context,
server::{self, Handler},
};
use std::io;
struct HelloServer;
impl HelloService for HelloServer {
fn hello(&self, name: String) -> String {
format!("Hello, {}!", name)
}
}
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();
// 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]
trait World {
/// Returns a greeting for name.
async fn hello(name: String) -> String;
}
```
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.
This service definition generates a trait called `World`. Next we need to
implement it for our Server struct.
```rust
// This is the type that implements the generated World trait. It is the business logic
// and is used to start the server.
#[derive(Clone)]
struct HelloServer;
impl World 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))
}
}
```
Lastly let's write our `main` that will start the server. While this example uses an
[in-process channel](rpc::transport::channel), tarpc also ships a generic [`serde_transport`]
behind the `serde-transport` feature, with additional [TCP](serde_transport::tcp) functionality
available behind the `tcp` feature.
```rust
#[tokio::main]
async fn main() -> io::Result<()> {
let (client_transport, server_transport) = tarpc::transport::channel::unbounded();
let server = server::new(server::Config::default())
// incoming() takes a stream of transports such as would be returned by
// TcpListener::incoming (but a stream instead of an iterator).
.incoming(stream::once(future::ready(server_transport)))
.respond_with(HelloServer.serve());
tokio::spawn(server);
// WorldClient is generated by the macro. It has a constructor `new` that takes a config and
// any Transport as input
let mut client = WorldClient::new(client::Config::default(), client_transport).spawn()?;
// The client has an RPC method for each RPC defined in the annotated trait. 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 = client.hello(context::current(), "Stim".to_string()).await?;
println!("{}", hello);
Ok(())
}
```
## 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.
<!-- cargo-sync-readme end -->
## 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.
License: MIT

View File

@@ -1,3 +1,200 @@
## 0.22.0 (2020-08-02)
This release adds some flexibility and consistency to `serde_transport`, with one new feature and
one small breaking change.
### New Features
`serde_transport::tcp` now exposes framing configuration on `connect()` and `listen()`. This is
useful if, for instance, you want to send requests or responses that are larger than the maximum
payload allowed by default:
```rust
let mut transport = tarpc::serde_transport::tcp::connect(server_addr, Json::default);
transport.config_mut().max_frame_length(4294967296);
let mut client = MyClient::new(client::Config::default(), transport.await?).spawn()?;
```
### Breaking Changes
The codec argument to `serde_transport::tcp::connect` changed from a Codec to impl Fn() -> Codec,
to be consistent with `serde_transport::tcp::listen`. While only one Codec is needed, more than one
person has been tripped up by the inconsistency between `connect` and `listen`. Unfortunately, the
compiler errors are not much help in this case, so it was decided to simply do the more intuitive
thing so that the compiler doesn't need to step in in the first place.
## 0.21.1 (2020-08-02)
### New Features
#### #[tarpc::server] diagnostics
When a service impl uses #[tarpc::server], only `async fn`s are re-written. This can lead to
confusing compiler errors about missing associated types:
```
error: not all trait items implemented, missing: `HelloFut`
--> $DIR/tarpc_server_missing_async.rs:9:1
|
9 | impl World for HelloServer {
| ^^^^
```
The proc macro now provides better diagnostics for this case:
```
error: not all trait items implemented, missing: `HelloFut`
--> $DIR/tarpc_server_missing_async.rs:9:1
|
9 | impl World for HelloServer {
| ^^^^
error: hint: `#[tarpc::server]` only rewrites async fns, and `fn hello` is not async
--> $DIR/tarpc_server_missing_async.rs:10:5
|
10 | fn hello(name: String) -> String {
| ^^
```
### Bug Fixes
#### Fixed client hanging when server shuts down
Previously, clients would ignore when the read half of the transport was closed, continuing to
write requests. This didn't make much sense, because without the ability to receive responses,
clients have no way to know if requests were actually processed by the server. It basically just
led to clients that would hang for a few seconds before shutting down. This has now been
corrected: clients will immediately shut down when the read-half of the transport is closed.
#### More docs.rs documentation
Previously, docs.rs only documented items enabled by default, notably leaving out documentation
for tokio and serde features. This has now been corrected: docs.rs should have documentation
for all optional features.
## 0.21.0 (2020-06-26)
### New Features
A new proc macro, `#[tarpc::server]` was added! This enables service impls to elide the boilerplate
of specifying associated types for each RPC. With the ubiquity of async-await, most code won't have
nameable futures and will just be boxing the return type anyway. This macro does that for you.
### Breaking Changes
- Enums had _non_exhaustive fields replaced with the #[non_exhaustive] attribute.
### Bug Fixes
- https://github.com/google/tarpc/issues/304
A race condition in code that limits number of connections per client caused occasional panics.
- https://github.com/google/tarpc/pull/295
Made request timeouts account for time spent in the outbound buffer. Previously, a large outbound
queue would lead to requests not timing out correctly.
## 0.20.0 (2019-12-11)
### Breaking Changes
1. tarpc has updated its tokio dependency to the latest 0.2 version.
2. The tarpc crates have been unified into just `tarpc`, with new Cargo features to enable
functionality.
- The bincode-transport and json-transport crates are deprecated and superseded by
the `serde_transport` module, which unifies much of the logic present in both crates.
## 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,35 @@
[package]
name = "tarpc-example-service"
version = "0.6.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]
clap = "2.0"
futures = "0.3"
serde = { version = "1.0" }
tarpc = { version = "0.22", path = "../tarpc", features = ["full"] }
tokio = { version = "0.2", features = ["full"] }
tokio-serde = { version = "0.6", features = ["json"] }
tokio-util = { version = "0.3", features = ["codec"] }
env_logger = "0.6"
[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,62 @@
// 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 clap::{App, Arg};
use std::{io, net::SocketAddr};
use tarpc::{client, context};
use tokio_serde::formats::Json;
#[tokio::main]
async fn main() -> io::Result<()> {
env_logger::init();
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();
let server_addr = flags.value_of("server_addr").unwrap();
let server_addr = server_addr
.parse::<SocketAddr>()
.unwrap_or_else(|e| panic!(r#"--server_addr value "{}" invalid: {}"#, server_addr, e));
let name = flags.value_of("name").unwrap().into();
let mut transport = tarpc::serde_transport::tcp::connect(server_addr, Json::default);
transport.config_mut().max_frame_length(4294967296);
// WorldClient is generated by the service attribute. It has a constructor `new` that takes a
// config and any Transport as input.
let mut client =
service::WorldClient::new(client::Config::default(), transport.await?).spawn()?;
// The client has an RPC method for each RPC defined in the annotated trait. 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 = client.hello(context::current(), name).await?;
println!("{}", hello);
Ok(())
}

View File

@@ -0,0 +1,13 @@
// 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.
/// 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]
pub trait World {
/// Returns a greeting for name.
async fn hello(name: String) -> String;
}

View File

@@ -0,0 +1,80 @@
// 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 clap::{App, Arg};
use futures::{future, prelude::*};
use service::World;
use std::{
io,
net::{IpAddr, SocketAddr},
};
use tarpc::{
context,
server::{self, Channel, Handler},
};
use tokio_serde::formats::Json;
// This is the type that implements the generated World trait. It is the business logic
// and is used to start the server.
#[derive(Clone)]
struct HelloServer(SocketAddr);
#[tarpc::server]
impl World for HelloServer {
async fn hello(self, _: context::Context, name: String) -> String {
format!("Hello, {}! You are connected from {:?}.", name, self.0)
}
}
#[tokio::main]
async fn main() -> io::Result<()> {
env_logger::init();
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));
let server_addr = (IpAddr::from([0, 0, 0, 0]), port);
// JSON transport is provided by the json_transport tarpc module. It makes it easy
// to start up a serde-powered json serialization strategy over TCP.
let mut listener = tarpc::serde_transport::tcp::listen(&server_addr, Json::default).await?;
listener.config_mut().max_frame_length(4294967296);
listener
// Ignore accept errors.
.filter_map(|r| future::ready(r.ok()))
.map(server::BaseChannel::with_defaults)
// Limit channels to 1 per IP.
.max_channels_per_key(1, |t| t.as_ref().peer_addr().unwrap().ip())
// serve is generated by the service attribute. It takes as input any type implementing
// the generated World trait.
.map(|channel| {
let server = HelloServer(channel.as_ref().as_ref().peer_addr().unwrap());
channel.respond_with(server.serve()).execute()
})
// Max 10 channels.
.buffer_unordered(10)
.for_each(|_| async {})
.await;
Ok(())
}

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 -- --unstable-features --skip-children --check $file)"
fi
done
if grep --quiet "^[-+]" <<< "$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,66 @@ 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 +stable build --color=always
try_run "Testing ... " cargo +stable test --color=always
try_run "Testing with all features enabled ... " cargo +stable test --all-features --color=always
for EXAMPLE in $(cargo +stable run --example 2>&1 | grep ' ' | awk '{print $1}')
do
try_run "Running example \"$EXAMPLE\" ... " cargo +stable run --example $EXAMPLE
done
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

33
plugins/Cargo.toml Normal file
View File

@@ -0,0 +1,33 @@
[package]
name = "tarpc-plugins"
version = "0.8.0"
authors = ["Adam Wright <adam.austin.wright@gmail.com>", "Tim Kuehn <timothy.j.kuehn@gmail.com>"]
edition = "2018"
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."
[features]
serde1 = []
[badges]
travis-ci = { repository = "google/tarpc" }
[dependencies]
syn = { version = "1.0.11", features = ["full"] }
quote = "1.0.2"
proc-macro2 = "1.0.6"
[lib]
proc-macro = true
[dev-dependencies]
futures = "0.3"
serde = { version = "1.0", features = ["derive"] }
tarpc = { path = "../tarpc" }
assert-type-eq = "0.1.0"

1
plugins/rustfmt.toml Normal file
View File

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

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

@@ -0,0 +1,804 @@
// 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.
#![recursion_limit = "512"]
extern crate proc_macro;
extern crate proc_macro2;
extern crate quote;
extern crate syn;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens};
use syn::{
braced,
ext::IdentExt,
parenthesized,
parse::{Parse, ParseStream},
parse_macro_input, parse_quote, parse_str,
spanned::Spanned,
token::Comma,
Attribute, FnArg, Ident, ImplItem, ImplItemMethod, ImplItemType, ItemImpl, Lit, LitBool,
MetaNameValue, Pat, PatType, ReturnType, Token, Type, Visibility,
};
/// Accumulates multiple errors into a result.
/// Only use this for recoverable errors, i.e. non-parse errors. Fatal errors should early exit to
/// avoid further complications.
macro_rules! extend_errors {
($errors: ident, $e: expr) => {
match $errors {
Ok(_) => $errors = Err($e),
Err(ref mut errors) => errors.extend($e),
}
};
}
struct Service {
attrs: Vec<Attribute>,
vis: Visibility,
ident: Ident,
rpcs: Vec<RpcMethod>,
}
struct RpcMethod {
attrs: Vec<Attribute>,
ident: Ident,
args: Vec<PatType>,
output: ReturnType,
}
impl Parse for Service {
fn parse(input: ParseStream) -> syn::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let vis = input.parse()?;
input.parse::<Token![trait]>()?;
let ident: Ident = input.parse()?;
let content;
braced!(content in input);
let mut rpcs = Vec::<RpcMethod>::new();
while !content.is_empty() {
rpcs.push(content.parse()?);
}
let mut ident_errors = Ok(());
for rpc in &rpcs {
if rpc.ident == "new" {
extend_errors!(
ident_errors,
syn::Error::new(
rpc.ident.span(),
format!(
"method name conflicts with generated fn `{}Client::new`",
ident.unraw()
)
)
);
}
if rpc.ident == "serve" {
extend_errors!(
ident_errors,
syn::Error::new(
rpc.ident.span(),
format!("method name conflicts with generated fn `{}::serve`", ident)
)
);
}
}
ident_errors?;
Ok(Self {
attrs,
vis,
ident,
rpcs,
})
}
}
impl Parse for RpcMethod {
fn parse(input: ParseStream) -> syn::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
input.parse::<Token![async]>()?;
input.parse::<Token![fn]>()?;
let ident = input.parse()?;
let content;
parenthesized!(content in input);
let mut args = Vec::new();
let mut errors = Ok(());
for arg in content.parse_terminated::<FnArg, Comma>(FnArg::parse)? {
match arg {
FnArg::Typed(captured) if matches!(&*captured.pat, Pat::Ident(_)) => {
args.push(captured);
}
FnArg::Typed(captured) => {
extend_errors!(
errors,
syn::Error::new(captured.pat.span(), "patterns aren't allowed in RPC args")
);
}
FnArg::Receiver(_) => {
extend_errors!(
errors,
syn::Error::new(arg.span(), "method args cannot start with self")
);
}
}
}
errors?;
let output = input.parse()?;
input.parse::<Token![;]>()?;
Ok(Self {
attrs,
ident,
args,
output,
})
}
}
// If `derive_serde` meta item is not present, defaults to cfg!(feature = "serde1").
// `derive_serde` can only be true when serde1 is enabled.
struct DeriveSerde(bool);
impl Parse for DeriveSerde {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut result = Ok(None);
let mut derive_serde = Vec::new();
let meta_items = input.parse_terminated::<MetaNameValue, Comma>(MetaNameValue::parse)?;
for meta in meta_items {
if meta.path.segments.len() != 1 {
extend_errors!(
result,
syn::Error::new(
meta.span(),
"tarpc::service does not support this meta item"
)
);
continue;
}
let segment = meta.path.segments.first().unwrap();
if segment.ident != "derive_serde" {
extend_errors!(
result,
syn::Error::new(
meta.span(),
"tarpc::service does not support this meta item"
)
);
continue;
}
match meta.lit {
Lit::Bool(LitBool { value: true, .. }) if cfg!(feature = "serde1") => {
result = result.and(Ok(Some(true)))
}
Lit::Bool(LitBool { value: true, .. }) => {
extend_errors!(
result,
syn::Error::new(
meta.span(),
"To enable serde, first enable the `serde1` feature of tarpc"
)
);
}
Lit::Bool(LitBool { value: false, .. }) => result = result.and(Ok(Some(false))),
_ => extend_errors!(
result,
syn::Error::new(
meta.lit.span(),
"`derive_serde` expects a value of type `bool`"
)
),
}
derive_serde.push(meta);
}
if derive_serde.len() > 1 {
for (i, derive_serde) in derive_serde.iter().enumerate() {
extend_errors!(
result,
syn::Error::new(
derive_serde.span(),
format!(
"`derive_serde` appears more than once (occurrence #{})",
i + 1
)
)
);
}
}
let derive_serde = result?.unwrap_or(cfg!(feature = "serde1"));
Ok(Self(derive_serde))
}
}
/// Generates:
/// - service trait
/// - serve fn
/// - client stub struct
/// - new_stub client factory fn
/// - Request and Response enums
/// - ResponseFut Future
#[proc_macro_attribute]
pub fn service(attr: TokenStream, input: TokenStream) -> TokenStream {
let derive_serde = parse_macro_input!(attr as DeriveSerde);
let unit_type: &Type = &parse_quote!(());
let Service {
ref attrs,
ref vis,
ref ident,
ref rpcs,
} = parse_macro_input!(input as Service);
let camel_case_fn_names: &Vec<_> = &rpcs
.iter()
.map(|rpc| snake_to_camel(&rpc.ident.unraw().to_string()))
.collect();
let args: &[&[PatType]] = &rpcs.iter().map(|rpc| &*rpc.args).collect::<Vec<_>>();
let response_fut_name = &format!("{}ResponseFut", ident.unraw());
let derive_serialize = if derive_serde.0 {
Some(quote!(#[derive(serde::Serialize, serde::Deserialize)]))
} else {
None
};
ServiceGenerator {
response_fut_name,
service_ident: ident,
server_ident: &format_ident!("Serve{}", ident),
response_fut_ident: &Ident::new(&response_fut_name, ident.span()),
client_ident: &format_ident!("{}Client", ident),
request_ident: &format_ident!("{}Request", ident),
response_ident: &format_ident!("{}Response", ident),
vis,
args,
method_attrs: &rpcs.iter().map(|rpc| &*rpc.attrs).collect::<Vec<_>>(),
method_idents: &rpcs.iter().map(|rpc| &rpc.ident).collect::<Vec<_>>(),
attrs,
rpcs,
return_types: &rpcs
.iter()
.map(|rpc| match rpc.output {
ReturnType::Type(_, ref ty) => ty,
ReturnType::Default => unit_type,
})
.collect::<Vec<_>>(),
arg_pats: &args
.iter()
.map(|args| args.iter().map(|arg| &*arg.pat).collect())
.collect::<Vec<_>>(),
camel_case_idents: &rpcs
.iter()
.zip(camel_case_fn_names.iter())
.map(|(rpc, name)| Ident::new(name, rpc.ident.span()))
.collect::<Vec<_>>(),
future_types: &camel_case_fn_names
.iter()
.map(|name| parse_str(&format!("{}Fut", name)).unwrap())
.collect::<Vec<_>>(),
derive_serialize: derive_serialize.as_ref(),
}
.into_token_stream()
.into()
}
/// generate an identifier consisting of the method name to CamelCase with
/// Fut appended to it.
fn associated_type_for_rpc(method: &ImplItemMethod) -> String {
snake_to_camel(&method.sig.ident.unraw().to_string()) + "Fut"
}
/// Transforms an async function into a sync one, returning a type declaration
/// for the return type (a future).
fn transform_method(method: &mut ImplItemMethod) -> ImplItemType {
method.sig.asyncness = None;
// get either the return type or ().
let ret = match &method.sig.output {
ReturnType::Default => quote!(()),
ReturnType::Type(_, ret) => quote!(#ret),
};
let fut_name = associated_type_for_rpc(method);
let fut_name_ident = Ident::new(&fut_name, method.sig.ident.span());
// generate the updated return signature.
method.sig.output = parse_quote! {
-> ::core::pin::Pin<Box<
dyn ::core::future::Future<Output = #ret> + ::core::marker::Send
>>
};
// transform the body of the method into Box::pin(async move { body }).
let block = method.block.clone();
method.block = parse_quote! [{
Box::pin(async move
#block
)
}];
// generate and return type declaration for return type.
let t: ImplItemType = parse_quote! {
type #fut_name_ident = ::core::pin::Pin<Box<dyn ::core::future::Future<Output = #ret> + ::core::marker::Send>>;
};
t
}
#[proc_macro_attribute]
pub fn server(_attr: TokenStream, input: TokenStream) -> TokenStream {
let mut item = syn::parse_macro_input!(input as ItemImpl);
let span = item.span();
// the generated type declarations
let mut types: Vec<ImplItemType> = Vec::new();
let mut expected_non_async_types: Vec<(&ImplItemMethod, String)> = Vec::new();
let mut found_non_async_types: Vec<&ImplItemType> = Vec::new();
for inner in &mut item.items {
match inner {
ImplItem::Method(method) => {
if method.sig.asyncness.is_some() {
// if this function is declared async, transform it into a regular function
let typedecl = transform_method(method);
types.push(typedecl);
} else {
// If it's not async, keep track of all required associated types for better
// error reporting.
expected_non_async_types.push((method, associated_type_for_rpc(method)));
}
}
ImplItem::Type(typedecl) => found_non_async_types.push(typedecl),
_ => {}
}
}
if let Err(e) =
verify_types_were_provided(span, &expected_non_async_types, &found_non_async_types)
{
return TokenStream::from(e.to_compile_error());
}
// add the type declarations into the impl block
for t in types.into_iter() {
item.items.push(syn::ImplItem::Type(t));
}
TokenStream::from(quote!(#item))
}
fn verify_types_were_provided(
span: Span,
expected: &[(&ImplItemMethod, String)],
provided: &[&ImplItemType],
) -> syn::Result<()> {
let mut result = Ok(());
for (method, expected) in expected {
if provided
.iter()
.find(|typedecl| typedecl.ident == expected)
.is_none()
{
let mut e = syn::Error::new(
span,
format!("not all trait items implemented, missing: `{}`", expected),
);
let fn_span = method.sig.fn_token.span();
e.extend(syn::Error::new(
fn_span.join(method.sig.ident.span()).unwrap_or(fn_span),
format!(
"hint: `#[tarpc::server]` only rewrites async fns, and `fn {}` is not async",
method.sig.ident
),
));
match result {
Ok(_) => result = Err(e),
Err(ref mut error) => error.extend(Some(e)),
}
}
}
result
}
// Things needed to generate the service items: trait, serve impl, request/response enums, and
// the client stub.
struct ServiceGenerator<'a> {
service_ident: &'a Ident,
server_ident: &'a Ident,
response_fut_ident: &'a Ident,
response_fut_name: &'a str,
client_ident: &'a Ident,
request_ident: &'a Ident,
response_ident: &'a Ident,
vis: &'a Visibility,
attrs: &'a [Attribute],
rpcs: &'a [RpcMethod],
camel_case_idents: &'a [Ident],
future_types: &'a [Type],
method_idents: &'a [&'a Ident],
method_attrs: &'a [&'a [Attribute]],
args: &'a [&'a [PatType]],
return_types: &'a [&'a Type],
arg_pats: &'a [Vec<&'a Pat>],
derive_serialize: Option<&'a TokenStream2>,
}
impl<'a> ServiceGenerator<'a> {
fn trait_service(&self) -> TokenStream2 {
let &Self {
attrs,
rpcs,
vis,
future_types,
return_types,
service_ident,
server_ident,
..
} = self;
let types_and_fns = rpcs
.iter()
.zip(future_types.iter())
.zip(return_types.iter())
.map(
|(
(
RpcMethod {
attrs, ident, args, ..
},
future_type,
),
output,
)| {
let ty_doc = format!("The response future returned by {}.", ident);
quote! {
#[doc = #ty_doc]
type #future_type: std::future::Future<Output = #output>;
#( #attrs )*
fn #ident(self, context: tarpc::context::Context, #( #args ),*) -> Self::#future_type;
}
},
);
quote! {
#( #attrs )*
#vis trait #service_ident: Clone {
#( #types_and_fns )*
/// Returns a serving function to use with [tarpc::server::Channel::respond_with].
fn serve(self) -> #server_ident<Self> {
#server_ident { service: self }
}
}
}
}
fn struct_server(&self) -> TokenStream2 {
let &Self {
vis, server_ident, ..
} = self;
quote! {
/// A serving function to use with [tarpc::server::Channel::respond_with].
#[derive(Clone)]
#vis struct #server_ident<S> {
service: S,
}
}
}
fn impl_serve_for_server(&self) -> TokenStream2 {
let &Self {
request_ident,
server_ident,
service_ident,
response_ident,
response_fut_ident,
camel_case_idents,
arg_pats,
method_idents,
..
} = self;
quote! {
impl<S> tarpc::server::Serve<#request_ident> for #server_ident<S>
where S: #service_ident
{
type Resp = #response_ident;
type Fut = #response_fut_ident<S>;
fn serve(self, ctx: tarpc::context::Context, req: #request_ident) -> Self::Fut {
match req {
#(
#request_ident::#camel_case_idents{ #( #arg_pats ),* } => {
#response_fut_ident::#camel_case_idents(
#service_ident::#method_idents(
self.service, ctx, #( #arg_pats ),*
)
)
}
)*
}
}
}
}
}
fn enum_request(&self) -> TokenStream2 {
let &Self {
derive_serialize,
vis,
request_ident,
camel_case_idents,
args,
..
} = self;
quote! {
/// The request sent over the wire from the client to the server.
#[derive(Debug)]
#derive_serialize
#vis enum #request_ident {
#( #camel_case_idents{ #( #args ),* } ),*
}
}
}
fn enum_response(&self) -> TokenStream2 {
let &Self {
derive_serialize,
vis,
response_ident,
camel_case_idents,
return_types,
..
} = self;
quote! {
/// The response sent over the wire from the server to the client.
#[derive(Debug)]
#derive_serialize
#vis enum #response_ident {
#( #camel_case_idents(#return_types) ),*
}
}
}
fn enum_response_future(&self) -> TokenStream2 {
let &Self {
vis,
service_ident,
response_fut_ident,
camel_case_idents,
future_types,
..
} = self;
quote! {
/// A future resolving to a server response.
#vis enum #response_fut_ident<S: #service_ident> {
#( #camel_case_idents(<S as #service_ident>::#future_types) ),*
}
}
}
fn impl_debug_for_response_future(&self) -> TokenStream2 {
let &Self {
service_ident,
response_fut_ident,
response_fut_name,
..
} = self;
quote! {
impl<S: #service_ident> std::fmt::Debug for #response_fut_ident<S> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.debug_struct(#response_fut_name).finish()
}
}
}
}
fn impl_future_for_response_future(&self) -> TokenStream2 {
let &Self {
service_ident,
response_fut_ident,
response_ident,
camel_case_idents,
..
} = self;
quote! {
impl<S: #service_ident> std::future::Future for #response_fut_ident<S> {
type Output = #response_ident;
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>)
-> std::task::Poll<#response_ident>
{
unsafe {
match std::pin::Pin::get_unchecked_mut(self) {
#(
#response_fut_ident::#camel_case_idents(resp) =>
std::pin::Pin::new_unchecked(resp)
.poll(cx)
.map(#response_ident::#camel_case_idents),
)*
}
}
}
}
}
}
fn struct_client(&self) -> TokenStream2 {
let &Self {
vis,
client_ident,
request_ident,
response_ident,
..
} = self;
quote! {
#[allow(unused)]
#[derive(Clone, Debug)]
/// The client stub that makes RPC calls to the server. Exposes a Future interface.
#vis struct #client_ident<C = tarpc::client::Channel<#request_ident, #response_ident>>(C);
}
}
fn impl_from_for_client(&self) -> TokenStream2 {
let &Self {
client_ident,
request_ident,
response_ident,
..
} = self;
quote! {
impl<C> From<C> for #client_ident<C>
where for <'a> C: tarpc::Client<'a, #request_ident, Response = #response_ident>
{
fn from(client: C) -> Self {
#client_ident(client)
}
}
}
}
fn impl_client_new(&self) -> TokenStream2 {
let &Self {
client_ident,
vis,
request_ident,
response_ident,
..
} = self;
quote! {
impl #client_ident {
/// Returns a new client stub that sends requests over the given transport.
#vis fn new<T>(config: tarpc::client::Config, transport: T)
-> tarpc::client::NewClient<
Self,
tarpc::client::channel::RequestDispatch<#request_ident, #response_ident, T>
>
where
T: tarpc::Transport<tarpc::ClientMessage<#request_ident>, tarpc::Response<#response_ident>>
{
let new_client = tarpc::client::new(config, transport);
tarpc::client::NewClient {
client: #client_ident(new_client.client),
dispatch: new_client.dispatch,
}
}
}
}
}
fn impl_client_rpc_methods(&self) -> TokenStream2 {
let &Self {
client_ident,
request_ident,
response_ident,
method_attrs,
vis,
method_idents,
args,
return_types,
arg_pats,
camel_case_idents,
..
} = self;
quote! {
impl<C> #client_ident<C>
where for<'a> C: tarpc::Client<'a, #request_ident, Response = #response_ident>
{
#(
#[allow(unused)]
#( #method_attrs )*
#vis fn #method_idents(&mut self, ctx: tarpc::context::Context, #( #args ),*)
-> impl std::future::Future<Output = std::io::Result<#return_types>> + '_ {
let request = #request_ident::#camel_case_idents { #( #arg_pats ),* };
let resp = tarpc::Client::call(&mut self.0, ctx, request);
async move {
match resp.await? {
#response_ident::#camel_case_idents(msg) => std::result::Result::Ok(msg),
_ => unreachable!(),
}
}
}
)*
}
}
}
}
impl<'a> ToTokens for ServiceGenerator<'a> {
fn to_tokens(&self, output: &mut TokenStream2) {
output.extend(vec![
self.trait_service(),
self.struct_server(),
self.impl_serve_for_server(),
self.enum_request(),
self.enum_response(),
self.enum_response_future(),
self.impl_debug_for_response_future(),
self.impl_future_for_response_future(),
self.struct_client(),
self.impl_from_for_client(),
self.impl_client_new(),
self.impl_client_rpc_methods(),
])
}
}
fn snake_to_camel(ident_str: &str) -> String {
let mut camel_ty = String::with_capacity(ident_str.len());
let mut last_char_was_underscore = true;
for c in ident_str.chars() {
match c {
'_' => last_char_was_underscore = true,
c if last_char_was_underscore => {
camel_ty.extend(c.to_uppercase());
last_char_was_underscore = false;
}
c => camel_ty.extend(c.to_lowercase()),
}
}
camel_ty.shrink_to_fit();
camel_ty
}
#[test]
fn snake_to_camel_basic() {
assert_eq!(snake_to_camel("abc_def"), "AbcDef");
}
#[test]
fn snake_to_camel_underscore_suffix() {
assert_eq!(snake_to_camel("abc_def_"), "AbcDef");
}
#[test]
fn snake_to_camel_underscore_prefix() {
assert_eq!(snake_to_camel("_abc_def"), "AbcDef");
}
#[test]
fn snake_to_camel_underscore_consecutive() {
assert_eq!(snake_to_camel("abc__def"), "AbcDef");
}
#[test]
fn snake_to_camel_capital_in_middle() {
assert_eq!(snake_to_camel("aBc_dEf"), "AbcDef");
}

144
plugins/tests/server.rs Normal file
View File

@@ -0,0 +1,144 @@
use assert_type_eq::assert_type_eq;
use futures::Future;
use std::pin::Pin;
use tarpc::context;
// these need to be out here rather than inside the function so that the
// assert_type_eq macro can pick them up.
#[tarpc::service]
trait Foo {
async fn two_part(s: String, i: i32) -> (String, i32);
async fn bar(s: String) -> String;
async fn baz();
}
#[test]
fn type_generation_works() {
#[tarpc::server]
impl Foo for () {
async fn two_part(self, _: context::Context, s: String, i: i32) -> (String, i32) {
(s, i)
}
async fn bar(self, _: context::Context, s: String) -> String {
s
}
async fn baz(self, _: context::Context) {}
}
// the assert_type_eq macro can only be used once per block.
{
assert_type_eq!(
<() as Foo>::TwoPartFut,
Pin<Box<dyn Future<Output = (String, i32)> + Send>>
);
}
{
assert_type_eq!(
<() as Foo>::BarFut,
Pin<Box<dyn Future<Output = String> + Send>>
);
}
{
assert_type_eq!(
<() as Foo>::BazFut,
Pin<Box<dyn Future<Output = ()> + Send>>
);
}
}
#[allow(non_camel_case_types)]
#[test]
fn raw_idents_work() {
type r#yield = String;
#[tarpc::service]
trait r#trait {
async fn r#await(r#struct: r#yield, r#enum: i32) -> (r#yield, i32);
async fn r#fn(r#impl: r#yield) -> r#yield;
async fn r#async();
}
#[tarpc::server]
impl r#trait for () {
async fn r#await(
self,
_: context::Context,
r#struct: r#yield,
r#enum: i32,
) -> (r#yield, i32) {
(r#struct, r#enum)
}
async fn r#fn(self, _: context::Context, r#impl: r#yield) -> r#yield {
r#impl
}
async fn r#async(self, _: context::Context) {}
}
}
#[test]
fn syntax() {
#[tarpc::service]
trait Syntax {
#[deny(warnings)]
#[allow(non_snake_case)]
async fn TestCamelCaseDoesntConflict();
async fn hello() -> String;
#[doc = "attr"]
async fn attr(s: String) -> String;
async fn no_args_no_return();
async fn no_args() -> ();
async fn one_arg(one: String) -> i32;
async fn two_args_no_return(one: String, two: u64);
async fn two_args(one: String, two: u64) -> String;
async fn no_args_ret_error() -> i32;
async fn one_arg_ret_error(one: String) -> String;
async fn no_arg_implicit_return_error();
#[doc = "attr"]
async fn one_arg_implicit_return_error(one: String);
}
#[tarpc::server]
impl Syntax for () {
#[deny(warnings)]
#[allow(non_snake_case)]
async fn TestCamelCaseDoesntConflict(self, _: context::Context) {}
async fn hello(self, _: context::Context) -> String {
String::new()
}
async fn attr(self, _: context::Context, _s: String) -> String {
String::new()
}
async fn no_args_no_return(self, _: context::Context) {}
async fn no_args(self, _: context::Context) -> () {}
async fn one_arg(self, _: context::Context, _one: String) -> i32 {
0
}
async fn two_args_no_return(self, _: context::Context, _one: String, _two: u64) {}
async fn two_args(self, _: context::Context, _one: String, _two: u64) -> String {
String::new()
}
async fn no_args_ret_error(self, _: context::Context) -> i32 {
0
}
async fn one_arg_ret_error(self, _: context::Context, _one: String) -> String {
String::new()
}
async fn no_arg_implicit_return_error(self, _: context::Context) {}
async fn one_arg_implicit_return_error(self, _: context::Context, _one: String) {}
}
}

85
plugins/tests/service.rs Normal file
View File

@@ -0,0 +1,85 @@
use tarpc::context;
#[test]
fn att_service_trait() {
use futures::future::{ready, Ready};
#[tarpc::service]
trait Foo {
async fn two_part(s: String, i: i32) -> (String, i32);
async fn bar(s: String) -> String;
async fn baz();
}
impl Foo for () {
type TwoPartFut = Ready<(String, i32)>;
fn two_part(self, _: context::Context, s: String, i: i32) -> Self::TwoPartFut {
ready((s, i))
}
type BarFut = Ready<String>;
fn bar(self, _: context::Context, s: String) -> Self::BarFut {
ready(s)
}
type BazFut = Ready<()>;
fn baz(self, _: context::Context) -> Self::BazFut {
ready(())
}
}
}
#[allow(non_camel_case_types)]
#[test]
fn raw_idents() {
use futures::future::{ready, Ready};
type r#yield = String;
#[tarpc::service]
trait r#trait {
async fn r#await(r#struct: r#yield, r#enum: i32) -> (r#yield, i32);
async fn r#fn(r#impl: r#yield) -> r#yield;
async fn r#async();
}
impl r#trait for () {
type AwaitFut = Ready<(r#yield, i32)>;
fn r#await(self, _: context::Context, r#struct: r#yield, r#enum: i32) -> Self::AwaitFut {
ready((r#struct, r#enum))
}
type FnFut = Ready<r#yield>;
fn r#fn(self, _: context::Context, r#impl: r#yield) -> Self::FnFut {
ready(r#impl)
}
type AsyncFut = Ready<()>;
fn r#async(self, _: context::Context) -> Self::AsyncFut {
ready(())
}
}
}
#[test]
fn syntax() {
#[tarpc::service]
trait Syntax {
#[deny(warnings)]
#[allow(non_snake_case)]
async fn TestCamelCaseDoesntConflict();
async fn hello() -> String;
#[doc = "attr"]
async fn attr(s: String) -> String;
async fn no_args_no_return();
async fn no_args() -> ();
async fn one_arg(one: String) -> i32;
async fn two_args_no_return(one: String, two: u64);
async fn two_args(one: String, two: u64) -> String;
async fn no_args_ret_error() -> i32;
async fn one_arg_ret_error(one: String) -> String;
async fn no_arg_implicit_return_error();
#[doc = "attr"]
async fn one_arg_implicit_return_error(one: String);
}
}

View File

@@ -1,23 +1,72 @@
[package]
name = "tarpc"
version = "0.5.1"
version = "0.22.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]
default = []
serde1 = ["tarpc-plugins/serde1", "serde", "serde/derive"]
tokio1 = []
serde-transport = ["tokio-serde", "tokio-util/codec"]
tcp = ["tokio/net", "tokio/stream"]
full = ["serde1", "tokio1", "serde-transport", "tcp"]
[badges]
travis-ci = { repository = "google/tarpc" }
[dependencies]
bincode = "0.6"
log = "0.3"
scoped-pool = "1.0"
serde = "0.8"
unix_socket = "0.5"
anyhow = "1.0"
fnv = "1.0"
futures = "0.3"
humantime = "1.0"
log = "0.4"
pin-project = "0.4.17"
rand = "0.7"
tokio = { version = "0.2", features = ["time"] }
serde = { optional = true, version = "1.0", features = ["derive"] }
static_assertions = "1.1.0"
tarpc-plugins = { path = "../plugins", version = "0.8" }
tokio-util = { optional = true, version = "0.3" }
tokio-serde = { optional = true, version = "0.6" }
[dev-dependencies]
lazy_static = "0.2"
env_logger = "0.3"
tempdir = "0.3"
assert_matches = "1.0"
bincode = "1.3"
bytes = { version = "0.5", features = ["serde"] }
env_logger = "0.6"
flate2 = "1.0.16"
futures = "0.3"
humantime = "1.0"
log = "0.4"
pin-utils = "0.1.0-alpha"
serde_bytes = "0.11"
tokio = { version = "0.2", features = ["full"] }
tokio-serde = { version = "0.6", features = ["json", "bincode"] }
trybuild = "1.0"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[[example]]
name = "server_calling_server"
required-features = ["full"]
[[example]]
name = "readme"
required-features = ["full"]
[[example]]
name = "pubsub"
required-features = ["full"]

1
tarpc/README.md Symbolic link
View File

@@ -0,0 +1 @@
../README.md

1
tarpc/clippy.toml Normal file
View File

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

View File

@@ -0,0 +1,130 @@
use flate2::{read::DeflateDecoder, write::DeflateEncoder, Compression};
use futures::{Sink, SinkExt, Stream, StreamExt, TryStreamExt};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
use std::{io, io::Read, io::Write};
use tarpc::{
client, context,
serde_transport::tcp,
server::{BaseChannel, Channel},
};
use tokio_serde::formats::Bincode;
/// Type of compression that should be enabled on the request. The transport is free to ignore this.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
pub enum CompressionAlgorithm {
Deflate,
}
#[derive(Debug, Deserialize, Serialize)]
pub enum CompressedMessage<T> {
Uncompressed(T),
Compressed {
algorithm: CompressionAlgorithm,
payload: ByteBuf,
},
}
#[derive(Deserialize, Serialize)]
enum CompressionType {
Uncompressed,
Compressed,
}
async fn compress<T>(message: T) -> io::Result<CompressedMessage<T>>
where
T: Serialize,
{
let message = serialize(message)?;
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&message).unwrap();
let compressed = encoder.finish()?;
Ok(CompressedMessage::Compressed {
algorithm: CompressionAlgorithm::Deflate,
payload: ByteBuf::from(compressed),
})
}
async fn decompress<T>(message: CompressedMessage<T>) -> io::Result<T>
where
for<'a> T: Deserialize<'a>,
{
match message {
CompressedMessage::Compressed { algorithm, payload } => {
if algorithm != CompressionAlgorithm::Deflate {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Compression algorithm {:?} not supported", algorithm),
));
}
let mut deflater = DeflateDecoder::new(payload.as_slice());
let mut payload = ByteBuf::new();
deflater.read_to_end(&mut payload)?;
let message = deserialize(payload)?;
Ok(message)
}
CompressedMessage::Uncompressed(message) => Ok(message),
}
}
fn serialize<T: Serialize>(t: T) -> io::Result<ByteBuf> {
bincode::serialize(&t)
.map(ByteBuf::from)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}
fn deserialize<D>(message: ByteBuf) -> io::Result<D>
where
for<'a> D: Deserialize<'a>,
{
bincode::deserialize(message.as_ref()).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}
fn add_compression<In, Out>(
transport: impl Stream<Item = io::Result<CompressedMessage<In>>>
+ Sink<CompressedMessage<Out>, Error = io::Error>,
) -> impl Stream<Item = io::Result<In>> + Sink<Out, Error = io::Error>
where
Out: Serialize,
for<'a> In: Deserialize<'a>,
{
transport.with(compress).and_then(decompress)
}
#[tarpc::service]
pub trait World {
async fn hello(name: String) -> String;
}
#[derive(Clone, Debug)]
struct HelloServer;
#[tarpc::server]
impl World for HelloServer {
async fn hello(self, _: context::Context, name: String) -> String {
format!("Hey, {}!", name)
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut incoming = tcp::listen("localhost:0", Bincode::default).await?;
let addr = incoming.local_addr();
tokio::spawn(async move {
let transport = incoming.next().await.unwrap().unwrap();
BaseChannel::with_defaults(add_compression(transport))
.respond_with(HelloServer.serve())
.execute()
.await;
});
let transport = tcp::connect(addr, Bincode::default).await?;
let mut client =
WorldClient::new(client::Config::default(), add_compression(transport)).spawn()?;
println!(
"{}",
client.hello(context::current(), "friend".into()).await?
);
Ok(())
}

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

@@ -0,0 +1,344 @@
// 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.
/// - The PubSub server sets up TCP listeners on 2 ports, the "subscriber" port and the "publisher"
/// port. Because both publishers and subscribers initiate their connections to the PubSub
/// server, the server requires no prior knowledge of either publishers or subscribers.
///
/// - Subscribers connect to the server on the server's "subscriber" port. Once a connection is
/// established, the server acts as the client of the Subscriber service, initially requesting
/// the topics the subscriber is interested in, and subsequently sending topical messages to the
/// subscriber.
///
/// - Publishers connect to the server on the "publisher" port and, once connected, they send
/// topical messages via Publisher service to the server. The server then broadcasts each
/// messages to all clients subscribed to the topic of that message.
///
/// Subscriber Publisher PubSub Server
/// T1 | | |
/// T2 |-----Connect------------------------------------------------------>|
/// T3 | | |
/// T2 |<-------------------------------------------------------Topics-----|
/// T2 |-----(OK) Topics-------------------------------------------------->|
/// T3 | | |
/// T4 | |-----Connect-------------------->|
/// T5 | | |
/// T6 | |-----Publish-------------------->|
/// T7 | | |
/// T8 |<------------------------------------------------------Receive-----|
/// T9 |-----(OK) Receive------------------------------------------------->|
/// T10 | | |
/// T11 | |<--------------(OK) Publish------|
use anyhow::anyhow;
use futures::{
channel::oneshot,
future::{self, AbortHandle},
prelude::*,
};
use log::info;
use publisher::Publisher as _;
use std::{
collections::HashMap,
io,
net::SocketAddr,
sync::{Arc, Mutex, RwLock},
};
use subscriber::Subscriber as _;
use tarpc::{
client, context,
serde_transport::tcp,
server::{self, Channel},
};
use tokio::net::ToSocketAddrs;
use tokio_serde::formats::Json;
pub mod subscriber {
#[tarpc::service]
pub trait Subscriber {
async fn topics() -> Vec<String>;
async fn receive(topic: String, message: String);
}
}
pub mod publisher {
#[tarpc::service]
pub trait Publisher {
async fn publish(topic: String, message: String);
}
}
#[derive(Clone, Debug)]
struct Subscriber {
local_addr: SocketAddr,
topics: Vec<String>,
}
#[tarpc::server]
impl subscriber::Subscriber for Subscriber {
async fn topics(self, _: context::Context) -> Vec<String> {
self.topics.clone()
}
async fn receive(self, _: context::Context, topic: String, message: String) {
info!(
"[{}] received message on topic '{}': {}",
self.local_addr, topic, message
);
}
}
struct SubscriberHandle(AbortHandle);
impl Drop for SubscriberHandle {
fn drop(&mut self) {
self.0.abort();
}
}
impl Subscriber {
async fn connect(
publisher_addr: impl ToSocketAddrs,
topics: Vec<String>,
) -> anyhow::Result<SubscriberHandle> {
let publisher = tcp::connect(publisher_addr, Json::default).await?;
let local_addr = publisher.local_addr()?;
let mut handler = server::BaseChannel::with_defaults(publisher)
.respond_with(Subscriber { local_addr, topics }.serve());
// The first request is for the topics being subscriibed to.
match handler.next().await {
Some(init_topics) => init_topics?.await,
None => {
return Err(anyhow!(
"[{}] Server never initialized the subscriber.",
local_addr
))
}
};
let (handler, abort_handle) = future::abortable(handler.execute());
tokio::spawn(async move {
match handler.await {
Ok(()) | Err(future::Aborted) => info!("[{}] subscriber shutdown.", local_addr),
}
});
Ok(SubscriberHandle(abort_handle))
}
}
#[derive(Debug)]
struct Subscription {
subscriber: subscriber::SubscriberClient,
topics: Vec<String>,
}
#[derive(Clone, Debug)]
struct Publisher {
clients: Arc<Mutex<HashMap<SocketAddr, Subscription>>>,
subscriptions: Arc<RwLock<HashMap<String, HashMap<SocketAddr, subscriber::SubscriberClient>>>>,
}
struct PublisherAddrs {
publisher: SocketAddr,
subscriptions: SocketAddr,
}
impl Publisher {
async fn start(self) -> io::Result<PublisherAddrs> {
let mut connecting_publishers = tcp::listen("localhost:0", Json::default).await?;
let publisher_addrs = PublisherAddrs {
publisher: connecting_publishers.local_addr(),
subscriptions: self.clone().start_subscription_manager().await?,
};
info!("[{}] listening for publishers.", publisher_addrs.publisher);
tokio::spawn(async move {
// Because this is just an example, we know there will only be one publisher. In more
// realistic code, this would be a loop to continually accept new publisher
// connections.
let publisher = connecting_publishers.next().await.unwrap().unwrap();
info!("[{}] publisher connected.", publisher.peer_addr().unwrap());
server::BaseChannel::with_defaults(publisher)
.respond_with(self.serve())
.execute()
.await
});
Ok(publisher_addrs)
}
async fn start_subscription_manager(mut self) -> io::Result<SocketAddr> {
let mut connecting_subscribers = tcp::listen("localhost:0", Json::default)
.await?
.filter_map(|r| future::ready(r.ok()));
let new_subscriber_addr = connecting_subscribers.get_ref().local_addr();
info!("[{}] listening for subscribers.", new_subscriber_addr);
tokio::spawn(async move {
while let Some(conn) = connecting_subscribers.next().await {
let subscriber_addr = conn.peer_addr().unwrap();
let tarpc::client::NewClient {
client: subscriber,
dispatch,
} = subscriber::SubscriberClient::new(client::Config::default(), conn);
let (ready_tx, ready) = oneshot::channel();
self.clone()
.start_subscriber_gc(subscriber_addr, dispatch, ready);
// Populate the topics
self.initialize_subscription(subscriber_addr, subscriber)
.await;
// Signal that initialization is done.
ready_tx.send(()).unwrap();
}
});
Ok(new_subscriber_addr)
}
async fn initialize_subscription(
&mut self,
subscriber_addr: SocketAddr,
mut subscriber: subscriber::SubscriberClient,
) {
// Populate the topics
if let Ok(topics) = subscriber.topics(context::current()).await {
self.clients.lock().unwrap().insert(
subscriber_addr,
Subscription {
subscriber: subscriber.clone(),
topics: topics.clone(),
},
);
info!("[{}] subscribed to topics: {:?}", subscriber_addr, topics);
let mut subscriptions = self.subscriptions.write().unwrap();
for topic in topics {
subscriptions
.entry(topic)
.or_insert_with(HashMap::new)
.insert(subscriber_addr, subscriber.clone());
}
}
}
fn start_subscriber_gc(
self,
subscriber_addr: SocketAddr,
client_dispatch: impl Future<Output = anyhow::Result<()>> + Send + 'static,
subscriber_ready: oneshot::Receiver<()>,
) {
tokio::spawn(async move {
if let Err(e) = client_dispatch.await {
info!(
"[{}] subscriber connection broken: {:?}",
subscriber_addr, e
)
}
// Don't clean up the subscriber until initialization is done.
let _ = subscriber_ready.await;
if let Some(subscription) = self.clients.lock().unwrap().remove(&subscriber_addr) {
info!(
"[{} unsubscribing from topics: {:?}",
subscriber_addr, subscription.topics
);
let mut subscriptions = self.subscriptions.write().unwrap();
for topic in subscription.topics {
let subscribers = subscriptions.get_mut(&topic).unwrap();
subscribers.remove(&subscriber_addr);
if subscribers.is_empty() {
subscriptions.remove(&topic);
}
}
}
});
}
}
#[tarpc::server]
impl publisher::Publisher for Publisher {
async fn publish(self, _: context::Context, topic: String, message: String) {
info!("received message to publish.");
let mut subscribers = match self.subscriptions.read().unwrap().get(&topic) {
None => return,
Some(subscriptions) => subscriptions.clone(),
};
let mut publications = Vec::new();
for client in subscribers.values_mut() {
publications.push(client.receive(context::current(), topic.clone(), message.clone()));
}
// Ignore failing subscribers. In a real pubsub, you'd want to continually retry until
// subscribers ack. Of course, a lot would be different in a real pubsub :)
for response in future::join_all(publications).await {
if let Err(e) = response {
info!("failed to broadcast to subscriber: {}", e);
}
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init();
let clients = Arc::new(Mutex::new(HashMap::new()));
let addrs = Publisher {
clients,
subscriptions: Arc::new(RwLock::new(HashMap::new())),
}
.start()
.await?;
let _subscriber0 = Subscriber::connect(
addrs.subscriptions,
vec!["calculus".into(), "cool shorts".into()],
)
.await?;
let _subscriber1 = Subscriber::connect(
addrs.subscriptions,
vec!["cool shorts".into(), "history".into()],
)
.await?;
let mut publisher = publisher::PublisherClient::new(
client::Config::default(),
tcp::connect(addrs.publisher, Json::default).await?,
)
.spawn()?;
publisher
.publish(context::current(), "calculus".into(), "sqrt(2)".into())
.await?;
publisher
.publish(
context::current(),
"cool shorts".into(),
"hello to all".into(),
)
.await?;
publisher
.publish(context::current(), "history".into(), "napoleon".to_string())
.await?;
drop(_subscriber0);
publisher
.publish(
context::current(),
"cool shorts".into(),
"hello to who?".into(),
)
.await?;
info!("done.");
Ok(())
}

79
tarpc/examples/readme.rs Normal file
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.
use futures::{
future::{self, Ready},
prelude::*,
};
use std::io;
use tarpc::{
client, context,
server::{BaseChannel, Channel},
};
use tokio_serde::formats::Json;
/// 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]
pub trait World {
async fn hello(name: String) -> String;
}
/// This is the type that implements the generated World trait. It is the business logic
/// and is used to start the server.
#[derive(Clone)]
struct HelloServer;
impl World 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))
}
}
#[tokio::main]
async fn main() -> io::Result<()> {
// tarpc_json_transport is provided by the associated crate json_transport. It makes it
// easy to start up a serde-powered JSON serialization strategy over TCP.
let mut transport = tarpc::serde_transport::tcp::listen("localhost:0", Json::default).await?;
let addr = transport.local_addr();
let server = async move {
// For this example, we're just going to wait for one connection.
let client = transport.next().await.unwrap().unwrap();
// `Channel` is a trait representing a server-side connection. It is a trait to allow
// for some channels to be instrumented: for example, to track the number of open connections.
// BaseChannel is the most basic channel, simply wrapping a transport with no added
// functionality.
BaseChannel::with_defaults(client)
// serve_world is generated by the tarpc::service attribute. It takes as input any type
// implementing the generated World trait.
.respond_with(HelloServer.serve())
.execute()
.await;
};
tokio::spawn(server);
let transport = tarpc::serde_transport::tcp::connect(addr, Json::default).await?;
// WorldClient is generated by the tarpc::service attribute. It has a constructor `new` that
// takes a config and any Transport as input.
let mut client = WorldClient::new(client::Config::default(), transport).spawn()?;
// The client has an RPC method for each RPC defined in the annotated trait. 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 = client.hello(context::current(), "Stim".to_string()).await?;
eprintln!("{}", hello);
Ok(())
}

View File

@@ -0,0 +1,92 @@
// 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::{add::Add as AddService, double::Double as DoubleService};
use futures::{future, prelude::*};
use std::io;
use tarpc::{
client, context,
server::{Handler, Server},
};
use tokio_serde::formats::Json;
pub mod add {
#[tarpc::service]
pub trait Add {
/// Add two ints together.
async fn add(x: i32, y: i32) -> i32;
}
}
pub mod double {
#[tarpc::service]
pub trait Double {
/// 2 * x
async fn double(x: i32) -> Result<i32, String>;
}
}
#[derive(Clone)]
struct AddServer;
#[tarpc::server]
impl AddService for AddServer {
async fn add(self, _: context::Context, x: i32, y: i32) -> i32 {
x + y
}
}
#[derive(Clone)]
struct DoubleServer {
add_client: add::AddClient,
}
#[tarpc::server]
impl DoubleService for DoubleServer {
async fn double(mut self, _: context::Context, x: i32) -> Result<i32, String> {
self.add_client
.add(context::current(), x, x)
.await
.map_err(|e| e.to_string())
}
}
#[tokio::main]
async fn main() -> io::Result<()> {
env_logger::init();
let add_listener = tarpc::serde_transport::tcp::listen("localhost:0", Json::default)
.await?
.filter_map(|r| future::ready(r.ok()));
let addr = add_listener.get_ref().local_addr();
let add_server = Server::default()
.incoming(add_listener)
.take(1)
.respond_with(AddServer.serve());
tokio::spawn(add_server);
let to_add_server = tarpc::serde_transport::tcp::connect(addr, Json::default).await?;
let add_client = add::AddClient::new(client::Config::default(), to_add_server).spawn()?;
let double_listener = tarpc::serde_transport::tcp::listen("localhost:0", Json::default)
.await?
.filter_map(|r| future::ready(r.ok()));
let addr = double_listener.get_ref().local_addr();
let double_server = tarpc::Server::default()
.incoming(double_listener)
.take(1)
.respond_with(DoubleServer { add_client }.serve());
tokio::spawn(double_server);
let to_double_server = tarpc::serde_transport::tcp::connect(addr, Json::default).await?;
let mut double_client =
double::DoubleClient::new(client::Config::default(), to_double_server).spawn()?;
for i in 1..=5 {
eprintln!("{:?}", double_client.double(context::current(), i).await?);
}
Ok(())
}

View File

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

View File

@@ -1,66 +1,290 @@
// 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.
//! *Disclaimer*: This is not an official Google product.
//!
//! Example usage:
//! 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 context switching between different languages.
//!
//! Some other features of tarpc:
//! - Pluggable transport: any type impling `Stream<Item = Request> + Sink<Response>` can be
//! used as a transport to connect the client and server.
//! - `Send + 'static` optional: if the transport doesn't require it, neither does tarpc!
//! - Cascading cancellation: dropping a request will send a cancellation message to the server.
//! The server will cease any unfinished work on the request, subsequently cancelling any of its
//! own requests, repeating for the entire chain of transitive dependencies.
//! - Configurable deadlines and deadline propagation: request deadlines default to 10s if
//! unspecified. The server will automatically cease work when the deadline has passed. Any
//! requests sent by the server that use the request context will propagate the request deadline.
//! For example, if a server is handling a request with a 10s deadline, does 2s of work, then
//! sends a request to another server, that server will see an 8s deadline.
//! - Serde serialization: enabling the `serde1` Cargo feature will make service requests and
//! responses `Serialize + Deserialize`. It's entirely optional, though: in-memory transports can
//! be used, as well, so the price of serialization doesn't have to be paid when it's not needed.
//!
//! ## Usage
//! Add to your `Cargo.toml` dependencies:
//!
//! ```toml
//! tarpc = "0.22.0"
//! ```
//! #[macro_use] extern crate tarpc;
//! mod my_server {
//! service! {
//! rpc hello(name: String) -> String;
//! rpc add(x: i32, y: i32) -> i32;
//! }
//! }
//!
//! use self::my_server::*;
//! use std::time::Duration;
//! The `tarpc::service` attribute expands to a collection of items that form an rpc service.
//! These generated types make it easy and ergonomic to write servers with less boilerplate.
//! Simply implement the generated service trait, and you're off to the races!
//!
//! 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
//! }
//! }
//! ## Example
//!
//! 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();
//! For this example, in addition to tarpc, also add two other dependencies to
//! your `Cargo.toml`:
//!
//! ```toml
//! futures = "0.3"
//! tokio = "0.2"
//! ```
//!
//! In the following example, we use an in-process channel for communication between
//! client and server. In real code, you will likely communicate over the network.
//! For a more real-world example, see [example-service](example-service).
//!
//! First, let's set up the dependencies and service definition.
//!
//! ```rust
//! # extern crate futures;
//!
//! use futures::{
//! 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]
//! trait World {
//! /// Returns a greeting for name.
//! async fn hello(name: String) -> String;
//! }
//! ```
//!
//! This service definition generates a trait called `World`. Next we need to
//! implement it for our Server struct.
//!
//! ```rust
//! # extern crate futures;
//! # use futures::{
//! # 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]
//! # trait World {
//! # /// Returns a greeting for name.
//! # async fn hello(name: String) -> String;
//! # }
//! // This is the type that implements the generated World trait. It is the business logic
//! // and is used to start the server.
//! #[derive(Clone)]
//! struct HelloServer;
//!
//! impl World 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))
//! }
//! }
//! ```
//!
//! Lastly let's write our `main` that will start the server. While this example uses an
//! [in-process channel](rpc::transport::channel), tarpc also ships a generic [`serde_transport`]
//! behind the `serde-transport` feature, with additional [TCP](serde_transport::tcp) functionality
//! available behind the `tcp` feature.
//!
//! ```rust
//! # extern crate futures;
//! # use futures::{
//! # 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]
//! # trait World {
//! # /// Returns a greeting for name.
//! # async fn hello(name: String) -> String;
//! # }
//! # // This is the type that implements the generated World trait. It is the business logic
//! # // and is used to start the server.
//! # #[derive(Clone)]
//! # struct HelloServer;
//! # impl World 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))
//! # }
//! # }
//! #[tokio::main]
//! async fn main() -> io::Result<()> {
//! let (client_transport, server_transport) = tarpc::transport::channel::unbounded();
//!
//! let server = server::new(server::Config::default())
//! // incoming() takes a stream of transports such as would be returned by
//! // TcpListener::incoming (but a stream instead of an iterator).
//! .incoming(stream::once(future::ready(server_transport)))
//! .respond_with(HelloServer.serve());
//!
//! tokio::spawn(server);
//!
//! // WorldClient is generated by the macro. It has a constructor `new` that takes a config and
//! // any Transport as input
//! let mut client = WorldClient::new(client::Config::default(), client_transport).spawn()?;
//!
//! // The client has an RPC method for each RPC defined in the annotated trait. 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 = client.hello(context::current(), "Stim".to_string()).await?;
//!
//! println!("{}", hello);
//!
//! Ok(())
//! }
//! ```
//!
//! ## 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)]
#![allow(clippy::type_complexity)]
#![cfg_attr(docsrs, feature(doc_cfg))]
extern crate serde;
extern crate bincode;
#[macro_use]
extern crate log;
extern crate scoped_pool;
extern crate unix_socket;
pub mod rpc;
pub use rpc::*;
macro_rules! pos {
() => (concat!(file!(), ":", line!()))
}
#[cfg(feature = "serde-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde-transport")))]
pub mod serde_transport;
/// Provides the tarpc client and server, which implements the tarpc protocol.
/// The protocol is defined by the implementation.
pub mod protocol;
pub mod trace;
/// Provides the macro used for constructing rpc services and client stubs.
pub mod macros;
/// The main macro that creates RPC services.
///
/// Rpc methods are specified, mirroring trait syntax:
///
/// ```
/// #[tarpc::service]
/// trait Service {
/// /// Say hello
/// async fn hello(name: String) -> String;
/// }
/// ```
///
/// Attributes can be attached to each rpc. These attributes
/// will then be attached to the generated service traits'
/// corresponding `fn`s, as well as to the client stubs' RPCs.
///
/// The following items are expanded in the enclosing module:
///
/// * `trait Service` -- defines the RPC service.
/// * `fn serve` -- turns a service impl into a request handler.
/// * `Client` -- a client stub with a fn for each RPC.
/// * `fn new_stub` -- creates a new Client stub.
pub use tarpc_plugins::service;
/// Provides transport traits and implementations.
pub mod transport;
pub use protocol::{Config, Error, Result, ServeHandle};
/// A utility macro that can be used for RPC server implementations.
///
/// Syntactic sugar to make using async functions in the server implementation
/// easier. It does this by rewriting code like this, which would normally not
/// compile because async functions are disallowed in trait implementations:
///
/// ```rust
/// # use tarpc::context;
/// # use std::net::SocketAddr;
/// #[tarpc::service]
/// trait World {
/// async fn hello(name: String) -> String;
/// }
///
/// #[derive(Clone)]
/// struct HelloServer(SocketAddr);
///
/// #[tarpc::server]
/// impl World for HelloServer {
/// async fn hello(self, _: context::Context, name: String) -> String {
/// format!("Hello, {}! You are connected from {:?}.", name, self.0)
/// }
/// }
/// ```
///
/// Into code like this, which matches the service trait definition:
///
/// ```rust
/// # use tarpc::context;
/// # use std::pin::Pin;
/// # use futures::Future;
/// # use std::net::SocketAddr;
/// #[derive(Clone)]
/// struct HelloServer(SocketAddr);
///
/// #[tarpc::service]
/// trait World {
/// async fn hello(name: String) -> String;
/// }
///
/// impl World for HelloServer {
/// type HelloFut = Pin<Box<dyn Future<Output = String> + Send>>;
///
/// fn hello(self, _: context::Context, name: String) -> Pin<Box<dyn Future<Output = String>
/// + Send>> {
/// Box::pin(async move {
/// format!("Hello, {}! You are connected from {:?}.", name, self.0)
/// })
/// }
/// }
/// ```
///
/// Note that this won't touch functions unless they have been annotated with
/// `async`, meaning that this should not break existing code.
pub use tarpc_plugins::server;

View File

@@ -1,632 +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.
/// 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};
}
}
// Required because if-let can't be used with irrefutable patterns, so it needs
// to be special cased.
#[doc(hidden)]
#[macro_export]
macro_rules! client_methods {
(
{ $(#[$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,)*))));
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)*););
}
/// The main macro that creates RPC services.
///
/// Rpc methods are specified, mirroring trait syntax:
///
/// ```
/// # #[macro_use] extern crate tarpc;
/// # fn main() {}
/// # service! {
/// #[doc="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.
///
/// 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
///
/// **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 {
// Entry point
(
$(
$(#[$attr:meta])*
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* ) $(-> $out:ty)*;
)*
) => {
service! {{
$(
$(#[$attr])*
rpc $fn_name( $( $arg : $in_ ),* ) $(-> $out)*;
)*
}}
};
// Pattern for when the next rpc has an implicit unit return type
(
{
$(#[$attr:meta])*
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* );
$( $unexpanded:tt )*
}
$( $expanded:tt )*
) => {
service! {
{ $( $unexpanded )* }
$( $expanded )*
$(#[$attr])*
rpc $fn_name( $( $arg : $in_ ),* ) -> ();
}
};
// Pattern for when the next rpc has an explicit return type
(
{
$(#[$attr:meta])*
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* ) -> $out:ty;
$( $unexpanded:tt )*
}
$( $expanded:tt )*
) => {
service! {
{ $( $unexpanded )* }
$( $expanded )*
$(#[$attr])*
rpc $fn_name( $( $arg : $in_ ),* ) -> $out;
}
};
// Pattern for when all return types have been expanded
(
{ } // none left to expand
$(
$(#[$attr:meta])*
rpc $fn_name:ident ( $( $arg:ident : $in_:ty ),* ) -> $out:ty;
)*
) => {
#[doc="Defines the RPC service"]
pub trait Service: Send + Sync + Sized {
$(
$(#[$attr])*
fn $fn_name(&self, $($arg:$in_),*) -> $out;
)*
#[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<P, S> Service for P
where P: Send + Sync + Sized + 'static + ::std::ops::Deref<Target=S>,
S: Service
{
$(
$(#[$attr])*
fn $fn_name(&self, $($arg:$in_),*) -> $out {
Service::$fn_name(&**self, $($arg),*)
}
)*
}
#[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)]
// 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;
}
}
}
#[cfg(test)]
mod functional_test {
extern crate env_logger;
extern crate tempdir;
use transport::unix::UnixTransport;
service! {
rpc add(x: i32, y: i32) -> i32;
rpc hey(name: String) -> String;
}
struct Server;
impl Service for Server {
fn add(&self, x: i32, y: i32) -> i32 {
x + y
}
fn hey(&self, name: String) -> String {
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();
}
#[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();
}
#[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);
}
}
}

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)
}
}

148
tarpc/src/rpc.rs Normal file
View File

@@ -0,0 +1,148 @@
// 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)]
//! 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, trace, transport::sealed::Transport};
use anyhow::Context as _;
use futures::task::*;
use std::{fmt::Display, io, time::SystemTime};
/// A message from a client to a server.
#[derive(Debug)]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ClientMessage<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 trace context associates the message with a specific chain of causally-related actions,
/// possibly orchestrated across many distributed systems.
#[cfg_attr(feature = "serde", serde(default))]
trace_context: trace::Context,
/// The ID of the request to cancel.
request_id: u64,
},
}
/// A request from a client to a server.
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
pub struct Request<T> {
/// Trace context, deadline, and other cross-cutting concerns.
pub context: context::Context,
/// Uniquely identifies the request across all requests sent over a single channel.
pub id: u64,
/// The request body.
pub message: T,
}
/// A response from a server to a client.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
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(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
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.context.deadline
}
}
pub(crate) type PollIo<T> = Poll<Option<io::Result<T>>>;
pub(crate) trait PollContext<T> {
fn context<C>(self, context: C) -> Poll<Option<anyhow::Result<T>>>
where
C: Display + Send + Sync + 'static;
fn with_context<C, F>(self, f: F) -> Poll<Option<anyhow::Result<T>>>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C;
}
impl<T> PollContext<T> for PollIo<T> {
fn context<C>(self, context: C) -> Poll<Option<anyhow::Result<T>>>
where
C: Display + Send + Sync + 'static,
{
self.map(|o| o.map(|r| r.context(context)))
}
fn with_context<C, F>(self, f: F) -> Poll<Option<anyhow::Result<T>>>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
{
self.map(|o| o.map(|r| r.with_context(f)))
}
}

155
tarpc/src/rpc/client.rs Normal file
View File

@@ -0,0 +1,155 @@
// 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;
use futures::prelude::*;
use std::io;
/// Provides a [`Client`] backed by a transport.
pub mod channel;
pub use channel::{new, 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.
#[derive(Clone, Debug)]
#[non_exhaustive]
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,
}
}
}
/// A channel and dispatch pair. The dispatch drives the sending and receiving of requests
/// and must be polled continuously or spawned.
#[derive(Debug)]
pub struct NewClient<C, D> {
/// The new client.
pub client: C,
/// The client's dispatch.
pub dispatch: D,
}
impl<C, D, E> NewClient<C, D>
where
D: Future<Output = Result<(), E>> + Send + 'static,
E: std::fmt::Display,
{
/// Helper method to spawn the dispatch on the default executor.
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub fn spawn(self) -> io::Result<C> {
use log::error;
let dispatch = self
.dispatch
.unwrap_or_else(move |e| error!("Connection broken: {}", e));
tokio::spawn(dispatch);
Ok(self.client)
}
}

View File

@@ -0,0 +1,907 @@
// 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,
trace::SpanId,
util::{Compact, TimeUntil},
ClientMessage, PollContext, PollIo, Request, Response, Transport,
};
use fnv::FnvHashMap;
use futures::{
channel::{mpsc, oneshot},
prelude::*,
ready,
stream::Fuse,
task::*,
};
use log::{debug, info, trace};
use pin_project::{pin_project, pinned_drop};
use std::{
io,
pin::Pin,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
use super::{Config, NewClient};
/// 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>,
}
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(),
}
}
}
/// A future returned by [`Channel::send`] that resolves to a server response.
#[pin_project]
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
struct Send<'a, Req, Resp> {
#[pin]
fut: MapOkDispatchResponse<SendMapErrConnectionReset<'a, Req, Resp>, Resp>,
}
type SendMapErrConnectionReset<'a, Req, Resp> = MapErrConnectionReset<
futures::sink::Send<'a, mpsc::Sender<DispatchRequest<Req, Resp>>, DispatchRequest<Req, Resp>>,
>;
impl<'a, Req, Resp> Future for Send<'a, Req, Resp> {
type Output = io::Result<DispatchResponse<Resp>>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.as_mut().project().fut.poll(cx)
}
}
/// A future returned by [`Channel::call`] that resolves to a server response.
#[pin_project]
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
pub struct Call<'a, Req, Resp> {
#[pin]
fut: tokio::time::Timeout<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>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let resp = ready!(self.as_mut().project().fut.poll(cx));
Poll::Ready(match resp {
Ok(resp) => resp,
Err(tokio::time::Elapsed { .. }) => Err(io::Error::new(
io::ErrorKind::TimedOut,
"Client dropped expired request.".to_string(),
)),
})
}
}
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(&mut self, mut ctx: context::Context, request: Req) -> Send<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 (response_completion, response) = oneshot::channel();
let cancellation = self.cancellation.clone();
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
Send {
fut: MapOkDispatchResponse::new(
MapErrConnectionReset::new(self.to_dispatch.send(DispatchRequest {
ctx,
request_id,
request,
response_completion,
})),
DispatchResponse {
response,
complete: false,
request_id,
cancellation,
ctx,
},
),
}
}
/// Sends a request to the dispatch task to forward to the server, returning a [`Future`] that
/// resolves to the response.
pub fn call(&mut self, ctx: context::Context, request: Req) -> Call<Req, Resp> {
let timeout = ctx.deadline.time_until();
trace!(
"[{}] Queuing request with timeout {:?}.",
ctx.trace_id(),
timeout,
);
Call {
fut: tokio::time::timeout(timeout, AndThenIdent::new(self.send(ctx, request))),
}
}
}
/// A server response that is completed by request dispatch when the corresponding response
/// arrives off the wire.
#[pin_project(PinnedDrop)]
#[derive(Debug)]
struct DispatchResponse<Resp> {
response: oneshot::Receiver<Response<Resp>>,
ctx: context::Context,
complete: bool,
cancellation: RequestCancellation,
request_id: u64,
}
impl<Resp> Future for DispatchResponse<Resp> {
type Output = io::Result<Resp>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<Resp>> {
let resp = ready!(self.response.poll_unpin(cx));
self.complete = true;
Poll::Ready(match resp {
Ok(resp) => Ok(resp.message?),
Err(oneshot::Canceled) => {
// The oneshot is Canceled when the dispatch task ends. In that case,
// there's nothing listening on the other side, so there's no point in
// propagating cancellation.
Err(io::Error::from(io::ErrorKind::ConnectionReset))
}
})
}
}
// Cancels the request when dropped, if not already complete.
#[pinned_drop]
impl<Resp> PinnedDrop for DispatchResponse<Resp> {
fn drop(mut self: Pin<&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.close();
let request_id = self.request_id;
self.cancellation.cancel(request_id);
}
}
}
/// Returns a channel and dispatcher that manages the lifecycle of requests initiated by the
/// channel.
pub fn new<Req, Resp, C>(
config: Config,
transport: C,
) -> NewClient<Channel<Req, Resp>, RequestDispatch<Req, Resp, C>>
where
C: Transport<ClientMessage<Req>, Response<Resp>>,
{
let (to_dispatch, pending_requests) = mpsc::channel(config.pending_request_buffer);
let (cancellation, canceled_requests) = cancellations();
let canceled_requests = canceled_requests.fuse();
NewClient {
client: Channel {
to_dispatch,
cancellation,
next_request_id: Arc::new(AtomicU64::new(0)),
},
dispatch: RequestDispatch {
config,
canceled_requests,
transport: transport.fuse(),
in_flight_requests: FnvHashMap::default(),
pending_requests: pending_requests.fuse(),
},
}
}
/// Handles the lifecycle of requests, writing requests to the wire, managing cancellations,
/// and dispatching responses to the appropriate channel.
#[pin_project]
#[derive(Debug)]
pub struct RequestDispatch<Req, Resp, C> {
/// Writes requests to the wire and reads responses off the wire.
#[pin]
transport: Fuse<C>,
/// Requests waiting to be written to the wire.
#[pin]
pending_requests: Fuse<mpsc::Receiver<DispatchRequest<Req, Resp>>>,
/// Requests that were dropped.
#[pin]
canceled_requests: Fuse<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,
}
impl<Req, Resp, C> RequestDispatch<Req, Resp, C>
where
C: Transport<ClientMessage<Req>, Response<Resp>>,
{
fn pump_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> PollIo<()> {
Poll::Ready(
match ready!(self.as_mut().project().transport.poll_next(cx)?) {
Some(response) => {
self.complete(response);
Some(Ok(()))
}
None => None,
},
)
}
fn pump_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> PollIo<()> {
enum ReceiverStatus {
NotReady,
Closed,
}
let pending_requests_status = match self.as_mut().poll_next_request(cx)? {
Poll::Ready(Some(dispatch_request)) => {
self.as_mut().write_request(dispatch_request)?;
return Poll::Ready(Some(Ok(())));
}
Poll::Ready(None) => ReceiverStatus::Closed,
Poll::Pending => ReceiverStatus::NotReady,
};
let canceled_requests_status = match self.as_mut().poll_next_cancellation(cx)? {
Poll::Ready(Some((context, request_id))) => {
self.as_mut().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.as_mut().project().transport.poll_flush(cx)?);
Poll::Ready(None)
}
(ReceiverStatus::NotReady, _) | (_, ReceiverStatus::NotReady) => {
// No more messages to process, so flush any messages buffered in the transport.
ready!(self.as_mut().project().transport.poll_flush(cx)?);
// 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(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> PollIo<DispatchRequest<Req, Resp>> {
if self.as_mut().project().in_flight_requests.len() >= self.config.max_in_flight_requests {
info!(
"At in-flight request capacity ({}/{}).",
self.as_mut().project().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.as_mut().project().transport.poll_ready(cx)? {
// We can't yield a request-to-be-sent before the transport is capable of buffering it.
ready!(self.as_mut().project().transport.poll_flush(cx)?);
}
loop {
match ready!(self.as_mut().project().pending_requests.poll_next_unpin(cx)) {
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 => return Poll::Ready(None),
}
}
}
/// Yields the next pending cancellation, and, if one is ready, cancels the associated request.
fn poll_next_cancellation(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> PollIo<(context::Context, u64)> {
while let Poll::Pending = self.as_mut().project().transport.poll_ready(cx)? {
ready!(self.as_mut().project().transport.poll_flush(cx)?);
}
loop {
let cancellation = self
.as_mut()
.project()
.canceled_requests
.poll_next_unpin(cx);
match ready!(cancellation) {
Some(request_id) => {
if let Some(in_flight_data) = self
.as_mut()
.project()
.in_flight_requests
.remove(&request_id)
{
self.as_mut().project().in_flight_requests.compact(0.1);
debug!("[{}] Removed request.", in_flight_data.ctx.trace_id());
return Poll::Ready(Some(Ok((in_flight_data.ctx, request_id))));
}
}
None => return Poll::Ready(None),
}
}
}
fn write_request(
mut self: Pin<&mut Self>,
dispatch_request: DispatchRequest<Req, Resp>,
) -> io::Result<()> {
let request_id = dispatch_request.request_id;
let request = ClientMessage::Request(Request {
id: request_id,
message: dispatch_request.request,
context: context::Context {
deadline: dispatch_request.ctx.deadline,
trace_context: dispatch_request.ctx.trace_context,
},
});
self.as_mut().project().transport.start_send(request)?;
self.as_mut().project().in_flight_requests.insert(
request_id,
InFlightData {
ctx: dispatch_request.ctx,
response_completion: dispatch_request.response_completion,
},
);
Ok(())
}
fn write_cancel(
mut self: Pin<&mut Self>,
context: context::Context,
request_id: u64,
) -> io::Result<()> {
let trace_id = *context.trace_id();
let cancel = ClientMessage::Cancel {
trace_context: context.trace_context,
request_id,
};
self.as_mut().project().transport.start_send(cancel)?;
trace!("[{}] Cancel message sent.", trace_id);
Ok(())
}
/// Sends a server response to the client task that initiated the associated request.
fn complete(mut self: Pin<&mut Self>, response: Response<Resp>) -> bool {
if let Some(in_flight_data) = self
.as_mut()
.project()
.in_flight_requests
.remove(&response.request_id)
{
self.as_mut().project().in_flight_requests.compact(0.1);
trace!("[{}] Received response.", in_flight_data.ctx.trace_id());
let _ = in_flight_data.response_completion.send(response);
return true;
}
debug!(
"No in-flight request found for request_id = {}.",
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
C: Transport<ClientMessage<Req>, Response<Resp>>,
{
type Output = anyhow::Result<()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<anyhow::Result<()>> {
loop {
match (
self.as_mut()
.pump_read(cx)
.context("failed to read from transport")?,
self.as_mut()
.pump_write(cx)
.context("failed to write to transport")?,
) {
(Poll::Ready(None), _) => {
info!("Shutdown: read half closed, so shutting down.");
return Poll::Ready(Ok(()));
}
(read, Poll::Ready(None)) => {
if self.as_mut().project().in_flight_requests.is_empty() {
info!("Shutdown: write half closed, and no requests in flight.");
return Poll::Ready(Ok(()));
}
info!(
"Shutdown: write half closed, and {} requests in flight.",
self.as_mut().project().in_flight_requests.len()
);
match read {
Poll::Ready(Some(())) => continue,
_ => return Poll::Pending,
}
}
(Poll::Ready(Some(())), _) | (_, Poll::Ready(Some(()))) => {}
_ => 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>>,
}
#[derive(Debug)]
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>, cx: &mut Context<'_>) -> Poll<Option<u64>> {
self.0.poll_next_unpin(cx)
}
}
#[pin_project]
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
struct MapErrConnectionReset<Fut> {
#[pin]
future: Fut,
finished: Option<()>,
}
impl<Fut> MapErrConnectionReset<Fut> {
fn new(future: Fut) -> MapErrConnectionReset<Fut> {
MapErrConnectionReset {
future,
finished: Some(()),
}
}
}
impl<Fut> Future for MapErrConnectionReset<Fut>
where
Fut: TryFuture,
{
type Output = io::Result<Fut::Ok>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project().future.try_poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(result) => {
self.project().finished.take().expect(
"MapErrConnectionReset must not be polled after it returned `Poll::Ready`",
);
Poll::Ready(result.map_err(|_| io::Error::from(io::ErrorKind::ConnectionReset)))
}
}
}
}
#[pin_project]
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
struct MapOkDispatchResponse<Fut, Resp> {
#[pin]
future: Fut,
response: Option<DispatchResponse<Resp>>,
}
impl<Fut, Resp> MapOkDispatchResponse<Fut, Resp> {
fn new(future: Fut, response: DispatchResponse<Resp>) -> MapOkDispatchResponse<Fut, Resp> {
MapOkDispatchResponse {
future,
response: Some(response),
}
}
}
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>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project().future.try_poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(result) => {
let response = self
.as_mut()
.project()
.response
.take()
.expect("MapOk must not be polled after it returned `Poll::Ready`");
Poll::Ready(result.map(|_| response))
}
}
}
}
#[pin_project]
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
struct AndThenIdent<Fut1, Fut2> {
#[pin]
try_chain: TryChain<Fut1, Fut2>,
}
impl<Fut1, Fut2> AndThenIdent<Fut1, Fut2>
where
Fut1: TryFuture<Ok = Fut2>,
Fut2: TryFuture,
{
/// 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(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project().try_chain.poll(cx, |result| match result {
Ok(ok) => TryChainAction::Future(ok),
Err(err) => TryChainAction::Output(Err(err)),
})
}
}
#[pin_project(project = TryChainProj)]
#[must_use = "futures do nothing unless polled"]
#[derive(Debug)]
enum TryChain<Fut1, Fut2> {
First(#[pin] Fut1),
Second(#[pin] 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>(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
f: F,
) -> Poll<Result<Fut2::Ok, Fut2::Error>>
where
F: FnOnce(Result<Fut1::Ok, Fut1::Error>) -> TryChainAction<Fut2>,
{
let mut f = Some(f);
loop {
let output = match self.as_mut().project() {
TryChainProj::First(fut1) => {
// Poll the first future
match fut1.try_poll(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(output) => output,
}
}
TryChainProj::Second(fut2) => {
// Poll the second future
return fut2.try_poll(cx);
}
TryChainProj::Empty => {
panic!("future must not be polled after it returned `Poll::Ready`");
}
};
self.set(TryChain::Empty); // Drop fut1
let f = f.take().unwrap();
match f(output) {
TryChainAction::Future(fut2) => self.set(TryChain::Second(fut2)),
TryChainAction::Output(output) => return Poll::Ready(output),
}
}
}
}
#[cfg(test)]
mod tests {
use super::{
cancellations, CanceledRequests, Channel, DispatchResponse, RequestCancellation,
RequestDispatch,
};
use crate::{
client::Config,
context,
transport::{self, channel::UnboundedChannel},
ClientMessage, Response,
};
use fnv::FnvHashMap;
use futures::{
channel::{mpsc, oneshot},
prelude::*,
task::*,
};
use std::{pin::Pin, sync::atomic::AtomicU64, sync::Arc};
#[tokio::test(threaded_scheduler)]
async fn dispatch_response_cancels_on_drop() {
let (cancellation, mut canceled_requests) = cancellations();
let (_, response) = oneshot::channel();
drop(DispatchResponse::<u32> {
response,
cancellation,
complete: false,
request_id: 3,
ctx: context::current(),
});
// resp's drop() is run, which should send a cancel message.
assert_eq!(canceled_requests.0.try_next().unwrap(), Some(3));
}
#[tokio::test(threaded_scheduler)]
async fn stage_request() {
let (mut dispatch, mut channel, _server_channel) = set_up();
let dispatch = Pin::new(&mut dispatch);
let cx = &mut Context::from_waker(&noop_waker_ref());
let _resp = send_request(&mut channel, "hi").await;
let req = dispatch.poll_next_request(cx).ready();
assert!(req.is_some());
let req = req.unwrap();
assert_eq!(req.request_id, 0);
assert_eq!(req.request, "hi".to_string());
}
// Regression test for https://github.com/google/tarpc/issues/220
#[tokio::test(threaded_scheduler)]
async fn stage_request_channel_dropped_doesnt_panic() {
let (mut dispatch, mut channel, mut server_channel) = set_up();
let mut dispatch = Pin::new(&mut dispatch);
let cx = &mut Context::from_waker(&noop_waker_ref());
let _ = send_request(&mut channel, "hi").await;
drop(channel);
assert!(dispatch.as_mut().poll(cx).is_ready());
send_response(
&mut server_channel,
Response {
request_id: 0,
message: Ok("hello".into()),
},
)
.await;
dispatch.await.unwrap();
}
#[tokio::test(threaded_scheduler)]
async fn stage_request_response_future_dropped_is_canceled_before_sending() {
let (mut dispatch, mut channel, _server_channel) = set_up();
let dispatch = Pin::new(&mut dispatch);
let cx = &mut Context::from_waker(&noop_waker_ref());
let _ = send_request(&mut channel, "hi").await;
// Drop the channel so polling returns none if no requests are currently ready.
drop(channel);
// 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.
assert!(dispatch.poll_next_request(cx).ready().is_none());
}
#[tokio::test(threaded_scheduler)]
async fn stage_request_response_future_dropped_is_canceled_after_sending() {
let (mut dispatch, mut channel, _server_channel) = set_up();
let cx = &mut Context::from_waker(&noop_waker_ref());
let mut dispatch = Pin::new(&mut dispatch);
let req = send_request(&mut channel, "hi").await;
assert!(dispatch.as_mut().pump_write(cx).ready().is_some());
assert!(!dispatch.as_mut().project().in_flight_requests.is_empty());
// Test that a request future dropped after it's processed by dispatch will cause the request
// to be removed from the in-flight request map.
drop(req);
if let Poll::Ready(Some(_)) = dispatch.as_mut().poll_next_cancellation(cx).unwrap() {
// ok
} else {
panic!("Expected request to be cancelled")
};
assert!(dispatch.project().in_flight_requests.is_empty());
}
#[tokio::test(threaded_scheduler)]
async fn stage_request_response_closed_skipped() {
let (mut dispatch, mut channel, _server_channel) = set_up();
let dispatch = Pin::new(&mut dispatch);
let cx = &mut Context::from_waker(&noop_waker_ref());
// 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 mut resp = send_request(&mut channel, "hi").await;
resp.response.close();
assert!(dispatch.poll_next_request(cx).is_pending());
}
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).fuse(),
in_flight_requests: FnvHashMap::default(),
config: Config::default(),
};
let cancellation = RequestCancellation(cancel_tx);
let channel = Channel {
to_dispatch,
cancellation,
next_request_id: Arc::new(AtomicU64::new(0)),
};
(dispatch, channel, server_channel)
}
async fn send_request(
channel: &mut Channel<String, String>,
request: &str,
) -> DispatchResponse<String> {
channel
.send(context::current(), request.to_string())
.await
.unwrap()
}
async fn send_response(
channel: &mut UnboundedChannel<ClientMessage<String>, Response<String>>,
response: Response<String>,
) {
channel.send(response).await.unwrap();
}
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,
{
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"),
}
}
}
}

63
tarpc/src/rpc/context.rs Normal file
View File

@@ -0,0 +1,63 @@
// 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 crate::trace::{self, TraceId};
use static_assertions::assert_impl_all;
use std::time::{Duration, SystemTime};
/// 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]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
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.
#[cfg_attr(
feature = "serde1",
serde(serialize_with = "crate::util::serde::serialize_epoch_secs")
)]
#[cfg_attr(
feature = "serde1",
serde(deserialize_with = "crate::util::serde::deserialize_epoch_secs")
)]
#[cfg_attr(feature = "serde1", serde(default = "ten_seconds_from_now"))]
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,
}
assert_impl_all!(Context: Send, Sync);
#[cfg(feature = "serde1")]
fn ten_seconds_from_now() -> SystemTime {
SystemTime::now() + Duration::from_secs(10)
}
/// 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
}
}

707
tarpc/src/rpc/server.rs Normal file
View File

@@ -0,0 +1,707 @@
// 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, trace, util::Compact, util::TimeUntil, ClientMessage, PollIo, Request, Response,
ServerError, Transport,
};
use fnv::FnvHashMap;
use futures::{
channel::mpsc,
future::{AbortHandle, AbortRegistration, Abortable},
prelude::*,
ready,
stream::Fuse,
task::*,
};
use humantime::format_rfc3339;
use log::{debug, trace};
use pin_project::pin_project;
use std::{fmt, hash::Hash, io, marker::PhantomData, pin::Pin, time::SystemTime};
use tokio::time::Timeout;
mod filter;
#[cfg(test)]
mod testing;
mod throttle;
pub use self::{
filter::ChannelFilter,
throttle::{Throttler, ThrottlerStream},
};
/// 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.
#[derive(Clone, Debug)]
pub struct Config {
/// 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 {
pending_response_buffer: 100,
}
}
}
impl Config {
/// Returns a channel backed by `transport` and configured with `self`.
pub fn channel<Req, Resp, T>(self, transport: T) -> BaseChannel<Req, Resp, T>
where
T: Transport<Response<Resp>, ClientMessage<Req>>,
{
BaseChannel::new(self, transport)
}
}
/// 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 server channels.
pub fn incoming<S, T>(self, listener: S) -> impl Stream<Item = BaseChannel<Req, Resp, T>>
where
S: Stream<Item = T>,
T: Transport<Response<Resp>, ClientMessage<Req>>,
{
listener.map(move |t| BaseChannel::new(self.config.clone(), t))
}
}
/// Basically a Fn(Req) -> impl Future<Output = Resp>;
pub trait Serve<Req>: Sized + Clone {
/// Type of response.
type Resp;
/// Type of response future.
type Fut: Future<Output = Self::Resp>;
/// Responds to a single request.
fn serve(self, ctx: context::Context, req: Req) -> Self::Fut;
}
impl<Req, Resp, Fut, F> Serve<Req> for F
where
F: FnOnce(context::Context, Req) -> Fut + Clone,
Fut: Future<Output = Resp>,
{
type Resp = Resp;
type Fut = Fut;
fn serve(self, ctx: context::Context, req: Req) -> Self::Fut {
self(ctx, req)
}
}
/// A utility trait enabling a stream to fluently chain a request handler.
pub trait Handler<C>
where
Self: Sized + Stream<Item = C>,
C: Channel,
{
/// Enforces channel per-key limits.
fn max_channels_per_key<K, KF>(self, n: u32, keymaker: KF) -> filter::ChannelFilter<Self, K, KF>
where
K: fmt::Display + Eq + Hash + Clone + Unpin,
KF: Fn(&C) -> K,
{
ChannelFilter::new(self, n, keymaker)
}
/// Caps the number of concurrent requests per channel.
fn max_concurrent_requests_per_channel(self, n: usize) -> ThrottlerStream<Self> {
ThrottlerStream::new(self, n)
}
/// Responds to all requests with [`server::serve`](Serve).
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
fn respond_with<S>(self, server: S) -> Running<Self, S>
where
S: Serve<C::Req, Resp = C::Resp>,
{
Running {
incoming: self,
server,
}
}
}
impl<S, C> Handler<C> for S
where
S: Sized + Stream<Item = C>,
C: Channel,
{
}
/// BaseChannel lifts a Transport to a Channel by tracking in-flight requests.
#[pin_project]
#[derive(Debug)]
pub struct BaseChannel<Req, Resp, T> {
config: Config,
/// Writes responses to the wire and reads requests off the wire.
#[pin]
transport: Fuse<T>,
/// Number of requests currently being responded to.
in_flight_requests: FnvHashMap<u64, AbortHandle>,
/// Types the request and response.
ghost: PhantomData<(Req, Resp)>,
}
impl<Req, Resp, T> BaseChannel<Req, Resp, T>
where
T: Transport<Response<Resp>, ClientMessage<Req>>,
{
/// Creates a new channel backed by `transport` and configured with `config`.
pub fn new(config: Config, transport: T) -> Self {
BaseChannel {
config,
transport: transport.fuse(),
in_flight_requests: FnvHashMap::default(),
ghost: PhantomData,
}
}
/// Creates a new channel backed by `transport` and configured with the defaults.
pub fn with_defaults(transport: T) -> Self {
Self::new(Config::default(), transport)
}
/// Returns the inner transport over which messages are sent and received.
pub fn get_ref(&self) -> &T {
self.transport.get_ref()
}
/// Returns the inner transport over which messages are sent and received.
pub fn get_pin_ref(self: Pin<&mut Self>) -> Pin<&mut T> {
self.project().transport.get_pin_mut()
}
fn cancel_request(mut self: 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
.as_mut()
.project()
.in_flight_requests
.remove(&request_id)
{
self.as_mut().project().in_flight_requests.compact(0.1);
cancel_handle.abort();
let remaining = self.as_mut().project().in_flight_requests.len();
trace!(
"[{}] Request canceled. In-flight requests = {}",
trace_context.trace_id,
remaining,
);
} else {
trace!(
"[{}] Received cancellation, but response handler \
is already complete.",
trace_context.trace_id,
);
}
}
}
/// The server end of an open connection with a client, streaming in requests from, and sinking
/// responses to, the client.
///
/// Channels are free to somewhat rely on the assumption that all in-flight requests are eventually
/// either [cancelled](BaseChannel::cancel_request) or [responded to](Sink::start_send). Safety cannot
/// rely on this assumption, but it is best for `Channel` users to always account for all outstanding
/// requests.
pub trait Channel
where
Self: Transport<Response<<Self as Channel>::Resp>, Request<<Self as Channel>::Req>>,
{
/// Type of request item.
type Req;
/// Type of response sink item.
type Resp;
/// Configuration of the channel.
fn config(&self) -> &Config;
/// Returns the number of in-flight requests over this channel.
fn in_flight_requests(self: Pin<&mut Self>) -> usize;
/// Caps the number of concurrent requests.
fn max_concurrent_requests(self, n: usize) -> Throttler<Self>
where
Self: Sized,
{
Throttler::new(self, n)
}
/// Tells the Channel that request with ID `request_id` is being handled.
/// The request will be tracked until a response with the same ID is sent
/// to the Channel.
fn start_request(self: Pin<&mut Self>, request_id: u64) -> AbortRegistration;
/// Respond to requests coming over the channel with `f`. Returns a future that drives the
/// responses and resolves when the connection is closed.
fn respond_with<S>(self, server: S) -> ClientHandler<Self, S>
where
S: Serve<Self::Req, Resp = Self::Resp>,
Self: Sized,
{
let (responses_tx, responses) = mpsc::channel(self.config().pending_response_buffer);
let responses = responses.fuse();
ClientHandler {
channel: self,
server,
pending_responses: responses,
responses_tx,
}
}
}
impl<Req, Resp, T> Stream for BaseChannel<Req, Resp, T>
where
T: Transport<Response<Resp>, ClientMessage<Req>>,
{
type Item = io::Result<Request<Req>>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
loop {
match ready!(self.as_mut().project().transport.poll_next(cx)?) {
Some(message) => match message {
ClientMessage::Request(request) => {
return Poll::Ready(Some(Ok(request)));
}
ClientMessage::Cancel {
trace_context,
request_id,
} => {
self.as_mut().cancel_request(&trace_context, request_id);
}
},
None => return Poll::Ready(None),
}
}
}
}
impl<Req, Resp, T> Sink<Response<Resp>> for BaseChannel<Req, Resp, T>
where
T: Transport<Response<Resp>, ClientMessage<Req>>,
{
type Error = io::Error;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.project().transport.poll_ready(cx)
}
fn start_send(mut self: Pin<&mut Self>, response: Response<Resp>) -> Result<(), Self::Error> {
if self
.as_mut()
.project()
.in_flight_requests
.remove(&response.request_id)
.is_some()
{
self.as_mut().project().in_flight_requests.compact(0.1);
}
self.project().transport.start_send(response)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.project().transport.poll_flush(cx)
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.project().transport.poll_close(cx)
}
}
impl<Req, Resp, T> AsRef<T> for BaseChannel<Req, Resp, T> {
fn as_ref(&self) -> &T {
self.transport.get_ref()
}
}
impl<Req, Resp, T> Channel for BaseChannel<Req, Resp, T>
where
T: Transport<Response<Resp>, ClientMessage<Req>>,
{
type Req = Req;
type Resp = Resp;
fn config(&self) -> &Config {
&self.config
}
fn in_flight_requests(mut self: Pin<&mut Self>) -> usize {
self.as_mut().project().in_flight_requests.len()
}
fn start_request(self: Pin<&mut Self>, request_id: u64) -> AbortRegistration {
let (abort_handle, abort_registration) = AbortHandle::new_pair();
assert!(self
.project()
.in_flight_requests
.insert(request_id, abort_handle)
.is_none());
abort_registration
}
}
/// A running handler serving all requests coming over a channel.
#[pin_project]
#[derive(Debug)]
pub struct ClientHandler<C, S>
where
C: Channel,
{
#[pin]
channel: C,
/// Responses waiting to be written to the wire.
#[pin]
pending_responses: Fuse<mpsc::Receiver<(context::Context, Response<C::Resp>)>>,
/// Handed out to request handlers to fan in responses.
#[pin]
responses_tx: mpsc::Sender<(context::Context, Response<C::Resp>)>,
/// Server
server: S,
}
impl<C, S> ClientHandler<C, S>
where
C: Channel,
S: Serve<C::Req, Resp = C::Resp>,
{
/// Returns the inner channel over which messages are sent and received.
pub fn get_pin_channel(self: Pin<&mut Self>) -> Pin<&mut C> {
self.project().channel
}
fn pump_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> PollIo<RequestHandler<S::Fut, C::Resp>> {
match ready!(self.as_mut().project().channel.poll_next(cx)?) {
Some(request) => Poll::Ready(Some(Ok(self.handle_request(request)))),
None => Poll::Ready(None),
}
}
fn pump_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
read_half_closed: bool,
) -> PollIo<()> {
match self.as_mut().poll_next_response(cx)? {
Poll::Ready(Some((ctx, response))) => {
trace!(
"[{}] Staging response. In-flight requests = {}.",
ctx.trace_id(),
self.as_mut().project().channel.in_flight_requests(),
);
self.as_mut().project().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.as_mut().project().channel.poll_flush(cx)?);
Poll::Ready(None)
}
Poll::Pending => {
// No more requests to process, so flush any requests buffered in the transport.
ready!(self.as_mut().project().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.as_mut().project().channel.in_flight_requests() == 0 {
Poll::Ready(None)
} else {
Poll::Pending
}
}
}
}
fn poll_next_response(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> PollIo<(context::Context, Response<C::Resp>)> {
// Ensure there's room to write a response.
while let Poll::Pending = self.as_mut().project().channel.poll_ready(cx)? {
ready!(self.as_mut().project().channel.poll_flush(cx)?);
}
match ready!(self.as_mut().project().pending_responses.poll_next(cx)) {
Some((ctx, response)) => Poll::Ready(Some(Ok((ctx, response)))),
None => {
// This branch likely won't happen, since the ClientHandler is holding a Sender.
Poll::Ready(None)
}
}
}
fn handle_request(
mut self: Pin<&mut Self>,
request: Request<C::Req>,
) -> RequestHandler<S::Fut, C::Resp> {
let request_id = request.id;
let deadline = request.context.deadline;
let timeout = deadline.time_until();
trace!(
"[{}] Received request with deadline {} (timeout {:?}).",
request.context.trace_id(),
format_rfc3339(deadline),
timeout,
);
let ctx = request.context;
let request = request.message;
let response = self.as_mut().project().server.clone().serve(ctx, request);
let response = Resp {
state: RespState::PollResp,
request_id,
ctx,
deadline,
f: tokio::time::timeout(timeout, response),
response: None,
response_tx: self.as_mut().project().responses_tx.clone(),
};
let abort_registration = self.as_mut().project().channel.start_request(request_id);
RequestHandler {
resp: Abortable::new(response, abort_registration),
}
}
}
/// A future fulfilling a single client request.
#[pin_project]
#[derive(Debug)]
pub struct RequestHandler<F, R> {
#[pin]
resp: Abortable<Resp<F, R>>,
}
impl<F, R> Future for RequestHandler<F, R>
where
F: Future<Output = R>,
{
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let _ = ready!(self.project().resp.poll(cx));
Poll::Ready(())
}
}
#[pin_project]
#[derive(Debug)]
struct Resp<F, R> {
state: RespState,
request_id: u64,
ctx: context::Context,
deadline: SystemTime,
#[pin]
f: Timeout<F>,
response: Option<Response<R>>,
#[pin]
response_tx: mpsc::Sender<(context::Context, Response<R>)>,
}
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
enum RespState {
PollResp,
PollReady,
PollFlush,
}
impl<F, R> Future for Resp<F, R>
where
F: Future<Output = R>,
{
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
loop {
match self.as_mut().project().state {
RespState::PollResp => {
let result = ready!(self.as_mut().project().f.poll(cx));
*self.as_mut().project().response = Some(Response {
request_id: self.request_id,
message: match result {
Ok(message) => Ok(message),
Err(tokio::time::Elapsed { .. }) => {
debug!(
"[{}] Response did not complete before deadline of {}s.",
self.ctx.trace_id(),
format_rfc3339(self.deadline)
);
// No point in responding, since the client will have dropped the
// request.
Err(ServerError {
kind: io::ErrorKind::TimedOut,
detail: Some(format!(
"Response did not complete before deadline of {}s.",
format_rfc3339(self.deadline)
)),
})
}
},
});
*self.as_mut().project().state = RespState::PollReady;
}
RespState::PollReady => {
let ready = ready!(self.as_mut().project().response_tx.poll_ready(cx));
if ready.is_err() {
return Poll::Ready(());
}
let resp = (self.ctx, self.as_mut().project().response.take().unwrap());
if self
.as_mut()
.project()
.response_tx
.start_send(resp)
.is_err()
{
return Poll::Ready(());
}
*self.as_mut().project().state = RespState::PollFlush;
}
RespState::PollFlush => {
let ready = ready!(self.as_mut().project().response_tx.poll_flush(cx));
if ready.is_err() {
return Poll::Ready(());
}
return Poll::Ready(());
}
}
}
}
}
impl<C, S> Stream for ClientHandler<C, S>
where
C: Channel,
S: Serve<C::Req, Resp = C::Resp>,
{
type Item = io::Result<RequestHandler<S::Fut, C::Resp>>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
let read = self.as_mut().pump_read(cx)?;
let read_closed = if let Poll::Ready(None) = read {
true
} else {
false
};
match (read, self.as_mut().pump_write(cx, read_closed)?) {
(Poll::Ready(None), Poll::Ready(None)) => {
return Poll::Ready(None);
}
(Poll::Ready(Some(request_handler)), _) => {
return Poll::Ready(Some(Ok(request_handler)));
}
(_, Poll::Ready(Some(()))) => {}
_ => {
return Poll::Pending;
}
}
}
}
}
// Send + 'static execution helper methods.
impl<C, S> ClientHandler<C, S>
where
C: Channel + 'static,
C::Req: Send + 'static,
C::Resp: Send + 'static,
S: Serve<C::Req, Resp = C::Resp> + Send + 'static,
S::Fut: Send + 'static,
{
/// Runs the client handler until completion by [spawning](tokio::spawn) each
/// request handler onto the default executor.
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub fn execute(self) -> impl Future<Output = ()> {
self.try_for_each(|request_handler| async {
tokio::spawn(request_handler);
Ok(())
})
.map_ok(|()| log::info!("ClientHandler finished."))
.unwrap_or_else(|e| log::info!("ClientHandler errored out: {}", e))
}
}
/// A future that drives the server by [spawning](tokio::spawn) channels and request handlers on the default
/// executor.
#[pin_project]
#[derive(Debug)]
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub struct Running<St, Se> {
#[pin]
incoming: St,
server: Se,
}
#[cfg(feature = "tokio1")]
impl<St, C, Se> Future for Running<St, Se>
where
St: Sized + Stream<Item = C>,
C: Channel + Send + 'static,
C::Req: Send + 'static,
C::Resp: Send + 'static,
Se: Serve<C::Req, Resp = C::Resp> + Send + 'static + Clone,
Se::Fut: Send + 'static,
{
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
while let Some(channel) = ready!(self.as_mut().project().incoming.poll_next(cx)) {
tokio::spawn(
channel
.respond_with(self.as_mut().project().server.clone())
.execute(),
);
}
log::info!("Server shutting down.");
Poll::Ready(())
}
}

View File

@@ -0,0 +1,471 @@
// 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::{self, Channel},
util::Compact,
};
use fnv::FnvHashMap;
use futures::{channel::mpsc, future::AbortRegistration, prelude::*, ready, stream::Fuse, task::*};
use log::{debug, info, trace};
use pin_project::pin_project;
use std::sync::{Arc, Weak};
use std::{
collections::hash_map::Entry, convert::TryInto, fmt, hash::Hash, marker::Unpin, pin::Pin,
};
/// A single-threaded filter that drops channels based on per-key limits.
#[pin_project]
#[derive(Debug)]
pub struct ChannelFilter<S, K, F>
where
K: Eq + Hash,
{
#[pin]
listener: Fuse<S>,
channels_per_key: u32,
#[pin]
dropped_keys: mpsc::UnboundedReceiver<K>,
#[pin]
dropped_keys_tx: mpsc::UnboundedSender<K>,
key_counts: FnvHashMap<K, Weak<Tracker<K>>>,
keymaker: F,
}
/// A channel that is tracked by a ChannelFilter.
#[pin_project]
#[derive(Debug)]
pub struct TrackedChannel<C, K> {
#[pin]
inner: C,
tracker: Arc<Tracker<K>>,
}
#[derive(Debug)]
struct Tracker<K> {
key: Option<K>,
dropped_keys: mpsc::UnboundedSender<K>,
}
impl<K> Drop for Tracker<K> {
fn drop(&mut self) {
// Don't care if the listener is dropped.
let _ = self.dropped_keys.unbounded_send(self.key.take().unwrap());
}
}
impl<C, K> Stream for TrackedChannel<C, K>
where
C: Stream,
{
type Item = <C as Stream>::Item;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
self.channel().poll_next(cx)
}
}
impl<C, I, K> Sink<I> for TrackedChannel<C, K>
where
C: Sink<I>,
{
type Error = C::Error;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.channel().poll_ready(cx)
}
fn start_send(self: Pin<&mut Self>, item: I) -> Result<(), Self::Error> {
self.channel().start_send(item)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.channel().poll_flush(cx)
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.channel().poll_close(cx)
}
}
impl<C, K> AsRef<C> for TrackedChannel<C, K> {
fn as_ref(&self) -> &C {
&self.inner
}
}
impl<C, K> Channel for TrackedChannel<C, K>
where
C: Channel,
{
type Req = C::Req;
type Resp = C::Resp;
fn config(&self) -> &server::Config {
self.inner.config()
}
fn in_flight_requests(self: Pin<&mut Self>) -> usize {
self.project().inner.in_flight_requests()
}
fn start_request(self: Pin<&mut Self>, request_id: u64) -> AbortRegistration {
self.project().inner.start_request(request_id)
}
}
impl<C, K> TrackedChannel<C, K> {
/// Returns the inner channel.
pub fn get_ref(&self) -> &C {
&self.inner
}
/// Returns the pinned inner channel.
fn channel<'a>(self: Pin<&'a mut Self>) -> Pin<&'a mut C> {
self.project().inner
}
}
impl<S, K, F> ChannelFilter<S, K, F>
where
K: Eq + Hash,
S: Stream,
F: Fn(&S::Item) -> K,
{
/// Sheds new channels to stay under configured limits.
pub(crate) fn new(listener: S, channels_per_key: u32, keymaker: F) -> Self {
let (dropped_keys_tx, dropped_keys) = mpsc::unbounded();
ChannelFilter {
listener: listener.fuse(),
channels_per_key,
dropped_keys,
dropped_keys_tx,
key_counts: FnvHashMap::default(),
keymaker,
}
}
}
impl<S, K, F> ChannelFilter<S, K, F>
where
S: Stream,
K: fmt::Display + Eq + Hash + Clone + Unpin,
F: Fn(&S::Item) -> K,
{
fn handle_new_channel(
mut self: Pin<&mut Self>,
stream: S::Item,
) -> Result<TrackedChannel<S::Item, K>, K> {
let key = (self.as_mut().keymaker)(&stream);
let tracker = self.as_mut().increment_channels_for_key(key.clone())?;
trace!(
"[{}] Opening channel ({}/{}) channels for key.",
key,
Arc::strong_count(&tracker),
self.as_mut().project().channels_per_key
);
Ok(TrackedChannel {
tracker,
inner: stream,
})
}
fn increment_channels_for_key(mut self: Pin<&mut Self>, key: K) -> Result<Arc<Tracker<K>>, K> {
let channels_per_key = self.channels_per_key;
let dropped_keys = self.dropped_keys_tx.clone();
let key_counts = &mut self.as_mut().project().key_counts;
match key_counts.entry(key.clone()) {
Entry::Vacant(vacant) => {
let tracker = Arc::new(Tracker {
key: Some(key),
dropped_keys,
});
vacant.insert(Arc::downgrade(&tracker));
Ok(tracker)
}
Entry::Occupied(mut o) => {
let count = o.get().strong_count();
if count >= channels_per_key.try_into().unwrap() {
info!(
"[{}] Opened max channels from key ({}/{}).",
key, count, channels_per_key
);
Err(key)
} else {
Ok(o.get().upgrade().unwrap_or_else(|| {
let tracker = Arc::new(Tracker {
key: Some(key),
dropped_keys,
});
*o.get_mut() = Arc::downgrade(&tracker);
tracker
}))
}
}
}
}
fn poll_listener(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<TrackedChannel<S::Item, K>, K>>> {
match ready!(self.as_mut().project().listener.poll_next_unpin(cx)) {
Some(codec) => Poll::Ready(Some(self.handle_new_channel(codec))),
None => Poll::Ready(None),
}
}
fn poll_closed_channels(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
match ready!(self.as_mut().project().dropped_keys.poll_next_unpin(cx)) {
Some(key) => {
debug!("All channels dropped for key [{}]", key);
self.as_mut().project().key_counts.remove(&key);
self.as_mut().project().key_counts.compact(0.1);
Poll::Ready(())
}
None => unreachable!("Holding a copy of closed_channels and didn't close it."),
}
}
}
impl<S, K, F> Stream for ChannelFilter<S, K, F>
where
S: Stream,
K: fmt::Display + Eq + Hash + Clone + Unpin,
F: Fn(&S::Item) -> K,
{
type Item = TrackedChannel<S::Item, K>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<TrackedChannel<S::Item, K>>> {
loop {
match (
self.as_mut().poll_listener(cx),
self.as_mut().poll_closed_channels(cx),
) {
(Poll::Ready(Some(Ok(channel))), _) => {
return Poll::Ready(Some(channel));
}
(Poll::Ready(Some(Err(_))), _) => {
continue;
}
(_, Poll::Ready(())) => continue,
(Poll::Pending, Poll::Pending) => return Poll::Pending,
(Poll::Ready(None), Poll::Pending) => {
trace!("Shutting down listener.");
return Poll::Ready(None);
}
}
}
}
}
#[cfg(test)]
fn ctx() -> Context<'static> {
use futures::task::*;
Context::from_waker(&noop_waker_ref())
}
#[test]
fn tracker_drop() {
use assert_matches::assert_matches;
let (tx, mut rx) = mpsc::unbounded();
Tracker {
key: Some(1),
dropped_keys: tx,
};
assert_matches!(rx.try_next(), Ok(Some(1)));
}
#[test]
fn tracked_channel_stream() {
use assert_matches::assert_matches;
use pin_utils::pin_mut;
let (chan_tx, chan) = mpsc::unbounded();
let (dropped_keys, _) = mpsc::unbounded();
let channel = TrackedChannel {
inner: chan,
tracker: Arc::new(Tracker {
key: Some(1),
dropped_keys,
}),
};
chan_tx.unbounded_send("test").unwrap();
pin_mut!(channel);
assert_matches!(channel.poll_next(&mut ctx()), Poll::Ready(Some("test")));
}
#[test]
fn tracked_channel_sink() {
use assert_matches::assert_matches;
use pin_utils::pin_mut;
let (chan, mut chan_rx) = mpsc::unbounded();
let (dropped_keys, _) = mpsc::unbounded();
let channel = TrackedChannel {
inner: chan,
tracker: Arc::new(Tracker {
key: Some(1),
dropped_keys,
}),
};
pin_mut!(channel);
assert_matches!(channel.as_mut().poll_ready(&mut ctx()), Poll::Ready(Ok(())));
assert_matches!(channel.as_mut().start_send("test"), Ok(()));
assert_matches!(channel.as_mut().poll_flush(&mut ctx()), Poll::Ready(Ok(())));
assert_matches!(chan_rx.try_next(), Ok(Some("test")));
}
#[test]
fn channel_filter_increment_channels_for_key() {
use assert_matches::assert_matches;
use pin_utils::pin_mut;
struct TestChannel {
key: &'static str,
}
let (_, listener) = mpsc::unbounded();
let filter = ChannelFilter::new(listener, 2, |chan: &TestChannel| chan.key);
pin_mut!(filter);
let tracker1 = filter.as_mut().increment_channels_for_key("key").unwrap();
assert_eq!(Arc::strong_count(&tracker1), 1);
let tracker2 = filter.as_mut().increment_channels_for_key("key").unwrap();
assert_eq!(Arc::strong_count(&tracker1), 2);
assert_matches!(filter.increment_channels_for_key("key"), Err("key"));
drop(tracker2);
assert_eq!(Arc::strong_count(&tracker1), 1);
}
#[test]
fn channel_filter_handle_new_channel() {
use assert_matches::assert_matches;
use pin_utils::pin_mut;
#[derive(Debug)]
struct TestChannel {
key: &'static str,
}
let (_, listener) = mpsc::unbounded();
let filter = ChannelFilter::new(listener, 2, |chan: &TestChannel| chan.key);
pin_mut!(filter);
let channel1 = filter
.as_mut()
.handle_new_channel(TestChannel { key: "key" })
.unwrap();
assert_eq!(Arc::strong_count(&channel1.tracker), 1);
let channel2 = filter
.as_mut()
.handle_new_channel(TestChannel { key: "key" })
.unwrap();
assert_eq!(Arc::strong_count(&channel1.tracker), 2);
assert_matches!(
filter.handle_new_channel(TestChannel { key: "key" }),
Err("key")
);
drop(channel2);
assert_eq!(Arc::strong_count(&channel1.tracker), 1);
}
#[test]
fn channel_filter_poll_listener() {
use assert_matches::assert_matches;
use pin_utils::pin_mut;
#[derive(Debug)]
struct TestChannel {
key: &'static str,
}
let (new_channels, listener) = mpsc::unbounded();
let filter = ChannelFilter::new(listener, 2, |chan: &TestChannel| chan.key);
pin_mut!(filter);
new_channels
.unbounded_send(TestChannel { key: "key" })
.unwrap();
let channel1 =
assert_matches!(filter.as_mut().poll_listener(&mut ctx()), Poll::Ready(Some(Ok(c))) => c);
assert_eq!(Arc::strong_count(&channel1.tracker), 1);
new_channels
.unbounded_send(TestChannel { key: "key" })
.unwrap();
let _channel2 =
assert_matches!(filter.as_mut().poll_listener(&mut ctx()), Poll::Ready(Some(Ok(c))) => c);
assert_eq!(Arc::strong_count(&channel1.tracker), 2);
new_channels
.unbounded_send(TestChannel { key: "key" })
.unwrap();
let key =
assert_matches!(filter.as_mut().poll_listener(&mut ctx()), Poll::Ready(Some(Err(k))) => k);
assert_eq!(key, "key");
assert_eq!(Arc::strong_count(&channel1.tracker), 2);
}
#[test]
fn channel_filter_poll_closed_channels() {
use assert_matches::assert_matches;
use pin_utils::pin_mut;
#[derive(Debug)]
struct TestChannel {
key: &'static str,
}
let (new_channels, listener) = mpsc::unbounded();
let filter = ChannelFilter::new(listener, 2, |chan: &TestChannel| chan.key);
pin_mut!(filter);
new_channels
.unbounded_send(TestChannel { key: "key" })
.unwrap();
let channel =
assert_matches!(filter.as_mut().poll_listener(&mut ctx()), Poll::Ready(Some(Ok(c))) => c);
assert_eq!(filter.key_counts.len(), 1);
drop(channel);
assert_matches!(
filter.as_mut().poll_closed_channels(&mut ctx()),
Poll::Ready(())
);
assert!(filter.key_counts.is_empty());
}
#[test]
fn channel_filter_stream() {
use assert_matches::assert_matches;
use pin_utils::pin_mut;
#[derive(Debug)]
struct TestChannel {
key: &'static str,
}
let (new_channels, listener) = mpsc::unbounded();
let filter = ChannelFilter::new(listener, 2, |chan: &TestChannel| chan.key);
pin_mut!(filter);
new_channels
.unbounded_send(TestChannel { key: "key" })
.unwrap();
let channel = assert_matches!(filter.as_mut().poll_next(&mut ctx()), Poll::Ready(Some(c)) => c);
assert_eq!(filter.key_counts.len(), 1);
drop(channel);
assert_matches!(filter.as_mut().poll_next(&mut ctx()), Poll::Pending);
assert!(filter.key_counts.is_empty());
}

View File

@@ -0,0 +1,129 @@
// Copyright 2020 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};
use crate::{context, Request, Response};
use fnv::FnvHashSet;
use futures::{
future::{AbortHandle, AbortRegistration},
task::*,
Sink, Stream,
};
use pin_project::pin_project;
use std::collections::VecDeque;
use std::io;
use std::pin::Pin;
use std::time::SystemTime;
#[pin_project]
pub(crate) struct FakeChannel<In, Out> {
#[pin]
pub stream: VecDeque<In>,
#[pin]
pub sink: VecDeque<Out>,
pub config: Config,
pub in_flight_requests: FnvHashSet<u64>,
}
impl<In, Out> Stream for FakeChannel<In, Out>
where
In: Unpin,
{
type Item = In;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> {
Poll::Ready(self.project().stream.pop_front())
}
}
impl<In, Resp> Sink<Response<Resp>> for FakeChannel<In, Response<Resp>> {
type Error = io::Error;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.project().sink.poll_ready(cx).map_err(|e| match e {})
}
fn start_send(mut self: Pin<&mut Self>, response: Response<Resp>) -> Result<(), Self::Error> {
self.as_mut()
.project()
.in_flight_requests
.remove(&response.request_id);
self.project()
.sink
.start_send(response)
.map_err(|e| match e {})
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.project().sink.poll_flush(cx).map_err(|e| match e {})
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.project().sink.poll_close(cx).map_err(|e| match e {})
}
}
impl<Req, Resp> Channel for FakeChannel<io::Result<Request<Req>>, Response<Resp>>
where
Req: Unpin,
{
type Req = Req;
type Resp = Resp;
fn config(&self) -> &Config {
&self.config
}
fn in_flight_requests(self: Pin<&mut Self>) -> usize {
self.in_flight_requests.len()
}
fn start_request(self: Pin<&mut Self>, id: u64) -> AbortRegistration {
self.project().in_flight_requests.insert(id);
AbortHandle::new_pair().1
}
}
impl<Req, Resp> FakeChannel<io::Result<Request<Req>>, Response<Resp>> {
pub fn push_req(&mut self, id: u64, message: Req) {
self.stream.push_back(Ok(Request {
context: context::Context {
deadline: SystemTime::UNIX_EPOCH,
trace_context: Default::default(),
},
id,
message,
}));
}
}
impl FakeChannel<(), ()> {
pub fn default<Req, Resp>() -> FakeChannel<io::Result<Request<Req>>, Response<Resp>> {
FakeChannel {
stream: Default::default(),
sink: Default::default(),
config: Default::default(),
in_flight_requests: Default::default(),
}
}
}
pub trait PollExt {
fn is_done(&self) -> bool;
}
impl<T> PollExt for Poll<Option<T>> {
fn is_done(&self) -> bool {
match self {
Poll::Ready(None) => true,
_ => false,
}
}
}
pub fn cx() -> Context<'static> {
Context::from_waker(&noop_waker_ref())
}

View File

@@ -0,0 +1,328 @@
// Copyright 2020 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 super::{Channel, Config};
use crate::{Response, ServerError};
use futures::{future::AbortRegistration, prelude::*, ready, task::*};
use log::debug;
use pin_project::pin_project;
use std::{io, pin::Pin};
/// A [`Channel`] that limits the number of concurrent
/// requests by throttling.
#[pin_project]
#[derive(Debug)]
pub struct Throttler<C> {
max_in_flight_requests: usize,
#[pin]
inner: C,
}
impl<C> Throttler<C> {
/// Returns the inner channel.
pub fn get_ref(&self) -> &C {
&self.inner
}
}
impl<C> Throttler<C>
where
C: Channel,
{
/// Returns a new `Throttler` that wraps the given channel and limits concurrent requests to
/// `max_in_flight_requests`.
pub fn new(inner: C, max_in_flight_requests: usize) -> Self {
Throttler {
inner,
max_in_flight_requests,
}
}
}
impl<C> Stream for Throttler<C>
where
C: Channel,
{
type Item = <C as Stream>::Item;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
while self.as_mut().in_flight_requests() >= *self.as_mut().project().max_in_flight_requests
{
ready!(self.as_mut().project().inner.poll_ready(cx)?);
match ready!(self.as_mut().project().inner.poll_next(cx)?) {
Some(request) => {
debug!(
"[{}] Client has reached in-flight request limit ({}/{}).",
request.context.trace_id(),
self.as_mut().in_flight_requests(),
self.as_mut().project().max_in_flight_requests,
);
self.as_mut().start_send(Response {
request_id: request.id,
message: Err(ServerError {
kind: io::ErrorKind::WouldBlock,
detail: Some("Server throttled the request.".into()),
}),
})?;
}
None => return Poll::Ready(None),
}
}
self.project().inner.poll_next(cx)
}
}
impl<C> Sink<Response<<C as Channel>::Resp>> for Throttler<C>
where
C: Channel,
{
type Error = io::Error;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.project().inner.poll_ready(cx)
}
fn start_send(self: Pin<&mut Self>, item: Response<<C as Channel>::Resp>) -> io::Result<()> {
self.project().inner.start_send(item)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
self.project().inner.poll_flush(cx)
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
self.project().inner.poll_close(cx)
}
}
impl<C> AsRef<C> for Throttler<C> {
fn as_ref(&self) -> &C {
&self.inner
}
}
impl<C> Channel for Throttler<C>
where
C: Channel,
{
type Req = <C as Channel>::Req;
type Resp = <C as Channel>::Resp;
fn in_flight_requests(self: Pin<&mut Self>) -> usize {
self.project().inner.in_flight_requests()
}
fn config(&self) -> &Config {
self.inner.config()
}
fn start_request(self: Pin<&mut Self>, request_id: u64) -> AbortRegistration {
self.project().inner.start_request(request_id)
}
}
/// A stream of throttling channels.
#[pin_project]
#[derive(Debug)]
pub struct ThrottlerStream<S> {
#[pin]
inner: S,
max_in_flight_requests: usize,
}
impl<S> ThrottlerStream<S>
where
S: Stream,
<S as Stream>::Item: Channel,
{
pub(crate) fn new(inner: S, max_in_flight_requests: usize) -> Self {
Self {
inner,
max_in_flight_requests,
}
}
}
impl<S> Stream for ThrottlerStream<S>
where
S: Stream,
<S as Stream>::Item: Channel,
{
type Item = Throttler<<S as Stream>::Item>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
match ready!(self.as_mut().project().inner.poll_next(cx)) {
Some(channel) => Poll::Ready(Some(Throttler::new(
channel,
*self.project().max_in_flight_requests,
))),
None => Poll::Ready(None),
}
}
}
#[cfg(test)]
use super::testing::{self, FakeChannel, PollExt};
#[cfg(test)]
use crate::Request;
#[cfg(test)]
use pin_utils::pin_mut;
#[cfg(test)]
use std::marker::PhantomData;
#[test]
fn throttler_in_flight_requests() {
let throttler = Throttler {
max_in_flight_requests: 0,
inner: FakeChannel::default::<isize, isize>(),
};
pin_mut!(throttler);
for i in 0..5 {
throttler.inner.in_flight_requests.insert(i);
}
assert_eq!(throttler.as_mut().in_flight_requests(), 5);
}
#[test]
fn throttler_start_request() {
let throttler = Throttler {
max_in_flight_requests: 0,
inner: FakeChannel::default::<isize, isize>(),
};
pin_mut!(throttler);
throttler.as_mut().start_request(1);
assert_eq!(throttler.inner.in_flight_requests.len(), 1);
}
#[test]
fn throttler_poll_next_done() {
let throttler = Throttler {
max_in_flight_requests: 0,
inner: FakeChannel::default::<isize, isize>(),
};
pin_mut!(throttler);
assert!(throttler.as_mut().poll_next(&mut testing::cx()).is_done());
}
#[test]
fn throttler_poll_next_some() -> io::Result<()> {
let throttler = Throttler {
max_in_flight_requests: 1,
inner: FakeChannel::default::<isize, isize>(),
};
pin_mut!(throttler);
throttler.inner.push_req(0, 1);
assert!(throttler.as_mut().poll_ready(&mut testing::cx()).is_ready());
assert_eq!(
throttler
.as_mut()
.poll_next(&mut testing::cx())?
.map(|r| r.map(|r| (r.id, r.message))),
Poll::Ready(Some((0, 1)))
);
Ok(())
}
#[test]
fn throttler_poll_next_throttled() {
let throttler = Throttler {
max_in_flight_requests: 0,
inner: FakeChannel::default::<isize, isize>(),
};
pin_mut!(throttler);
throttler.inner.push_req(1, 1);
assert!(throttler.as_mut().poll_next(&mut testing::cx()).is_done());
assert_eq!(throttler.inner.sink.len(), 1);
let resp = throttler.inner.sink.get(0).unwrap();
assert_eq!(resp.request_id, 1);
assert!(resp.message.is_err());
}
#[test]
fn throttler_poll_next_throttled_sink_not_ready() {
let throttler = Throttler {
max_in_flight_requests: 0,
inner: PendingSink::default::<isize, isize>(),
};
pin_mut!(throttler);
assert!(throttler.poll_next(&mut testing::cx()).is_pending());
struct PendingSink<In, Out> {
ghost: PhantomData<fn(Out) -> In>,
}
impl PendingSink<(), ()> {
pub fn default<Req, Resp>() -> PendingSink<io::Result<Request<Req>>, Response<Resp>> {
PendingSink { ghost: PhantomData }
}
}
impl<In, Out> Stream for PendingSink<In, Out> {
type Item = In;
fn poll_next(self: Pin<&mut Self>, _: &mut Context) -> Poll<Option<Self::Item>> {
unimplemented!()
}
}
impl<In, Out> Sink<Out> for PendingSink<In, Out> {
type Error = io::Error;
fn poll_ready(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Self::Error>> {
Poll::Pending
}
fn start_send(self: Pin<&mut Self>, _: Out) -> Result<(), Self::Error> {
Err(io::Error::from(io::ErrorKind::WouldBlock))
}
fn poll_flush(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Self::Error>> {
Poll::Pending
}
fn poll_close(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Self::Error>> {
Poll::Pending
}
}
impl<Req, Resp> Channel for PendingSink<io::Result<Request<Req>>, Response<Resp>> {
type Req = Req;
type Resp = Resp;
fn config(&self) -> &Config {
unimplemented!()
}
fn in_flight_requests(self: Pin<&mut Self>) -> usize {
0
}
fn start_request(self: Pin<&mut Self>, _: u64) -> AbortRegistration {
unimplemented!()
}
}
}
#[test]
fn throttler_start_send() {
let throttler = Throttler {
max_in_flight_requests: 0,
inner: FakeChannel::default::<isize, isize>(),
};
pin_mut!(throttler);
throttler.inner.in_flight_requests.insert(0);
throttler
.as_mut()
.start_send(Response {
request_id: 0,
message: Ok(1),
})
.unwrap();
assert!(throttler.inner.in_flight_requests.is_empty());
assert_eq!(
throttler.inner.sink.get(0),
Some(&Response {
request_id: 0,
message: Ok(1),
})
);
}

View File

@@ -0,0 +1,30 @@
// 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`](sealed::Transport) trait as well as implementations.
//!
//! The rpc crate is transport- and protocol-agnostic. Any transport that impls [`Transport`](sealed::Transport)
//! can be plugged in, using whatever protocol it wants.
use futures::prelude::*;
use std::io;
pub mod channel;
pub(crate) mod sealed {
use super::*;
/// A bidirectional stream ([`Sink`] + [`Stream`]) of messages.
pub trait Transport<SinkItem, Item>:
Stream<Item = io::Result<Item>> + Sink<SinkItem, Error = io::Error>
{
}
impl<T, SinkItem, Item> Transport<SinkItem, Item> for T where
T: Stream<Item = io::Result<Item>> + Sink<SinkItem, Error = io::Error> + ?Sized
{
}
}

View File

@@ -0,0 +1,123 @@
// 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::PollIo;
use futures::{channel::mpsc, task::*, Sink, Stream};
use pin_project::pin_project;
use std::io;
use std::pin::Pin;
/// 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).
#[pin_project]
#[derive(Debug)]
pub struct UnboundedChannel<Item, SinkItem> {
#[pin]
rx: mpsc::UnboundedReceiver<Item>,
#[pin]
tx: mpsc::UnboundedSender<SinkItem>,
}
impl<Item, SinkItem> Stream for UnboundedChannel<Item, SinkItem> {
type Item = Result<Item, io::Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> PollIo<Item> {
self.project().rx.poll_next(cx).map(|option| option.map(Ok))
}
}
impl<Item, SinkItem> Sink<SinkItem> for UnboundedChannel<Item, SinkItem> {
type Error = io::Error;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.project()
.tx
.poll_ready(cx)
.map_err(|_| io::Error::from(io::ErrorKind::NotConnected))
}
fn start_send(self: Pin<&mut Self>, item: SinkItem) -> io::Result<()> {
self.project()
.tx
.start_send(item)
.map_err(|_| io::Error::from(io::ErrorKind::NotConnected))
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.project()
.tx
.poll_flush(cx)
.map_err(|_| io::Error::from(io::ErrorKind::NotConnected))
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.project()
.tx
.poll_close(cx)
.map_err(|_| io::Error::from(io::ErrorKind::NotConnected))
}
}
#[cfg(test)]
mod tests {
use crate::{
client, context,
server::{Handler, Server},
transport,
};
use assert_matches::assert_matches;
use futures::{prelude::*, stream};
use log::trace;
use std::io;
#[cfg(feature = "tokio1")]
#[tokio::test(threaded_scheduler)]
async fn integration() -> io::Result<()> {
let _ = env_logger::try_init();
let (client_channel, server_channel) = transport::channel::unbounded();
tokio::spawn(
Server::default()
.incoming(stream::once(future::ready(server_channel)))
.respond_with(|_ctx, request: String| {
future::ready(request.parse::<u64>().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{:?} is not an int", request),
)
}))
}),
);
let mut client = client::new(client::Config::default(), client_channel).spawn()?;
let response1 = client.call(context::current(), "123".into()).await?;
let response2 = client.call(context::current(), "abc".into()).await?;
trace!("response1: {:?}, response2: {:?}", response1, response2);
assert_matches!(response1, Ok(123));
assert_matches!(response2, Err(ref e) if e.kind() == io::ErrorKind::InvalidInput);
Ok(())
}
}

48
tarpc/src/rpc/util.rs Normal file
View File

@@ -0,0 +1,48 @@
// 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},
};
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub mod serde;
/// Extension trait for [SystemTimes](SystemTime) in the future, i.e. deadlines.
pub trait TimeUntil {
/// How much time from now until this time is reached.
fn time_until(&self) -> Duration;
}
impl TimeUntil for SystemTime {
fn time_until(&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) {
if self.capacity() > 1000 {
let usage_ratio = self.len() as f64 / self.capacity() as f64;
if usage_ratio < usage_ratio_threshold {
self.shrink_to_fit();
}
}
}
}

View File

@@ -0,0 +1,97 @@
// 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,
{
const ZERO_SECS: Duration = Duration::from_secs(0);
system_time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(ZERO_SECS)
.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`.
#[allow(clippy::trivially_copy_pass_by_ref)] // Exact fn signature required by serde derive
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

@@ -0,0 +1,393 @@
// Copyright 2019 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 generic Serde-based `Transport` that can serialize anything supported by `tokio-serde` via any medium that implements `AsyncRead` and `AsyncWrite`.
#![deny(missing_docs)]
use futures::{prelude::*, task::*};
use pin_project::pin_project;
use serde::{Deserialize, Serialize};
use std::{error::Error, io, pin::Pin};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_serde::{Framed as SerdeFramed, *};
use tokio_util::codec::{
length_delimited::{self, LengthDelimitedCodec},
Framed,
};
/// A transport that serializes to, and deserializes from, a byte stream.
#[pin_project]
pub struct Transport<S, Item, SinkItem, Codec> {
#[pin]
inner: SerdeFramed<Framed<S, LengthDelimitedCodec>, Item, SinkItem, Codec>,
}
impl<S, Item, SinkItem, Codec> Transport<S, Item, SinkItem, Codec> {
/// Returns the inner transport over which messages are sent and received.
pub fn get_ref(&self) -> &S {
self.inner.get_ref().get_ref()
}
}
impl<S, Item, SinkItem, Codec, CodecError> Stream for Transport<S, Item, SinkItem, Codec>
where
S: AsyncWrite + AsyncRead,
Item: for<'a> Deserialize<'a>,
Codec: Deserializer<Item>,
CodecError: Into<Box<dyn std::error::Error + Send + Sync>>,
SerdeFramed<Framed<S, LengthDelimitedCodec>, Item, SinkItem, Codec>:
Stream<Item = Result<Item, CodecError>>,
{
type Item = io::Result<Item>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<io::Result<Item>>> {
match self.project().inner.poll_next(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some(Ok::<_, CodecError>(next))) => Poll::Ready(Some(Ok(next))),
Poll::Ready(Some(Err::<_, CodecError>(e))) => {
Poll::Ready(Some(Err(io::Error::new(io::ErrorKind::Other, e))))
}
}
}
}
impl<S, Item, SinkItem, Codec, CodecError> Sink<SinkItem> for Transport<S, Item, SinkItem, Codec>
where
S: AsyncWrite,
SinkItem: Serialize,
Codec: Serializer<SinkItem>,
CodecError: Into<Box<dyn Error + Send + Sync>>,
SerdeFramed<Framed<S, LengthDelimitedCodec>, Item, SinkItem, Codec>:
Sink<SinkItem, Error = CodecError>,
{
type Error = io::Error;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
convert(self.project().inner.poll_ready(cx))
}
fn start_send(self: Pin<&mut Self>, item: SinkItem) -> io::Result<()> {
self.project()
.inner
.start_send(item)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
convert(self.project().inner.poll_flush(cx))
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
convert(self.project().inner.poll_close(cx))
}
}
fn convert<E: Into<Box<dyn Error + Send + Sync>>>(
poll: Poll<Result<(), E>>,
) -> Poll<io::Result<()>> {
poll.map(|ready| ready.map_err(|e| io::Error::new(io::ErrorKind::Other, e)))
}
/// Constructs a new transport from a framed transport and a serialization codec.
pub fn new<S, Item, SinkItem, Codec>(
framed_io: Framed<S, LengthDelimitedCodec>,
codec: Codec,
) -> Transport<S, Item, SinkItem, Codec>
where
S: AsyncWrite + AsyncRead,
Item: for<'de> Deserialize<'de>,
SinkItem: Serialize,
Codec: Serializer<SinkItem> + Deserializer<Item>,
{
Transport {
inner: SerdeFramed::new(framed_io, codec),
}
}
impl<S, Item, SinkItem, Codec> From<(S, Codec)> for Transport<S, Item, SinkItem, Codec>
where
S: AsyncWrite + AsyncRead,
Item: for<'de> Deserialize<'de>,
SinkItem: Serialize,
Codec: Serializer<SinkItem> + Deserializer<Item>,
{
fn from((io, codec): (S, Codec)) -> Self {
new(Framed::new(io, LengthDelimitedCodec::new()), codec)
}
}
#[cfg(feature = "tcp")]
#[cfg_attr(docsrs, doc(cfg(feature = "tcp")))]
/// TCP support for generic transport using Tokio.
pub mod tcp {
use {
super::*,
futures::ready,
std::{marker::PhantomData, net::SocketAddr},
tokio::net::{TcpListener, TcpStream, ToSocketAddrs},
};
mod private {
use super::*;
pub trait Sealed {}
impl<Item, SinkItem, Codec> Sealed for Transport<TcpStream, Item, SinkItem, Codec> {}
}
impl<Item, SinkItem, Codec> Transport<TcpStream, Item, SinkItem, Codec> {
/// Returns the peer address of the underlying TcpStream.
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
self.inner.get_ref().get_ref().peer_addr()
}
/// Returns the local address of the underlying TcpStream.
pub fn local_addr(&self) -> io::Result<SocketAddr> {
self.inner.get_ref().get_ref().local_addr()
}
}
/// A connection Future that also exposes the length-delimited framing config.
#[pin_project]
pub struct Connect<T, Item, SinkItem, CodecFn> {
#[pin]
inner: T,
codec_fn: CodecFn,
config: length_delimited::Builder,
ghost: PhantomData<(fn(SinkItem), fn() -> Item)>,
}
impl<T, Item, SinkItem, Codec, CodecFn> Future for Connect<T, Item, SinkItem, CodecFn>
where
T: Future<Output = io::Result<TcpStream>>,
Item: for<'de> Deserialize<'de>,
SinkItem: Serialize,
Codec: Serializer<SinkItem> + Deserializer<Item>,
CodecFn: Fn() -> Codec,
{
type Output = io::Result<Transport<TcpStream, Item, SinkItem, Codec>>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let io = ready!(self.as_mut().project().inner.poll(cx))?;
Poll::Ready(Ok(new(self.config.new_framed(io), (self.codec_fn)())))
}
}
impl<T, Item, SinkItem, CodecFn> Connect<T, Item, SinkItem, CodecFn> {
/// Returns an immutable reference to the length-delimited codec's config.
pub fn config(&self) -> &length_delimited::Builder {
&self.config
}
/// Returns a mutable reference to the length-delimited codec's config.
pub fn config_mut(&mut self) -> &mut length_delimited::Builder {
&mut self.config
}
}
/// Connects to `addr`, wrapping the connection in a TCP transport.
pub fn connect<A, Item, SinkItem, Codec, CodecFn>(
addr: A,
codec_fn: CodecFn,
) -> Connect<impl Future<Output = io::Result<TcpStream>>, Item, SinkItem, CodecFn>
where
A: ToSocketAddrs,
Item: for<'de> Deserialize<'de>,
SinkItem: Serialize,
Codec: Serializer<SinkItem> + Deserializer<Item>,
CodecFn: Fn() -> Codec,
{
Connect {
inner: TcpStream::connect(addr),
codec_fn,
config: LengthDelimitedCodec::builder(),
ghost: PhantomData,
}
}
/// Listens on `addr`, wrapping accepted connections in TCP transports.
pub async fn listen<A, Item, SinkItem, Codec, CodecFn>(
addr: A,
codec_fn: CodecFn,
) -> io::Result<Incoming<Item, SinkItem, Codec, CodecFn>>
where
A: ToSocketAddrs,
Item: for<'de> Deserialize<'de>,
Codec: Serializer<SinkItem> + Deserializer<Item>,
CodecFn: Fn() -> Codec,
{
let listener = TcpListener::bind(addr).await?;
let local_addr = listener.local_addr()?;
Ok(Incoming {
listener,
codec_fn,
local_addr,
config: LengthDelimitedCodec::builder(),
ghost: PhantomData,
})
}
/// A [`TcpListener`] that wraps connections in [transports](Transport).
#[pin_project]
#[derive(Debug)]
pub struct Incoming<Item, SinkItem, Codec, CodecFn> {
listener: TcpListener,
local_addr: SocketAddr,
codec_fn: CodecFn,
config: length_delimited::Builder,
ghost: PhantomData<(fn() -> Item, fn(SinkItem), Codec)>,
}
impl<Item, SinkItem, Codec, CodecFn> Incoming<Item, SinkItem, Codec, CodecFn> {
/// Returns the address being listened on.
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
/// Returns an immutable reference to the length-delimited codec's config.
pub fn config(&self) -> &length_delimited::Builder {
&self.config
}
/// Returns a mutable reference to the length-delimited codec's config.
pub fn config_mut(&mut self) -> &mut length_delimited::Builder {
&mut self.config
}
}
impl<Item, SinkItem, Codec, CodecFn> Stream for Incoming<Item, SinkItem, Codec, CodecFn>
where
Item: for<'de> Deserialize<'de>,
SinkItem: Serialize,
Codec: Serializer<SinkItem> + Deserializer<Item>,
CodecFn: Fn() -> Codec,
{
type Item = io::Result<Transport<TcpStream, Item, SinkItem, Codec>>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let next =
ready!(Pin::new(&mut self.as_mut().project().listener.incoming()).poll_next(cx)?);
Poll::Ready(next.map(|conn| Ok(new(self.config.new_framed(conn), (self.codec_fn)()))))
}
}
}
#[cfg(test)]
mod tests {
use super::Transport;
use assert_matches::assert_matches;
use futures::{task::*, Sink, Stream};
use pin_utils::pin_mut;
use std::{
io::{self, Cursor},
pin::Pin,
};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_serde::formats::SymmetricalJson;
fn ctx() -> Context<'static> {
Context::from_waker(&noop_waker_ref())
}
#[test]
fn test_stream() {
struct TestIo(Cursor<&'static [u8]>);
impl AsyncRead for TestIo {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
AsyncRead::poll_read(Pin::new(self.0.get_mut()), cx, buf)
}
}
impl AsyncWrite for TestIo {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &[u8],
) -> Poll<io::Result<usize>> {
unreachable!()
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
unreachable!()
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
unreachable!()
}
}
let data = b"\x00\x00\x00\x18\"Test one, check check.\"";
let transport = Transport::from((
TestIo(Cursor::new(data)),
SymmetricalJson::<String>::default(),
));
pin_mut!(transport);
assert_matches!(
transport.poll_next(&mut ctx()),
Poll::Ready(Some(Ok(ref s))) if s == "Test one, check check.");
}
#[test]
fn test_sink() {
struct TestIo<'a>(&'a mut Vec<u8>);
impl<'a> AsyncRead for TestIo<'a> {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &mut [u8],
) -> Poll<io::Result<usize>> {
unreachable!()
}
}
impl<'a> AsyncWrite for TestIo<'a> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
AsyncWrite::poll_write(Pin::new(&mut *self.0), cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
AsyncWrite::poll_flush(Pin::new(&mut *self.0), cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
AsyncWrite::poll_shutdown(Pin::new(&mut *self.0), cx)
}
}
let mut writer = vec![];
let transport =
Transport::from((TestIo(&mut writer), SymmetricalJson::<String>::default()));
pin_mut!(transport);
assert_matches!(
transport.as_mut().poll_ready(&mut ctx()),
Poll::Ready(Ok(()))
);
assert_matches!(
transport
.as_mut()
.start_send("Test one, check check.".into()),
Ok(())
);
assert_matches!(transport.poll_flush(&mut ctx()), Poll::Ready(Ok(())));
assert_eq!(writer, b"\x00\x00\x00\x18\"Test one, check check.\"");
}
}

97
tarpc/src/trace.rs Normal file
View File

@@ -0,0 +1,97 @@
// 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, Default, 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, Default, 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, Default, 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(u128::from(rng.next_u64()) << mem::size_of::<u64>() | u128::from(rng.next_u64()))
}
}
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(())
}
}

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)
}
}

View File

@@ -0,0 +1,5 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/compile_fail/*.rs");
}

View File

@@ -0,0 +1,15 @@
#[tarpc::service]
trait World {
async fn hello(name: String) -> String;
}
struct HelloServer;
#[tarpc::server]
impl World for HelloServer {
fn hello(name: String) -> String {
format!("Hello, {}!", name)
}
}
fn main() {}

View File

@@ -0,0 +1,19 @@
error: not all trait items implemented, missing: `HelloFut`
--> $DIR/tarpc_server_missing_async.rs:9:1
|
9 | impl World for HelloServer {
| ^^^^
error: hint: `#[tarpc::server]` only rewrites async fns, and `fn hello` is not async
--> $DIR/tarpc_server_missing_async.rs:10:5
|
10 | fn hello(name: String) -> String {
| ^^
error[E0433]: failed to resolve: use of undeclared type or module `serde`
--> $DIR/tarpc_server_missing_async.rs:1:1
|
1 | #[tarpc::service]
| ^^^^^^^^^^^^^^^^^ use of undeclared type or module `serde`
|
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@@ -0,0 +1,6 @@
#[tarpc::service]
trait World {
async fn pat((a, b): (u8, u32));
}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: patterns aren't allowed in RPC args
--> $DIR/tarpc_service_arg_pat.rs:3:18
|
3 | async fn pat((a, b): (u8, u32));
| ^^^^^^

View File

@@ -0,0 +1,6 @@
#[tarpc::service]
trait World {
async fn new();
}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: method name conflicts with generated fn `WorldClient::new`
--> $DIR/tarpc_service_fn_new.rs:3:14
|
3 | async fn new();
| ^^^

View File

@@ -0,0 +1,6 @@
#[tarpc::service]
trait World {
async fn serve();
}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: method name conflicts with generated fn `World::serve`
--> $DIR/tarpc_service_fn_serve.rs:3:14
|
3 | async fn serve();
| ^^^^^

View File

@@ -0,0 +1,169 @@
use assert_matches::assert_matches;
use futures::{
future::{join_all, ready, Ready},
prelude::*,
};
use std::io;
use tarpc::{
client::{self},
context, serde_transport,
server::{self, BaseChannel, Channel, Handler},
transport::channel,
};
use tokio::join;
use tokio_serde::formats::Json;
#[tarpc_plugins::service]
trait Service {
async fn add(x: i32, y: i32) -> i32;
async fn hey(name: String) -> String;
}
#[derive(Clone)]
struct Server;
impl Service for Server {
type AddFut = Ready<i32>;
fn add(self, _: context::Context, x: i32, y: i32) -> Self::AddFut {
ready(x + y)
}
type HeyFut = Ready<String>;
fn hey(self, _: context::Context, name: String) -> Self::HeyFut {
ready(format!("Hey, {}.", name))
}
}
#[tokio::test(threaded_scheduler)]
async fn sequential() -> io::Result<()> {
let _ = env_logger::try_init();
let (tx, rx) = channel::unbounded();
tokio::spawn(
BaseChannel::new(server::Config::default(), rx)
.respond_with(Server.serve())
.execute(),
);
let mut client = ServiceClient::new(client::Config::default(), tx).spawn()?;
assert_matches!(client.add(context::current(), 1, 2).await, Ok(3));
assert_matches!(
client.hey(context::current(), "Tim".into()).await,
Ok(ref s) if s == "Hey, Tim.");
Ok(())
}
#[cfg(feature = "serde1")]
#[tokio::test(threaded_scheduler)]
async fn serde() -> io::Result<()> {
let _ = env_logger::try_init();
let transport = serde_transport::tcp::listen("localhost:56789", Json::default).await?;
let addr = transport.local_addr();
tokio::spawn(
tarpc::Server::default()
.incoming(transport.take(1).filter_map(|r| async { r.ok() }))
.respond_with(Server.serve()),
);
let transport = serde_transport::tcp::connect(addr, Json::default).await?;
let mut client = ServiceClient::new(client::Config::default(), transport).spawn()?;
assert_matches!(client.add(context::current(), 1, 2).await, Ok(3));
assert_matches!(
client.hey(context::current(), "Tim".to_string()).await,
Ok(ref s) if s == "Hey, Tim."
);
Ok(())
}
#[tokio::test(threaded_scheduler)]
async fn concurrent() -> io::Result<()> {
let _ = env_logger::try_init();
let (tx, rx) = channel::unbounded();
tokio::spawn(
tarpc::Server::default()
.incoming(stream::once(ready(rx)))
.respond_with(Server.serve()),
);
let client = ServiceClient::new(client::Config::default(), tx).spawn()?;
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_matches!(req1.await, Ok(3));
assert_matches!(req2.await, Ok(7));
assert_matches!(req3.await, Ok(ref s) if s == "Hey, Tim.");
Ok(())
}
#[tokio::test(threaded_scheduler)]
async fn concurrent_join() -> io::Result<()> {
let _ = env_logger::try_init();
let (tx, rx) = channel::unbounded();
tokio::spawn(
tarpc::Server::default()
.incoming(stream::once(ready(rx)))
.respond_with(Server.serve()),
);
let client = ServiceClient::new(client::Config::default(), tx).spawn()?;
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());
let (resp1, resp2, resp3) = join!(req1, req2, req3);
assert_matches!(resp1, Ok(3));
assert_matches!(resp2, Ok(7));
assert_matches!(resp3, Ok(ref s) if s == "Hey, Tim.");
Ok(())
}
#[tokio::test(threaded_scheduler)]
async fn concurrent_join_all() -> io::Result<()> {
let _ = env_logger::try_init();
let (tx, rx) = channel::unbounded();
tokio::spawn(
tarpc::Server::default()
.incoming(stream::once(ready(rx)))
.respond_with(Server.serve()),
);
let client = ServiceClient::new(client::Config::default(), tx).spawn()?;
let mut c1 = client.clone();
let mut c2 = client.clone();
let req1 = c1.add(context::current(), 1, 2);
let req2 = c2.add(context::current(), 3, 4);
let responses = join_all(vec![req1, req2]).await;
assert_matches!(responses[0], Ok(3));
assert_matches!(responses[1], Ok(7));
Ok(())
}

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();
}
}
});
}
}