Skip to content
Browse files

add Actor.preReceive/Actor.postReceive to support mixin traits

These return an Option[Receive]; each time we add something
to the behavior stack, if these return Some, we compose
them with the new behavior.

By implementing base classes and mixin traits with these
methods rather than with receive, receive remains available
for concrete classes. This keeps mixins orthogonal i.e.
you don't have to change the concrete class in order to
add a mixin, and you can mix in multiple traits.
  • Loading branch information...
1 parent b8b0a9d commit a168454a6048944bc04b6c16b336a525d7908d30 @havocp committed May 18, 2012
View
78 akka-actor-tests/src/test/scala/akka/actor/ActorRefSpec.scala
@@ -402,43 +402,79 @@ class ActorRefSpec extends AkkaSpec with DefaultTimeout {
}
}
- "support mixin message handlers" in {
- // "override" mixin that runs other handlers second
- trait HandlesA extends Actor {
- override def aroundReceive = ({
- case 'A' sender ! 1
- }: Receive) orElse super.aroundReceive
+ "support mixin message handlers and execute in proper order" in {
+ // "pre" mixin that runs other handlers second
+ trait HandlesA1 extends Actor {
+ override def preReceive = {
+ val handler: Receive = {
+ case "A1" sender ! "HandlesA1"
+ }
+ Some(super.preReceive.foldRight(handler)(_ orElse _))
+ }
+ }
+
+ // another "pre" mixin
+ trait HandlesA2 extends Actor {
+ override def preReceive = {
+ val handler: Receive = {
+ case "A2" sender ! "HandlesA2"
+ case "A1" sender ! "HandlesA2" // not reached, HandlesA1 filters
+ }
+ Some(super.preReceive.foldRight(handler)(_ orElse _))
+ }
+ }
+
+ // "post" mixin that runs other handlers first
+ trait HandlesB1 extends Actor {
+ override def postReceive = {
+ val handler: Receive = {
+ case "B1" sender ! "HandlesB1"
+ case "C" sender ! "HandlesB1" // not reached, HandlesC filters
+ }
+ Some(super.postReceive.foldRight(handler)(_ orElse _))
+ }
}
- // "fallback" mixin that runs other handlers first
- trait HandlesB extends Actor {
- override def aroundReceive = super.aroundReceive orElse ({
- case 'B' sender ! 2
- case 'C' sender ! 42 // not reached, HandlesC filters
- }: Receive)
+
+ // another "post" mixin
+ trait HandlesB2 extends Actor {
+ override def postReceive = {
+ val handler: Receive = {
+ case "B2" sender ! "HandlesB2"
+ case "B1" sender ! "HandlesB2" // not reached, HandlesB1 filters
+ case "C" sender ! "HandlesB2" // not reached, HandlesC filters
+ }
+ Some(super.postReceive.foldRight(handler)(_ orElse _))
+ }
}
+
// this is a completely unmodified actor other
// than having "with HandlesA with HandlesB",
// it doesn't have to worry about chaining up
// or anything like that.
- class HandlesC extends Actor with HandlesA with HandlesB {
+ class HandlesC extends Actor with HandlesA1 with HandlesA2 with HandlesB1 with HandlesB2 {
def receive = {
- case 'C' sender ! 3
- case 'A' sender ! 42 // not reached, HandlesA filters
+ case "C" sender ! "HandlesC"
+ case "A1" sender ! "HandlesC" // not reached, HandlesA1 filters
+ case "A2" sender ! "HandlesC" // not reached, HandlesA2 filters
}
}
val timeout = Timeout(20000)
val ref = system.actorOf(Props(new HandlesC))
- val one = (ref.ask('A')(timeout)).mapTo[Int]
- val two = (ref.ask('B')(timeout)).mapTo[Int]
- val three = (ref.ask('C')(timeout)).mapTo[Int]
+ val a1 = (ref.ask("A1")(timeout)).mapTo[String]
+ val a2 = (ref.ask("A2")(timeout)).mapTo[String]
+ val c = (ref.ask("C")(timeout)).mapTo[String]
+ val b1 = (ref.ask("B1")(timeout)).mapTo[String]
+ val b2 = (ref.ask("B2")(timeout)).mapTo[String]
ref ! PoisonPill
- Await.result(one, timeout.duration) must be(1)
- Await.result(two, timeout.duration) must be(2)
- Await.result(three, timeout.duration) must be(3)
+ Await.result(a1, timeout.duration) must be("HandlesA1")
+ Await.result(a2, timeout.duration) must be("HandlesA2")
+ Await.result(c, timeout.duration) must be("HandlesC")
+ Await.result(b1, timeout.duration) must be("HandlesB1")
+ Await.result(b2, timeout.duration) must be("HandlesB2")
awaitCond(ref.isTerminated, 2000 millis)
}
View
73 akka-actor/src/main/scala/akka/actor/Actor.scala
@@ -240,24 +240,60 @@ trait Actor {
final def sender: ActorRef = context.sender
/**
- * This allows traits and subclasses to mix in actor behavior;
- * by default it chains to receive(), so you _must_ chain up
- * if you override this! If you want your mixin's message
- * handling to run before other handlers, chain up
- * _after_ you run your own message-handling code:
- * <p/>
+ * This method allows traits and subclasses to mix in actor behavior;
+ * the provided partial function, if any, will receive the message
+ * before `receive` and have a chance to intercept it.
+ * To allow multiple mixin traits, implementations of this
+ * method should chain up to `super.preReceive` and
+ * combine their own handler with any pre-existing handler
+ * using `PartialFunction.orElse`.
+ *
* {{{
- * trait Mixin extends Actor {
- * override def aroundReceive = ({
- * case CustomMessage =>
- * }: Receive) orElse super.aroundReceive
+ * override def preReceive = {
+ * val handler: Receive = {
+ * case "MyMessage" ⇒
+ * }
+ * Some(super.preReceive.foldRight(handler)(_ orElse _))
* }
* }}}
- * If you want your mixin to provide a fallback after
- * the subtype's handlers run, then chain up first and
- * fall back to your own code second.
+ *
+ * Using `foldRight` runs your handler after the supertype's handler
+ * if any, which means mixin traits run from left to right. This
+ * is a good convention to follow.
+ *
+ * Note that `preReceive` is only invoked once each time the Actor's
+ * behavior changes (for example on actor creation, and when someone
+ * calls `become`). The `preReceive` handler is not recreated for every message.
*/
- protected def aroundReceive: Receive = receive
+ protected def preReceive: Option[Receive] = None
+
+ /**
+ * This method allows traits and subclasses to mix in actor behavior;
+ * the provided partial function, if any, will receive the message
+ * after `receive()` if `receive()` does not handle it.
+ * To allow multiple mixin traits, implementations of this
+ * method should chain up to `super.postReceive` and
+ * combine their own handler with any pre-existing handler
+ * using `PartialFunction.orElse`.
+ *
+ * {{{
+ * override def postReceive = {
+ * val handler: Receive = {
+ * case "MyMessage" ⇒
+ * }
+ * Some(super.postReceive.foldRight(handler)(_ orElse _))
+ * }
+ * }}}
+ *
+ * Using `foldRight` runs your handler after the supertype's handler
+ * if any, which means mixin traits run from left to right. This
+ * is a good convention to follow.
+ *
+ * Note that `postReceive` is only invoked once each time the Actor's
+ * behavior changes (for example on actor creation, and when someone
+ * calls `become`). The `postReceive` handler is not recreated for every message.
+ */
+ protected def postReceive: Option[Receive] = None
/**
* This defines the initial actor behavior, it must return a partial function
@@ -337,11 +373,16 @@ trait Actor {
if (head.isDefinedAt(msg)) head.apply(msg) else unhandled(msg)
}
+ private def wrapBehavior(behavior: Receive): Receive = {
+ val chain = Seq(preReceive, Some(behavior), postReceive).flatMap(_.toSeq)
+ chain.reduce(_ orElse _)
+ }
+
/**
* For Akka internal use only.
*/
private[akka] def pushBehavior(behavior: Receive): Unit = {
- behaviorStack = behaviorStack.push(behavior)
+ behaviorStack = behaviorStack.push(wrapBehavior(behavior))
}
/**
@@ -359,6 +400,6 @@ trait Actor {
private[akka] def clearBehaviorStack(): Unit =
behaviorStack = Stack.empty[Receive].push(behaviorStack.last)
- private var behaviorStack: Stack[Receive] = Stack.empty[Receive].push(aroundReceive)
+ private var behaviorStack: Stack[Receive] = Stack.empty[Receive].push(wrapBehavior(receive))
}
View
28 akka-docs/scala/actors.rst
@@ -668,20 +668,24 @@ state of the failing actor instance is lost if you don't store and restore it in
``preRestart`` and ``postRestart`` callbacks.
-Extending Actors using aroundReceive and PartialFunction chaining
-===============================================
+Extending Actors using preReceive, postReceive, and PartialFunction chaining
+============================================================================
You can create "mixin" traits or abstract classes using the
-``aroundReceive`` method on ``Actor``. By default,
-``aroundReceive`` simply calls ``receive``; the difference is that
-when overriding ``aroundReceive``, you are required to chain up to
-``super.aroundReceive`` with ``PartialFunction.orElse``:
-
-.. includecode:: code/akka/docs/actor/ActorDocSpec.scala#receive-aroundReceive
-
-Multiple traits that extend ``aroundReceive`` in this way can be
-mixed in to the same concrete class. The concrete class need not
-do anything special, it implements ``receive`` as usual.
+``preReceive`` and ``postReceive`` methods on ``Actor``. These
+return an ``Option[Receive]]``, ``None`` by default. If you
+override them to return a handler, it will run before or after the
+standard actor behavior as defined by ``receive`` or ``become``.
+To allow multiple traits to be mixed in to one actor, when you
+override ``preReceive`` or ``postReceive`` you should always chain
+up and combine your handler with other handlers using ``orElse``:
+
+.. includecode:: code/akka/docs/actor/ActorDocSpec.scala#receive-preReceive
+
+Multiple traits that implement ``preReceive`` or ``postReceive``
+in this way can be mixed in to the same concrete class. The
+concrete class need not do anything special, it implements
+``receive`` as usual.
``PartialFunction.orElse`` chaining can also be used for more
complex scenarios, like dynamic runtime registration of handlers:
View
38 akka-docs/scala/code/akka/docs/actor/ActorDocSpec.scala
@@ -114,7 +114,7 @@ object SwapperApp extends App {
}
//#swapper
-//#receive-aroundReceive
+//#receive-preReceive
// trait providing a generic fallback message handler
trait GenericActor extends Actor {
@@ -123,12 +123,12 @@ trait GenericActor extends Actor {
case event printf("generic: %s\n", event)
}
- // You could reverse the order of your
- // handler and super.aroundReceive to
- // override rather than fall back, of course.
- // The default Actor.aroundReceive forwards
- // to receive.
- override def aroundReceive = super.aroundReceive orElse genericMessageHandler
+ // you'd do preReceive in exactly the same way.
+ // foldRight (rather than foldLeft) means if you
+ // mix in two traits, the one written on the left
+ // will get messages first.
+ override def postReceive =
+ Some(super.postReceive.foldRight(genericMessageHandler)(_ orElse _))
}
class SpecificActor extends GenericActor {
@@ -138,19 +138,31 @@ class SpecificActor extends GenericActor {
}
case class MyMsg(subject: String)
-//#receive-aroundReceive
+//#receive-preReceive
//#receive-orElse
trait ComposableActor extends Actor {
private var receives: List[Receive] = List()
+ private var composedReceives: Receive = Map.empty // in Scala 2.10, PartialFunction.empty
+
protected def registerReceive(receive: Receive) {
+ // keep a list (allows unregistration)
receives = receive :: receives
+ // cache the composition of all receives
+ composedReceives = receives reduce { _ orElse _ }
+ }
+
+ // this indirection is because preReceive is only called
+ // once, but we want to allow registration post-construct,
+ // so we need a constant Receive that forwards to our
+ // dynamic Receive
+ private def handleRegisteredReceives: Receive = new PartialFunction[Any, Unit]() {
+ override def apply(x: Any) = composedReceives.apply(x)
+ override def isDefinedAt(x: Any) = composedReceives.isDefinedAt(x)
}
- // Runs dynamically-registered handlers before
- // default handler (default aroundReceive forwards
- // to receive)
- override def aroundReceive = (receives reduce { _ orElse _ }) orElse super.aroundReceive
+ override def preReceive =
+ Some(super.preReceive.foldRight(handleRegisteredReceives)(_ orElse _))
}
class MyComposableActor extends ComposableActor {
@@ -166,7 +178,7 @@ class MyComposableActor extends ComposableActor {
}
// Runs after the dynamically-registered handlers,
- // which are run in aroundReceive
+ // which are added with preReceive
def receive = {
case "baz"
}

0 comments on commit a168454

Please sign in to comment.
Something went wrong with that request. Please try again.