ActiveRPC | Memcached-like Application

Introduction

In this guide we will create a memcached-like client-server application that is based on the RPC communication protocol and ActiveJ technologies.

You can find full example sources on GitHub.

Memcached Client and Server Modules

First of all, consider the initial ActiveRPC implementation of these modules since our application will be built with their help:

Note: This implementation covers only the basic usage. You may add more features as your application requires.

Create Client and Server

Let’s write our own MemcacheLikeServer server. We will also use ActiveJ Launcher to manage application lifecycle and lightning-fast Dependency Injection library ActiveInject:

public class MemcacheLikeServer extends Launcher {
	@Inject
	WorkerPool.Instances<RpcServer> instances;

	@Provides
	WorkerPool workerPool(WorkerPools workerPools) {
		return workerPools.createPool(3);
	}

	@Provides
	Config config() {
		return Config.create()
				.with("memcache.buffers", "4")
				.with("memcache.bufferCapacity", "64mb");
	}

	@Override
	protected Module getModule() {
		return ModuleBuilder.create()
				.install(ServiceGraphModule.create())
				.install(MemcacheMultiServerModule.create())
				.install(WorkerPoolModule.create())
				.build();
	}

	@Override
	protected void run() throws Exception {
		awaitShutdown();
	}

	public static void main(String[] args) throws Exception {
		MemcacheLikeServer server = new MemcacheLikeServer();
		server.launch(args);
	}
}

Since we extend Launcher, we need to override 2 methods: getModule to provide ServiceGraphModule and run to describe the main logic of the example.

  • The number of server shards equals the number of workerPools.
  • As for the “memcached” functionality - we specify the number of buffers and their capacity in the Config.
  • Config is used to set up everything MemcacheMultiServerModule needs to handle upcoming requests.

Our MemcacheLikeClient will create put and get requests:

public class MemcacheLikeClient extends Launcher {
	@Provides
	Eventloop eventloop() {
		return Eventloop.create();
	}

	@Provides
	RawMemcacheClientAdapter rawMemcacheClientAdapter(RawMemcacheClient client) {
		return new RawMemcacheClientAdapter(client);
	}

	@Provides
	Config config() {
		return Config.create()
				.with("protocol.compression", "false")
				.with("client.addresses", "localhost:9000, localhost:9001, localhost:9002");
	}

	@Inject
	RawMemcacheClientAdapter client;

	@Inject
	Eventloop eventloop;

	@Override
	protected Module getModule() {
		return ModuleBuilder.create()
				.install(ServiceGraphModule.create())
				.install(MemcacheClientModule.create())
				.install(ConfigModule.create()
						.withEffectiveConfigLogger())
				.build();
	}

	@Override
	protected void run() throws ExecutionException, InterruptedException {
		String message = "Hello, Memcached Server";

		System.out.println();
		CompletableFuture<Void> future = eventloop.submit(() ->
				sequence(
						() -> Promises.all(range(0, 25).mapToObj(i ->
								client.put(i, message))),
						() -> Promises.all(range(0, 25).mapToObj(i ->
								client.get(i).whenResult(res -> System.out.println(i + " : " + res))))));
		future.get();
		System.out.println();
	}

	public static void main(String[] args) throws Exception {
		Launcher client = new MemcacheLikeClient();
		client.launch(args);
	}
}
  • Since MemcacheClientModule uses Rendezvous Hashing Strategy to achieve agreement for requests between shards of servers, in client we need to provide the addresses of these shards - 9001, 9002, and 9003 ports.
  • In the Eventloop we ask to put a message in the current i of the bytes[i] array, and get it back from the corresponding cell.
  • So the client will perform these operations asynchronously for three shards, therefore we will receive a disordered output block as a result.
  • RawMemcacheClientAdapter is a manually created class. It uses RawMemcacheClient (a MemcacheClient implementation) and defines the logic of put and get methods for our MemcacheLikeClient.

Running the application

  • First, open and run the MemcacheLikeServer to launch the server;
  • To launch the client, run main() method of MemcacheLikeClient;

You will see roughly the same output in the server’s console (... omits similar messages):

Server on port #9000 accepted message!
Server on port #9000 accepted message!
...
Server on port #9000 accepted message!
Server on port #9002 accepted message!
Server on port #9002 accepted message!
...
Server on port #9002 accepted message!
Server on port #9001 accepted message!
...
Server on port #9001 accepted message!

In the client console you will see a similar output confirming the client received asynchronous responses from the server:

0 : Hello, Memcached Server
3 : Hello, Memcached Server
4 : Hello, Memcached Server
...
11 : Hello, Memcached Server
13 : Hello, Memcached Server
...
20 : Hello, Memcached Server
21 : Hello, Memcached Server
24 : Hello, Memcached Server
...
17 : Hello, Memcached Server
19 : Hello, Memcached Server