GRPC.io is interestingly different from CORBA
eighty-twenty.org
eighty-twenty.org
Unfortunately, gRPC is still very much at the release level of "it works at Google and also on some developers laptop once", so while we haven't really encountered any bugs, dealing with gRPC has been a bit of a devops headache. That being said, we don't have a full-time devops person or anything, and I bet someone who knew their way around automated configuration and deployment better than myself could handle gRPC fine.
This is discussed- in relation to Sandstorm- in a Sandstorm blog post dating from last December. Other notables- the post mentions CORBA explicitly, and it discusses persisting capabilities (anti "First Class" objects extreme if I'm reading OP right). https://blog.sandstorm.io/news/2014-12-15-capnproto-0.5.html
I don't know Cap'n'Proto's RPC at all (I looked at it briefly for some projects, but rejected it since we needed Ruby support), but I've worked with CORBA, Microsoft's COM (specifically, DCOM) and Java's RMI. These all let you pass around references to remote objects.
The danger in these systems is that that every API is locality-transparent, you don't know if any given call will be to remote object or a local object. You can be super careful about what you pass around, and still end up doing remote calls by accident.
It also leads to "normalized" API designs that too easily incur 1+N roundtrips even for basic information retrieval; for example, an endpoint that returns Book[], where each book has an Author, accessing book.author ends up with a network roundtrip to get the author.
I very much prefer the dumb-but-explicit approach where APIs only exchange data, and you only exchange data.
Another big challenge that DCOM struggled (and, I think, CORBA and RMI) with was object releasing. DCOM, for example, had explicit reference counting. This meant that a client could hold onto an instance for a long time, and that while doing so the server also had to keep the instance around. If a client died, keepalive logic in the DCOM framework would eventually release the object.
Perhaps the biggest challenge in any DCOM server was thread safety. Since any client could come in on a separate thread, your entire API model had to be thread-safe, or rely on thread-safe wrappers that acted as proxies for your real internal data objects.
I should say that locality transparency is very nice to work with — until you start having issues.
For example, you need the latest Protobuf 3.0 beta release, which no current OS distro currently has, and build gRPC from source. The language-specific packages (Ruby, Node, etc.) have releases that lag behind gRPC itself, so you'll probably have to build those from source, too (linking directly to HEAD from a Gemfile or package.json doesn't work, last I checked). Performance also seems decidedly lackluster, though it's been a few months since I did some casual benchmarking.
As for the API and feel of the library, it's similar to Thrift, and much like CORBA without the OO and attempts at location transparency. With the current generators, you'll get some low-level and not very friendly wrappers generated from the Protobuf declarations that don't attempt to hide the fact that you're writing RPC requests and responses as Protobuf structs.
One disappointing aspect of gRPC at this point is the lack of discoverability. Clients have to connect to a specific host and port, and you have to build your own glue based on ZooKeeper/etcd/Consul/DNS/whatever. Since no fault tolerance is built in, things like retries and load balancing is left as an exercise for the reader.
You get load balancing and discoverability for free, since NATS will distribute messages equally to consumers: Just fire up new consumers, and they will get messages to the topics they subscribe to, and all a client needs is the host/port of the NATS server, which is going to be the same for all parties. Couple that with some hand-coded serialization — Protobuf, Msgpack or even JSON — and you have a fairly resilient, fault-tolerant RPC.
You could trivially do streaming RPC if you handled the request/response matching yourself — if you look at the clients, NAT's RPC is all handled on the client side, using ordinary messages with a unique topic name generated for each request/response. Extending it to support multiple replies per request would be simple.
There are other, more feature-rich message queues, of course, such as RabbitMQ. NATS' advantage is that it's extremely simple.
Another option is ZeroMQ, but it's a bit lower-level and doesn't solve the discoverability part. You'll end up writing much more glue for each client and server.
[1] http://nats.io
The main benefit we get from gRPC vs something like thrift is a nice way to do bidirectional streaming.
Drop that misfeature and you have no reason to invoke its name in the same breath as CORBA, other than to confound people under the age of 35.
PS: I was the co-author of it.
Over an RPC boundary it seems impossible that there could be any scenario so serious that is so unexpected and which it is not possible to form a contingency for that it would be necessary to propagate the exception across to the other side. It opens up a whole can of worms.
An inherent requirement for any contract language is a signal to refuse a service if the pre-conditions are not met (for example, refusing to withdraw money because of insufficient funds in an account).
The most pragmatic solution to this inherent feature of contracts are typed (checked) exceptions - anything else is a less robust, incomplete solution.
First of all for debugging it is a huge help to get as rich data back as you can from the other end and logged.
As for the proper handling of exceptions, I think there are a number of semantic attributes that an exception can have: for instance, transient/permanent, and the scope of the exception (is the whole system hosed? just this record? etc.)
In a greenfield application you can build an exception hierarchy that records this, but in the real world it is necessary to build a system that does some inference, or even guessing, to decide what to do about a failure.
I just remember the bad old days when C programs would return an int and it would be -1 if the function failed, or maybe they set errno, and either way you could double the size of some functions if you add in error checking and probably get it wrong.
Exceptions on the other hand are good for the normal kind of call stack and also work in "callback hell" situations since you can stuff them in a field or collection, pass them as a function method, etc.
Most of the time, when you're handling RPCs, you're processing the responses asynchronously anyway, which makes the concept of an exception (which unwinds the stack) superfluous anyway. You should absolutely be handling error conditions. That doesn't mean you need to use the language mechanisms of exceptions to do so.
CORBA got into trouble because it tried to make remote objects look too much like local objects. Don't think that way: think in terms of messages passed between communicating sequential processes, with an error indicating that the process will have to do something else.
How could I? I would have designed that interface long before implementing it. No, in that case, this is an unexpected system error - for example, an unchecked RuntimeException in Java.
Just because people don't know how to write software contracts properly, doesn't mean that languages and protocols should remove the notion of a checked exception.
When used properly, they are an essential part of an understandable contract. And that's before we even look at it from a type system point of view, where it's absolutely silly to use the same response message to represent either success of failure, greatly increasing the cognitive overhead of the developer calling your service.
A checked exception is okay but it is still an exception with all the stack frame capturing overhead that brings.
In better languages like ML variants (say F#) we have discriminated unions. These let us return structured, type safe, result codes and information. Without all the overhead and OO-ness of nasty exceptions. We can still throw exceptions but these are strictly, and I mean strictly, for those very exceptional and unexpected scenarios.
message FooResult {
optional Exception exception = 1;
optional Foo foo = 2;
}You have no clean way of describing multiple pre-conditions (refusal reasons), or to evolve those over time in existing systems, without assuming that developers will somehow correctly read your API documentation, and manually write the code everywhere to handle all possible exceptions, with no help from your compiler or your language's type system.
This is how to indicate exceptions in the 1970s. The world has moved on since then :-)
Modern languages have a compiler-checked sum type, and protobuf can support these (oneof). You get the good parts of exceptions (being able to cleanly separate error handling from happy-path code, and doing it concisely and readably via do-notation or equivalent) without the bad parts (invisible nonlocal control flow, breaking referential transparency, hard to abstract over).
(You still have the problem of returning a new type of error condition that your client code wasn't expecting, but it's the same problem as returning a new type of successful value that your client code wasn't expecting, and you can solve it in the exact same way).
I'd settle for 1990s tech over 1970s tech any day when it comes to contracts (APIs).
(There are certainly plenty of places stuck on older tech - but I'd think those places won't be interested in adopting something as new as GRPC)