Scala---Akka Actor(一)tutorial-2

本文继续上文的讲解 源代码下载

(三)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

之后,我们面临一个问题,谁要来处理这个请求。我们可能会先到DeviceGroup,但是我们会感觉到这样会使得DeviceGroup处理的内容太多了(它已经要创建Device了),我们希望一个actor尽量做少一点事情,使得系统具有更好的并发性(各司其职吗)。所以我们创建一个新的actor类型DeviceGroupQuery来处理这个请求。

我们要在创建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的脚步不会停止。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值