Konrad Malawski presents on Akka Typed Actors and building distributed systems with Akka. Some key points:
- Akka Typed Actors introduce behaviors and protocols to make actors more type-safe compared to untyped actors. Behaviors can change state and return new behaviors.
- A sample burglar alarm actor is implemented as a state machine using behaviors and protocols to handle enabled and disabled states.
- Distributed systems can be built using Akka Cluster, where actors on different nodes can communicate. A distributed burglar alarm example uses a receptionist to discover actor references across nodes.
6. • mutate state (including spawning a child actor)
• send messages to other actors
• change its behavior
For a message an Actor
Actor
Message
Inbox
MessageMessage
can
7. Untyped Actors => Akka Typed
Untyped Actors
• “sender()” propagated automatically
• any message type can be sent to any Actor
• actor’s def receive = { … } don’t return values
8. Untyped Actors => Akka Typed
Untyped Actors
• “sender()” propagated automatically
• any message type can be sent to any Actor
• actor’s def receive = { … } don’t return values
Akka Typed Actors
• “sender” is part of the protocol
• only the right type of messages can be sent:
ActorRef[MessageProtocol]
• behaviors always return new behaviors
(state machines for the win)
11. // message protocol
trait AlarmMessage
case class EnableAlarm(pinCode: String) extends AlarmMessage
case class DisableAlarm(pinCode: String) extends AlarmMessage
case object ActivityEvent extends AlarmMessage
12. // behavior
def enabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case DisableAlarm(enteredCode) if enteredCode == pinCode =>
ctx.log.info("Disabling alarm")
disabled(pinCode) // change behavior
case ActivityEvent =>
ctx.log.warning("OEOEOEOEOEOE alarm, activity detected!")
Behaviors.same
case _ =>
Behaviors.ignore
}
}
def disabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case EnableAlarm(enteredCode) if enteredCode == pinCode =>
enabled(pinCode) // change behavior
case _ =>
Behaviors.ignore
}
}
13. // behavior
def enabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case DisableAlarm(enteredCode) if enteredCode == pinCode =>
ctx.log.info("Disabling alarm")
disabled(pinCode) // change behavior
case ActivityEvent =>
ctx.log.warning("OEOEOEOEOEOE alarm, activity detected!")
Behaviors.same
case _ =>
Behaviors.ignore
}
}
def disabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case EnableAlarm(enteredCode) if enteredCode == pinCode =>
enabled(pinCode) // change behavior
case _ =>
Behaviors.ignore
}
}
14. // behavior
def enabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case DisableAlarm(enteredCode) if enteredCode == pinCode =>
ctx.log.info("Disabling alarm")
disabled(pinCode) // change behavior
case ActivityEvent =>
ctx.log.warning("OEOEOEOEOEOE alarm, activity detected!")
Behaviors.same
case _ =>
Behaviors.ignore
}
}
def disabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case EnableAlarm(enteredCode) if enteredCode == pinCode =>
enabled(pinCode) // change behavior
case _ =>
Behaviors.ignore
}
}
15. // behavior
def enabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case DisableAlarm(enteredCode) if enteredCode == pinCode =>
ctx.log.info("Disabling alarm")
disabled(pinCode) // change behavior
case ActivityEvent =>
ctx.log.warning("OEOEOEOEOEOE alarm, activity detected!")
Behaviors.same
case _ =>
Behaviors.ignore
}
}
def disabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case EnableAlarm(enteredCode) if enteredCode == pinCode =>
enabled(pinCode) // change behavior
case _ =>
Behaviors.ignore
}
}
16. // behavior
def enabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case DisableAlarm(enteredCode) if enteredCode == pinCode =>
ctx.log.info("Disabling alarm")
disabled(pinCode) // change behavior
case ActivityEvent =>
ctx.log.warning("OEOEOEOEOEOE alarm, activity detected!")
Behaviors.same
case _ =>
Behaviors.ignore
}
}
def disabled(pinCode: String): Behavior[AlarmMessage] =
Behaviors.immutable[AlarmMessage] { (ctx, msg) =>
msg match {
case EnableAlarm(enteredCode) if enteredCode == pinCode =>
enabled(pinCode) // change behavior
case _ =>
Behaviors.ignore
}
}
17. // running it
val system = ActorSystem(enabled("0000"), "AlarmSystem")
// system is also reference to top level actor
val alarmRef: ActorRef[AlarmMessage] = system
alarmRef ! DisableAlarm("1234")
alarmRef ! ActivityEvent
alarmRef ! DisableAlarm("0000")
alarmRef ! ActivityEvent
alarmRef ! EnableAlarm("0000")
18. // running it
val system = ActorSystem(enabled("0000"), "AlarmSystem")
// system is also reference to top level actor
val alarmRef: ActorRef[AlarmMessage] = system
alarmRef ! DisableAlarm("1234")
alarmRef ! ActivityEvent
alarmRef ! DisableAlarm("0000")
alarmRef ! ActivityEvent
alarmRef ! EnableAlarm(“0000")
alarmRef ! ”0000” // compile error
26. // protocol
trait SensorEvent
case object WindowOpened extends SensorEvent
// behavior
def sensorBehavior: Behavior[SensorEvent] =
Behaviors.deferred { ctx =>
var alarms = Set.empty[ActorRef[AlarmMessage]]
Receptionist(ctx.system).ref ! Receptionist.Subscribe(
alarmKey, ctx.self.narrow[Listing[AlarmMessage]])
Behaviors.immutable[Any]((ctx, msg) =>
msg match {
case Listing(_, updatedAlarms: Set[ActorRef[AlarmMessage]]) =>
// updated list of alarms known
alarms = updatedAlarms
Behaviors.same
case WindowOpened =>
// inform all known alarms about activity
alarms.foreach(_ ! ActivityEvent)
Behaviors.same
}
)
}.narrow // narrow Behavior[Any] down to Behavior[SensorEvent]
27. // protocol
trait SensorEvent
case object WindowOpened extends SensorEvent
// behavior
def sensorBehavior: Behavior[SensorEvent] =
Behaviors.deferred { ctx =>
var alarms = Set.empty[ActorRef[AlarmMessage]]
Receptionist(ctx.system).ref ! Receptionist.Subscribe(
alarmKey, ctx.self.narrow[Listing[AlarmMessage]])
Behaviors.immutable[Any]((ctx, msg) =>
msg match {
case Listing(_, updatedAlarms: Set[ActorRef[AlarmMessage]]) =>
// updated list of alarms known
alarms = updatedAlarms
Behaviors.same
case WindowOpened =>
// inform all known alarms about activity
alarms.foreach(_ ! ActivityEvent)
Behaviors.same
}
)
}.narrow // narrow Behavior[Any] down to Behavior[SensorEvent]
28. // protocol
trait SensorEvent
case object WindowOpened extends SensorEvent
// behavior
def sensorBehavior: Behavior[SensorEvent] =
Behaviors.deferred { ctx =>
var alarms = Set.empty[ActorRef[AlarmMessage]]
Receptionist(ctx.system).ref ! Receptionist.Subscribe(
alarmKey, ctx.self.narrow[Listing[AlarmMessage]])
Behaviors.immutable[Any]((ctx, msg) =>
msg match {
case Listing(_, updatedAlarms: Set[ActorRef[AlarmMessage]]) =>
// updated list of alarms known
alarms = updatedAlarms
Behaviors.same
case WindowOpened =>
// inform all known alarms about activity
alarms.foreach(_ ! ActivityEvent)
Behaviors.same
}
)
}.narrow // narrow Behavior[Any] down to Behavior[SensorEvent]
29. class SensorBehavior(ctx: ActorContext[SensorProtocol]) extends MutableBehavior[SensorProtocol] {
private var alarms = Set.empty[ActorRef[AlarmMessage]]
private val receptionist: ActorRef[Receptionist.Command] = Receptionist(ctx.system).ref
ctx.ask(receptionist)(Receptionist.Subscribe(alarmKey, _))(res AlarmListing(res.get.serviceInstances))
override def onMessage(msg: SensorProtocol): Behavior[SensorProtocol] = msg match {
case AlarmListing(updatedAlarms) =>
// updated list of alarms known
alarms = updatedAlarms
this
case WindowOpened =>
// inform all known alarms about activity
alarms.foreach(_ ! ActivityEvent)
this
}
}
“Mutable”Behavior:
30. // running it
val system1 = ActorSystem(startAlarm("0000"), "AlarmSystem")
val system2 = ActorSystem(sensorBehavior, "AlarmSystem")
// programmatic cluster formation
val node1 = Cluster(system1)
val node2 = Cluster(system2)
// node1 joins itself to form cluster
node1.manager ! Join(node1.selfMember.address)
// node2 joins the now existing cluster
node2.manager ! Join(node1.selfMember.address)
// a bit later the burglar comes
after(1.day) {
system2 ! WindowOpened
}
31. Thanks for listening!
Konrad ktoso@lightbend.com Malawski
http://kto.so / @ktosopl
Akka docs: akka.io/docs
Free O’Reilly report – bit.ly/why-reactive