Introduction

Time Travel! (Promise Pipelining)

Cap’n Proto RPC employs TIME TRAVEL! The results of an RPC call are returned to the client instantly, before the server even receives the initial request!

There is, of course, a catch: The results can only be used as part of a new request sent to the same server. If you want to use the results for anything else, you must wait.

This is useful, however: Say that, as in the picture, you want to call foo(), then call bar() on its result, i.e. bar(foo()). Or – as is very common in object-oriented programming – you want to call a method on the result of another call, i.e. foo().bar(). With any traditional RPC system, this will require two network round trips. With Cap’n Proto, it takes only one. In fact, you can chain any number of such calls together – with diamond dependencies and everything – and Cap’n Proto will collapse them all into one round trip.

By now you can probably imagine how it works: if you execute bar(foo()), the client sends two messages to the server, one saying “Please execute foo()”, and a second saying “Please execute bar() on the result of the first call”. These messages can be sent together – there’s no need to wait for the first call to actually return.

To make programming to this model easy, in your code, each call returns a “promise”. Promises work much like JavaScript promises or promises/futures in other languages: the promise is returned immediately, but you must later call wait() on it, or call then() to register an asynchronous callback.

However, Cap’n Proto promises support an additional feature: pipelining. The promise actually has methods corresponding to whatever methods the final result would have, except that these methods may only be used for the purpose of calling back to the server. Moreover, a pipelined promise can be used in the parameters to another call without waiting.

But isn’t that just syntax sugar?

OK, fair enough. In a traditional RPC system, we might solve our problem by introducing a new method foobar() which combines foo() and bar(). Now we’ve eliminated the round trip, without inventing a whole new RPC protocol.

The problem is, this kind of arbitrary combining of orthogonal features quickly turns elegant object-oriented protocols into ad-hoc messes.

For example, consider the following interface:

`# A happy, object-oriented interface!

interface Node {}

interface Directory extends(Node) { list @0 () -> (list: List(Entry)); struct Entry { name @0 :Text; file @1 :Node; }

create @1 (name :Text) -> (node :Node); open @2 (name :Text) -> (node :Node); delete @3 (name :Text); link @4 (name :Text, node :Node); }

interface File extends(Node) { size @0 () -> (size: UInt64); read @1 (startAt :UInt64, amount :UInt64) -> (data: Data); write @2 (startAt :UInt64, data :Data); truncate @3 (size :UInt64); }`

This is a very clean interface for interacting with a file system. But say you are using this interface over a satellite link with 1000ms latency. Now you have a problem: simply reading the file foo in directory bar takes four round trips!

`# pseudocode bar = root.open("bar"); # 1 foo = bar.open("foo"); # 2 size = foo.size(); # 3 data = foo.read(0, size); # 4

The above is four calls but takes only one network

round trip with Cap'n Proto!`

In such a high-latency scenario, making your interface elegant is simply not worth 4x the latency. So now you’re going to change it. You’ll probably do something like:

`# A sad, singleton-ish interface.

interface Filesystem { list @0 (path :Text) -> (list :List(Text)); create @1 (path :Text, data :Data); delete @2 (path :Text); link @3 (path :Text, target :Text);

fileSize @4 (path :Text) -> (size: UInt64); read @5 (path :Text, startAt :UInt64, amount :UInt64) -> (data :Data); readAll @6 (path :Text) -> (data: Data); write @7 (path :Text, startAt :UInt64, data :Data); truncate @8 (path :Text, size :UInt64); }`