RPC system rewritten to use async I/O

11 Jan 2016

Today I’ve released verson 0.6 of the three main Cap’n Proto crates: the runtime, the code generator, and the remote procedure call system.

The biggest change is that the RPC system is now asynchronous, built on top of the GJ event loop library.

promises

Cap’n Proto’s RPC protocol allows method calls to be made on distributed objects. It has a built-in notion of pipelining that can minimize network round-trips, and it allows object references to be transmitted within messages, alongside plain-old-data.

Such a system has an inherent need to deal with concurrency, both in its internal implementation in its publicly-exposed interfaces.

For example, it’s common for a user to want to implement an RPC method that makes a bunch of other RPC calls and then collects their results before returning. How should these actions compose? In the new version of capnp-rpc, which uses GJ’s Promise abstraction, each of the inner method calls returns a promise, and those promises can be collected with Promise::all() to form a new promise which can then be returned from the outer method.

To see more concretely what this kind of thing looks like, take a look at the calculator example, which also showcases some fancier features. There are also some more-practical examples in the form of Sandstorm apps: a simple GET/PUT/DELETE server and a word game.

error handling

A Promise<T, E> is essentially a deferred Result<T, E>, so it should be no surprise that today’s release pertains to our continuing story about error handling.

Last time, we described our switch to using Result<T,E> pervasively, so that we could return an Err(e) on a decode error, rather than panicking. That switch had some costs:

  1. We now need to sprinkle try!()s in our code, one for any time we dereference a Cap’n Proto pointer.
  2. We also need to edit some return types from T to Result<T, E>, and in some cases need to define helper functions so that try!() has a place to return to.

In my opinion, (1) is not so bad, and it has the advantage of making control flow more clear. The proposed ? operator would make this even nicer.

In the asynchronous world of Cap’n Proto RPC, (2) becomes less of a hassle, because most functions that need to read a Cap’n Proto message are asynchronous, and therefore already return a Promise<T, E>. In such cases, we can use the pry!() macro that GJ defines. The pry!() macro acts like try!(), but in the early return case returns Promise::err(e) rather than Result::Err(e).

a simpler error type

One error-related question that often arises when designing interfaces that use Promise<T,E> is: what concrete error type should be plugged in for E?

In previous versions of capnproto-rust, I had defined an error enum capnp::Error with various cases, one of which wrapped a std::io::Error. This got me into trouble when I wanted to start using Promise::fork(), which requires that E be Clone. The problem is that std::io::Error is not Clone!

To address this problem, I have redefined ::capnp::Error to follow the design of kj::Exception. It’s now a very simple struct with a String description and an ErrorKind enum, where the only variants of ErrorKind are Failed, Disconnected, Overloaded, and Unimplemented. The observation here is that there seems to be very little gained by defining hierarchies of errors wrapping other errors.

-- posted by dwrensha

capnproto-rust on github
more posts