MODULES

Remote Key-Value Storage

Purpose

In this guide we will create a remote key-value storage using RPC. App will have 2 basic operations: “put” and “get”

Introduction

During writing distributed application the common concern is what protocol to use for communication. There are two main options:

HTTP/REST

RPC

While HTTP is more popular and well-specified, it has some overhead. When performance is significant aspect of application, you should use something faster than HTTP. And for this purpose Datakernel Framework has RPC module which is based on fast serialzers and custom optimized communication protocol, which enables to greatly improve application performance.

Since we have two basic operations to implement (put and get), let’s first write down classes that will be used for communication between client and server. Specifically, PutRequest, PutResponse, GetRequest and GetResponse. Instances of these classes will be serialized using fast DataKernel Serializer, but to enable serializer to work, we should provide some meta information about this classes using appropriate annotations. The basic rules are:

Use @Serialize annotation with order number on getter of property. Ordering provides better compatibility in case when classes are changed

Use @Deserialize annotation with property name (which should be same as in getter) in constructor

Now, let’s write down a guice module for RPC server using Datakernel Boot, that will handle “get” and “put” requests (Note: if you are not familiar with Datakernel Boot, please take a look at Hello World HTTP Server Tutorial)

Now, let’s write RPC client. In order to create RPC client we should again indicate all the classes that will be used for communication and specify RpcStrategy. There is a whole bunch of strategies in RPC module (such as single-server, first-available, round-robin, sharding and so on) and the nice thing about them: all strategies can be combined. For example, if you want to dispatch requests between 2 shards, and each shard actually contains main and reserve servers, you can easily tell RPC client to dispatch request in a proper way using the following code:

Let’s also build RpcClientLauncher. In run() we will consider command line arguments and make appropriate requests to RpcServer.

publicclassRpcClientLauncherextendsLauncher{@InjectprivateRpcClientclient;@Inject@ArgsprivateString[]args;@OverrideprotectedCollection<Module>getModules(){returnasList(ServiceGraphModule.defaultInstance(),newRpcClientModule());}@Overrideprotectedvoidrun()throwsException{inttimeout=1000;if(args.length<2){thrownewRuntimeException("Command line args should be like following 1) --put key value 2) --get key");}switch(args[0]){case"--put":client.<PutRequest,PutResponse>sendRequest(newPutRequest(args[1],args[2]),timeout).whenComplete((response,err)->{if(err==null){System.out.println("put request was made successfully");System.out.println("previous value: "+response.getPreviousValue());}else{err.printStackTrace();}requestShutdown();});break;case"--get":client.<GetRequest,GetResponse>sendRequest(newGetRequest(args[1]),timeout).whenComplete((response,err)->{if(err==null){System.out.println("get request was made successfully");System.out.println("value: "+response.getValue());}else{err.printStackTrace();}requestShutdown();});break;default:thrownewRuntimeException("Error. You should use --put or --get option");}awaitShutdown();}publicstaticvoidmain(String[]args)throwsException{RpcClientLauncherlauncher=newRpcClientLauncher();launcher.launch(true,args);}}

As you can see, sendRequest method returns a CompletionStage, at which we could listen for its results asynchronously with lambdas.

Contratulation! We’ve finished writing code for this app. Let’s now complile it. In order to do it go to project root directory and enter the following command: