The Actor Model describes precisely what it means for computation to be distributed: encapsulated and isolated behaviors process messages that are sent asynchronously between them. Akka’s implementation of this model has been widely successful, but for a long time it had the restriction that Actor interactions were not statically type-checked. With the addition of the akka-typed module we have finally found a formulation that brings Actor messaging to the same type-safety as normal method invocation—if not beyond—while being simple and intuitive. In this presentation we will look at why this addition has taken so long, how it works, and what we can express with it.
Actor-skeptics beware: this may shift your world-view!
3. Motivation
3
case class Get
case class Got(contents: Map[String, ActorRef])
class Server extends Actor {
var map = Map.empty[String, ActorRef]
def receive = {
case Get =>
sender ! Got(map)
}
}
4. 4
case class GetRef(name: String)
case class GetRefReply(ref: Option[ActorRef])
class Client(server: ActorRef) extends Actor {
def receive = {
case GetRef(name) =>
val worker = context.actorOf(Worker.props(name, sender()))
server.tell(Get, worker)
}
}
object Worker {
def props(name: String, replyTo: ActorRef) =
Props(new Worker(name, replyTo))
}
class Worker(name: String, replyTo: ActorRef) extends Actor {
def receive = {
case Got(map) =>
replyTo ! GetRefReply(map.get(name))
context.stop(self)
}
}
5. 5
case class Get(id: Int)
case class Got(id: Int, contents: Map[String, ActorRef])
class Server extends Actor {
var map = Map.empty[String, ActorRef]
def receive = {
case Get(id) =>
sender ! Got(id, map)
}
}
6. 6
case class GetRef(name: String)
case class GetRefReply(ref: Option[ActorRef])
class Client(server: ActorRef) extends Actor {
def receive = {
case GetRef(name) =>
val worker = context.actorOf(Worker.props(name, sender()))
server.tell(Get, worker)
}
}
object Worker {
def props(name: String, replyTo: ActorRef) =
Props(new Worker(name, replyTo))
}
class Worker(name: String, replyTo: ActorRef) extends Actor {
def receive = {
case Got(id, map) =>
replyTo ! GetRefReply(map.get(name))
context.stop(self)
}
}
7. 7
class Asker(server: ActorRef) extends Actor {
implicit val timeout = Timeout(1.second)
import context.dispatcher
def receive = {
case GetRef(name) =>
(server ? Get(42))
.mapTo[Got]
.map(got => GetRefReply(got.contents get name))
.pipeTo(sender())
}
}
9. Akka 1.2: Channel[-T]
9
/**
* Abstraction for unification of sender and senderFuture for later reply.
* Can be stored away and used at a later point in time.
*
* The possible reply channel which can be passed into ! and tryTell is always
* untyped, as there is no way to utilize its real static type without
* requiring runtime-costly manifests.
*/
trait Channel[-T] extends japi.Channel[T] {
/**
* Scala API. <p/>
* Sends the specified message to the channel.
*/
def !(msg: T)(implicit sender: UntypedChannel): Unit
...
}
13. The Failures Summarized
• first no clear vision of the goal
• then trying to go too far
• too complicated to declare
• white-box macros required
• not bold enough
• untyped Actors have features that are incompatible with
static typing
13
15. What we want: Parameterized ActorRef
15
object Server {
case class Get(id: Int)(val replyTo: ActorRef[Got])
case class Got(id: Int, contents: Map[String, ActorRef[OtherCommand]])
}
object Client {
case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply])
case class GetRefReply(ref: Option[ActorRef[OtherCommand]])
}
val server: ActorRef[Server.Get] = ???
val behavior: PartialFunction[Any, Unit] = {
case g @ GetRef(name) =>
(server ? Server.Get(42))
.map(got => g.replyTo ! GetRefReply(got.contents get name))
}
16. What we want: Parameterized ActorRef
16
object Server {
case class Get(id: Int)(val replyTo: ActorRef[Got])
case class Got(id: Int, contents: Map[String, ActorRef[OtherCommand]])
}
object Client {
case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply])
case class GetRefReply(ref: Option[ActorRef[OtherCommand]])
}
val server: ActorRef[Server.Get] = ???
val behavior: PartialFunction[Any, Unit] = {
case g @ GetRef(name) =>
(server ? Server.Get(42))
.map(got => g.replyTo ! GetRefReply(got.contents get name))
}
17. What we want: Parameterized ActorRef
17
object Server {
case class Get(id: Int)(val replyTo: ActorRef[Got])
case class Got(id: Int, contents: Map[String, ActorRef[OtherCommand]])
}
object Client {
case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply])
case class GetRefReply(ref: Option[ActorRef[OtherCommand]])
}
val server: ActorRef[Server.Get] = ???
val behavior: PartialFunction[Any, Unit] = {
case g @ GetRef(name) =>
(server ? Server.Get(42))
.map(got => g.replyTo ! GetRefReply(got.contents get name))
}
18. The Guiding Principle
• build everything around ActorRef[-T]
• do not use macros or type calculations that Java
cannot do (i.e. “keep it simple”)
• remove all features that are incompatible with this
• in particular the automatic “sender” capture must go
18
19. Possible Plan
• add type parameter to ActorRef, Actor, …
• remove sender()
• type Receive = PartialFunction[T, Unit]
• restrict context.become to this type
• type-safety achieved—everyone happy!
19
21. « … and determine the behavior to be
applied to the next message.»
— Carl Hewitt, 1973
22. gålbma (sami) — kolme (finnish): THREE
We have one chance to rectify some things
23. Project Gålbma
• distill an Actor to its essence: the Behavior
• everything is a message—for real this time
• remove the danger to close over Actor environment
• behavior composition
• allow completely pure formulation of Actors
23
24. Behavior is King, no more Actor trait
24
object Server {
sealed trait Command
case class Get(id: Int)(val replyTo: ActorRef[Got]) extends Command
case class Put(name: String, ref: ActorRef[OtherCommand]) extends Command
case class Got(id: Int, contents: Map[String, ActorRef[OtherCommand]])
val initial: Behavior[Command] = withMap(Map.empty)
private def withMap(map: Map[String, ActorRef[OtherCommand]]) =
Total[Command] {
case g @ Get(id) =>
g.replyTo ! Got(id, Map.empty)
Same
case Put(name, ref) =>
withMap(map.updated(name, ref))
}
}
25. No More Closing over ActorContext
• ActorContext is passed in for every message
• processing a message returns the next behavior
• lifecycle hooks, Terminated and ReceiveTimeout
are management “signals”
25
final case class Total[T](behavior: T => Behavior[T]) extends Behavior[T] {
override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = Unhandled
override def message(ctx: ActorContext[T], msg: T): Behavior[T] = behavior(msg)
override def toString = s"Total(${LineNumbers(behavior)})"
}
26. Everything behaves like a Message
• ActorContext remains the system interface:
• spawn, stop, watch, unwatch, setReceiveTimeout, schedule,
executionContext, spawnAdapter, props, system, self
• actorOf — for interoperability with untyped Actors
26
Full[Command] {
case Msg(ctx, cmd) => // def receive
case Sig(ctx, PreStart) => // def preStart()
case Sig(ctx, PreRestart(ex)) => // def preRestart(...)
case Sig(ctx, PostRestart(ex)) => // def postRestart(...)
case Sig(ctx, PostStop) => // def postStop()
case Sig(ctx, Failed(ex, child)) => // val supervisorStrategy
case Sig(ctx, ReceiveTimeout) => // case ReceiveTimeout
case Sig(ctx, Terminated(ref)) => // case Terminated(...)
}
27. 27
object Client {
sealed trait Command
case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) extends Command
case class GotWrapper(id: Int, contents: Map[String, ActorRef[OtherCommand]])
extends Command
case class GetRefReply(ref: Option[ActorRef[OtherCommand]])
def initial(server: ActorRef[Server.Command]) =
ContextAware[Command] { ctx =>
val adapter =
ctx.spawnAdapter((got: Server.Got) => GotWrapper(got.id, got.contents))
behv(0, Map.empty)(adapter, server)
}
def behv(nextId: Int,
replies: Map[Int, (String, ActorRef[GetRefReply])])(
implicit adapter: ActorRef[Server.Got],
server: ActorRef[Server.Command]): Behavior[Command] =
Total {
case g @ GetRef(name) =>
server ! Server.Get(nextId)(adapter)
behv(nextId + 1, replies.updated(nextId, name -> g.replyTo))
case GotWrapper(id, contents) =>
replies get id map (p => p._2 ! GetRefReply(contents get p._1))
behv(nextId, replies - id)
}
}
28. 28
object Client {
sealed trait Command
case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) extends Command
case class GotWrapper(id: Int, contents: Map[String, ActorRef[OtherCommand]])
extends Command
case class GetRefReply(ref: Option[ActorRef[OtherCommand]])
def initial(server: ActorRef[Server.Command]) =
ContextAware[Command] { ctx =>
val adapter: ActorRef[Server.Got] =
ctx.spawnAdapter((got: Server.Got) => GotWrapper(got.id, got.contents))
behv(0, Map.empty)(adapter, server)
}
def behv(nextId: Int,
replies: Map[Int, (String, ActorRef[GetRefReply])])(
implicit adapter: ActorRef[Server.Got],
server: ActorRef[Server.Command]): Behavior[Command] =
Total {
case g @ GetRef(name) =>
server ! Server.Get(nextId)(adapter)
behv(nextId + 1, replies.updated(nextId, name -> g.replyTo))
case GotWrapper(id, contents) =>
replies get id map (p => p._2 ! GetRefReply(contents get p._1))
behv(nextId, replies - id)
}
}
29. 29
object Client {
sealed trait Command
case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) extends Command
case class GotWrapper(id: Int, contents: Map[String, ActorRef[OtherCommand]])
extends Command
case class GetRefReply(ref: Option[ActorRef[OtherCommand]])
def initial(server: ActorRef[Server.Command]) =
ContextAware[Command] { ctx =>
val adapter =
ctx.spawnAdapter((got: Server.Got) => GotWrapper(got.id, got.contents))
behv(0, Map.empty)(adapter, server)
}
def behv(nextId: Int,
replies: Map[Int, (String, ActorRef[GetRefReply])]
)(implicit adapter: ActorRef[Server.Got],
server: ActorRef[Server.Command]): Behavior[Command] =
Total {
case g @ GetRef(name) =>
server ! Server.Get(nextId)(adapter)
behv(nextId + 1, replies.updated(nextId, name -> g.replyTo))
case GotWrapper(id, contents) =>
replies get id map (p => p._2 ! GetRefReply(contents get p._1))
behv(nextId, replies - id)
}
}
31. The Implementation
• independent add-on library
• layered completely on top of untyped Actors
• currently 2kLOC main + 1.7kLOC tests
• fully interoperable
31
35. Behavior Rulez!
• decoupling of logic from execution mechanism
• synchronous behavioral tests of individual Actors
• mock ActorContext allows inspection of effects
35
36. 36
object `A Receptionist` {
def `must register a service`(): Unit = {
val ctx = new EffectfulActorContext("register", Props(behavior), system)
val a = Inbox.sync[ServiceA]("a")
val r = Inbox.sync[Registered[_]]("r")
ctx.run(Register(ServiceKeyA, a.ref)(r.ref))
ctx.getAllEffects() should be(Effect.Watched(a.ref) :: Nil)
r.receiveMsg() should be(Registered(ServiceKeyA, a.ref))
val q = Inbox.sync[Listing[ServiceA]]("q")
ctx.run(Find(ServiceKeyA)(q.ref))
ctx.getAllEffects() should be(Nil)
q.receiveMsg() should be(Listing(ServiceKeyA, Set(a.ref)))
assertEmpty(a, r, q)
}
...
}
38. Encoding Types with Members
38
class MyClass {
def myMethod(id: Int): String
def otherMethod(name: String): Unit
protected def helper(arg: Double): Unit
}
39. Encoding Types with Members
• Typed Actors provide complete modules with members
• Typed Actors can encode more flexible access privileges
• more verbose due to syntax being optimized for classes
39
object MyClass {
sealed trait AllCommand
sealed trait Command extends AllCommand
case class MyMethod(id: Int)(replyTo: ActorRef[String]) extends Command
case class OtherMethod(name: String) extends Command
case class Helper(arg: Double) extends AllCommand
val behavior: Behavior[Command] = behavior(42).narrow
private def behavior(x: Int): Behavior[AllCommand] = ???
}
40. Calling Methods
40
object MyClassDemo {
import MyClass._
val myClass: MyClass = ???
val myActor: ActorRef[Command] = ???
implicit val t: Timeout = ???
myClass.otherMethod("John")
myActor!OtherMethod("John")
val result = myClass.myMethod(42)
val future = myActor?MyMethod(42)
}
41. But Actors can do more: Protocols
41
object Protocol {
case class GetSession(replyTo: ActorRef[GetSessionResult])
sealed trait GetSessionResult
case class ActiveSession(service: ActorRef[SessionCommand])
extends GetSessionResult with AuthenticateResult
case class NewSession(auth: ActorRef[Authenticate])
extends GetSessionResult
case class Authenticate(username: String,
password: String,
replyTo: ActorRef[AuthenticateResult])
sealed trait AuthenticateResult
case object FailedSession extends AuthenticateResult
trait SessionCommand
}
43. What can we express?
• everything a classical module with methods can
• pass object references as inputs and outputs
• patterns beyond request–response
• dynamic proxying / delegation
43
44. What can we NOT express?
• any dynamic behavior (e.g. internal state changes)
• session invalidation
44
46. Current Status
• part of Akka 2.4-M1
• http://doc.akka.io/docs/akka/2.4-M1/scala/typed.html
• only bare Actors
• no persistence
• no stash
• no at-least-once delivery
• no Java API yet (but taken into account already)
46
47. Next Steps
• proper Java API (probably in 2.4-M2)
• Receptionist plus akka-distributed-data for Cluster
• port Actor-based APIs to typed ones (e.g. Akka IO)
• add FSM support with transition triggers
• completely pure Actor implementation,
«Actor Action Monad» (inspired by JoinCalculus)
• listen to community feedback
47
48. … and in the far future:
• reap internal benefits by inverting implementation:
• remove sender field (and thus Envelope)
• make untyped Actor a DSL layer on top of Akka Typed
• declare it non-experimental
48