我正在尝试使用Akka actor的层次结构来处理每个用户状态。有一个拥有所有孩子的父actor,并以正确的方式处理get-or-create(参见a1,a2):
class UserActorRegistry extends Actor {
override def Receive = {
case msg@ DoPerUserWork(userId, _) =>
val perUserActor = getOrCreateUserActor(userId)
// perUserActor is live now, but will it receive "msg"?
perUserActor.forward(msg)
}
def getOrCreateUserActor(userId: UserId): ActorRef = {
val childName = userId.toActorName
context.child(childName) match {
case Some(child) => child
case None => context.actorOf(Props(classOf[UserActor], userId), childName)
}
}
为了回收记忆,UserActors
在闲置一段时间后过期(即计时器触发儿童演员呼叫context.stop(self)
)。
我的问题是我认为“getOrCreateUserActor”与接收转发邮件的子actor之间存在竞争条件 - 如果子进程在该窗口中到期,则转发的邮件将丢失。
有什么方法可以检测到这个边缘情况,或者重构UserActorRegistry
以排除它?
答案 0 :(得分:9)
我可以看到你当前设计的两个问题,让你自己接受你提到的竞争条件:
1)终止条件(计时器发送毒丸)直接进入儿童演员。通过采用这种方法,子进程当然可以在一个单独的线程上(在调度程序内)终止,同时,已经设置了一条消息在UserActorRegistry
actor中转发给它(在一个不同的线程内)调度员)。
2)使用PoisonPill
终止孩子。 PoisonPill
用于正常停止,允许首先处理邮箱中的其他邮件。在您的情况下,由于不活动而终止,这似乎表明邮箱中已有其他邮件。我在此处看到PoisonPill
错误,因为在您的情况下,可能会在PosionPill
之后发送另一条消息,并且在处理PoisonPill
后该消息肯定会丢失。
所以我建议你将非活动儿童的终止委托给UserActorRegistry
,而不是在孩子们自己做。当您检测到不活动状态时,向UserActorRegistry
实例发送一条消息,指示需要终止某个特定子项。收到该邮件后,请通过stop
终止该子邮件,而不是发送PoisonPill
。通过使用以串行方式处理的UserActorRegistry
的单个邮箱,您可以帮助确保在您即将向其发送消息时不会并行终止子项。
现在,这里有一个复杂的问题,你必须处理。停止一个actor是异步的。因此,如果您对某个孩子调用stop
,则在处理DoPerUserWork
消息时可能不会完全停止该消息,从而可能会向其发送一条消息,因为它正处于停止状态。您可以通过保留一些表示正在停止的子项的内部状态(List)来解决此问题。当您停止孩子时,请将其名称添加到该列表中,然后在其上设置DeathWatch
(通过context watch child
)。当您收到该孩子的Terminated
事件时,请从被终止的孩子列表中删除该名称。如果您的名字在该列表中,则为其接收工作,请将其重新排列以进行重新处理,最多可能达到最大次数,以便不会尝试永久重新处理。
这不是一个完美的解决方案;它只是对您的方法的一些问题的识别,并推动正确的方向来解决其中的一些问题。如果你想看到这个的代码,请告诉我,我会一起鞭打。
修改强>
回应你的第二条评论。我认为你不能看到一个孩子ActorRef
并且看到它正在关闭,因此需要关闭正在关闭的孩子列表。您可以增强DoPerUserWork
消息以包含numberOfAttempts:Int字段并将其递增并发送回self以进行重新处理(如果您看到目标子节点当前正在关闭)。然后,您可以使用numberOfAttempts来防止永久重新排队,在某些最大尝试次数停止。如果依靠DeathWatch
感觉不太舒服,可以将生存时间组件添加到关闭子项列表中的项目。然后你可以在你遇到它们时修剪它们,如果它们在列表中但已经存在太久了。