第四部分:使用設備組
akka版本2.5.8
版權聲明:本文爲博主原創文章,未經博主允許不得轉載。
讓我們仔細觀察下我們用例所要求的主要功能。在完整的檢測家庭溫度的物聯網系統中,傳感器設備和我們的系統進行連接的步驟大概會像這樣:
1、家裏的一個傳感器設備通過某個協議發起連接
2、組件管理器處理網絡連接並接受連接
3、傳感器提供組ID和設備ID來向我們的系統組件管理器註冊
4、設備管理器組件處理註冊信息,並通過詢問或創建actor負責任務的方式保持傳感器狀態
5、actor返回確認信息,並暴露其ActorRef
6、網絡組件使用ActorRef
來實現傳感器和設備actor的交流,而不用通過設備管理器
第一步和第二步在我們教程之外,在本章中,我們會開始討論步驟3-6,並且創建一個讓傳感器可以與我們的系統註冊和通信的方式。然而首先我們要討論一個架構問題,我們需要用幾個層次來表示設備組和傳感器設備?
對於Akka程序員,其中一個主要的挑戰就是給actor選擇一個合適的粒度。在實踐中,依賴於actor之間的特性,我們有幾種有效的方式來組織系統。在我們的用例中,可能會存在單一actor來維護所有的組和設備——可能使用哈希映射。如果我們爲每個組創建一個actor來維護狀態也是合理的。
以下指南幫助我們選擇合適的actor層次:
1、在通常情況下,我們偏向較大的粒度。引入過細粒度(超過需求)的actor可能引入的問題會超過其解決的問題。
2、當系統需要時添加更精細的粒度。1、更高的併發性。
2、actor之間很多複雜的交流會擁有很多狀態信息,我們會在下章裏看到很多好的實例來解決它。
3、分成小的actor提供了足夠的狀態信息。
4、很多互相無關的責任。使用獨立的actor可以在個體失敗並恢復時對系統產生較小的影響。
設備管理器層級
考慮到上一節所述的原則,我們會將設備管理器建模爲一個三層的樹:
1、最高級監管actor代表設備的系統組件,它同樣負責維持和創建設備組和設備actor
2、在下一級,各個組actor監管着屬於他們的設備actor。他們也提供服務,例如向組裏所有的設備請求溫度數據
3、設備actor管理所有和實際傳感器設備的交互,例如存儲溫度讀數
我們使用三層架構主要原因是:
1、使單獨的actor組成一個組
1、將組內的故障進行隔離,如果只有一個actor去管理所有的設備組,一旦一個組出現錯誤就會導致重啓,並可能摧毀所有組的狀態,不管其有沒有出錯
2、簡化請求一個組內所有設備數據的問題,每個組actor只包含與組相關的狀態
3、增加系統的並行性。每個組都有一個專用的actor,他們可以並行地運行,我們也可以並行地去取所有組的數據2、將傳感器建模爲單個設備actor
1、隔離組內各個設備actor的錯誤
2、增加讀取溫度數據的並行度,每個傳感器通過網絡和它們對應的設備actor通信,減少網絡爭用點
在我們定義的架構下,我們可以開始處理傳感器註冊協議了。
註冊協議
第一步,我們需要創建它所負責的協議:註冊設備、創建組和創建設備actor。這個協議會被DeviceManager
組件提供,因爲它是唯一可以預先了解設備情況的———設備組和設備actor是按需創建的。
讓我們來詳細看下注冊,我們可以大概列出必要功能:
1、當
DeviceManager
接收到一個包含組ID和設備ID的請求時:1、如果管理者已經擁有了這個組的組actor,則將請求轉發給它
2、否則創建一個組actor,然後轉發請求給它
2、當DeviceGroup
actor接收到設備的註冊請求:
1、如果組內已經有了這個設備的actor,則將請求轉發給這個actor
2、否則創建一個設備actor,然後轉發請求給它
3、設備actor接收請求併發送回應給原始傳感器。由於設備actor發送了確認信息(而不是組actor),傳感器就可以持有ActorRef
,並直接向它對應的設備actor發送消息
我們對用來請求註冊和迴應的消息做個簡單的定義:
final case class RequestTrackDevice(groupId: String, deviceId: String)
case object DeviceRegistered
在這個場景下,我們沒有在消息內包含ID字段,因爲在註冊時時ID並不重要。但是包含請求ID通常是最佳做法。
現在我們開始自下而上實現我們的協議。在實踐中,自上而下和自下而上都是可以的,但是在我們的場景中,我們使用自下而上的設計將會很有用。這允許我們立即編寫測試用例而不用模擬我們之後要編寫的功能來測試新功能。
向設備actor添加註冊支持
Device
actor在我們層級結構的底部,註冊的工作是很簡單的:迴應註冊請求給發送者。謹慎而言,我們需要對不匹配的組ID和設備ID做安全防護。
我們假定註冊信息發送者的ID在上層被存儲,我們將在下節來展示實現方式。
設備actor的註冊代碼看起來就像這樣,修改你的示例代碼來匹配它。
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 {
import Device._
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 DeviceManager.RequestTrackDevice(`groupId`, `deviceId`) ⇒
sender() ! DeviceManager.DeviceRegistered
case DeviceManager.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)
}
}
注意:
我們使用了scala模式匹配的特性來檢查某個字段是不是我們的期望值。通過使用反引號把變量括起來
variable
,模式只有在值一致時纔會匹配。
現在我們可以編寫兩個新的測試用例,一個用於成功的註冊,一個用於測試ID不匹配的情況:
"reply to registration requests" in {
val probe = TestProbe()
val deviceActor = system.actorOf(Device.props("group", "device"))
deviceActor.tell(DeviceManager.RequestTrackDevice("group", "device"), probe.ref)
probe.expectMsg(DeviceManager.DeviceRegistered)
probe.lastSender should ===(deviceActor)
}
"ignore wrong registration requests" in {
val probe = TestProbe()
val deviceActor = system.actorOf(Device.props("group", "device"))
deviceActor.tell(DeviceManager.RequestTrackDevice("wrongGroup", "device"), probe.ref)
probe.expectNoMsg(500.milliseconds)
deviceActor.tell(DeviceManager.RequestTrackDevice("group", "Wrongdevice"), probe.ref)
probe.expectNoMsg(500.milliseconds)
}
注意:
在
TestProbe
中我們使用了幫助方法expectNoMsg()
,這個方法等待指定的時間,如果在限制時間內接收到消息則失敗,如果超時則斷言成功。使用這些超時(但不能太低)是個好方法,但是它增加了測試的執行時間。
向設備組actor添加註冊支持
我們已經完成了設備級別的註冊支持,現在我們來實現組級別的註冊支持。一個組actor在接收到註冊請求時有很多工作要做,包括:
1、處理註冊請求。轉發給已經存在的設備actor或者創建新的actor並且將請求轉發給它
2、跟蹤組內的設備actor,當它們停止時從組內移除它們
處理註冊請求
一個設備組actor必須轉發請求或者創建一個actor。爲了通過設備ID查找子actor,我們需要一個Map[String, ActorRef]
。
我們還想保留原始請求發件人的ID,以便我們的設備actor可以直接回復。這可以通過使用forward
而不是!
來實現。兩者唯一的不同之處就是forward
保持了原始的發件人信息,而!
把當前actor作爲發件人。就像我們的設備actor一樣,我們保證不會回覆錯誤的組ID請求。添加以下文件到你的源文件中:
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]
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}")
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
)
}
}
就像我們對設備所做的一樣,我們測試新功能。我們還測試了actor對於不同的ID的回覆是不同的,並且我們還嘗試記錄每個設備讀取的溫度,以查看actor是否響應。
"be able to register a device actor" in {
val probe = TestProbe()
val groupActor = system.actorOf(DeviceGroup.props("group"))
groupActor.tell(DeviceManager.RequestTrackDevice("group", "device1"), probe.ref)
probe.expectMsg(DeviceManager.DeviceRegistered)
val deviceActor1 = probe.lastSender
groupActor.tell(DeviceManager.RequestTrackDevice("group", "device2"), probe.ref)
probe.expectMsg(DeviceManager.DeviceRegistered)
val deviceActor2 = probe.lastSender
deviceActor1 should !==(deviceActor2)
// Check that the device actors are working
deviceActor1.tell(Device.RecordTemperature(requestId = 0, 1.0), probe.ref)
probe.expectMsg(Device.TemperatureRecorded(requestId = 0))
deviceActor2.tell(Device.RecordTemperature(requestId = 1, 2.0), probe.ref)
probe.expectMsg(Device.TemperatureRecorded(requestId = 1))
}
"ignore requests for wrong groupId" in {
val probe = TestProbe()
val groupActor = system.actorOf(DeviceGroup.props("group"))
groupActor.tell(DeviceManager.RequestTrackDevice("wrongGroup", "device1"), probe.ref)
probe.expectNoMsg(500.milliseconds)
}
在註冊請求中,如果設備actor已經存在,我們會使用現有的這個actor而不是再新建一個actor。我們還沒有測試這個功能,所以我們修改一下:
"return same actor for same deviceId" in {
val probe = TestProbe()
val groupActor = system.actorOf(DeviceGroup.props("group"))
groupActor.tell(DeviceManager.RequestTrackDevice("group", "device1"), probe.ref)
probe.expectMsg(DeviceManager.DeviceRegistered)
val deviceActor1 = probe.lastSender
groupActor.tell(DeviceManager.RequestTrackDevice("group", "device1"), probe.ref)
probe.expectMsg(DeviceManager.DeviceRegistered)
val deviceActor2 = probe.lastSender
deviceActor1 should ===(deviceActor2)
}
跟蹤組內的設備actor
到目前爲止,我們已經實現了向組註冊設備actor的邏輯。設備可以放置和去除,所以我們需要一種方式從Map[String, ActorRef]
刪除設備actor。我們假設當設備被去除時,它對應的設備actor也會簡單地停止。我們之前講過監管者,但是它只能處理錯誤的情況,正常停止是沒有辦法的。所以我們需要在設備actor停止時通知父actor。
Akka提供了一個死亡觀察功能,它允許一個actor去觀察另一個actor,當另一個actor停止時,這個actor會被通知。和監管者不同,這種觀察不侷限於父子關係,在擁有其ActorRef
的情況下,所有的actor都可以觀察任何actor。當被觀察的actor停止時,觀察者會接收到Terminated(actorRef)
消息,消息裏包含了被關閉actor的引用。這個觀察着或者顯式地處理這個消息,或者因爲DeathPactException
而失敗。後者在某些情況下是非常有用的,例如一個actor在被觀察的actor停止後就不能繼續自己的工作了。在我們的情境下,組actor應該在一個設備停止後繼續工作,所以我們需要去處理Terminated(actorRef)
消息。
我們的設備組需要具備以下功能:
1、當新的設備actor被創建時要開始觀察它
2、當收到actor停止的消息時,從Map[String, ActorRef]
中清除它對應的條目
不幸的是,Terminated
消息只包含ActorRef
,我們需要獲得actor的ID來從map裏將其刪除。爲了有能力刪除它,我們需要另一個結構Map[ActorRef, String]
,這允許我們通過獲得的ActorRef
找出設備ID。
添加了標識功能的actor如下:
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
}
}
到目前爲止,我們沒辦法獲取組actor所跟蹤的設備列表,因此我們無法測試我們的這個新功能。爲了使其可測,我們添加了一個新的查詢功能(RequestDeviceList(requestId: Long)
消息):列出當前活動的設備ID:
object DeviceGroup {
def props(groupId: String): Props = Props(new DeviceGroup(groupId))
final case class RequestDeviceList(requestId: Long)
final case class ReplyDeviceList(requestId: Long, ids: Set[String])
}
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 RequestDeviceList(requestId) ⇒
sender() ! ReplyDeviceList(requestId, deviceIdToActor.keySet)
case Terminated(deviceActor) ⇒
val deviceId = actorToDeviceId(deviceActor)
log.info("Device actor for {} has been terminated", deviceId)
actorToDeviceId -= deviceActor
deviceIdToActor -= deviceId
}
}
我們幾乎已經準備好了測試這個刪除功能,但是我們仍需要以下能力:
1、在測試用例裏停止我們的設備actor。任何人都可以通過發送一個內置的消息
PoisonPill
從外部停止一個actor。
2、我們需要在設備停止時被通知,我麼可以使用死亡觀察功能來實現。我們可以輕鬆使用TestProbe
擁有的兩個消息:使用watch()
來觀察一個特定的actor,使用expectTerminated
來斷言被觀察的actor已經被終止。
我們添加兩個測試用例。第一個測試用例,我們僅僅測試我們在添加設備後可以獲取ID列表。第二個測試用例確保設備ID在設備停止後被適當地移除。
"be able to list active devices" in {
val probe = TestProbe()
val groupActor = system.actorOf(DeviceGroup.props("group"))
groupActor.tell(DeviceManager.RequestTrackDevice("group", "device1"), probe.ref)
probe.expectMsg(DeviceManager.DeviceRegistered)
groupActor.tell(DeviceManager.RequestTrackDevice("group", "device2"), probe.ref)
probe.expectMsg(DeviceManager.DeviceRegistered)
groupActor.tell(DeviceGroup.RequestDeviceList(requestId = 0), probe.ref)
probe.expectMsg(DeviceGroup.ReplyDeviceList(requestId = 0, Set("device1", "device2")))
}
"be able to list active devices after one shuts down" in {
val probe = TestProbe()
val groupActor = system.actorOf(DeviceGroup.props("group"))
groupActor.tell(DeviceManager.RequestTrackDevice("group", "device1"), probe.ref)
probe.expectMsg(DeviceManager.DeviceRegistered)
val toShutDown = probe.lastSender
groupActor.tell(DeviceManager.RequestTrackDevice("group", "device2"), probe.ref)
probe.expectMsg(DeviceManager.DeviceRegistered)
groupActor.tell(DeviceGroup.RequestDeviceList(requestId = 0), probe.ref)
probe.expectMsg(DeviceGroup.ReplyDeviceList(requestId = 0, Set("device1", "device2")))
probe.watch(toShutDown)
toShutDown ! PoisonPill
probe.expectTerminated(toShutDown)
// 因爲可能會需要很長時間才能接受到設備actor的停止,因此使用awaitAssert來重試用例
probe.awaitAssert {
groupActor.tell(DeviceGroup.RequestDeviceList(requestId = 1), probe.ref)
probe.expectMsg(DeviceGroup.ReplyDeviceList(requestId = 1, Set("device2")))
}
}
創建設備管理actor
我們繼續進入到actor層次結構的下一層,我們需要在源文件DeviceManager
中創建我們管理組件的入口。這個actor和設備組actor很相似,但是它創建的是設備組actor:
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很像,因此我們把它留給你作爲測驗。
接下來
我們已經有了一個分級的組件來跟蹤設備並且記錄測量信息。我們已經看到如何實現不同類型的會話模式,例如:
1、請求-迴應(對於臨時記錄)
2、代理-迴應(對於與註冊設備)
3、創建-觀察-終止(對於創建組和設備actor,並把其作爲子actor)
在下一章節,我們會介紹組請求能力,這會建立一個新的分散-聚合會話模式。我們會特別地實現允許用戶請求組內所有設備狀態的功能。