Microservice

../../_images/example-protocol-module.png

Microservice

Introduction

A microservice service is useful to convert an existing multi-threaded, blocking module into a loosely coupled service. When the existing pre-service module is a blocking module, it needs multiple workers to support concurrent requests.

The module pattern can be used in an existing deployment, for example in a servlet web-app. Baratine’s service creates a loosely-coupled boundary around the service.

Used for:

  • Existing multithreaded modules
  • Existing deployment (in JVM services)

Baratine microservices have the following properties:

  • Use Java APIs
  • Can support asynchronous results with Result.
  • Have isolated service thread contexts
  • Have inbox queue for performance under heavy load and synchronization
  • Can support multiple worker threads for legacy modules

Benefits:

  • Context Boundary
  • Loose coupling
  • Async callers
  • Testability
  • Isolation
  • Some CPU affinity
  • Worker limits

Examples:

  • internal modules in a monolithic application
  • lucene library
  • Data-access service for Hibernate

Service Source code

This example will show how to deploy an existing module as a microservice deployed in a Baratine server.

  • MyModule - existing module to be transformed into a microservice
  • MyModuleService - API for the module as a service
  • MyModuleServiceImpl - service worker implementation

Existing module (MyModule)

public class MyModule
{
  String myMethod(String arg1);
}

Service API (MyModuleService)

The MyModuleService interface defines the proxy API. By convention, MyModuleService defines the non-blocking calls and MyModuleServiceSync defines the blocking versions. The non-blocking version can be used by callers that want an async response.

MyModuleService.java:

import io.baratine.core.Result;

public interface MyModuleService
{
  void myMethod(String arg, Result<String> result);
}

The synchronous version MyModuleServiceSync is used for existing Java clients of the module and for unit testing.

MyModuleServiceSync.java:

import io.baratine.core.Result;

public interface MyModuleServiceSync extends MyModuleService
{
  String myMethod(String arg);
}

Service workers (MyModuleServiceImpl)

The module service uses multiple worker instances defined by @Workers. Because each worker is instantiated independently, the worker will need to obtain the shared legacy module. In this example, we use an @Inject to obtain the shared instance.

Each worker gets its own implementation instance. Since that instance is single threaded, the worker can save per-thread objects like connections in the instance.

Annotate the service with @Workers and inject the shared instance:

@Service("public:///my-module")
@Workers(64)
public class MyModuleServiceImpl implements MyModuleService
{
  private @Inject MyModule _myModule;
  ...

  public void myMethod(String arg, Result<String> result)
  {
    result.complete(_myModule.myMethod(arg));
  }
}

The application can also use an @OnInit to initialize the MyModule instance, giving it more flexibility to obtain the module.

public class MyModuleServiceImpl implements MyModuleService
{
  private MyModule _myModule;
  ...
  @OnInit
  private void onInit()
  {
    _myModule = myModuleLookup();
  }

Client Access

After it’s deployed, the service is available at the deployed server. The client can connect to the service in several ways:

  • A “pod:” proxy for Java clients using a binary protocol
  • HTTP REST
  • HTTP call using jamp-rpc
  • WebSockets using jamp
  • HTTP long polling using jamp as a fallback to WebSockets

Client libraries are available for JavaScript, PHP, and Python, as well as calling from Java. Since JAMP is a JSON-RPC protocol, it’s straightforward to create other clients.

See HTTP/WebSocket Clients for more information on remote calls using Baratine.

Java Web-App Servlet Client

The ServiceManager can provide a proxy to the module service. In a servlet, the code looks like:

public void init()
{
  super.init();

  ServiceManager manager = ServiceManager.current();

  MyModuleServiceSync myModule = manager.lookup("pod://pod/my-module")
                                        .as(MyModuleServiceSync.class);
}

Since the proxy is thread-safe, it can be looked up once during initialization and used for deployment.

To locate the server, the baratine.jar web-app uses a WEB-INF/baratine.cf file.

WEB-INF/baratine.cf:

cluster {
  server 10.0.0.1 8085;
  server 8084;
}

Deploying to a Baratine Server

After the service is compiled into a my-service.jar, it can be deployed to a Baratine server:

$ bin/baratine start
$ bin/baratine deploy my-service.jar

Non-Baratine Programmatic Deployment

The service module pattern can be used outside of a Baratine container. For example, during testing. The ServiceManager interface includes a newManager method, which creates a new ServiceManager that can be used to create and lookup the service module.

ServiceManager manager = ServiceManager.newManager().build();

MyModule module = new MyModule();

manager.newService()
       .address("/my-module")
       .service(()->new MyModuleServiceImpl(module))
       .workers(32)
       .build();

MyModuleServiceSync myModule = manager.lookup("/my-module")
                                      .as(MyModuleServiceSync.class);

Testing (jUnit)

Showing two versions:

  • Using programmatic Baratine
  • With RunnerBaratine

Programmatic Testing

When a test needs control over Baratine and the service bindings, it can create an embedded Baratine manager to bind and create services. Mock services can be easily bound in place of the implementation services.

{
  @org.junit.Test
  public void test() throws InterruptedException
  {
    try (ServiceManager manager = ServiceManager.newManager().build()) {
      MyModule myModuleImpl = ...;

      MyModuleServiceSync myModule;
      myModule = manager.newService()
                        .address("public:///my-module")
                        .service(new MyModuleServiceImpl(myModuleImpl))
                        .build()
                        .as(MyModuleServiceSync.class);

     Assert.assertEquals("my-result", myModule.myMethod("my-test"));
   }
 }

RunnerBaratine Testing

RunnerBaratine simplifies the testing by configuring common service manager patterns, which avoids some of the housekeeping code.

import io.baratine.core.*;
import com.caucho.junit.*;

@RunWith(RunnerBaratine.class)
@ConfigurationBaratine(services=MyModuleServiceImpl.class)
public class Test
{
  @Lookup("/my-module")
  MyModuleService _myModule;

  @org.junit.Test
  public void test() throws InterruptedException
  {
    Assert.assertEquals("my-result", _myModule.myMethod("my-test"));
  }
}

Next Steps

As an example, Lucene has been converted into a Baratine microservice, including sharding Lucene to make it scalable.

The Bartil project is a microservice that provides distributed and persistent lists and maps, similar in function to Redis.

Papers:

Examples on GitHub:

  • Lucene (Lucene microservice)
  • Bartil (In-Memory map/list server like Redis)