Scala 从主管处重新启动后向参与者发送消息

Scala 从主管处重新启动后向参与者发送消息,scala,akka,actor,Scala,Akka,Actor,我正在使用BackoffSupervisor策略创建一个必须处理某些消息的儿童演员。我想实现一个非常简单的重启策略,在异常情况下: 子进程将失败消息传播给主管 主管重新启动子进程并再次发送失败消息 主管在重试3次后放弃 Akka持久性不是一种选择 到目前为止,我得到的是: 主管定义: val childProps = Props(new SenderActor()) val supervisor = BackoffSupervisor.props( Backoff.onFailure(

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

  • 子进程将失败消息传播给主管
  • 主管重新启动子进程并再次发送失败消息

  • 主管在重试3次后放弃

  • Akka持久性不是一种选择
  • 到目前为止,我得到的是:

    主管定义:

    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
    
    本应发送电子邮件但失败(引发某些异常)并将异常传播回主管的子参与者:

    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
    hook上向参与者重新发送消息,但不知何故,没有触发hook:

    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
    
    routee收到的新邮件
    捕捉到特定的消息!
    儿童停车
    [错误][01/26/2018 10:15:35.690]
    [示例akka.actor.default-dispatcher-2]
    [akka://example/user/persistentActor-4-scala/$a/1962829645]无法
    处理消息sample.persistence.MessageException:
    无法处理消息
    儿童启动
    

    但是没有来自
    重新启动前的日志
    hook

    失败的子参与者在您的主管策略中作为发送者可用。引述:

    如果战略是在监督参与者内部宣布的(相反 它的决策者有权访问所有内部对象 以线程安全方式显示参与者的状态,包括获取 对当前失败的子项的引用(可作为 失败消息)


    在您的情况下,使用某些第三方软件发送电子邮件是一种危险的操作。为什么不应用模式并完全跳过发送者角色呢?此外,您还可以在其中设置一个参与者(带有一些退避管理器)和断路器(如果对您有意义的话)。

    未调用孩子的
    重新启动前
    钩子的原因是
    退避。onFailure
    在盖子下面使用,它将默认重启行为替换为与退避策略一致的停止和延迟启动行为。换句话说,当使用
    Backoff.onFailure
    时,当子级重新启动时,不会调用子级的
    preRestart
    方法,因为底层主管实际上会停止子级,然后再重新启动它。(使用可以触发孩子的
    预重启
    钩子,但这与当前的讨论无关。)

    BackoffSupervisor
    API不支持在主管的子级重新启动时自动重新发送消息:您必须自己实现此行为。重试消息的一个想法是让
    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")
    }
    
    在上面的代码中,
    MessageException
    中嵌入的
    NewMail
    消息被包装在一个定制的case类中(以便容易地将其与“普通”//new
    NewMail
    消息区分开来),并发送到
    self
    。在此上下文中,
    self
    是创建
    BackoffSupervisor
    的参与者。然后,这个封闭的参与者使用a在某个点重播原始消息。这一时间点在将来应该足够远,以便
    BackoffSupervisor
    可能会耗尽
    SenderActor
    的重新启动尝试,以便孩子有足够的机会在收到重新发送消息之前进入“良好”状态。显然,这个示例只涉及一条消息的重新发送,而不考虑子重新启动的次数


    另一个想法是为每个
    NewMail
    消息创建
    BackoffSupervisor
    -
    senderator
    对,并让
    senderator
    preStart
    钩子中将
    NewMail
    消息发送给自己。这种方法的一个问题是清理资源;i、 例如,当处理成功或子系统重新启动时,关闭
    BackoffSupervisors
    (这将依次关闭其各自的
    SenderActor
    子系统)。在这种情况下,
    NewMail
    id到
    (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
    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
    的当前实现设置为每次都失败。我将在
    SenderActor
    中留下实现快乐路径所需的额外更改,在快乐路径中,
    SenderActor
    ProcessingDone
    消息发送给
    target

    在@chunjef提供的良好解决方案中,他提醒在退避主管启动工人之前安排作业重新发送的风险

    然后,这个封闭的参与者使用一个计时器在某个点上重播原始消息。这个时间点在将来应该足够长,这样BackoffSupervisor就有可能耗尽SenderActor的重新启动尝试,这样子级就有足够的机会在收到重新发送的消息之前进入“良好”状态

    如果发生这种情况,情况将是工作一文不值,不会有进一步的进展。 我已经把小提琴简化了

    所以,时间表应该推迟