Оказывается, в Akka невозможно сделать дэдлок

19 ноября 2014

Как нам с вами известно из опыта программирования на Erlang, при работе с акторами иногда могут возникать дэдлоки. Актор А1 шлет запрос с помощью gen_server::call актору А2, тот в свою очередь спрашивает что-то у А1, но А1 ему не отвечает, так как сам еще ждет ответа от А2. Дэдлок, отваливаемся по таймауту. К счастью, когда вы сталкиваетесь с этой проблемой, в логах есть стектрейсы, позволяющие легко диагностировать и исправить ошибку. Так вот, а совсем недавно я узнал, что в Akka дэдлок в этом случае не произойдет… за исключением одного граничного случая, о котором будет рассказано далее.

Рассмотрим следующие код:

package me.eax.akka_examples

import akka.actor._
import akka.pattern.{ask, pipe}
import akka.util.Timeout

import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

case class SecondActorRequest(second: ActorRef)
case class SecondActorResponse(trace: Seq[String])

case object FirstActorRequest
case class FirstActorResponse(trace: Seq[String])

case object FirstActorSimpleRequest
case class FirstActorSimpleResponse(trace: Seq[String])

class SecondActor extends Actor {
  implicit val timeout = Timeout(5 seconds)

  def receive = {
    case r: SecondActorRequest =>
      val fResp = /*** UNDER CONSTRUCTION ***/
      fResp.map(resp => SecondActorResponse(resp.trace :+ "a2_resp"))
           .pipeTo(sender)
  }
}

class FirstActor extends Actor {
  implicit val timeout = Timeout(5 seconds)
  val secondActor = context.actorOf(Props(new SecondActor), "A2")

  def receive = {
    case FirstActorRequest =>
      val fResp = (secondActor ? SecondActorRequest(self))
                    .mapTo[SecondActorResponse]
      fResp.map(resp => FirstActorResponse(resp.trace :+ "a1_resp"))
           .pipeTo(sender)

    case FirstActorSimpleRequest =>
      sender ! FirstActorSimpleResponse(Seq("a1_simple_resp"))
  }
}

object Example3 extends App {
  implicit val timeout = Timeout(5 seconds)
  val system = ActorSystem("system")
  val firstActor = system.actorOf(Props(new FirstActor), "A1")

  val fResp = (firstActor ? FirstActorRequest).mapTo[FirstActorResponse]
  fResp map { resp =>
    println(s"RESPONSE: $resp")
    system.shutdown()
  }

  system.awaitTermination()
}

Итак, в A1 приходит запрос от пользователя, он шлет запрос A2… в этом месте возможны варианты. Рассмотрим самый простой, A2 вообще пока что не ходит в актор A1:

val fResp = Future successful { FirstActorResponse(Seq("fake_a1_resp")) }

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

List(fake_a1_resp, a2_resp, a1_resp)

Теперь, собственно, рассмотрим интересующий нас случай. При получении запроса от A1 актор A2 в свою очередь ходит за какой-то информацией обратно в актор A1:

val fResp = (r.second ? FirstActorSimpleRequest)
              .mapTo[FirstActorSimpleResponse]

Как уже отмечалось, в Erlang в этом случае мы бы получили дэдлок. Однако в Akka все прекрасно отработает:

List(a1_simple_resp, a2_resp, a1_resp)

Так происходит по той причине, что акторы в Akka не блокируются, а сразу возвращают футуры. Очень хорошо!

Наконец, рассмотрим обещанный граничный случай. При получении запроса от A1 актор A2 идет в A1 с тем же запросом, что вызвал обращение к A2:

val fResp = (r.second ? FirstActorRequest).mapTo[FirstActorResponse]

То есть, получается, что A1 спрашивает что-то у A2, тот спрашивает у A1, тот снова спрашивает у A2 и так до бесконечности. При этом создаются новые и новые футуры. Легко догадаться, что память очень быстро закончится (или не так уж быстро, если вы забыли про -Xmx) и мы увидим:

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "system-scheduler-1"

Следует однако отметить, что на практике вы вряд ли столкнетесь с описанной проблемой, так как соответствующий код будет довольно бессмысленным. А если и столкнетесь, то очень быстро, еще до попадания на прод, обнаружите и исправите ошибку, особенно если вы логируете все сообщения. И вообще, строго говоря, это даже и не совсем дэдлок — программа делает в точности то, что вы ей сказали, непрерывно шлет запросы от A1 к A2 и обратно.

Другими словами, с Akka можно работать, не думая о дэдлоках. Ура футурам!

Дополнение: Пример работы с шедулером в Akka

Метки: , , .

Подпишись через RSS, E-Mail, Google+, Facebook, Vk или Twitter!

Понравился пост? Поделись с другими: