儿童演员,未来和例外

时间:2017-10-09 14:22:13

标签: scala asynchronous parallel-processing akka actor

我目前正在使用注册过程处理应用程序。此注册过程将在某个时刻以异步方式与外部系统通信。为了使这个问题简明扼要,我向你展示了我写过的两位重要演员:

SignupActor.scala

class SignupActor extends PersistentFSM[SignupActor.State, Data, DomainEvt] {
    private val apiActor = context.actorOf(ExternalAPIActor.props(new HttpClient))

    // At a certain point, a CreateUser(data) message is sent to the apiActor
}

ExternalAPIActor.scala

class ExternalAPIActor(apiClient: HttpClient) extends Actor {
    override def preRestart(reason: Throwable, message: Option[Any]) = {
        message.foreach(context.system.scheduler.scheduleOnce(3 seconds, self, _))
        super.preRestart(reason, message)
    }

    def receive: Receive = {
        case CreateUser(data) =>
            Await.result(
                apiClient.post(data)
                    .map(_ => UserCreatedInAPI())
                    .pipeTo(context.parent),
                Timeout(5 seconds).duration
            )
    }
}

此设置似乎按预期工作。当外部API出现问题(例如超时或网络问题)时,Future返回的HttpClient::post会失败,并且会因Await.result而导致异常。这反过来归功于SupervisorStrategy父actor的SignupActor,我们将重新启动ExternalAPIActor,我们可以稍微延迟将最后一条消息发送给自己,以避免死锁。< / p>

我发现这个设置有几个问题:

  • receive的{​​{1}}方法中,会发生阻塞。据我所知,Actors中的阻塞被认为是一种反模式。
  • 用于重新发送邮件的延迟是静态的。如果API长时间不可用,我们将继续每3秒发送一次HTTP请求。我想在这里使用某种指数退避机制。

继续使用后者,我在ExternalAPIActor中尝试了以下内容:

SignupActor.scala

SignupActor

不幸的是,这似乎根本没有做任何事情 - val supervisor = BackoffSupervisor.props( Backoff.onFailure( ExternalAPIActor.props(new HttpClient), childName = "external-api", minBackoff = 3 seconds, maxBackoff = 30 seconds, randomFactor = 0.2 ) ) private val apiActor = context.actorOf(supervisor) 的{​​{1}}方法根本没有被调用。将preRestart替换为ExternalAPIActor时,Backoff.onFailure方法调用,但根本没有任何指数退避。

鉴于上述情况,我的问题如下:

  • 使用Backoff.onStop建议的(唯一的?)方法来确保在actor中调用的服务返回的preRestart中抛出的异常被捕获并相应地处理?我的特定用例中一个特别重要的部分是不应该删除消息,而是在出现问题时重试。或者是否有其他(惯用)方式,异步上下文中抛出的异常应该在Actors中处理?
  • 在这种情况下如何使用Await.result按预期方式?同样:非常重要的是,不会删除负责异常的消息,而是重试N次(由Future的{​​{1}}参数确定。

2 个答案:

答案 0 :(得分:3)

  

使用Await.result建议(唯一的?)方式来确保   从内部调用的服务返回的Future中抛出的异常   演员被抓住并相应处理?

没有。通常,这不是你想要如何处理Akka的失败。更好的选择是将失败传递给您自己的演员,从而无需使用Await.result

def receive: Receive = {
  case CreateUser(data) =>
    apiClient.post(data)
      .map(_ => UserCreatedInAPI())
      .pipeTo(self)
  case Success(res) => context.parent ! res
  case Failure(e) => // Invoke retry here
}

这意味着无需重启就可以处理失败,它们都是你演员正常流程的一部分。

处理此问题的另一种方法是创建“监督未来”。取自this blog post

object SupervisedPipe {

  case class SupervisedFailure(ex: Throwable)
  class SupervisedPipeableFuture[T](future: Future[T])(implicit executionContext: ExecutionContext) {
    // implicit failure recipient goes to self when used inside an actor
    def supervisedPipeTo(successRecipient: ActorRef)(implicit failureRecipient: ActorRef): Unit =
      future.andThen {
        case Success(result) => successRecipient ! result
        case Failure(ex) => failureRecipient ! SupervisedFailure(ex)
      }
  }

  implicit def supervisedPipeTo[T](future: Future[T])(implicit executionContext: ExecutionContext): SupervisedPipeableFuture[T] =
    new SupervisedPipeableFuture[T](future)

  /* `orElse` with the actor receive logic */
  val handleSupervisedFailure: Receive = {
    // just throw the exception and make the actor logic handle it
    case SupervisedFailure(ex) => throw ex
  }

  def supervised(receive: Receive): Receive = 
    handleSupervisedFailure orElse receive
}

这样,一旦你得到Failure,你只能自我管理,否则将它发送给消息本来要发送给的演员,避免需要我添加的case Success receive方法。您需要做的就是使用supervisedPipeTo提供的原始框架替换pipeTo

答案 1 :(得分:0)

好吧,我已经做了一些思考和修补,我想出了以下内容。

<强> ExternalAPIActor.scala

class ExternalAPIActor(apiClient: HttpClient) extends Actor with Stash {
        import ExternalAPIActor._

        def receive: Receive = {
            case msg @ CreateUser(data) =>
                context.become(waitingForExternalServiceReceive(msg))
                apiClient.post(data)
                    .map(_ => UserCreatedInAPI())
                    .pipeTo(self)
        }

        def waitingForExternalServiceReceive(event: InputEvent): Receive = LoggingReceive {
            case Failure(_) =>
              unstashAll()
              context.unbecome()
              context.system.scheduler.scheduleOnce(3 seconds, self, event)

            case msg:OutputEvent =>
              unstashAll()
              context.unbecome()
              context.parent ! msg

            case _ => stash()
        }
}

object ExternalAPIActor {
    sealed trait InputEvent
    sealed trait OutputEvent

    final case class CreateUser(data: Map[String,Any]) extends InputEvent
    final case class UserCreatedInAPI() extends OutputEvent
}

我已经使用这种技术来防止原始邮件丢失,以防我们正在调用的外部服务出现问题。在请求外部服务的过程中,我切换上下文,等待故障响应并在之后切换回来。感谢Stash特性,我可以确保其他对外部服务的请求也不会丢失。

由于我的应用程序中有多个actor调用外部服务,因此我将waitingForExternalServiceReceive抽象为自己的特性:

<强> WaitingForExternalService.scala

trait WaitingForExternalServiceReceive[-tInput, +tOutput] extends Stash {

  def waitingForExternalServiceReceive(event: tInput)(implicit ec: ExecutionContext): Receive = LoggingReceive {
    case akka.actor.Status.Failure(_) =>
      unstashAll()
      context.unbecome()
      context.system.scheduler.scheduleOnce(3 seconds, self, event)

    case msg:tOutput =>
      unstashAll()
      context.unbecome()
      context.parent ! msg

    case _ => stash()
  }
}

现在,ExternalAPIActor可以扩展这个特性:

<强> ExternalAPIActor.scala

class ExternalAPIActor(apiClient: HttpClient) extends Actor with WaitingForExternalServiceReceive[InputEvent,OutputEvent] {
        import ExternalAPIActor._

        def receive: Receive = {
            case msg @ CreateUser(data) =>
                context.become(waitingForExternalServiceReceive(msg))
                apiClient.post(data)
                    .map(_ => UserCreatedInAPI())
                    .pipeTo(self)
        }
}

object ExternalAPIActor {
    sealed trait InputEvent
    sealed trait OutputEvent

    final case class CreateUser(data: Map[String,Any]) extends InputEvent
    final case class UserCreatedInAPI() extends OutputEvent
}

现在,如果出现故障/错误并且消息没有丢失,则不会重新启动actor。更重要的是,演员的整个流程现在都是非阻塞的。

这种设置(很可能)远非完美,但它似乎完全符合我的需要。

相关问题