Обработка ошибок в актерах Akka

У меня есть очень простой пример, когда у меня есть Актер (SimpleActor), который выполняет периодическую задачу, отправляя сообщение самому себе. Сообщение запланировано в конструкторе актора. В обычном случае (т.е. без сбоев) все работает нормально.

Но что, если Актёру приходится иметь дело с ошибками. У меня есть еще один Актер (SimpleActorWithFault). У этого актера могут быть недостатки. В этом случае я генерирую его сам, выбрасывая исключение. Когда происходит сбой (т. е. SimpleActorWithFault выдает исключение), он автоматически перезапускается. Однако этот перезапуск портит планировщик внутри Актера, который больше не работает как исключенный. И если ошибки происходят достаточно быстро, это приводит к более неожиданному поведению.

Мой вопрос: какой предпочтительный способ обработки ошибок в таких случаях? Я знаю, что могу использовать блоки Try для обработки исключений. Но что, если я расширяю другого актера, где я не могу поместить попытку в суперкласс, или в каком-то случае, когда я являюсь исключенной ошибкой, происходит в актере.

import akka.actor.{Props, ActorLogging}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import akka.actor.Actor

case object MessageA

case object MessageToSelf


class SimpleActor extends Actor with ActorLogging {

  //schedule a message to self every second
  context.system.scheduler.schedule(0 seconds, 1 seconds, self, MessageToSelf)

  //keeps track of some internal state
  var count: Int = 0

  def receive: Receive = {
    case MessageA => {
      log.info("[SimpleActor] Got MessageA at %d".format(count))
    }
    case MessageToSelf => {
      //update state and tell the world about its current state 
      count = count + 1
      log.info("[SimpleActor] Got scheduled message at %d".format(count))

    }
  }

}


class SimpleActorWithFault extends Actor with ActorLogging {

  //schedule a message to self every second
  context.system.scheduler.schedule(0 seconds, 1 seconds, self, MessageToSelf)

  var count: Int = 0

  def receive: Receive = {
    case MessageA => {
      log.info("[SimpleActorWithFault] Got MessageA at %d".format(count))
    }
    case MessageToSelf => {
      count = count + 1
      log.info("[SimpleActorWithFault] Got scheduled message at %d".format(count))

      //at some point generate a fault
      if (count > 5) {
        log.info("[SimpleActorWithFault] Going to throw an exception now %d".format(count))
        throw new Exception("Excepttttttiooooooon")
      }
    }
  }

}


object MainApp extends App {
  implicit val akkaSystem = akka.actor.ActorSystem()
  //Run the Actor without any faults or exceptions 
  akkaSystem.actorOf(Props(classOf[SimpleActor]))

  //comment the above line and uncomment the following to run the actor with faults  
  //akkaSystem.actorOf(Props(classOf[SimpleActorWithFault]))

}

person Soumya Simanta    schedule 16.04.2014    source источник


Ответы (2)


Чтобы не испортить планировщик:

class SimpleActor extends Actor with ActorLogging {

  private var cancellable: Option[Cancellable] = None

  override def preStart() = {
    //schedule a message to self every second
    cancellable = Option(context.system.scheduler.schedule(0 seconds, 1 seconds, self, MessageToSelf))
  }

  override def postStop() = {
    cancellable.foreach(_.cancel())
    cancellable = None
  }
...

Для правильной обработки исключений (akka.actor.Status.Failure для правильного ответа на запрос в случае использования шаблона Ask отправителем):

...
def receive: Receive = {
    case MessageA => {
      try {
        log.info("[SimpleActor] Got MessageA at %d".format(count))
      } catch {
        case e: Exception =>
          sender ! akka.actor.Status.Failure(e)
          log.error(e.getMessage, e)
      }
    }
...
person Sergiy Prydatchenko    schedule 16.04.2014
comment
Нет, так актеры не справляются с неудачами: вы обременяете клиента внутренними неудачами актера. Вместо этого исключение должно быть передано руководителю субъекта и обработано на этом уровне. - person Roland Kuhn; 17.04.2014
comment
Да, я согласен. Но в некоторых случаях нам нужно моментально сообщить клиенту, что именно не удалось. В противном случае это всегда будет заканчиваться AskTimeoutException для клиента. - person Sergiy Prydatchenko; 17.04.2014

Правильный путь — запихнуть рискованное поведение в его собственного актора. Этот шаблон называется шаблоном ядра ошибки (см. Akka Concurrency, раздел 8.5):

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

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

Шаблон ядра ошибки подразумевает продвижение уровней риска ниже по дереву.

См. также другие руководство здесь.

Итак, в вашем случае это будет примерно так:

SimpleActor 
 |- ActorWithFault

Здесь SimpleActor действует как руководитель для ActorWithFault. Стратегия надзора по умолчанию для любого субъекта заключается в перезапуске дочернего элемента на Exception и повышении уровня на всех остальных: http://doc.akka.io/docs/akka/snapshot/scala/отказоустойчивость.html

Эскалация означает, что сам актор может быть перезапущен. Поскольку вы на самом деле не хотите перезапускать SimpleActor, вы можете сделать так, чтобы он всегда перезапускал ActorWithFault и никогда не эскалировался, переопределив стратегию супервизора:

class SimpleActor {
  override def preStart(){
    // our faulty actor --- we will supervise it from now on
    context.actorOf(Props[ActorWithFault], "FaultyActor") 
  ...

  override val supervisorStrategy = OneForOneStrategy () {
    case _: ActorKilledException => Escalate
    case _: ActorInitializationException => Escalate
    case _ => Restart // keep restarting faulty actor
  }

}
person Andriy Drozdyuk    schedule 16.04.2014
comment
см. также общее руководство: doc.akka.io/docs/akka/ snapshot/general/supervision.html - person Rob Starling; 16.04.2014
comment
Я нахожу эту страницу немного многословной :( - person Andriy Drozdyuk; 16.04.2014
comment
Слова, безусловно, стоит прочитать, потому что этот способ обработки ошибок намного мощнее, чем традиционный способ «винить во всем клиента». С актерами вы разделяете ошибки проверки (которые отправляются клиенту как обычные ответы) и фатальные сбои (которые обрабатываются супервизором). - person Roland Kuhn; 17.04.2014
comment
Согласовано. Только что добавил еще одну ссылку на отличный учебник Даниэля: danielwestheide.com/blog/2013/03/20/ - person Andriy Drozdyuk; 17.04.2014