Статьи

Akka Typed: Первые шаги с набранными актерами в Scala

С выпуском Акки 2.4.0 | http://akka.io/news/] пару недель назад был добавлен экспериментальный модуль Akka Typed. С Akka Typed можно создавать и взаимодействовать с актерами безопасным способом. Поэтому вместо того, чтобы просто отправлять сообщения нетипизированному Actor, с помощью Akka Typed мы можем добавить проверку типов времени компиляции в наши взаимодействия с Actor. Что, конечно, отличная вещь! В этой первой статье об Akka Typed мы рассмотрим пару новых концепций Akka Typed и то, как вы можете создавать и общаться с актерами таким новым способом. Полный код, используемый в этой статье, можно найти в этой Gist .

Как всегда, давайте быстро покажем файл сборки SBT:

1
2
3
4
5
6
7
name := "akka-typed"
  
version := "1.0"
  
scalaVersion := "2.11.7"
  
libraryDependencies += "com.typesafe.akka" %% "akka-typed-experimental" % "2.4.0"

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

Простое статическое поведение

Сначала давайте посмотрим на код для простого статического актера. Обратите внимание, что мы показываем некоторые дополнительные классы case, чтобы сделать примеры немного более полезными:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import scala.concurrent.ExecutionContext.Implicits.global
  implicit val timeout = Timeout(5 seconds)
 
  // 1. First simple example, we'll create a typed actor
  // which just prints out the received message.
  println("Step 1: Using static Actor")
 
  // which can only receive HelloMsg message.
  sealed trait HelloMsg
  final case class HelloCountry(country: String) extends HelloMsg
  final case class HelloCity(city: String) extends HelloMsg
  final case class HelloWorld() extends HelloMsg
  final case class Hello(msg: String) extends HelloMsg
 
  // simple static actor, which just prints out the message
  val helloSayer = Static[HelloMsg] { msg =>
    println("Msg received:" + msg);
  }
 
  // create a new root actor and send it types messages
  val helloSystem: ActorSystem[HelloMsg] = ActorSystem("helloSayer", Props(helloSayer))
  helloSystem ! HelloCountry("Netherlands")
  helloSystem ! HelloWorld()
  helloSystem ! HelloCity("Waalwijk")
  helloSystem ! Hello("HelloHelloHello")

Сначала мы определим несколько простых последствий и набор классов case, которые мы отправим нашему Actor. Фактическое поведение определяется с помощью класса case Static. Статическое поведение, как следует из названия, обеспечивает неизменяемый субъект, который всегда выполняется одинаково. В этом случае мы просто распечатываем полученное сообщение. Чтобы создать актера из этого поведения, мы инициируем новую систему ActorSystem. Эту систему ActorSystem можно рассматривать как корневой субъект, и мы можем отправлять ему сообщения. Отправка сообщений происходит таким же образом, и результат этого фрагмента кода следующий:

1
2
3
4
5
Step 1: Using static Actor
Msg received:HelloCountry(Netherlands)
Msg received:HelloWorld()
Msg received:HelloCity(Waalwijk)
Msg received:Hello(HelloHelloHello)

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

Использование шаблона Ask

Другой важный шаблон актеров Akka — это способность использовать шаблон Ask. С помощью шаблона ask мы отправляем актеру сообщение и получаем Future [T], который содержит ответ. В Akka Typed вы используете для этого следующий подход:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
// 2. Now we use an actor that responds to the sending actor using the
  // ask pattern.
  Thread.sleep(1000)
  println("\n\nStep 2: Using reply Actor")
 
  // Simple case class, which is used for the ask pattern
  final case class HelloReply(say: String, replyTo: ActorRef[HelloMsg])
 
  // static actor that responds to the passed in actorRef
  val helloReplyer = Static[HelloReply] {msg =>
    msg.replyTo ! Hello(s"You said: ${msg.say} ")
  }
 
  // create a new system and use the ask pattern to send it messages.
  val replySystem: ActorSystem[HelloReply] = ActorSystem("hello", Props(helloReplyer))
  val response = replySystem ? { f:ActorRef[HelloMsg] => HelloReply("Hello", f)}
  // or use a shorter form: val response = replySystem ? (HelloReply("Hello", _))
  val res = Await.result(response, 5 seconds)
  println("Response recevied: " + res)

Для этого сообщения мы используем дополнительный класс case. Этот класс case содержит не только сообщение, но и actorRef, на который нужно ответить. Это сделано потому, что в Akka Typed мы не можем получить доступ к отправителю непосредственно в актере, поэтому нам нужно передать его во входящем сообщении. Поведение нашего актера очень простое. Мы просто отправляем новое сообщение обратно переданному субъекту через его actorRef. Интересно, что как запрос, так и ответ набираются. Чтобы спросить что-то об актере, мы используем знакомое «?» операции, и так как мы добавили поле replyTo в наш класс case, мы передаем анонимный актер, который создается, когда мы используем? функция. ? Операция возвращает Future [HelloMsg], на котором мы просто ждем в этом примере.

Когда мы запустим этот пример, мы получим следующее:

1
2
Step 2: Using reply Actor
Response recevied: Hello(You said: Hello )

Переключение поведения актера

Одна из замечательных особенностей актеров заключается в том, что они могут переключать поведение на основе обработанных сообщений. Это очень хорошо работает для реализации конечных автоматов, обработчиков протоколов и т. Д. С помощью Typed Actors мы, конечно, можем это сделать. Давайте сначала посмотрим на код для этого примера:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// two functions which we'll switch in the actor implementation. One
  // prints everything in lower case, the ohter in uppercase
  val f1 = (msg: HelloMsg) => {println(s"In total function: $msg".toLowerCase)}
  val f2 = (msg: HelloMsg) => {println(s"In total function: $msg".toUpperCase)}
 
  // create a new Total 'Actor'. It runs the first function and returns the second one
  // effectively switching the implementation between the two functions. Note that we use
  // the Total behavior for this example. With a Total we don't handle any system messages
  // of type [Signal], if we want to do that we could use the FullTotal instead
  def newTotal(f1: HelloMsg => Unit, f2: HelloMsg => Unit): Total[HelloMsg] = Total[HelloMsg] { msg =>
    f1(msg)
    newTotal(f2, f1)
  }
 
  val behavior1: Behavior[HelloMsg] = newTotal(f1, f2) // normally we can send all the base traits
  val behavior2: Behavior[HelloWorld] = newTotal(f1, f2).narrow // by using narrow we can limit the behavior to a type
 
  // now create a new actor, and use the function to create the stateless total
  val totalSystem: ActorSystem[HelloMsg] = ActorSystem("hello", Props(behavior1))
  totalSystem ! HelloCountry("Netherlands")
  totalSystem ! HelloWorld()
  totalSystem ! HelloCity("Waalwijk")
  totalSystem ! Hello("HelloHelloHello")

В этом фрагменте кода мы используем класс case Total [T] для реализации поведения переключения. С помощью класса Total case мы определяем поведение, которое необходимо выполнить при обработке сообщения. Кроме того, нам также необходимо вернуть новое поведение, которое будет выполняться при получении следующего сообщения. В этом примере мы переключаем два разных поведения. Первый распечатывает все в верхнем регистре, а другой в нижнем регистре. Таким образом, первое сообщение будет напечатано в нижнем регистре, второе — в верхнем, и так далее.

Это приводит к следующему выводу:

1
2
3
4
in total function: hellocountry(netherlands)
IN TOTAL FUNCTION: HELLOWORLD()
in total function: hellocity(waalwijk)
IN TOTAL FUNCTION: HELLO(HELLOHELLOHELLO)

Использование полного поведения

До сих пор мы только рассматривали, как обрабатывать сообщения. Помимо обработки сообщений, некоторые поведения могут также нуждаться в реагировании на события жизненного цикла. Традиционно это означало бы переопределение определенных функций жизненного цикла. Однако с помощью Akka Typed мы больше не имеем доступа к этим функциям. События жизненного цикла доставляются поведению так же, как и обычные сообщения. Если вам нужен прямой доступ к ним, вы можете использовать другой способ построения своего поведения. Для этого можно использовать класс Full [T]:

01
02
03
04
05
06
07
08
09
10
11
12
val fullBehavior = ContextAware[HelloMsg] { ctx =>
    println(s"We can access the context: $ctx")
    Full[HelloMsg] {
      case msg: MessageOrSignal[HelloMsg] => println(s"Recevied messageOrSignal: $msg"); Same[HelloMsg]
    }
  }
 
  val fullSystem: ActorSystem[HelloMsg] = ActorSystem("hello", Props(fullBehavior))
  fullSystem ! HelloCountry("Netherlands")
  fullSystem ! HelloWorld()
  fullSystem ! HelloCity("Waalwijk")
  fullSystem ! Hello("HelloHelloHello")

Как вы можете видеть, используя класс Full [T], мы получаем сообщение или сигнал в конверте MessageOrSignal, который мы можем обрабатывать так же, как обычно. Обратите внимание, что мы также добавили декоратор вокруг этого актера. Используя декоратор ContextAware, мы можем сделать контекст актора доступным для поведения (однако мы здесь больше его не используем).

Вывод из этих сообщений выглядит следующим образом:

1
2
3
4
5
6
7
8
9
We can access the context: akka.typed.ActorContextAdapter@ee559a3
Recevied messageOrSignal: Sig(akka.typed.ActorContextAdapter@ee559a3,PreStart)
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@ee559a3,HelloCountry(Netherlands))
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@ee559a3,HelloWorld())
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@ee559a3,HelloCity(Waalwijk))
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@ee559a3,Hello(HelloHelloHello))
  
// when the system is shutdown
Recevied messageOrSignal: Sig(akka.typed.ActorContextAdapter@ee559a3,PostStop)

Как видите, мы получаем либо сообщение, либо сигнал.

Сочетание поведения

В последнем примере этой статьи мы кратко рассмотрим, как сочетать поведения для создания новых. Для этого Akka Typed вводит следующие два оператора:

  • &&: отправляет сообщение обоим поведениям.
  • ||: Сначала отправляет сообщение левому поведению, если это поведение возвращает необработанный ответ, пробуется правая сторона.

В коде это выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
val andCombined = helloSayer && behavior1
  val andSystem: ActorSystem[HelloMsg] = ActorSystem("and",Props(andCombined))
  andSystem ! HelloCountry("Netherlands")
  andSystem ! HelloWorld()
  andSystem ! HelloCity("Waalwijk")
  andSystem ! Hello("HelloHelloHello")
 
 
  Thread.sleep(1000)
  println("\n\nStep 5a: Use the || combinator")
  // first try the left one, if it is unhandled tries the right one
  val orCombined = Total[HelloMsg]{
    case _ => {println("Can't handle it here!");Unhandled}
  } || fullBehavior
  val orSystem: ActorSystem[HelloMsg] = ActorSystem("or", Props(orCombined))
  orSystem ! HelloCountry("Netherlands")
  orSystem ! HelloWorld()
  orSystem ! HelloCity("Waalwijk")
  orSystem ! Hello("HelloHelloHello")

Система && очень проста, и мы просто повторно используем существующее поведение. Для || По функциональности мы объединяем новое поведение, которое просто всегда возвращает Unhanded, поэтому все сообщения должны передаваться поведению fullBehavior.

Результат для оператора && выглядит следующим образом:

1
2
3
4
5
6
7
8
Msg received:HelloCountry(Netherlands)
in total function: hellocountry(netherlands)
Msg received:HelloWorld()
IN TOTAL FUNCTION: HELLOWORLD()
Msg received:HelloCity(Waalwijk)
in total function: hellocity(waalwijk)
Msg received:Hello(HelloHelloHello)
IN TOTAL FUNCTION: HELLO(HELLOHELLOHELLO)

Как видите, сообщение обрабатывается обоими способами. Для || Оператор вывода выглядит так:

01
02
03
04
05
06
07
08
09
10
We can access the context: akka.typed.ActorContextAdapter@4cba0952
Recevied messageOrSignal: Sig(akka.typed.ActorContextAdapter@4cba0952,PreStart)
Can't handle it here!
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@4cba0952,HelloCountry(Netherlands))
Can't handle it here!
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@4cba0952,HelloWorld())
Can't handle it here!
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@4cba0952,HelloCity(Waalwijk))
Can't handle it here!
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@4cba0952,Hello(HelloHelloHello))

Здесь мы можем видеть это после «Не могу справиться с этим здесь!» сообщение правая сторона оператора вступает во владение.

первые выводы

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