从Supervisor重启后向actor发送消息

时间:2018-01-25 15:16:20

标签: scala akka actor

我正在使用BackoffSupervisor策略来创建一个必须处理某些消息的子actor。我想实现一个非常简单的重启策略,在异常的情况下:

  1. 儿童将失败的消息传播给主管
  2. 主管重新启动子节点并再次发送失败消息。

  3. 主管在3次重试后放弃

  4. Akka持久性一个选项
  5. 到目前为止,我所拥有的是:

    主管定义:

    val childProps = Props(new SenderActor())
    val supervisor = BackoffSupervisor.props(
      Backoff.onFailure(
        childProps,
        childName = cmd.hashCode.toString,
        minBackoff = 1.seconds,
        maxBackoff = 2.seconds,
        randomFactor = 0.2 
      )
        .withSupervisorStrategy(
          OneForOneStrategy(maxNrOfRetries = 3, loggingEnabled = true) {
            case msg: MessageException => {
              println("caught specific message!")
              SupervisorStrategy.Restart
            }
            case _: Exception => SupervisorStrategy.Restart
            case _              ⇒ SupervisorStrategy.Escalate
          })
    )
    
    val sup = context.actorOf(supervisor)
    
    
    sup ! cmd
    

    应该发送电子邮件的子actor,但是失败(抛出一些Exception)并将Exception传播回主管:

    class SenderActor() extends Actor {
    
      def fakeSendMail():Unit =  {
        Thread.sleep(1000)
        throw new Exception("surprising exception")
      } 
    
      override def receive: Receive = {
        case cmd: NewMail =>
    
          println("new mail received routee")
          try {
            fakeSendMail()
          } catch {
            case t => throw MessageException(cmd, t)
          }
    
      }
    }
    

    在上面的代码中,我将任何异常包装到自定义类MessageException中,该类传播到SupervisorStrategy,但是如何将它进一步传播给新的子进程以强制重新处理?这是正确的做法吗?

    编辑。我试图在preRestart钩子上向Actor重新发送消息,但不知何故钩子没有被触发:

    class SenderActor() extends Actor {
    
      def fakeSendMail():Unit =  {
        Thread.sleep(1000)
        //    println("mail sent!")
        throw new Exception("surprising exception")
      }
    
      override def preStart(): Unit = {
        println("child starting")
      }
    
    
      override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        reason match {
          case m: MessageException => {
            println("aaaaa")
            message.foreach(self ! _)
          }
          case _ => println("bbbb")
        }
      }
    
      override def postStop(): Unit = {
        println("child stopping")
      }
    
      override def receive: Receive = {
        case cmd: NewMail =>
    
          println("new mail received routee")
          try {
            fakeSendMail()
          } catch {
            case t => throw MessageException(cmd, t)
          }
    
      }
    }
    

    这给了我类似于以下输出的内容:

    new mail received routee
    caught specific message!
    child stopping
    [ERROR] [01/26/2018 10:15:35.690]
    [example-akka.actor.default-dispatcher-2]
    [akka://example/user/persistentActor-4-scala/$a/1962829645] Could not
    process message sample.persistence.MessageException:
    Could not process message <stacktrace>
    child starting
    

    但是没有来自preRestart的日志挂钩

4 个答案:

答案 0 :(得分:5)

未调用子级func createImage(from aView:UIView) -> UIImage { UIGraphicsBeginImageContextWithOptions(CGSize(width: aView.frame.width, height: aView.frame.height), true, 1) aView.layer.render(in: UIGraphicsGetCurrentContext()!) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image! } 挂钩的原因是因为preRestart在封面下方使用BackoffOnRestartSupervisor,因此使用stop-and-替换默认重启行为延迟启动行为与退避策略一致。换句话说,当使用Backoff.onFailure时,当孩子重新启动时,不会调用孩子的Backoff.onFailure方法,因为基础主管实际上会停止孩子,然后再次启动它。 (使用Backoff.onStop可以触发孩子的preRestart钩子,但这与当前的讨论相关。)

当主管的孩子重新启动时,preRestart API不支持自动重新发送消息:您必须自己实现此行为。重试消息的想法是让BackoffSupervisor的主管处理它。例如:

BackoffSupervisor

在上面的代码中,val supervisor = BackoffSupervisor.props( Backoff.onFailure( ... ).withReplyWhileStopped(ChildIsStopped) ).withSupervisorStrategy( OneForOneStrategy(maxNrOfRetries = 3, loggingEnabled = true) { case msg: MessageException => println("caught specific message!") self ! Error(msg.cmd) // replace cmd with whatever the property name is SupervisorStrategy.Restart case ... }) ) val sup = context.actorOf(supervisor) def receive = { case cmd: NewMail => sup ! cmd case Error(cmd) => timers.startSingleTimer(cmd.id, Replay(cmd), 10.seconds) // We assume that NewMail has an id field. Also, adjust the time as needed. case Replay(cmd) => sup ! cmd case ChildIsStopped => println("child is stopped") } 中嵌入的NewMail消息包含在自定义案例类中(为了便于将其与&#34; normal&#34; / new {区分开来{1}}消息)并发送给MessageException。在此上下文中,NewMail是创建self的角色。然后,这个封闭的演员使用single timer在某个时刻重播原始消息。这个时间点在未来应该足够远,以致self可能会耗尽BackoffSupervisor的重启尝试,这样孩子就有充足的机会进入&#34 ;良好的&#34;在收到重新发送的消息之前的状态。显然,无论子重启次数多少,此示例都只涉及一次重发消息。

另一个想法是为每条BackoffSupervisor条消息创建一个SenderActor - BackoffSupervisor对,并让SenderActorNewMail条消息发送给自己SenderActor勾。这种方法的一个问题是清理资源;即,当处理成功或孩子重新启动时,关闭NewMail(这将关闭他们各自的preStart孩子)。 BackoffSupervisors ID到SenderActor元组的地图(其中NewMail是对(ActorRef, Int)元素的引用,ActorRef是重启尝试的次数)在这种情况下会有所帮助:

BackoffSupervisor

请注意,上面示例中的Int采用class Overlord extends Actor { var state = Map[Long, (ActorRef, Int)]() // assuming the mail id is a Long def receive = { case cmd: NewMail => val childProps = Props(new SenderActor(cmd, self)) val supervisor = BackoffSupervisor.props( Backoff.onFailure( ... ).withSupervisorStrategy( OneForOneStrategy(maxNrOfRetries = 3, loggingEnabled = true) { case msg: MessageException => println("caught specific message!") self ! Error(msg.cmd) SupervisorStrategy.Restart case ... }) ) val sup = context.actorOf(supervisor) state += (cmd.id -> (sup, 0)) case ProcessingDone(cmdId) => state.get(cmdId) match { case Some((backoffSup, _)) => context.stop(backoffSup) state -= cmdId case None => println(s"${cmdId} not found") } case Error(cmd) => val cmdId = cmd.id state.get(cmdId) match { case Some((backoffSup, numRetries)) => if (numRetries == 3) { println(s"${cmdId} has already been retried 3 times. Giving up.") context.stop(backoffSup) state -= cmdId } else state += (cmdId -> (backoffSup, numRetries + 1)) case None => println(s"${cmdId} not found") } case ... } } SenderActor作为构造函数参数。后一个参数允许NewMail向封闭的actor发送自定义ActorRef消息:

SenderActor

显然ProcessingDone每次都设置为失败,当前实现class SenderActor(cmd: NewMail, target: ActorRef) extends Actor { override def preStart(): Unit = { println(s"child starting, sending ${cmd} to self") self ! cmd } def fakeSendMail(): Unit = ... def receive = { case cmd: NewMail => ... } } 。我将在SenderActor中保留所需的其他更改,以实现fakeSendMailSenderActor发送SenderActor消息的快乐路径。

答案 1 :(得分:0)

失败的子actor可以作为主管策略中的发件人。引用https://doc.akka.io/docs/akka/current/fault-tolerance.html#creating-a-supervisor-strategy

  

如果战略是在监督行动者内宣布的(相反   在同伴对象内)其决策者可以访问所有内部   以线程安全的方式处理actor的状态,包括获取a   引用当前失败的孩子(可作为发送者)   失败的消息)。

答案 2 :(得分:0)

在@chunjef提供的良好解决方案中,他提醒有关在退避监督员启动工作人员之前重新安排工作的风险

  

然后,这个封闭的actor使用单个计时器在某个时刻重放原始消息。此时间点应该足够远,以便BackoffSupervisor可能会耗尽SenderActor的重启尝试,以便孩子在收到重新发送的消息之前可以有充分的机会进入“良好”状态。

如果发生这种情况,该方案将成为死信,不会有进一步的进展。 我用this scenario做了一个简化的小提琴。

因此,计划延迟应该大于maxBackoff,这可能代表工作完成时间的影响。 避免这种情况的一种可能的解决方案是让工作人员在准备好工作时向父亲发送消息,例如here

答案 3 :(得分:-1)

发送电子邮件是一种危险的操作,在您的情况下使用某些第三方软件。为什么不应用Circuit Breaker模式并完全跳过发送者角色?此外,你仍然可以有一个演员(有一些Backoff Supervisor)和Circuit Breaker(如果这对你有意义的话)。