Advertisement
Advertisement

More Related Content

Advertisement
Advertisement

Project Gålbma – Actors vs Types

  1. Project Gålbma: Actors vs Types Dr. Roland Kuhn @rolandkuhn — Akka Tech Lead
  2. Motivation
  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()) } }
  8. Failed Attempts
  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 ... }
  10. Akka 2.1: Typed Channels 10
  11. Akka 2.1: Typed Channels 11
  12. Akka 2.1: Typed Channels 12
  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
  14. The Solution
  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
  20. But why stop here?
  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) } }
  30. Under the Hood
  31. The Implementation • independent add-on library • layered completely on top of untyped Actors • currently 2kLOC main + 1.7kLOC tests • fully interoperable 31
  32. The most important interface: Behavior[T] • Behaviors: • Full, FullTotal, Total, Partial, Static • Decorators: • ContextAware, SelfAware, SynchronousSelf, Tap • Combinators: • And, Or, Widened 32 abstract class Behavior[T] { def management(ctx: ActorContext[T], msg: Signal): Behavior[T] def message(ctx: ActorContext[T], msg: T): Behavior[T] def narrow[U <: T]: Behavior[U] = this.asInstanceOf[Behavior[U]] }
  33. ActorSystem ≈ ActorRef 33 object Demo extends App { implicit val t = Timeout(1.second) val guardian = ContextAware[Client.Command] { ctx => val server = ctx.spawn(Props(Server.initial), "server") val client = ctx.spawn(Props(Client.initial(server)), "client") Static { case msg => client ! msg } } val system = ActorSystem("Demo", Props(guardian)) import system.executionContext system ? Client.GetRef("X") map println foreach (_ => system.terminate()) }
  34. Testing
  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) } ... }
  37. What can we do with it?
  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 }
  42. But Actors can do more: Protocols 42
  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
  45. Summary and Outlook
  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
  49. ©Typesafe 2015 – All Rights Reserved
Advertisement