Building a gRPC Service with Ballerina — Part II (Integration)

Kevin Hoffman
7 min readJul 27, 2018

--

In my last blog post, I started exploring the creation of gRPC services in a new language called Ballerina. Its proposition is that it can alleviate boilerplate and give developers first-class language primitives that let people write cloud native applications quickly while still maintaining reliability, scalability, observability, and security. That’s a huge claim, so I’m digging into the language in a number of blog posts.

What I built last time was still fairly well entrenched in the realm of “hello world”. Anyone can make any language look impressive if all you have to do is expose an RPC method that reads a record from an in-memory map.

In a more realistic scenario, we would have more than one service. In my next sample, I still have the “drone management” service responsible for adding and querying individual drones. However, there’s also a second service that receives drone status messages from a message broker. In a realistic IoT architecture, we might be streaming telemetry from devices in the field via MQTT. This service receives location update messages and stores the most current location for each drone, discarding the messages when its done.

With this service in place, we would naturally want the drone management service to query the drone status service seamlessly, without letting the consumer know what’s happening. When I query the drone management service for drone details, I should get both the information in the drone management service and the information contained in the status service.

This kind of integration where you have a facade service and another service receiving out-of-band updates via a message broker that also exposes a query-only endpoint is extremely common when building microservices architectures.

If Ballerina is true to its premise, writing this kind of service-to-service integration should be a piece of cake. Let’s put that to the test.

Let’s see what the drone status service looks like, since it’s at the bottom of the dependency chain. First, we’ve got the part of the service that waits for messages on the DroneStatusUpdates queue (I’ve snipped out some of the logging to keep the code more readable):

public type DroneStatus record {
string drone_id;
float latitude;
float longitude;
float altitude;
float battery_remaining;
!...
};
map<DroneStatus> statusUpdates;documentation { Queue receiver endpoint for drone location updates }
endpoint mb:SimpleQueueReceiver queueReceiverDroneLocations {
host: "localhost",
port: 5672,
queueName: "DroneStatusUpdates"
};
service<mb:Consumer> locationListener bind queueReceiverDroneLocations {
onMessage(endpoint consumer, mb:Message message) {
string textStatus = check message.getTextMessageContent();
json status = check internal:parseJson(textStatus);

DroneStatus|error dstat = <DroneStatus>status;
match dstat {
DroneStatus value => {
statusUpdates[value.drone_id] = value;
}
error err => {
// log error
}
}
}
}

Here I ran into a problem trying to get JSON data from the message queue. When I looked at the Ballerina docs, it looked like you could just write something like:

DroneStatus status = <json>messageText;

Turns out this isn’t the case. You can create a json primitive type from a single string value, but you have to parse to get a full JSON object to convert into a Ballerina record type. I think the confusion here was a combination of the documentation, my lack of exposure to the language, and preconceptions about what would or wouldn’t be “automatic”.

So that’s it. That’s the entire set of code to manage my queue listener that reads drone status updates and caches them. I’m guessing it might only add a few lines to persist those updates in an out-of-memory cache.

Can we easily host a gRPC endpoint in the same file/package as a queue listener? Yep:

endpoint grpc:Listener statusListener {
host: "localhost",
port: 9091
};
@grpc:ServiceConfig
service StatusQuery bind statusListener {
GetDroneStatus(endpoint caller, string droneId) {
log:printInfo("Querying drone status: " + droneId);
DroneStatus status;
match statusUpdates[droneId] {
DroneStatus value => {
status = value;
}
() => {
_ = caller->sendError(grpc:NOT_FOUND,
"No updates from that drone");
status = {};
}
}
_ = caller->send(status);
_ = caller->complete();
}
}

At this point, my spider sense is starting to tingle. I’m seeing some things that are breaking through my skepticism. I know exactly how much code a service that both has a gRPC endpoint and a message queue listener takes in Go and Rust and even Spring Boot. This is a staggeringly small amount of code. However, I haven’t gotten to things like observability, tracing, and security yet… so the size of the code may still balloon in subsequent blog posts.

It’s also worth pointing out here how easy it is to send legitimate gRPC status codes. A large number of libraries for various languages try and hide this from you, or ruin the readability of your code to get at these codes.

Now we just need to go back into the drone management service and update it to add a call to the new StatusQuery gRPC service. First, let’s create a drone details record that is a superset of the original DroneInfo record:

type DroneDetails record {
DroneInfo info;
float latitude;
float longitude;
float altitude;
float battery_remaining;
};

Now we can update the GetDrone function to make a call to the service:

GetDrone(endpoint caller, string droneId) {        
log:printInfo("Querying drone: " + droneId);
DroneDetails details;
match dronesMap[droneId] {
DroneInfo value => {
details.info = value;
var status =
statusQueryBlockingEp->GetDroneStatus(droneId);
match status {
(DroneStatus, grpc:Headers) payload => {
DroneStatus dstat;
grpc:Headers headers;
(dstat, headers) = payload;
details.latitude = dstat.latitude;
details.longitude = dstat.longitude;
details.altitude = dstat.altitude;
details.battery_remaining =
dstat.battery_remaining;
}
error err => {
log:printError("Failed to get drone status: " +
err.message);
}
}
}
() => {
_ = caller->sendError(grpc:NOT_FOUND, "No such drone");
details = {};
}
}
_ = caller->send(details);
_ = caller->complete();
}

There’s a little quirk in how I’ve had to write this function. I have to write it such that there is an always-followed path that invokes send with the same data type. This is currently required to properly generate the protobuf IDL for the client stub. Hopefully this will change in the future to get a little more intelligent, and I’ll probably submit an issue about it.

I also didn’t try and hide complexity from you by creating a second method responsible for making the gRPC client call to the drone status service and parsing the result. If this was a real app, I would absolutely refactor that code out to its own function. I think the point here is that without doing anything to compress this code, it’s still remarkably simple and, in my opinion, very readable.

Oh, speaking of readability, here’s an updated sequence diagram for the new GetDrone function:

GetDrone Sequence Diagram

To generate this, all I had to do was open the Visual Studio Code command palette and choose Ballerina’s “create diagram” option. I could make it even simpler by binding a hotkey, but I just haven’t gotten around to it.

Seeing how that endpoint variable name shows up in the diagram, I think I’ll probably adopt a slightly better naming convention for my variables. Up to this point I’ve just been re-using variable names from the samples in documentation and guides.

So, how did I test this? Well, I used the Ballerina message broker (I’m assuming that more brokers will be available in the future as the ecosystem grows). To send a message, I actually wrote a test sender:

import ballerina/mb;
import ballerina/log;
function main (string... args) {
endpoint mb:SimpleQueueSender droneStatusSender {
host: "localhost",
port: 5672,
queueName: "DroneStatusUpdates"
};
json status = {
drone_id: "DRONE1",
latitude: 40.156,
longitude: 72.1345,
altitude: 100.0,
battery_remaining: 86.5
};
string statusMsg = status.toString();
mb:Message message = check
droneStatusSender.createTextMessage(statusMsg);
_ = droneStatusSender->send(message);
}

It was so easy to write this, I didn’t bother trying to write a shell script to send sample messages. After I sent this message, I was able to exercise my drone management client code:

$ ballerina run dmclient
2018-07-27 08:26:29,069 INFO [kevin/dmclient:0.0.1] - Drone DRONE1 added.
2018-07-27 08:26:29,300 INFO [kevin/dmclient:0.0.1] - Got drone details: 86.5% battery

There you have it — I’ve got a gRPC service that augments one of its own functions by calling another gRPC service, which is also a listener on a message queue. Had I not run into the problem with JSON parsing and discovered a bug with nested protobufs (which the Ballerina team patched within an hour!!), this whole thing would’ve taken me less than an hour to create.

More important than the actual time spent is how I feel while spending that time. I didn’t do anything in this example that I think would contribute to service fatigue. I wasn’t copy/pasting endlessly or wiring up countless support libraries or fussing with complicated tooling that uses code generation to produce boilerplate on my behalf. I was just writing what I wanted my service integration to look like.

Of course, this is all still superficial. None of this stuff is production-grade yet, and that’s where the buck stops, so in the coming posts I’ll be exploring the friction level and potential code bloat that might come from taking this sample from in-memory integration that works on my laptop to something that deploys to and runs in Kubernetes and ultimately gives me the kind of reliability and visibility that I need for any deployed production workload.

--

--

Kevin Hoffman
Kevin Hoffman

Written by Kevin Hoffman

In relentless pursuit of elegant simplicity. Tinkerer, writer of tech, fantasy, and sci-fi. Converting napkin drawings into code for @CapitalOne

No responses yet