EliseeAlex.me

Шаг 2

Akka на практике, первые шаги

Введение в акку, разбираемся с примитивами и учимся использовать их в собственной архитектуре

В прошлой статье я рассказал об общих идея акки.

В этой статье я покажу на практике, как устроены акторы и как они взаимодействуют. Я расскажу о том, как создаются акторы, как они общаются между собой и том, как нужно обрабатывать ошибки.

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

Mailbox

Задачи, которые обрабатывает актор — это сообщения, полученные от других акторов. В стандартном поведении акки, актор достаёт сообщения из специального списка (почтового ящика) и обрабатывает их.

Кладутся эти сообщения путём передачи объектов актору по ссылке (ActorRef или ActorSelection) и делается это примерно так:

actor ! message

В результате этой команды, в почтовом ящике актора, который лежит по ссылке окажется сообщение . Как только сообщение попадёт в почтовый ящик, исполнение этой команды закончится и вызывающий актор продолжит работу. В этом случае говорят, что актор не блокируется (если бы он ожидал исполнения команды, он оказался заблокированым и не мог выполнять другие задачи в это время). Но это также значит, что актор не получает результата исполнения этой команды (вообще, он может его получить в виде ответного сообщения или через другие скаловские примитивы).

Что происходит дальше с сообщением?

Основная логика актора сосредоточена в методе recieve, в нём обрабатываются сообщения. Сообщение в этот метод передаёт система акторов. Акка даёт гарантии, в каком порядке будут доставлены сообщения, но задумываться об этом стоит только в том случае, если без этого не обойтись.

На обработку сообщения лучше посмотреть на примере:

import akka.actor.Actor
import akka.actor.Props
import akka.event.Logging

class MyActor extends Actor {
  val log = Logging(context.system, this)

  def receive = {
    case "test" => log.info("received test")
    case myObject: MyObject => log.info("received MyObject")
    case MyClass(x, _) => log.info(s"received an instance of MyClass with first field [$x]")
    case x: Any => log.warn(s"received unknown message: $x")
  }
}

Мы создали актор и научили принимать его четыре вида сообщений. Результаты приёма мы записываем в логи. Для обработки сообщений используется Pattern Matching, эта конструкция, похожа на switch...case, но более универсальна. В частности, её можно использовать для определения типа объекта, как во второй конструкции Case или даже для определения типа объекта и получения значения полей объекта, как показано на третьей строчке.

Как получить ссылки на другие акторы?

Для отправки сообщения нужна ссылка на актор, который будет его обрабатывать. Есть два вида ссылок, о которых я уже упомянул: ActorSelection и ActorRef. Их отличие в том, что ActorSelection может указывать сразу на несколько акторов и доставляет сообщения сразу в несколько почтовых ящиков, а ActorRef — это ссылка на один конкретный актор.

Получить ActorSelection можно, указав путь до актора. Это может быть абсолютный или относительный путь, в этом пути могут содержаться wildcard’ы:

context.actorSelection("../brother") ! msg
context.actorSelection("/user/serviceA") ! msg
context.actorSelection("../*") ! msg

Относительный путь имеет отношение к иерархии акторов, о ней мы поговорим чуть ниже.

Ссылку ActorRef получить проще. У любого актора есть родитель и на него можно получить ссылку. Эта ссылка прикладывается к каждому сообщения и можно получить такую ссылку вызвав метод sender(). Эту ссылку можно передавать в конструкторе при создании актора. И, наконец, эта ссылка получается при создании актора:

val myActor: ActorRef = Actor.of(Props.create(MyActor.class))

actorOf vs actorSelection

actorOf создаёт актор, а actorSelection ищет.

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

Кто должен создавать акторы?

Актор, который был создан внутри актора становится его ребёнком. Актор-родитель становится ответственным за обработку ошибок ребёнка и может либо обработать ошибку самостоятельно, либо делегировать его своему родителю. Один из принципов, лежащий в основе акторов, — Let It Crash связан именно с этим явлением. В иерархии акторов должен быть какой-то родитель, который решит, что делать со своими детьми в случае ошибки. Зная это, можно не задумываться об ошибках на нижних уровнях и не писать «defensive code».

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

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

Практика

Учитывая эти особенности акки, можно построить первый набросок архитектуры. Для начала рассмотрим системную архитектуру:

Диаграмма компонентов сервиса аналитики dynamica

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

  • интеграция с InfluxDb и абстрагирование базы данных;
  • бизнес-логика, связанная с подготовкой статистики для пользователей;
  • веб-сервер, который предоставляет Rest Api клиентам, принимает запросы и подготавливает ответы клиентам.

И для этих трёх компонентов я создам три актора верхнего уровня. Они будут обрабатывать ошибки и заниматься конфигурированием. А в акторах нижнего уровня будет содержаться вся бизнес-логика.

В следующей статье [создадим и запустим проект на Scala](/dynamica-1/3-configuration).

Пишите свои пожелания на eliseealex@gmail.com