← На главную

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

Как нам с вами известно из опыта программирования на 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