project gålbma – actors vs types

49
Project Gålbma: Actors vs Types Dr. Roland Kuhn @rolandkuhn — Akka Tech Lead

Upload: roland-kuhn

Post on 03-Aug-2015

379 views

Category:

Software


1 download

TRANSCRIPT

Project Gålbma: Actors vs Types

Dr. Roland Kuhn @rolandkuhn — Akka Tech Lead

Motivation

Motivation

3

case class Getcase class Got(contents: Map[String, ActorRef])

class Server extends Actor { var map = Map.empty[String, ActorRef] def receive = { case Get => sender ! Got(map) }}

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

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

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

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

Failed Attempts

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

...}

Akka 2.1: Typed Channels

10

Akka 2.1: Typed Channels

11

Akka 2.1: Typed Channels

12

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

The Solution

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))}

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))}

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))}

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

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

But why stop here?

« … and determine the behavior to be applied to the next message.»

— Carl Hewitt, 1973

gålbma (sami) — kolme (finnish): THREE

We have one chance to rectify some things

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

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)) }}

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)})"}

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

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

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

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) }}

Under the Hood

The Implementation

• independent add-on library

• layered completely on top of untyped Actors • currently 2kLOC main + 1.7kLOC tests

• fully interoperable

31

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]]}

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

Testing

Behavior Rulez!

• decoupling of logic from execution mechanism

• synchronous behavioral tests of individual Actors

• mock ActorContext allows inspection of effects

35

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) }

...}

What can we do with it?

Encoding Types with Members

38

class MyClass {

def myMethod(id: Int): String def otherMethod(name: String): Unit protected def helper(arg: Double): Unit

}

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] = ???}

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)}

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}

But Actors can do more: Protocols

42

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

What can we NOT express?

• any dynamic behavior (e.g. internal state changes)

• session invalidation

44

Summary and Outlook

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

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 Join Calculus)

• listen to community feedback

47

… 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

©Typesafe 2015 – All Rights Reserved