From 0afc3b17213d9a2a80232b1121ae4a99e4507545 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 27 Sep 2012 15:57:46 +0200 Subject: [PATCH] add ReliableProxy pattern to demo akka-contrib --- akka-contrib/README.md | 8 +- akka-contrib/docs/ReliableProxy.png | Bin 0 -> 32304 bytes akka-contrib/docs/index.rst | 66 ++++++ akka-contrib/docs/reliable-proxy.rst | 92 ++++++++ .../akka/contrib/pattern/ReliableProxy.scala | 167 +++++++++++++++ .../contrib/pattern/ReliableProxySpec.scala | 196 ++++++++++++++++++ .../contrib/pattern/ReliableProxyTest.java | 123 +++++++++++ .../pattern/ReliableProxyDocSpec.scala | 41 ++++ akka-docs/rst/experimental/index.rst | 14 +- project/AkkaBuild.scala | 78 +++---- 10 files changed, 731 insertions(+), 54 deletions(-) create mode 100644 akka-contrib/docs/ReliableProxy.png create mode 100644 akka-contrib/docs/index.rst create mode 100644 akka-contrib/docs/reliable-proxy.rst create mode 100644 akka-contrib/src/main/scala/akka/contrib/pattern/ReliableProxy.scala create mode 100644 akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ReliableProxySpec.scala create mode 100644 akka-contrib/src/test/java/akka/contrib/pattern/ReliableProxyTest.java create mode 100644 akka-contrib/src/test/scala/akka/contrib/pattern/ReliableProxyDocSpec.scala diff --git a/akka-contrib/README.md b/akka-contrib/README.md index c01a24d7c8..ced22e74f3 100644 --- a/akka-contrib/README.md +++ b/akka-contrib/README.md @@ -14,8 +14,12 @@ A module in this subproject doesn't have to obey the rule of staying binary comp ## Suggested Format of Contributions -Each contribution should be a self-contained unit, consisting of one source file without dependencies to other modules in this subproject (it may depend on anything else in the Akka distribution, though). This ensures that contributions may be moved into the standard distribution individually. +Each contribution should be a self-contained unit, consisting of one source file or one exclusively used package, without dependencies to other modules in this subproject; it may depend on everything else in the Akka distribution, though. This ensures that contributions may be moved into the standard distribution individually. The module shall be within a subpackage of `akka.contrib`. + +Each module must be accompanied by a test suite which verifies that the provided features work, possibly complemented by integration and unit tests. The tests should follow the [Developer Guidelines](http://doc.akka.io/docs/akka/current/dev/developer-guidelines.html#testing) and go into the `src/test/scala` or `src/test/java` directories (with package name matching the module which is being tested). As an example, if the module were called `akka.contrib.pattern.ReliableProxy`, then the test suite should be called `akka.contrib.pattern.ReliableProxySpec`. + +Each module must also have proper documentation in [reStructured Text format](http://sphinx.pocoo.org/rest.html). The documentation should be a single `.rst` file in the `akka-contrib/docs` directory, including a link from `index.rst`. ## Suggested Way of Using these Contributions -Since the Akka team does not restrict updates to this subproject even during otherwise binary compatible releases, and modules may be removed without deprecation, it is suggested to copy the source files into your own code base, changing the package name. This way you can choose when to update or which fixes to include (to keep binary compatibility if needed) and later releases of Akka do not potentially break your application. \ No newline at end of file +Since the Akka team does not restrict updates to this subproject even during otherwise binary compatible releases, and modules may be removed without deprecation, it is suggested to copy the source files into your own code base, changing the package name. This way you can choose when to update or which fixes to include (to keep binary compatibility if needed) and later releases of Akka do not potentially break your application. diff --git a/akka-contrib/docs/ReliableProxy.png b/akka-contrib/docs/ReliableProxy.png new file mode 100644 index 0000000000000000000000000000000000000000..81575fd9f24d28dee5f4fe62fb157a31b03ee7bc GIT binary patch literal 32304 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 literal 0 HcmV?d00001 diff --git a/akka-contrib/docs/index.rst b/akka-contrib/docs/index.rst new file mode 100644 index 0000000000..9f5b57c513 --- /dev/null +++ b/akka-contrib/docs/index.rst @@ -0,0 +1,66 @@ +External Contributions +====================== + +This subproject provides a home to modules contributed by external developers +which may or may not move into the officially supported code base over time. +The conditions under which this transition can occur include: + +* there must be enough interest in the module to warrant inclusion in the + standard distribution, +* the module must be actively maintained and +* code quality must be good enough to allow efficient maintenance by the Akka + core development team + +If a contributions turns out to not “take off” it may be removed again at a +later time. + +Caveat Emptor +------------- + +A module in this subproject doesn't have to obey the rule of staying binary +compatible between minor releases. Breaking API changes may be introduced in +minor releases without notice as we refine and simplify based on your feedback. +A module may be dropped in any release without prior deprecation. The Typesafe +subscription does not cover support for these modules. + +The Current List of Modules +--------------------------- + +.. toctree:: + + reliable-proxy + +Suggested Way of Using these Contributions +------------------------------------------ + +Since the Akka team does not restrict updates to this subproject even during +otherwise binary compatible releases, and modules may be removed without +deprecation, it is suggested to copy the source files into your own code base, +changing the package name. This way you can choose when to update or which +fixes to include (to keep binary compatibility if needed) and later releases of +Akka do not potentially break your application. + +Suggested Format of Contributions +--------------------------------- + +Each contribution should be a self-contained unit, consisting of one source +file or one exclusively used package, without dependencies to other modules in +this subproject; it may depend on everything else in the Akka distribution, +though. This ensures that contributions may be moved into the standard +distribution individually. The module shall be within a subpackage of +``akka.contrib``. + +Each module must be accompanied by a test suite which verifies that the +provided features work, possibly complemented by integration and unit tests. +The tests should follow the :ref:`developer_guidelines` and go into the +``src/test/scala`` or ``src/test/java`` directories (with package name matching +the module which is being tested). As an example, if the module were called +``akka.contrib.pattern.ReliableProxy``, then the test suite should be called +``akka.contrib.pattern.ReliableProxySpec``. + +Each module must also have proper documentation in `reStructured Text`_ format. +The documentation should be a single ``.rst`` file in the +``akka-contrib/docs`` directory, including a link from ``index.rst`` (this file). + +.. _reStructured Text: http://sphinx.pocoo.org/rest.html + diff --git a/akka-contrib/docs/reliable-proxy.rst b/akka-contrib/docs/reliable-proxy.rst new file mode 100644 index 0000000000..add4fa0340 --- /dev/null +++ b/akka-contrib/docs/reliable-proxy.rst @@ -0,0 +1,92 @@ +Reliable Proxy Pattern +====================== + +Looking at :ref:`message-send-semantics` one might come to the conclusion that +Akka actors are made for blue-sky scenarios: sending messages is the only way +for actors to communicate, and then that is not even guaranteed to work. Is the +whole paradigm built on sand? Of course the answer is an emphatic “No!”. + +A local message send—within the same JVM instance—is not likely to fail, and if +it does the reason was one of + +* it was meant to fail (due to consciously choosing a bounded mailbox, which + upon overflow will have to drop messages) +* or it failed due to a catastrophic VM error, e.g. an + :class:`OutOfMemoryError`, a memory access violation (“segmentation fault”, + GPF, etc.), JVM bug—or someone calling ``System.exit()``. + +In all of these cases, the actor was very likely not in a position to process +the message anyway, so this part of the non-guarantee is not problematic. + +It is a lot more likely for an unintended message delivery failure to occur +when a message send crosses JVM boundaries, i.e. an intermediate unreliable +network is involved. If someone unplugs an ethernet cable, or a power failure +shuts down a router, messages will be lost while the actors would be able to +process them just fine. + +.. note:: + + This does not mean that message send semantics are different between local + and remote operations, it just means that in practice there is a difference + between how good the “best effort” is. + +Introducing the Reliable Proxy +------------------------------ + +.. image:: ReliableProxy.png + +To bridge the disparity between “local” and “remote” sends is the goal of this +pattern. When sending from A to B must be as reliable as in-JVM, regardless of +the deployment, then you can interject a reliable tunnel and send through that +instead. The tunnel consists of two end-points, where the ingress point P (the +“proxy”) is a child of A and the egress point E is a child of P, deployed onto +the same network node where B lives. Messages sent to P will be wrapped in an +envelope, tagged with a sequence number and sent to E, who verifies that the +received envelope has the right sequence number (the next expected one) and +forwards the contained message to B. When B receives this message, the +``sender`` will be a reference to the sender of the original message to P. +Reliability is added by E replying to orderly received messages with an ACK, so +that P can tick those messages off its resend list. If ACKs do not come in a +timely fashion, P will try to resend until successful. + +Exactly what does it guarantee? +------------------------------- + +Sending via a :class:`ReliableProxy` makes the message send exactly as reliable +as if the represented target were to live within the same JVM, provided that +the remote actor system does not terminate. In effect, both ends (i.e. JVM and +actor system) must be considered as one when evaluating the reliability of this +communication channel. The benefit is that the network in-between is taken out +of that equation. + +When the target actor terminates, the proxy will terminate as well (on the +terms of :ref:`deathwatch-java` / :ref:`deathwath-scala`). + +How to use it +------------- + +Since this implementation does not offer much in the way of configuration, +simply instantiate a proxy wrapping some target reference. From Java it looks +like this: + +.. includecode:: ../src/test/java/akka/contrib/pattern/ReliableProxyTest.java#imports +.. includecode:: ../src/test/java/akka/contrib/pattern/ReliableProxyTest.java#demo-proxy + +And from Scala like this: + +.. includecode:: ../src/multi-jvm/scala/akka/contrib/pattern/ReliableProxySpec.scala#demo + +Since the :class:`ReliableProxy` actor is an :ref:`fsm-scala`, it also offers +the capability to subscribe to state transitions. If you need to know when all +enqueued messages have been received by the remote end-point (and consequently +been forwarded to the target), you can subscribe to the FSM notifications and +observe a transition from state :class:`ReliableProxy.Active` to state +:class:`ReliableProxy.Idle`. + +.. includecode:: ../src/test/java/akka/contrib/pattern/ReliableProxyTest.java#demo-transition + +From Scala it would look like so: + +.. includecode:: ../src/test/scala/akka/contrib/pattern/ReliableProxyDocSpec.scala#demo-transition + + diff --git a/akka-contrib/src/main/scala/akka/contrib/pattern/ReliableProxy.scala b/akka-contrib/src/main/scala/akka/contrib/pattern/ReliableProxy.scala new file mode 100644 index 0000000000..d46eff9f5f --- /dev/null +++ b/akka-contrib/src/main/scala/akka/contrib/pattern/ReliableProxy.scala @@ -0,0 +1,167 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.contrib.pattern + +import akka.actor._ +import akka.remote.RemoteScope +import scala.concurrent.util._ + +object ReliableProxy { + + class Receiver(target: ActorRef) extends Actor with ActorLogging { + var lastSerial = 0 + + context.watch(target) + + def receive = { + case Message(msg, snd, serial) ⇒ + if (serial == lastSerial + 1) { + target.tell(msg, snd) + sender ! Ack(serial) + lastSerial = serial + } else if (compare(serial, lastSerial) <= 0) { + sender ! Ack(serial) + } else { + log.debug("received msg of {} from {} with wrong serial", msg.asInstanceOf[AnyRef].getClass, snd) + } + case Terminated(`target`) ⇒ context stop self + } + } + + /** + * Wrap-around aware comparison of integers: differences limited to 2^31-1 + * in magnitude will work correctly. + */ + def compare(a: Int, b: Int): Int = (a - b) match { + case x if x < 0 ⇒ -1 + case x if x == 0 ⇒ 0 + case x if x > 0 ⇒ 1 + } + + case class Message(msg: Any, sender: ActorRef, serial: Int) + case class Ack(serial: Int) + case object Tick + + def receiver(target: ActorRef): Props = Props(new Receiver(target)) + + sealed trait State + case object Idle extends State + case object Active extends State + + // Java API + val idle = Idle + val active = Active +} + +import ReliableProxy._ + +/** + * A ReliableProxy is a means to wrap a remote actor reference in order to + * obtain certain improved delivery guarantees: + * + * - as long as none of the JVMs crashes and the proxy and its remote-deployed + * peer are not forcefully terminated or restarted, no messages will be lost + * - messages re-sent due to the first point will not be delivered out-of-order, + * message ordering is preserved + * + * These guarantees are valid for the communication between the two end-points + * of the reliable “tunnel”, which usually spans an unreliable network. Delivery + * from the remote end-point to the target actor is still subject to in-JVM + * delivery semantics (i.e. not strictly guaranteed due to possible OutOfMemory + * situations or other VM errors). + * + * You can create a reliable connection like this: + * {{{ + * val proxy = context.actorOf(Props(new ReliableProxy(target))) + * }}} + * or in Java: + * {{{ + * final ActorRef proxy = getContext().actorOf(new Props(new UntypedActorFactory() { + * public Actor create() { + * return new ReliableProxy(target); + * } + * })); + * }}} + * + * '''''Please note:''''' the tunnel is uni-directional, and original sender + * information is retained, hence replies by the wrapped target reference will + * go back in the normal “unreliable” way unless also secured by a ReliableProxy + * from the remote end. + * + * ==Message Types== + * + * This actor is an [[akka.actor.FSM]], hence it offers the service of + * transition callbacks to those actors which subscribe using the + * ``SubscribeTransitionCallBack`` and ``UnsubscribeTransitionCallBack`` + * messages; see [[akka.actor.FSM]] for more documentation. The proxy will + * transition into [[ReliableProxy.Active]] state when ACKs are outstanding and + * return to the [[ReliableProxy.Idle]] state when every message send so far + * has been confirmed by the peer end-point. + * Any other message type sent to this actor will be delivered via a remote-deployed + * child actor to the designated target. Message types declared in the companion + * object are for internal use only and not to be sent from the outside. + * + * ==Failure Cases== + * + * All failures of either the local or the remote end-point are escalated to the + * parent of this actor; there are no specific error cases which are predefined. + * + * ==Arguments== + * + * '''''target''''' is the [[akka.actor.ActorRef]] to which all messages will be + * forwarded which are sent to this actor. It can be any type of actor reference, + * but the “remote” tunnel endpoint will be deployed on the node where the target + * ref points to. + * + * '''''retryAfter''''' is the ACK timeout after which all outstanding messages + * will be resent. There is not limit on the queue size or the number of retries. + */ +class ReliableProxy(target: ActorRef, retryAfter: FiniteDuration) extends Actor with FSM[State, Vector[Message]] { + + val tunnel = context.actorOf(receiver(target).withDeploy(Deploy(scope = RemoteScope(target.path.address))), "tunnel") + context.watch(tunnel) + + override def supervisorStrategy = OneForOneStrategy() { + case _ ⇒ SupervisorStrategy.Escalate + } + + startWith(Idle, Vector.empty) + + when(Idle) { + case Event(Terminated(`tunnel`), _) ⇒ stop + case Event(Ack(_), _) ⇒ stay + case Event(msg, _) ⇒ goto(Active) using Vector(send(msg)) + } + + onTransition { + case Idle -> Active ⇒ scheduleTick() + case Active -> Idle ⇒ cancelTimer("resend") + } + + when(Active) { + case Event(Terminated(`tunnel`), _) ⇒ stop + case Event(Ack(serial), queue) ⇒ + val q = queue dropWhile (m ⇒ compare(m.serial, serial) <= 0) + scheduleTick() + if (q.isEmpty) goto(Idle) using Vector.empty + else stay using q + case Event(Tick, queue) ⇒ + queue foreach (tunnel ! _) + scheduleTick() + stay + case Event(msg, queue) ⇒ stay using (queue :+ send(msg)) + } + + def scheduleTick(): Unit = setTimer("resend", Tick, retryAfter, false) + + var nextSerial = 1 + def send(msg: Any): Message = { + val m = Message(msg, sender, nextSerial) + nextSerial += 1 + tunnel ! m + m + } + +} \ No newline at end of file diff --git a/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ReliableProxySpec.scala b/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ReliableProxySpec.scala new file mode 100644 index 0000000000..03fef8da54 --- /dev/null +++ b/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ReliableProxySpec.scala @@ -0,0 +1,196 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.contrib.pattern + +import language.postfixOps + +import akka.remote.testkit.MultiNodeConfig +import akka.remote.testkit.MultiNodeSpec +import akka.remote.testkit.STMultiNodeSpec +import org.scalatest.BeforeAndAfterEach +import akka.remote.testconductor.Direction +import akka.actor.Props +import akka.actor.Actor +import akka.testkit.ImplicitSender +import scala.concurrent.util.duration._ +import akka.actor.FSM +import akka.actor.ActorRef +import akka.testkit.TestProbe + +object ReliableProxySpec extends MultiNodeConfig { + val local = role("local") + val remote = role("remote") +} + +class ReliableProxyMultiJvmNode1 extends ReliableProxySpec +class ReliableProxyMultiJvmNode2 extends ReliableProxySpec + +class ReliableProxySpec extends MultiNodeSpec(ReliableProxySpec) with STMultiNodeSpec with BeforeAndAfterEach with ImplicitSender { + import ReliableProxySpec._ + import ReliableProxy._ + + override def initialParticipants = 2 + + override def afterEach { + runOn(local) { + testConductor.throttle(local, remote, Direction.Both, -1).await + } + } + + runOn(remote) { + system.actorOf(Props(new Actor { + def receive = { + case x ⇒ testActor ! x + } + }), "echo") + } + + val target = system.actorFor(node(remote) / "user" / "echo") + + var proxy: ActorRef = _ + def expectState(s: State) = expectMsg(FSM.CurrentState(proxy, s)) + def expectTransition(s1: State, s2: State) = expectMsg(FSM.Transition(proxy, s1, s2)) + + runOn(local) { + //#demo + import akka.contrib.pattern.ReliableProxy + + proxy = system.actorOf(Props(new ReliableProxy(target, 100.millis)), "proxy") + //#demo + proxy ! FSM.SubscribeTransitionCallBack(testActor) + expectState(Idle) + //#demo + proxy ! "hello" + //#demo + expectTransition(Idle, Active) + expectTransition(Active, Idle) + } + runOn(remote) { + expectMsg("hello") + } + + "A ReliableProxy" must { + + "forward messages in sequence" in { + runOn(local) { + (1 to 100) foreach (proxy ! _) + expectTransition(Idle, Active) + expectTransition(Active, Idle) + } + runOn(remote) { + within(1 second) { + (1 to 100) foreach { n ⇒ expectMsg(n); lastSender must be === target } + } + } + + enterBarrier("test1a") + + runOn(local) { + (1 to 100) foreach (proxy ! _) + expectTransition(Idle, Active) + expectTransition(Active, Idle) + } + runOn(remote) { + within(1 second) { + (1 to 100) foreach { n ⇒ expectMsg(n); lastSender must be === target } + } + } + + enterBarrier("test1b") + } + + "retry when sending fails" in { + runOn(local) { + testConductor.blackhole(local, remote, Direction.Send).await + (1 to 100) foreach (proxy ! _) + within(1 second) { + expectTransition(Idle, Active) + expectNoMsg + } + } + + enterBarrier("test2a") + + runOn(remote) { + expectNoMsg(0 seconds) + } + + enterBarrier("test2b") + + runOn(local) { + testConductor.throttle(local, remote, Direction.Send, -1) + expectTransition(Active, Idle) + } + runOn(remote) { + within(1 second) { + (1 to 100) foreach { n ⇒ expectMsg(n); lastSender must be === target } + } + } + + enterBarrier("test2c") + } + + "retry when receiving fails" in { + runOn(local) { + testConductor.blackhole(local, remote, Direction.Receive).await + (1 to 100) foreach (proxy ! _) + within(1 second) { + expectTransition(Idle, Active) + expectNoMsg + } + } + runOn(remote) { + within(1 second) { + (1 to 100) foreach { n ⇒ expectMsg(n); lastSender must be === target } + } + } + + enterBarrier("test3a") + + runOn(local) { + testConductor.throttle(local, remote, Direction.Receive, -1) + expectTransition(Active, Idle) + } + + enterBarrier("test3b") + } + + "resend across a slow link" in { + runOn(local) { + testConductor.throttle(local, remote, Direction.Send, rateMBit = 0.1).await + (1 to 50) foreach (proxy ! _) + within(5 seconds) { + expectTransition(Idle, Active) + expectTransition(Active, Idle) + } + } + runOn(remote) { + within(5 seconds) { + (1 to 50) foreach { n ⇒ expectMsg(n); lastSender must be === target } + } + } + + enterBarrier("test4a") + + runOn(local) { + testConductor.throttle(local, remote, Direction.Send, rateMBit = -1).await + testConductor.throttle(local, remote, Direction.Receive, rateMBit = 0.1).await + (1 to 50) foreach (proxy ! _) + within(5 seconds) { + expectTransition(Idle, Active) + expectTransition(Active, Idle) + } + } + runOn(remote) { + within(1 second) { + (1 to 50) foreach { n ⇒ expectMsg(n); lastSender must be === target } + } + } + + enterBarrier("test4a") + } + + } +} \ No newline at end of file diff --git a/akka-contrib/src/test/java/akka/contrib/pattern/ReliableProxyTest.java b/akka-contrib/src/test/java/akka/contrib/pattern/ReliableProxyTest.java new file mode 100644 index 0000000000..9522155d81 --- /dev/null +++ b/akka-contrib/src/test/java/akka/contrib/pattern/ReliableProxyTest.java @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.contrib.pattern; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import scala.concurrent.util.Duration; +import scala.concurrent.util.FiniteDuration; +import akka.actor.Actor; +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.FSM; +import akka.actor.Props; +import akka.actor.UntypedActor; +import akka.actor.UntypedActorFactory; +import akka.testkit.TestProbe; + +//#import +import akka.contrib.pattern.ReliableProxy; +//#import + +public class ReliableProxyTest { + + private static ActorSystem system; + + @BeforeClass + public static void setup() { + system = ActorSystem.create(); + } + + @AfterClass + public static void teardown() { + system.shutdown(); + } + + @Test + public void demonstrateUsage() { + final TestProbe probe = TestProbe.apply(system); + final ActorRef target = probe.ref(); + final ActorRef parent = system.actorOf(new Props(new UntypedActorFactory() { + private static final long serialVersionUID = 1L; + + public Actor create() { + return new UntypedActor() { + + //#demo-proxy + final ActorRef proxy = getContext().actorOf( + new Props(new UntypedActorFactory() { + private static final long serialVersionUID = 1L; + + public Actor create() { + final FiniteDuration retry = Duration.create(100, "millis"); + return new ReliableProxy(target, retry); + } + })); + + public void onReceive(Object msg) { + if ("hello".equals(msg)) { + proxy.tell("world!", getSelf()); + } + } + //#demo-proxy + }; + } + })); + parent.tell("hello", null); + probe.expectMsg("world!"); + } + + @Test + public void demonstrateTransitions() { + final ActorRef target = system.deadLetters(); + final ActorRef parent = system.actorOf(new Props(new UntypedActorFactory() { + private static final long serialVersionUID = 1L; + + public Actor create() { + return new UntypedActor() { + + //#demo-transition + final ActorRef proxy = getContext().actorOf( + new Props(new UntypedActorFactory() { + private static final long serialVersionUID = 1L; + + public Actor create() { + final FiniteDuration retry = Duration.create(100, "millis"); + return new ReliableProxy(target, retry); + } + })); + ActorRef client = null; + + { + proxy.tell(new FSM.SubscribeTransitionCallBack(getSelf()), getSelf()); + } + + public void onReceive(Object msg) { + if ("hello".equals(msg)) { + proxy.tell("world!", getSelf()); + client = getSender(); + } else if (msg instanceof FSM.CurrentState) { + // get initial state + } else if (msg instanceof FSM.Transition) { + @SuppressWarnings("unchecked") + final FSM.Transition transition = + (FSM.Transition) msg; + assert transition.fsmRef().equals(proxy); + if (transition.to().equals(ReliableProxy.idle())) { + client.tell("done", getSelf()); + } + } + } + //#demo-transition + }; + } + })); + final TestProbe probe = TestProbe.apply(system); + parent.tell("hello", probe.ref()); + probe.expectMsg("done"); + } +} diff --git a/akka-contrib/src/test/scala/akka/contrib/pattern/ReliableProxyDocSpec.scala b/akka-contrib/src/test/scala/akka/contrib/pattern/ReliableProxyDocSpec.scala new file mode 100644 index 0000000000..1e89cb8a05 --- /dev/null +++ b/akka-contrib/src/test/scala/akka/contrib/pattern/ReliableProxyDocSpec.scala @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.contrib.pattern + +import akka.testkit.AkkaSpec +import akka.actor.Props +import akka.actor.Actor +import akka.testkit.ImplicitSender +import scala.concurrent.util.duration._ +import akka.actor.FSM +import akka.actor.ActorRef + +class ReliableProxyDocSpec extends AkkaSpec with ImplicitSender { + + "A ReliableProxy" must { + + "show state transitions" in { + val target = system.deadLetters + val a = system.actorOf(Props(new Actor { + //#demo-transition + val proxy = context.actorOf(Props(new ReliableProxy(target, 100.millis))) + proxy ! FSM.SubscribeTransitionCallBack(self) + + var client: ActorRef = _ + + def receive = { + case "go" ⇒ proxy ! 42; client = sender + case FSM.CurrentState(`proxy`, initial) ⇒ + case FSM.Transition(`proxy`, from, to) ⇒ if (to == ReliableProxy.Idle) client ! "done" + } + //#demo-transition + })) + a ! "go" + expectMsg("done") + } + + } + +} \ No newline at end of file diff --git a/akka-docs/rst/experimental/index.rst b/akka-docs/rst/experimental/index.rst index 5b3b1465ee..47339a8fb0 100644 --- a/akka-docs/rst/experimental/index.rst +++ b/akka-docs/rst/experimental/index.rst @@ -16,12 +16,18 @@ in minor releases without notice as we refine and simplify based on your feedback. An experimental module may be dropped in major releases without prior deprecation. -Another reason for marking a module as experimental is that it's too early -to tell if the module has a maintainer that can take the responsibility -of the module over time. - .. toctree:: :maxdepth: 1 ../cluster/index ../dev/multi-node-testing + +Another reason for marking a module as experimental is that it's too early +to tell if the module has a maintainer that can take the responsibility +of the module over time. These modules live in the ``akka-contrib`` subproject: + +.. toctree:: + :maxdepth: 2 + + ../b/../../../akka-contrib/docs/index.rst + diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index a0881ef6ed..5a0506b6f0 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -135,7 +135,7 @@ object AkkaBuild extends Build { id = "akka-remote-tests-experimental", base = file("akka-remote-tests"), dependencies = Seq(remote, actorTests % "test->test", testkit), - settings = defaultSettings ++ multiJvmSettings ++ Seq( + settings = defaultSettings ++ multiJvmSettings ++ experimentalSettings ++ Seq( libraryDependencies ++= Dependencies.remoteTests, // disable parallel tests parallelExecution in Test := false, @@ -143,19 +143,7 @@ object AkkaBuild extends Build { (name: String) => (src ** (name + ".conf")).get.headOption.map("-Dakka.config=" + _.absolutePath).toSeq }, scalatestOptions in MultiJvm := defaultMultiJvmScalatestOptions, - jvmOptions in MultiJvm := defaultMultiJvmOptions, - previousArtifact := akkaPreviousArtifact("akka-remote"), - description := """|This module of Akka is marked as - |experimental, which means that it is in early - |access mode, which also means that it is not covered - |by commercial support. An experimental module doesn't - |have to obey the rule of staying binary compatible - |between minor releases. Breaking API changes may be - |introduced in minor releases without notice as we - |refine and simplify based on your feedback. An - |experimental module may be dropped in major releases - |without prior deprecation. - |""".stripMargin + previousArtifact := akkaPreviousArtifact("akka-remote") ) ) configs (MultiJvm) @@ -163,7 +151,7 @@ object AkkaBuild extends Build { id = "akka-cluster-experimental", base = file("akka-cluster"), dependencies = Seq(remote, remoteTests % "test->test" , testkit % "test->test"), - settings = defaultSettings ++ multiJvmSettings ++ OSGi.cluster ++ Seq( + settings = defaultSettings ++ multiJvmSettings ++ OSGi.cluster ++ experimentalSettings ++ Seq( libraryDependencies ++= Dependencies.cluster, // disable parallel tests parallelExecution in Test := false, @@ -171,19 +159,7 @@ object AkkaBuild extends Build { (name: String) => (src ** (name + ".conf")).get.headOption.map("-Dakka.config=" + _.absolutePath).toSeq }, scalatestOptions in MultiJvm := defaultMultiJvmScalatestOptions, - jvmOptions in MultiJvm := defaultMultiJvmOptions, - previousArtifact := akkaPreviousArtifact("akka-remote"), - description := """|This module of Akka is marked as - |experimental, which means that it is in early - |access mode, which also means that it is not covered - |by commercial support. An experimental module doesn't - |have to obey the rule of staying binary compatible - |between minor releases. Breaking API changes may be - |introduced in minor releases without notice as we - |refine and simplify based on your feedback. An - |experimental module may be dropped in major releases - |without prior deprecation. - |""".stripMargin + previousArtifact := akkaPreviousArtifact("akka-remote") ) ) configs (MultiJvm) @@ -354,25 +330,13 @@ object AkkaBuild extends Build { id = "akka-sample-cluster-experimental", base = file("akka-samples/akka-sample-cluster"), dependencies = Seq(cluster, remoteTests % "test", testkit % "test"), - settings = sampleSettings ++ multiJvmSettings ++ Seq( + settings = sampleSettings ++ multiJvmSettings ++ experimentalSettings ++ Seq( libraryDependencies ++= Dependencies.clusterSample, // disable parallel tests parallelExecution in Test := false, extraOptions in MultiJvm <<= (sourceDirectory in MultiJvm) { src => (name: String) => (src ** (name + ".conf")).get.headOption.map("-Dakka.config=" + _.absolutePath).toSeq - }, - jvmOptions in MultiJvm := defaultMultiJvmOptions, - description := """|This module of Akka is marked as - |experimental, which means that it is in early - |access mode, which also means that it is not covered - |by commercial support. An experimental module doesn't - |have to obey the rule of staying binary compatible - |between minor releases. Breaking API changes may be - |introduced in minor releases without notice as we - |refine and simplify based on your feedback. An - |experimental module may be dropped in major releases - |without prior deprecation. - |""".stripMargin + } ) ) configs (MultiJvm) @@ -380,14 +344,13 @@ object AkkaBuild extends Build { id = "akka-sample-multi-node-experimental", base = file("akka-samples/akka-sample-multi-node"), dependencies = Seq(remoteTests % "test", testkit % "test"), - settings = sampleSettings ++ multiJvmSettings ++ Seq( + settings = sampleSettings ++ multiJvmSettings ++ experimentalSettings ++ Seq( libraryDependencies ++= Dependencies.multiNodeSample, // disable parallel tests parallelExecution in Test := false, extraOptions in MultiJvm <<= (sourceDirectory in MultiJvm) { src => (name: String) => (src ** (name + ".conf")).get.headOption.map("-Dakka.config=" + _.absolutePath).toSeq - }, - jvmOptions in MultiJvm := defaultMultiJvmOptions + } ) ) configs (MultiJvm) @@ -408,8 +371,10 @@ object AkkaBuild extends Build { lazy val contrib = Project( id = "akka-contrib", base = file("akka-contrib"), - dependencies = Seq(actor), - settings = defaultSettings ++ Seq( + dependencies = Seq(remote, remoteTests % "compile;test->test"), + settings = defaultSettings ++ multiJvmSettings ++ Seq( + libraryDependencies ++= Dependencies.contrib, + testOptions += Tests.Argument(TestFrameworks.JUnit, "-v"), description := """| |This subproject provides a home to modules contributed by external |developers which may or may not move into the officially supported code @@ -421,7 +386,7 @@ object AkkaBuild extends Build { |support for these modules. |""".stripMargin ) - ) + ) configs (MultiJvm) // Settings @@ -448,6 +413,20 @@ object AkkaBuild extends Build { publishArtifact in Compile := false ) + lazy val experimentalSettings = Seq( + description := """|This module of Akka is marked as + |experimental, which means that it is in early + |access mode, which also means that it is not covered + |by commercial support. An experimental module doesn't + |have to obey the rule of staying binary compatible + |between minor releases. Breaking API changes may be + |introduced in minor releases without notice as we + |refine and simplify based on your feedback. An + |experimental module may be dropped in major releases + |without prior deprecation. + |""".stripMargin + ) + val excludeTestNames = SettingKey[Seq[String]]("exclude-test-names") val excludeTestTags = SettingKey[Set[String]]("exclude-test-tags") val includeTestTags = SettingKey[Set[String]]("include-test-tags") @@ -589,6 +568,7 @@ object AkkaBuild extends Build { } lazy val multiJvmSettings = SbtMultiJvm.multiJvmSettings ++ inConfig(MultiJvm)(ScalariformPlugin.scalariformSettings) ++ Seq( + jvmOptions in MultiJvm := defaultMultiJvmOptions, compileInputs in MultiJvm <<= (compileInputs in MultiJvm) dependsOn (ScalariformKeys.format in MultiJvm), compile in MultiJvm <<= (compile in MultiJvm) triggeredBy (compile in Test), ScalariformKeys.preferences in MultiJvm := formattingPreferences) ++ @@ -706,6 +686,8 @@ object Dependencies { val clusterSample = Seq(Test.scalatest) + val contrib = Seq(Test.junitIntf) + val multiNodeSample = Seq(Test.scalatest) }