Forcing asynchrony in Java services for Baratine

Baratine server for micro-services is one of the most unusual platforms I have worked on. The design of this server is based on several complementary principles.


  • Asynchronous Service Interfaces
  • Making service calls in a single thread
  • Undivided Data Ownership
  • Asynchronous Web
  • Asynchronous Service Execution Platform

    Asynchronous Service Interfaces


Micro services in Baratine are described by interfaces. The interface defines the operations provided by the service. A feature of the asynchronous interface is that the interface methods return the result asynchronously, like a Future object.


For example, a familiar interface for a credit card payment transaction might look like this:


public interface CreditService {
    PaymentStatus pay(int amount, CreditCard card); 
}

This method returns the result upon payment, and the code using it looks like this:


CreditService _creditService;
PaymentStatus executePayment(int amount, Client client) {
  return _creditService.pay(amount, client.getCreditCard());      
}

The asynchronous interface does not return the result, but fills the Future object asynchronously:


public interface CreditService {
    void pay(int amount, CreditCard card, Result result);
}

User code for such an interface might look like this:


CreditService _creditService;
void executePayment(int amount, Client client, Result result) {
  return _creditService.pay(amount, client.getCreditCard(), result.then());      
}

A feature of this client code is that the code passes its Future object to the final Payment service using result.then ().


In cases where the client needs to further process the result, you can use a lambda that will be called upon filling out the result:


  void executePayment(int amount, Client client, Result result)
  {
    _creditService.pay(amount,
                       client.getCreditCard(),
                       result.then(
                         status -> {
                           log(status);
                           return status;
                         }
                       ));
  }

At first glance, asynchronous interfaces may not seem very convenient, but such an organization of the code allows you to quickly release threads to perform the following tasks, and clients get results when they are ready.


Making service calls in a single thread


Micro-services in Baratine are executed in one thread allocated to this service. The flow is allocated to the service immediately upon the appearance of calls. In general, calls to the service come from many clients. Calls are queued and executed sequentially by one dedicated thread.


In this context, it should be noted that services should be written in such a way as not to occupy the thread while waiting for operations to complete. For this, Future objects of the io.baratine.service.Result type are used (see above). They allow us to transfer the processing of the result of the call of expensive blocking operations to the future.


For example, payment using a check account may take several hours, and a custom payment initiation code will be executed in real time in a fraction of a millisecond.


CheckingService _checkingService = ...;
void executePayment(int amount, Client client, Result result) 
{
  _checkingPayment.pay(amount,
                       client.getCheckingAccInfo(),
                       result.then(
                         status-> {
                           log(status);
                           if (status.isAppoved()) {
                             shipGoods();
                           } else {
                             handleFailedPayment(status);
                           }
                         }
                       ));
  );
}

In the above code, the execution of the lambda of the then () call will be delayed until _checkingService returns the payment result, and the executePayment () method instantly becomes available for the next call.


Running in a single thread has a positive effect on performance by reducing the number of context switches and good consistency with the processor cache.


Undivided Data Ownership


Owning write access to a master copy is one of the hallmarks of the micro-service architecture on Baratine . Since the micro-service processes calls sequentially, and not in parallel, the data can be stored in the memory of a single instance of the service and is always the last, most relevant copy of the data.


(In this case, the use of the word "copy" is not entirely appropriate and is used idiomatically).


The micro data service has an extended life cycle in which, before the service goes into use, Baratine will execute the service method with the @OnLoad annotation or load the instance fields from the asynchronous object database (Kraken) which is part of Baratine .


A micro-service backed by data can represent a system user as follows:


@Asset
public class User
{
  @Id
  private IdAsset _id;
  private UserData _data;
}

In the above code, a UserData instance with user data will be loaded from Kraken.


Asynchronous Web


To achieve speed and better interfacing with asynchronous services, the principle of asynchrony subjugated the execution of Web requests. This is achieved using a Future response object.


io.baratine.web.RequestWeb, like io.baratine.service.Result, provides the ability to defer filling out a response until the data for the response is ready.


For example, the code for a request via REST may look like this:


@Service
public class QuoteRestService
{
  QuoteService _quotes;
  @Get
  public void quote(@Query("symbol") String symbol, RequestWeb requestWeb)
  {
    _quotes.quote(symbol, requestWeb.then(quote -> quote));
  }
}

In the above code, the quote () method is marked with the Get annotation and this makes the method available for Web requests. In the Baratine platform, a single instance of a service responds to all incoming requests in a single thread allocated for this service. In such an architecture, performance is achieved by quickly returning from the quote () method by delegating the operation on request to a specific quote to the service responsible for Quotes - QuoteService.


Asynchronous Service Execution Platform


In the process of working on the platform, the tendency for the spread of asynchrony to the platform components began to crystallize by itself. Thus, all embedded services provided by the platform are asynchronous.


So as a result of the development of the system appeared database services (Kraken), Scheduling, Events, Pipe, Web; and they all repaired the rule of gravity to asynchrony.


As one of the developers of this system, I would be very interested to know the opinion of the habr community about Baratine .


Also popular now: