gRPC Java Function Service

Murat Kilic
7 min readSep 30, 2019

--

In my previous post, I explored how we can use gRPC to have a function execution service, with a gRPC server receiving execution requests from clients via gRPC protocol. If you have not done so, I’d recommend reading that one first to get an idea of what we are exploring.

In this post, I am going to take the previous exercise further. I will use the same Executable code I built in the original “Running Arbitrary Java Code On The Fly” post. Just like previous exercise, I will build a gRPC server with RPC call implementations, and a client to invoke those calls. On top of it, I am adding JUnit tests, so anybody can run a Maven package and go thru the whole exercise. Complete code can be found in my GitHub Repo

Service Descriptor File

As it’s always the case with gRPC, we start with a service definition. For this exercise, I created a proto file called FunctionService.proto under /src/main/proto directory of Maven project. Our function basically has 3 fields: name, handler which contains class and method name, and jarFile which specifies the library that contains the function and all required Java libraries.

In the proto file we define rpc calls for this service as :

What these calls do are self-explanatory, basic CRUD methods plus one for uploading JAR file and one for executing the function.

Execute call is similar to my previous exercise, except this time we only specify the function name and output format we would like to receive.

Based on input from Carl Mastrangelo I added functionality to return the Execution Response as a serialized Java Object in addition to the default JSON format. To do this, I used ‘oneof’ feature of protobuf to set either JSON or Java output. If we set serializationFormat to ‘java’ in the ExecutionRequest, Response contains javaOut field with serialized object returned from the function, if not, it returns object serialized into JSON.

After creating .proto file, we update pom.xml file with necessary dependencies and plugins as explained here , same updates as we want to implement a gRPC service, we can run ‘mvn package’ and see the stubs generated by the protobuf-maven-plugin under ‘target/generated sources/protobuf’. There is one grpc stub:

./grpc-java/io/mark/java_examples/Executors/grpc:
total 36
-rw-rw-r — . 1 centos centos 32899 Sep 27 23:32 FunctionServiceGrpc.java

And there are multiple Java classes(since option java_multiple_files = true; in the proto file)

./java/io/mark/java_examples/Executors/grpc:
total 420
-rw-rw-r--. 1 centos centos 21817 Sep 27 23:32 AddRequest.java
-rw-rw-r--. 1 centos centos 702 Sep 27 23:32 AddRequestOrBuilder.java
-rw-rw-r--. 1 centos centos 16447 Sep 27 23:32 AddResponse.java
-rw-rw-r--. 1 centos centos 401 Sep 27 23:32 AddResponseOrBuilder.java
…………

gRPC Function Server

As with any Java gRPC server coding, we start with implementing the service. For this I created FunctionService.java implementing the Base Interface.

public class FunctionService extends  FunctionServiceGrpc.FunctionServiceImplBase {

Here, most of the RPC methods are easy to implement, since they are unary calls, no streaming involved(although all Server side implementations are coded as streaming in gRPC-java). To keep this exercise simple, I will just use a HashMap to store function data, instead of using some sort of persistent storage like DB or file.

So with these methods we can add a Function giving it a name and a handler, update, get and delete it.

Once we have a function created this way, we need to upload a JAR file that contains the code for this function. For this let’s create an uploadFile method which takes a bit more effort to implement since well be doing client side streaming to upload the file using buffer, instead of sending the file in one shot. So the beginning of our method is a bit different than the others

@Overridepublic StreamObserver<UploadFileRequest> uploadFile(final StreamObserver<UploadFileResponse> responseObserver) { 
return new StreamObserver<UploadFileRequest>() {

We return a StreamObserver that implements onNext, onError and onCompleted methods. Within onNext, which is invoked when client sends data, we check if we already have received function name, if not, we check to see if client is sending function name now, if it is, we will use that and also get the JAR file name from the request. Then we create a directory with the function name under ‘TMPDIR/uploaded” directory where we use for storing client files.

If function name is already set, or client is not sending function name we srite byte chunk with

byteSink.write(req.getFileContent().toByteArray());

Once client sends onCompleted, we set the and jarFile of the Function in the ‘functions’ HashMap

Then we send a response with Uploaded:true message and tell client we completed on server side by responseObserver.onCompleted()

Another place which code gets a bit complicated is where I implement the execute method. Here we receive the function name client wants to execute and get the function from the HashMap

After this we try to load the class from the JAR file client uploaded.

Then we use the same method we used in previous post to invoke the method. The difference from that is we change the output based on what client requested. If client requested Java output by sending serializationFormat as ‘java’ in the ExecutionRequest, we use ObjectOutputStream to serialize this in Java and set JavaOut with ByteString. If client requested ‘json’ or specified nothing, we set JsonOut using GSON library to convert returned object to JSON.

as I wrote above, full code of FunctionService.java is here

gRPC Function Client

As in previous post, let’s create a gRPC client in Java. Just like the server side, add,update,delete and get calls are easy. So I’ll pass the explanation here. Since we are doing client side streaming and we’d like to wait until server sends a response back, to make sure threads have completed execution, we use a CountDownLatch.When server sends onError or onCompleted, we count down 1. Since our latch count is 1, any of these responses bring it down to 0, hence closing threads.

As for client sending file, here is how we process. Client has the JAR file in a location(here we assume TMPDIR), and we create a FileInputStream from that. Then we read bytes using a buffer size we define and red the file in chunks and send over to the server using setFileContent until file is read completely. Once completed, we send onCompleted to the server, so the server knows we finished uploading the fie. We also start waiting on the CountDownLatch, so that we do not just end the thread and wait for server sending us a result or error.

This completes our uploadFile call. Now, for execute call, we set output format first

Since output is OneOf JSON or Java, we check that using response.getOutputOneofCase() and if it is JSON, we just return it, if not, we create an Object , by deserializing JavaOut using ObjectInputStream

JUnit Tests

To test all of this, First we need to add junit dependencies and include the SureFire plugin to POM file.

<dependency>                                            <groupId>org.junit.jupiter</groupId>                                            <artifactId>junit-jupiter-engine</artifactId>                                            <version>5.3.1</version>                                            <scope>test</scope>                                        </dependency> 
<dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <version>5.3.1</version> <scope>test</scope> </dependency>
………….
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.0</version> </plugin>

Let’s create JUnit tests as TestJavaExecutionGRPCService which can be found here. To setup the environment first we need to start the gRPC server and create client object using @BeforeClass annotation and after tests are completed we shutdown client with @AfterClass annotation.

Tests are self explanatory, so I’ll just mention execute call

Here, we addFunction first. Then we uploadFile. Both times we check servere sends true response. Then we execute the function first without specifying output time, hence defaulting to JSON. We compare this to the string we should receive. For second execution, we execute the function with ‘java’ output type, cast the returned object to AddPersonResponse and set this to ‘person’ object. Then we check two properties of this person object, full name to“Mark Kose” and shouldExercise more to ‘true’

It’s time to give it a try!

Alright, since we have finished coding, let’s run a maven package to compile and test this.

[INFO] --- maven-surefire-plugin:2.22.0:test (default-test) @ dynamicExecutionGRPC ---
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running TestJavaExecutionGRPCService
Setting up test environment
……….
2019-09-30 16:19:15 INFO JavaExecutorGRPCServer:20 - Starting JavaExecutorGRPCServer
JavaExecutorGRPCServer is ready

2019-09-30 16:19:15 INFO JavaExecutorGRPCServer:32 - JavaExecutorGRPCServer is ready
……….
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.061 s - in TestJavaExecutionGRPCService
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0

Great!

So what we have accomplished is creating a gRPC Java Function Server that takes requests over gRPC, manages Functions which could be handled by any method of any class as long as it is in a self containing JAR file.Execute method accepts one or zero parameters, parameter can be any type as long as it is a primitive type already known by JVM, or a custom type whose definition is in the JAR. gRPC client can provide any data to the method as JSON string and server responds with JSON or serialized Java based on what client requests.

Also we wrote a JUnit testing class to test all this functionality to make sure our gRPC server and gRPC client are working properly

--

--

Murat Kilic

Tech enthusiast and leader. Love inspiring people to follow their dreams in tech. Coded all the way from BASIC to Go.