本文继续上文的讲解 源代码下载
(三)DeviceGroup实现
这一部分我们要实现一个DeviceGroup的actor,它能够管理若干个Device,DeviceGroup的主要功能如下:
1. 注册一个Device
2. 建立deviceId和device actor之间的映射
3. 监控device actor的推出
4. 获取device group的所有deviceId
(1)注册协议
以下是消息的格式:
final case class RequestTrackDevice(groupId: String, deviceId: String)
case object DeviceRegistered
我们将消息的定义放在DeviceManager中,因为DeviceManager是控制所有Device的actor,注册消息应该由它来定义。我们使用注册->确认的模式,完成Device对注册的支持:
object Device {
def props(groupId: String, deviceId: String): Props = Props(new Device(groupId, deviceId))
final case class RecordTemperature(requestId: Long, value: Double)
final case class TemperatureRecorded(requestId: Long)
final case class ReadTemperature(requestId: Long)
final case class RespondTemperature(requestId: Long, value: Option[Double])
}
class Device(groupId: String, deviceId: String) extends Actor with ActorLogging {
var lastTemperatureReading: Option[Double] = None
override def preStart(): Unit = log.info("Device actor {}-{} started", groupId, deviceId)
override def postStop(): Unit = log.info("Device actor {}-{} stopped", groupId, deviceId)
override def receive: Receive = {
case RequestTrackDevice(`groupId`, `deviceId`) =>
sender() ! DeviceRegistered
case RequestTrackDevice(groupId, deviceId) =>
log.warning(
"Ignoring TrackDevice request for {}-{}.This actor is responsible for {}-{}.",
groupId, deviceId, this.groupId, this.deviceId
)
case RecordTemperature(id, value) =>
log.info("Recorded temperature reading {} with {}", value, id)
lastTemperatureReading = Some(value)
sender() ! TemperatureRecorded(id)
case ReadTemperature(id) =>
sender() ! RespondTemperature(id, lastTemperatureReading)
}
}
注意红字部分。
(2)创建deviceId和device actor之间的映射
我们需要一个Map[String, ActorRef]类来支持这一映射关系。
object DeviceGroup {
def props(groupId: String): Props = Props(new DeviceGroup(groupId))
}
class DeviceGroup(groupId: String) extends Actor with ActorLogging {
var deviceIdToActor = Map.empty[String, ActorRef] // 1
override def preStart(): Unit = log.info("DeviceGroup {} started", groupId)
override def postStop(): Unit = log.info("DeviceGroup {} stopped", groupId)
override def receive: Receive = {
case trackMsg @ RequestTrackDevice(`groupId`, _) =>
deviceIdToActor.get(trackMsg.deviceId) match {
case Some(deviceActor) =>
deviceActor forward trackMsg // 2
case None =>
log.info("Creating device actor for {}", trackMsg.deviceId)
val deviceActor = context.actorOf(Device.props(groupId, trackMsg.deviceId), s"device-${trackMsg.deviceId}")
deviceIdToActor += trackMsg.deviceId -> deviceActor
deviceActor forward trackMsg // 3
}
case RequestTrackDevice(groupId, deviceId) => // 4
log.warning(
"Ignoring TrackDevice request for {}. This actor is responsible for {}.",
groupId, this.groupId
)
}
}
1. 我们使用一个immutable map来保存deviceId和device actor之间的映射,注意在这里,我们必须使用immutable map,应为一个map对应于一个特定的状态,状态改变需要生成一个新的immutable map,而不是修改原来的map。
2. 如果存在一个以保存的deviceId对应的device actor,直接将消息使用forward进行转发。
3. 如果deviceId不存在,使用context.actorOf()方法进行创建,同时生成一个新的map保存当前的deviceId和device actor之间的映射。
4. groupId与当前的DeviceGroup的groupId不匹配,不处理日志记录错误信息。
(3)监控Device的退出
使用context.watch()监控子actor,同时接受来自子actor的Terminated消息。class DeviceGroup(groupId: String) extends Actor with ActorLogging {
var deviceIdToActor = Map.empty[String, ActorRef]
var actorToDeviceId = Map.empty[ActorRef, String]
override def preStart(): Unit = log.info("DeviceGroup {} started", groupId)
override def postStop(): Unit = log.info("DeviceGroup {} stopped", groupId)
override def receive: Receive = {
case trackMsg @ RequestTrackDevice(`groupId`, _) =>
deviceIdToActor.get(trackMsg.deviceId) match {
case Some(deviceActor) =>
deviceActor forward trackMsg
case None =>
log.info("Creating device actor for {}", trackMsg.deviceId)
val deviceActor = context.actorOf(Device.props(groupId, trackMsg.deviceId), s"device-${trackMsg.deviceId}")
context.watch(deviceActor)
actorToDeviceId += deviceActor -> trackMsg.deviceId
deviceIdToActor += trackMsg.deviceId -> deviceActor
deviceActor forward trackMsg
}
case RequestTrackDevice(groupId, deviceId) =>
log.warning(
"Ignoring TrackDevice request for {}. This actor is responsible for {}.",
groupId, this.groupId
)
case Terminated(deviceActor) =>
val deviceId = actorToDeviceId(deviceActor)
log.info("Device actor for {} has been terminated", deviceId)
actorToDeviceId -= deviceActor
deviceIdToActor -= deviceId
}
}
(4)获取全部groupId
定义协议
final case class RequestDeviceList(requestId: Long)
final case class ReplyDeviceList(requestId: Long, ids: Set[String])
编写receive接受消息
case RequestDeviceList(requestId) =>
sender() ! ReplyDeviceList(requestId, deviceIdToActor.keySet)
(四)DeviceManager实现
不多说了,代码很简洁object DeviceManager {
def props(): Props = Props(new DeviceManager)
final case class RequestTrackDevice(groupId: String, deviceId: String)
case object DeviceRegistered
}
class DeviceManager extends Actor with ActorLogging {
var groupIdToActor = Map.empty[String, ActorRef]
var actorToGroupId = Map.empty[ActorRef, String]
override def preStart(): Unit = log.info("DeviceManager started")
override def postStop(): Unit = log.info("DeviceManager stopped")
override def receive = {
case trackMsg @ RequestTrackDevice(groupId, _) =>
groupIdToActor.get(groupId) match {
case Some(ref) =>
ref forward trackMsg
case None =>
log.info("Creating device group actor for {}", groupId)
val groupActor = context.actorOf(DeviceGroup.props(groupId), "group-" + groupId)
context.watch(groupActor)
groupActor forward trackMsg
groupIdToActor += groupId -> groupActor
actorToGroupId += groupActor -> groupId
}
case Terminated(groupActor) =>
val groupId = actorToGroupId(groupActor)
log.info("Device group actor for {} has been terminated", groupId)
actorToGroupId -= groupActor
groupIdToActor -= groupId
}
}
注:所有的消息都有对应的测试方法,这里没有讲,读者可以自行在 网站上查看。
(五)一个复杂的查询请求
在整片文章中,我们可能已经发现了actor编程的一个基本规律,就是先定义消息协议(case object/class),再在receive方法中,编写对应的消息处理函数。
现在我们要做下面一个请求,获取一个DeviceGroup中的全部Device的数据。
首先,我们还是要先定义消息协议,在GroupDevice中定义:
final case class RequestAllTemperatures(requestId: Long)
final case class RespondAllTemperatures(requestId: Long, temperatures: Map[String, TemperatureReading])
sealed trait TemperatureReading
final case class Temperature(value: Double) extends TemperatureReading
case object TemperatureNotAvailable extends TemperatureReading
case object DeviceNotAvailable extends TemperatureReading
case object DeviceTimedOut extends TemperatureReading
我们要在创建DeviceGroupQuery的时候,watch所有该组的Device,这样当Device错误推出的时候,能够返回DeviceNotAvailable。
在这个功能当中,我们需要一个定时器,当Device返回超时的时候
,我们能够中止等待,并返回DeviceTimeOut。这里我们使用
scheduler.scheduleOnce(time, actorRef, message)
,在time的时间之后,会向actorRef发送message。如果actor正常结束,我们就可以提前关闭这个timer。
object DeviceGroupQuery {
case object CollectionTimeout
def props(
actorToDeviceId: Map[ActorRef, String],
requestId: Long,
requester: ActorRef,
timeout: FiniteDuration
): Props = {
Props(new DeviceGroupQuery(actorToDeviceId, requestId, requester, timeout))
}
}
class DeviceGroupQuery(
actorToDeviceId: Map[ActorRef, String],
requestId: Long,
requester: ActorRef,
timeout: FiniteDuration
) extends Actor with ActorLogging {
import DeviceGroupQuery._
import context.dispatcher
val queryTimeoutTimer = context.system.scheduler.scheduleOnce(timeout, self, CollectionTimeout)
override def preStart(): Unit = {
actorToDeviceId.keysIterator.foreach { deviceActor =>
context.watch(deviceActor)
deviceActor ! Device.ReadTemperature(0)
}
}
override def postStop(): Unit = {
queryTimeoutTimer.cancel()
}
}
回忆一下之前在DeviceGroup中的deviceIdToActor这个map,它其实是actor的一个状态变量,我们需要用var来定义它。在一个actor中我们还会定义一些不变的(val)的量,比如在DeviceGroupQuery中,我们要定义一个queryTimeoutTimer变量,而这个变量是一个常量。为了和上述的状态变量区别,actor强烈建议我们将状态变量作为Receive变量中的一个引用。
我们直接看代码:
override def receive: Receive =
waitingForReplies( // 1
Map.empty,
actorToDeviceId.keySet
)
def waitingForReplies(
repliesSoFar: Map[String, DeviceGroup.TemperatureReading],
stillWaiting: Set[ActorRef]
): Receive = {
case Device.RespondTemperature(0, valueOption) =>
val deviceActor = sender()
val reading = valueOption match {
case Some(value) => DeviceGroup.Temperature(value)
case None => DeviceGroup.TemperatureNotAvailable
}
receivedResponse(deviceActor, reading, stillWaiting, repliesSoFar)
case Terminated(deviceActor) =>
receivedResponse(deviceActor, DeviceGroup.DeviceNotAvailable, stillWaiting, repliesSoFar)
case CollectionTimeout =>
val timedOutReplies =
stillWaiting.map { deviceActor =>
val deviceId = actorToDeviceId(deviceActor)
deviceId -> DeviceGroup.DeviceTimedOut
}
requester ! DeviceGroup.RespondAllTemperatures(requestId, repliesSoFar ++ timedOutReplies)
context.stop(self)
}
在这里,我们首先需要理解当消息传入的时候,并不是使用receive方法进行处理,而是actor会调用context.become(receive)函数进行处理,而receive方法返回的
PartialFunction[Any, Unit]对象才是关键。之后关键就是receiveResponse方法,它将状态继续传递下去。
def receivedResponse(
deviceActor: ActorRef,
reading: DeviceGroup.TemperatureReading,
stillWaiting: Set[ActorRef],
repliesSoFar: Map[String, DeviceGroup.TemperatureReading]
): Unit = {
context.unwatch(deviceActor)
val deviceId = actorToDeviceId(deviceActor)
val newStillWaiting = stillWaiting - deviceActor
val newRepliesSoFar = repliesSoFar + (deviceId -> reading)
if (newStillWaiting.isEmpty) {
requester ! DeviceGroup.RespondAllTemperatures(requestId, newRepliesSoFar)
context.stop(self)
} else {
context.become(waitingForReplies(newRepliesSoFar, newStillWaiting))
}
}
红字部分是重点,其他代码不多做解释了。
最后我们让DeviceGroup同样支持上述请求:
class DeviceGroup(groupId: String) extends Actor with ActorLogging {
var deviceIdToActor = Map.empty[String, ActorRef]
var actorToDeviceId = Map.empty[ActorRef, String]
var nextCollectionId = 0L
override def preStart(): Unit = log.info("DeviceGroup {} started", groupId)
override def postStop(): Unit = log.info("DeviceGroup {} stopped", groupId)
override def receive: Receive = {
// ... other cases omitted
case RequestAllTemperatures(requestId) =>
context.actorOf(DeviceGroupQuery.props(
actorToDeviceId = actorToDeviceId,
requestId = requestId,
requester = sender(),
3.seconds
))
}
}
(六)总结
本文中,我们对Akka中的Actor进行了最基础的探讨,初步了解了Actor框架编程的基本模式。我们在最后也见到了如何对复杂状态的处理。探索Actor的脚步不会停止。