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