From 07e361c6845979bb7cd48c10293a8893704bc624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjo=CC=88rn=20Antonsson?= Date: Mon, 24 Feb 2014 12:11:02 +0100 Subject: [PATCH] !act,sam #3889 Adding Activator template FSM/become for Java with Lambda support * Dining Hakkers Activator template for FSM and become * Cleaup of FSM event matchers to be more usable and consistent --- .../main/java/akka/japi/pf/AbstractMatch.java | 2 + .../java/akka/japi/pf/AbstractPFBuilder.java | 2 + akka-actor/src/main/java/akka/japi/pf/FI.java | 19 ++ .../akka/japi/pf/FSMStateFunctionBuilder.java | 226 +++++++++++------- .../java/akka/japi/pf/FSMStopBuilder.java | 2 + .../japi/pf/FSMTransitionHandlerBuilder.java | 2 + .../src/main/java/akka/japi/pf/Match.java | 2 + .../src/main/java/akka/japi/pf/PFBuilder.java | 2 + .../java/akka/japi/pf/ReceiveBuilder.java | 1 + .../src/main/java/akka/japi/pf/UnitMatch.java | 2 + .../main/java/akka/japi/pf/UnitPFBuilder.java | 2 + .../main/scala/akka/actor/AbstractActor.scala | 21 ++ .../src/main/scala/akka/actor/FSM.scala | 95 ++++++-- akka-docs/rst/experimental/index.rst | 2 + akka-docs/rst/java/lambda-actors.rst | 7 + akka-docs/rst/java/lambda-fsm.rst | 8 + .../src/test/java/docs/actor/fsm/Buncher.java | 2 +- .../test/java/docs/actor/fsm/FSMDocTest.java | 6 +- .../akka-sample-fsm-java-lambda/COPYING | 121 ++++++++++ .../akka-sample-fsm-java-lambda/LICENSE | 10 + .../activator.properties | 7 + .../akka-sample-fsm-java-lambda/build.sbt | 15 ++ .../akka-sample-fsm-java-lambda/pom.xml | 53 ++++ .../project/build.properties | 1 + .../sample/become/DiningHakkersOnBecome.java | 162 +++++++++++++ .../src/main/java/sample/become/Messages.java | 39 +++ .../java/sample/fsm/DiningHakkersOnFsm.java | 205 ++++++++++++++++ .../src/main/java/sample/fsm/Messages.java | 35 +++ .../tutorial/index.html | 73 ++++++ scripts/build/extra-build-steps.sh | 5 + 30 files changed, 1026 insertions(+), 103 deletions(-) create mode 100644 akka-samples/akka-sample-fsm-java-lambda/COPYING create mode 100644 akka-samples/akka-sample-fsm-java-lambda/LICENSE create mode 100644 akka-samples/akka-sample-fsm-java-lambda/activator.properties create mode 100644 akka-samples/akka-sample-fsm-java-lambda/build.sbt create mode 100644 akka-samples/akka-sample-fsm-java-lambda/pom.xml create mode 100644 akka-samples/akka-sample-fsm-java-lambda/project/build.properties create mode 100644 akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/become/DiningHakkersOnBecome.java create mode 100644 akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/become/Messages.java create mode 100644 akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/fsm/DiningHakkersOnFsm.java create mode 100644 akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/fsm/Messages.java create mode 100644 akka-samples/akka-sample-fsm-java-lambda/tutorial/index.html diff --git a/akka-actor/src/main/java/akka/japi/pf/AbstractMatch.java b/akka-actor/src/main/java/akka/japi/pf/AbstractMatch.java index cb2083a921..11218ffff5 100644 --- a/akka-actor/src/main/java/akka/japi/pf/AbstractMatch.java +++ b/akka-actor/src/main/java/akka/japi/pf/AbstractMatch.java @@ -12,6 +12,8 @@ import scala.PartialFunction; * * @param the input type, that this PartialFunction will be applied to * @param the return type, that the results of the application will have + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ class AbstractMatch { diff --git a/akka-actor/src/main/java/akka/japi/pf/AbstractPFBuilder.java b/akka-actor/src/main/java/akka/japi/pf/AbstractPFBuilder.java index e780178998..4a1e0e7fd5 100644 --- a/akka-actor/src/main/java/akka/japi/pf/AbstractPFBuilder.java +++ b/akka-actor/src/main/java/akka/japi/pf/AbstractPFBuilder.java @@ -11,6 +11,8 @@ import scala.PartialFunction; * * @param the input type, that this PartialFunction will be applied to * @param the return type, that the results of the application will have + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ abstract class AbstractPFBuilder { diff --git a/akka-actor/src/main/java/akka/japi/pf/FI.java b/akka-actor/src/main/java/akka/japi/pf/FI.java index beacf08511..9d3564b465 100644 --- a/akka-actor/src/main/java/akka/japi/pf/FI.java +++ b/akka-actor/src/main/java/akka/japi/pf/FI.java @@ -7,6 +7,8 @@ package akka.japi.pf; /** * Class that encapsulates all the Functional Interfaces * used for creating partial functions. + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ public final class FI { private FI() { @@ -61,6 +63,23 @@ public final class FI { public boolean defined(T t); } + /** + * Functional interface for a predicate. + * + * @param the type that the predicate will operate on. + * @param the type that the predicate will operate on. + */ + public static interface TypedPredicate2 { + /** + * The predicate to evaluate. + * + * @param t an instance that the predicate is evaluated on. + * @param u an instance that the predicate is evaluated on. + * @return the result of the predicate + */ + public boolean defined(T t, U u); + } + /** * Functional interface for an application. * diff --git a/akka-actor/src/main/java/akka/japi/pf/FSMStateFunctionBuilder.java b/akka-actor/src/main/java/akka/japi/pf/FSMStateFunctionBuilder.java index 2d94ba7b4d..2ea031f5c7 100644 --- a/akka-actor/src/main/java/akka/japi/pf/FSMStateFunctionBuilder.java +++ b/akka-actor/src/main/java/akka/japi/pf/FSMStateFunctionBuilder.java @@ -6,8 +6,6 @@ package akka.japi.pf; import akka.actor.FSM; import scala.PartialFunction; - -import java.util.Arrays; import java.util.List; /** @@ -15,6 +13,8 @@ import java.util.List; * * @param the state type * @param the data type + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ @SuppressWarnings("rawtypes") public class FSMStateFunctionBuilder { @@ -22,6 +22,89 @@ public class FSMStateFunctionBuilder { private PFBuilder, FSM.State> builder = new PFBuilder, FSM.State>(); + /** + * An erased processing of the event matcher. The compile time checks are enforced + * by the public typed versions. + * + * It works like this. + * + * If eventOrType or dataOrType is a Class, then we do a isInstance check, + * otherwise we do an equals check. The null value compares true for anything. + * If the predicate is null, it is skipped otherwise the predicate has to match + * as well. + * + * @param eventOrType an event or a type to match against + * @param dataOrType a data instance or a type to match against + * @param predicate a predicate to match against + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + private FSMStateFunctionBuilder erasedEvent(final Object eventOrType, + final Object dataOrType, + final FI.TypedPredicate2 predicate, + final FI.Apply2 apply) { + builder.match(FSM.Event.class, + new FI.TypedPredicate() { + @Override + public boolean defined(FSM.Event e) { + boolean res = true; + if (eventOrType != null) { + if (eventOrType instanceof Class) { + Class eventType = (Class) eventOrType; + res = eventType.isInstance(e.event()); + } + else { + res = eventOrType.equals(e.event()); + } + } + if (res && dataOrType != null) { + if (dataOrType instanceof Class) { + Class dataType = (Class) dataOrType; + res = dataType.isInstance(e.stateData()); + } + else { + res = dataOrType.equals(e.stateData()); + } + } + if (res && predicate != null) { + @SuppressWarnings("unchecked") + boolean ures = predicate.defined(e.event(), e.stateData()); + res = ures; + } + return res; + } + }, + new FI.Apply>() { + public FSM.State apply(FSM.Event e) throws Exception { + @SuppressWarnings("unchecked") + FSM.State res = (FSM.State) apply.apply(e.event(), e.stateData()); + return res; + } + } + ); + + return this; + } + + /** + * Add a case statement that matches on an event and data type and a predicate. + * + * @param eventType the event type to match on + * @param dataType the data type to match on + * @param predicate a predicate to evaluate on the matched types + * @param apply an action to apply to the event and state data if there is a match + * @param

the event type to match on + * @param the data type to match on + * @return the builder with the case statement added + */ + public final FSMStateFunctionBuilder event(final Class

eventType, + final Class dataType, + final FI.TypedPredicate2 predicate, + final FI.Apply2> apply) { + erasedEvent(eventType, dataType, predicate, apply); + return this; + } + /** * Add a case statement that matches on an event and data type. * @@ -35,25 +118,45 @@ public class FSMStateFunctionBuilder { public FSMStateFunctionBuilder event(final Class

eventType, final Class dataType, final FI.Apply2> apply) { - builder.match(FSM.Event.class, - new FI.TypedPredicate() { - @Override - public boolean defined(FSM.Event e) { - return eventType.isInstance(e.event()) && dataType.isInstance(e.stateData()); - } - }, - new FI.Apply>() { - public FSM.State apply(FSM.Event e) throws Exception { - @SuppressWarnings("unchecked") - P p = (P) e.event(); - @SuppressWarnings("unchecked") - Q q = (Q) e.stateData(); - return apply.apply(p, q); - } - } - ); + return erasedEvent(eventType, dataType, null, apply); + } - return this; + /** + * Add a case statement that matches if the event type and predicate matches. + * + * @param eventType the event type to match on + * @param predicate a predicate that will be evaluated on the data and the event + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + public

FSMStateFunctionBuilder event(final Class

eventType, + final FI.TypedPredicate2 predicate, + final FI.Apply2> apply) { + return erasedEvent(eventType, null, predicate, apply); + } + + /** + * Add a case statement that matches if the event type and predicate matches. + * + * @param eventType the event type to match on + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + public

FSMStateFunctionBuilder event(final Class

eventType, + final FI.Apply2> apply) { + return erasedEvent(eventType, null, null, apply); + } + + /** + * Add a case statement that matches if the predicate matches. + * + * @param predicate a predicate that will be evaluated on the data and the event + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + public FSMStateFunctionBuilder event(final FI.TypedPredicate2 predicate, + final FI.Apply2> apply) { + return erasedEvent(null, null, predicate, apply); } /** @@ -68,12 +171,12 @@ public class FSMStateFunctionBuilder { */ public FSMStateFunctionBuilder event(final List eventMatches, final Class dataType, - final FI.Apply> apply) { + final FI.Apply2> apply) { builder.match(FSM.Event.class, new FI.TypedPredicate() { @Override public boolean defined(FSM.Event e) { - if (!dataType.isInstance(e.stateData())) + if (dataType != null && !dataType.isInstance(e.stateData())) return false; boolean emMatch = false; @@ -95,7 +198,7 @@ public class FSMStateFunctionBuilder { public FSM.State apply(FSM.Event e) throws Exception { @SuppressWarnings("unchecked") Q q = (Q) e.stateData(); - return apply.apply(q); + return apply.apply(e.event(), q); } } ); @@ -103,21 +206,6 @@ public class FSMStateFunctionBuilder { return this; } - /** - * Add a case statement that matches on the data type and if the event compares equal. - * - * @param event an event to compare equal against - * @param dataType the data type to match on - * @param apply an action to apply to the event and state data if there is a match - * @param the data type to match on - * @return the builder with the case statement added - */ - public FSMStateFunctionBuilder eventEquals(final Object event, - final Class dataType, - final FI.Apply> apply) { - return event(Arrays.asList(event), dataType, apply); - } - /** * Add a case statement that matches if any of the event types in the list match or * any of the event instances in the list compares equal. @@ -127,36 +215,23 @@ public class FSMStateFunctionBuilder { * @return the builder with the case statement added */ public FSMStateFunctionBuilder event(final List eventMatches, - final FI.Apply> apply) { - builder.match(FSM.Event.class, - new FI.TypedPredicate() { - @Override - public boolean defined(FSM.Event e) { - boolean emMatch = false; - Object event = e.event(); - for (Object em : eventMatches) { - if (em instanceof Class) { - Class emc = (Class) em; - emMatch = emc.isInstance(event); - } else { - emMatch = event.equals(em); - } - if (emMatch) - break; - } - return emMatch; - } - }, - new FI.Apply>() { - public FSM.State apply(FSM.Event e) throws Exception { - @SuppressWarnings("unchecked") - D d = (D) e.stateData(); - return apply.apply(d); - } - } - ); + final FI.Apply2> apply) { + return event(eventMatches, null, apply); + } - return this; + /** + * Add a case statement that matches on the data type and if the event compares equal. + * + * @param event an event to compare equal against + * @param dataType the data type to match on + * @param apply an action to apply to the event and state data if there is a match + * @param the data type to match on + * @return the builder with the case statement added + */ + public FSMStateFunctionBuilder eventEquals(final P event, + final Class dataType, + final FI.Apply2> apply) { + return erasedEvent(event, dataType, null, apply); } /** @@ -166,9 +241,9 @@ public class FSMStateFunctionBuilder { * @param apply an action to apply to the event and state data if there is a match * @return the builder with the case statement added */ - public FSMStateFunctionBuilder eventEquals(final Object event, - final FI.Apply> apply) { - return event(Arrays.asList(event), apply); + public

FSMStateFunctionBuilder eventEquals(final P event, + final FI.Apply2> apply) { + return erasedEvent(event, null, null, apply); } /** @@ -178,16 +253,7 @@ public class FSMStateFunctionBuilder { * @return the builder with the case statement added */ public FSMStateFunctionBuilder anyEvent(final FI.Apply2> apply) { - builder.match(FSM.Event.class, - new FI.Apply>() { - public FSM.State apply(FSM.Event e) throws Exception { - @SuppressWarnings("unchecked") - D d = (D) e.stateData(); - return apply.apply(e.event(), d); - } - }); - - return this; + return erasedEvent(null, null, null, apply); } /** diff --git a/akka-actor/src/main/java/akka/japi/pf/FSMStopBuilder.java b/akka-actor/src/main/java/akka/japi/pf/FSMStopBuilder.java index 50affc24b8..045be389c3 100644 --- a/akka-actor/src/main/java/akka/japi/pf/FSMStopBuilder.java +++ b/akka-actor/src/main/java/akka/japi/pf/FSMStopBuilder.java @@ -13,6 +13,8 @@ import scala.runtime.BoxedUnit; * * @param the state type * @param the data type + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ public class FSMStopBuilder { diff --git a/akka-actor/src/main/java/akka/japi/pf/FSMTransitionHandlerBuilder.java b/akka-actor/src/main/java/akka/japi/pf/FSMTransitionHandlerBuilder.java index 118131c367..6c05e4e90b 100644 --- a/akka-actor/src/main/java/akka/japi/pf/FSMTransitionHandlerBuilder.java +++ b/akka-actor/src/main/java/akka/japi/pf/FSMTransitionHandlerBuilder.java @@ -12,6 +12,8 @@ import scala.Tuple2; * Builder used to create a partial function for {@link akka.actor.FSM#onTransition}. * * @param the state type + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ public class FSMTransitionHandlerBuilder { diff --git a/akka-actor/src/main/java/akka/japi/pf/Match.java b/akka-actor/src/main/java/akka/japi/pf/Match.java index 1cfffae15d..c4926a869c 100644 --- a/akka-actor/src/main/java/akka/japi/pf/Match.java +++ b/akka-actor/src/main/java/akka/japi/pf/Match.java @@ -13,6 +13,8 @@ import scala.PartialFunction; * * @param the input type, that this PartialFunction will be applied to * @param the return type, that the results of the application will have + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ public class Match extends AbstractMatch { diff --git a/akka-actor/src/main/java/akka/japi/pf/PFBuilder.java b/akka-actor/src/main/java/akka/japi/pf/PFBuilder.java index ab7e0bd41d..414c8c0b84 100644 --- a/akka-actor/src/main/java/akka/japi/pf/PFBuilder.java +++ b/akka-actor/src/main/java/akka/japi/pf/PFBuilder.java @@ -9,6 +9,8 @@ package akka.japi.pf; * * @param the input type, that this PartialFunction will be applied to * @param the return type, that the results of the application will have + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ public final class PFBuilder extends AbstractPFBuilder { diff --git a/akka-actor/src/main/java/akka/japi/pf/ReceiveBuilder.java b/akka-actor/src/main/java/akka/japi/pf/ReceiveBuilder.java index 7004611212..4768489dde 100644 --- a/akka-actor/src/main/java/akka/japi/pf/ReceiveBuilder.java +++ b/akka-actor/src/main/java/akka/japi/pf/ReceiveBuilder.java @@ -28,6 +28,7 @@ package akka.japi.pf; * } * * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ public class ReceiveBuilder { private ReceiveBuilder() { diff --git a/akka-actor/src/main/java/akka/japi/pf/UnitMatch.java b/akka-actor/src/main/java/akka/japi/pf/UnitMatch.java index 6c23143552..dcc81af128 100644 --- a/akka-actor/src/main/java/akka/japi/pf/UnitMatch.java +++ b/akka-actor/src/main/java/akka/japi/pf/UnitMatch.java @@ -15,6 +15,8 @@ import scala.runtime.BoxedUnit; * void methods to {@link scala.runtime.BoxedUnit}. * * @param the input type, that this PartialFunction will be applied to + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ public class UnitMatch extends AbstractMatch { diff --git a/akka-actor/src/main/java/akka/japi/pf/UnitPFBuilder.java b/akka-actor/src/main/java/akka/japi/pf/UnitPFBuilder.java index 1347b0ef5b..78ae74368d 100644 --- a/akka-actor/src/main/java/akka/japi/pf/UnitPFBuilder.java +++ b/akka-actor/src/main/java/akka/japi/pf/UnitPFBuilder.java @@ -12,6 +12,8 @@ import scala.runtime.BoxedUnit; * void methods to {@link scala.runtime.BoxedUnit}. * * @param the input type, that this PartialFunction to be applied to + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ public final class UnitPFBuilder extends AbstractPFBuilder { diff --git a/akka-actor/src/main/scala/akka/actor/AbstractActor.scala b/akka-actor/src/main/scala/akka/actor/AbstractActor.scala index f5f71b6877..6727c43562 100644 --- a/akka-actor/src/main/scala/akka/actor/AbstractActor.scala +++ b/akka-actor/src/main/scala/akka/actor/AbstractActor.scala @@ -4,6 +4,11 @@ package akka.actor +/** + * Java API: compatible with lambda expressions + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. + */ object AbstractActor { /** * emptyBehavior is a Receive-expression that matches no messages at all, ever. @@ -12,6 +17,8 @@ object AbstractActor { } /** + * Java API: compatible with lambda expressions + * * Actor base class that should be extended to create Java actors that use lambdas. *

* Example: @@ -33,6 +40,8 @@ object AbstractActor { * } * } * + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ abstract class AbstractActor extends Actor { /** @@ -44,6 +53,8 @@ abstract class AbstractActor extends Actor { } /** + * Java API: compatible with lambda expressions + * * Actor base class that should be extended to create an actor with a stash. * * The stash enables an actor to temporarily stash away messages that can not or @@ -83,18 +94,28 @@ abstract class AbstractActor extends Actor { * For a `Stash` based actor that enforces unbounded deques see [[akka.actor.AbstractActorWithUnboundedStash]]. * There is also an unrestricted version [[akka.actor.AbstractActorWithUnrestrictedStash]] that does not * enforce the mailbox type. + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ abstract class AbstractActorWithStash extends AbstractActor with Stash /** + * Java API: compatible with lambda expressions + * * Actor base class with `Stash` that enforces an unbounded deque for the actor. The proper mailbox has to be configured * manually, and the mailbox should extend the [[akka.dispatch.DequeBasedMessageQueueSemantics]] marker trait. * See [[akka.actor.AbstractActorWithStash]] for details on how `Stash` works. + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ abstract class AbstractActorWithUnboundedStash extends AbstractActor with UnboundedStash /** + * Java API: compatible with lambda expressions + * * Actor base class with `Stash` that does not enforce any mailbox type. The mailbox of the actor has to be configured * manually. See [[akka.actor.AbstractActorWithStash]] for details on how `Stash` works. + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ abstract class AbstractActorWithUnrestrictedStash extends AbstractActor with UnrestrictedStash diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 9bba9ee5fa..f084b42f13 100644 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -755,7 +755,9 @@ trait LoggingFSM[S, D] extends FSM[S, D] { this: Actor ⇒ } /** - * Java API + * Java API: compatible with lambda expressions + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ object AbstractFSM { /** @@ -770,9 +772,11 @@ object AbstractFSM { } /** - * Java API + * Java API: compatible with lambda expressions * * Finite State Machine actor abstract base class. + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ abstract class AbstractFSM[S, D] extends FSM[S, D] { import akka.japi.pf._ @@ -874,6 +878,20 @@ abstract class AbstractFSM[S, D] extends FSM[S, D] { final def onTermination(stopBuilder: FSMStopBuilder[S, D]): Unit = onTermination(stopBuilder.build().asInstanceOf[PartialFunction[StopEvent, Unit]]) + /** + * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. + * + * A case statement that matches on an event and data type and a predicate. + * + * @param eventType the event type to match on + * @param dataType the data type to match on + * @param predicate a predicate to evaluate on the matched types + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchEvent[ET, DT <: D](eventType: Class[ET], dataType: Class[DT], predicate: TypedPredicate2[ET, DT], apply: Apply2[ET, DT, State]): FSMStateFunctionBuilder[S, D] = + new FSMStateFunctionBuilder[S, D]().event(eventType, dataType, apply) + /** * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. * @@ -887,6 +905,43 @@ abstract class AbstractFSM[S, D] extends FSM[S, D] { final def matchEvent[ET, DT <: D](eventType: Class[ET], dataType: Class[DT], apply: Apply2[ET, DT, State]): FSMStateFunctionBuilder[S, D] = new FSMStateFunctionBuilder[S, D]().event(eventType, dataType, apply) + /** + * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. + * + * A case statement that matches if the event type and predicate matches. + * + * @param eventType the event type to match on + * @param predicate a predicate that will be evaluated on the data and the event + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchEvent[ET](eventType: Class[ET], predicate: TypedPredicate2[ET, D], apply: Apply2[ET, D, State]): FSMStateFunctionBuilder[S, D] = + new FSMStateFunctionBuilder[S, D]().event(eventType, predicate, apply); + + /** + * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. + * + * A case statement that matches if the event type matches. + * + * @param eventType the event type to match on + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchEvent[ET](eventType: Class[ET], apply: Apply2[ET, D, State]): FSMStateFunctionBuilder[S, D] = + new FSMStateFunctionBuilder[S, D]().event(eventType, apply); + + /** + * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. + * + * A case statement that matches if the predicate matches. + * + * @param predicate a predicate that will be evaluated on the data and the event + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchEvent(predicate: TypedPredicate2[AnyRef, D], apply: Apply2[AnyRef, D, State]): FSMStateFunctionBuilder[S, D] = + new FSMStateFunctionBuilder[S, D]().event(predicate, apply); + /** * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. * @@ -898,22 +953,9 @@ abstract class AbstractFSM[S, D] extends FSM[S, D] { * @param apply an action to apply to the event and state data if there is a match * @return the builder with the case statement added */ - final def matchEvent[DT <: D](eventMatches: JList[AnyRef], dataType: Class[DT], apply: Apply[DT, State]): FSMStateFunctionBuilder[S, D] = + final def matchEvent[DT <: D](eventMatches: JList[AnyRef], dataType: Class[DT], apply: Apply2[AnyRef, DT, State]): FSMStateFunctionBuilder[S, D] = new FSMStateFunctionBuilder[S, D]().event(eventMatches, dataType, apply); - /** - * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. - * - * A case statement that matches on the data type and if the event compares equal. - * - * @param event an event to compare equal against - * @param dataType the data type to match on - * @param apply an action to apply to the event and state data if there is a match - * @return the builder with the case statement added - */ - final def matchEventEquals[DT <: D](event: AnyRef, dataType: Class[DT], apply: Apply[DT, State]): FSMStateFunctionBuilder[S, D] = - new FSMStateFunctionBuilder[S, D]().eventEquals(event, dataType, apply); - /** * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. * @@ -924,9 +966,22 @@ abstract class AbstractFSM[S, D] extends FSM[S, D] { * @param apply an action to apply to the event and state data if there is a match * @return the builder with the case statement added */ - final def matchEvent(eventMatches: JList[AnyRef], apply: Apply[D, State]): FSMStateFunctionBuilder[S, D] = + final def matchEvent(eventMatches: JList[AnyRef], apply: Apply2[AnyRef, D, State]): FSMStateFunctionBuilder[S, D] = new FSMStateFunctionBuilder[S, D]().event(eventMatches, apply); + /** + * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. + * + * A case statement that matches on the data type and if the event compares equal. + * + * @param event an event to compare equal against + * @param dataType the data type to match on + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchEventEquals[E, DT <: D](event: E, dataType: Class[DT], apply: Apply2[E, DT, State]): FSMStateFunctionBuilder[S, D] = + new FSMStateFunctionBuilder[S, D]().eventEquals(event, dataType, apply); + /** * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. * @@ -936,7 +991,7 @@ abstract class AbstractFSM[S, D] extends FSM[S, D] { * @param apply an action to apply to the event and state data if there is a match * @return the builder with the case statement added */ - final def matchEventEquals(event: AnyRef, apply: Apply[D, State]): FSMStateFunctionBuilder[S, D] = + final def matchEventEquals[E](event: E, apply: Apply2[E, D, State]): FSMStateFunctionBuilder[S, D] = new FSMStateFunctionBuilder[S, D]().eventEquals(event, apply); /** @@ -1068,8 +1123,10 @@ abstract class AbstractFSM[S, D] extends FSM[S, D] { } /** - * Java API + * Java API: compatible with lambda expressions * * Finite State Machine actor abstract base class. + * + * This is an EXPERIMENTAL feature and is subject to change until it has received more real world testing. */ abstract class AbstractLoggingFSM[S, D] extends AbstractFSM[S, D] with LoggingFSM[S, D] diff --git a/akka-docs/rst/experimental/index.rst b/akka-docs/rst/experimental/index.rst index aacda42122..d94014b5af 100644 --- a/akka-docs/rst/experimental/index.rst +++ b/akka-docs/rst/experimental/index.rst @@ -21,6 +21,8 @@ prior deprecation. ../scala/persistence ../dev/multi-node-testing + ../java/lambda-actors + ../java/lambda-fsm 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 diff --git a/akka-docs/rst/java/lambda-actors.rst b/akka-docs/rst/java/lambda-actors.rst index a88c04e4f3..8f68872d40 100644 --- a/akka-docs/rst/java/lambda-actors.rst +++ b/akka-docs/rst/java/lambda-actors.rst @@ -17,6 +17,13 @@ its syntax from Erlang. .. _Actor Model: http://en.wikipedia.org/wiki/Actor_model +.. warning:: + + The Java with lambda support part of Akka is marked as **“experimental”** as of its introduction in + Akka 2.3.0. We will continue to improve this API based on our users’ feedback, which implies that + while we try to keep incompatible changes to a minimum, but the binary compatibility guarantee for + maintenance releases does not apply to the :class:`akka.actor.AbstractActor`, related classes and + the :class:`akka.japi.pf` package. Creating Actors =============== diff --git a/akka-docs/rst/java/lambda-fsm.rst b/akka-docs/rst/java/lambda-fsm.rst index 161744c767..474b25fc0b 100644 --- a/akka-docs/rst/java/lambda-fsm.rst +++ b/akka-docs/rst/java/lambda-fsm.rst @@ -21,6 +21,14 @@ These relations are interpreted as meaning: *If we are in state S and the event E occurs, we should perform the actions A and make a transition to the state S'.* +.. warning:: + + The Java with lambda support part of Akka is marked as **“experimental”** as of its introduction in + Akka 2.3.0. We will continue to improve this API based on our users’ feedback, which implies that + while we try to keep incompatible changes to a minimum, but the binary compatibility guarantee for + maintenance releases does not apply to the :class:`akka.actor.AbstractFSM`, related classes and the + :class:`akka.japi.pf` package. + A Simple Example ================ diff --git a/akka-samples/akka-docs-java-lambda/src/test/java/docs/actor/fsm/Buncher.java b/akka-samples/akka-docs-java-lambda/src/test/java/docs/actor/fsm/Buncher.java index af1e7bd6ea..335eb732f9 100644 --- a/akka-samples/akka-docs-java-lambda/src/test/java/docs/actor/fsm/Buncher.java +++ b/akka-samples/akka-docs-java-lambda/src/test/java/docs/actor/fsm/Buncher.java @@ -47,7 +47,7 @@ public class Buncher extends AbstractFSM { when(Active, Duration.create(1, "second"), matchEvent(Arrays.asList(Flush.class, StateTimeout()), Todo.class, - todo -> goTo(Idle).using(todo.copy(new LinkedList<>())))); + (event, todo) -> goTo(Idle).using(todo.copy(new LinkedList<>())))); //#unhandled-elided whenUnhandled( diff --git a/akka-samples/akka-docs-java-lambda/src/test/java/docs/actor/fsm/FSMDocTest.java b/akka-samples/akka-docs-java-lambda/src/test/java/docs/actor/fsm/FSMDocTest.java index 2626982efd..57c7a21e84 100644 --- a/akka-samples/akka-docs-java-lambda/src/test/java/docs/actor/fsm/FSMDocTest.java +++ b/akka-samples/akka-docs-java-lambda/src/test/java/docs/actor/fsm/FSMDocTest.java @@ -85,7 +85,7 @@ public class FSMDocTest { //#alt-transition-syntax //#stop-syntax - when(Error, matchEventEquals("stop", (data) -> { + when(Error, matchEventEquals("stop", (event, data) -> { // do cleanup ... return stop(); })); @@ -103,7 +103,7 @@ public class FSMDocTest { //#unhandled-syntax whenUnhandled( - matchEvent(X.class, null, (x, data) -> { + matchEvent(X.class, (x, data) -> { log().info("Received unhandled event: " + x); return stay(); }). @@ -146,7 +146,7 @@ public class FSMDocTest { target.tell("going active", self()); return goTo(Active); })); - when(Active, matchEventEquals("stop", (data) -> { + when(Active, matchEventEquals("stop", (event, data) -> { target.tell("stopping", self()); return stop(new Failure("This is not the error you're looking for")); })); diff --git a/akka-samples/akka-sample-fsm-java-lambda/COPYING b/akka-samples/akka-sample-fsm-java-lambda/COPYING new file mode 100644 index 0000000000..0e259d42c9 --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/akka-samples/akka-sample-fsm-java-lambda/LICENSE b/akka-samples/akka-sample-fsm-java-lambda/LICENSE new file mode 100644 index 0000000000..287f8dd7fa --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/LICENSE @@ -0,0 +1,10 @@ +Activator Template by Typesafe + +Licensed under Public Domain (CC0) + +To the extent possible under law, the person who associated CC0 with +this Activator Tempate has waived all copyright and related or neighboring +rights to this Activator Template. + +You should have received a copy of the CC0 legalcode along with this +work. If not, see . diff --git a/akka-samples/akka-sample-fsm-java-lambda/activator.properties b/akka-samples/akka-sample-fsm-java-lambda/activator.properties new file mode 100644 index 0000000000..e12c4ed0e3 --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/activator.properties @@ -0,0 +1,7 @@ +name=akka-sample-fsm-java-lambda +title=Akka FSM in Java with Lambdas +description=Illustrating how to implement a finite state machine in actors. +tags=akka,java,java8,sample +authorName=Akka Team +authorLink=http://akka.io/ +sourceLink=https://github.com/akka/akka diff --git a/akka-samples/akka-sample-fsm-java-lambda/build.sbt b/akka-samples/akka-sample-fsm-java-lambda/build.sbt new file mode 100644 index 0000000000..1aecca6cba --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/build.sbt @@ -0,0 +1,15 @@ +name := "akka-docs-java-lambda" + +version := "1.0" + +scalaVersion := "2.10.3" + +javacOptions ++= Seq("-source", "1.8", "-target", "1.8", "-Xlint") + +testOptions += Tests.Argument(TestFrameworks.JUnit, "-v", "-a") + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-actor" % "2.3-SNAPSHOT", + "com.typesafe.akka" %% "akka-testkit" % "2.3-SNAPSHOT" % "test", + "junit" % "junit" % "4.11" % "test", + "com.novocode" % "junit-interface" % "0.10" % "test") diff --git a/akka-samples/akka-sample-fsm-java-lambda/pom.xml b/akka-samples/akka-sample-fsm-java-lambda/pom.xml new file mode 100644 index 0000000000..2c6d8ac9a2 --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/pom.xml @@ -0,0 +1,53 @@ + + 4.0.0 + + + UTF-8 + + + sample + akka-sample-fsm-java-lambda + jar + 1.0 + + + + com.typesafe.akka + akka-actor_2.10 + 2.3-SNAPSHOT + + + com.typesafe.akka + akka-testkit_2.10 + 2.3-SNAPSHOT + + + junit + junit + 4.11 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + true + + -Xlint + + + + + + + diff --git a/akka-samples/akka-sample-fsm-java-lambda/project/build.properties b/akka-samples/akka-sample-fsm-java-lambda/project/build.properties new file mode 100644 index 0000000000..37b489cb6e --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.1 diff --git a/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/become/DiningHakkersOnBecome.java b/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/become/DiningHakkersOnBecome.java new file mode 100644 index 0000000000..873d4655d0 --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/become/DiningHakkersOnBecome.java @@ -0,0 +1,162 @@ +package sample.become; + +import akka.actor.*; +import akka.japi.pf.ReceiveBuilder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import scala.concurrent.duration.Duration; +import scala.concurrent.duration.FiniteDuration; +import scala.PartialFunction; +import scala.runtime.BoxedUnit; + +import static sample.become.Messages.*; +import static java.util.concurrent.TimeUnit.*; + +// Akka adaptation of +// http://www.dalnefre.com/wp/2010/08/dining-philosophers-in-humus/ + +public class DiningHakkersOnBecome { + + /* + * A Chopstick is an actor, it can be taken, and put back + */ + public static class Chopstick extends AbstractActor { + + //When a Chopstick is taken by a hakker + //It will refuse to be taken by other hakkers + //But the owning hakker can put it back + PartialFunction takenBy(ActorRef hakker) { + return ReceiveBuilder. + match(Take.class, + t -> t.hakker.tell(new Busy(self()), self())). + match(Put.class, p -> p.hakker == hakker, + p -> context().become(available)). + build(); + } + + //When a Chopstick is available, it can be taken by a hakker + PartialFunction available = ReceiveBuilder. + match(Take.class, t -> { + context().become(takenBy(t.hakker)); + t.hakker.tell(new Taken(self()), self()); + }).build(); + + //A Chopstick begins its existence as available + public PartialFunction receive() { + return available; + } + } + + /* + * A hakker is an awesome dude or dudette who either thinks about hacking or has to eat ;-) + */ + public static class Hakker extends AbstractActor { + private String name; + private ActorRef left; + private ActorRef right; + + public Hakker(String name, ActorRef left, ActorRef right) { + this.name = name; + this.left = left; + this.right = right; + } + + //When a hakker is eating, he can decide to start to think, + //then he puts down his chopsticks and starts to think + PartialFunction eating = ReceiveBuilder. + matchEquals(Think, m -> { + left.tell(new Put(self()), self()); + right.tell(new Put(self()), self()); + System.out.println(String.format("%s puts down his chopsticks and starts to think", name)); + startThinking(Duration.create(5, SECONDS)); + }).build(); + + //When a hakker is waiting for the last chopstick it can either obtain it + //and start eating, or the other chopstick was busy, and the hakker goes + //back to think about how he should obtain his chopsticks :-) + PartialFunction waitingFor(ActorRef chopstickToWaitFor, ActorRef otherChopstick) { + return ReceiveBuilder. + match(Taken.class, t -> t.chopstick == chopstickToWaitFor, t -> { + System.out.println(String.format("%s has picked up %s and %s and starts to eat", + name, left.path().name(), right.path().name())); + context().become(eating); + context().system().scheduler().scheduleOnce(Duration.create(5, SECONDS), self(), Think, context().system().dispatcher(), self()); + }). + match(Busy.class, b -> { + otherChopstick.tell(new Put(self()), self()); + startThinking(Duration.create(10, MILLISECONDS)); + }). + build(); + } + + //When the results of the other grab comes back, + //he needs to put it back if he got the other one. + //Then go back and think and try to grab the chopsticks again + PartialFunction deniedAChopstick = ReceiveBuilder. + match(Taken.class, t -> { + t.chopstick.tell(new Put(self()), self()); + startThinking(Duration.create(10, MILLISECONDS)); + }). + match(Busy.class, b -> + startThinking(Duration.create(10, MILLISECONDS))). + build(); + + //When a hakker is hungry it tries to pick up its chopsticks and eat + //When it picks one up, it goes into wait for the other + //If the hakkers first attempt at grabbing a chopstick fails, + //it starts to wait for the response of the other grab + PartialFunction hungry = ReceiveBuilder. + match(Taken.class, t -> t.chopstick == left, + t -> context().become(waitingFor(right, left))). + match(Taken.class, t -> t.chopstick == right, + t -> context().become(waitingFor(left, right))). + match(Busy.class, + b -> context().become(deniedAChopstick)). + build(); + + //When a hakker is thinking it can become hungry + //and try to pick up its chopsticks and eat + PartialFunction thinking = ReceiveBuilder. + matchEquals(Eat, m -> { + context().become(hungry); + left.tell(new Take(self()), self()); + right.tell(new Take(self()), self()); + }).build(); + + //All hakkers start in a non-eating state + public PartialFunction receive() { + return ReceiveBuilder.matchEquals(Think, m -> { + System.out.println(String.format("%s starts to think", name)); + startThinking(Duration.create(5, SECONDS)); + }).build(); + } + + private void startThinking(FiniteDuration duration) { + context().become(thinking); + context().system().scheduler().scheduleOnce(duration, self(), Eat, context().system().dispatcher(), self()); + } + } + + /* + * Alright, here's our test-harness + */ + public static void main(String[] args) { + ActorSystem system = ActorSystem.create(); + //Create 5 chopsticks + ActorRef[] chopsticks = new ActorRef[5]; + for (int i = 0; i < 5; i++) + chopsticks[i] = system.actorOf(Props.create(Chopstick.class), "Chopstick" + i); + + //Create 5 awesome hakkers and assign them their left and right chopstick + List names = Arrays.asList("Ghosh", "Boner", "Klang", "Krasser", "Manie"); + List hakkers = new ArrayList<>(); + int i = 0; + for (String name: names) { + hakkers.add(system.actorOf(Props.create(Hakker.class, name, chopsticks[i], chopsticks[(i + 1) % 5]))); + i++; + } + //Signal all hakkers that they should start thinking, and watch the show + hakkers.stream().forEach(hakker -> hakker.tell(Think, ActorRef.noSender())); + } +} diff --git a/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/become/Messages.java b/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/become/Messages.java new file mode 100644 index 0000000000..c61da59e39 --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/become/Messages.java @@ -0,0 +1,39 @@ +package sample.become; + +import akka.actor.ActorRef; + +public class Messages { + public static final class Busy { + public final ActorRef chopstick; + public Busy(ActorRef chopstick){ + this.chopstick = chopstick; + } + } + + public static final class Put { + public final ActorRef hakker; + public Put(ActorRef hakker){ + this.hakker = hakker; + } + } + + public static final class Take { + public final ActorRef hakker; + public Take(ActorRef hakker){ + this.hakker = hakker; + } + } + + public static final class Taken { + public final ActorRef chopstick; + public Taken(ActorRef chopstick){ + this.chopstick = chopstick; + } + } + + private static interface EatMessage {}; + public static final Object Eat = new EatMessage() {}; + + private static interface ThinkMessage {}; + public static final Object Think = new ThinkMessage() {}; +} diff --git a/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/fsm/DiningHakkersOnFsm.java b/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/fsm/DiningHakkersOnFsm.java new file mode 100644 index 0000000000..2a03fdb4e5 --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/fsm/DiningHakkersOnFsm.java @@ -0,0 +1,205 @@ +package sample.fsm; + +import akka.actor.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import scala.concurrent.duration.Duration; +import scala.concurrent.duration.FiniteDuration; + +import static java.util.concurrent.TimeUnit.*; +import static sample.fsm.Messages.*; + +// Akka adaptation of +// http://www.dalnefre.com/wp/2010/08/dining-philosophers-in-humus/ + +public class DiningHakkersOnFsm { + /** + * Some states the chopstick can be in + */ + public static enum CS { + Available, + Taken + } + + /** + * Some state container for the chopstick + */ + public static final class TakenBy { + public final ActorRef hakker; + public TakenBy(ActorRef hakker){ + this.hakker = hakker; + } + } + + /* + * A chopstick is an actor, it can be taken, and put back + */ + public static class Chopstick extends AbstractLoggingFSM { + { + // A chopstick begins its existence as available and taken by no one + startWith(CS.Available, new TakenBy(context().system().deadLetters())); + + // When a chopstick is available, it can be taken by a some hakker + when(CS.Available, + matchEventEquals(Take, (take, data) -> + goTo(CS.Taken).using(new TakenBy(sender())).replying(new Taken(self())))); + + // When a chopstick is taken by a hakker + // It will refuse to be taken by other hakkers + // But the owning hakker can put it back + when(CS.Taken, + matchEventEquals(Take, (take, data) -> + stay().replying(new Busy(self()))). + event((event, data) -> (event == Put) && (data.hakker == sender()), (event, data) -> + goTo(CS.Available).using(new TakenBy(context().system().deadLetters())))); + + // Initialze the chopstick + initialize(); + } + } + + /** + * Some fsm hakker states + */ + public static enum HS { + Waiting, + Thinking, + Hungry, + WaitForOtherChopstick, + FirstChopstickDenied, + Eating + } + + /** + * Some state container to keep track of which chopsticks we have + */ + public static final class TakenChopsticks { + public final ActorRef left; + public final ActorRef right; + + public TakenChopsticks(ActorRef left, ActorRef right) { + this.left = left; + this.right = right; + } + } + + /* + * A fsm hakker is an awesome dude or dudette who either thinks about hacking or has to eat ;-) + */ + public static class Hakker extends AbstractLoggingFSM { + private String name; + private ActorRef left; + private ActorRef right; + + public Hakker(String name, ActorRef left, ActorRef right) { + this.name = name; + this.left = left; + this.right = right; + } + + { + //All hakkers start waiting + startWith(HS.Waiting, new TakenChopsticks(null, null)); + + when(HS.Waiting, + matchEventEquals(Think, (think, data) -> { + System.out.println(String.format("%s starts to think", name)); + return startThinking(Duration.create(5, SECONDS)); + })); + + //When a hakker is thinking it can become hungry + //and try to pick up its chopsticks and eat + when(HS.Thinking, + matchEventEquals(StateTimeout(), (event, data) -> { + left.tell(Take, self()); + right.tell(Take, self()); + return goTo(HS.Hungry); + })); + + // When a hakker is hungry it tries to pick up its chopsticks and eat + // When it picks one up, it goes into wait for the other + // If the hakkers first attempt at grabbing a chopstick fails, + // it starts to wait for the response of the other grab + when(HS.Hungry, + matchEvent(Taken.class, (taken, data) -> taken.chopstick == left, + (taken, data) -> goTo(HS.WaitForOtherChopstick).using(new TakenChopsticks(left, null))). + event(Taken.class, (taken, data) -> taken.chopstick == right, + (taken, data) -> goTo(HS.WaitForOtherChopstick).using(new TakenChopsticks(null, right))). + event(Busy.class, + (busy, data) -> goTo(HS.FirstChopstickDenied))); + + // When a hakker is waiting for the last chopstick it can either obtain it + // and start eating, or the other chopstick was busy, and the hakker goes + // back to think about how he should obtain his chopsticks :-) + when(HS.WaitForOtherChopstick, + matchEvent(Taken.class, + (taken, data) -> (taken.chopstick == left && data.left == null && data.right != null), + (taken, data) -> startEating(left, right)). + event(Taken.class, + (taken, data) -> (taken.chopstick == right && data.left != null && data.right == null), + (taken, data) -> startEating(left, right)). + event(Busy.class, (busy, data) -> { + if (data.left != null) left.tell(Put, self()); + if (data.right != null) right.tell(Put, self()); + return startThinking(Duration.create(10, MILLISECONDS)); + })); + + // When the results of the other grab comes back, + // he needs to put it back if he got the other one. + // Then go back and think and try to grab the chopsticks again + when(HS.FirstChopstickDenied, + matchEvent(Taken.class, (taken, data) -> { + taken.chopstick.tell(Put, self()); + return startThinking(Duration.create(10, MILLISECONDS)); + }). + event(Busy.class, (busy, data) -> + startThinking(Duration.create(10, MILLISECONDS)))); + + // When a hakker is eating, he can decide to start to think, + // then he puts down his chopsticks and starts to think + when(HS.Eating, + matchEventEquals(StateTimeout(), (event, data) -> { + left.tell(Put, self()); + right.tell(Put, self()); + System.out.println(String.format("%s puts down his chopsticks and starts to think", name)); + return startThinking(Duration.create(5, SECONDS)); + })); + + // Initialize the hakker + initialize(); + } + + private FSM.State startEating(ActorRef left, ActorRef right) { + System.out.println(String.format("%s has picked up %s and %s and starts to eat", + name, left.path().name(), right.path().name())); + return goTo(HS.Eating).using(new TakenChopsticks(left, right)).forMax(Duration.create(5, SECONDS)); + } + + private FSM.State startThinking(FiniteDuration duration) { + return goTo(HS.Thinking).using(new TakenChopsticks(null, null)).forMax(duration); + } + } + + /* + * Alright, here's our test-harness + */ + public static void main(String[] args) { + ActorSystem system = ActorSystem.create(); + //Create 5 chopsticks + ActorRef[] chopsticks = new ActorRef[5]; + for (int i = 0; i < 5; i++) + chopsticks[i] = system.actorOf(Props.create(Chopstick.class), "Chopstick" + i); + + //Create 5 awesome hakkers and assign them their left and right chopstick + List names = Arrays.asList("Ghosh", "Boner", "Klang", "Krasser", "Manie"); + List hakkers = new ArrayList<>(); + int i = 0; + for (String name: names) { + hakkers.add(system.actorOf(Props.create(Hakker.class, name, chopsticks[i], chopsticks[(i + 1) % 5]), name)); + i++; + } + //Signal all hakkers that they should start thinking, and watch the show + hakkers.stream().forEach(hakker -> hakker.tell(Think, ActorRef.noSender())); + } +} diff --git a/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/fsm/Messages.java b/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/fsm/Messages.java new file mode 100644 index 0000000000..cb321dca96 --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/src/main/java/sample/fsm/Messages.java @@ -0,0 +1,35 @@ +package sample.fsm; + +import akka.actor.ActorRef; + +public class Messages { + + public static final class Busy { + public final ActorRef chopstick; + public Busy(ActorRef chopstick){ + this.chopstick = chopstick; + } + } + + private static interface PutMessage {}; + public static final Object Put = new PutMessage() { + @Override + public String toString() { return "Put"; } + }; + + private static interface TakeMessage {}; + public static final Object Take = new TakeMessage() { + @Override + public String toString() { return "Take"; } + }; + + public static final class Taken { + public final ActorRef chopstick; + public Taken(ActorRef chopstick){ + this.chopstick = chopstick; + } + } + + private static interface ThinkMessage {}; + public static final Object Think = new ThinkMessage() {}; +} diff --git a/akka-samples/akka-sample-fsm-java-lambda/tutorial/index.html b/akka-samples/akka-sample-fsm-java-lambda/tutorial/index.html new file mode 100644 index 0000000000..f2ff91c437 --- /dev/null +++ b/akka-samples/akka-sample-fsm-java-lambda/tutorial/index.html @@ -0,0 +1,73 @@ + + + Akka FSM in Java with Lambdas + + + + +

+

Finite State Machine in Actors

+ +

+This sample is an adaptation of +Dining Hakkers. +It illustrates how state and behavior can be managed within +an Actor with two different approaches; using become and using +the AbstractFSM class. +

+ + +
+
+ +

Dining Hakkers with Become

+ +

+Open DiningHakkersOnBecome.java. +

+ +

+It illustrates how current behavior can be replaced with context.become. +Note that no var members are used, instead the state is encoded in the current +behavior and its parameters. +

+ +

+Go to the Run tab, and start the application main class +sample.become.DiningHakkersOnBecome. +In the log output you can see the actions of the Hakker actors. +

+ +

+Read more about become in +the documentation. +

+ +
+
+ +

Dining Hakkers with FSM

+ +

+Open DiningHakkersOnFsm.java. +

+ +

+It illustrates how the states and transitions can be defined with the akka.actor.AbstractFSM class. +

+ +

+Go to the Run tab, and start the application main class +sample.fsm.DiningHakkersOnFsm. +In the log output you can see the actions of the Hakker actors. +

+ +

+Read more about akka.actor.FSM in +the documentation. +

+ +
+ + + diff --git a/scripts/build/extra-build-steps.sh b/scripts/build/extra-build-steps.sh index dc9e90051c..13ef12f006 100755 --- a/scripts/build/extra-build-steps.sh +++ b/scripts/build/extra-build-steps.sh @@ -92,3 +92,8 @@ tmp="$script_dir/../../akka-samples/akka-docs-java-lambda" try cd "$tmp" "can't step into project directory: $tmp" export JAVA_HOME="$java8_home" try mvn clean test "mvn execution in akka-docs-java-lambda failed" + +tmp="$script_dir/../../akka-samples/akka-sample-fsm-java-lambda" +try cd "$tmp" "can't step into project directory: $tmp" +export JAVA_HOME="$java8_home" +try mvn clean test "mvn execution in akka-sample-fsm-java-lambda failed"