Pull to refresh

Принуждение к асинхронности в Java сервисах для Baratine

Reading time 4 min
Views 7.8K

Baratine сервер для микро-сервисов — одна из самых необычных платформ над которой мне довелось работать. В основе дизайна этого сервера лежат несколько дополняющих друг друга принципов.


  • Асинхронные сервис интерфейсы
  • Выполнение вызовов сервиса в едином потоке
  • Неразделённое владение данными
  • Асинхронный Web
  • Асинхронная платформа исполнения сервисов

    Асинхронные сервис интерфейсы


Микро-сервисы в Baratine описываются интерфейсами. Интерфейс определяет операции предоставляемые сервисом. Особенностью асинхронного интерфейса является то, что методы интерфейса возвращают результат асинхронно, подобно объекту Future.


Например привычный интерфейс для операции оплаты кредитной картой может выглядеть следующим образом:


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

Такой метод возвращает результат по факту проведения оплаты, а код использующий его, выглядит следующим образом:


CreditService _creditService;

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

Асинхронный интерфейс не возвращает результат, а заполняет Future–объект асинхронно:


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

Пользовательский код для такого интерфейса может выглядеть следующим образом:


CreditService _creditService;

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

Особенностью этот клиентского кода является то, что код передаёт свой Future-объект в конечный сервис Payment с помощью result.then().


В тех случаях когда клиенту нужно дополнительно обработать результат можно использовать лямбду, которая будет вызвана по заполнению результата:


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

На первый взгляд асинхронные интерфейсы могут показаться не совсем удобными, но такая организация кода позволяет быстро высвобождать потоки для выполнения следующих задач, а клиенты получают результаты по их готовности.


Выполнение вызовов сервиса в едином потоке


Микро-сервисы в Baratine выполняются в одном, выделенном этому сервису, потоке. Поток выделяется сервису сразу по появлению вызовов. В общем случае вызовы к сервису идут от множества клиентов. Вызовы помещаются в очередь и выполняются последовательно одним выделенным потоком.


В этом контексте следует отметить что сервисы должны быть написаны таким образом, чтобы не занимать поток ожиданием выполнения операций. Для этого используются Future–объекты типа io.baratine.service.Result (см. выше). Именно они позволяют перенести обработку результата вызова дорогих блокирующих операций в будущее.


Например, оплата с использованием чекового счёта может занять несколько часов, а пользовательский код инициации оплаты будет выполнен в реальном времени за доли миллисекунды.


CheckingService _checkingService = ...;

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

В приведённом выше коде исполнение лямбды вызова then() будет отложено до возвращения _checkingService'ом результата оплаты, a метод executePayment() моментально становиться доступным для следующего вызова.


Выполнение в едином потоке положительно сказывается на производительности через сокращение числа смены контекстов и хорошее согласование с процессорным кэшем.


Неразделённое владение данными


Владение правами записи на мастер-копию — одна из отличительных особенностей архитектуры микро-сервисов на Baratine. Так как микро-сервис обрабатывает вызовы последовательно, а не параллельно, данные могут храниться в памяти единичного экземпляра (singleton) сервиса и всегда является последней, наиболее актуальной копией данных.


(В данном случае использование слова "копия" не совсем уместно и используется идиоматично).


Микро-сервис с данными имеет расширенный жизненный цикл, в котором, прежде чем сервис поступит в использование, Baratine выполнит метод сервиса с аннотацией @OnLoad или загрузит поля экземпляра из асинхронной объектной базы данных (Kraken) являющейся частью Baratine.


Микро-сервис подкреплённый данными может представлять пользователя системы следующим образом:


@Asset
public class User
{
  @Id
  private IdAsset _id;

  private UserData _data;
}

В приведённом выше коде экземляр UserData с данными о пользователе будет загружен из Kraken.


Асинхронный Web


Для достижения быстродействия и лучшего сопряжения с асинхронными сервисами принцип асинхронности подчинил себе и выполнение Web запросов. Это достигается при помощи Future–объекта для ответа.


io.baratine.web.RequestWeb, подобно io.baratine.service.Result предоставляет возможность отложить заполнение ответа до тех пор, пока не будут готовы данные для ответа.


К примеру, код для запроса по протоколу REST может выглядеть следующим образом:


@Service
public class QuoteRestService
{
  QuoteService _quotes;

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

В приведенном выше коде метод quote() помечен аннотацией Get и это делает метод доступным для Web запросов. В Baratine платформе единственный экземпляр сервиса отвечает на все приходящие запросы в одном, отведённом для этого сервиса, потоке. В такой архитектуре производительность достигается быстрым возвратом из метода quote() с помощью делегирования операции по запросу конкретной quote сервису отвечающему за Quotes — QuoteService.


Асинхронная платформа исполнения сервисов


В процессе работы над платформой сама собою стала выкристаллизовываться тенденция распространения асинхронности на компоненты платформы. Таким образом все встроенные сервисы предоставляемые платформой являются асинхронными.


Так в результате разработки системы появились сервисы базы данных (Kraken), Scheduling, Events, Pipe, Web; и все они починились правилу тяготения к асинхронности.


Как одному из разработчиков этой системы мне было бы очень интересно узнать мнение хабра-сообщества о Baratine.

Tags:
Hubs:
+15
Comments 29
Comments Comments 29

Articles