Remote Procedure Calls
Remote Procedure Calls (RPCs) have been around since the earliest days of computer networks, dating from about 1980. It seems natural that one computer might want to send a request for computation to be accomplished by another computer and then for the remote computer to send back the result. These systems have mostly followed the same basic design.
The design is that a specification is written to specify what Procedure Calls can be made between the two computers, what parameters the first will send to the second, and what return result will be sent from the second to the first. This “specification” requires that the two computers send compatible data formats between them when making the request and the response.
It should be clear that the previous section section explained a particular specification language (the protobuf IDL) and the data format to use when the communcation channel (the protobuf data serialization format).
The second part of the common design of RPC systems is that the RPC system generates “boilerplate” code that handles the packing up of parameters, sending them, waiting for the response, then unpacking the result and presenting back to other layers of the program in a way that makes sense for the programming language. This generated, repetitive code is ofter referred to as the stubs. There is a “stub” for each method call and response that makes the calling of a Remote Procedure Call look either mostly or completely like a “normal” procedure call that does not utilize the network.
This ability to “hide” the use of the network in an RPC is of arguable value. Consider
this procedure call where the method Bar
is being called on the object Foo
and
the result placed in the variable result
.
result=Foo.Bar()
If this is a normal, non-networked function call that our current processors can do billions of times per second, the odds of this function truly failing are very low. “Failing” here might be a situation such that the program is out of memory, memory corruption by cosmic rays has been detected, or the processor is powering down so as to not overheat. These are all failures, it is true, but outside the first one these types of failures are so rare that a typical developer may never see them in their who career. Running out of memory is not outside the experience of most developers, but that usally is a catastrophic development for the running program not one that most programming languages provide much way to do anything about (the program just crashes).
However, if by the generation of stubs the call in the example above uses a network,
the class of failures that can happen is not only much larger, but the liklihood
of a failure is vastly larger. Some reasonable failures might be:
* The machine that was expected to do the computation Bar
on behalf of Foo
is
currently overloaded and cannot accept the request, although it might be able
to later!
* The network connecting the caller and receiver of the mesasge about Bar
are
not connected via a network right now (the network is down, the plug was pulled
out by the dog).
* The remote machine that would normally do the computation of Bar
is offline
for maintainence.
… and there are many more. This is the reason that parigot always returns
an error code from any method call, because we do not want to have a situation
where the parigot developer “forgets” that the are using a network when making
a procedure call. The case of Foo.Bar()
above makes it very pleasant to ignore
the networking involved in the computation, until it doesn’t.
parigot and generated code
Previously, we mentioned that RPC systems for 40+ years have been generating
code to make RPC calls easier and more pleasant to use. We also mentioned that
there are risks with completely hiding the network from a user trying to do
what appears to be a simple procedure call. parigot generates a large amount of
code based on the .proto
files that specify the interfaces between services in
your system. parigot tries to strike a balance between convenience of notation
and exposing the multitude ways that a network can fail. In the case of the
current golang support parigot generates code that is strongly typed such that
the developer must use the correct types when interacting between a caller and
receiver (there are no loopholes in the type system). Further, parigot is
careful to expose to the user the return value that would be expected and might
include information in it about the details of the network failure. Finally,
when using the continuations sytle of development with parigot, parigot has
strongly typed notions called Futures
that express that a network call is in
progress and may yet fail.