我目前正在使用注册过程处理应用程序。此注册过程将在某个时刻以异步方式与外部系统通信。为了使这个问题简明扼要,我向你展示了我写过的两位重要演员:
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中的阻塞被认为是一种反模式。继续使用后者,我在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}}参数确定。答案 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。更重要的是,演员的整个流程现在都是非阻塞的。
这种设置(很可能)远非完美,但它似乎完全符合我的需要。