Оказывается, в Akka невозможно сделать дэдлок
19 ноября 2014
Как нам с вами известно из опыта программирования на Erlang, при работе с акторами иногда могут возникать дэдлоки. Актор А1 шлет запрос с помощью gen_server::call
актору А2, тот в свою очередь спрашивает что-то у А1, но А1 ему не отвечает, так как сам еще ждет ответа от А2. Дэдлок, отваливаемся по таймауту. К счастью, когда вы сталкиваетесь с этой проблемой, в логах есть стектрейсы, позволяющие легко диагностировать и исправить ошибку. Так вот, а совсем недавно я узнал, что в Akka дэдлок в этом случае не произойдет… за исключением одного граничного случая, о котором будет рассказано далее.
Рассмотрим следующие код:
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:
FirstActorResponse(Seq("fake_a1_resp"))
}
В результате A1 получает ответ и в свою очередь шлет ответ пользователю. При этом сообщения, обмен которым производится, содержат в себе трейс всего происходящего. В этом, самом простом, случае трейс будет таким:
Теперь, собственно, рассмотрим интересующий нас случай. При получении запроса от A1 актор A2 в свою очередь ходит за какой-то информацией обратно в актор A1:
.mapTo[FirstActorSimpleResponse]
Как уже отмечалось, в Erlang в этом случае мы бы получили дэдлок. Однако в Akka все прекрасно отработает:
Так происходит по той причине, что акторы в Akka не блокируются, а сразу возвращают футуры. Очень хорошо!
Наконец, рассмотрим обещанный граничный случай. При получении запроса от A1 актор A2 идет в A1 с тем же запросом, что вызвал обращение к A2:
То есть, получается, что A1 спрашивает что-то у A2, тот спрашивает у A1, тот снова спрашивает у A2 и так до бесконечности. При этом создаются новые и новые футуры. Легко догадаться, что память очень быстро закончится (или не так уж быстро, если вы забыли про -Xmx
) и мы увидим:
Строго говоря, это не дэдлок — программа делает в точности то, что вы ей сказали, непрерывно шлет запросы от A1 к A2 и обратно.
Такое вот интересное наблюдение.
Дополнение: Пример работы с шедулером в Akka
Метки: Akka, Scala, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.