diff --git a/akka-actor/src/main/scala/akka/actor/ActorCell.scala b/akka-actor/src/main/scala/akka/actor/ActorCell.scala index 480f84b993..7dbcf41d45 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorCell.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorCell.scala @@ -152,6 +152,14 @@ trait ActorContext extends ActorRefFactory { */ def watch(subject: ActorRef): ActorRef + /** + * Registers this actor as a Monitor for the provided ActorRef. + * This actor will receive the specified message when watched + * actor is terminated. + * @return the provided ActorRef + */ + def watchWith(subject: ActorRef, msg: Any): ActorRef + /** * Unregisters this actor as Monitor for the provided ActorRef. * @return the provided ActorRef diff --git a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala index affb50038b..d4513a4433 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala @@ -706,7 +706,8 @@ private[akka] class ActorSystemImpl( def actorOf(props: Props, name: String): ActorRef = if (guardianProps.isEmpty) guardian.underlying.attachChild(props, name, systemService = false) - else throw new UnsupportedOperationException("cannot create top-level actor from the outside on ActorSystem with custom user guardian") + else throw new UnsupportedOperationException( + s"cannot create top-level actor [$name] from the outside on ActorSystem with custom user guardian") def actorOf(props: Props): ActorRef = if (guardianProps.isEmpty) guardian.underlying.attachChild(props, systemService = false) diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala b/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala index 756d45520e..dfd9271f67 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala @@ -11,7 +11,11 @@ import akka.event.AddressTerminatedTopic private[akka] trait DeathWatch { this: ActorCell ⇒ - private var watching: Set[ActorRef] = ActorCell.emptyActorRefSet + /** + * This map holds a [[None]] for actors for which we send a [[Terminated]] notification on termination, + * ``Some(message)`` for actors for which we send a custom termination message. + */ + private var watching: Map[ActorRef, Option[Any]] = Map.empty private var watchedBy: Set[ActorRef] = ActorCell.emptyActorRefSet private var terminatedQueued: Set[ActorRef] = ActorCell.emptyActorRefSet @@ -22,7 +26,18 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ if (a != self && !watchingContains(a)) { maintainAddressTerminatedSubscription(a) { a.sendSystemMessage(Watch(a, self)) // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ - watching += a + watching = watching.updated(a, None) + } + } + a + } + + override final def watchWith(subject: ActorRef, msg: Any): ActorRef = subject match { + case a: InternalActorRef ⇒ + if (a != self && !watchingContains(a)) { + maintainAddressTerminatedSubscription(a) { + a.sendSystemMessage(Watch(a, self)) // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ + watching = watching.updated(a, Some(msg)) } } a @@ -33,7 +48,7 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ if (a != self && watchingContains(a)) { a.sendSystemMessage(Unwatch(a, self)) // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ maintainAddressTerminatedSubscription(a) { - watching = removeFromSet(a, watching) + watching = removeFromMap(a, watching) } } terminatedQueued = removeFromSet(a, terminatedQueued) @@ -51,14 +66,16 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ * it will be propagated to user's receive. */ protected def watchedActorTerminated(actor: ActorRef, existenceConfirmed: Boolean, addressTerminated: Boolean): Unit = { - if (watchingContains(actor)) { - maintainAddressTerminatedSubscription(actor) { - watching = removeFromSet(actor, watching) - } - if (!isTerminating) { - self.tell(Terminated(actor)(existenceConfirmed, addressTerminated), actor) - terminatedQueuedFor(actor) - } + watchingGet(actor) match { + case None ⇒ // We're apparently no longer watching this actor. + case Some(optionalMessage) ⇒ + maintainAddressTerminatedSubscription(actor) { + watching = removeFromMap(actor, watching) + } + if (!isTerminating) { + self.tell(optionalMessage.getOrElse(Terminated(actor)(existenceConfirmed, addressTerminated)), actor) + terminatedQueuedFor(actor) + } } if (childrenRefs.getByRef(actor).isDefined) handleChildTerminated(actor) } @@ -72,12 +89,28 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ watching.contains(subject) || (subject.path.uid != ActorCell.undefinedUid && watching.contains(new UndefinedUidActorRef(subject))) + // TODO this should be removed and be replaced with `watching.get(subject)` + // when all actor references have uid, i.e. actorFor is removed + // Returns None when the subject is not being watched. + // If the subject is being matched, the inner option is the optional custom termination + // message that should be sent instead of the default Terminated. + private def watchingGet(subject: ActorRef): Option[Option[Any]] = + watching.get(subject).orElse( + if (subject.path.uid == ActorCell.undefinedUid) None + else watching.get(new UndefinedUidActorRef(subject))) + // TODO this should be removed and be replaced with `set - subject` // when all actor references have uid, i.e. actorFor is removed private def removeFromSet(subject: ActorRef, set: Set[ActorRef]): Set[ActorRef] = if (subject.path.uid != ActorCell.undefinedUid) (set - subject) - new UndefinedUidActorRef(subject) else set filterNot (_.path == subject.path) + // TODO this should be removed and be replaced with `set - subject` + // when all actor references have uid, i.e. actorFor is removed + private def removeFromMap[T](subject: ActorRef, map: Map[ActorRef, T]): Map[ActorRef, T] = + if (subject.path.uid != ActorCell.undefinedUid) (map - subject) - new UndefinedUidActorRef(subject) + else map filterKeys (_.path != subject.path) + protected def tellWatchersWeDied(): Unit = if (!watchedBy.isEmpty) { try { @@ -113,10 +146,13 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ maintainAddressTerminatedSubscription() { try { watching foreach { // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ - case watchee: InternalActorRef ⇒ watchee.sendSystemMessage(Unwatch(watchee, self)) + case (watchee: InternalActorRef, _) ⇒ watchee.sendSystemMessage(Unwatch(watchee, self)) + case (watchee, _) ⇒ + // should never happen, suppress "match may not be exhaustive" compiler warning + throw new IllegalStateException(s"Expected InternalActorRef, but got [${watchee.getClass.getName}]") } } finally { - watching = ActorCell.emptyActorRefSet + watching = Map.empty terminatedQueued = ActorCell.emptyActorRefSet } } @@ -166,7 +202,7 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ // When a parent is watching a child and it terminates due to AddressTerminated // it is removed by sending DeathWatchNotification with existenceConfirmed = true to support // immediate creation of child with same name. - for (a ← watching; if a.path.address == address) { + for ((a, _) ← watching; if a.path.address == address) { self.sendSystemMessage(DeathWatchNotification(a, existenceConfirmed = childrenRefs.getByRef(a).isDefined, addressTerminated = true)) } } @@ -185,7 +221,7 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ } if (isNonLocal(change)) { - def hasNonLocalAddress: Boolean = ((watching exists isNonLocal) || (watchedBy exists isNonLocal)) + def hasNonLocalAddress: Boolean = ((watching.keysIterator exists isNonLocal) || (watchedBy exists isNonLocal)) val had = hasNonLocalAddress val result = block val has = hasNonLocalAddress diff --git a/akka-docs/src/main/paradox/java/index-actors.md b/akka-docs/src/main/paradox/java/index-actors.md index 2cfc6bb1ae..6b7db6aa6b 100644 --- a/akka-docs/src/main/paradox/java/index-actors.md +++ b/akka-docs/src/main/paradox/java/index-actors.md @@ -5,6 +5,7 @@ @@@ index * [actors](actors.md) +* [typed](typed.md) * [fault-tolerance](fault-tolerance.md) * [dispatchers](dispatchers.md) * [mailboxes](mailboxes.md) @@ -17,4 +18,4 @@ * [testing](testing.md) * [typed-actors](typed-actors.md) -@@@ \ No newline at end of file +@@@ diff --git a/akka-docs/src/main/paradox/java/typed.md b/akka-docs/src/main/paradox/java/typed.md new file mode 100644 index 0000000000..fc51f28ca5 --- /dev/null +++ b/akka-docs/src/main/paradox/java/typed.md @@ -0,0 +1,273 @@ +# Akka Typed + +@@@ warning + +This module is currently marked as @ref:[may change](common/may-change.md) in the sense + of being the subject of active research. This means that API or semantics can + change without warning or deprecation period and it is not recommended to use + this module in production just yet—you have been warned. + +@@@ + +As discussed in `actor-systems` (and following chapters) Actors are about +sending messages between independent units of computation, but how does that +look like? In all of the following these imports are assumed: + +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #imports } + +With these in place we can define our first Actor, and of course it will say +hello! + +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #hello-world-actor } + +This small piece of code defines two message types, one for commanding the +Actor to greet someone and one that the Actor will use to confirm that it has +done so. The `Greet` type contains not only the information of whom to +greet, it also holds an `ActorRef` that the sender of the message +supplies so that the `HelloWorld` Actor can send back the confirmation +message. + +The behavior of the Actor is defined as the `greeter` value with the help +of the `immutable` behavior constructor. This constructor is called +immutable because the behavior instance doesn't have or close over any mutable +state. Processing the next message may result in a new behavior that can +potentially be different from this one. State is updated by returning a new +behavior that holds the new immutable state. In this case we don't need to +update any state, so we return `Same`. + +The type of the messages handled by this behavior is declared to be of class +`Greet`, which implies that the supplied function’s `msg` argument is +also typed as such. This is why we can access the `whom` and `replyTo` +members without needing to use a pattern match. + +On the last line we see the `HelloWorld` Actor send a message to another +Actor, which is done using the `!` operator (pronounced “tell”). Since the +`replyTo` address is declared to be of type `ActorRef` the +compiler will only permit us to send messages of this type, other usage will +not be accepted. + +The accepted message types of an Actor together with all reply types defines +the protocol spoken by this Actor; in this case it is a simple request–reply +protocol but Actors can model arbitrarily complex protocols when needed. The +protocol is bundled together with the behavior that implements it in a nicely +wrapped scope—the `HelloWorld` class. + +Now we want to try out this Actor, so we must start an ActorSystem to host it: + +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #hello-world } + +We start an Actor system from the defined `greeter` behavior. + +As Carl Hewitt said, one Actor is no Actor—it would be quite lonely with +nobody to talk to. In this sense the example is a little cruel because we only +give the `HelloWorld` Actor a fake person to talk to—the “ask” pattern +can be used to send a message such that the reply fulfills a `CompletionStage`. + +Note that the `CompletionStage` that is returned by the “ask” operation is +properly typed already, no type checks or casts needed. This is possible due to +the type information that is part of the message protocol: the `ask` operator +takes as argument a function that pass an `ActorRef`, which is the +`replyTo` parameter of the `Greet` message, which means that when sending +the reply message to that `ActorRef` the message that fulfills the +`CompletionStage` can only be of type `Greeted`. + +We use this here to send the `Greet` command to the Actor and when the +reply comes back we will print it out and tell the actor system to shut down and +the program ends. + +This shows that there are aspects of Actor messaging that can be type-checked +by the compiler, but this ability is not unlimited, there are bounds to what we +can statically express. Before we go on with a more complex (and realistic) +example we make a small detour to highlight some of the theory behind this. + +## A Little Bit of Theory + +The [Actor Model](http://en.wikipedia.org/wiki/Actor_model) as defined by +Hewitt, Bishop and Steiger in 1973 is a computational model that expresses +exactly what it means for computation to be distributed. The processing +units—Actors—can only communicate by exchanging messages and upon reception of a +message an Actor can do the following three fundamental actions: + + 1. send a finite number of messages to Actors it knows + 2. create a finite number of new Actors + 3. designate the behavior to be applied to the next message + +The Akka Typed project expresses these actions using behaviors and addresses. +Messages can be sent to an address and behind this façade there is a behavior +that receives the message and acts upon it. The binding between address and +behavior can change over time as per the third point above, but that is not +visible on the outside. + +With this preamble we can get to the unique property of this project, namely +that it introduces static type checking to Actor interactions: addresses are +parameterized and only messages that are of the specified type can be sent to +them. The association between an address and its type parameter must be made +when the address (and its Actor) is created. For this purpose each behavior is +also parameterized with the type of messages it is able to process. Since the +behavior can change behind the address façade, designating the next behavior is +a constrained operation: the successor must handle the same type of messages as +its predecessor. This is necessary in order to not invalidate the addresses +that refer to this Actor. + +What this enables is that whenever a message is sent to an Actor we can +statically ensure that the type of the message is one that the Actor declares +to handle—we can avoid the mistake of sending completely pointless messages. +What we cannot statically ensure, though, is that the behavior behind the +address will be in a given state when our message is received. The fundamental +reason is that the association between address and behavior is a dynamic +runtime property, the compiler cannot know it while it translates the source +code. + +This is the same as for normal Java objects with internal variables: when +compiling the program we cannot know what their value will be, and if the +result of a method call depends on those variables then the outcome is +uncertain to a degree—we can only be certain that the returned value is of a +given type. + +We have seen above that the return type of an Actor command is described by the +type of reply-to address that is contained within the message. This allows a +conversation to be described in terms of its types: the reply will be of type +A, but it might also contain an address of type B, which then allows the other +Actor to continue the conversation by sending a message of type B to this new +address. While we cannot statically express the “current” state of an Actor, we +can express the current state of a protocol between two Actors, since that is +just given by the last message type that was received or sent. + +In the next section we demonstrate this on a more realistic example. + +## A More Complex Example + +Consider an Actor that runs a chat room: client Actors may connect by sending +a message that contains their screen name and then they can post messages. The +chat room Actor will disseminate all posted messages to all currently connected +client Actors. The protocol definition could look like the following: + +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #chatroom-protocol } + +Initially the client Actors only get access to an `ActorRef` +which allows them to make the first step. Once a client’s session has been +established it gets a `SessionGranted` message that contains a `handle` to +unlock the next protocol step, posting messages. The `PostMessage` +command will need to be sent to this particular address that represents the +session that has been added to the chat room. The other aspect of a session is +that the client has revealed its own address, via the `replyTo` argument, so that subsequent +`MessagePosted` events can be sent to it. + +This illustrates how Actors can express more than just the equivalent of method +calls on Java objects. The declared message types and their contents describe a +full protocol that can involve multiple Actors and that can evolve over +multiple steps. The implementation of the chat room protocol would be as simple +as the following: + +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #chatroom-behavior } + +The core of this behavior is stateful, the chat room itself does not change +into something else when sessions are established, but we introduce a variable +that tracks the opened sessions. Note that by using a method parameter a `var` +is not needed. When a new `GetSession` command comes in we add that client to the +list that is in the returned behavior. Then we also need to create the session’s +`ActorRef` that will be used to post messages. In this case we want to +create a very simple Actor that just repackages the `PostMessage` +command into a `PostSessionMessage` command which also includes the +screen name. Such a wrapper Actor can be created by using the +`spawnAdapter` method on the `ActorContext`, so that we can then +go on to reply to the client with the `SessionGranted` result. + +The behavior that we declare here can handle both subtypes of `Command`. +`GetSession` has been explained already and the +`PostSessionMessage` commands coming from the wrapper Actors will +trigger the dissemination of the contained chat room message to all connected +clients. But we do not want to give the ability to send +`PostSessionMessage` commands to arbitrary clients, we reserve that +right to the wrappers we create—otherwise clients could pose as completely +different screen names (imagine the `GetSession` protocol to include +authentication information to further secure this). Therefore `PostSessionMessage` +has `private` visibility and can't be created outside the actor. + +If we did not care about securing the correspondence between a session and a +screen name then we could change the protocol such that `PostMessage` is +removed and all clients just get an `ActorRef` to +send to. In this case no wrapper would be needed and we could just use +`ctx.getSelf()`. The type-checks work out in that case because +`ActorRef` is contravariant in its type parameter, meaning that we +can use a `ActorRef` wherever an +`ActorRef` is needed—this makes sense because the +former simply speaks more languages than the latter. The opposite would be +problematic, so passing an `ActorRef` where +`ActorRef` is required will lead to a type error. + +### Trying it out + +In order to see this chat room in action we need to write a client Actor that can use it: + +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #chatroom-gabbler } + +From this behavior we can create an Actor that will accept a chat room session, +post a message, wait to see it published, and then terminate. The last step +requires the ability to change behavior, we need to transition from the normal +running behavior into the terminated state. This is why here we do not return +`same`, as above, but another special value `stopped`. + +Now to try things out we must start both a chat room and a gabbler and of +course we do this inside an Actor system. Since there can be only one guardian +supervisor we could either start the chat room from the gabbler (which we don’t +want—it complicates its logic) or the gabbler from the chat room (which is +nonsensical) or we start both of them from a third Actor—our only sensible +choice: + +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #chatroom-main } + +In good tradition we call the `main` Actor what it is, it directly +corresponds to the `main` method in a traditional Java application. This +Actor will perform its job on its own accord, we do not need to send messages +from the outside, so we declare it to be of type `Void`. Actors receive not +only external messages, they also are notified of certain system events, +so-called Signals. In order to get access to those we choose to implement this +particular one using the `immutable` behavior decorator. The +provided `onSignal` function will be invoked for signals (subclasses of `Signal`) +or the `onMessage` function for user messages. + +This particular `main` Actor is created using `Actor.deferred`, which is like a factory for a behavior. +Creation of the behavior instance is deferred until the actor is started, as opposed to `Actor.immutable` +that creates the behavior instance immediately before the actor is running. The factory function in +`deferred` pass the `ActorContext` as parameter and that can for example be used for spawning child actors. +This `main` Actor creates the chat room and the gabbler and the session between them is initiated, and when the +gabbler is finished we will receive the `Terminated` event due to having +called `ctx.watch` for it. This allows us to shut down the Actor system: when +the main Actor terminates there is nothing more to do. + +## Status of this Project and Relation to Akka Actors + +Akka Typed is the result of many years of research and previous attempts +(including Typed Channels in the 2.2.x series) and it is on its way to +stabilization, but maturing such a profound change to the core concept of Akka +will take a long time. We expect that this module will stay marked +@ref:[may change](common/may-change.md) for multiple major releases of Akka and the +plain `akka.actor.Actor` will not be deprecated or go away anytime soon. + +Being a research project also entails that the reference documentation is not +as detailed as it will be for a final version, please refer to the API +documentation for greater depth and finer detail. + +### Main Differences + +The most prominent difference is the removal of the `sender()` functionality. +This turned out to be the Achilles heel of the Typed Channels project, it is +the feature that makes its type signatures and macros too complex to be viable. +The solution chosen in Akka Typed is to explicitly include the properly typed +reply-to address in the message, which both burdens the user with this task but +also places this aspect of protocol design where it belongs. + +The other prominent difference is the removal of the `Actor` trait. In +order to avoid closing over unstable references from different execution +contexts (e.g. Future transformations) we turned all remaining methods that +were on this trait into messages: the behavior receives the +`ActorContext` as an argument during processing and the lifecycle hooks +have been converted into Signals. + +A side-effect of this is that behaviors can now be tested in isolation without +having to be packaged into an Actor, tests can run fully synchronously without +having to worry about timeouts and spurious failures. Another side-effect is +that behaviors can nicely be composed and decorated, see `tap`, or +`widened` combinators; nothing about these is special or internal, new +combinators can be written as external libraries or tailor-made for each project. diff --git a/akka-docs/src/main/paradox/scala/typed.md b/akka-docs/src/main/paradox/scala/typed.md index 6df60c0694..e01b045b1b 100644 --- a/akka-docs/src/main/paradox/scala/typed.md +++ b/akka-docs/src/main/paradox/scala/typed.md @@ -3,9 +3,9 @@ @@@ warning This module is currently marked as @ref:[may change](common/may-change.md) in the sense -of being the subject of active research. This means that API or semantics can -change without warning or deprecation period and it is not recommended to use -this module in production just yet—you have been warned. + of being the subject of active research. This means that API or semantics can + change without warning or deprecation period and it is not recommended to use + this module in production just yet—you have been warned. @@@ @@ -13,12 +13,12 @@ As discussed in @ref:[Actor Systems](general/actor-systems.md) (and following ch sending messages between independent units of computation, but how does that look like? In all of the following these imports are assumed: -@@snip [IntroSpec.scala]($code$/scala/docs/akka/typed/IntroSpec.scala) { #imports } +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala) { #imports } With these in place we can define our first Actor, and of course it will say hello! -@@snip [IntroSpec.scala]($code$/scala/docs/akka/typed/IntroSpec.scala) { #hello-world-actor } +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala) { #hello-world-actor } This small piece of code defines two message types, one for commanding the Actor to greet someone and one that the Actor will use to confirm that it has @@ -28,10 +28,12 @@ supplies so that the `HelloWorld` Actor can send back the confirmation message. The behavior of the Actor is defined as the `greeter` value with the help -of the `Stateless` behavior constructor—there are many different ways of -formulating behaviors as we shall see in the following. The “stateless” behavior -is not capable of changing in response to a message, it will stay the same -until the Actor is stopped by its parent. +of the `immutable` behavior constructor. This constructor is called +immutable because the behavior instance doesn't have or close over any mutable +state. Processing the next message may result in a new behavior that can +potentially be different from this one. State is updated by returning a new +behavior that holds the new immutable state. In this case we don't need to +update any state, so we return `Same`. The type of the messages handled by this behavior is declared to be of class `Greet`, which implies that the supplied function’s `msg` argument is @@ -39,8 +41,8 @@ also typed as such. This is why we can access the `whom` and `replyTo` members without needing to use a pattern match. On the last line we see the `HelloWorld` Actor send a message to another -Actor, which is done using the `!` operator (pronounced “tell”). Since the -`replyTo` address is declared to be of type `ActorRef[Greeted]` the +Actor, which is done using the `tell` method (represented by the `!` operator). +Since the `replyTo` address is declared to be of type `ActorRef[Greeted]` the compiler will only permit us to send messages of this type, other usage will not be accepted. @@ -52,7 +54,7 @@ wrapped scope—the `HelloWorld` object. Now we want to try out this Actor, so we must start an ActorSystem to host it: -@@snip [IntroSpec.scala]($code$/scala/docs/akka/typed/IntroSpec.scala) { #hello-world } +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala) { #hello-world } After importing the Actor’s protocol definition we start an Actor system from the defined behavior. @@ -94,9 +96,9 @@ exactly what it means for computation to be distributed. The processing units—Actors—can only communicate by exchanging messages and upon reception of a message an Actor can do the following three fundamental actions: -1. send a finite number of messages to Actors it knows -2. create a finite number of new Actors -3. designate the behavior to be applied to the next message + 1. send a finite number of messages to Actors it knows + 2. create a finite number of new Actors + 3. designate the behavior to be applied to the next message The Akka Typed project expresses these actions using behaviors and addresses. Messages can be sent to an address and behind this façade there is a behavior @@ -148,7 +150,7 @@ a message that contains their screen name and then they can post messages. The chat room Actor will disseminate all posted messages to all currently connected client Actors. The protocol definition could look like the following: -@@snip [IntroSpec.scala]($code$/scala/docs/akka/typed/IntroSpec.scala) { #chatroom-protocol } +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala) { #chatroom-protocol } Initially the client Actors only get access to an `ActorRef[GetSession]` which allows them to make the first step. Once a client’s session has been @@ -165,7 +167,7 @@ full protocol that can involve multiple Actors and that can evolve over multiple steps. The implementation of the chat room protocol would be as simple as the following: -@@snip [IntroSpec.scala]($code$/scala/docs/akka/typed/IntroSpec.scala) { #chatroom-behavior } +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala) { #chatroom-behavior } The core of this behavior is stateful, the chat room itself does not change into something else when sessions are established, but we introduce a variable @@ -206,23 +208,17 @@ problematic, so passing an `ActorRef[PostSessionMessage]` where In order to see this chat room in action we need to write a client Actor that can use it: -@@snip [IntroSpec.scala]($code$/scala/docs/akka/typed/IntroSpec.scala) { #chatroom-gabbler } +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala) { #chatroom-gabbler } From this behavior we can create an Actor that will accept a chat room session, post a message, wait to see it published, and then terminate. The last step requires the ability to change behavior, we need to transition from the normal -running behavior into the terminated state. This is why this Actor uses a -different behavior constructor named `Total`. This constructor takes as -argument a function from the handled message type, in this case -`SessionEvent`, to the next behavior. That next behavior must again be -of the same type as we discussed in the theory section above. Here we either -stay in the very same behavior or we terminate, and both of these cases are so -common that there are special values `Same` and `Stopped` that can be used. -The behavior is named “total” (as opposed to “partial”) because the declared -function must handle all values of its input type. Since `SessionEvent` -is a sealed trait the Scala compiler will warn us if we forget to handle one of -the subtypes; in this case it reminded us that alternatively to -`SessionGranted` we may also receive a `SessionDenied` event. +running behavior into the terminated state. This is why here we do not return +`same`, as above, but another special value `stopped`. +Since `SessionEvent` is a sealed trait the Scala compiler will warn us +if we forget to handle one of the subtypes; in this case it reminded us that +alternatively to `SessionGranted` we may also receive a +`SessionDenied` event. Now to try things out we must start both a chat room and a gabbler and of course we do this inside an Actor system. Since there can be only one guardian @@ -231,7 +227,7 @@ want—it complicates its logic) or the gabbler from the chat room (which is nonsensical) or we start both of them from a third Actor—our only sensible choice: -@@snip [IntroSpec.scala]($code$/scala/docs/akka/typed/IntroSpec.scala) { #chatroom-main } +@@snip [IntroSpec.scala]($akka$/akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala) { #chatroom-main } In good tradition we call the `main` Actor what it is, it directly corresponds to the `main` method in a traditional Java application. This @@ -239,19 +235,21 @@ Actor will perform its job on its own accord, we do not need to send messages from the outside, so we declare it to be of type `NotUsed`. Actors receive not only external messages, they also are notified of certain system events, so-called Signals. In order to get access to those we choose to implement this -particular one using the `Stateful` behavior decorator. The -provided `signal` function will be invoked for signals (subclasses of `Signal`) -or the `mesg` function for user messages. +particular one using the `immutable` behavior decorator. The +provided `onSignal` function will be invoked for signals (subclasses of `Signal`) +or the `onMessage` function for user messages. -This particular main Actor reacts to two signals: when it is started it will -first receive the `PreStart` signal, upon which the chat room and the -gabbler are created and the session between them is initiated, and when the +This particular `main` Actor is created using `Actor.deferred`, which is like a factory for a behavior. +Creation of the behavior instance is deferred until the actor is started, as opposed to `Actor.immutable` +that creates the behavior instance immediately before the actor is running. The factory function in +`deferred` pass the `ActorContext` as parameter and that can for example be used for spawning child actors. +This ``main`` Actor creates the chat room and the gabbler and the session between them is initiated, and when the gabbler is finished we will receive the `Terminated` event due to having called `ctx.watch` for it. This allows us to shut down the Actor system: when the main Actor terminates there is nothing more to do. Therefore after creating the Actor system with the `main` Actor’s -`Props` we just await its termination. +`Behavior` we just await its termination. ## Status of this Project and Relation to Akka Actors @@ -285,7 +283,6 @@ have been converted into Signals. A side-effect of this is that behaviors can now be tested in isolation without having to be packaged into an Actor, tests can run fully synchronously without having to worry about timeouts and spurious failures. Another side-effect is -that behaviors can nicely be composed and decorated, see the `And`, -`Or`, `Widened`, `ContextAware` combinators; nothing about -these is special or internal, new combinators can be written as external -libraries or tailor-made for each project. +that behaviors can nicely be composed and decorated, see `tap`, or +`widen` combinators; nothing about these is special or internal, new +combinators can be written as external libraries or tailor-made for each project. diff --git a/akka-typed-testkit/build.sbt b/akka-typed-testkit/build.sbt new file mode 100644 index 0000000000..ea235f557d --- /dev/null +++ b/akka-typed-testkit/build.sbt @@ -0,0 +1,6 @@ +import akka.{ AkkaBuild, Formatting, OSGi } + +AkkaBuild.defaultSettings +Formatting.formatSettings + +disablePlugins(MimaPlugin) diff --git a/akka-typed-testkit/src/main/resources/reference.conf b/akka-typed-testkit/src/main/resources/reference.conf new file mode 100644 index 0000000000..1230df0591 --- /dev/null +++ b/akka-typed-testkit/src/main/resources/reference.conf @@ -0,0 +1,20 @@ +############################################ +# Akka Typed Testkit Reference Config File # +############################################ + +# This is the reference config file that contains all the default settings. +# Make your edits/overrides in your application.conf. + +akka.typed.test { + # factor by which to scale timeouts during tests, e.g. to account for shared + # build system load + timefactor = 1.0 + + # duration to wait in expectMsg and friends outside of within() block + # by default + single-expect-default = 3s + + # The timeout that is added as an implicit by DefaultTimeout trait + default-timeout = 5s + +} diff --git a/akka-typed/src/main/scala/akka/typed/Effects.scala b/akka-typed-testkit/src/main/scala/akka/typed/testkit/Effects.scala similarity index 69% rename from akka-typed/src/main/scala/akka/typed/Effects.scala rename to akka-typed-testkit/src/main/scala/akka/typed/testkit/Effects.scala index 1b13fb9e61..2a0be055f2 100644 --- a/akka-typed/src/main/scala/akka/typed/Effects.scala +++ b/akka-typed-testkit/src/main/scala/akka/typed/testkit/Effects.scala @@ -1,13 +1,17 @@ /** * Copyright (C) 2014-2017 Lightbend Inc. */ -package akka.typed +package akka.typed.testkit -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.duration.Duration import java.util.concurrent.ConcurrentLinkedQueue + +import akka.typed.{ ActorContext, ActorRef, ActorSystem, Behavior, EmptyProps, PostStop, Props, Signal } + import scala.annotation.tailrec import scala.collection.immutable +import scala.util.control.Exception.Catcher +import scala.util.control.NonFatal +import scala.concurrent.duration.{ Duration, FiniteDuration } /** * All tracked effects must extend implement this type. It is deliberately @@ -32,8 +36,8 @@ object Effect { */ class EffectfulActorContext[T](_name: String, _initialBehavior: Behavior[T], _mailboxCapacity: Int, _system: ActorSystem[Nothing]) extends StubbedActorContext[T](_name, _mailboxCapacity, _system) { - import akka.{ actor ⇒ a } import Effect._ + import akka.{ actor ⇒ a } private val effectQueue = new ConcurrentLinkedQueue[Effect] def getEffect(): Effect = effectQueue.poll() match { @@ -49,17 +53,31 @@ class EffectfulActorContext[T](_name: String, _initialBehavior: Behavior[T], _ma } def hasEffects: Boolean = effectQueue.peek() != null - private var current = _initialBehavior - - if (Behavior.isAlive(current)) signal(PreStart) + private var current = Behavior.validateAsInitial(Behavior.undefer(_initialBehavior, this)) def currentBehavior: Behavior[T] = current def isAlive: Boolean = Behavior.isAlive(current) - def run(msg: T): Unit = current = Behavior.canonicalize(current.message(this, msg), current) - def signal(signal: Signal): Unit = current = Behavior.canonicalize(current.management(this, signal), current) + private def handleException: Catcher[Unit] = { + case NonFatal(e) ⇒ + try Behavior.canonicalize(Behavior.interpretSignal(current, this, PostStop), current, this) // TODO why canonicalize here? + catch { case NonFatal(ex) ⇒ /* ignore, real is logging */ } + throw e + } - override def spawnAnonymous[U](behavior: Behavior[U], deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[U] = { + def run(msg: T): Unit = { + try { + current = Behavior.canonicalize(Behavior.interpretMessage(current, this, msg), current, this) + } catch handleException + } + + def signal(signal: Signal): Unit = { + try { + current = Behavior.canonicalize(Behavior.interpretSignal(current, this, signal), current, this) + } catch handleException + } + + override def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty): ActorRef[U] = { val ref = super.spawnAnonymous(behavior) effectQueue.offer(Spawned(ref.path.name)) ref @@ -69,19 +87,19 @@ class EffectfulActorContext[T](_name: String, _initialBehavior: Behavior[T], _ma effectQueue.offer(Spawned(ref.path.name)) ref } - override def spawn[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[U] = { + override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] = { effectQueue.offer(Spawned(name)) super.spawn(behavior, name) } - override def stop(child: ActorRef[_]): Boolean = { + override def stop[U](child: ActorRef[U]): Boolean = { effectQueue.offer(Stopped(child.path.name)) super.stop(child) } - override def watch[U](other: ActorRef[U]): ActorRef[U] = { + override def watch[U](other: ActorRef[U]): Unit = { effectQueue.offer(Watched(other)) super.watch(other) } - override def unwatch[U](other: ActorRef[U]): ActorRef[U] = { + override def unwatch[U](other: ActorRef[U]): Unit = { effectQueue.offer(Unwatched(other)) super.unwatch(other) } diff --git a/akka-typed/src/main/scala/akka/typed/Inbox.scala b/akka-typed-testkit/src/main/scala/akka/typed/testkit/Inbox.scala similarity index 84% rename from akka-typed/src/main/scala/akka/typed/Inbox.scala rename to akka-typed-testkit/src/main/scala/akka/typed/testkit/Inbox.scala index 233a12713e..42cd305b50 100644 --- a/akka-typed/src/main/scala/akka/typed/Inbox.scala +++ b/akka-typed-testkit/src/main/scala/akka/typed/testkit/Inbox.scala @@ -1,16 +1,15 @@ /** * Copyright (C) 2014-2017 Lightbend Inc. */ -package akka.typed +package akka.typed.testkit + +import java.util.concurrent.{ ConcurrentLinkedQueue, ThreadLocalRandom } + +import akka.actor.{ Address, RootActorPath } +import akka.typed.{ ActorRef, internal } -import java.util.concurrent.ConcurrentLinkedQueue -import akka.actor.ActorPath -import akka.actor.RootActorPath -import akka.actor.Address -import scala.collection.immutable import scala.annotation.tailrec -import akka.actor.ActorRefProvider -import java.util.concurrent.ThreadLocalRandom +import scala.collection.immutable /** * Utility for receiving messages outside of an actor. No methods are provided diff --git a/akka-typed-testkit/src/main/scala/akka/typed/testkit/StubbedActorContext.scala b/akka-typed-testkit/src/main/scala/akka/typed/testkit/StubbedActorContext.scala new file mode 100644 index 0000000000..4bbc5c361b --- /dev/null +++ b/akka-typed-testkit/src/main/scala/akka/typed/testkit/StubbedActorContext.scala @@ -0,0 +1,106 @@ +package akka.typed.testkit + +import akka.{ actor ⇒ untyped } +import akka.typed._ +import akka.util.Helpers + +import scala.collection.immutable.TreeMap +import scala.concurrent.ExecutionContextExecutor +import scala.concurrent.duration.FiniteDuration +import akka.annotation.InternalApi +import akka.typed.internal.ActorContextImpl + +/** + * An [[ActorContext]] for synchronous execution of a [[Behavior]] that + * provides only stubs for the effects an Actor can perform and replaces + * created child Actors by a synchronous Inbox (see `Inbox.sync`). + * + * See [[EffectfulActorContext]] for more advanced uses. + */ +class StubbedActorContext[T]( + val name: String, + override val mailboxCapacity: Int, + override val system: ActorSystem[Nothing]) extends ActorContextImpl[T] { + + val selfInbox = Inbox[T](name) + override val self = selfInbox.ref + + private var _children = TreeMap.empty[String, Inbox[_]] + private val childName = Iterator from 1 map (Helpers.base64(_)) + + override def children: Iterable[ActorRef[Nothing]] = _children.values map (_.ref) + override def child(name: String): Option[ActorRef[Nothing]] = _children get name map (_.ref) + override def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty): ActorRef[U] = { + val i = Inbox[U](childName.next()) + _children += i.ref.path.name → i + i.ref + } + override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] = + _children get name match { + case Some(_) ⇒ throw untyped.InvalidActorNameException(s"actor name $name is already taken") + case None ⇒ + // FIXME correct child path for the Inbox ref + val i = Inbox[U](name) + _children += name → i + i.ref + } + + /** + * Do not actually stop the child inbox, only simulate the liveness check. + * Removal is asynchronous, explicit removeInbox is needed from outside afterwards. + */ + override def stop[U](child: ActorRef[U]): Boolean = { + _children.get(child.path.name) match { + case None ⇒ false + case Some(inbox) ⇒ inbox.ref == child + } + } + override def watch[U](other: ActorRef[U]): Unit = () + override def watchWith[U](other: ActorRef[U], msg: T): Unit = () + override def unwatch[U](other: ActorRef[U]): Unit = () + override def setReceiveTimeout(d: FiniteDuration, msg: T): Unit = () + override def cancelReceiveTimeout(): Unit = () + + override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): untyped.Cancellable = new untyped.Cancellable { + override def cancel() = false + override def isCancelled = true + } + + override def executionContext: ExecutionContextExecutor = system.executionContext + + /** + * INTERNAL API + */ + @InternalApi private[akka] def internalSpawnAdapter[U](f: U ⇒ T, name: String): ActorRef[U] = { + val n = if (name != "") s"${childName.next()}-$name" else childName.next() + val i = Inbox[U](n) + _children += i.ref.path.name → i + new internal.FunctionRef[U]( + self.path / i.ref.path.name, + (msg, _) ⇒ { val m = f(msg); if (m != null) { selfInbox.ref ! m; i.ref ! msg } }, + (self) ⇒ selfInbox.ref.sorry.sendSystem(internal.DeathWatchNotification(self, null))) + } + + /** + * Retrieve the inbox representing the given child actor. The passed ActorRef must be one that was returned + * by one of the spawn methods earlier. + */ + def childInbox[U](child: ActorRef[U]): Inbox[U] = { + val inbox = _children(child.path.name).asInstanceOf[Inbox[U]] + if (inbox.ref != child) throw new IllegalArgumentException(s"$child is not a child of $this") + inbox + } + + /** + * Retrieve the inbox representing the child actor with the given name. + */ + def childInbox[U](name: String): Inbox[U] = _children(name).asInstanceOf[Inbox[U]] + + /** + * Remove the given inbox from the list of children, for example after + * having simulated its termination. + */ + def removeChildInbox(child: ActorRef[Nothing]): Unit = _children -= child.path.name + + override def toString: String = s"Inbox($self)" +} diff --git a/akka-typed-testkit/src/main/scala/akka/typed/testkit/TestEventListener.scala b/akka-typed-testkit/src/main/scala/akka/typed/testkit/TestEventListener.scala new file mode 100644 index 0000000000..faebf639d9 --- /dev/null +++ b/akka-typed-testkit/src/main/scala/akka/typed/testkit/TestEventListener.scala @@ -0,0 +1,68 @@ +package akka.typed.testkit + +import akka.event.Logging.{ LogEvent, StdOutLogger } +import akka.testkit.{ EventFilter, TestEvent ⇒ TE } +import akka.typed.Logger +import akka.typed.Logger.{ Command, Initialize } + +import scala.annotation.tailrec +import akka.typed.scaladsl.Actor +import akka.typed.Behavior + +/** + * EventListener for running tests, which allows selectively filtering out + * expected messages. To use it, include something like this into + * your config + * + *

+ * akka.typed {
+ *   loggers = ["akka.typed.testkit.TestEventListener"]
+ * }
+ * 
+ */ +class TestEventListener extends Logger with StdOutLogger { + + override val initialBehavior: Behavior[Command] = { + Actor.deferred[Command] { _ ⇒ + Actor.immutable[Command] { + case (ctx, Initialize(eventStream, replyTo)) ⇒ + val log = ctx.spawn(Actor.deferred[AnyRef] { childCtx ⇒ + var filters: List[EventFilter] = Nil + + def filter(event: LogEvent): Boolean = filters exists (f ⇒ try { f(event) } catch { case e: Exception ⇒ false }) + + def addFilter(filter: EventFilter): Unit = filters ::= filter + + def removeFilter(filter: EventFilter) { + @tailrec def removeFirst(list: List[EventFilter], zipped: List[EventFilter] = Nil): List[EventFilter] = list match { + case head :: tail if head == filter ⇒ tail.reverse_:::(zipped) + case head :: tail ⇒ removeFirst(tail, head :: zipped) + case Nil ⇒ filters // filter not found, just return original list + } + filters = removeFirst(filters) + } + + Actor.immutable[AnyRef] { + case (_, TE.Mute(filters)) ⇒ + filters foreach addFilter + Actor.same + case (_, TE.UnMute(filters)) ⇒ + filters foreach removeFilter + Actor.same + case (_, event: LogEvent) ⇒ + if (!filter(event)) print(event) + Actor.same + case _ ⇒ Actor.unhandled + } + }, "logger") + + eventStream.subscribe(log, classOf[TE.Mute]) + eventStream.subscribe(log, classOf[TE.UnMute]) + ctx.watch(log) // sign death pact + replyTo ! log + + Actor.empty + } + } + } +} diff --git a/akka-typed-testkit/src/main/scala/akka/typed/testkit/TestKitSettings.scala b/akka-typed-testkit/src/main/scala/akka/typed/testkit/TestKitSettings.scala new file mode 100644 index 0000000000..3cfa588d65 --- /dev/null +++ b/akka-typed-testkit/src/main/scala/akka/typed/testkit/TestKitSettings.scala @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.testkit + +import com.typesafe.config.Config +import scala.concurrent.duration.FiniteDuration +import akka.util.Timeout +import akka.typed.ActorSystem + +object TestKitSettings { + /** + * Reads configuration settings from `akka.typed.test` section. + */ + def apply(system: ActorSystem[_]): TestKitSettings = + apply(system.settings.config) + + /** + * Reads configuration settings from given `Config` that + * must have the same layout as the `akka.typed.test` section. + */ + def apply(config: Config): TestKitSettings = + new TestKitSettings(config) +} + +class TestKitSettings(val config: Config) { + + import akka.util.Helpers._ + + val TestTimeFactor = config.getDouble("akka.typed.test.timefactor"). + requiring(tf ⇒ !tf.isInfinite && tf > 0, "akka.typed.test.timefactor must be positive finite double") + val SingleExpectDefaultTimeout: FiniteDuration = config.getMillisDuration("akka.typed.test.single-expect-default") + val DefaultTimeout: Timeout = Timeout(config.getMillisDuration("akka.typed.test.default-timeout")) +} diff --git a/akka-typed-testkit/src/main/scala/akka/typed/testkit/scaladsl/TestProbe.scala b/akka-typed-testkit/src/main/scala/akka/typed/testkit/scaladsl/TestProbe.scala new file mode 100644 index 0000000000..d6be133da0 --- /dev/null +++ b/akka-typed-testkit/src/main/scala/akka/typed/testkit/scaladsl/TestProbe.scala @@ -0,0 +1,229 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.testkit.scaladsl + +import scala.concurrent.duration._ +import java.util.concurrent.BlockingDeque +import akka.typed.Behavior +import akka.typed.scaladsl.Actor +import akka.typed.ActorSystem +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.atomic.AtomicInteger +import akka.typed.ActorRef +import akka.util.Timeout +import akka.util.PrettyDuration.PrettyPrintableDuration +import scala.concurrent.Await +import com.typesafe.config.Config +import akka.typed.testkit.TestKitSettings +import akka.util.BoxedType +import scala.reflect.ClassTag + +object TestProbe { + private val testActorId = new AtomicInteger(0) + + def apply[M]()(implicit system: ActorSystem[_], settings: TestKitSettings): TestProbe[M] = + apply(name = "testProbe") + + def apply[M](name: String)(implicit system: ActorSystem[_], settings: TestKitSettings): TestProbe[M] = + new TestProbe(name) + + private def testActor[M](queue: BlockingDeque[M]): Behavior[M] = Actor.immutable { (ctx, msg) ⇒ + queue.offerLast(msg) + Actor.same + } +} + +class TestProbe[M](name: String)(implicit val system: ActorSystem[_], val settings: TestKitSettings) { + import TestProbe._ + private val queue = new LinkedBlockingDeque[M] + + private var end: Duration = Duration.Undefined + + /** + * if last assertion was expectNoMsg, disable timing failure upon within() + * block end. + */ + private var lastWasNoMsg = false + + private var lastMessage: Option[M] = None + + val testActor: ActorRef[M] = { + implicit val timeout = Timeout(3.seconds) + val futRef = system.systemActorOf(TestProbe.testActor(queue), s"$name-${testActorId.incrementAndGet()}") + Await.result(futRef, timeout.duration + 1.second) + } + + /** + * Shorthand to get the `testActor`. + */ + def ref: ActorRef[M] = testActor + + /** + * Obtain current time (`System.nanoTime`) as Duration. + */ + protected def now: FiniteDuration = System.nanoTime.nanos + + /** + * Obtain time remaining for execution of the innermost enclosing `within` + * block or missing that it returns the properly dilated default for this + * case from settings (key "akka.typed.test.single-expect-default"). + */ + def remainingOrDefault = remainingOr(settings.SingleExpectDefaultTimeout.dilated) + + /** + * Obtain time remaining for execution of the innermost enclosing `within` + * block or throw an [[AssertionError]] if no `within` block surrounds this + * call. + */ + def remaining: FiniteDuration = end match { + case f: FiniteDuration ⇒ f - now + case _ ⇒ throw new AssertionError("`remaining` may not be called outside of `within`") + } + + /** + * Obtain time remaining for execution of the innermost enclosing `within` + * block or missing that it returns the given duration. + */ + def remainingOr(duration: FiniteDuration): FiniteDuration = end match { + case x if x eq Duration.Undefined ⇒ duration + case x if !x.isFinite ⇒ throw new IllegalArgumentException("`end` cannot be infinite") + case f: FiniteDuration ⇒ f - now + } + + private def remainingOrDilated(max: Duration): FiniteDuration = max match { + case x if x eq Duration.Undefined ⇒ remainingOrDefault + case x if !x.isFinite ⇒ throw new IllegalArgumentException("max duration cannot be infinite") + case f: FiniteDuration ⇒ f.dilated + } + + /** + * Execute code block while bounding its execution time between `min` and + * `max`. `within` blocks may be nested. All methods in this trait which + * take maximum wait times are available in a version which implicitly uses + * the remaining time governed by the innermost enclosing `within` block. + * + * Note that the timeout is scaled using Duration.dilated, which uses the + * configuration entry "akka.typed.test.timefactor", while the min Duration is not. + * + * {{{ + * val ret = within(50 millis) { + * test ! Ping + * expectMsgType[Pong] + * } + * }}} + */ + def within[T](min: FiniteDuration, max: FiniteDuration)(f: ⇒ T): T = { + val _max = max.dilated + val start = now + val rem = if (end == Duration.Undefined) Duration.Inf else end - start + assert(rem >= min, s"required min time $min not possible, only ${rem.pretty} left") + + lastWasNoMsg = false + + val max_diff = _max min rem + val prev_end = end + end = start + max_diff + + val ret = try f finally end = prev_end + + val diff = now - start + assert(min <= diff, s"block took ${diff.pretty}, should at least have been $min") + if (!lastWasNoMsg) { + assert(diff <= max_diff, s"block took ${diff.pretty}, exceeding ${max_diff.pretty}") + } + + ret + } + + /** + * Same as calling `within(0 seconds, max)(f)`. + */ + def within[T](max: FiniteDuration)(f: ⇒ T): T = within(Duration.Zero, max)(f) + + /** + * Same as `expectMsg(remainingOrDefault, obj)`, but correctly treating the timeFactor. + */ + def expectMsg[T <: M](obj: T): T = expectMsg_internal(remainingOrDefault, obj) + + /** + * Receive one message from the test actor and assert that it equals the + * given object. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsg[T <: M](max: FiniteDuration, obj: T): T = expectMsg_internal(max.dilated, obj) + + /** + * Receive one message from the test actor and assert that it equals the + * given object. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsg[T <: M](max: FiniteDuration, hint: String, obj: T): T = expectMsg_internal(max.dilated, obj, Some(hint)) + + private def expectMsg_internal[T <: M](max: Duration, obj: T, hint: Option[String] = None): T = { + val o = receiveOne(max) + val hintOrEmptyString = hint.map(": " + _).getOrElse("") + assert(o != null, s"timeout ($max) during expectMsg while waiting for $obj" + hintOrEmptyString) + assert(obj == o, s"expected $obj, found $o" + hintOrEmptyString) + o.asInstanceOf[T] + } + + /** + * Receive one message from the internal queue of the TestActor. If the given + * duration is zero, the queue is polled (non-blocking). + * + * This method does NOT automatically scale its Duration parameter! + */ + private def receiveOne(max: Duration): M = { + val message = + if (max == 0.seconds) { + queue.pollFirst + } else if (max.isFinite) { + queue.pollFirst(max.length, max.unit) + } else { + queue.takeFirst + } + lastWasNoMsg = false + lastMessage = if (message == null) None else Some(message) + message + } + + /** + * Assert that no message is received for the specified time. + */ + def expectNoMsg(max: FiniteDuration) { expectNoMsg_internal(max.dilated) } + + private def expectNoMsg_internal(max: FiniteDuration) { + val o = receiveOne(max) + assert(o == null, s"received unexpected message $o") + lastWasNoMsg = true + } + + /** + * Same as `expectMsgType[T](remainingOrDefault)`, but correctly treating the timeFactor. + */ + def expectMsgType[T <: M](implicit t: ClassTag[T]): T = + expectMsgClass_internal(remainingOrDefault, t.runtimeClass.asInstanceOf[Class[T]]) + + /** + * Receive one message from the test actor and assert that it conforms to the + * given type (after erasure). Wait time is bounded by the given duration, + * with an AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsgType[T <: M](max: FiniteDuration)(implicit t: ClassTag[T]): T = + expectMsgClass_internal(max.dilated, t.runtimeClass.asInstanceOf[Class[T]]) + + private def expectMsgClass_internal[C](max: FiniteDuration, c: Class[C]): C = { + val o = receiveOne(max) + assert(o != null, s"timeout ($max) during expectMsgClass waiting for $c") + assert(BoxedType(c) isInstance o, s"expected $c, found ${o.getClass} ($o)") + o.asInstanceOf[C] + } + +} diff --git a/akka-typed-testkit/src/main/scala/akka/typed/testkit/scaladsl/package.scala b/akka-typed-testkit/src/main/scala/akka/typed/testkit/scaladsl/package.scala new file mode 100644 index 0000000000..81053e8c94 --- /dev/null +++ b/akka-typed-testkit/src/main/scala/akka/typed/testkit/scaladsl/package.scala @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package akka.typed.testkit + +import scala.concurrent.duration.{ Duration, FiniteDuration } +import scala.reflect.ClassTag +import scala.collection.immutable +import java.util.concurrent.TimeUnit.MILLISECONDS +import akka.typed.ActorSystem + +package object scaladsl { + + /** + * Scala API. Scale timeouts (durations) during tests with the configured + * 'akka.test.timefactor'. + * Implicit class providing `dilated` method. + * + * {{{ + * import scala.concurrent.duration._ + * import akka.typed.testkit.scaladsl._ + * 10.milliseconds.dilated + * }}} + * + * Uses the scaling factor from the `TestTimeFactor` in the [[TestKitSettings]] + * (in implicit scope). + * + */ + implicit class TestDuration(val duration: FiniteDuration) extends AnyVal { + def dilated(implicit settings: TestKitSettings): FiniteDuration = + (duration * settings.TestTimeFactor).asInstanceOf[FiniteDuration] + } + +} diff --git a/akka-typed-tests/build.sbt b/akka-typed-tests/build.sbt new file mode 100644 index 0000000000..a457754e83 --- /dev/null +++ b/akka-typed-tests/build.sbt @@ -0,0 +1,8 @@ +import akka.{ AkkaBuild, Formatting } + +AkkaBuild.defaultSettings +AkkaBuild.mayChangeSettings +Formatting.formatSettings + +disablePlugins(MimaPlugin) + diff --git a/akka-typed-tests/src/test/java/akka/typed/DispatcherSelectorTest.java b/akka-typed-tests/src/test/java/akka/typed/DispatcherSelectorTest.java new file mode 100644 index 0000000000..83bcb69381 --- /dev/null +++ b/akka-typed-tests/src/test/java/akka/typed/DispatcherSelectorTest.java @@ -0,0 +1,16 @@ +package akka.typed; + +import scala.concurrent.ExecutionContext; + +import java.util.concurrent.Executor; + +public class DispatcherSelectorTest { + // Compile time only test to verify + // dispatcher factories are accessible from Java + + private DispatcherSelector def = DispatcherSelector.defaultDispatcher(); + private DispatcherSelector conf = DispatcherSelector.fromConfig("somepath"); + private DispatcherSelector ex = DispatcherSelector.fromExecutor((Executor) null); + private DispatcherSelector ec = DispatcherSelector.fromExecutionContext((ExecutionContext) null); + +} diff --git a/akka-typed-tests/src/test/java/akka/typed/ExtensionsTest.java b/akka-typed-tests/src/test/java/akka/typed/ExtensionsTest.java new file mode 100644 index 0000000000..86b5d78d68 --- /dev/null +++ b/akka-typed-tests/src/test/java/akka/typed/ExtensionsTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed; + +import akka.actor.*; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; +import org.scalatest.junit.JUnitSuite; + +import java.util.Optional; + +import static junit.framework.TestCase.assertSame; +import static org.junit.Assert.assertTrue; + +public class ExtensionsTest extends JUnitSuite { + + + public static class MyExtImpl implements Extension { + } + + public static class MyExtension extends ExtensionId { + + private final static MyExtension instance = new MyExtension(); + + private MyExtension() { + } + + public static MyExtension getInstance() { + return instance; + } + + public MyExtImpl createExtension(ActorSystem system) { + return new MyExtImpl(); + } + + public static MyExtImpl get(ActorSystem system) { + return instance.apply(system); + } + } + + + @Test + public void loadJavaExtensionsFromConfig() { + final ActorSystem system = ActorSystem.create( + "loadJavaExtensionsFromConfig", + Behavior.empty(), + Optional.empty(), + Optional.of(ConfigFactory.parseString("akka.typed.extensions += \"akka.typed.ExtensionsTest$MyExtension\"").resolve()), + Optional.empty(), + Optional.empty() + ); + + try { + // note that this is not the intended end user way to access it + assertTrue(system.hasExtension(MyExtension.getInstance())); + + MyExtImpl instance1 = MyExtension.get(system); + MyExtImpl instance2 = MyExtension.get(system); + + assertSame(instance1, instance2); + } finally { + system.terminate(); + } + } + + @Test + public void loadScalaExtension() { + final ActorSystem system = ActorSystem.create("loadScalaExtension", Behavior.empty()); + try { + DummyExtension1 instance1 = DummyExtension1.get(system); + DummyExtension1 instance2 = DummyExtension1.get(system); + + assertSame(instance1, instance2); + } finally { + system.terminate(); + } + } + + +} diff --git a/akka-typed-tests/src/test/java/akka/typed/javadsl/ActorCompile.java b/akka-typed-tests/src/test/java/akka/typed/javadsl/ActorCompile.java new file mode 100644 index 0000000000..9a88567812 --- /dev/null +++ b/akka-typed-tests/src/test/java/akka/typed/javadsl/ActorCompile.java @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.javadsl; + +import akka.typed.*; +import akka.typed.ActorContext; + +import static akka.typed.javadsl.Actor.*; + +import java.util.concurrent.TimeUnit; +import scala.concurrent.duration.Duration; + +@SuppressWarnings("unused") +public class ActorCompile { + + interface MyMsg {} + + static class MyMsgA implements MyMsg { + final ActorRef replyTo; + + public MyMsgA(ActorRef replyTo) { + this.replyTo = replyTo; + } + } + + static class MyMsgB implements MyMsg { + final String greeting; + + public MyMsgB(String greeting) { + this.greeting = greeting; + } + } + + Behavior actor1 = immutable((ctx, msg) -> stopped(), (ctx, signal) -> same()); + Behavior actor2 = immutable((ctx, msg) -> unhandled()); + Behavior actor4 = empty(); + Behavior actor5 = ignore(); + Behavior actor6 = tap((ctx, signal) -> {}, (ctx, msg) -> {}, actor5); + Behavior actor7 = actor6.narrow(); + Behavior actor8 = deferred(ctx -> { + final ActorRef self = ctx.getSelf(); + return monitor(self, ignore()); + }); + Behavior actor9 = widened(actor7, pf -> pf.match(MyMsgA.class, x -> x)); + Behavior actor10 = immutable((ctx, msg) -> stopped(actor4), (ctx, signal) -> same()); + + ActorSystem system = ActorSystem.create("Sys", actor1); + + { + Actor.immutable((ctx, msg) -> { + if (msg instanceof MyMsgA) { + return immutable((ctx2, msg2) -> { + if (msg2 instanceof MyMsgB) { + ((MyMsgA) msg).replyTo.tell(((MyMsgB) msg2).greeting); + + ActorRef adapter = ctx2.spawnAdapter(s -> new MyMsgB(s.toUpperCase())); + } + return same(); + }); + } else return unhandled(); + }); + } + + { + Behavior b = Actor.withTimers(timers -> { + timers.startPeriodicTimer("key", new MyMsgB("tick"), Duration.create(1, TimeUnit.SECONDS)); + return Actor.ignore(); + }); + } + + + static class MyBehavior extends ExtensibleBehavior { + + @Override + public Behavior receiveSignal(ActorContext ctx, Signal msg) throws Exception { + return this; + } + + @Override + public Behavior receiveMessage(ActorContext ctx, MyMsg msg) throws Exception { + ActorRef adapter = ctx.asJava().spawnAdapter(s -> new MyMsgB(s.toUpperCase())); + return this; + } + + } + + // SupervisorStrategy + { + SupervisorStrategy strategy1 = SupervisorStrategy.restart(); + SupervisorStrategy strategy2 = SupervisorStrategy.restart().withLoggingEnabled(false); + SupervisorStrategy strategy3 = SupervisorStrategy.resume(); + SupervisorStrategy strategy4 = + SupervisorStrategy.restartWithLimit(3, Duration.create(1, TimeUnit.SECONDS)); + + SupervisorStrategy strategy5 = + SupervisorStrategy.restartWithBackoff( + Duration.create(200, TimeUnit.MILLISECONDS), + Duration.create(10, TimeUnit.SECONDS), + 0.1); + + BackoffSupervisorStrategy strategy6 = + SupervisorStrategy.restartWithBackoff( + Duration.create(200, TimeUnit.MILLISECONDS), + Duration.create(10, TimeUnit.SECONDS), + 0.1); + SupervisorStrategy strategy7 = strategy6.withResetBackoffAfter(Duration.create(2, TimeUnit.SECONDS)); + + Behavior behv = + Actor.restarter(RuntimeException.class, strategy1, + Actor.restarter(IllegalStateException.class, + strategy6, Actor.ignore())); + } + + +} diff --git a/akka-typed-tests/src/test/java/akka/typed/javadsl/AdapterTest.java b/akka-typed-tests/src/test/java/akka/typed/javadsl/AdapterTest.java new file mode 100644 index 0000000000..7a59034fd2 --- /dev/null +++ b/akka-typed-tests/src/test/java/akka/typed/javadsl/AdapterTest.java @@ -0,0 +1,339 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ + +package akka.typed.javadsl; + +import org.junit.ClassRule; +import org.junit.Test; +import org.scalatest.junit.JUnitSuite; + +import scala.concurrent.duration.FiniteDuration; +import java.util.concurrent.TimeUnit; +import akka.actor.ActorSystem; +import akka.testkit.AkkaJUnitActorSystemResource; +import akka.testkit.AkkaSpec; +import akka.typed.ActorRef; +import akka.typed.Behavior; +import akka.typed.Signal; +import akka.typed.Terminated; +import akka.testkit.javadsl.TestKit; +import akka.actor.SupervisorStrategy; +import static akka.typed.javadsl.Actor.*; + +public class AdapterTest extends JUnitSuite { + + static akka.actor.Props untyped1() { + return akka.actor.Props.create(Untyped1.class, () -> new Untyped1()); + } + + static class Untyped1 extends akka.actor.AbstractActor { + @Override + public Receive createReceive() { + return receiveBuilder() + .matchEquals("ping", s -> getSender().tell("pong", getSelf())) + .match(ThrowIt.class, t -> { + throw t; + }) + .build(); + } + } + + static class Typed1 { + private final akka.actor.ActorRef ref; + private final akka.actor.ActorRef probe; + + private Typed1(akka.actor.ActorRef ref, akka.actor.ActorRef probe) { + this.ref = ref; + this.probe = probe; + } + + static Behavior create(akka.actor.ActorRef ref, akka.actor.ActorRef probe) { + Typed1 logic = new Typed1(ref, probe); + return immutable( + (ctx, msg) -> logic.onMessage(ctx, msg), + (ctx, sig) -> logic.onSignal(ctx, sig)); + } + + Behavior onMessage(ActorContext ctx, String msg) { + if (msg.equals("send")) { + akka.actor.ActorRef replyTo = Adapter.toUntyped(ctx.getSelf()); + ref.tell("ping", replyTo); + return same(); + } else if (msg.equals("pong")) { + probe.tell("ok", akka.actor.ActorRef.noSender()); + return same(); + } else if (msg.equals("actorOf")) { + akka.actor.ActorRef child = Adapter.actorOf(ctx, untyped1()); + child.tell("ping", Adapter.toUntyped(ctx.getSelf())); + return same(); + } else if (msg.equals("watch")) { + Adapter.watch(ctx, ref); + return same(); + } else if (msg.equals("supervise-stop")) { + akka.actor.ActorRef child = Adapter.actorOf(ctx, untyped1()); + Adapter.watch(ctx, child); + child.tell(new ThrowIt3(), Adapter.toUntyped(ctx.getSelf())); + child.tell("ping", Adapter.toUntyped(ctx.getSelf())); + return same(); + } else if (msg.equals("stop-child")) { + akka.actor.ActorRef child = Adapter.actorOf(ctx, untyped1()); + Adapter.watch(ctx, child); + Adapter.stop(ctx, child); + return same(); + } else { + return unhandled(); + } + } + + Behavior onSignal(ActorContext ctx, Signal sig) { + if (sig instanceof Terminated) { + probe.tell("terminated", akka.actor.ActorRef.noSender()); + return same(); + } else { + return unhandled(); + } + } + } + + static interface Typed2Msg {}; + static final class Ping implements Typed2Msg { + public final ActorRef replyTo; + + public Ping(ActorRef replyTo) { + this.replyTo = replyTo; + } + } + static final class StopIt implements Typed2Msg {} + static abstract class ThrowIt extends RuntimeException implements Typed2Msg {} + static class ThrowIt1 extends ThrowIt {} + static class ThrowIt2 extends ThrowIt {} + static class ThrowIt3 extends ThrowIt {} + + static akka.actor.Props untyped2(ActorRef ref, akka.actor.ActorRef probe) { + return akka.actor.Props.create(Untyped2.class, () -> new Untyped2(ref, probe)); + } + + static class Untyped2 extends akka.actor.AbstractActor { + private final ActorRef ref; + private final akka.actor.ActorRef probe; + private final SupervisorStrategy strategy; + + Untyped2(ActorRef ref, akka.actor.ActorRef probe) { + this.ref = ref; + this.probe = probe; + this.strategy = strategy(); + } + + @Override + public Receive createReceive() { + return receiveBuilder() + .matchEquals("send", s -> { + ActorRef replyTo = Adapter.toTyped(getSelf()); + ref.tell(new Ping(replyTo)); + }) + .matchEquals("pong", s -> probe.tell("ok", getSelf())) + .matchEquals("spawn", s -> { + ActorRef child = Adapter.spawnAnonymous(getContext(), typed2()); + child.tell(new Ping(Adapter.toTyped(getSelf()))); + }) + .matchEquals("actorOf-props", s -> { + // this is how Cluster Sharding can be used + akka.actor.ActorRef child = getContext().actorOf(typed2Props()); + child.tell(new Ping(Adapter.toTyped(getSelf())), akka.actor.ActorRef.noSender()); + }) + .matchEquals("watch", s -> Adapter.watch(getContext(), ref)) + .match(akka.actor.Terminated.class, t -> probe.tell("terminated", getSelf())) + .matchEquals("supervise-stop", s -> testSupervice(new ThrowIt1())) + .matchEquals("supervise-resume", s -> testSupervice(new ThrowIt2())) + .matchEquals("supervise-restart", s -> testSupervice(new ThrowIt3())) + .matchEquals("stop-child", s -> { + ActorRef child = Adapter.spawnAnonymous(getContext(), typed2()); + Adapter.watch(getContext(), child); + Adapter.stop(getContext(), child); + }) + .build(); + } + + private void testSupervice(ThrowIt t) { + ActorRef child = Adapter.spawnAnonymous(getContext(), typed2()); + Adapter.watch(getContext(), child); + child.tell(t); + child.tell(new Ping(Adapter.toTyped(getSelf()))); + } + + private SupervisorStrategy strategy() { + return new akka.actor.OneForOneStrategy(false, akka.japi.pf.DeciderBuilder + .match(ThrowIt1.class, e -> { + probe.tell("thrown-stop", getSelf()); + return SupervisorStrategy.stop(); + }) + .match(ThrowIt2.class, e -> { + probe.tell("thrown-resume", getSelf()); + return SupervisorStrategy.resume(); + }) + .match(ThrowIt3.class, e -> { + probe.tell("thrown-restart", getSelf()); + // TODO Restart will not really restart the behavior + return SupervisorStrategy.restart(); + }) + .build()); + } + + @Override + public SupervisorStrategy supervisorStrategy() { + return strategy; + } + } + + static Behavior typed2() { + return Actor.immutable((ctx, msg) -> { + if (msg instanceof Ping) { + ActorRef replyTo = ((Ping) msg).replyTo; + replyTo.tell("pong"); + return same(); + } else if (msg instanceof StopIt) { + return stopped(); + } else if (msg instanceof ThrowIt) { + throw (ThrowIt) msg; + } else { + return unhandled(); + } + }); + } + + static akka.actor.Props typed2Props() { + return Adapter.props(() -> typed2()); + } + + @ClassRule + public static AkkaJUnitActorSystemResource actorSystemResource = new AkkaJUnitActorSystemResource("ActorSelectionTest", + AkkaSpec.testConf()); + + private final ActorSystem system = actorSystemResource.getSystem(); + + + + @Test + public void shouldSendMessageFromTypedToUntyped() { + TestKit probe = new TestKit(system); + akka.actor.ActorRef untypedRef = system.actorOf(untyped1()); + ActorRef typedRef = Adapter.spawnAnonymous(system, Typed1.create(untypedRef, probe.getRef())); + typedRef.tell("send"); + probe.expectMsg("ok"); + } + + @Test + public void shouldSendMessageFromUntypedToTyped() { + TestKit probe = new TestKit(system); + ActorRef typedRef = Adapter.spawnAnonymous(system, typed2()).narrow(); + akka.actor.ActorRef untypedRef = system.actorOf(untyped2(typedRef, probe.getRef())); + untypedRef.tell("send", akka.actor.ActorRef.noSender()); + probe.expectMsg("ok"); + } + + @Test + public void shouldSpawnTypedChildFromUntypedParent() { + TestKit probe = new TestKit(system); + ActorRef ignore = Adapter.spawnAnonymous(system, ignore()); + akka.actor.ActorRef untypedRef = system.actorOf(untyped2(ignore, probe.getRef())); + untypedRef.tell("spawn", akka.actor.ActorRef.noSender()); + probe.expectMsg("ok"); + } + + @Test + public void shouldActorOfTypedChildViaPropsFromUntypedParent() { + TestKit probe = new TestKit(system); + ActorRef ignore = Adapter.spawnAnonymous(system, ignore()); + akka.actor.ActorRef untypedRef = system.actorOf(untyped2(ignore, probe.getRef())); + untypedRef.tell("actorOf-props", akka.actor.ActorRef.noSender()); + probe.expectMsg("ok"); + } + + @Test + public void shouldActorOfUntypedChildFromTypedParent() { + TestKit probe = new TestKit(system); + akka.actor.ActorRef ignore = system.actorOf(akka.actor.Props.empty()); + ActorRef typedRef = Adapter.spawnAnonymous(system, Typed1.create(ignore, probe.getRef())); + typedRef.tell("actorOf"); + probe.expectMsg("ok"); + } + + @Test + public void shouldWatchTypedFromUntyped() { + TestKit probe = new TestKit(system); + ActorRef typedRef = Adapter.spawnAnonymous(system, typed2()); + ActorRef typedRef2 = typedRef.narrow(); + akka.actor.ActorRef untypedRef = system.actorOf(untyped2(typedRef2, probe.getRef())); + untypedRef.tell("watch", akka.actor.ActorRef.noSender()); + typedRef.tell(new StopIt()); + probe.expectMsg("terminated"); + } + + @Test + public void shouldWatchUntypedFromTyped() { + TestKit probe = new TestKit(system); + akka.actor.ActorRef untypedRef = system.actorOf(untyped1()); + ActorRef typedRef = Adapter.spawnAnonymous(system, Typed1.create(untypedRef, probe.getRef())); + typedRef.tell("watch"); + untypedRef.tell(akka.actor.PoisonPill.getInstance() , akka.actor.ActorRef.noSender()); + probe.expectMsg("terminated"); + } + + @Test + public void shouldSuperviseTypedChildFromUntypedParent() { + TestKit probe = new TestKit(system); + ActorRef ignore = Adapter.spawnAnonymous(system, ignore()); + akka.actor.ActorRef untypedRef = system.actorOf(untyped2(ignore, probe.getRef())); + untypedRef.tell("supervise-stop", akka.actor.ActorRef.noSender()); + probe.expectMsg("thrown-stop"); + // ping => ok should not get through here + probe.expectMsg("terminated"); + + untypedRef.tell("supervise-resume", akka.actor.ActorRef.noSender()); + probe.expectMsg("thrown-resume"); + probe.expectMsg("ok"); + + untypedRef.tell("supervise-restart", akka.actor.ActorRef.noSender()); + probe.expectMsg("thrown-restart"); + probe.expectMsg("ok"); + } + + @Test + public void shouldSuperviseUntypedChildFromTypedParent() { + TestKit probe = new TestKit(system); + akka.actor.ActorRef ignore = system.actorOf(akka.actor.Props.empty()); + ActorRef typedRef = Adapter.spawnAnonymous(system, Typed1.create(ignore, probe.getRef())); + + int originalLogLevel = system.eventStream().logLevel(); + try { + // supress the logging with stack trace + system.eventStream().setLogLevel(Integer.MIN_VALUE); // OFF + + // only stop supervisorStrategy + typedRef.tell("supervise-stop"); + probe.expectMsg("terminated"); + } finally { + system.eventStream().setLogLevel(originalLogLevel); + } + probe.expectNoMsg(FiniteDuration.create(100, TimeUnit.MILLISECONDS)); // no pong + } + + @Test + public void shouldStopTypedChildFromUntypedParent() { + TestKit probe = new TestKit(system); + ActorRef ignore = Adapter.spawnAnonymous(system, ignore()); + akka.actor.ActorRef untypedRef = system.actorOf(untyped2(ignore, probe.getRef())); + untypedRef.tell("stop-child", akka.actor.ActorRef.noSender()); + probe.expectMsg("terminated"); + } + + @Test + public void shouldStopUntypedChildFromTypedParent() { + TestKit probe = new TestKit(system); + akka.actor.ActorRef ignore = system.actorOf(akka.actor.Props.empty()); + ActorRef typedRef = Adapter.spawnAnonymous(system, Typed1.create(ignore, probe.getRef())); + typedRef.tell("stop-child"); + probe.expectMsg("terminated"); + } +} diff --git a/akka-typed-tests/src/test/java/akka/typed/javadsl/BehaviorBuilderTest.java b/akka-typed-tests/src/test/java/akka/typed/javadsl/BehaviorBuilderTest.java new file mode 100644 index 0000000000..b9294ab5f0 --- /dev/null +++ b/akka-typed-tests/src/test/java/akka/typed/javadsl/BehaviorBuilderTest.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.javadsl; + +import org.junit.Test; +import org.scalatest.junit.JUnitSuite; + +import akka.typed.Behavior; +import akka.typed.Terminated; +import akka.typed.ActorRef; + +import java.util.ArrayList; + +import static akka.typed.javadsl.Actor.same; +import static akka.typed.javadsl.Actor.stopped; + +/** + * Test creating [[Behavior]]s using [[BehaviorBuilder]] + */ +public class BehaviorBuilderTest extends JUnitSuite { + interface Message { + } + + static final class One implements Message { + public String foo() { + return "Bar"; + } + } + static final class MyList extends ArrayList implements Message { + }; + + @Test + public void shouldCompile() { + Behavior b = Actor.immutable(Message.class) + .onMessage(One.class, (ctx, o) -> { + o.foo(); + return same(); + }) + .onMessage(One.class, o -> o.foo().startsWith("a"), (ctx, o) -> same()) + .onMessageUnchecked(MyList.class, (ActorContext ctx, MyList l) -> { + String first = l.get(0); + return Actor.same(); + }) + .onSignal(Terminated.class, (ctx, t) -> { + System.out.println("Terminating along with " + t.ref()); + return stopped(); + }) + .build(); + } + + interface CounterMessage {}; + static final class Increase implements CounterMessage {}; + static final class Get implements CounterMessage { + final ActorRef sender; + public Get(ActorRef sender) { + this.sender = sender; + } + }; + static final class Got { + final int n; + public Got(int n) { + this.n = n; + } + } + + public Behavior immutableCounter(int currentValue) { + return Actor.immutable(CounterMessage.class) + .onMessage(Increase.class, (ctx, o) -> { + return immutableCounter(currentValue + 1); + }) + .onMessage(Get.class, (ctx, o) -> { + o.sender.tell(new Got(currentValue)); + return same(); + }) + .build(); + } + + @Test + public void testImmutableCounter() { + Behavior immutable = immutableCounter(0); + } + +} diff --git a/akka-typed-tests/src/test/java/akka/typed/javadsl/ReceiveBuilderTest.java b/akka-typed-tests/src/test/java/akka/typed/javadsl/ReceiveBuilderTest.java new file mode 100644 index 0000000000..78a792c0d9 --- /dev/null +++ b/akka-typed-tests/src/test/java/akka/typed/javadsl/ReceiveBuilderTest.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.javadsl; + +import org.junit.Test; +import org.scalatest.junit.JUnitSuite; + +import akka.typed.Behavior; + +import static org.junit.Assert.assertEquals; + +/** + * Test creating [[MutableActor]]s using [[ReceiveBuilder]] + */ +public class ReceiveBuilderTest extends JUnitSuite { + + @Test + public void testMutableCounter() { + Behavior mutable = Actor.mutable(ctx -> new Actor.MutableBehavior() { + int currentValue = 0; + + private Behavior receiveIncrease(BehaviorBuilderTest.Increase msg) { + currentValue++; + return this; + } + + private Behavior receiveGet(BehaviorBuilderTest.Get get) { + get.sender.tell(new BehaviorBuilderTest.Got(currentValue)); + return this; + } + + @Override + public Actor.Receive createReceive() { + return receiveBuilder() + .onMessage(BehaviorBuilderTest.Increase.class, this::receiveIncrease) + .onMessage(BehaviorBuilderTest.Get.class, this::receiveGet) + .build(); + } + }); + } + + private static class MyMutableBehavior extends Actor.MutableBehavior { + private int value; + + public MyMutableBehavior(int initialValue) { + super(); + this.value = initialValue; + } + + @Override + public Actor.Receive createReceive() { + assertEquals(42, value); + return receiveBuilder().build(); + } + } + + @Test + public void testInitializationOrder() throws Exception { + MyMutableBehavior mutable = new MyMutableBehavior(42); + assertEquals(Actor.unhandled(), mutable.receiveMessage(null, new BehaviorBuilderTest.Increase())); + } +} diff --git a/akka-typed-tests/src/test/java/akka/typed/javadsl/WatchTest.java b/akka-typed-tests/src/test/java/akka/typed/javadsl/WatchTest.java new file mode 100644 index 0000000000..c8da9ddfd0 --- /dev/null +++ b/akka-typed-tests/src/test/java/akka/typed/javadsl/WatchTest.java @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.javadsl; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; + +import akka.Done; +import org.scalatest.junit.JUnitSuite; +import scala.concurrent.Await; +import scala.concurrent.duration.Duration; +import akka.util.Timeout; +import org.junit.Test; + +import akka.typed.*; +import static akka.typed.javadsl.Actor.*; +import static akka.typed.javadsl.AskPattern.*; + +public class WatchTest extends JUnitSuite { + + static interface Message {} + static final class RunTest implements Message { + private final ActorRef replyTo; + public RunTest(ActorRef replyTo) { + this.replyTo = replyTo; + } + } + static final class Stop {} + static final class CustomTerminationMessage implements Message {} + + // final FiniteDuration fiveSeconds = FiniteDuration.create(5, TimeUnit.SECONDS); + final Timeout timeout = new Timeout(Duration.create(5, TimeUnit.SECONDS)); + + final Behavior exitingActor = immutable((ctx, msg) -> { + System.out.println("Stopping!"); + return stopped(); + }); + + private Behavior> waitingForTermination(ActorRef replyWhenTerminated) { + return immutable( + (ctx, msg) -> unhandled(), + (ctx, sig) -> { + if (sig instanceof Terminated) { + replyWhenTerminated.tell(Done.getInstance()); + } + return same(); + } + ); + } + + private Behavior waitingForMessage(ActorRef replyWhenReceived) { + return immutable( + (ctx, msg) -> { + if (msg instanceof CustomTerminationMessage) { + replyWhenReceived.tell(Done.getInstance()); + return same(); + } else { + return unhandled(); + } + } + ); + } + + @Test + public void shouldWatchTerminatingActor() throws Exception { + Behavior> root = immutable((ctx, msg) -> { + ActorRef watched = ctx.spawn(exitingActor, "exitingActor"); + ctx.watch(watched); + watched.tell(new Stop()); + return waitingForTermination(msg.replyTo); + }); + ActorSystem> system = ActorSystem.create("sysname", root); + try { + // Not sure why this does not compile without an explicit cast? + // system.tell(new RunTest()); + CompletionStage result = AskPattern.ask((ActorRef>)system, (ActorRef ref) -> new RunTest(ref), timeout, system.scheduler()); + result.toCompletableFuture().get(3, TimeUnit.SECONDS); + } finally { + Await.ready(system.terminate(), Duration.create(10, TimeUnit.SECONDS)); + } + } + + @Test + public void shouldWatchWithCustomMessage() throws Exception { + Behavior root = immutable((ctx, msg) -> { + if (msg instanceof RunTest) { + ActorRef watched = ctx.spawn(exitingActor, "exitingActor"); + ctx.watchWith(watched, new CustomTerminationMessage()); + watched.tell(new Stop()); + return waitingForMessage(((RunTest) msg).replyTo); + } else { + return unhandled(); + } + }); + ActorSystem system = ActorSystem.create("sysname", root); + try { + // Not sure why this does not compile without an explicit cast? + // system.tell(new RunTest()); + CompletionStage result = AskPattern.ask((ActorRef)system, (ActorRef ref) -> new RunTest(ref), timeout, system.scheduler()); + result.toCompletableFuture().get(3, TimeUnit.SECONDS); + } finally { + Await.ready(system.terminate(), Duration.create(10, TimeUnit.SECONDS)); + } + } +} \ No newline at end of file diff --git a/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java b/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java new file mode 100644 index 0000000000..5d59744b7d --- /dev/null +++ b/akka-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java @@ -0,0 +1,207 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package jdocs.akka.typed; + +//#imports +import akka.typed.ActorRef; +import akka.typed.ActorSystem; +import akka.typed.Behavior; +import akka.typed.Terminated; +import akka.typed.javadsl.Actor; +import akka.typed.javadsl.AskPattern; +import akka.util.Timeout; + +//#imports +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import scala.concurrent.Await; +import scala.concurrent.duration.Duration; + +public class IntroTest { + + //#hello-world-actor + public abstract static class HelloWorld { + //no instances of this class, it's only a name space for messages + // and static methods + private HelloWorld() { + } + + public static final class Greet{ + public final String whom; + public final ActorRef replyTo; + + public Greet(String whom, ActorRef replyTo) { + this.whom = whom; + this.replyTo = replyTo; + } + } + + public static final class Greeted { + public final String whom; + + public Greeted(String whom) { + this.whom = whom; + } + } + + public static final Behavior greeter = Actor.immutable((ctx, msg) -> { + System.out.println("Hello " + msg.whom + "!"); + msg.replyTo.tell(new Greeted(msg.whom)); + return Actor.same(); + }); + } + //#hello-world-actor + + public static void main(String[] args) { + //#hello-world + final ActorSystem system = + ActorSystem.create("hello", HelloWorld.greeter); + + final CompletionStage reply = + AskPattern.ask(system, + (ActorRef replyTo) -> new HelloWorld.Greet("world", replyTo), + new Timeout(3, TimeUnit.SECONDS), system.scheduler()); + + reply.thenAccept(greeting -> { + System.out.println("result: " + greeting.whom); + system.terminate(); + }); + //#hello-world + } + + //#chatroom-actor + public static class ChatRoom { + //#chatroom-protocol + static interface Command {} + public static final class GetSession implements Command { + public final String screenName; + public final ActorRef replyTo; + public GetSession(String screenName, ActorRef replyTo) { + this.screenName = screenName; + this.replyTo = replyTo; + } + } + //#chatroom-protocol + //#chatroom-behavior + private static final class PostSessionMessage implements Command { + public final String screenName; + public final String message; + public PostSessionMessage(String screenName, String message) { + this.screenName = screenName; + this.message = message; + } + } + //#chatroom-behavior + //#chatroom-protocol + + static interface SessionEvent {} + public static final class SessionGranted implements SessionEvent { + public final ActorRef handle; + public SessionGranted(ActorRef handle) { + this.handle = handle; + } + } + public static final class SessionDenied implements SessionEvent { + public final String reason; + public SessionDenied(String reason) { + this.reason = reason; + } + } + public static final class MessagePosted implements SessionEvent { + public final String screenName; + public final String message; + public MessagePosted(String screenName, String message) { + this.screenName = screenName; + this.message = message; + } + } + + public static final class PostMessage { + public final String message; + public PostMessage(String message) { + this.message = message; + } + } + //#chatroom-protocol + //#chatroom-behavior + + public static Behavior behavior() { + return chatRoom(new ArrayList>()); + } + + private static Behavior chatRoom(List> sessions) { + return Actor.immutable(Command.class) + .onMessage(GetSession.class, (ctx, getSession) -> { + ActorRef wrapper = ctx.spawnAdapter(p -> + new PostSessionMessage(getSession.screenName, p.message)); + getSession.replyTo.tell(new SessionGranted(wrapper)); + List> newSessions = + new ArrayList>(sessions); + newSessions.add(getSession.replyTo); + return chatRoom(newSessions); + }) + .onMessage(PostSessionMessage.class, (ctx, post) -> { + MessagePosted mp = new MessagePosted(post.screenName, post.message); + sessions.forEach(s -> s.tell(mp)); + return Actor.same(); + }) + .build(); + } + //#chatroom-behavior + + } + //#chatroom-actor + + //#chatroom-gabbler + public static abstract class Gabbler { + private Gabbler() { + } + + public static Behavior behavior() { + return Actor.immutable(ChatRoom.SessionEvent.class) + .onMessage(ChatRoom.SessionDenied.class, (ctx, msg) -> { + System.out.println("cannot start chat room session: " + msg.reason); + return Actor.stopped(); + }) + .onMessage(ChatRoom.SessionGranted.class, (ctx, msg) -> { + msg.handle.tell(new ChatRoom.PostMessage("Hello World!")); + return Actor.same(); + }) + .onMessage(ChatRoom.MessagePosted.class, (ctx, msg) -> { + System.out.println("message has been posted by '" + + msg.screenName +"': " + msg.message); + return Actor.stopped(); + }) + .build(); + } + + } + //#chatroom-gabbler + + public static void runChatRoom() throws Exception { + + //#chatroom-main + Behavior main = Actor.deferred(ctx -> { + ActorRef chatRoom = + ctx.spawn(ChatRoom.behavior(), "chatRoom"); + ActorRef gabbler = + ctx.spawn(Gabbler.behavior(), "gabbler"); + ctx.watch(gabbler); + chatRoom.tell(new ChatRoom.GetSession("ol’ Gabbler", gabbler)); + + return Actor.immutable(Void.class) + .onSignal(Terminated.class, (c, sig) -> Actor.stopped()) + .build(); + }); + + final ActorSystem system = + ActorSystem.create("ChatRoomDemo", main); + + Await.result(system.whenTerminated(), Duration.create(3, TimeUnit.SECONDS)); + //#chatroom-main + } + +} diff --git a/akka-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java b/akka-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java new file mode 100644 index 0000000000..a2bb502506 --- /dev/null +++ b/akka-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package jdocs.akka.typed; + +//#imports +import akka.typed.ActorRef; +import akka.typed.Behavior; +import akka.typed.javadsl.Actor; +import akka.typed.javadsl.Actor.Receive; +import akka.typed.javadsl.ActorContext; +//#imports +import java.util.ArrayList; +import java.util.List; + +public class MutableIntroTest { + + //#chatroom-actor + public static class ChatRoom { + //#chatroom-protocol + static interface Command {} + public static final class GetSession implements Command { + public final String screenName; + public final ActorRef replyTo; + public GetSession(String screenName, ActorRef replyTo) { + this.screenName = screenName; + this.replyTo = replyTo; + } + } + //#chatroom-protocol + //#chatroom-behavior + private static final class PostSessionMessage implements Command { + public final String screenName; + public final String message; + public PostSessionMessage(String screenName, String message) { + this.screenName = screenName; + this.message = message; + } + } + //#chatroom-behavior + //#chatroom-protocol + + static interface SessionEvent {} + public static final class SessionGranted implements SessionEvent { + public final ActorRef handle; + public SessionGranted(ActorRef handle) { + this.handle = handle; + } + } + public static final class SessionDenied implements SessionEvent { + public final String reason; + public SessionDenied(String reason) { + this.reason = reason; + } + } + public static final class MessagePosted implements SessionEvent { + public final String screenName; + public final String message; + public MessagePosted(String screenName, String message) { + this.screenName = screenName; + this.message = message; + } + } + + public static final class PostMessage { + public final String message; + public PostMessage(String message) { + this.message = message; + } + } + //#chatroom-protocol + //#chatroom-behavior + + public static Behavior behavior() { + return Actor.mutable(ChatRoomBehavior::new); + } + + public static class ChatRoomBehavior extends Actor.MutableBehavior { + final ActorContext ctx; + final List> sessions = new ArrayList>(); + + public ChatRoomBehavior(ActorContext ctx) { + this.ctx = ctx; + } + + @Override + public Receive createReceive() { + return receiveBuilder() + .onMessage(GetSession.class, getSession -> { + ActorRef wrapper = ctx.spawnAdapter(p -> + new PostSessionMessage(getSession.screenName, p.message)); + getSession.replyTo.tell(new SessionGranted(wrapper)); + sessions.add(getSession.replyTo); + return Actor.same(); + }) + .onMessage(PostSessionMessage.class, post -> { + MessagePosted mp = new MessagePosted(post.screenName, post.message); + sessions.forEach(s -> s.tell(mp)); + return this; + }) + .build(); + } + } + + //#chatroom-behavior + } + //#chatroom-actor + +} diff --git a/akka-typed/src/test/resources/application.conf b/akka-typed-tests/src/test/resources/application.conf similarity index 100% rename from akka-typed/src/test/resources/application.conf rename to akka-typed-tests/src/test/resources/application.conf diff --git a/akka-typed-tests/src/test/resources/reference.conf b/akka-typed-tests/src/test/resources/reference.conf new file mode 100644 index 0000000000..fd19764c9e --- /dev/null +++ b/akka-typed-tests/src/test/resources/reference.conf @@ -0,0 +1,4 @@ +akka.typed { + # for the akka.actor.ExtensionSpec + library-extensions += "akka.typed.InstanceCountingExtension" +} \ No newline at end of file diff --git a/akka-typed/src/test/scala/akka/typed/ActorContextSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/ActorContextSpec.scala similarity index 61% rename from akka-typed/src/test/scala/akka/typed/ActorContextSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/ActorContextSpec.scala index 93526d555e..b5b2667f4f 100644 --- a/akka-typed/src/test/scala/akka/typed/ActorContextSpec.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/ActorContextSpec.scala @@ -5,7 +5,6 @@ import scala.concurrent.Future import com.typesafe.config.ConfigFactory import akka.actor.DeadLetterSuppression import akka.typed.scaladsl.Actor -import akka.typed.scaladsl.Actor.Stateful object ActorContextSpec { @@ -73,171 +72,183 @@ object ActorContextSpec { final case class GetAdapter(replyTo: ActorRef[Adapter], name: String = "") extends Command final case class Adapter(a: ActorRef[Command]) extends Event - def subject(monitor: ActorRef[Monitor]): Behavior[Command] = - Actor.Stateful( - (ctx, message) ⇒ message match { + def subject(monitor: ActorRef[Monitor], ignorePostStop: Boolean): Behavior[Command] = + Actor.immutable[Command] { + (ctx, message) ⇒ + message match { + case ReceiveTimeout ⇒ + monitor ! GotReceiveTimeout + Actor.same + case Ping(replyTo) ⇒ + replyTo ! Pong1 + Actor.same + case Miss(replyTo) ⇒ + replyTo ! Missed + Actor.unhandled + case Renew(replyTo) ⇒ + replyTo ! Renewed + subject(monitor, ignorePostStop) + case Throw(ex) ⇒ + throw ex + case MkChild(name, mon, replyTo) ⇒ + val child = name match { + case None ⇒ ctx.spawnAnonymous(Actor.restarter[Throwable]().wrap(subject(mon, ignorePostStop))) + case Some(n) ⇒ ctx.spawn(Actor.restarter[Throwable]().wrap(subject(mon, ignorePostStop)), n) + } + replyTo ! Created(child) + Actor.same + case SetTimeout(d, replyTo) ⇒ + d match { + case f: FiniteDuration ⇒ ctx.setReceiveTimeout(f, ReceiveTimeout) + case _ ⇒ ctx.cancelReceiveTimeout() + } + replyTo ! TimeoutSet + Actor.same + case Schedule(delay, target, msg, replyTo) ⇒ + replyTo ! Scheduled + ctx.schedule(delay, target, msg) + Actor.same + case Stop ⇒ + Actor.stopped + case Kill(ref, replyTo) ⇒ + if (ctx.stop(ref)) replyTo ! Killed + else replyTo ! NotKilled + Actor.same + case Watch(ref, replyTo) ⇒ + ctx.watch(ref) + replyTo ! Watched + Actor.same + case Unwatch(ref, replyTo) ⇒ + ctx.unwatch(ref) + replyTo ! Unwatched + Actor.same + case GetInfo(replyTo) ⇒ + replyTo ! Info(ctx.self, ctx.system) + Actor.same + case GetChild(name, replyTo) ⇒ + replyTo ! Child(ctx.child(name)) + Actor.same + case GetChildren(replyTo) ⇒ + replyTo ! Children(ctx.children.toSet) + Actor.same + case BecomeInert(replyTo) ⇒ + replyTo ! BecameInert + Actor.immutable { + case (_, Ping(replyTo)) ⇒ + replyTo ! Pong2 + Actor.same + case (_, Throw(ex)) ⇒ + throw ex + case _ ⇒ Actor.unhandled + } + case BecomeCareless(replyTo) ⇒ + replyTo ! BecameCareless + Actor.immutable[Command] { + case (_, _) ⇒ Actor.unhandled + } onSignal { + case (_, PostStop) if ignorePostStop ⇒ Actor.same // ignore PostStop here + case (_, Terminated(_)) ⇒ Actor.unhandled + case (_, sig) ⇒ + monitor ! GotSignal(sig) + Actor.same + } + case GetAdapter(replyTo, name) ⇒ + replyTo ! Adapter(ctx.spawnAdapter(identity, name)) + Actor.same + } + } onSignal { + case (_, PostStop) if ignorePostStop ⇒ Actor.same // ignore PostStop here + case (ctx, signal) ⇒ monitor ! GotSignal(signal); Actor.same + } + + def oldSubject(monitor: ActorRef[Monitor], ignorePostStop: Boolean): Behavior[Command] = { + Actor.immutable[Command] { + case (ctx, message) ⇒ message match { case ReceiveTimeout ⇒ monitor ! GotReceiveTimeout - Actor.Same + Actor.same case Ping(replyTo) ⇒ replyTo ! Pong1 - Actor.Same + Actor.same case Miss(replyTo) ⇒ replyTo ! Missed - Actor.Unhandled + Actor.unhandled case Renew(replyTo) ⇒ replyTo ! Renewed - subject(monitor) + subject(monitor, ignorePostStop) case Throw(ex) ⇒ throw ex case MkChild(name, mon, replyTo) ⇒ val child = name match { - case None ⇒ ctx.spawnAnonymous(Actor.Restarter[Throwable]().wrap(subject(mon))) - case Some(n) ⇒ ctx.spawn(Actor.Restarter[Throwable]().wrap(subject(mon)), n) + case None ⇒ ctx.spawnAnonymous(Actor.restarter[Throwable]().wrap(subject(mon, ignorePostStop))) + case Some(n) ⇒ ctx.spawn(Actor.restarter[Throwable]().wrap(subject(mon, ignorePostStop)), n) } replyTo ! Created(child) - Actor.Same + Actor.same case SetTimeout(d, replyTo) ⇒ d match { case f: FiniteDuration ⇒ ctx.setReceiveTimeout(f, ReceiveTimeout) case _ ⇒ ctx.cancelReceiveTimeout() } replyTo ! TimeoutSet - Actor.Same + Actor.same case Schedule(delay, target, msg, replyTo) ⇒ replyTo ! Scheduled ctx.schedule(delay, target, msg) - Actor.Same - case Stop ⇒ Actor.Stopped + Actor.same + case Stop ⇒ + Actor.stopped case Kill(ref, replyTo) ⇒ if (ctx.stop(ref)) replyTo ! Killed else replyTo ! NotKilled - Actor.Same + Actor.same case Watch(ref, replyTo) ⇒ - ctx.watch[Nothing](ref) + ctx.watch(ref) replyTo ! Watched - Actor.Same + Actor.same case Unwatch(ref, replyTo) ⇒ - ctx.unwatch[Nothing](ref) + ctx.unwatch(ref) replyTo ! Unwatched - Actor.Same + Actor.same case GetInfo(replyTo) ⇒ replyTo ! Info(ctx.self, ctx.system) - Actor.Same + Actor.same case GetChild(name, replyTo) ⇒ replyTo ! Child(ctx.child(name)) - Actor.Same + Actor.same case GetChildren(replyTo) ⇒ replyTo ! Children(ctx.children.toSet) - Actor.Same + Actor.same case BecomeInert(replyTo) ⇒ replyTo ! BecameInert - Actor.Stateless { + Actor.immutable[Command] { case (_, Ping(replyTo)) ⇒ replyTo ! Pong2 + Actor.same case (_, Throw(ex)) ⇒ throw ex - case _ ⇒ () + case _ ⇒ Actor.same } case BecomeCareless(replyTo) ⇒ replyTo ! BecameCareless - Actor.Stateful( - (ctx, message) ⇒ Actor.Unhandled, - (ctx, signal) ⇒ signal match { - case Terminated(_) ⇒ Actor.Unhandled - case sig ⇒ - monitor ! GotSignal(sig) - Actor.Same - }) - case GetAdapter(replyTo, name) ⇒ - replyTo ! Adapter(ctx.spawnAdapter(identity, name)) - Actor.Same - }, - (ctx, signal) ⇒ { monitor ! GotSignal(signal); Actor.Same }) - - def oldSubject(monitor: ActorRef[Monitor]): Behavior[Command] = { - import ScalaDSL._ - FullTotal { - case Sig(ctx, signal) ⇒ - monitor ! GotSignal(signal) - Same - case Msg(ctx, message) ⇒ message match { - case ReceiveTimeout ⇒ - monitor ! GotReceiveTimeout - Same - case Ping(replyTo) ⇒ - replyTo ! Pong1 - Same - case Miss(replyTo) ⇒ - replyTo ! Missed - Unhandled - case Renew(replyTo) ⇒ - replyTo ! Renewed - subject(monitor) - case Throw(ex) ⇒ - throw ex - case MkChild(name, mon, replyTo) ⇒ - val child = name match { - case None ⇒ ctx.spawnAnonymous(patterns.Restarter[Command, Throwable](subject(mon), false)()) - case Some(n) ⇒ ctx.spawn(patterns.Restarter[Command, Throwable](subject(mon), false)(), n) - } - replyTo ! Created(child) - Same - case SetTimeout(d, replyTo) ⇒ - d match { - case f: FiniteDuration ⇒ ctx.setReceiveTimeout(f, ReceiveTimeout) - case _ ⇒ ctx.cancelReceiveTimeout() - } - replyTo ! TimeoutSet - Same - case Schedule(delay, target, msg, replyTo) ⇒ - replyTo ! Scheduled - ctx.schedule(delay, target, msg) - Same - case Stop ⇒ Stopped - case Kill(ref, replyTo) ⇒ - if (ctx.stop(ref)) replyTo ! Killed - else replyTo ! NotKilled - Same - case Watch(ref, replyTo) ⇒ - ctx.watch[Nothing](ref) - replyTo ! Watched - Same - case Unwatch(ref, replyTo) ⇒ - ctx.unwatch[Nothing](ref) - replyTo ! Unwatched - Same - case GetInfo(replyTo) ⇒ - replyTo ! Info(ctx.self, ctx.system) - Same - case GetChild(name, replyTo) ⇒ - replyTo ! Child(ctx.child(name)) - Same - case GetChildren(replyTo) ⇒ - replyTo ! Children(ctx.children.toSet) - Same - case BecomeInert(replyTo) ⇒ - replyTo ! BecameInert - Full { - case Msg(_, Ping(replyTo)) ⇒ - replyTo ! Pong2 - Same - case Msg(_, Throw(ex)) ⇒ - throw ex - case _ ⇒ Same - } - case BecomeCareless(replyTo) ⇒ - replyTo ! BecameCareless - Full { - case Sig(_, Terminated(_)) ⇒ Unhandled - case Sig(_, sig) ⇒ + Actor.immutable[Command] { + case _ ⇒ Actor.unhandled + } onSignal { + case (_, PostStop) if ignorePostStop ⇒ Actor.same // ignore PostStop here + case (_, Terminated(_)) ⇒ Actor.unhandled + case (_, sig) ⇒ monitor ! GotSignal(sig) - Same + Actor.same } case GetAdapter(replyTo, name) ⇒ replyTo ! Adapter(ctx.spawnAdapter(identity, name)) - Same + Actor.same } + } onSignal { + case (_, PostStop) if ignorePostStop ⇒ Actor.same // ignore PostStop here + case (_, signal) ⇒ + monitor ! GotSignal(signal) + Actor.same } } @@ -250,6 +261,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( | lifecycle = off | autoreceive = off | } + | typed.loggers = ["akka.typed.testkit.TestEventListener"] |}""".stripMargin)) { import ActorContextSpec._ @@ -264,7 +276,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( /** * The behavior against which to run all the tests. */ - def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] + def behavior(ctx: scaladsl.ActorContext[Event], ignorePostStop: Boolean): Behavior[Command] implicit def system: ActorSystem[TypedSpec.Command] @@ -272,16 +284,12 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( if (system eq nativeSystem) suite + "Native" else suite + "Adapted" - def setup(name: String, wrapper: Option[Actor.Restarter.Apply[_]] = None)( + def setup(name: String, wrapper: Option[Actor.Restarter[_]] = None, ignorePostStop: Boolean = true)( proc: (scaladsl.ActorContext[Event], StepWise.Steps[Event, ActorRef[Command]]) ⇒ StepWise.Steps[Event, _]): Future[TypedSpec.Status] = runTest(s"$mySuite-$name")(StepWise[Event] { (ctx, startWith) ⇒ - val props = wrapper.map(_.wrap(behavior(ctx))).getOrElse(behavior(ctx)) - val steps = - startWith.withKeepTraces(true)(ctx.spawn(props, "subject")) - .expectMessage(expectTimeout) { (msg, ref) ⇒ - msg should ===(GotSignal(PreStart)) - ref - } + val props = wrapper.map(_.wrap(behavior(ctx, ignorePostStop))).getOrElse(behavior(ctx, ignorePostStop)) + val steps = startWith.withKeepTraces(true)(ctx.spawn(props, "subject")) + proc(ctx, steps) }) @@ -299,11 +307,8 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( val s = startWith.keep { subj ⇒ subj ! MkChild(name, monitor, self) - }.expectMultipleMessages(expectTimeout, 2) { (msgs, subj) ⇒ - val child = msgs match { - case Created(child) :: ChildEvent(GotSignal(PreStart)) :: Nil ⇒ child - case ChildEvent(GotSignal(PreStart)) :: Created(child) :: Nil ⇒ child - } + }.expectMessage(expectTimeout) { (msg, subj) ⇒ + val Created(child) = msg (subj, child) } @@ -345,32 +350,30 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( } }) - def `01 must correctly wire the lifecycle hooks`(): Unit = sync(setup("ctx01", Some(Actor.Restarter[Throwable]())) { (ctx, startWith) ⇒ - val self = ctx.self - val ex = new Exception("KABOOM1") - startWith { subj ⇒ - val log = muteExpectedException[Exception]("KABOOM1", occurrences = 1) - subj ! Throw(ex) - (subj, log) - }.expectMessage(expectTimeout) { - case (msg, (subj, log)) ⇒ - msg should ===(GotSignal(PreRestart)) - log.assertDone(expectTimeout) - subj - }.expectMessage(expectTimeout) { (msg, subj) ⇒ - msg should ===(GotSignal(PreStart)) - ctx.stop(subj) - }.expectMessage(expectTimeout) { (msg, _) ⇒ - msg should ===(GotSignal(PostStop)) - } - }) + def `01 must correctly wire the lifecycle hooks`(): Unit = + sync(setup("ctx01", Some(Actor.restarter[Throwable]()), ignorePostStop = false) { (ctx, startWith) ⇒ + val self = ctx.self + val ex = new Exception("KABOOM1") + startWith { subj ⇒ + val log = muteExpectedException[Exception]("KABOOM1", occurrences = 1) + subj ! Throw(ex) + (subj, log) + }.expectMessage(expectTimeout) { + case (msg, (subj, log)) ⇒ + msg should ===(GotSignal(PreRestart)) + log.assertDone(expectTimeout) + ctx.stop(subj) + }.expectMessage(expectTimeout) { (msg, _) ⇒ + msg should ===(GotSignal(PostStop)) + } + }) - def `02 must not signal PostStop after voluntary termination`(): Unit = sync(setup("ctx02") { (ctx, startWith) ⇒ + def `02 must signal PostStop after voluntary termination`(): Unit = sync(setup("ctx02", ignorePostStop = false) { (ctx, startWith) ⇒ startWith.keep { subj ⇒ - ctx.watch(subj) stop(subj) - }.expectTermination(expectTimeout) { (t, subj) ⇒ - t.ref should ===(subj) + }.expectMessage(expectTimeout) { + case (msg, _) ⇒ + msg should ===(GotSignal(PostStop)) } }) @@ -382,11 +385,9 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( val log = muteExpectedException[Exception]("KABOOM2", occurrences = 1) child ! Throw(ex) (subj, child, log) - }.expectMultipleMessages(expectTimeout, 2) { - case (msgs, (subj, child, log)) ⇒ - msgs should ===( - ChildEvent(GotSignal(PreRestart)) :: - ChildEvent(GotSignal(PreStart)) :: Nil) + }.expectMessage(expectTimeout) { + case (msg, (subj, child, log)) ⇒ + msg should ===(ChildEvent(GotSignal(PreRestart))) log.assertDone(expectTimeout) child ! BecomeInert(self) // necessary to avoid PostStop/Terminated interference (subj, child) @@ -420,7 +421,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( } }) - def `05 must reset behavior upon Restart`(): Unit = sync(setup("ctx05", Some(Actor.Restarter[Exception]())) { (ctx, startWith) ⇒ + def `05 must reset behavior upon Restart`(): Unit = sync(setup("ctx05", Some(Actor.restarter[Exception]())) { (ctx, startWith) ⇒ val self = ctx.self val ex = new Exception("KABOOM05") startWith @@ -428,28 +429,25 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( .stimulate(_ ! Ping(self), _ ⇒ Pong2) { subj ⇒ val log = muteExpectedException[Exception]("KABOOM05") subj ! Throw(ex) - (subj, log) - }.expectMessage(expectTimeout) { - case (msg, (subj, log)) ⇒ - msg should ===(GotSignal(PreStart)) - log.assertDone(expectTimeout) - subj + subj } .stimulate(_ ! Ping(self), _ ⇒ Pong1) }) - def `06 must not reset behavior upon Resume`(): Unit = sync(setup("ctx06", Some(Actor.Restarter[Exception](resume = true))) { (ctx, startWith) ⇒ - val self = ctx.self - val ex = new Exception("KABOOM06") - startWith - .stimulate(_ ! BecomeInert(self), _ ⇒ BecameInert) - .stimulate(_ ! Ping(self), _ ⇒ Pong2).keep { subj ⇒ - muteExpectedException[Exception]("KABOOM06", occurrences = 1) - subj ! Throw(ex) - }.stimulate(_ ! Ping(self), _ ⇒ Pong2) - }) + def `06 must not reset behavior upon Resume`(): Unit = sync(setup( + "ctx06", + Some(Actor.restarter[Exception](SupervisorStrategy.resume))) { (ctx, startWith) ⇒ + val self = ctx.self + val ex = new Exception("KABOOM06") + startWith + .stimulate(_ ! BecomeInert(self), _ ⇒ BecameInert) + .stimulate(_ ! Ping(self), _ ⇒ Pong2).keep { subj ⇒ + muteExpectedException[Exception]("KABOOM06", occurrences = 1) + subj ! Throw(ex) + }.stimulate(_ ! Ping(self), _ ⇒ Pong2) + }) - def `07 must stop upon Stop`(): Unit = sync(setup("ctx07") { (ctx, startWith) ⇒ + def `07 must stop upon Stop`(): Unit = sync(setup("ctx07", ignorePostStop = false) { (ctx, startWith) ⇒ val self = ctx.self val ex = new Exception("KABOOM07") startWith @@ -464,11 +462,9 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( def `08 must not stop non-child actor`(): Unit = sync(setup("ctx08") { (ctx, startWith) ⇒ val self = ctx.self - startWith.mkChild(Some("A"), ctx.spawnAdapter(ChildEvent), self) { pair ⇒ - (pair._1, pair._2, ctx.spawn(behavior(ctx), "A")) - }.expectMessage(expectTimeout) { - case (msg, (subj, child, other)) ⇒ - msg should ===(GotSignal(PreStart)) + startWith.mkChild(Some("A"), ctx.spawnAdapter(ChildEvent), self) { + case (subj, child) ⇒ + val other = ctx.spawn(behavior(ctx, ignorePostStop = true), "A") subj ! Kill(other, ctx.self) child }.expectMessageKeep(expectTimeout) { (msg, _) ⇒ @@ -526,7 +522,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( } }) - def `13 must terminate upon not handling Terminated`(): Unit = sync(setup("ctx13") { (ctx, startWith) ⇒ + def `13 must terminate upon not handling Terminated`(): Unit = sync(setup("ctx13", ignorePostStop = false) { (ctx, startWith) ⇒ val self = ctx.self startWith.mkChild(None, ctx.spawnAdapter(ChildEvent), self).keep { case (subj, child) ⇒ @@ -542,6 +538,9 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( child ! Stop }.expectMessage(expectTimeout) { case (msg, (subj, child)) ⇒ + msg should ===(ChildEvent(GotSignal(PostStop))) + }.expectMessage(expectTimeout) { + case (msg, _) ⇒ msg should ===(GotSignal(PostStop)) } }) @@ -590,7 +589,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( } }) - def `40 must create a working adapter`(): Unit = sync(setup("ctx40") { (ctx, startWith) ⇒ + def `40 must create a working adapter`(): Unit = sync(setup("ctx40", ignorePostStop = false) { (ctx, startWith) ⇒ startWith.keep { subj ⇒ subj ! GetAdapter(ctx.self) }.expectMessage(expectTimeout) { (msg, subj) ⇒ @@ -620,8 +619,8 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( trait Normal extends Tests { override def suite = "normal" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - subject(ctx.self) + override def behavior(ctx: scaladsl.ActorContext[Event], ignorePostStop: Boolean): Behavior[Command] = + subject(ctx.self, ignorePostStop) } object `An ActorContext (native)` extends Normal with NativeSystem object `An ActorContext (adapted)` extends Normal with AdaptedSystem @@ -629,105 +628,34 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( trait Widened extends Tests { import Actor._ override def suite = "widened" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - subject(ctx.self).widen { case x ⇒ x } + override def behavior(ctx: scaladsl.ActorContext[Event], ignorePostStop: Boolean): Behavior[Command] = + subject(ctx.self, ignorePostStop).widen { case x ⇒ x } } object `An ActorContext with widened Behavior (native)` extends Widened with NativeSystem object `An ActorContext with widened Behavior (adapted)` extends Widened with AdaptedSystem trait Deferred extends Tests { override def suite = "deferred" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - Actor.Deferred(_ ⇒ subject(ctx.self)) + override def behavior(ctx: scaladsl.ActorContext[Event], ignorePostStop: Boolean): Behavior[Command] = + Actor.deferred(_ ⇒ subject(ctx.self, ignorePostStop)) } object `An ActorContext with deferred Behavior (native)` extends Deferred with NativeSystem object `An ActorContext with deferred Behavior (adapted)` extends Deferred with AdaptedSystem + trait NestedDeferred extends Tests { + override def suite = "deferred" + override def behavior(ctx: scaladsl.ActorContext[Event], ignorePostStop: Boolean): Behavior[Command] = + Actor.deferred(_ ⇒ Actor.deferred(_ ⇒ subject(ctx.self, ignorePostStop))) + } + object `An ActorContext with nested deferred Behavior (native)` extends NestedDeferred with NativeSystem + object `An ActorContext with nested deferred Behavior (adapted)` extends NestedDeferred with AdaptedSystem + trait Tap extends Tests { override def suite = "tap" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - Actor.Tap((_, _) ⇒ (), (_, _) ⇒ (), subject(ctx.self)) + override def behavior(ctx: scaladsl.ActorContext[Event], ignorePostStop: Boolean): Behavior[Command] = + Actor.tap((_, _) ⇒ (), (_, _) ⇒ (), subject(ctx.self, ignorePostStop)) } object `An ActorContext with Tap (old-native)` extends Tap with NativeSystem object `An ActorContext with Tap (old-adapted)` extends Tap with AdaptedSystem - trait NormalOld extends Tests { - override def suite = "basic" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - oldSubject(ctx.self) - } - object `An ActorContext (old-native)` extends NormalOld with NativeSystem - object `An ActorContext (old-adapted)` extends NormalOld with AdaptedSystem - - trait WidenedOld extends Tests { - import ScalaDSL._ - override def suite = "widened" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - oldSubject(ctx.self).widen { case x ⇒ x } - } - object `An ActorContext with widened Behavior (old-native)` extends WidenedOld with NativeSystem - object `An ActorContext with widened Behavior (old-adapted)` extends WidenedOld with AdaptedSystem - - trait SynchronousSelfOld extends Tests { - override def suite = "synchronous" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = ScalaDSL.SynchronousSelf(self ⇒ oldSubject(ctx.self)) - } - object `An ActorContext with SynchronousSelf (old-native)` extends SynchronousSelfOld with NativeSystem - object `An ActorContext with SynchronousSelf (old-adapted)` extends SynchronousSelfOld with AdaptedSystem - - trait NonMatchingTapOld extends Tests { - override def suite = "TapNonMatch" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = ScalaDSL.Tap({ case null ⇒ }, oldSubject(ctx.self)) - } - object `An ActorContext with non-matching Tap (old-native)` extends NonMatchingTapOld with NativeSystem - object `An ActorContext with non-matching Tap (old-adapted)` extends NonMatchingTapOld with AdaptedSystem - - trait MatchingTapOld extends Tests { - override def suite = "TapMatch" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = ScalaDSL.Tap({ case _ ⇒ }, oldSubject(ctx.self)) - } - object `An ActorContext with matching Tap (old-native)` extends MatchingTapOld with NativeSystem - object `An ActorContext with matching Tap (old-adapted)` extends MatchingTapOld with AdaptedSystem - - private val stoppingBehavior = ScalaDSL.Full[Command] { case ScalaDSL.Msg(_, Stop) ⇒ ScalaDSL.Stopped } - - trait AndLeftOld extends Tests { - override def suite = "andLeft" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - ScalaDSL.And(oldSubject(ctx.self), stoppingBehavior) - } - object `An ActorContext with And (left, native)` extends AndLeftOld with NativeSystem - object `An ActorContext with And (left, adapted)` extends AndLeftOld with AdaptedSystem - - trait AndRightOld extends Tests { - override def suite = "andRight" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - ScalaDSL.And(stoppingBehavior, oldSubject(ctx.self)) - } - object `An ActorContext with And (right, native)` extends AndRightOld with NativeSystem - object `An ActorContext with And (right, adapted)` extends AndRightOld with AdaptedSystem - - trait OrLeftOld extends Tests { - override def suite = "orLeft" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - ScalaDSL.Or(oldSubject(ctx.self), stoppingBehavior) - override def stop(ref: ActorRef[Command]) = { - ref ! Stop - ref ! Stop - } - } - object `An ActorContext with Or (left, native)` extends OrLeftOld with NativeSystem - object `An ActorContext with Or (left, adapted)` extends OrLeftOld with AdaptedSystem - - trait OrRightOld extends Tests { - override def suite = "orRight" - override def behavior(ctx: scaladsl.ActorContext[Event]): Behavior[Command] = - ScalaDSL.Or(stoppingBehavior, oldSubject(ctx.self)) - override def stop(ref: ActorRef[Command]) = { - ref ! Stop - ref ! Stop - } - } - object `An ActorContext with Or (right, native)` extends OrRightOld with NativeSystem - object `An ActorContext with Or (right, adapted)` extends OrRightOld with AdaptedSystem } diff --git a/akka-typed/src/test/scala/akka/typed/AskSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/AskSpec.scala similarity index 84% rename from akka-typed/src/test/scala/akka/typed/AskSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/AskSpec.scala index 4a124e287d..9233495585 100644 --- a/akka-typed/src/test/scala/akka/typed/AskSpec.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/AskSpec.scala @@ -5,13 +5,10 @@ package akka.typed import scala.concurrent.ExecutionContext import scala.concurrent.duration._ - import org.scalatest.concurrent.ScalaFutures - import akka.util.Timeout import akka.pattern.AskTimeoutException - -import ScalaDSL._ +import akka.typed.scaladsl.Actor._ import akka.typed.scaladsl.AskPattern._ object AskSpec { @@ -32,13 +29,13 @@ class AskSpec extends TypedSpec with ScalaFutures { implicit def executor: ExecutionContext = system.executionContext - val behavior: Behavior[Msg] = Total[Msg] { - case foo @ Foo(_) ⇒ + val behavior: Behavior[Msg] = immutable[Msg] { + case (_, foo: Foo) ⇒ foo.replyTo ! "foo" - Same - case Stop(r) ⇒ + same + case (_, Stop(r)) ⇒ r ! (()) - Stopped + stopped } def `must fail the future if the actor is already terminated`(): Unit = { @@ -68,8 +65,9 @@ class AskSpec extends TypedSpec with ScalaFutures { /** See issue #19947 (MatchError with adapted ActorRef) */ def `must fail the future if the actor doesn't exist`(): Unit = { val noSuchActor: ActorRef[Msg] = system match { - case adaptedSys: adapter.ActorSystemAdapter[_] ⇒ - adapter.actorRefAdapter(adaptedSys.untyped.provider.resolveActorRef("/foo/bar")) + case adaptedSys: akka.typed.internal.adapter.ActorSystemAdapter[_] ⇒ + import akka.typed.scaladsl.adapter._ + adaptedSys.untyped.provider.resolveActorRef("/foo/bar") case _ ⇒ fail("this test must only run in an adapted actor system") } diff --git a/akka-typed-tests/src/test/scala/akka/typed/BehaviorSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/BehaviorSpec.scala new file mode 100644 index 0000000000..3b2adaf8a9 --- /dev/null +++ b/akka-typed-tests/src/test/scala/akka/typed/BehaviorSpec.scala @@ -0,0 +1,620 @@ +/** + * Copyright (C) 2014-2017 Lightbend Inc. + */ +package akka.typed + +import akka.typed.scaladsl.{ Actor ⇒ SActor } +import akka.typed.javadsl.{ Actor ⇒ JActor, ActorContext ⇒ JActorContext } +import akka.japi.function.{ Function ⇒ F1e, Function2 ⇒ F2, Procedure2 ⇒ P2 } +import akka.japi.pf.{ FI, PFBuilder } +import java.util.function.{ Function ⇒ F1 } + +import akka.Done +import akka.typed.testkit.{ EffectfulActorContext, Inbox } + +class BehaviorSpec extends TypedSpec { + + sealed trait Command { + def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Nil + } + case object GetSelf extends Command { + override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Self(ctx.asScala.self) :: Nil + } + // Behavior under test must return Unhandled + case object Miss extends Command { + override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Missed :: Nil + } + // Behavior under test must return same + case object Ignore extends Command { + override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Ignored :: Nil + } + case object Ping extends Command { + override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Pong :: Nil + } + case object Swap extends Command { + override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Swapped :: Nil + } + case class GetState()(s: State) extends Command { + override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = s :: Nil + } + case class AuxPing(id: Int) extends Command { + override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Pong :: Nil + } + case object Stop extends Command + + sealed trait Event + case class GotSignal(signal: Signal) extends Event + case class Self(self: ActorRef[Command]) extends Event + case object Missed extends Event + case object Ignored extends Event + case object Pong extends Event + case object Swapped extends Event + + sealed trait State extends Event { def next: State } + val StateA: State = new State { override def toString = "StateA"; override def next = StateB } + val StateB: State = new State { override def toString = "StateB"; override def next = StateA } + + trait Common { + type Aux >: Null <: AnyRef + def system: ActorSystem[TypedSpec.Command] + def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) + def checkAux(signal: Signal, aux: Aux): Unit = () + def checkAux(command: Command, aux: Aux): Unit = () + + case class Init(behv: Behavior[Command], inbox: Inbox[Event], aux: Aux) { + def mkCtx(): Setup = { + val ctx = new EffectfulActorContext("ctx", behv, 1000, system) + val msgs = inbox.receiveAll() + Setup(ctx, inbox, aux) + } + } + case class Setup(ctx: EffectfulActorContext[Command], inbox: Inbox[Event], aux: Aux) + + def init(): Init = { + val inbox = Inbox[Event]("evt") + val (behv, aux) = behavior(inbox.ref) + Init(behv, inbox, aux) + } + + def init(factory: ActorRef[Event] ⇒ (Behavior[Command], Aux)): Init = { + val inbox = Inbox[Event]("evt") + val (behv, aux) = factory(inbox.ref) + Init(behv, inbox, aux) + } + + def mkCtx(requirePreStart: Boolean = false): Setup = + init().mkCtx() + + implicit class Check(val setup: Setup) { + def check(signal: Signal): Setup = { + setup.ctx.signal(signal) + setup.inbox.receiveAll() should ===(GotSignal(signal) :: Nil) + checkAux(signal, setup.aux) + setup + } + def check(command: Command): Setup = { + setup.ctx.run(command) + setup.inbox.receiveAll() should ===(command.expectedResponse(setup.ctx)) + checkAux(command, setup.aux) + setup + } + def check2(command: Command): Setup = { + setup.ctx.run(command) + val expected = command.expectedResponse(setup.ctx) + setup.inbox.receiveAll() should ===(expected ++ expected) + checkAux(command, setup.aux) + setup + } + } + + val ex = new Exception("mine!") + } + + trait Siphon extends Common { + override type Aux = Inbox[Command] + + override def checkAux(command: Command, aux: Aux): Unit = { + aux.receiveAll() should ===(command :: Nil) + } + } + + trait SignalSiphon extends Common { + override type Aux = Inbox[Either[Signal, Command]] + + override def checkAux(command: Command, aux: Aux): Unit = { + aux.receiveAll() should ===(Right(command) :: Nil) + } + + override def checkAux(signal: Signal, aux: Aux): Unit = { + aux.receiveAll() should ===(Left(signal) :: Nil) + } + } + + trait Lifecycle extends Common { + def `must react to PreStart`(): Unit = { + mkCtx(requirePreStart = true) + } + + def `must react to PostStop`(): Unit = { + mkCtx().check(PostStop) + } + + def `must react to PostStop after a message`(): Unit = { + mkCtx().check(GetSelf).check(PostStop) + } + + def `must react to PreRestart`(): Unit = { + mkCtx().check(PreRestart) + } + + def `must react to PreRestart after a message`(): Unit = { + mkCtx().check(GetSelf).check(PreRestart) + } + + def `must react to Terminated`(): Unit = { + mkCtx().check(Terminated(Inbox("x").ref)(null)) + } + + def `must react to Terminated after a message`(): Unit = { + mkCtx().check(GetSelf).check(Terminated(Inbox("x").ref)(null)) + } + + def `must react to a message after Terminated`(): Unit = { + mkCtx().check(Terminated(Inbox("x").ref)(null)).check(GetSelf) + } + } + + trait Messages extends Common { + def `must react to two messages`(): Unit = { + mkCtx().check(Ping).check(Ping) + } + + def `must react to a message after missing one`(): Unit = { + mkCtx().check(Miss).check(Ping) + } + + def `must react to a message after ignoring one`(): Unit = { + mkCtx().check(Ignore).check(Ping) + } + } + + trait Unhandled extends Common { + def `must return Unhandled`(): Unit = { + val Setup(ctx, inbox, aux) = mkCtx() + Behavior.interpretMessage(ctx.currentBehavior, ctx, Miss) should be(Behavior.UnhandledBehavior) + inbox.receiveAll() should ===(Missed :: Nil) + checkAux(Miss, aux) + } + } + + trait Stoppable extends Common { + def `must stop`(): Unit = { + val Setup(ctx, inbox, aux) = mkCtx() + ctx.run(Stop) + ctx.currentBehavior should be(Behavior.StoppedBehavior) + checkAux(Stop, aux) + } + } + + trait Become extends Common with Unhandled { + private implicit val inbox = Inbox[State]("state") + + def `must be in state A`(): Unit = { + mkCtx().check(GetState()(StateA)) + } + + def `must switch to state B`(): Unit = { + mkCtx().check(Swap).check(GetState()(StateB)) + } + + def `must switch back to state A`(): Unit = { + mkCtx().check(Swap).check(Swap).check(GetState()(StateA)) + } + } + + trait BecomeWithLifecycle extends Become with Lifecycle { + def `must react to PostStop after swap`(): Unit = { + mkCtx().check(Swap).check(PostStop) + } + + def `must react to PostStop after a message after swap`(): Unit = { + mkCtx().check(Swap).check(GetSelf).check(PostStop) + } + + def `must react to PreRestart after swap`(): Unit = { + mkCtx().check(Swap).check(PreRestart) + } + + def `must react to PreRestart after a message after swap`(): Unit = { + mkCtx().check(Swap).check(GetSelf).check(PreRestart) + } + + def `must react to Terminated after swap`(): Unit = { + mkCtx().check(Swap).check(Terminated(Inbox("x").ref)(null)) + } + + def `must react to Terminated after a message after swap`(): Unit = { + mkCtx().check(Swap).check(GetSelf).check(Terminated(Inbox("x").ref)(null)) + } + + def `must react to a message after Terminated after swap`(): Unit = { + mkCtx().check(Swap).check(Terminated(Inbox("x").ref)(null)).check(GetSelf) + } + } + + /** + * This targets behavior wrappers to ensure that the wrapper does not + * hold on to the changed behavior. Wrappers must be immutable. + */ + trait Reuse extends Common { + def `must be reusable`(): Unit = { + val i = init() + i.mkCtx().check(GetState()(StateA)).check(Swap).check(GetState()(StateB)) + i.mkCtx().check(GetState()(StateA)).check(Swap).check(GetState()(StateB)) + } + } + + private def mkFull(monitor: ActorRef[Event], state: State = StateA): Behavior[Command] = { + SActor.immutable[Command] { + case (ctx, GetSelf) ⇒ + monitor ! Self(ctx.self) + SActor.same + case (ctx, Miss) ⇒ + monitor ! Missed + SActor.unhandled + case (ctx, Ignore) ⇒ + monitor ! Ignored + SActor.same + case (ctx, Ping) ⇒ + monitor ! Pong + mkFull(monitor, state) + case (ctx, Swap) ⇒ + monitor ! Swapped + mkFull(monitor, state.next) + case (ctx, GetState()) ⇒ + monitor ! state + SActor.same + case (ctx, Stop) ⇒ SActor.stopped + case (_, _) ⇒ SActor.unhandled + } onSignal { + case (ctx, signal) ⇒ + monitor ! GotSignal(signal) + SActor.same + } + } + + trait FullBehavior extends Messages with BecomeWithLifecycle with Stoppable { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = mkFull(monitor) → null + } + object `A Full Behavior (native)` extends FullBehavior with NativeSystem + object `A Full Behavior (adapted)` extends FullBehavior with AdaptedSystem + + trait ImmutableBehavior extends Messages with BecomeWithLifecycle with Stoppable { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor, StateA) → null + private def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = { + SActor.immutable[Command] { + case (ctx, GetSelf) ⇒ + monitor ! Self(ctx.self) + SActor.same + case (_, Miss) ⇒ + monitor ! Missed + SActor.unhandled + case (_, Ignore) ⇒ + monitor ! Ignored + SActor.same + case (_, Ping) ⇒ + monitor ! Pong + behv(monitor, state) + case (_, Swap) ⇒ + monitor ! Swapped + behv(monitor, state.next) + case (_, GetState()) ⇒ + monitor ! state + SActor.same + case (_, Stop) ⇒ SActor.stopped + case (_, _: AuxPing) ⇒ SActor.unhandled + } onSignal { + case (ctx, signal) ⇒ + monitor ! GotSignal(signal) + SActor.same + } + } + } + object `A immutable Behavior (native)` extends ImmutableBehavior with NativeSystem + object `A immutable Behavior (adapted)` extends ImmutableBehavior with AdaptedSystem + + trait ImmutableWithSignalScalaBehavior extends Messages with BecomeWithLifecycle with Stoppable { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor) → null + def behv(monitor: ActorRef[Event], state: State = StateA): Behavior[Command] = + SActor.immutable[Command] { + (ctx, msg) ⇒ + msg match { + case GetSelf ⇒ + monitor ! Self(ctx.self) + SActor.same + case Miss ⇒ + monitor ! Missed + SActor.unhandled + case Ignore ⇒ + monitor ! Ignored + SActor.same + case Ping ⇒ + monitor ! Pong + behv(monitor, state) + case Swap ⇒ + monitor ! Swapped + behv(monitor, state.next) + case GetState() ⇒ + monitor ! state + SActor.same + case Stop ⇒ SActor.stopped + case _: AuxPing ⇒ SActor.unhandled + } + } onSignal { + case (ctx, sig) ⇒ + monitor ! GotSignal(sig) + SActor.same + } + } + object `A ImmutableWithSignal Behavior (scala,native)` extends ImmutableWithSignalScalaBehavior with NativeSystem + object `A ImmutableWithSignal Behavior (scala,adapted)` extends ImmutableWithSignalScalaBehavior with AdaptedSystem + + trait ImmutableScalaBehavior extends Messages with Become with Stoppable { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor, StateA) → null + def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = + SActor.immutable[Command] { (ctx, msg) ⇒ + msg match { + case GetSelf ⇒ + monitor ! Self(ctx.self) + SActor.same + case Miss ⇒ + monitor ! Missed + SActor.unhandled + case Ignore ⇒ + monitor ! Ignored + SActor.same + case Ping ⇒ + monitor ! Pong + behv(monitor, state) + case Swap ⇒ + monitor ! Swapped + behv(monitor, state.next) + case GetState() ⇒ + monitor ! state + SActor.same + case Stop ⇒ SActor.stopped + case _: AuxPing ⇒ SActor.unhandled + } + } + } + object `A immutable Behavior (scala,native)` extends ImmutableScalaBehavior with NativeSystem + object `A immutable Behavior (scala,adapted)` extends ImmutableScalaBehavior with AdaptedSystem + + trait MutableScalaBehavior extends Messages with Become with Stoppable { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor) → null + def behv(monitor: ActorRef[Event]): Behavior[Command] = + SActor.mutable[Command] { ctx ⇒ + new SActor.MutableBehavior[Command] { + private var state: State = StateA + + override def onMessage(msg: Command): Behavior[Command] = { + msg match { + case GetSelf ⇒ + monitor ! Self(ctx.self) + this + case Miss ⇒ + monitor ! Missed + SActor.unhandled + case Ignore ⇒ + monitor ! Ignored + SActor.same // this or same works the same way + case Ping ⇒ + monitor ! Pong + this + case Swap ⇒ + monitor ! Swapped + state = state.next + this + case GetState() ⇒ + monitor ! state + this + case Stop ⇒ SActor.stopped + case _: AuxPing ⇒ SActor.unhandled + } + } + } + } + } + object `A mutable Behavior (scala,native)` extends MutableScalaBehavior with NativeSystem + object `A mutable Behavior (scala,adapted)` extends MutableScalaBehavior with AdaptedSystem + + trait WidenedScalaBehavior extends ImmutableWithSignalScalaBehavior with Reuse with Siphon { + import SActor.BehaviorDecorators + + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { + val inbox = Inbox[Command]("widenedListener") + super.behavior(monitor)._1.widen[Command] { case c ⇒ inbox.ref ! c; c } → inbox + } + } + object `A widened Behavior (scala,native)` extends WidenedScalaBehavior with NativeSystem + object `A widened Behavior (scala,adapted)` extends WidenedScalaBehavior with AdaptedSystem + + trait DeferredScalaBehavior extends ImmutableWithSignalScalaBehavior { + override type Aux = Inbox[Done] + + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { + val inbox = Inbox[Done]("deferredListener") + (SActor.deferred(ctx ⇒ { + inbox.ref ! Done + super.behavior(monitor)._1 + }), inbox) + } + + override def checkAux(signal: Signal, aux: Aux): Unit = + aux.receiveAll() should ===(Done :: Nil) + } + object `A deferred Behavior (scala,native)` extends DeferredScalaBehavior with NativeSystem + object `A deferred Behavior (scala,adapted)` extends DeferredScalaBehavior with AdaptedSystem + + trait TapScalaBehavior extends ImmutableWithSignalScalaBehavior with Reuse with SignalSiphon { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { + val inbox = Inbox[Either[Signal, Command]]("tapListener") + (SActor.tap((_, msg) ⇒ inbox.ref ! Right(msg), (_, sig) ⇒ inbox.ref ! Left(sig), super.behavior(monitor)._1), inbox) + } + } + object `A tap Behavior (scala,native)` extends TapScalaBehavior with NativeSystem + object `A tap Behavior (scala,adapted)` extends TapScalaBehavior with AdaptedSystem + + trait RestarterScalaBehavior extends ImmutableWithSignalScalaBehavior with Reuse { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { + SActor.restarter[Exception]().wrap(super.behavior(monitor)._1) → null + } + } + object `A restarter Behavior (scala,native)` extends RestarterScalaBehavior with NativeSystem + object `A restarter Behavior (scala,adapted)` extends RestarterScalaBehavior with AdaptedSystem + + /* + * function converters for Java, to ease the pain on Scala 2.11 + */ + def fs(f: (JActorContext[Command], Signal) ⇒ Behavior[Command]) = + new F2[JActorContext[Command], Signal, Behavior[Command]] { + override def apply(ctx: JActorContext[Command], sig: Signal) = f(ctx, sig) + } + def fc(f: (JActorContext[Command], Command) ⇒ Behavior[Command]) = + new F2[JActorContext[Command], Command, Behavior[Command]] { + override def apply(ctx: JActorContext[Command], command: Command) = f(ctx, command) + } + def ps(f: (JActorContext[Command], Signal) ⇒ Unit) = + new P2[JActorContext[Command], Signal] { + override def apply(ctx: JActorContext[Command], sig: Signal) = f(ctx, sig) + } + def pc(f: (JActorContext[Command], Command) ⇒ Unit) = + new P2[JActorContext[Command], Command] { + override def apply(ctx: JActorContext[Command], command: Command) = f(ctx, command) + } + def pf(f: PFBuilder[Command, Command] ⇒ PFBuilder[Command, Command]) = + new F1[PFBuilder[Command, Command], PFBuilder[Command, Command]] { + override def apply(in: PFBuilder[Command, Command]) = f(in) + } + def fi(f: Command ⇒ Command) = + new FI.Apply[Command, Command] { + override def apply(in: Command) = f(in) + } + def df(f: JActorContext[Command] ⇒ Behavior[Command]) = + new F1e[JActorContext[Command], Behavior[Command]] { + override def apply(in: JActorContext[Command]) = f(in) + } + + trait ImmutableWithSignalJavaBehavior extends Messages with BecomeWithLifecycle with Stoppable { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor) → null + def behv(monitor: ActorRef[Event], state: State = StateA): Behavior[Command] = + JActor.immutable( + fc((ctx, msg) ⇒ msg match { + case GetSelf ⇒ + monitor ! Self(ctx.getSelf) + SActor.same + case Miss ⇒ + monitor ! Missed + SActor.unhandled + case Ignore ⇒ + monitor ! Ignored + SActor.same + case Ping ⇒ + monitor ! Pong + behv(monitor, state) + case Swap ⇒ + monitor ! Swapped + behv(monitor, state.next) + case GetState() ⇒ + monitor ! state + SActor.same + case Stop ⇒ SActor.stopped + case _: AuxPing ⇒ SActor.unhandled + }), + fs((ctx, sig) ⇒ { + monitor ! GotSignal(sig) + SActor.same + })) + } + object `A ImmutableWithSignal Behavior (java,native)` extends ImmutableWithSignalJavaBehavior with NativeSystem + object `A ImmutableWithSignal Behavior (java,adapted)` extends ImmutableWithSignalJavaBehavior with AdaptedSystem + + trait ImmutableJavaBehavior extends Messages with Become with Stoppable { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor, StateA) → null + def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = + JActor.immutable { + fc((ctx, msg) ⇒ + msg match { + case GetSelf ⇒ + monitor ! Self(ctx.getSelf) + SActor.same + case Miss ⇒ + monitor ! Missed + SActor.unhandled + case Ignore ⇒ + monitor ! Ignored + SActor.same + case Ping ⇒ + monitor ! Pong + behv(monitor, state) + case Swap ⇒ + monitor ! Swapped + behv(monitor, state.next) + case GetState() ⇒ + monitor ! state + SActor.same + case Stop ⇒ SActor.stopped + case _: AuxPing ⇒ SActor.unhandled + }) + } + } + object `A immutable Behavior (java,native)` extends ImmutableJavaBehavior with NativeSystem + object `A immutable Behavior (java,adapted)` extends ImmutableJavaBehavior with AdaptedSystem + + trait WidenedJavaBehavior extends ImmutableWithSignalJavaBehavior with Reuse with Siphon { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { + val inbox = Inbox[Command]("widenedListener") + JActor.widened(super.behavior(monitor)._1, pf(_.`match`(classOf[Command], fi(x ⇒ { inbox.ref ! x; x })))) → inbox + } + } + object `A widened Behavior (java,native)` extends WidenedJavaBehavior with NativeSystem + object `A widened Behavior (java,adapted)` extends WidenedJavaBehavior with AdaptedSystem + + trait DeferredJavaBehavior extends ImmutableWithSignalJavaBehavior { + override type Aux = Inbox[Done] + + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { + val inbox = Inbox[Done]("deferredListener") + (JActor.deferred(df(ctx ⇒ { + inbox.ref ! Done + super.behavior(monitor)._1 + })), inbox) + } + + override def checkAux(signal: Signal, aux: Aux): Unit = + aux.receiveAll() should ===(Done :: Nil) + } + object `A deferred Behavior (java,native)` extends DeferredJavaBehavior with NativeSystem + object `A deferred Behavior (java,adapted)` extends DeferredJavaBehavior with AdaptedSystem + + trait TapJavaBehavior extends ImmutableWithSignalJavaBehavior with Reuse with SignalSiphon { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { + val inbox = Inbox[Either[Signal, Command]]("tapListener") + (JActor.tap( + pc((_, msg) ⇒ inbox.ref ! Right(msg)), + ps((_, sig) ⇒ inbox.ref ! Left(sig)), + super.behavior(monitor)._1), inbox) + } + } + object `A tap Behavior (java,native)` extends TapJavaBehavior with NativeSystem + object `A tap Behavior (java,adapted)` extends TapJavaBehavior with AdaptedSystem + + trait RestarterJavaBehavior extends ImmutableWithSignalJavaBehavior with Reuse { + override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { + JActor.restarter(classOf[Exception], SupervisorStrategy.restart, super.behavior(monitor)._1) → null + } + } + object `A restarter Behavior (java,native)` extends RestarterJavaBehavior with NativeSystem + object `A restarter Behavior (java,adapted)` extends RestarterJavaBehavior with AdaptedSystem + +} diff --git a/akka-typed-tests/src/test/scala/akka/typed/DeferredSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/DeferredSpec.scala new file mode 100644 index 0000000000..1590ada735 --- /dev/null +++ b/akka-typed-tests/src/test/scala/akka/typed/DeferredSpec.scala @@ -0,0 +1,167 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed + +import scala.concurrent.Await +import scala.concurrent.duration._ +import scala.util.control.NoStackTrace +import akka.typed.scaladsl.Actor +import akka.typed.scaladsl.Actor.BehaviorDecorators +import akka.typed.scaladsl.AskPattern._ +import akka.typed.testkit.{ EffectfulActorContext, Inbox, TestKitSettings } +import akka.typed.testkit.scaladsl._ + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class DeferredSpec extends TypedSpec { + + sealed trait Command + case object Ping extends Command + + sealed trait Event + case object Pong extends Event + case object Started extends Event + + def target(monitor: ActorRef[Event]): Behavior[Command] = + Actor.immutable((ctx, cmd) ⇒ cmd match { + case Ping ⇒ + monitor ! Pong + Actor.same + }) + + trait StubbedTests { + def system: ActorSystem[TypedSpec.Command] + + def mkCtx(behv: Behavior[Command]): EffectfulActorContext[Command] = + new EffectfulActorContext("ctx", behv, 1000, system) + + def `must create underlying deferred behavior immediately`(): Unit = { + val inbox = Inbox[Event]("evt") + val behv = Actor.deferred[Command] { _ ⇒ + inbox.ref ! Started + target(inbox.ref) + } + val ctx = mkCtx(behv) + // it's supposed to be created immediately (not waiting for first message) + inbox.receiveMsg() should ===(Started) + } + + def `must stop when exception from factory`(): Unit = { + val inbox = Inbox[Event]("evt") + val exc = new RuntimeException("simulated exc from factory") with NoStackTrace + val behv = Actor.deferred[Command] { _ ⇒ + inbox.ref ! Started + throw exc + } + intercept[RuntimeException] { + mkCtx(behv) + } should ===(exc) + inbox.receiveMsg() should ===(Started) + } + + } + + trait RealTests extends StartSupport { + implicit def system: ActorSystem[TypedSpec.Command] + implicit val testSettings = TestKitSettings(system) + + def `must create underlying`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.deferred[Command] { _ ⇒ + probe.ref ! Started + target(probe.ref) + } + probe.expectNoMsg(100.millis) // not yet + start(behv) + // it's supposed to be created immediately (not waiting for first message) + probe.expectMsg(Started) + } + + def `must stop when exception from factory`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.deferred[Command] { ctx ⇒ + val child = ctx.spawnAnonymous(Actor.deferred[Command] { _ ⇒ + probe.ref ! Started + throw new RuntimeException("simulated exc from factory") with NoStackTrace + }) + ctx.watch(child) + Actor.immutable[Command]((_, _) ⇒ Actor.same).onSignal { + case (_, Terminated(`child`)) ⇒ + probe.ref ! Pong + Actor.stopped + } + } + start(behv) + probe.expectMsg(Started) + probe.expectMsg(Pong) + } + + def `must stop when deferred result it Stopped`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.deferred[Command] { ctx ⇒ + val child = ctx.spawnAnonymous(Actor.deferred[Command](_ ⇒ Actor.stopped)) + ctx.watch(child) + Actor.immutable[Command]((_, _) ⇒ Actor.same).onSignal { + case (_, Terminated(`child`)) ⇒ + probe.ref ! Pong + Actor.stopped + } + } + start(behv) + probe.expectMsg(Pong) + } + + def `must create underlying when nested`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.deferred[Command] { _ ⇒ + Actor.deferred[Command] { _ ⇒ + probe.ref ! Started + target(probe.ref) + } + } + start(behv) + probe.expectMsg(Started) + } + + def `must undefer underlying when wrapped by widen`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.deferred[Command] { _ ⇒ + probe.ref ! Started + target(probe.ref) + }.widen[Command] { + case m ⇒ m + } + probe.expectNoMsg(100.millis) // not yet + val ref = start(behv) + // it's supposed to be created immediately (not waiting for first message) + probe.expectMsg(Started) + ref ! Ping + probe.expectMsg(Pong) + } + + def `must undefer underlying when wrapped by monitor`(): Unit = { + // monitor is implemented with tap, so this is testing both + val probe = TestProbe[Event]("evt") + val monitorProbe = TestProbe[Command]("monitor") + val behv = Actor.monitor(monitorProbe.ref, Actor.deferred[Command] { _ ⇒ + probe.ref ! Started + target(probe.ref) + }) + probe.expectNoMsg(100.millis) // not yet + val ref = start(behv) + // it's supposed to be created immediately (not waiting for first message) + probe.expectMsg(Started) + ref ! Ping + monitorProbe.expectMsg(Ping) + probe.expectMsg(Pong) + } + + } + + object `A DeferredBehavior (stubbed, native)` extends StubbedTests with NativeSystem + object `A DeferredBehavior (stubbed, adapted)` extends StubbedTests with AdaptedSystem + + object `A DeferredBehavior (real, native)` extends RealTests with NativeSystem + object `A DeferredBehavior (real, adapted)` extends RealTests with AdaptedSystem + +} diff --git a/akka-typed-tests/src/test/scala/akka/typed/ExtensionsSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/ExtensionsSpec.scala new file mode 100644 index 0000000000..0899dfb337 --- /dev/null +++ b/akka-typed-tests/src/test/scala/akka/typed/ExtensionsSpec.scala @@ -0,0 +1,161 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed + +import java.util.concurrent.atomic.AtomicInteger + +import com.typesafe.config.{ Config, ConfigFactory } +import org.scalatest.concurrent.ScalaFutures + +import scala.concurrent.Future + +class DummyExtension1 extends Extension +object DummyExtension1 extends ExtensionId[DummyExtension1] { + def createExtension(system: ActorSystem[_]) = new DummyExtension1 + def get(system: ActorSystem[_]): DummyExtension1 = apply(system) +} + +class SlowExtension extends Extension +object SlowExtension extends ExtensionId[SlowExtension] { + def createExtension(system: ActorSystem[_]) = { + Thread.sleep(25) + new SlowExtension + } +} + +class FailingToLoadExtension extends Extension +object FailingToLoadExtension extends ExtensionId[FailingToLoadExtension] { + def createExtension(system: ActorSystem[_]) = { + throw new RuntimeException("I cannot be trusted!") + } +} + +class MultiExtension(val n: Int) extends Extension +class MultiExtensionId(n: Int) extends ExtensionId[MultiExtension] { + def createExtension(system: ActorSystem[_]): MultiExtension = new MultiExtension(n) +} + +object InstanceCountingExtension extends ExtensionId[DummyExtension1] { + val createCount = new AtomicInteger(0) + override def createExtension(system: ActorSystem[_]): DummyExtension1 = { + createCount.addAndGet(1) + new DummyExtension1 + } +} + +class ExtensionsSpec extends TypedSpecSetup { + + object `The extensions subsystem` { + + def `01 should return the same instance for the same id`(): Unit = + withEmptyActorSystem("ExtensionsSpec01") { system ⇒ + val instance1 = system.registerExtension(DummyExtension1) + val instance2 = system.registerExtension(DummyExtension1) + + instance1 should be theSameInstanceAs instance2 + + val instance3 = DummyExtension1(system) + instance3 should be theSameInstanceAs instance2 + + val instance4 = DummyExtension1.get(system) + instance4 should be theSameInstanceAs instance3 + } + + def `02 should return the same instance for the same id concurrently`(): Unit = + withEmptyActorSystem("ExtensionsSpec02") { system ⇒ + // not exactly water tight but better than nothing + import system.executionContext + val futures = (0 to 1000).map(n ⇒ + Future { + system.registerExtension(SlowExtension) + } + ) + + val instances = Future.sequence(futures).futureValue + + instances.reduce { (a, b) ⇒ + a should be theSameInstanceAs b + b + } + } + + def `03 should load extensions from the configuration`(): Unit = + withEmptyActorSystem("ExtensionsSpec03", Some(ConfigFactory.parseString( + """ + akka.typed.extensions = ["akka.typed.DummyExtension1$", "akka.typed.SlowExtension$"] + """)) + ) { system ⇒ + system.hasExtension(DummyExtension1) should ===(true) + system.extension(DummyExtension1) shouldBe a[DummyExtension1] + + system.hasExtension(SlowExtension) should ===(true) + system.extension(SlowExtension) shouldBe a[SlowExtension] + } + + def `04 handle extensions that fail to initialize`(): Unit = { + def create(): Unit = { + ActorSystem[Any]("ExtensionsSpec04", Behavior.EmptyBehavior, config = Some(ConfigFactory.parseString( + """ + akka.typed.extensions = ["akka.typed.FailingToLoadExtension$"] + """))) + } + intercept[RuntimeException] { + create() + } + // and keeps happening (a retry to create on each access) + intercept[RuntimeException] { + create() + } + } + + def `05 support multiple instances of the same type of extension (with different ids)`(): Unit = + withEmptyActorSystem("ExtensionsSpec06") { system ⇒ + val id1 = new MultiExtensionId(1) + val id2 = new MultiExtensionId(2) + + system.registerExtension(id1).n should ===(1) + system.registerExtension(id2).n should ===(2) + system.registerExtension(id1).n should ===(1) + } + + def `06 allow for auto-loading of library-extensions`(): Unit = + withEmptyActorSystem("ExtensionsSpec06") { system ⇒ + val listedExtensions = system.settings.config.getStringList("akka.typed.library-extensions") + listedExtensions.size should be > 0 + // could be initalized by other tests, so at least once + InstanceCountingExtension.createCount.get() should be > 0 + } + + def `07 fail the system if a library-extension cannot be loaded`(): Unit = + intercept[RuntimeException] { + withEmptyActorSystem( + "ExtensionsSpec07", + Some(ConfigFactory.parseString("""akka.library-extensions += "akka.typed.FailingToLoadExtension$""")) + ) { _ ⇒ () } + } + + def `08 fail the system if a library-extension cannot be loaded`(): Unit = + intercept[RuntimeException] { + withEmptyActorSystem( + "ExtensionsSpec08", + Some(ConfigFactory.parseString("""akka.library-extensions += "akka.typed.MissingExtension""")) + ) { _ ⇒ () } + } + + def `09 load an extension implemented in Java`(): Unit = + withEmptyActorSystem("ExtensionsSpec09") { system ⇒ + // no way to make apply work cleanly with extensions implemented in Java + val instance1 = ExtensionsTest.MyExtension.get(system) + val instance2 = ExtensionsTest.MyExtension.get(system) + + instance1 should be theSameInstanceAs instance2 + } + + def withEmptyActorSystem[T](name: String, config: Option[Config] = None)(f: ActorSystem[_] ⇒ T): T = { + val system = ActorSystem[Any](name, Behavior.EmptyBehavior, config = config) + try f(system) finally system.terminate().futureValue + } + + } +} diff --git a/akka-typed/src/test/scala/akka/typed/PerformanceSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/PerformanceSpec.scala similarity index 84% rename from akka-typed/src/test/scala/akka/typed/PerformanceSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/PerformanceSpec.scala index 8e3b39a02f..cafef74c55 100644 --- a/akka-typed/src/test/scala/akka/typed/PerformanceSpec.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/PerformanceSpec.scala @@ -3,11 +3,10 @@ */ package akka.typed -import ScalaDSL._ import scala.concurrent.duration._ import com.typesafe.config.ConfigFactory import org.junit.runner.RunWith -import akka.testkit.AkkaSpec +import akka.typed.scaladsl.Actor._ import akka.util.Timeout @RunWith(classOf[org.scalatest.junit.JUnitRunner]) @@ -19,7 +18,7 @@ class PerformanceSpec extends TypedSpec( override def setTimeout = Timeout(20.seconds) - object `A static behavior` { + object `A immutable behavior` { case class Ping(x: Int, pong: ActorRef[Pong], report: ActorRef[Pong]) case class Pong(x: Int, ping: ActorRef[Ping], report: ActorRef[Pong]) @@ -28,15 +27,20 @@ class PerformanceSpec extends TypedSpec( StepWise[Pong] { (ctx, startWith) ⇒ startWith { - val pinger = SelfAware[Ping](self ⇒ Static { msg ⇒ + val pinger = immutable[Ping] { (ctx, msg) ⇒ if (msg.x == 0) { - msg.report ! Pong(0, self, msg.report) - } else msg.pong ! Pong(msg.x - 1, self, msg.report) - }) // FIXME .withDispatcher(executor) + msg.report ! Pong(0, ctx.self, msg.report) + same + } else { + msg.pong ! Pong(msg.x - 1, ctx.self, msg.report) + same + } + } // FIXME .withDispatcher(executor) - val ponger = SelfAware[Pong](self ⇒ Static { msg ⇒ - msg.ping ! Ping(msg.x, self, msg.report) - }) // FIXME .withDispatcher(executor) + val ponger = immutable[Pong] { (ctx, msg) ⇒ + msg.ping ! Ping(msg.x, ctx.self, msg.report) + same + } // FIXME .withDispatcher(executor) val actors = for (i ← 1 to pairs) diff --git a/akka-typed/src/test/scala/akka/typed/DeploymentConfigSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/PropsSpec.scala similarity index 90% rename from akka-typed/src/test/scala/akka/typed/DeploymentConfigSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/PropsSpec.scala index 36ddf1dccc..72abfd4d61 100644 --- a/akka-typed/src/test/scala/akka/typed/DeploymentConfigSpec.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/PropsSpec.scala @@ -3,12 +3,12 @@ */ package akka.typed -class DeploymentConfigSpec extends TypedSpecSetup { +class PropsSpec extends TypedSpecSetup { val dispatcherFirst = DispatcherDefault(MailboxCapacity(666, DispatcherFromConfig("pool"))) val mailboxFirst = MailboxCapacity(999) withNext dispatcherFirst - object `A DeploymentConfig` { + object `A Props` { def `must get first dispatcher`(): Unit = { dispatcherFirst.firstOrElse[DispatcherSelector](null) should ===(dispatcherFirst) @@ -31,7 +31,7 @@ class DeploymentConfigSpec extends TypedSpecSetup { } def `must yield all configs of some type`(): Unit = { - dispatcherFirst.allOf[DispatcherSelector] should ===(DispatcherDefault() :: DispatcherFromConfig("pool") :: Nil) + dispatcherFirst.allOf[DispatcherSelector] should ===(DispatcherSelector.default() :: DispatcherSelector.fromConfig("pool") :: Nil) mailboxFirst.allOf[MailboxCapacity] should ===(List(999, 666).map(MailboxCapacity(_))) } diff --git a/akka-typed-tests/src/test/scala/akka/typed/RestarterSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/RestarterSpec.scala new file mode 100644 index 0000000000..526a1996f5 --- /dev/null +++ b/akka-typed-tests/src/test/scala/akka/typed/RestarterSpec.scala @@ -0,0 +1,423 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed + +import scala.concurrent.duration._ +import akka.typed.scaladsl.Actor._ +import akka.typed.testkit.{ EffectfulActorContext, Inbox, TestKitSettings } + +import scala.util.control.NoStackTrace +import akka.typed.testkit.scaladsl._ +import akka.typed.scaladsl.AskPattern._ + +import scala.concurrent.Await + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class RestarterSpec extends TypedSpec { + + sealed trait Command + case object Ping extends Command + case class Throw(e: Throwable) extends Command + case object NextState extends Command + case object GetState extends Command + case class CreateChild[T](behavior: Behavior[T], name: String) extends Command + + sealed trait Event + case object Pong extends Event + case class GotSignal(signal: Signal) extends Event + case class State(n: Int, children: Map[String, ActorRef[Command]]) extends Event + case object Started extends Event + + class Exc1(msg: String = "exc-1") extends RuntimeException(msg) with NoStackTrace + class Exc2 extends Exc1("exc-2") + class Exc3(msg: String = "exc-3") extends RuntimeException(msg) with NoStackTrace + + def target(monitor: ActorRef[Event], state: State = State(0, Map.empty)): Behavior[Command] = + immutable[Command] { (ctx, cmd) ⇒ + cmd match { + case Ping ⇒ + monitor ! Pong + same + case NextState ⇒ + target(monitor, state.copy(n = state.n + 1)) + case GetState ⇒ + val reply = state.copy(children = ctx.children.map(c ⇒ c.path.name → c.upcast[Command]).toMap) + monitor ! reply + same + case CreateChild(childBehv, childName) ⇒ + ctx.spawn(childBehv, childName) + same + case Throw(e) ⇒ + throw e + } + } onSignal { + case (ctx, sig) ⇒ + monitor ! GotSignal(sig) + same + } + + class FailingConstructor(monitor: ActorRef[Event]) extends MutableBehavior[Command] { + monitor ! Started + throw new RuntimeException("simulated exc from constructor") with NoStackTrace + + override def onMessage(msg: Command): Behavior[Command] = { + monitor ! Pong + same + } + } + + trait StubbedTests { + def system: ActorSystem[TypedSpec.Command] + + def mkCtx(behv: Behavior[Command]): EffectfulActorContext[Command] = + new EffectfulActorContext("ctx", behv, 1000, system) + + def `must receive message`(): Unit = { + val inbox = Inbox[Event]("evt") + val behv = restarter[Throwable]().wrap(target(inbox.ref)) + val ctx = mkCtx(behv) + ctx.run(Ping) + inbox.receiveMsg() should ===(Pong) + } + + def `must stop when no restarter`(): Unit = { + val inbox = Inbox[Event]("evt") + val behv = target(inbox.ref) + val ctx = mkCtx(behv) + intercept[Exc3] { + ctx.run(Throw(new Exc3)) + } + inbox.receiveMsg() should ===(GotSignal(PostStop)) + } + + def `must stop when unhandled exception`(): Unit = { + val inbox = Inbox[Event]("evt") + val behv = restarter[Exc1]().wrap(target(inbox.ref)) + val ctx = mkCtx(behv) + intercept[Exc3] { + ctx.run(Throw(new Exc3)) + } + inbox.receiveMsg() should ===(GotSignal(PostStop)) + } + + def `must restart when handled exception`(): Unit = { + val inbox = Inbox[Event]("evt") + val behv = restarter[Exc1]().wrap(target(inbox.ref)) + val ctx = mkCtx(behv) + ctx.run(NextState) + ctx.run(GetState) + inbox.receiveMsg() should ===(State(1, Map.empty)) + + ctx.run(Throw(new Exc2)) + inbox.receiveMsg() should ===(GotSignal(PreRestart)) + ctx.run(GetState) + inbox.receiveMsg() should ===(State(0, Map.empty)) + } + + def `must resume when handled exception`(): Unit = { + val inbox = Inbox[Event]("evt") + val behv = restarter[Exc1](SupervisorStrategy.resume).wrap(target(inbox.ref)) + val ctx = mkCtx(behv) + ctx.run(NextState) + ctx.run(GetState) + inbox.receiveMsg() should ===(State(1, Map.empty)) + + ctx.run(Throw(new Exc2)) + ctx.run(GetState) + inbox.receiveMsg() should ===(State(1, Map.empty)) + } + + def `must support nesting to handle different exceptions`(): Unit = { + val inbox = Inbox[Event]("evt") + val behv = restarter[Exc3]().wrap(restarter[Exc2](SupervisorStrategy.resume).wrap(target(inbox.ref))) + val ctx = mkCtx(behv) + ctx.run(NextState) + ctx.run(GetState) + inbox.receiveMsg() should ===(State(1, Map.empty)) + + // resume + ctx.run(Throw(new Exc2)) + ctx.run(GetState) + inbox.receiveMsg() should ===(State(1, Map.empty)) + + // restart + ctx.run(Throw(new Exc3)) + inbox.receiveMsg() should ===(GotSignal(PreRestart)) + ctx.run(GetState) + inbox.receiveMsg() should ===(State(0, Map.empty)) + + // stop + intercept[Exc1] { + ctx.run(Throw(new Exc1)) + } + inbox.receiveMsg() should ===(GotSignal(PostStop)) + } + + def `must not catch fatal error`(): Unit = { + val inbox = Inbox[Event]("evt") + val behv = restarter[Throwable]().wrap(target(inbox.ref)) + val ctx = mkCtx(behv) + intercept[StackOverflowError] { + ctx.run(Throw(new StackOverflowError)) + } + inbox.receiveAll() should ===(Nil) + } + + def `must stop after restart retries limit`(): Unit = { + val inbox = Inbox[Event]("evt") + val strategy = SupervisorStrategy.restartWithLimit(maxNrOfRetries = 2, withinTimeRange = 1.minute) + val behv = restarter[Exc1](strategy).wrap(target(inbox.ref)) + val ctx = mkCtx(behv) + ctx.run(Throw(new Exc1)) + inbox.receiveMsg() should ===(GotSignal(PreRestart)) + ctx.run(Throw(new Exc1)) + inbox.receiveMsg() should ===(GotSignal(PreRestart)) + intercept[Exc1] { + ctx.run(Throw(new Exc1)) + } + inbox.receiveMsg() should ===(GotSignal(PostStop)) + } + + def `must reset retry limit after withinTimeRange`(): Unit = { + val inbox = Inbox[Event]("evt") + val withinTimeRange = 2.seconds + val strategy = SupervisorStrategy.restartWithLimit(maxNrOfRetries = 2, withinTimeRange) + val behv = restarter[Exc1](strategy).wrap(target(inbox.ref)) + val ctx = mkCtx(behv) + ctx.run(Throw(new Exc1)) + inbox.receiveMsg() should ===(GotSignal(PreRestart)) + ctx.run(Throw(new Exc1)) + inbox.receiveMsg() should ===(GotSignal(PreRestart)) + Thread.sleep((2.seconds + 100.millis).toMillis) + + ctx.run(Throw(new Exc1)) + inbox.receiveMsg() should ===(GotSignal(PreRestart)) + ctx.run(Throw(new Exc1)) + inbox.receiveMsg() should ===(GotSignal(PreRestart)) + intercept[Exc1] { + ctx.run(Throw(new Exc1)) + } + inbox.receiveMsg() should ===(GotSignal(PostStop)) + } + + def `must stop at first exception when restart retries limit is 0`(): Unit = { + val inbox = Inbox[Event]("evt") + val strategy = SupervisorStrategy.restartWithLimit(maxNrOfRetries = 0, withinTimeRange = 1.minute) + val behv = restarter[Exc1](strategy).wrap(target(inbox.ref)) + val ctx = mkCtx(behv) + intercept[Exc1] { + ctx.run(Throw(new Exc1)) + } + inbox.receiveMsg() should ===(GotSignal(PostStop)) + } + + def `must create underlying deferred behavior immediately`(): Unit = { + val inbox = Inbox[Event]("evt") + val behv = restarter[Exception]().wrap(deferred[Command] { _ ⇒ + inbox.ref ! Started + target(inbox.ref) + }) + val ctx = mkCtx(behv) + // it's supposed to be created immediately (not waiting for first message) + inbox.receiveMsg() should ===(Started) + } + } + + trait RealTests extends StartSupport { + import akka.typed.scaladsl.adapter._ + implicit def system: ActorSystem[TypedSpec.Command] + implicit val testSettings = TestKitSettings(system) + + def `must receive message`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = restarter[Throwable]().wrap(target(probe.ref)) + val ref = start(behv) + ref ! Ping + probe.expectMsg(Pong) + } + + def `must stop when no restarter`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = target(probe.ref) + val ref = start(behv) + ref ! Throw(new Exc3) + + probe.expectMsg(GotSignal(PostStop)) + } + + def `must stop when unhandled exception`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = restarter[Exc1]().wrap(target(probe.ref)) + val ref = start(behv) + ref ! Throw(new Exc3) + probe.expectMsg(GotSignal(PostStop)) + } + + def `must restart when handled exception`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = restarter[Exc1]().wrap(target(probe.ref)) + val ref = start(behv) + ref ! NextState + ref ! GetState + probe.expectMsg(State(1, Map.empty)) + + ref ! Throw(new Exc2) + probe.expectMsg(GotSignal(PreRestart)) + ref ! GetState + probe.expectMsg(State(0, Map.empty)) + } + + def `must NOT stop children when restarting`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = restarter[Exc1]().wrap(target(probe.ref)) + val ref = start(behv) + + val childProbe = TestProbe[Event]("childEvt") + val childName = nextName() + ref ! CreateChild(target(childProbe.ref), childName) + ref ! GetState + probe.expectMsgType[State].children.keySet should contain(childName) + + ref ! Throw(new Exc1) + probe.expectMsg(GotSignal(PreRestart)) + ref ! GetState + // TODO document this difference compared to classic actors, and that + // children can be stopped if needed in PreRestart + probe.expectMsgType[State].children.keySet should contain(childName) + } + + def `must resume when handled exception`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = restarter[Exc1](SupervisorStrategy.resume).wrap(target(probe.ref)) + val ref = start(behv) + ref ! NextState + ref ! GetState + probe.expectMsg(State(1, Map.empty)) + + ref ! Throw(new Exc2) + ref ! GetState + probe.expectMsg(State(1, Map.empty)) + } + + def `must support nesting to handle different exceptions`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = restarter[Exc3]().wrap(restarter[Exc2](SupervisorStrategy.resume).wrap(target(probe.ref))) + val ref = start(behv) + ref ! NextState + ref ! GetState + probe.expectMsg(State(1, Map.empty)) + + // resume + ref ! Throw(new Exc2) + ref ! GetState + probe.expectMsg(State(1, Map.empty)) + + // restart + ref ! Throw(new Exc3) + probe.expectMsg(GotSignal(PreRestart)) + ref ! GetState + probe.expectMsg(State(0, Map.empty)) + + // stop + ref ! Throw(new Exc1) + probe.expectMsg(GotSignal(PostStop)) + } + + def `must restart after exponential backoff`(): Unit = { + val probe = TestProbe[Event]("evt") + val startedProbe = TestProbe[Event]("started") + val minBackoff = 1.seconds + val strategy = SupervisorStrategy.restartWithBackoff(minBackoff, 10.seconds, 0.0) + .withResetBackoffAfter(10.seconds) + val behv = restarter[Exc1](strategy).wrap(deferred[Command] { _ ⇒ + startedProbe.ref ! Started + target(probe.ref) + }) + val ref = start(behv) + + startedProbe.expectMsg(Started) + ref ! NextState + ref ! Throw(new Exc1) + probe.expectMsg(GotSignal(PreRestart)) + ref ! Ping // dropped due to backoff + + startedProbe.expectNoMsg(minBackoff - 100.millis) + probe.expectNoMsg(minBackoff + 100.millis) + startedProbe.expectMsg(Started) + ref ! GetState + probe.expectMsg(State(0, Map.empty)) + + // one more time + ref ! NextState + ref ! Throw(new Exc1) + probe.expectMsg(GotSignal(PreRestart)) + ref ! Ping // dropped due to backoff + + startedProbe.expectNoMsg((minBackoff * 2) - 100.millis) + probe.expectNoMsg((minBackoff * 2) + 100.millis) + startedProbe.expectMsg(Started) + ref ! GetState + probe.expectMsg(State(0, Map.empty)) + } + + def `must reset exponential backoff count after reset timeout`(): Unit = { + val probe = TestProbe[Event]("evt") + val minBackoff = 1.seconds + val strategy = SupervisorStrategy.restartWithBackoff(minBackoff, 10.seconds, 0.0) + .withResetBackoffAfter(100.millis) + val behv = restarter[Exc1](strategy).wrap(target(probe.ref)) + val ref = start(behv) + + ref ! NextState + ref ! Throw(new Exc1) + probe.expectMsg(GotSignal(PreRestart)) + ref ! Ping // dropped due to backoff + + probe.expectNoMsg(minBackoff + 100.millis) + ref ! GetState + probe.expectMsg(State(0, Map.empty)) + + // one more time after the reset timeout + probe.expectNoMsg(strategy.resetBackoffAfter + 100.millis) + ref ! NextState + ref ! Throw(new Exc1) + probe.expectMsg(GotSignal(PreRestart)) + ref ! Ping // dropped due to backoff + + // backoff was reset, so restarted after the minBackoff + probe.expectNoMsg(minBackoff + 100.millis) + ref ! GetState + probe.expectMsg(State(0, Map.empty)) + } + + def `must create underlying deferred behavior immediately`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = restarter[Exception]().wrap(deferred[Command] { _ ⇒ + probe.ref ! Started + target(probe.ref) + }) + probe.expectNoMsg(100.millis) // not yet + start(behv) + // it's supposed to be created immediately (not waiting for first message) + probe.expectMsg(Started) + } + + def `must stop when exception from MutableBehavior constructor`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = restarter[Exception]().wrap(mutable[Command](_ ⇒ new FailingConstructor(probe.ref))) + val ref = start(behv) + probe.expectMsg(Started) + ref ! Ping + probe.expectNoMsg(100.millis) + } + + } + + object `A restarter (stubbed, native)` extends StubbedTests with NativeSystem + object `A restarter (stubbed, adapted)` extends StubbedTests with AdaptedSystem + + object `A restarter (real, native)` extends RealTests with NativeSystem + object `A restarter (real, adapted)` extends RealTests with AdaptedSystem + +} diff --git a/akka-typed/src/test/scala/akka/typed/StepWise.scala b/akka-typed-tests/src/test/scala/akka/typed/StepWise.scala similarity index 82% rename from akka-typed/src/test/scala/akka/typed/StepWise.scala rename to akka-typed-tests/src/test/scala/akka/typed/StepWise.scala index dbdaba83d8..b4d4d61511 100644 --- a/akka-typed/src/test/scala/akka/typed/StepWise.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/StepWise.scala @@ -5,6 +5,9 @@ package akka.typed import scala.concurrent.duration.FiniteDuration import java.util.concurrent.TimeoutException + +import akka.typed.scaladsl.Actor._ + import scala.concurrent.duration.Deadline /** @@ -35,7 +38,6 @@ import scala.concurrent.duration.Deadline */ @deprecated("to be replaced by process DSL", "2.4-M2") object StepWise { - import ScalaDSL._ sealed trait AST private final case class Thunk(f: () ⇒ Any) extends AST @@ -107,8 +109,8 @@ object StepWise { } def apply[T](f: (scaladsl.ActorContext[T], StartWith[T]) ⇒ Steps[T, _]): Behavior[T] = - Full[Any] { - case Sig(ctx, PreStart) ⇒ run(ctx, f(ctx.asInstanceOf[ActorContext[T]], new StartWith(keepTraces = false)).ops.reverse, ()) + deferred[Any] { ctx ⇒ + run(ctx, f(ctx.asInstanceOf[scaladsl.ActorContext[T]], new StartWith(keepTraces = false)).ops.reverse, ()) }.narrow private def throwTimeout(trace: Trace, message: String): Nothing = @@ -133,27 +135,35 @@ object StepWise { case ThunkV(f) :: tail ⇒ run(ctx, tail, f(value)) case Message(t, f, trace) :: tail ⇒ ctx.setReceiveTimeout(t, ReceiveTimeout) - Full { - case Msg(_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for a message") - case Msg(_, msg) ⇒ + immutable[Any] { + case (_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for a message") + case (_, msg) ⇒ ctx.cancelReceiveTimeout() run(ctx, tail, f(msg, value)) - case Sig(_, other) ⇒ throwIllegalState(trace, s"unexpected $other while waiting for a message") + } onSignal { + case (_, PostStop) ⇒ + // ignore PostStop here + run(ctx, ops, value) + case (_, other) ⇒ throwIllegalState(trace, s"unexpected $other while waiting for a message") } case MultiMessage(t, c, f, trace) :: tail ⇒ val deadline = Deadline.now + t def behavior(count: Int, acc: List[Any]): Behavior[Any] = { ctx.setReceiveTimeout(deadline.timeLeft, ReceiveTimeout) - Full { - case Msg(_, ReceiveTimeout) ⇒ + immutable[Any] { + case (_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for $c messages (got only $count)") - case Msg(_, msg) ⇒ + case (_, msg) ⇒ val nextCount = count + 1 if (nextCount == c) { ctx.cancelReceiveTimeout() run(ctx, tail, f((msg :: acc).reverse, value)) } else behavior(nextCount, msg :: acc) - case Sig(_, other) ⇒ throwIllegalState(trace, s"unexpected $other while waiting for $c messages (got $count valid ones)") + } onSignal { + case (_, PostStop) ⇒ + // ignore PostStop here + run(ctx, ops, value) + case (_, other) ⇒ throwIllegalState(trace, s"unexpected $other while waiting for $c messages (got $count valid ones)") } } behavior(0, Nil) @@ -161,16 +171,20 @@ object StepWise { val deadline = Deadline.now + t def behavior(count: Int, acc: List[Either[Signal, Any]]): Behavior[Any] = { ctx.setReceiveTimeout(deadline.timeLeft, ReceiveTimeout) - Full { - case Msg(_, ReceiveTimeout) ⇒ + immutable[Any] { + case (_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for $c messages (got only $count)") - case Msg(_, msg) ⇒ + case (_, msg) ⇒ val nextCount = count + 1 if (nextCount == c) { ctx.cancelReceiveTimeout() run(ctx, tail, f((Right(msg) :: acc).reverse, value)) } else behavior(nextCount, Right(msg) :: acc) - case Sig(_, other) ⇒ + } onSignal { + case (_, PostStop) ⇒ + // ignore PostStop here + run(ctx, ops, value) + case (_, other) ⇒ val nextCount = count + 1 if (nextCount == c) { ctx.cancelReceiveTimeout() @@ -181,14 +195,20 @@ object StepWise { behavior(0, Nil) case Termination(t, f, trace) :: tail ⇒ ctx.setReceiveTimeout(t, ReceiveTimeout) - Full { - case Msg(_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for termination") - case Sig(_, t: Terminated) ⇒ + immutable[Any] { + case (_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for termination") + case other ⇒ throwIllegalState(trace, s"unexpected $other while waiting for termination") + } onSignal { + case (_, PostStop) ⇒ + // ignore PostStop here + run(ctx, ops, value) + case (_, t: Terminated) ⇒ ctx.cancelReceiveTimeout() run(ctx, tail, f(t, value)) case other ⇒ throwIllegalState(trace, s"unexpected $other while waiting for termination") } - case Nil ⇒ Stopped + case Nil ⇒ + stopped } } diff --git a/akka-typed-tests/src/test/scala/akka/typed/TimerSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/TimerSpec.scala new file mode 100644 index 0000000000..7bbb3b2f5d --- /dev/null +++ b/akka-typed-tests/src/test/scala/akka/typed/TimerSpec.scala @@ -0,0 +1,230 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +import scala.concurrent.duration._ +import scala.util.control.NoStackTrace + +import akka.typed.scaladsl.Actor +import akka.typed.scaladsl.AskPattern._ +import akka.typed.scaladsl.TimerScheduler +import akka.typed.testkit.TestKitSettings +import akka.typed.testkit.scaladsl._ + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class TimerSpec extends TypedSpec(""" + #akka.loglevel = DEBUG + """) { + + sealed trait Command + case class Tick(n: Int) extends Command + case object Bump extends Command + case class SlowThenBump(latch: CountDownLatch) extends Command + case object End extends Command + case class Throw(e: Throwable) extends Command + case object Cancel extends Command + case class SlowThenThrow(latch: CountDownLatch, e: Throwable) extends Command + + sealed trait Event + case class Tock(n: Int) extends Event + case class GotPostStop(timerActive: Boolean) extends Event + case class GotPreRestart(timerActive: Boolean) extends Event + + class Exc extends RuntimeException("simulated exc") with NoStackTrace + + trait RealTests extends StartSupport { + implicit def system: ActorSystem[TypedSpec.Command] + implicit val testSettings = TestKitSettings(system) + + val interval = 1.second + val dilatedInterval = interval.dilated + + def target(monitor: ActorRef[Event], timer: TimerScheduler[Command], bumpCount: Int): Behavior[Command] = { + def bump(): Behavior[Command] = { + val nextCount = bumpCount + 1 + timer.startPeriodicTimer("T", Tick(nextCount), interval) + target(monitor, timer, nextCount) + } + + Actor.immutable[Command] { (ctx, cmd) ⇒ + cmd match { + case Tick(n) ⇒ + monitor ! Tock(n) + Actor.same + case Bump ⇒ + bump() + case SlowThenBump(latch) ⇒ + latch.await(10, TimeUnit.SECONDS) + bump() + case End ⇒ + Actor.stopped + case Cancel ⇒ + timer.cancel("T") + Actor.same + case Throw(e) ⇒ + throw e + case SlowThenThrow(latch, e) ⇒ + latch.await(10, TimeUnit.SECONDS) + throw e + } + } onSignal { + case (ctx, PreRestart) ⇒ + monitor ! GotPreRestart(timer.isTimerActive("T")) + Actor.same + case (ctx, PostStop) ⇒ + monitor ! GotPostStop(timer.isTimerActive("T")) + Actor.same + } + } + + def `01 must schedule non-repeated ticks`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.withTimers[Command] { timer ⇒ + timer.startSingleTimer("T", Tick(1), 10.millis) + target(probe.ref, timer, 1) + } + + val ref = start(behv) + probe.expectMsg(Tock(1)) + probe.expectNoMsg(100.millis) + + ref ! End + probe.expectMsg(GotPostStop(false)) + } + + def `02 must schedule repeated ticks`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.withTimers[Command] { timer ⇒ + timer.startPeriodicTimer("T", Tick(1), interval) + target(probe.ref, timer, 1) + } + + val ref = start(behv) + probe.within((interval * 4) - 100.millis) { + probe.expectMsg(Tock(1)) + probe.expectMsg(Tock(1)) + probe.expectMsg(Tock(1)) + } + + ref ! End + probe.expectMsg(GotPostStop(false)) + } + + def `03 must replace timer`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.withTimers[Command] { timer ⇒ + timer.startPeriodicTimer("T", Tick(1), interval) + target(probe.ref, timer, 1) + } + + val ref = start(behv) + probe.expectMsg(Tock(1)) + val latch = new CountDownLatch(1) + // next Tock(1) enqueued in mailboxed, but should be discarded because of new timer + ref ! SlowThenBump(latch) + probe.expectNoMsg(interval + 100.millis) + latch.countDown() + probe.expectMsg(Tock(2)) + + ref ! End + probe.expectMsg(GotPostStop(false)) + } + + def `04 must cancel timer`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.withTimers[Command] { timer ⇒ + timer.startPeriodicTimer("T", Tick(1), interval) + target(probe.ref, timer, 1) + } + + val ref = start(behv) + probe.expectMsg(Tock(1)) + ref ! Cancel + probe.expectNoMsg(dilatedInterval + 100.millis) + + ref ! End + probe.expectMsg(GotPostStop(false)) + } + + def `05 must discard timers from old incarnation after restart, alt 1`(): Unit = { + val probe = TestProbe[Event]("evt") + val startCounter = new AtomicInteger(0) + val behv = Actor.restarter[Exception]().wrap(Actor.withTimers[Command] { timer ⇒ + timer.startPeriodicTimer("T", Tick(startCounter.incrementAndGet()), interval) + target(probe.ref, timer, 1) + }) + + val ref = start(behv) + probe.expectMsg(Tock(1)) + + val latch = new CountDownLatch(1) + // next Tock(1) is enqueued in mailbox, but should be discarded by new incarnation + ref ! SlowThenThrow(latch, new Exc) + probe.expectNoMsg(interval + 100.millis) + latch.countDown() + probe.expectMsg(GotPreRestart(false)) + probe.expectNoMsg(interval / 2) + probe.expectMsg(Tock(2)) + + ref ! End + probe.expectMsg(GotPostStop(false)) + } + + def `06 must discard timers from old incarnation after restart, alt 2`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.restarter[Exception]().wrap(Actor.withTimers[Command] { timer ⇒ + timer.startPeriodicTimer("T", Tick(1), interval) + target(probe.ref, timer, 1) + }) + + val ref = start(behv) + probe.expectMsg(Tock(1)) + // change state so that we see that the restart starts over again + ref ! Bump + + probe.expectMsg(Tock(2)) + + val latch = new CountDownLatch(1) + // next Tock(2) is enqueued in mailbox, but should be discarded by new incarnation + ref ! SlowThenThrow(latch, new Exc) + probe.expectNoMsg(interval + 100.millis) + latch.countDown() + probe.expectMsg(GotPreRestart(false)) + probe.expectMsg(Tock(1)) + + ref ! End + probe.expectMsg(GotPostStop(false)) + } + + def `07 must cancel timers when stopped from exception`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.withTimers[Command] { timer ⇒ + timer.startPeriodicTimer("T", Tick(1), interval) + target(probe.ref, timer, 1) + } + val ref = start(behv) + ref ! Throw(new Exc) + probe.expectMsg(GotPostStop(false)) + } + + def `08 must cancel timers when stopped voluntarily`(): Unit = { + val probe = TestProbe[Event]("evt") + val behv = Actor.withTimers[Command] { timer ⇒ + timer.startPeriodicTimer("T", Tick(1), interval) + target(probe.ref, timer, 1) + } + val ref = start(behv) + ref ! End + probe.expectMsg(GotPostStop(false)) + } + } + + object `A Restarter (real, native)` extends RealTests with NativeSystem + object `A Restarter (real, adapted)` extends RealTests with AdaptedSystem + +} diff --git a/akka-typed/src/test/scala/akka/typed/TypedSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/TypedSpec.scala similarity index 66% rename from akka-typed/src/test/scala/akka/typed/TypedSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/TypedSpec.scala index 039e09e45d..e4460568b9 100644 --- a/akka-typed/src/test/scala/akka/typed/TypedSpec.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/TypedSpec.scala @@ -7,68 +7,107 @@ import org.scalatest.refspec.RefSpec import org.scalatest.Matchers import org.scalatest.BeforeAndAfterAll import akka.testkit.AkkaSpec + import scala.concurrent.Await import scala.concurrent.duration._ import scala.concurrent.Future import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import akka.util.Timeout + import scala.reflect.ClassTag import akka.actor.ActorInitializationException + import language.existentials -import akka.testkit.EventFilter import akka.testkit.TestEvent.Mute +import akka.typed.scaladsl.Actor._ import org.scalatest.concurrent.ScalaFutures import org.scalactic.TypeCheckedTripleEquals import org.scalactic.CanEqual import org.junit.runner.RunWith + import scala.util.control.NonFatal -import org.scalatest.exceptions.TestFailedException -import akka.util.TypedMultiMap import akka.typed.scaladsl.AskPattern +import scala.util.control.NoStackTrace +import akka.typed.testkit.{ Inbox, TestKitSettings } +import org.scalatest.time.Span + /** * Helper class for writing tests for typed Actors with ScalaTest. */ @RunWith(classOf[org.scalatest.junit.JUnitRunner]) -class TypedSpecSetup extends RefSpec with Matchers with BeforeAndAfterAll with ScalaFutures with TypeCheckedTripleEquals +class TypedSpecSetup extends RefSpec with Matchers with BeforeAndAfterAll with ScalaFutures with TypeCheckedTripleEquals { + + // TODO hook this up with config like in akka-testkit/AkkaSpec? + implicit val akkaPatience = PatienceConfig(3.seconds, Span(100, org.scalatest.time.Millis)) + +} /** * Helper class for writing tests against both ActorSystemImpl and ActorSystemAdapter. */ -class TypedSpec(val config: Config) extends TypedSpecSetup { +abstract class TypedSpec(val config: Config) extends TypedSpecSetup { import TypedSpec._ import AskPattern._ def this() = this(ConfigFactory.empty) + def this(config: String) = this(ConfigFactory.parseString(config)) + // extension point def setTimeout: Timeout = Timeout(1.minute) - lazy val nativeSystem = ActorSystem(AkkaSpec.getCallerName(classOf[TypedSpec]), guardian(), config = Some(config withFallback AkkaSpec.testConf)) - lazy val adaptedSystem = ActorSystem.adapter(AkkaSpec.getCallerName(classOf[TypedSpec]), guardian(), config = Some(config withFallback AkkaSpec.testConf)) + private var nativeSystemUsed = false + lazy val nativeSystem: ActorSystem[TypedSpec.Command] = { + val sys = ActorSystem(AkkaSpec.getCallerName(classOf[TypedSpec]), guardian(), config = Some(config withFallback AkkaSpec.testConf)) + nativeSystemUsed = true + sys + } + private var adaptedSystemUsed = false + lazy val adaptedSystem: ActorSystem[TypedSpec.Command] = { + val sys = ActorSystem.adapter(AkkaSpec.getCallerName(classOf[TypedSpec]), guardian(), config = Some(config withFallback AkkaSpec.testConf)) + adaptedSystemUsed = true + sys + } + + trait StartSupport { + def system: ActorSystem[TypedSpec.Command] + + private val nameCounter = Iterator.from(0) + def nextName(prefix: String = "a"): String = s"$prefix-${nameCounter.next()}" + + def start[T](behv: Behavior[T]): ActorRef[T] = { + import akka.typed.scaladsl.AskPattern._ + import akka.typed.testkit.scaladsl._ + implicit val testSettings = TestKitSettings(system) + Await.result(system ? TypedSpec.Create(behv, nextName()), 3.seconds.dilated) + } + } trait NativeSystem { - def system = nativeSystem + def system: ActorSystem[TypedSpec.Command] = nativeSystem } + trait AdaptedSystem { - def system = adaptedSystem + def system: ActorSystem[TypedSpec.Command] = adaptedSystem } implicit val timeout = setTimeout - implicit val patience = PatienceConfig(3.seconds) implicit def scheduler = nativeSystem.scheduler override def afterAll(): Unit = { - Await.result(nativeSystem ? (Terminate(_)), timeout.duration): Status - Await.result(adaptedSystem ? (Terminate(_)), timeout.duration): Status + if (nativeSystemUsed) + Await.result(nativeSystem.terminate, timeout.duration) + if (adaptedSystemUsed) + Await.result(adaptedSystem.terminate, timeout.duration) } // TODO remove after basing on ScalaTest 3 with async support import akka.testkit._ def await[T](f: Future[T]): T = Await.result(f, timeout.duration * 1.1) - lazy val blackhole = await(nativeSystem ? Create(ScalaDSL.Full[Any] { case _ ⇒ ScalaDSL.Same }, "blackhole")) + lazy val blackhole = await(nativeSystem ? Create(immutable[Any] { case _ ⇒ same }, "blackhole")) /** * Run an Actor-based test. The test procedure is most conveniently @@ -87,12 +126,19 @@ class TypedSpec(val config: Config) extends TypedSpecSetup { try await(f) match { case Success ⇒ () case Failed(ex) ⇒ - println(system.printTree) - throw unwrap(ex) + unwrap(ex) match { + case ex2: TypedSpec.SimulatedException ⇒ + throw ex2 + case _ ⇒ + println(system.printTree) + throw unwrap(ex) + } case Timedout ⇒ println(system.printTree) fail("test timed out") } catch { + case ex: TypedSpec.SimulatedException ⇒ + throw ex case NonFatal(ex) ⇒ println(system.printTree) throw ex @@ -130,7 +176,6 @@ class TypedSpec(val config: Config) extends TypedSpecSetup { } object TypedSpec { - import ScalaDSL._ import akka.{ typed ⇒ t } sealed abstract class Start @@ -146,28 +191,31 @@ object TypedSpec { case class Failed(thr: Throwable) extends Status case object Timedout extends Status + class SimulatedException(message: String) extends RuntimeException(message) with NoStackTrace + def guardian(outstanding: Map[ActorRef[_], ActorRef[Status]] = Map.empty): Behavior[Command] = - FullTotal { - case Sig(ctx, t @ Terminated(test)) ⇒ + immutable[Command] { + case (ctx, r: RunTest[t]) ⇒ + val test = ctx.spawn(r.behavior, r.name) + ctx.schedule(r.timeout, r.replyTo, Timedout) + ctx.watch(test) + guardian(outstanding + ((test, r.replyTo))) + case (_, Terminate(reply)) ⇒ + reply ! Success + stopped + case (ctx, c: Create[t]) ⇒ + c.replyTo ! ctx.spawn(c.behavior, c.name) + same + } onSignal { + case (ctx, t @ Terminated(test)) ⇒ outstanding get test match { case Some(reply) ⇒ if (t.failure eq null) reply ! Success else reply ! Failed(t.failure) guardian(outstanding - test) - case None ⇒ Same + case None ⇒ same } - case _: Sig[_] ⇒ Same - case Msg(ctx, r: RunTest[t]) ⇒ - val test = ctx.spawn(r.behavior, r.name) - ctx.schedule(r.timeout, r.replyTo, Timedout) - ctx.watch(test) - guardian(outstanding + ((test, r.replyTo))) - case Msg(_, Terminate(reply)) ⇒ - reply ! Success - Stopped - case Msg(ctx, c: Create[t]) ⇒ - c.replyTo ! ctx.spawn(c.behavior, c.name) - Same + case _ ⇒ same } def getCallerName(clazz: Class[_]): String = { @@ -189,12 +237,11 @@ class TypedSpecSpec extends TypedSpec { implicit def system: ActorSystem[TypedSpec.Command] def `must report failures`(): Unit = { - a[TestFailedException] must be thrownBy { + a[TypedSpec.SimulatedException] must be thrownBy { sync(runTest("failure")(StepWise[String]((ctx, startWith) ⇒ startWith { - fail("expected") - } - ))) + throw new TypedSpec.SimulatedException("expected") + }))) } } } diff --git a/akka-typed-tests/src/test/scala/akka/typed/WatchSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/WatchSpec.scala new file mode 100644 index 0000000000..b836942518 --- /dev/null +++ b/akka-typed-tests/src/test/scala/akka/typed/WatchSpec.scala @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed + +import scala.concurrent._ +import scala.concurrent.duration._ +import akka.typed.scaladsl.Actor._ +import akka.typed.scaladsl.AskPattern._ +import akka.testkit._ + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class WatchSpec extends TypedSpec { + + trait Tests { + implicit def system: ActorSystem[TypedSpec.Command] + + def `get notified of actor termination`(): Unit = { + case object Stop + case class StartWatching(watchee: ActorRef[Stop.type]) + + val terminator = Await.result(system ? TypedSpec.Create(immutable[Stop.type] { + case (ctx, `Stop`) ⇒ stopped + }, "t1"), 3.seconds /*.dilated*/ ) + + val receivedTerminationSignal: Promise[Unit] = Promise() + + val watcher = Await.result(system ? TypedSpec.Create(immutable[StartWatching] { + case (ctx, StartWatching(watchee)) ⇒ ctx.watch(watchee); same + }.onSignal { + case (ctx, Terminated(_)) ⇒ receivedTerminationSignal.success(()); stopped + }, "w1"), 3.seconds /*.dilated*/ ) + + watcher ! StartWatching(terminator) + terminator ! Stop + + Await.result(receivedTerminationSignal.future, 3.seconds /*.dilated*/ ) + } + + def `get notified of actor termination with a custom message`(): Unit = { + case object Stop + + sealed trait Message + case object CustomTerminationMessage extends Message + case class StartWatchingWith(watchee: ActorRef[Stop.type], msg: CustomTerminationMessage.type) extends Message + + val terminator = Await.result(system ? TypedSpec.Create(immutable[Stop.type] { + case (ctx, `Stop`) ⇒ stopped + }, "t2"), 3.seconds /*.dilated*/ ) + + val receivedTerminationSignal: Promise[Unit] = Promise() + + val watcher = Await.result(system ? TypedSpec.Create(immutable[Message] { + case (ctx, StartWatchingWith(watchee, msg)) ⇒ + ctx.watchWith(watchee, msg) + same + case (ctx, `CustomTerminationMessage`) ⇒ + receivedTerminationSignal.success(()) + stopped + }, "w2"), 3.seconds /*.dilated*/ ) + + watcher ! StartWatchingWith(terminator, CustomTerminationMessage) + terminator ! Stop + + Await.result(receivedTerminationSignal.future, 3.seconds /*.dilated*/ ) + } + } + + object `Actor monitoring (native)` extends Tests with NativeSystem + object `Actor monitoring (adapted)` extends Tests with AdaptedSystem +} diff --git a/akka-typed/src/test/scala/akka/typed/internal/ActorCellSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/internal/ActorCellSpec.scala similarity index 87% rename from akka-typed/src/test/scala/akka/typed/internal/ActorCellSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/internal/ActorCellSpec.scala index 3d2082a059..60005cf684 100644 --- a/akka-typed/src/test/scala/akka/typed/internal/ActorCellSpec.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/internal/ActorCellSpec.scala @@ -4,17 +4,17 @@ package akka.typed package internal +import akka.typed.scaladsl.Actor +import akka.typed.scaladsl.Actor._ import org.scalactic.ConversionCheckedTripleEquals import org.scalatest.concurrent.ScalaFutures import org.scalatest.exceptions.TestFailedException -import org.scalatest._ import org.junit.runner.RunWith +import org.scalatest._ @RunWith(classOf[org.scalatest.junit.JUnitRunner]) class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with ScalaFutures with ConversionCheckedTripleEquals { - import ScalaDSL._ - val sys = new ActorSystemStub("ActorCellSpec") def ec = sys.controlledExecutor @@ -22,7 +22,14 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must be creatable`(): Unit = { val parent = new DebugRef[String](sys.path / "creatable", true) - val cell = new ActorCell(sys, Deferred[String](() ⇒ { parent ! "created"; Static { s ⇒ parent ! s } }), ec, 1000, parent) + val cell = new ActorCell(sys, deferred[String](_ ⇒ { + parent ! "created" + immutable[String] { + case (_, s) ⇒ + parent ! s + same + } + }), ec, 1000, parent) debugCell(cell) { ec.queueSize should ===(0) cell.sendSystem(Create()) @@ -40,7 +47,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala val parent = new DebugRef[String](sys.path / "creatable???", true) val self = new DebugRef[String](sys.path / "creatableSelf", true) val ??? = new NotImplementedError - val cell = new ActorCell(sys, Deferred[String](() ⇒ { parent ! "created"; throw ??? }), ec, 1000, parent) + val cell = new ActorCell(sys, deferred[String](_ ⇒ { parent ! "created"; throw ??? }), ec, 1000, parent) cell.setSelf(self) debugCell(cell) { ec.queueSize should ===(0) @@ -60,7 +67,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must be able to terminate after construction`(): Unit = { val parent = new DebugRef[String](sys.path / "terminate", true) val self = new DebugRef[String](sys.path / "terminateSelf", true) - val cell = new ActorCell(sys, Deferred[String](() ⇒ { parent ! "created"; Stopped }), ec, 1000, parent) + val cell = new ActorCell(sys, deferred[String](_ ⇒ { parent ! "created"; stopped }), ec, 1000, parent) cell.setSelf(self) debugCell(cell) { ec.queueSize should ===(0) @@ -77,10 +84,10 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala } } - def `must be able to terminate after PreStart`(): Unit = { + def `must be able to terminate after being started`(): Unit = { val parent = new DebugRef[String](sys.path / "terminate", true) val self = new DebugRef[String](sys.path / "terminateSelf", true) - val cell = new ActorCell(sys, Deferred(() ⇒ { parent ! "created"; Full[String] { case Sig(ctx, PreStart) ⇒ Stopped } }), ec, 1000, parent) + val cell = new ActorCell(sys, deferred[String](_ ⇒ { parent ! "created"; stopped }), ec, 1000, parent) cell.setSelf(self) debugCell(cell) { ec.queueSize should ===(0) @@ -101,7 +108,8 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala val parent = new DebugRef[String](sys.path / "terminate", true) val self = new DebugRef[String](sys.path / "terminateSelf", true) val ex = new AssertionError - val cell = new ActorCell(sys, Deferred(() ⇒ { parent ! "created"; Static[String](s ⇒ throw ex) }), ec, 1000, parent) + val behavior = deferred[String](_ ⇒ { parent ! "created"; immutable[String] { case (s, _) ⇒ throw ex } }) + val cell = new ActorCell(sys, behavior, ec, 1000, parent) cell.setSelf(self) debugCell(cell) { ec.queueSize should ===(0) @@ -124,7 +132,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must signal failure when starting behavior is "same"`(): Unit = { val parent = new DebugRef[String](sys.path / "startSame", true) val self = new DebugRef[String](sys.path / "startSameSelf", true) - val cell = new ActorCell(sys, Deferred(() ⇒ { parent ! "created"; Same[String] }), ec, 1000, parent) + val cell = new ActorCell(sys, deferred[String](_ ⇒ { parent ! "created"; same[String] }), ec, 1000, parent) cell.setSelf(self) debugCell(cell) { ec.queueSize should ===(0) @@ -150,7 +158,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must signal failure when starting behavior is "unhandled"`(): Unit = { val parent = new DebugRef[String](sys.path / "startSame", true) val self = new DebugRef[String](sys.path / "startSameSelf", true) - val cell = new ActorCell(sys, Deferred(() ⇒ { parent ! "created"; Unhandled[String] }), ec, 1000, parent) + val cell = new ActorCell(sys, deferred[String](_ ⇒ { parent ! "created"; unhandled[String] }), ec, 1000, parent) cell.setSelf(self) debugCell(cell) { ec.queueSize should ===(0) @@ -181,7 +189,14 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala */ def `must not execute more messages than were batched naturally`(): Unit = { val parent = new DebugRef[String](sys.path / "batching", true) - val cell = new ActorCell(sys, SelfAware[String] { self ⇒ Static { s ⇒ self ! s; parent ! s } }, ec, 1000, parent) + val cell = new ActorCell(sys, deferred[String] { ctx ⇒ + immutable[String] { + case (_, s) ⇒ + ctx.self ! s + parent ! s + same + } + }, ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) debugCell(cell) { @@ -214,7 +229,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must signal DeathWatch when terminating normally`(): Unit = { val parent = new DebugRef[String](sys.path / "watchNormal", true) val client = new DebugRef[String](parent.path / "client", true) - val cell = new ActorCell(sys, Empty[String], ec, 1000, parent) + val cell = new ActorCell(sys, Actor.empty[String], ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) debugCell(cell) { @@ -238,7 +253,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala val parent = new DebugRef[String](sys.path / "watchAbnormal", true) val client = new DebugRef[String](parent.path / "client", true) val other = new DebugRef[String](parent.path / "other", true) - val cell = new ActorCell(sys, ContextAware[String] { ctx ⇒ ctx.watch(parent); Empty }, ec, 1000, parent) + val cell = new ActorCell(sys, deferred[String] { ctx ⇒ ctx.watch(parent); Actor.empty }, ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) debugCell(cell) { @@ -270,7 +285,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must signal DeathWatch when watching after termination`(): Unit = { val parent = new DebugRef[String](sys.path / "watchLate", true) val client = new DebugRef[String](parent.path / "client", true) - val cell = new ActorCell(sys, Stopped[String], ec, 1000, parent) + val cell = new ActorCell(sys, stopped[String], ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) debugCell(cell) { @@ -289,7 +304,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must signal DeathWatch when watching after termination but before deactivation`(): Unit = { val parent = new DebugRef[String](sys.path / "watchSomewhatLate", true) val client = new DebugRef[String](parent.path / "client", true) - val cell = new ActorCell(sys, Empty[String], ec, 1000, parent) + val cell = new ActorCell(sys, Actor.empty[String], ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) debugCell(cell) { @@ -309,7 +324,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must not signal DeathWatch after Unwatch has been processed`(): Unit = { val parent = new DebugRef[String](sys.path / "watchUnwatch", true) val client = new DebugRef[String](parent.path / "client", true) - val cell = new ActorCell(sys, Empty[String], ec, 1000, parent) + val cell = new ActorCell(sys, Actor.empty[String], ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) debugCell(cell) { @@ -326,7 +341,7 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must send messages to deadLetters after being terminated`(): Unit = { val parent = new DebugRef[String](sys.path / "sendDeadLetters", true) - val cell = new ActorCell(sys, Stopped[String], ec, 1000, parent) + val cell = new ActorCell(sys, stopped[String], ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) debugCell(cell) { @@ -347,9 +362,9 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala */ def `must not terminate before children have terminated`(): Unit = { val parent = new DebugRef[ActorRef[Nothing]](sys.path / "waitForChild", true) - val cell = new ActorCell(sys, ContextAware[String] { ctx ⇒ - ctx.spawn(SelfAware[String] { self ⇒ parent ! self; Empty }, "child") - Empty + val cell = new ActorCell(sys, deferred[String] { ctx ⇒ + ctx.spawn(deferred[String] { ctx ⇒ parent ! ctx.self; Actor.empty }, "child") + Actor.empty }, ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) @@ -380,9 +395,9 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must properly terminate if failing while handling Terminated for child actor`(): Unit = { val parent = new DebugRef[ActorRef[Nothing]](sys.path / "terminateWhenDeathPact", true) - val cell = new ActorCell(sys, ContextAware[String] { ctx ⇒ - ctx.watch(ctx.spawn(SelfAware[String] { self ⇒ parent ! self; Empty }, "child")) - Empty + val cell = new ActorCell(sys, deferred[String] { ctx ⇒ + ctx.watch(ctx.spawn(deferred[String] { ctx ⇒ parent ! ctx.self; Actor.empty }, "child")) + Actor.empty }, ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) @@ -416,7 +431,11 @@ class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with Scala def `must not terminate twice if failing in PostStop`(): Unit = { val parent = new DebugRef[String](sys.path / "terminateProperlyPostStop", true) - val cell = new ActorCell(sys, Full[String] { case Sig(_, PostStop) ⇒ ??? }, ec, 1000, parent) + val cell = new ActorCell(sys, immutable[String] { + case _ ⇒ unhandled + } onSignal { + case (_, PostStop) ⇒ ??? + }, ec, 1000, parent) val ref = new LocalActorRef(parent.path / "child", cell) cell.setSelf(ref) debugCell(cell) { diff --git a/akka-typed/src/test/scala/akka/typed/internal/ActorSystemSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/internal/ActorSystemSpec.scala similarity index 79% rename from akka-typed/src/test/scala/akka/typed/internal/ActorSystemSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/internal/ActorSystemSpec.scala index 5466b778de..003358eec1 100644 --- a/akka-typed/src/test/scala/akka/typed/internal/ActorSystemSpec.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/internal/ActorSystemSpec.scala @@ -4,21 +4,22 @@ package akka.typed package internal +import akka.Done +import akka.typed.scaladsl.Actor +import akka.typed.scaladsl.Actor._ +import akka.typed.testkit.Inbox +import akka.util.Timeout import org.junit.runner.RunWith import org.scalactic.ConversionCheckedTripleEquals import org.scalatest._ -import org.scalatest.exceptions.TestFailedException -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.concurrent.Eventually +import org.scalatest.concurrent.{ Eventually, ScalaFutures } + import scala.concurrent.duration._ import scala.concurrent.{ Future, Promise } import scala.util.control.NonFatal -import akka.util.Timeout -import akka.Done @RunWith(classOf[org.scalatest.junit.JUnitRunner]) class ActorSystemSpec extends Spec with Matchers with BeforeAndAfterAll with ScalaFutures with Eventually with ConversionCheckedTripleEquals { - import ScalaDSL._ override implicit val patienceConfig = PatienceConfig(1.second) @@ -41,7 +42,7 @@ class ActorSystemSpec extends Spec with Matchers with BeforeAndAfterAll with Sca } def `must start the guardian actor and terminate when it terminates`(): Unit = { - val t = withSystem("a", Total[Probe] { p ⇒ p.replyTo ! p.msg; Stopped }, doTerminate = false) { sys ⇒ + val t = withSystem("a", immutable[Probe] { case (_, p) ⇒ p.replyTo ! p.msg; stopped }, doTerminate = false) { sys ⇒ val inbox = Inbox[String]("a") sys ! Probe("hello", inbox.ref) eventually { inbox.hasMessages should ===(true) } @@ -54,10 +55,12 @@ class ActorSystemSpec extends Spec with Matchers with BeforeAndAfterAll with Sca def `must terminate the guardian actor`(): Unit = { val inbox = Inbox[String]("terminate") - val sys = system("terminate", Full[Probe] { - case Sig(ctx, PostStop) ⇒ + val sys = system("terminate", immutable[Probe] { + case (_, _) ⇒ unhandled + } onSignal { + case (ctx, PostStop) ⇒ inbox.ref ! "done" - Same + same }) sys.terminate().futureValue inbox.receiveAll() should ===("done" :: Nil) @@ -66,19 +69,19 @@ class ActorSystemSpec extends Spec with Matchers with BeforeAndAfterAll with Sca def `must log to the event stream`(): Unit = pending def `must have a name`(): Unit = - withSystem("name", Empty[String]) { sys ⇒ + withSystem("name", Actor.empty[String]) { sys ⇒ sys.name should ===(suite + "-name") } def `must report its uptime`(): Unit = - withSystem("uptime", Empty[String]) { sys ⇒ + withSystem("uptime", Actor.empty[String]) { sys ⇒ sys.uptime should be < 1L Thread.sleep(1000) sys.uptime should be >= 1L } def `must have a working thread factory`(): Unit = - withSystem("thread", Empty[String]) { sys ⇒ + withSystem("thread", Actor.empty[String]) { sys ⇒ val p = Promise[Int] sys.threadFactory.newThread(new Runnable { def run(): Unit = p.success(42) @@ -87,7 +90,7 @@ class ActorSystemSpec extends Spec with Matchers with BeforeAndAfterAll with Sca } def `must be able to run Futures`(): Unit = - withSystem("futures", Empty[String]) { sys ⇒ + withSystem("futures", Actor.empty[String]) { sys ⇒ val f = Future(42)(sys.executionContext) f.futureValue should ===(42) } @@ -100,21 +103,25 @@ class ActorSystemSpec extends Spec with Matchers with BeforeAndAfterAll with Sca // this is essential to complete ActorCellSpec, see there def `must correctly treat Watch dead letters`(): Unit = - withSystem("deadletters", Empty[String]) { sys ⇒ + withSystem("deadletters", Actor.empty[String]) { sys ⇒ val client = new DebugRef[Int](sys.path / "debug", true) sys.deadLetters.sorry.sendSystem(Watch(sys, client)) client.receiveAll() should ===(Left(DeathWatchNotification(sys, null)) :: Nil) } def `must start system actors and mangle their names`(): Unit = { - withSystem("systemActorOf", Empty[String]) { sys ⇒ + withSystem("systemActorOf", Actor.empty[String]) { sys ⇒ import akka.typed.scaladsl.AskPattern._ implicit val timeout = Timeout(1.second) implicit val sched = sys.scheduler case class Doner(ref: ActorRef[Done]) - val ref1, ref2 = sys.systemActorOf(Static[Doner](_.ref ! Done), "empty").futureValue + val ref1, ref2 = sys.systemActorOf(immutable[Doner] { + case (_, doner) ⇒ + doner.ref ! Done + same + }, "empty").futureValue (ref1 ? Doner).futureValue should ===(Done) (ref2 ? Doner).futureValue should ===(Done) val RE = "(\\d+)-empty".r diff --git a/akka-typed/src/test/scala/akka/typed/internal/ActorSystemStub.scala b/akka-typed-tests/src/test/scala/akka/typed/internal/ActorSystemStub.scala similarity index 77% rename from akka-typed/src/test/scala/akka/typed/internal/ActorSystemStub.scala rename to akka-typed-tests/src/test/scala/akka/typed/internal/ActorSystemStub.scala index b05c424bbf..ae080526c5 100644 --- a/akka-typed/src/test/scala/akka/typed/internal/ActorSystemStub.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/internal/ActorSystemStub.scala @@ -5,14 +5,18 @@ package akka.typed package internal import akka.{ actor ⇒ a, event ⇒ e } + import scala.concurrent._ import com.typesafe.config.ConfigFactory import java.util.concurrent.ThreadFactory + +import akka.typed.testkit.Inbox import akka.util.Timeout private[typed] class ActorSystemStub(val name: String) - extends ActorRef[Nothing](a.RootActorPath(a.Address("akka", name)) / "user") - with ActorSystem[Nothing] with ActorRefImpl[Nothing] { + extends ActorSystem[Nothing] with ActorRef[Nothing] with ActorRefImpl[Nothing] { + + override val path: a.ActorPath = a.RootActorPath(a.Address("akka", name)) / "user" override val settings: Settings = new Settings(getClass.getClassLoader, ConfigFactory.empty, name) @@ -57,8 +61,16 @@ private[typed] class ActorSystemStub(val name: String) val receptionistInbox = Inbox[patterns.Receptionist.Command]("receptionist") override def receptionist: ActorRef[patterns.Receptionist.Command] = receptionistInbox.ref - def systemActorOf[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig)(implicit timeout: Timeout): Future[ActorRef[U]] = { + def systemActorOf[U](behavior: Behavior[U], name: String, props: Props)(implicit timeout: Timeout): Future[ActorRef[U]] = { Future.failed(new UnsupportedOperationException("ActorSystemStub cannot create system actors")) } + def registerExtension[T <: Extension](ext: ExtensionId[T]): T = + throw new UnsupportedOperationException("ActorSystemStub cannot register extensions") + + def extension[T <: Extension](ext: ExtensionId[T]): T = + throw new UnsupportedOperationException("ActorSystemStub cannot register extensions") + + def hasExtension(ext: ExtensionId[_ <: Extension]): Boolean = + throw new UnsupportedOperationException("ActorSystemStub cannot register extensions") } diff --git a/akka-typed/src/test/scala/akka/typed/internal/ControlledExecutor.scala b/akka-typed-tests/src/test/scala/akka/typed/internal/ControlledExecutor.scala similarity index 100% rename from akka-typed/src/test/scala/akka/typed/internal/ControlledExecutor.scala rename to akka-typed-tests/src/test/scala/akka/typed/internal/ControlledExecutor.scala diff --git a/akka-typed/src/test/scala/akka/typed/internal/DebugRef.scala b/akka-typed-tests/src/test/scala/akka/typed/internal/DebugRef.scala similarity index 91% rename from akka-typed/src/test/scala/akka/typed/internal/DebugRef.scala rename to akka-typed-tests/src/test/scala/akka/typed/internal/DebugRef.scala index 464c040d70..24223c10ec 100644 --- a/akka-typed/src/test/scala/akka/typed/internal/DebugRef.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/internal/DebugRef.scala @@ -8,8 +8,8 @@ import akka.{ actor ⇒ a } import java.util.concurrent.ConcurrentLinkedQueue import scala.annotation.tailrec -private[typed] class DebugRef[T](_path: a.ActorPath, override val isLocal: Boolean) - extends ActorRef[T](_path) with ActorRefImpl[T] { +private[typed] class DebugRef[T](override val path: a.ActorPath, override val isLocal: Boolean) + extends ActorRef[T] with ActorRefImpl[T] { private val q = new ConcurrentLinkedQueue[Either[SystemMessage, T]] diff --git a/akka-typed/src/test/scala/akka/typed/internal/EventStreamSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/internal/EventStreamSpec.scala similarity index 95% rename from akka-typed/src/test/scala/akka/typed/internal/EventStreamSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/internal/EventStreamSpec.scala index 4aedbea9b1..3b8dc64acc 100644 --- a/akka-typed/src/test/scala/akka/typed/internal/EventStreamSpec.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/internal/EventStreamSpec.scala @@ -4,27 +4,31 @@ package akka.typed package internal -import scala.concurrent.duration._ import akka.Done import akka.event.Logging._ -import akka.typed.ScalaDSL._ +import akka.typed.scaladsl.Actor._ import akka.typed.scaladsl.AskPattern._ +import akka.typed.testkit.Inbox import com.typesafe.config.ConfigFactory -import java.util.concurrent.Executor import org.scalatest.concurrent.Eventually import org.scalatest.concurrent.PatienceConfiguration.Timeout +import scala.concurrent.duration._ + object EventStreamSpec { @volatile var logged = Vector.empty[LogEvent] class MyLogger extends Logger { def initialBehavior: Behavior[Logger.Command] = - ContextAware { ctx ⇒ - Total { - case Logger.Initialize(es, replyTo) ⇒ - replyTo ! ctx.watch(ctx.spawn(Static { (ev: LogEvent) ⇒ logged :+= ev }, "logger")) - Empty - } + immutable { + case (ctx, Logger.Initialize(es, replyTo)) ⇒ + val logger = ctx.spawn(immutable[LogEvent] { (_, ev: LogEvent) ⇒ + logged :+= ev + same + }, "logger") + ctx.watch(logger) + replyTo ! logger + empty } } @@ -262,7 +266,7 @@ class EventStreamSpec extends TypedSpec(EventStreamSpec.config) with Eventually } def `must unsubscribe an actor upon termination`(): Unit = { - val ref = nativeSystem ? TypedSpec.Create(Total[Done](d ⇒ Stopped), "tester") futureValue Timeout(1.second) + val ref = nativeSystem ? TypedSpec.Create(immutable[Done] { case _ ⇒ stopped }, "tester") futureValue Timeout(1.second) es.subscribe(ref, classOf[Done]) should ===(true) es.subscribe(ref, classOf[Done]) should ===(false) ref ! Done @@ -270,7 +274,7 @@ class EventStreamSpec extends TypedSpec(EventStreamSpec.config) with Eventually } def `must unsubscribe the actor, when it subscribes already in terminated state`(): Unit = { - val ref = nativeSystem ? TypedSpec.Create(Stopped[Done], "tester") futureValue Timeout(1.second) + val ref = nativeSystem ? TypedSpec.Create(stopped[Done], "tester") futureValue Timeout(1.second) val wait = new DebugRef[Done](root / "wait", true) ref.sorry.sendSystem(Watch(ref, wait)) eventually(wait.hasSignal should ===(true)) diff --git a/akka-typed/src/test/scala/akka/typed/internal/FunctionRefSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/internal/FunctionRefSpec.scala similarity index 100% rename from akka-typed/src/test/scala/akka/typed/internal/FunctionRefSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/internal/FunctionRefSpec.scala diff --git a/akka-typed/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala similarity index 96% rename from akka-typed/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala rename to akka-typed-tests/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala index 93ac31ea51..e7d54af006 100644 --- a/akka-typed/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala +++ b/akka-typed-tests/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala @@ -4,20 +4,23 @@ package akka.typed.patterns import Receptionist._ -import akka.typed.ScalaDSL._ import akka.typed.scaladsl.AskPattern._ + import scala.concurrent.duration._ import akka.typed._ +import akka.typed.scaladsl.Actor +import akka.typed.scaladsl.Actor._ +import akka.typed.testkit.{ Effect, EffectfulActorContext, Inbox } class ReceptionistSpec extends TypedSpec { trait ServiceA case object ServiceKeyA extends ServiceKey[ServiceA] - val behaviorA = Static[ServiceA](msg ⇒ ()) + val behaviorA = Actor.empty[ServiceA] trait ServiceB case object ServiceKeyB extends ServiceKey[ServiceB] - val behaviorB = Static[ServiceB](msg ⇒ ()) + val behaviorB = Actor.empty[ServiceB] trait CommonTests { implicit def system: ActorSystem[TypedSpec.Command] diff --git a/akka-typed-tests/src/test/scala/akka/typed/scaladsl/adapter/AdapterSpec.scala b/akka-typed-tests/src/test/scala/akka/typed/scaladsl/adapter/AdapterSpec.scala new file mode 100644 index 0000000000..4ad7bf0b50 --- /dev/null +++ b/akka-typed-tests/src/test/scala/akka/typed/scaladsl/adapter/AdapterSpec.scala @@ -0,0 +1,254 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.scaladsl.adapter + +import scala.concurrent.duration._ +import scala.util.control.NoStackTrace +import akka.{ actor ⇒ untyped } +import akka.typed.ActorRef +import akka.typed.Behavior +import akka.typed.Terminated +import akka.typed.scaladsl.Actor +import akka.{ actor ⇒ untyped } +import akka.annotation.ApiMayChange +import akka.annotation.DoNotInherit +import akka.annotation.InternalApi +import akka.testkit._ + +object AdapterSpec { + val untyped1: untyped.Props = untyped.Props(new Untyped1) + + class Untyped1 extends untyped.Actor { + def receive = { + case "ping" ⇒ sender() ! "pong" + case t: ThrowIt ⇒ throw t + } + } + + def typed1(ref: untyped.ActorRef, probe: ActorRef[String]): Behavior[String] = + Actor.immutable[String] { + (ctx, msg) ⇒ + msg match { + case "send" ⇒ + val replyTo = ctx.self.toUntyped + ref.tell("ping", replyTo) + Actor.same + case "pong" ⇒ + probe ! "ok" + Actor.same + case "actorOf" ⇒ + val child = ctx.actorOf(untyped1) + child.tell("ping", ctx.self.toUntyped) + Actor.same + case "watch" ⇒ + ctx.watch(ref) + Actor.same + case "supervise-stop" ⇒ + val child = ctx.actorOf(untyped1) + ctx.watch(child) + child ! ThrowIt3 + child.tell("ping", ctx.self.toUntyped) + Actor.same + case "stop-child" ⇒ + val child = ctx.actorOf(untyped1) + ctx.watch(child) + ctx.stop(child) + Actor.same + } + } onSignal { + case (ctx, Terminated(ref)) ⇒ + probe ! "terminated" + Actor.same + } + + sealed trait Typed2Msg + final case class Ping(replyTo: ActorRef[String]) extends Typed2Msg + case object StopIt extends Typed2Msg + sealed trait ThrowIt extends RuntimeException with Typed2Msg with NoStackTrace + case object ThrowIt1 extends ThrowIt + case object ThrowIt2 extends ThrowIt + case object ThrowIt3 extends ThrowIt + + def untyped2(ref: ActorRef[Ping], probe: ActorRef[String]): untyped.Props = + untyped.Props(new Untyped2(ref, probe)) + + class Untyped2(ref: ActorRef[Ping], probe: ActorRef[String]) extends untyped.Actor { + + override val supervisorStrategy = untyped.OneForOneStrategy() { + ({ + case ThrowIt1 ⇒ + probe ! "thrown-stop" + untyped.SupervisorStrategy.Stop + case ThrowIt2 ⇒ + probe ! "thrown-resume" + untyped.SupervisorStrategy.Resume + case ThrowIt3 ⇒ + probe ! "thrown-restart" + // TODO Restart will not really restart the behavior + untyped.SupervisorStrategy.Restart + }: untyped.SupervisorStrategy.Decider).orElse(untyped.SupervisorStrategy.defaultDecider) + } + + def receive = { + case "send" ⇒ ref ! Ping(self) // implicit conversion + case "pong" ⇒ probe ! "ok" + case "spawn" ⇒ + val child = context.spawnAnonymous(typed2) + child ! Ping(self) + case "actorOf-props" ⇒ + // this is how Cluster Sharding can be used + val child = context.actorOf(typed2Props) + child ! Ping(self) + case "watch" ⇒ + context.watch(ref) + case untyped.Terminated(_) ⇒ + probe ! "terminated" + case "supervise-stop" ⇒ + testSupervice(ThrowIt1) + case "supervise-resume" ⇒ + testSupervice(ThrowIt2) + case "supervise-restart" ⇒ + testSupervice(ThrowIt3) + case "stop-child" ⇒ + val child = context.spawnAnonymous(typed2) + context.watch(child) + context.stop(child) + } + + private def testSupervice(t: ThrowIt): Unit = { + val child = context.spawnAnonymous(typed2) + context.watch(child) + child ! t + child ! Ping(self) + } + } + + def typed2: Behavior[Typed2Msg] = + Actor.immutable { (ctx, msg) ⇒ + msg match { + case Ping(replyTo) ⇒ + replyTo ! "pong" + Actor.same + case StopIt ⇒ + Actor.stopped + case t: ThrowIt ⇒ + throw t + } + } + + def typed2Props: untyped.Props = PropsAdapter(typed2) + +} + +class AdapterSpec extends AkkaSpec { + import AdapterSpec._ + + "Adapted actors" must { + + "send message from typed to untyped" in { + val probe = TestProbe() + val untypedRef = system.actorOf(untyped1) + val typedRef = system.spawnAnonymous(typed1(untypedRef, probe.ref)) + typedRef ! "send" + probe.expectMsg("ok") + } + + "send message from untyped to typed" in { + val probe = TestProbe() + val typedRef = system.spawnAnonymous(typed2) + val untypedRef = system.actorOf(untyped2(typedRef, probe.ref)) + untypedRef ! "send" + probe.expectMsg("ok") + } + + "spawn typed child from untyped parent" in { + val probe = TestProbe() + val ign = system.spawnAnonymous(Actor.ignore[Ping]) + val untypedRef = system.actorOf(untyped2(ign, probe.ref)) + untypedRef ! "spawn" + probe.expectMsg("ok") + } + + "actorOf typed child via Props from untyped parent" in { + val probe = TestProbe() + val ign = system.spawnAnonymous(Actor.ignore[Ping]) + val untypedRef = system.actorOf(untyped2(ign, probe.ref)) + untypedRef ! "actorOf-props" + probe.expectMsg("ok") + } + + "actorOf untyped child from typed parent" in { + val probe = TestProbe() + val ignore = system.actorOf(untyped.Props.empty) + val typedRef = system.spawnAnonymous(typed1(ignore, probe.ref)) + typedRef ! "actorOf" + probe.expectMsg("ok") + } + + "watch typed from untyped" in { + val probe = TestProbe() + val typedRef = system.spawnAnonymous(typed2) + val untypedRef = system.actorOf(untyped2(typedRef, probe.ref)) + untypedRef ! "watch" + typedRef ! StopIt + probe.expectMsg("terminated") + } + + "watch untyped from typed" in { + val probe = TestProbe() + val untypedRef = system.actorOf(untyped1) + val typedRef = system.spawnAnonymous(typed1(untypedRef, probe.ref)) + typedRef ! "watch" + untypedRef ! untyped.PoisonPill + probe.expectMsg("terminated") + } + + "supervise typed child from untyped parent" in { + val probe = TestProbe() + val ign = system.spawnAnonymous(Actor.ignore[Ping]) + val untypedRef = system.actorOf(untyped2(ign, probe.ref)) + + untypedRef ! "supervise-stop" + probe.expectMsg("thrown-stop") + // ping => ok should not get through here + probe.expectMsg("terminated") + + untypedRef ! "supervise-resume" + probe.expectMsg("thrown-resume") + probe.expectMsg("ok") + + untypedRef ! "supervise-restart" + probe.expectMsg("thrown-restart") + probe.expectMsg("ok") + } + + "supervise untyped child from typed parent" in { + val probe = TestProbe() + val ignore = system.actorOf(untyped.Props.empty) + val typedRef = system.spawnAnonymous(typed1(ignore, probe.ref)) + + // only stop supervisorStrategy + typedRef ! "supervise-stop" + probe.expectMsg("terminated") + probe.expectNoMsg(100.millis) // no pong + } + + "stop typed child from untyped parent" in { + val probe = TestProbe() + val ignore = system.spawnAnonymous(Actor.ignore[Ping]) + val untypedRef = system.actorOf(untyped2(ignore, probe.ref)) + untypedRef ! "stop-child" + probe.expectMsg("terminated") + } + + "stop untyped child from typed parent" in { + val probe = TestProbe() + val ignore = system.actorOf(untyped.Props.empty) + val typedRef = system.spawnAnonymous(typed1(ignore, probe.ref)) + typedRef ! "stop-child" + probe.expectMsg("terminated") + } + } + +} diff --git a/akka-docs/src/test/scala/docs/akka/typed/IntroSpec.scala b/akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala similarity index 74% rename from akka-docs/src/test/scala/docs/akka/typed/IntroSpec.scala rename to akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala index 233f5d403c..65214cacfe 100644 --- a/akka-docs/src/test/scala/docs/akka/typed/IntroSpec.scala +++ b/akka-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala @@ -5,14 +5,12 @@ package docs.akka.typed //#imports import akka.typed._ -import akka.typed.scaladsl.Actor._ +import akka.typed.scaladsl.Actor import akka.typed.scaladsl.AskPattern._ import scala.concurrent.Future import scala.concurrent.duration._ import scala.concurrent.Await //#imports -import akka.testkit.AkkaSpec -import akka.typed.TypedSpec object IntroSpec { @@ -21,9 +19,10 @@ object IntroSpec { final case class Greet(whom: String, replyTo: ActorRef[Greeted]) final case class Greeted(whom: String) - val greeter = Stateless[Greet] { (_, msg) ⇒ + val greeter = Actor.immutable[Greet] { (_, msg) ⇒ println(s"Hello ${msg.whom}!") msg.replyTo ! Greeted(msg.whom) + Actor.same } } //#hello-world-actor @@ -50,8 +49,11 @@ object IntroSpec { //#chatroom-protocol //#chatroom-behavior - def chatRoom(sessions: List[ActorRef[SessionEvent]] = List.empty): Behavior[Command] = - Stateful[Command] { (ctx, msg) ⇒ + val behavior: Behavior[Command] = + chatRoom(List.empty) + + private def chatRoom(sessions: List[ActorRef[SessionEvent]]): Behavior[Command] = + Actor.immutable[Command] { (ctx, msg) ⇒ msg match { case GetSession(screenName, client) ⇒ val wrapper = ctx.spawnAdapter { @@ -62,7 +64,7 @@ object IntroSpec { case PostSessionMessage(screenName, message) ⇒ val mp = MessagePosted(screenName, message) sessions foreach (_ ! mp) - Same + Actor.same } } //#chatroom-behavior @@ -97,42 +99,42 @@ class IntroSpec extends TypedSpec { import ChatRoom._ val gabbler = - Stateful[SessionEvent] { (_, msg) ⇒ + Actor.immutable[SessionEvent] { (_, msg) ⇒ msg match { + //#chatroom-gabbler + // We document that the compiler warns about the missing handler for `SessionDenied` case SessionDenied(reason) ⇒ println(s"cannot start chat room session: $reason") - Stopped + Actor.stopped + //#chatroom-gabbler case SessionGranted(handle) ⇒ handle ! PostMessage("Hello World!") - Same + Actor.same case MessagePosted(screenName, message) ⇒ println(s"message has been posted by '$screenName': $message") - Stopped + Actor.stopped } } //#chatroom-gabbler //#chatroom-main val main: Behavior[akka.NotUsed] = - Stateful( - behavior = (_, _) ⇒ Unhandled, - signal = { (ctx, sig) ⇒ - sig match { - case PreStart ⇒ - val chatRoom = ctx.spawn(ChatRoom.chatRoom(), "chatroom") - val gabblerRef = ctx.spawn(gabbler, "gabbler") - ctx.watch(gabblerRef) - chatRoom ! GetSession("ol’ Gabbler", gabblerRef) - Same - case Terminated(ref) ⇒ - Stopped - case _ ⇒ - Unhandled + Actor.deferred { ctx ⇒ + val chatRoom = ctx.spawn(ChatRoom.behavior, "chatroom") + val gabblerRef = ctx.spawn(gabbler, "gabbler") + ctx.watch(gabblerRef) + chatRoom ! GetSession("ol’ Gabbler", gabblerRef) + + Actor.immutable[akka.NotUsed] { + (_, _) ⇒ Actor.unhandled + } onSignal { + case (ctx, Terminated(ref)) ⇒ + Actor.stopped } - }) + } val system = ActorSystem("ChatRoomDemo", main) - Await.result(system.whenTerminated, 1.second) + Await.result(system.whenTerminated, 3.seconds) //#chatroom-main } diff --git a/akka-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala b/akka-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala new file mode 100644 index 0000000000..8b86e518a7 --- /dev/null +++ b/akka-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2014-2017 Lightbend Inc. + */ +package docs.akka.typed + +//#imports +import akka.typed._ +import akka.typed.scaladsl.Actor +import akka.typed.scaladsl.ActorContext +import akka.typed.scaladsl.AskPattern._ +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.concurrent.Await +//#imports + +object MutableIntroSpec { + + //#chatroom-actor + object ChatRoom { + //#chatroom-protocol + sealed trait Command + final case class GetSession(screenName: String, replyTo: ActorRef[SessionEvent]) + extends Command + //#chatroom-protocol + //#chatroom-behavior + private final case class PostSessionMessage(screenName: String, message: String) + extends Command + //#chatroom-behavior + //#chatroom-protocol + + sealed trait SessionEvent + final case class SessionGranted(handle: ActorRef[PostMessage]) extends SessionEvent + final case class SessionDenied(reason: String) extends SessionEvent + final case class MessagePosted(screenName: String, message: String) extends SessionEvent + + final case class PostMessage(message: String) + //#chatroom-protocol + //#chatroom-behavior + + def behavior(): Behavior[Command] = + Actor.mutable[Command](ctx ⇒ new ChatRoomBehavior(ctx)) + + class ChatRoomBehavior(ctx: ActorContext[Command]) extends Actor.MutableBehavior[Command] { + private var sessions: List[ActorRef[SessionEvent]] = List.empty + + override def onMessage(msg: Command): Behavior[Command] = { + msg match { + case GetSession(screenName, client) ⇒ + val wrapper = ctx.spawnAdapter { + p: PostMessage ⇒ PostSessionMessage(screenName, p.message) + } + client ! SessionGranted(wrapper) + sessions = client :: sessions + this + case PostSessionMessage(screenName, message) ⇒ + val mp = MessagePosted(screenName, message) + sessions foreach (_ ! mp) + this + } + } + + } + //#chatroom-behavior + } + //#chatroom-actor + +} + +class MutableIntroSpec extends TypedSpec { + import MutableIntroSpec._ + + def `must chat`(): Unit = { + //#chatroom-gabbler + import ChatRoom._ + + val gabbler = + Actor.immutable[SessionEvent] { (_, msg) ⇒ + msg match { + case SessionDenied(reason) ⇒ + println(s"cannot start chat room session: $reason") + Actor.stopped + case SessionGranted(handle) ⇒ + handle ! PostMessage("Hello World!") + Actor.same + case MessagePosted(screenName, message) ⇒ + println(s"message has been posted by '$screenName': $message") + Actor.stopped + } + } + //#chatroom-gabbler + + //#chatroom-main + val main: Behavior[akka.NotUsed] = + Actor.deferred { ctx ⇒ + val chatRoom = ctx.spawn(ChatRoom.behavior(), "chatroom") + val gabblerRef = ctx.spawn(gabbler, "gabbler") + ctx.watch(gabblerRef) + chatRoom ! GetSession("ol’ Gabbler", gabblerRef) + + Actor.immutable[akka.NotUsed] { + (_, _) ⇒ Actor.unhandled + } onSignal { + case (ctx, Terminated(ref)) ⇒ + Actor.stopped + } + } + + val system = ActorSystem("ChatRoomDemo", main) + Await.result(system.whenTerminated, 1.second) + //#chatroom-main + } + +} diff --git a/akka-typed/build.sbt b/akka-typed/build.sbt index 2ee6ecd446..77aabbf672 100644 --- a/akka-typed/build.sbt +++ b/akka-typed/build.sbt @@ -8,7 +8,7 @@ disablePlugins(MimaPlugin) initialCommands := """ import akka.typed._ - import ScalaDSL._ + import import akka.typed.scaladsl.Actor import scala.concurrent._ import duration._ import akka.util.Timeout diff --git a/akka-typed/src/main/java/akka/typed/javadsl/Actor.java b/akka-typed/src/main/java/akka/typed/javadsl/Actor.java deleted file mode 100644 index 9bfddb38c5..0000000000 --- a/akka-typed/src/main/java/akka/typed/javadsl/Actor.java +++ /dev/null @@ -1,385 +0,0 @@ -/** - * Copyright (C) 2017 Lightbend Inc. - */ -package akka.typed.javadsl; - -import static akka.typed.scaladsl.Actor.Empty; -import static akka.typed.scaladsl.Actor.Ignore; -import static akka.typed.scaladsl.Actor.Same; -import static akka.typed.scaladsl.Actor.Stopped; -import static akka.typed.scaladsl.Actor.Unhandled; - -import java.util.function.Function; - -import akka.japi.function.Function2; -import akka.japi.function.Procedure2; -import akka.japi.pf.PFBuilder; -import akka.typed.ActorRef; -import akka.typed.Behavior; -import akka.typed.PreStart; -import akka.typed.Signal; -import akka.typed.patterns.Restarter; -import akka.typed.scaladsl.Actor.Widened; -import scala.reflect.ClassTag; - -public abstract class Actor { - /* - * This DSL is implemented in Java in order to ensure consistent usability from Java, - * taking possible Scala oddities out of the equation. There is some duplication in - * the behavior implementations, but that is unavoidable if both DSLs shall offer the - * same runtime performance (especially concerning allocations for function converters). - */ - - private static class Deferred extends Behavior { - final akka.japi.function.Function, Behavior> producer; - - public Deferred(akka.japi.function.Function, Behavior> producer) { - this.producer = producer; - } - - @Override - public Behavior management(akka.typed.ActorContext ctx, Signal msg) throws Exception { - if (msg instanceof PreStart) { - return Behavior.preStart(producer.apply(ctx), ctx); - } else - throw new IllegalStateException("Deferred behavior must receive PreStart as first signal"); - } - - @Override - public Behavior message(akka.typed.ActorContext ctx, T msg) throws Exception { - throw new IllegalStateException("Deferred behavior must receive PreStart as first signal"); - } - } - - private static class Stateful extends Behavior { - final Function2, T, Behavior> message; - final Function2, Signal, Behavior> signal; - - public Stateful(Function2, T, Behavior> message, - Function2, Signal, Behavior> signal) { - this.signal = signal; - this.message = message; - } - - @Override - public Behavior management(akka.typed.ActorContext ctx, Signal msg) throws Exception { - return signal.apply(ctx, msg); - } - - @Override - public Behavior message(akka.typed.ActorContext ctx, T msg) throws Exception { - return message.apply(ctx, msg); - } - } - - private static class Stateless extends Behavior { - final Procedure2, T> message; - - public Stateless(Procedure2, T> message) { - this.message = message; - } - - @Override - public Behavior management(akka.typed.ActorContext ctx, Signal msg) throws Exception { - return Same(); - } - - @Override - public Behavior message(akka.typed.ActorContext ctx, T msg) throws Exception { - message.apply(ctx, msg); - return Same(); - } - } - - private static class Tap extends Behavior { - final Procedure2, Signal> signal; - final Procedure2, T> message; - final Behavior behavior; - - public Tap(Procedure2, Signal> signal, Procedure2, T> message, - Behavior behavior) { - this.signal = signal; - this.message = message; - this.behavior = behavior; - } - - private Behavior canonicalize(Behavior behv) { - if (Behavior.isUnhandled(behv)) - return Unhandled(); - else if (behv == Same()) - return Same(); - else if (Behavior.isAlive(behv)) - return new Tap(signal, message, behv); - else - return Stopped(); - } - - @Override - public Behavior management(akka.typed.ActorContext ctx, Signal msg) throws Exception { - signal.apply(ctx, msg); - return canonicalize(behavior.management(ctx, msg)); - } - - @Override - public Behavior message(akka.typed.ActorContext ctx, T msg) throws Exception { - message.apply(ctx, msg); - return canonicalize(behavior.message(ctx, msg)); - } - } - - /** - * Mode selector for the {@link #restarter} wrapper that decides whether an actor - * upon a failure shall be restarted (to clean initial state) or resumed (keep - * on running, with potentially compromised state). - * - * Resuming is less safe. If you use OnFailure.RESUME you should at least - * not hold mutable data fields or collections within the actor as those might - * be in an inconsistent state (the exception might have interrupted normal - * processing); avoiding mutable state is possible by returning a fresh - * behavior with the new state after every message. - */ - public enum OnFailure { - RESUME, RESTART; - } - - private static Function2 _unhandledFun = (ctx, msg) -> Unhandled(); - - @SuppressWarnings("unchecked") - private static Function2, Signal, Behavior> unhandledFun() { - return (Function2, Signal, Behavior>) (Object) _unhandledFun; - } - - private static Procedure2 _doNothing = (ctx, msg) -> { - }; - - @SuppressWarnings("unchecked") - private static Procedure2, Signal> doNothing() { - return (Procedure2, Signal>) (Object) _doNothing; - } - - /** - * Construct an actor behavior that can react to incoming messages but not to - * lifecycle signals. After spawning this actor from another actor (or as the - * guardian of an {@link akka.typed.ActorSystem}) it will be executed within an - * {@link ActorContext} that allows access to the system, spawning and watching - * other actors, etc. - * - * This constructor is called stateful because processing the next message - * results in a new behavior that can potentially be different from this one. - * If no change is desired, use {@link #same}. - * - * @param message - * the function that describes how this actor reacts to the next - * message - * @return the behavior - */ - static public Behavior stateful(Function2, T, Behavior> message) { - return new Stateful(message, unhandledFun()); - } - - /** - * Construct an actor behavior that can react to both incoming messages and - * lifecycle signals. After spawning this actor from another actor (or as the - * guardian of an {@link akka.typed.ActorSystem}) it will be executed within an - * {@link ActorContext} that allows access to the system, spawning and watching - * other actors, etc. - * - * This constructor is called stateful because processing the next message - * results in a new behavior that can potentially be different from this one. - * If no change is desired, use {@link #same}. - * - * @param message - * the function that describes how this actor reacts to the next - * message - * @param signal - * the function that describes how this actor reacts to the given - * signal - * @return the behavior - */ - static public Behavior stateful(Function2, T, Behavior> message, - Function2, Signal, Behavior> signal) { - return new Stateful(message, signal); - } - - /** - * Construct an actor behavior that can react to incoming messages but not to - * lifecycle signals. After spawning this actor from another actor (or as the - * guardian of an {@link akka.typed.ActorSystem}) it will be executed within an - * {@link ActorContext} that allows access to the system, spawning and watching - * other actors, etc. - * - * This constructor is called stateless because it cannot be replaced by - * another one after it has been installed. It is most useful for leaf actors - * that do not create child actors themselves. - * - * @param message - * the function that describes how this actor reacts to the next - * message - * @return the behavior - */ - static public Behavior stateless(Procedure2, T> message) { - return new Stateless(message); - } - - /** - * Return this behavior from message processing in order to advise the system - * to reuse the previous behavior. This is provided in order to avoid the - * allocation overhead of recreating the current behavior where that is not - * necessary. - * - * @return pseudo-behavior marking “no change” - */ - static public Behavior same() { - return Same(); - } - - /** - * Return this behavior from message processing in order to advise the system - * to reuse the previous behavior, including the hint that the message has not - * been handled. This hint may be used by composite behaviors that delegate - * (partial) handling to other behaviors. - * - * @return pseudo-behavior marking “unhandled” - */ - static public Behavior unhandled() { - return Unhandled(); - } - - /** - * Return this behavior from message processing to signal that this actor - * shall terminate voluntarily. If this actor has created child actors then - * these will be stopped as part of the shutdown procedure. The PostStop - * signal that results from stopping this actor will NOT be passed to the - * current behavior, it will be effectively ignored. - * - * @return the inert behavior - */ - static public Behavior stopped() { - return Stopped(); - } - - /** - * A behavior that treats every incoming message as unhandled. - * - * @return the empty behavior - */ - static public Behavior empty() { - return Empty(); - } - - /** - * A behavior that ignores every incoming message and returns “same”. - * - * @return the inert behavior - */ - static public Behavior ignore() { - return Ignore(); - } - - /** - * Behavior decorator that allows you to perform any side-effect before a - * signal or message is delivered to the wrapped behavior. The wrapped - * behavior can evolve (i.e. be stateful) without needing to be wrapped in a - * tap(...) call again. - * - * @param signal - * procedure to invoke with the {@link ActorContext} and the - * {@link akka.typed.Signal} as arguments before delivering the signal to - * the wrapped behavior - * @param message - * procedure to invoke with the {@link ActorContext} and the received - * message as arguments before delivering the signal to the wrapped - * behavior - * @param behavior - * initial behavior to be wrapped - * @return the decorated behavior - */ - static public Behavior tap(Procedure2, Signal> signal, Procedure2, T> message, - Behavior behavior) { - return new Tap(signal, message, behavior); - } - - /** - * Behavior decorator that copies all received message to the designated - * monitor {@link akka.typed.ActorRef} before invoking the wrapped behavior. The - * wrapped behavior can evolve (i.e. be stateful) without needing to be - * wrapped in a monitor(...) call again. - * - * @param monitor - * ActorRef to which to copy all received messages - * @param behavior - * initial behavior to be wrapped - * @return the decorated behavior - */ - static public Behavior monitor(ActorRef monitor, Behavior behavior) { - return new Tap(doNothing(), (ctx, msg) -> monitor.tell(msg), behavior); - } - - /** - * Wrap the given behavior such that it is restarted (i.e. reset to its - * initial state) whenever it throws an exception of the given class or a - * subclass thereof. Exceptions that are not subtypes of Thr will not be - * caught and thus lead to the termination of the actor. - * - * It is possible to specify that the actor shall not be restarted but - * resumed. This entails keeping the same state as before the exception was - * thrown and is thus less safe. If you use OnFailure.RESUME you should at - * least not hold mutable data fields or collections within the actor as those - * might be in an inconsistent state (the exception might have interrupted - * normal processing); avoiding mutable state is possible by returning a fresh - * behavior with the new state after every message. - * - * @param clazz - * the type of exceptions that shall be caught - * @param mode - * whether to restart or resume the actor upon a caught failure - * @param initialBehavior - * the initial behavior, that is also restored during a restart - * @return the wrapped behavior - */ - static public Behavior restarter(Class clazz, OnFailure mode, - Behavior initialBehavior) { - final ClassTag catcher = akka.japi.Util.classTag(clazz); - return new Restarter(initialBehavior, mode == OnFailure.RESUME, initialBehavior, catcher); - } - - /** - * Widen the wrapped Behavior by placing a funnel in front of it: the supplied - * PartialFunction decides which message to pull in (those that it is defined - * at) and may transform the incoming message to place them into the wrapped - * Behavior’s type hierarchy. Signals are not transformed. - * - *
-   * Behavior<String> s = stateless((ctx, msg) -> System.out.println(msg))
-   * Behavior<Number> n = widened(s, pf -> pf.
-   *         match(BigInteger.class, i -> "BigInteger(" + i + ")").
-   *         match(BigDecimal.class, d -> "BigDecimal(" + d + ")")
-   *         // drop all other kinds of Number
-   *     );
-   * 
- * - * @param behavior - * the behavior that will receive the selected messages - * @param selector - * a partial function builder for describing the selection and - * transformation - * @return a behavior of the widened type - */ - static public Behavior widened(Behavior behavior, Function, PFBuilder> selector) { - return new Widened(behavior, selector.apply(new PFBuilder<>()).build()); - } - - /** - * Wrap a behavior factory so that it runs upon PreStart, i.e. behavior - * creation is deferred to the child actor instead of running within the - * parent. - * - * @param producer - * behavior factory that takes the child actor’s context as argument - * @return the deferred behavior - */ - static public Behavior deferred(akka.japi.function.Function, Behavior> producer) { - return new Deferred(producer); - } - -} diff --git a/akka-typed/src/main/java/akka/typed/javadsl/ActorContext.java b/akka-typed/src/main/java/akka/typed/javadsl/ActorContext.java deleted file mode 100644 index 5021e3c1e1..0000000000 --- a/akka-typed/src/main/java/akka/typed/javadsl/ActorContext.java +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Copyright (C) 2017 Lightbend Inc. - */ -package akka.typed.javadsl; - -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -import akka.actor.Cancellable; -import akka.typed.ActorRef; -import akka.typed.ActorSystem; -import akka.typed.Behavior; -import akka.typed.DeploymentConfig; -import scala.concurrent.ExecutionContextExecutor; -import scala.concurrent.duration.FiniteDuration; - -/** - * An Actor is given by the combination of a {@link akka.typed.Behavior} and a context in which - * this behavior is executed. As per the Actor Model an Actor can perform the - * following actions when processing a message: - * - *
    - *
  • send a finite number of messages to other Actors it knows
  • - *
  • create a finite number of Actors
  • - *
  • designate the behavior for the next message
  • - *
- * - * In Akka the first capability is accessed by using the {@link akka.typed.ActorRef#tell} - * method, the second is provided by {@link akka.typed.ActorContext#spawn} and the - * third is implicit in the signature of {@link akka.typed.Behavior} in that the next behavior - * is always returned from the message processing logic. - * - * An ActorContext in addition provides access to the Actor’s own identity - * ({@link #getSelf “self”}), the {@link akka.typed.ActorSystem} it is part of, methods for querying the list - * of child Actors it created, access to {@link akka.typed.Terminated DeathWatch} and timed - * message scheduling. - */ -public interface ActorContext { - - /** - * The identity of this Actor, bound to the lifecycle of this Actor instance. - * An Actor with the same name that lives before or after this instance will - * have a different {@link akka.typed.ActorRef}. - */ - public ActorRef getSelf(); - - /** - * Return the mailbox capacity that was configured by the parent for this - * actor. - */ - public int getMailboxCapacity(); - - /** - * The {@link akka.typed.ActorSystem} to which this Actor belongs. - */ - public ActorSystem getSystem(); - - /** - * The list of child Actors created by this Actor during its lifetime that are - * still alive, in no particular order. - */ - public List> getChildren(); - - /** - * The named child Actor if it is alive. - */ - public Optional> getChild(String name); - - /** - * Create a child Actor from the given {@link akka.typed.Behavior} under a randomly chosen name. - * It is good practice to name Actors wherever practical. - */ - public ActorRef spawnAnonymous(Behavior behavior); - - /** - * Create a child Actor from the given {@link akka.typed.Behavior} under a randomly chosen name. - * It is good practice to name Actors wherever practical. - */ - public ActorRef spawnAnonymous(Behavior behavior, DeploymentConfig deployment); - - /** - * Create a child Actor from the given {@link akka.typed.Behavior} and with the given name. - */ - public ActorRef spawn(Behavior behavior, String name); - - /** - * Create a child Actor from the given {@link akka.typed.Behavior} and with the given name. - */ - public ActorRef spawn(Behavior behavior, String name, DeploymentConfig deployment); - - /** - * Force the child Actor under the given name to terminate after it finishes - * processing its current message. Nothing happens if the ActorRef does not - * refer to a current child actor. - * - * @return whether the passed-in {@link akka.typed.ActorRef} points to a current child Actor - */ - public boolean stop(ActorRef child); - - /** - * Register for {@link akka.typed.Terminated} notification once the Actor identified by the - * given {@link akka.typed.ActorRef} terminates. This notification is also generated when the - * {@link akka.typed.ActorSystem} to which the referenced Actor belongs is declared as failed - * (e.g. in reaction to being unreachable). - */ - public ActorRef watch(ActorRef other); - - /** - * Revoke the registration established by {@link #watch}. A {@link akka.typed.Terminated} - * notification will not subsequently be received for the referenced Actor. - */ - public ActorRef unwatch(ActorRef other); - - /** - * Schedule the sending of a notification in case no other message is received - * during the given period of time. The timeout starts anew with each received - * message. Provide scala.concurrent.duration.Duration.Undefined to switch off this mechanism. - */ - public void setReceiveTimeout(FiniteDuration d, T msg); - - /** - * Cancel the sending of receive timeout notifications. - */ - public void cancelReceiveTimeout(); - - /** - * Schedule the sending of the given message to the given target Actor after - * the given time period has elapsed. The scheduled action can be cancelled by - * invoking {@link akka.actor.Cancellable#cancel} on the returned handle. - */ - public Cancellable schedule(FiniteDuration delay, ActorRef target, U msg); - - /** - * This Actor’s execution context. It can be used to run asynchronous tasks - * like scala.concurrent.Future combinators. - */ - public ExecutionContextExecutor getExecutionContext(); - - /** - * Create a child actor that will wrap messages such that other Actor’s - * protocols can be ingested by this Actor. You are strongly advised to cache - * these ActorRefs or to stop them when no longer needed. - * - * The name of the child actor will be composed of a unique identifier - * starting with a dollar sign to which the given name argument is appended, - * with an inserted hyphen between these two parts. Therefore the given name - * argument does not need to be unique within the scope of the parent actor. - */ - public ActorRef createAdapter(Function f, String name); - - /** - * Create an anonymous child actor that will wrap messages such that other - * Actor’s protocols can be ingested by this Actor. You are strongly advised - * to cache these ActorRefs or to stop them when no longer needed. - */ - public ActorRef createAdapter(Function f); - -} diff --git a/akka-typed/src/main/resources/reference.conf b/akka-typed/src/main/resources/reference.conf index f40d9d7e3e..1d6a4dd975 100644 --- a/akka-typed/src/main/resources/reference.conf +++ b/akka-typed/src/main/resources/reference.conf @@ -12,5 +12,19 @@ akka.typed { # Default mailbox capacity for actors where nothing else is configured by # their parent, see also class akka.typed.MailboxCapacity mailbox-capacity = 1000 - + + # List FQCN of `akka.typed.ExtensionId`s which shall be loaded at actor system startup. + # Should be on the format: 'extensions = ["com.example.MyExtId1", "com.example.MyExtId2"]' etc. + # See the Akka Documentation for more info about Extensions + extensions = [] + + # List FQCN of extensions which shall be loaded at actor system startup. + # Library extensions are regular extensions that are loaded at startup and are + # available for third party library authors to enable auto-loading of extensions when + # present on the classpath. This is done by appending entries: + # 'library-extensions += "Extension"' in the library `reference.conf`. + # + # Should not be set by end user applications in 'application.conf', use the extensions property for that + # + library-extensions = ${?akka.library-extensions} [] } diff --git a/akka-typed/src/main/scala/akka/typed/ActorContext.scala b/akka-typed/src/main/scala/akka/typed/ActorContext.scala index c631ea4802..5f69d92a70 100644 --- a/akka-typed/src/main/scala/akka/typed/ActorContext.scala +++ b/akka-typed/src/main/scala/akka/typed/ActorContext.scala @@ -3,14 +3,6 @@ */ package akka.typed -import scala.concurrent.duration.Duration -import scala.collection.immutable.TreeMap -import akka.util.Helpers -import akka.{ actor ⇒ untyped } -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.ExecutionContextExecutor -import java.util.Optional -import java.util.ArrayList import akka.annotation.DoNotInherit import akka.annotation.ApiMayChange @@ -20,133 +12,17 @@ import akka.annotation.ApiMayChange */ @DoNotInherit @ApiMayChange -trait ActorContext[T] extends javadsl.ActorContext[T] with scaladsl.ActorContext[T] { - override def getChild(name: String): Optional[ActorRef[Void]] = - child(name) match { - case Some(c) ⇒ Optional.of(c.upcast[Void]) - case None ⇒ Optional.empty() - } +trait ActorContext[T] { + // this should be a pure interface, i.e. only abstract methods - override def getChildren(): java.util.List[akka.typed.ActorRef[Void]] = { - val c = children - val a = new ArrayList[ActorRef[Void]](c.size) - val i = c.iterator - while (i.hasNext) a.add(i.next().upcast[Void]) - a - } + /** + * Get the `javadsl` of this `ActorContext`. + */ + def asJava: javadsl.ActorContext[T] - override def getExecutionContext(): scala.concurrent.ExecutionContextExecutor = - executionContext - - override def getMailboxCapacity(): Int = - mailboxCapacity - - override def getSelf(): akka.typed.ActorRef[T] = - self - - override def getSystem(): akka.typed.ActorSystem[Void] = - system.asInstanceOf[ActorSystem[Void]] - - override def spawn[U](behavior: akka.typed.Behavior[U], name: String): akka.typed.ActorRef[U] = - spawn(behavior, name, EmptyDeploymentConfig) - - override def spawnAnonymous[U](behavior: akka.typed.Behavior[U]): akka.typed.ActorRef[U] = - spawnAnonymous(behavior, EmptyDeploymentConfig) - - override def createAdapter[U](f: java.util.function.Function[U, T]): akka.typed.ActorRef[U] = - spawnAdapter(f.apply _) - - override def createAdapter[U](f: java.util.function.Function[U, T], name: String): akka.typed.ActorRef[U] = - spawnAdapter(f.apply _, name) + /** + * Get the `scaladsl` of this `ActorContext`. + */ + def asScala: scaladsl.ActorContext[T] } -/** - * An [[ActorContext]] for synchronous execution of a [[Behavior]] that - * provides only stubs for the effects an Actor can perform and replaces - * created child Actors by a synchronous Inbox (see `Inbox.sync`). - * - * See [[EffectfulActorContext]] for more advanced uses. - */ -class StubbedActorContext[T]( - val name: String, - override val mailboxCapacity: Int, - override val system: ActorSystem[Nothing]) extends ActorContext[T] { - - val selfInbox = Inbox[T](name) - override val self = selfInbox.ref - - private var _children = TreeMap.empty[String, Inbox[_]] - private val childName = Iterator from 1 map (Helpers.base64(_)) - - override def children: Iterable[ActorRef[Nothing]] = _children.values map (_.ref) - override def child(name: String): Option[ActorRef[Nothing]] = _children get name map (_.ref) - override def spawnAnonymous[U](behavior: Behavior[U], deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[U] = { - val i = Inbox[U](childName.next()) - _children += i.ref.path.name → i - i.ref - } - override def spawn[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[U] = - _children get name match { - case Some(_) ⇒ throw untyped.InvalidActorNameException(s"actor name $name is already taken") - case None ⇒ - // FIXME correct child path for the Inbox ref - val i = Inbox[U](name) - _children += name → i - i.ref - } - - /** - * Do not actually stop the child inbox, only simulate the liveness check. - * Removal is asynchronous, explicit removeInbox is needed from outside afterwards. - */ - override def stop(child: ActorRef[_]): Boolean = { - _children.get(child.path.name) match { - case None ⇒ false - case Some(inbox) ⇒ inbox.ref == child - } - } - override def watch[U](other: ActorRef[U]): ActorRef[U] = other - override def unwatch[U](other: ActorRef[U]): ActorRef[U] = other - override def setReceiveTimeout(d: FiniteDuration, msg: T): Unit = () - override def cancelReceiveTimeout(): Unit = () - - override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): untyped.Cancellable = new untyped.Cancellable { - override def cancel() = false - override def isCancelled = true - } - - override def executionContext: ExecutionContextExecutor = system.executionContext - - override def spawnAdapter[U](f: U ⇒ T, name: String = ""): ActorRef[U] = { - val n = if (name != "") s"${childName.next()}-$name" else childName.next() - val i = Inbox[U](n) - _children += i.ref.path.name → i - new internal.FunctionRef[U]( - self.path / i.ref.path.name, - (msg, _) ⇒ { val m = f(msg); if (m != null) { selfInbox.ref ! m; i.ref ! msg } }, - (self) ⇒ selfInbox.ref.sorry.sendSystem(internal.DeathWatchNotification(self, null))) - } - - /** - * Retrieve the inbox representing the given child actor. The passed ActorRef must be one that was returned - * by one of the spawn methods earlier. - */ - def childInbox[U](child: ActorRef[U]): Inbox[U] = { - val inbox = _children(child.path.name).asInstanceOf[Inbox[U]] - if (inbox.ref != child) throw new IllegalArgumentException(s"$child is not a child of $this") - inbox - } - - /** - * Retrieve the inbox representing the child actor with the given name. - */ - def childInbox[U](name: String): Inbox[U] = _children(name).asInstanceOf[Inbox[U]] - - /** - * Remove the given inbox from the list of children, for example after - * having simulated its termination. - */ - def removeChildInbox(child: ActorRef[Nothing]): Unit = _children -= child.path.name - - override def toString: String = s"Inbox($self)" -} diff --git a/akka-typed/src/main/scala/akka/typed/ActorRef.scala b/akka-typed/src/main/scala/akka/typed/ActorRef.scala index 0fbc0b28bf..7eb3c1ec82 100644 --- a/akka-typed/src/main/scala/akka/typed/ActorRef.scala +++ b/akka-typed/src/main/scala/akka/typed/ActorRef.scala @@ -17,7 +17,8 @@ import scala.concurrent.Future * [[EventStream]] on a best effort basis * (i.e. this delivery is not reliable). */ -abstract class ActorRef[-T](_path: a.ActorPath) extends java.lang.Comparable[ActorRef[_]] { this: internal.ActorRefImpl[T] ⇒ +trait ActorRef[-T] extends java.lang.Comparable[ActorRef[_]] { + this: internal.ActorRefImpl[T] ⇒ /** * Send a message to the Actor referenced by this ActorRef using *at-most-once* @@ -26,17 +27,16 @@ abstract class ActorRef[-T](_path: a.ActorPath) extends java.lang.Comparable[Act def tell(msg: T): Unit /** - * Send a message to the Actor referenced by this ActorRef using *at-most-once* - * messaging semantics. + * Narrow the type of this `ActorRef, which is always a safe operation. */ - def !(msg: T): Unit = tell(msg) + def narrow[U <: T]: ActorRef[U] /** * Unsafe utility method for widening the type accepted by this ActorRef; * provided to avoid having to use `asInstanceOf` on the full reference type, * which would unfortunately also work on non-ActorRefs. */ - def upcast[U >: T @uncheckedVariance]: ActorRef[U] = this.asInstanceOf[ActorRef[U]] + def upcast[U >: T @uncheckedVariance]: ActorRef[U] /** * The hierarchical path name of the referenced Actor. The lifecycle of the @@ -44,36 +44,28 @@ abstract class ActorRef[-T](_path: a.ActorPath) extends java.lang.Comparable[Act * and more than one Actor instance can exist with the same path at different * points in time, but not concurrently. */ - final val path: a.ActorPath = _path + def path: a.ActorPath - /** - * Comparison takes path and the unique id of the actor cell into account. - */ - final override def compareTo(other: ActorRef[_]) = { - val x = this.path compareTo other.path - if (x == 0) if (this.path.uid < other.path.uid) -1 else if (this.path.uid == other.path.uid) 0 else 1 - else x - } - - final override def hashCode: Int = path.uid - - /** - * Equals takes path and the unique id of the actor cell into account. - */ - final override def equals(that: Any): Boolean = that match { - case other: ActorRef[_] ⇒ path.uid == other.path.uid && path == other.path - case _ ⇒ false - } - - final override def toString: String = s"Actor[${path}#${path.uid}]" } object ActorRef { + + implicit final class ActorRefOps[-T](val ref: ActorRef[T]) extends AnyVal { + /** + * Send a message to the Actor referenced by this ActorRef using *at-most-once* + * messaging semantics. + */ + def !(msg: T): Unit = ref.tell(msg) + } + + // FIXME factory methods for below for Java (trait + object) + /** * Create an ActorRef from a Future, buffering up to the given number of * messages in while the Future is not fulfilled. */ - def apply[T](f: Future[ActorRef[T]], bufferSize: Int = 1000): ActorRef[T] = new internal.FutureRef(FuturePath, bufferSize, f) + def apply[T](f: Future[ActorRef[T]], bufferSize: Int = 1000): ActorRef[T] = + new internal.FutureRef(FuturePath, bufferSize, f) /** * Create an ActorRef by providing a function that is invoked for sending diff --git a/akka-typed/src/main/scala/akka/typed/ActorSystem.scala b/akka-typed/src/main/scala/akka/typed/ActorSystem.scala index 9557befad6..b0f9092d06 100644 --- a/akka-typed/src/main/scala/akka/typed/ActorSystem.scala +++ b/akka-typed/src/main/scala/akka/typed/ActorSystem.scala @@ -11,10 +11,11 @@ import akka.actor.setup.ActorSystemSetup import com.typesafe.config.{ Config, ConfigFactory } import scala.concurrent.{ ExecutionContextExecutor, Future } -import akka.typed.adapter.{ ActorSystemAdapter, PropsAdapter } +import akka.typed.internal.adapter.{ ActorSystemAdapter, PropsAdapter } import akka.util.Timeout import akka.annotation.DoNotInherit import akka.annotation.ApiMayChange +import java.util.Optional /** * An ActorSystem is home to a hierarchy of Actors. It is created using @@ -25,7 +26,7 @@ import akka.annotation.ApiMayChange */ @DoNotInherit @ApiMayChange -trait ActorSystem[-T] extends ActorRef[T] { this: internal.ActorRefImpl[T] ⇒ +abstract class ActorSystem[-T] extends ActorRef[T] with Extensions { this: internal.ActorRefImpl[T] ⇒ /** * The name of this actor system, used to distinguish multiple ones within @@ -133,7 +134,7 @@ trait ActorSystem[-T] extends ActorRef[T] { this: internal.ActorRefImpl[T] ⇒ /** * Ask the system guardian of this system to create an actor from the given - * behavior and deployment and with the given name. The name does not need to + * behavior and props and with the given name. The name does not need to * be unique since the guardian will prefix it with a running number when * creating the child actor. The timeout sets the timeout used for the [[akka.typed.scaladsl.AskPattern$]] * invocation when asking the guardian. @@ -142,7 +143,7 @@ trait ActorSystem[-T] extends ActorRef[T] { this: internal.ActorRefImpl[T] ⇒ * to which messages can immediately be sent by using the [[ActorRef$.apply[T](s*]] * method. */ - def systemActorOf[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig = EmptyDeploymentConfig)(implicit timeout: Timeout): Future[ActorRef[U]] + def systemActorOf[U](behavior: Behavior[U], name: String, props: Props = Props.empty)(implicit timeout: Timeout): Future[ActorRef[U]] /** * Return a reference to this system’s [[akka.typed.patterns.Receptionist$]]. @@ -154,36 +155,66 @@ object ActorSystem { import internal._ /** - * Create an ActorSystem implementation that is optimized for running + * Scala API: Create an ActorSystem implementation that is optimized for running * Akka Typed [[Behavior]] hierarchies—this system cannot run untyped * [[akka.actor.Actor]] instances. */ def apply[T](name: String, guardianBehavior: Behavior[T], - guardianDeployment: DeploymentConfig = EmptyDeploymentConfig, - config: Option[Config] = None, - classLoader: Option[ClassLoader] = None, - executionContext: Option[ExecutionContext] = None): ActorSystem[T] = { + guardianProps: Props = Props.empty, + config: Option[Config] = None, + classLoader: Option[ClassLoader] = None, + executionContext: Option[ExecutionContext] = None): ActorSystem[T] = { Behavior.validateAsInitial(guardianBehavior) val cl = classLoader.getOrElse(akka.actor.ActorSystem.findClassLoader()) val appConfig = config.getOrElse(ConfigFactory.load(cl)) - new ActorSystemImpl(name, appConfig, cl, executionContext, guardianBehavior, guardianDeployment) + new ActorSystemImpl(name, appConfig, cl, executionContext, guardianBehavior, guardianProps) } + /** + * Java API: Create an ActorSystem implementation that is optimized for running + * Akka Typed [[Behavior]] hierarchies—this system cannot run untyped + * [[akka.actor.Actor]] instances. + */ + def create[T](name: String, guardianBehavior: Behavior[T], + guardianProps: Optional[Props], + config: Optional[Config], + classLoader: Optional[ClassLoader], + executionContext: Optional[ExecutionContext]): ActorSystem[T] = { + import scala.compat.java8.OptionConverters._ + apply(name, guardianBehavior, guardianProps.asScala.getOrElse(EmptyProps), config.asScala, classLoader.asScala, executionContext.asScala) + } + + /** + * Java API: Create an ActorSystem implementation that is optimized for running + * Akka Typed [[Behavior]] hierarchies—this system cannot run untyped + * [[akka.actor.Actor]] instances. + */ + def create[T](name: String, guardianBehavior: Behavior[T]): ActorSystem[T] = + apply(name, guardianBehavior) + /** * Create an ActorSystem based on the untyped [[akka.actor.ActorSystem]] * which runs Akka Typed [[Behavior]] on an emulation layer. In this * system typed and untyped actors can coexist. */ def adapter[T](name: String, guardianBehavior: Behavior[T], - guardianDeployment: DeploymentConfig = EmptyDeploymentConfig, + guardianProps: Props = Props.empty, config: Option[Config] = None, classLoader: Option[ClassLoader] = None, executionContext: Option[ExecutionContext] = None, actorSystemSettings: ActorSystemSetup = ActorSystemSetup.empty): ActorSystem[T] = { + + // TODO I'm not sure how useful this mode is for end-users. It has the limitation that untyped top level + // actors can't be created, because we have a custom user guardian. I would imagine that if you have + // a system of both untyped and typed actors (e.g. adding some typed actors to an existing application) + // you would start an untyped.ActorSystem and spawn typed actors from that system or from untyped actors. + // Same thing with `wrap` below. + Behavior.validateAsInitial(guardianBehavior) val cl = classLoader.getOrElse(akka.actor.ActorSystem.findClassLoader()) val appConfig = config.getOrElse(ConfigFactory.load(cl)) - val untyped = new a.ActorSystemImpl(name, appConfig, cl, executionContext, Some(PropsAdapter(guardianBehavior, guardianDeployment)), actorSystemSettings) + val untyped = new a.ActorSystemImpl(name, appConfig, cl, executionContext, + Some(PropsAdapter(() ⇒ guardianBehavior, guardianProps)), actorSystemSettings) untyped.start() new ActorSystemAdapter(untyped) } diff --git a/akka-typed/src/main/scala/akka/typed/Behavior.scala b/akka-typed/src/main/scala/akka/typed/Behavior.scala index 3821f236e1..777248fa59 100644 --- a/akka-typed/src/main/scala/akka/typed/Behavior.scala +++ b/akka-typed/src/main/scala/akka/typed/Behavior.scala @@ -3,7 +3,11 @@ */ package akka.typed -import akka.annotation.InternalApi +import scala.annotation.tailrec +import akka.util.LineNumbers +import akka.annotation.{ DoNotInherit, InternalApi } +import akka.typed.scaladsl.{ ActorContext ⇒ SAC } +import akka.util.OptionVal /** * The behavior of an actor defines how it reacts to the messages that it @@ -13,14 +17,32 @@ import akka.annotation.InternalApi * its child actors. * * Behaviors can be formulated in a number of different ways, either by - * creating a derived class or by employing factory methods like the ones - * in the [[ScalaDSL$]] object. + * using the DSLs in [[akka.typed.scaladsl.Actor]] and [[akka.typed.javadsl.Actor]] + * or extending the abstract [[ExtensibleBehavior]] class. * * Closing over ActorContext makes a Behavior immobile: it cannot be moved to * another context and executed there, and therefore it cannot be replicated or * forked either. + * + * This base class is not meant to be extended by user code. If you do so, you may + * lose binary compatibility. */ -abstract class Behavior[T] { +@InternalApi +@DoNotInherit +sealed abstract class Behavior[T] { + /** + * Narrow the type of this Behavior, which is always a safe operation. This + * method is necessary to implement the contravariant nature of Behavior + * (which cannot be expressed directly due to type inference problems). + */ + final def narrow[U <: T]: Behavior[U] = this.asInstanceOf[Behavior[U]] +} + +/** + * Extension point for implementing custom behaviors in addition to the existing + * set of behaviors available through the DSLs in [[akka.typed.scaladsl.Actor]] and [[akka.typed.javadsl.Actor]] + */ +abstract class ExtensibleBehavior[T] extends Behavior[T] { /** * Process an incoming [[Signal]] and return the next behavior. This means * that all lifecycle hooks, ReceiveTimeout, Terminated and Failed messages @@ -29,15 +51,15 @@ abstract class Behavior[T] { * The returned behavior can in addition to normal behaviors be one of the * canned special objects: * - * * returning `Stopped` will terminate this Behavior - * * returning `Same` designates to reuse the current Behavior - * * returning `Unhandled` keeps the same Behavior and signals that the message was not yet handled + * * returning `stopped` will terminate this Behavior + * * returning `same` designates to reuse the current Behavior + * * returning `unhandled` keeps the same Behavior and signals that the message was not yet handled * * Code calling this method should use [[Behavior$]] `canonicalize` to replace * the special objects with real Behaviors. */ @throws(classOf[Exception]) - def management(ctx: ActorContext[T], msg: Signal): Behavior[T] + def receiveSignal(ctx: ActorContext[T], msg: Signal): Behavior[T] /** * Process an incoming message and return the next behavior. @@ -45,128 +67,214 @@ abstract class Behavior[T] { * The returned behavior can in addition to normal behaviors be one of the * canned special objects: * - * * returning `Stopped` will terminate this Behavior - * * returning `Same` designates to reuse the current Behavior - * * returning `Unhandled` keeps the same Behavior and signals that the message was not yet handled + * * returning `stopped` will terminate this Behavior + * * returning `same` designates to reuse the current Behavior + * * returning `unhandled` keeps the same Behavior and signals that the message was not yet handled * * Code calling this method should use [[Behavior$]] `canonicalize` to replace * the special objects with real Behaviors. */ @throws(classOf[Exception]) - def message(ctx: ActorContext[T], msg: T): Behavior[T] + def receiveMessage(ctx: ActorContext[T], msg: T): Behavior[T] - /** - * Narrow the type of this Behavior, which is always a safe operation. This - * method is necessary to implement the contravariant nature of Behavior - * (which cannot be expressed directly due to type inference problems). - */ - def narrow[U <: T]: Behavior[U] = this.asInstanceOf[Behavior[U]] } object Behavior { + /** + * Return this behavior from message processing in order to advise the + * system to reuse the previous behavior. This is provided in order to + * avoid the allocation overhead of recreating the current behavior where + * that is not necessary. + */ + def same[T]: Behavior[T] = SameBehavior.asInstanceOf[Behavior[T]] + /** + * Return this behavior from message processing in order to advise the + * system to reuse the previous behavior, including the hint that the + * message has not been handled. This hint may be used by composite + * behaviors that delegate (partial) handling to other behaviors. + */ + def unhandled[T]: Behavior[T] = UnhandledBehavior.asInstanceOf[Behavior[T]] + + /** + * Return this behavior from message processing to signal that this actor + * shall terminate voluntarily. If this actor has created child actors then + * these will be stopped as part of the shutdown procedure. + * + * The PostStop signal that results from stopping this actor will be passed to the + * current behavior. All other messages and signals will effectively be + * ignored. + */ + def stopped[T]: Behavior[T] = StoppedBehavior.asInstanceOf[Behavior[T]] + + /** + * Return this behavior from message processing to signal that this actor + * shall terminate voluntarily. If this actor has created child actors then + * these will be stopped as part of the shutdown procedure. + * + * The PostStop signal that results from stopping this actor will be passed to the + * given `postStop` behavior. All other messages and signals will effectively be + * ignored. + */ + def stopped[T](postStop: Behavior[T]): Behavior[T] = + new StoppedBehavior(OptionVal.Some(postStop)) + + /** + * A behavior that treats every incoming message as unhandled. + */ + def empty[T]: Behavior[T] = EmptyBehavior.asInstanceOf[Behavior[T]] + + /** + * A behavior that ignores every incoming message and returns “same”. + */ + def ignore[T]: Behavior[T] = IgnoreBehavior.asInstanceOf[Behavior[T]] + /** * INTERNAL API. */ - @SerialVersionUID(1L) - private[akka] object emptyBehavior extends Behavior[Any] { - override def management(ctx: ActorContext[Any], msg: Signal): Behavior[Any] = ScalaDSL.Unhandled - override def message(ctx: ActorContext[Any], msg: Any): Behavior[Any] = ScalaDSL.Unhandled + @InternalApi + private[akka] object EmptyBehavior extends Behavior[Any] { override def toString = "Empty" } /** * INTERNAL API. */ - @SerialVersionUID(1L) - private[akka] object ignoreBehavior extends Behavior[Any] { - override def management(ctx: ActorContext[Any], msg: Signal): Behavior[Any] = ScalaDSL.Same - override def message(ctx: ActorContext[Any], msg: Any): Behavior[Any] = ScalaDSL.Same + @InternalApi + private[akka] object IgnoreBehavior extends Behavior[Any] { override def toString = "Ignore" } /** * INTERNAL API. */ - @SerialVersionUID(1L) - private[akka] object unhandledBehavior extends Behavior[Nothing] { - override def management(ctx: ActorContext[Nothing], msg: Signal): Behavior[Nothing] = throw new UnsupportedOperationException("Not Implemented") - override def message(ctx: ActorContext[Nothing], msg: Nothing): Behavior[Nothing] = throw new UnsupportedOperationException("Not Implemented") + @InternalApi + private[akka] object UnhandledBehavior extends Behavior[Nothing] { override def toString = "Unhandled" } /** * INTERNAL API */ - @InternalApi private[akka] val unhandledSignal: (ActorContext[Nothing], Signal) ⇒ Behavior[Nothing] = - (_, _) ⇒ unhandledBehavior + @InternalApi private[akka] val unhandledSignal: PartialFunction[(ActorContext[Nothing], Signal), Behavior[Nothing]] = { + case (_, _) ⇒ UnhandledBehavior + } + + /** + * INTERNAL API. + * Not placed in internal.BehaviorImpl because Behavior is sealed. + */ + @InternalApi + private[akka] final case class DeferredBehavior[T](factory: SAC[T] ⇒ Behavior[T]) extends Behavior[T] { + + /** "undefer" the deferred behavior */ + @throws(classOf[Exception]) + def apply(ctx: ActorContext[T]): Behavior[T] = factory(ctx.asScala) + + override def toString: String = s"Deferred(${LineNumbers(factory)})" + } /** * INTERNAL API. */ - @SerialVersionUID(1L) - private[akka] object sameBehavior extends Behavior[Nothing] { - override def management(ctx: ActorContext[Nothing], msg: Signal): Behavior[Nothing] = throw new UnsupportedOperationException("Not Implemented") - override def message(ctx: ActorContext[Nothing], msg: Nothing): Behavior[Nothing] = throw new UnsupportedOperationException("Not Implemented") + private[akka] object SameBehavior extends Behavior[Nothing] { override def toString = "Same" } /** * INTERNAL API. */ - @SerialVersionUID(1L) - private[akka] object stoppedBehavior extends Behavior[Nothing] { - override def management(ctx: ActorContext[Nothing], msg: Signal): Behavior[Nothing] = { - assert( - msg == PostStop || msg.isInstanceOf[Terminated], - s"stoppedBehavior received $msg (only PostStop or Terminated expected)") - this - } - override def message(ctx: ActorContext[Nothing], msg: Nothing): Behavior[Nothing] = throw new UnsupportedOperationException("Not Implemented") + private[akka] object StoppedBehavior extends StoppedBehavior[Nothing](OptionVal.None) + + /** + * INTERNAL API: When the cell is stopping this behavior is used, so + * that PostStop can be sent to previous behavior from `finishTerminate`. + */ + private[akka] class StoppedBehavior[T](val postStop: OptionVal[Behavior[T]]) extends Behavior[T] { override def toString = "Stopped" } /** * Given a possibly special behavior (same or unhandled) and a - * “current” behavior (which defines the meaning of encountering a `Same` + * “current” behavior (which defines the meaning of encountering a `same` * behavior) this method computes the next behavior, suitable for passing a * message or signal. */ - def canonicalize[T](behavior: Behavior[T], current: Behavior[T]): Behavior[T] = + @tailrec + def canonicalize[T](behavior: Behavior[T], current: Behavior[T], ctx: ActorContext[T]): Behavior[T] = behavior match { - case `sameBehavior` ⇒ current - case `unhandledBehavior` ⇒ current - case other ⇒ other + case SameBehavior ⇒ current + case UnhandledBehavior ⇒ current + case deferred: DeferredBehavior[T] ⇒ canonicalize(deferred(ctx), deferred, ctx) + case other ⇒ other } + @tailrec + def undefer[T](behavior: Behavior[T], ctx: ActorContext[T]): Behavior[T] = { + behavior match { + case innerDeferred: DeferredBehavior[T] ⇒ undefer(innerDeferred(ctx), ctx) + case _ ⇒ behavior + } + } + /** * Validate the given behavior as a suitable initial actor behavior; most - * notably the behavior can neither be `Same` nor `Unhandled`. Starting + * notably the behavior can neither be `same` nor `unhandled`. Starting * out with a `Stopped` behavior is allowed, though. */ def validateAsInitial[T](behavior: Behavior[T]): Behavior[T] = behavior match { - case `sameBehavior` | `unhandledBehavior` ⇒ + case SameBehavior | UnhandledBehavior ⇒ throw new IllegalArgumentException(s"cannot use $behavior as initial behavior") case x ⇒ x } /** - * Validate the given behavior as initial, pass it a [[PreStart]] message - * and canonicalize the result. + * Returns true if the given behavior is not stopped. */ - def preStart[T](behavior: Behavior[T], ctx: ActorContext[T]): Behavior[T] = { - val b = validateAsInitial(behavior) - if (isAlive(b)) canonicalize(b.management(ctx, PreStart), b) else b + def isAlive[T](behavior: Behavior[T]): Boolean = behavior match { + case _: StoppedBehavior[_] ⇒ false + case _ ⇒ true } /** - * Returns true if the given behavior is not stopped. + * Returns true if the given behavior is the special `unhandled` marker. */ - def isAlive[T](behavior: Behavior[T]): Boolean = behavior ne stoppedBehavior + def isUnhandled[T](behavior: Behavior[T]): Boolean = behavior eq UnhandledBehavior /** * Returns true if the given behavior is the special `Unhandled` marker. */ - def isUnhandled[T](behavior: Behavior[T]): Boolean = behavior eq unhandledBehavior + def isDeferred[T](behavior: Behavior[T]): Boolean = behavior match { + case _: DeferredBehavior[T] ⇒ true + case _ ⇒ false + } + + /** + * Execute the behavior with the given message + */ + def interpretMessage[T](behavior: Behavior[T], ctx: ActorContext[T], msg: T): Behavior[T] = + interpret(behavior, ctx, msg) + + /** + * Execute the behavior with the given signal + */ + def interpretSignal[T](behavior: Behavior[T], ctx: ActorContext[T], signal: Signal): Behavior[T] = + interpret(behavior, ctx, signal) + + private def interpret[T](behavior: Behavior[T], ctx: ActorContext[T], msg: Any): Behavior[T] = + behavior match { + case SameBehavior | UnhandledBehavior ⇒ throw new IllegalArgumentException(s"cannot execute with [$behavior] as behavior") + case d: DeferredBehavior[_] ⇒ throw new IllegalArgumentException(s"deferred [$d] should not be passed to interpreter") + case IgnoreBehavior ⇒ SameBehavior.asInstanceOf[Behavior[T]] + case s: StoppedBehavior[T] ⇒ s + case EmptyBehavior ⇒ UnhandledBehavior.asInstanceOf[Behavior[T]] + case ext: ExtensibleBehavior[T] ⇒ + val possiblyDeferredResult = msg match { + case signal: Signal ⇒ ext.receiveSignal(ctx, signal) + case msg ⇒ ext.receiveMessage(ctx, msg.asInstanceOf[T]) + } + undefer(possiblyDeferredResult, ctx) + } + } diff --git a/akka-typed/src/main/scala/akka/typed/Deployment.scala b/akka-typed/src/main/scala/akka/typed/Deployment.scala deleted file mode 100644 index acfa98c249..0000000000 --- a/akka-typed/src/main/scala/akka/typed/Deployment.scala +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Copyright (C) 2016-2017 Lightbend Inc. - */ -package akka.typed - -import scala.concurrent.{ ExecutionContext, ExecutionContextExecutor } -import java.util.concurrent.{ Executor, Executors } -import scala.reflect.ClassTag -import scala.annotation.tailrec - -/** - * Data structure for describing an actor’s deployment details like which - * executor to run it on. For each type of setting (e.g. [[DispatcherSelector]] - * or [[MailboxCapacity]]) the FIRST occurrence is used when creating the - * actor; this means that adding configuration using the "with" methods - * overrides what was configured previously. - * - * Deliberately not sealed in order to emphasize future extensibility by the - * framework—this is not intended to be extended by user code. - * - * The DeploymentConfig includes a `next` reference so that it can form an - * internally linked list. Traversal of this list stops when encountering the - * [[EmptyDeploymentConfig$]] object. - */ -abstract class DeploymentConfig extends Product with Serializable { - /** - * Reference to the tail of this DeploymentConfig list. - */ - def next: DeploymentConfig - - /** - * Create a copy of this DeploymentConfig node with its `next` reference - * replaced by the given object. This does NOT append the given list - * of configuration nodes to the current list! - */ - def withNext(next: DeploymentConfig): DeploymentConfig - - /** - * Prepend a selection of the [[ActorSystem]] default executor to this DeploymentConfig. - */ - def withDispatcherDefault: DeploymentConfig = DispatcherDefault(this) - - /** - * Prepend a selection of the executor found at the given Config path to this DeploymentConfig. - * The path is relative to the configuration root of the [[ActorSystem]] that looks up the - * executor. - */ - def withDispatcherFromConfig(path: String): DeploymentConfig = DispatcherFromConfig(path, this) - - /** - * Prepend a selection of the given executor to this DeploymentConfig. - */ - def withDispatcherFromExecutor(executor: Executor): DeploymentConfig = DispatcherFromExecutor(executor, this) - - /** - * Prepend a selection of the given execution context to this DeploymentConfig. - */ - def withDispatcherFromExecutionContext(ec: ExecutionContext): DeploymentConfig = DispatcherFromExecutionContext(ec, this) - - /** - * Prepend the given mailbox capacity configuration to this DeploymentConfig. - */ - def withMailboxCapacity(capacity: Int): DeploymentConfig = MailboxCapacity(capacity, this) - - /** - * Find the first occurrence of a configuration node of the given type, falling - * back to the provided default if none is found. - */ - def firstOrElse[T <: DeploymentConfig: ClassTag](default: T): T = { - @tailrec def rec(d: DeploymentConfig): T = { - d match { - case EmptyDeploymentConfig ⇒ default - case t: T ⇒ t - case _ ⇒ rec(d.next) - } - } - rec(this) - } - - /** - * Retrieve all configuration nodes of a given type in the order that they - * are present in this DeploymentConfig. The `next` reference for all returned - * nodes will be the [[EmptyDeploymentConfig$]]. - */ - def allOf[T <: DeploymentConfig: ClassTag]: List[DeploymentConfig] = { - @tailrec def select(d: DeploymentConfig, acc: List[DeploymentConfig]): List[DeploymentConfig] = - d match { - case EmptyDeploymentConfig ⇒ acc.reverse - case t: T ⇒ select(d.next, (d withNext EmptyDeploymentConfig) :: acc) - case _ ⇒ select(d.next, acc) - } - select(this, Nil) - } - - /** - * Remove all configuration nodes of a given type and return the resulting - * DeploymentConfig. - */ - def filterNot[T <: DeploymentConfig: ClassTag]: DeploymentConfig = { - @tailrec def select(d: DeploymentConfig, acc: List[DeploymentConfig]): List[DeploymentConfig] = - d match { - case EmptyDeploymentConfig ⇒ acc - case t: T ⇒ select(d.next, acc) - case _ ⇒ select(d.next, d :: acc) - } - @tailrec def link(l: List[DeploymentConfig], acc: DeploymentConfig): DeploymentConfig = - l match { - case d :: ds ⇒ link(ds, d withNext acc) - case Nil ⇒ acc - } - link(select(this, Nil), EmptyDeploymentConfig) - } -} - -/** - * Configure the maximum mailbox capacity for the actor. If more messages are - * enqueued because the actor does not process them quickly enough then further - * messages will be dropped. - * - * The default mailbox capacity that is used when this option is not given is - * taken from the `akka.typed.mailbox-capacity` configuration setting. - */ -final case class MailboxCapacity(capacity: Int, next: DeploymentConfig = EmptyDeploymentConfig) extends DeploymentConfig { - override def withNext(next: DeploymentConfig): DeploymentConfig = copy(next = next) -} - -/** - * The empty configuration node, used as a terminator for the internally linked - * list of each DeploymentConfig. - */ -case object EmptyDeploymentConfig extends DeploymentConfig { - override def next = throw new NoSuchElementException("EmptyDeploymentConfig has no next") - override def withNext(next: DeploymentConfig): DeploymentConfig = next -} - -/** - * Subclasses of this type describe which thread pool shall be used to run - * the actor to which this configuration is applied. - * - * The default configuration if none of these options are present is to run - * the actor on the same executor as its parent. - */ -sealed trait DispatcherSelector extends DeploymentConfig - -/** - * Use the [[ActorSystem]] default executor to run the actor. - */ -sealed case class DispatcherDefault(next: DeploymentConfig) extends DispatcherSelector { - override def withNext(next: DeploymentConfig): DeploymentConfig = copy(next = next) -} -object DispatcherDefault { - // this is hidden in order to avoid having people match on this object - private val empty = DispatcherDefault(EmptyDeploymentConfig) - /** - * Retrieve an instance for this configuration node with empty `next` reference. - */ - def apply(): DispatcherDefault = empty -} - -/** - * Look up an executor definition in the [[ActorSystem]] configuration. - * ExecutorServices created in this fashion will be shut down when the - * ActorSystem terminates. - */ -final case class DispatcherFromConfig(path: String, next: DeploymentConfig = EmptyDeploymentConfig) extends DispatcherSelector { - override def withNext(next: DeploymentConfig): DeploymentConfig = copy(next = next) -} - -/** - * Directly use the given Executor whenever the actor needs to be run. - * No attempt will be made to shut down this thread pool, even if it is an - * instance of ExecutorService. - */ -final case class DispatcherFromExecutor(executor: Executor, next: DeploymentConfig = EmptyDeploymentConfig) extends DispatcherSelector { - override def withNext(next: DeploymentConfig): DeploymentConfig = copy(next = next) -} - -/** - * Directly use the given ExecutionContext whenever the actor needs to be run. - * No attempt will be made to shut down this thread pool, even if it is an - * instance of ExecutorService. - */ -final case class DispatcherFromExecutionContext(ec: ExecutionContext, next: DeploymentConfig = EmptyDeploymentConfig) extends DispatcherSelector { - override def withNext(next: DeploymentConfig): DeploymentConfig = copy(next = next) -} diff --git a/akka-typed/src/main/scala/akka/typed/EventStream.scala b/akka-typed/src/main/scala/akka/typed/EventStream.scala index 7ac81dcc6e..5a1f816399 100644 --- a/akka-typed/src/main/scala/akka/typed/EventStream.scala +++ b/akka-typed/src/main/scala/akka/typed/EventStream.scala @@ -5,8 +5,6 @@ package akka.typed import akka.{ event ⇒ e } import akka.event.Logging.{ LogEvent, LogLevel, StdOutLogger } -import akka.testkit.{ EventFilter, TestEvent ⇒ TE } -import scala.annotation.tailrec /** * An EventStream allows local actors to register for certain message types, including @@ -66,42 +64,31 @@ object Logger { } class DefaultLogger extends Logger with StdOutLogger { - import ScalaDSL._ import Logger._ - val initialBehavior = - ContextAware[Command] { ctx ⇒ - Total { - case Initialize(eventStream, replyTo) ⇒ - val log = ctx.spawn(Deferred[AnyRef] { () ⇒ - var filters: List[EventFilter] = Nil + val initialBehavior = { + // TODO avoid depending on dsl here? + import scaladsl.Actor._ + deferred[Command] { _ ⇒ + immutable[Command] { + case (ctx, Initialize(eventStream, replyTo)) ⇒ + val log = ctx.spawn(deferred[AnyRef] { childCtx ⇒ - def filter(event: LogEvent): Boolean = filters exists (f ⇒ try { f(event) } catch { case e: Exception ⇒ false }) - - def addFilter(filter: EventFilter): Unit = filters ::= filter - - def removeFilter(filter: EventFilter) { - @tailrec def removeFirst(list: List[EventFilter], zipped: List[EventFilter] = Nil): List[EventFilter] = list match { - case head :: tail if head == filter ⇒ tail.reverse_:::(zipped) - case head :: tail ⇒ removeFirst(tail, head :: zipped) - case Nil ⇒ filters // filter not found, just return original list - } - filters = removeFirst(filters) - } - - Static { - case TE.Mute(filters) ⇒ filters foreach addFilter - case TE.UnMute(filters) ⇒ filters foreach removeFilter - case event: LogEvent ⇒ if (!filter(event)) print(event) + immutable[AnyRef] { + case (_, event: LogEvent) ⇒ + print(event) + same + case _ ⇒ unhandled } }, "logger") - eventStream.subscribe(log, classOf[TE.Mute]) - eventStream.subscribe(log, classOf[TE.UnMute]) + ctx.watch(log) // sign death pact replyTo ! log - Empty + + empty } } + } } class DefaultLoggingFilter(settings: Settings, eventStream: EventStream) extends e.DefaultLoggingFilter(() ⇒ eventStream.logLevel) diff --git a/akka-typed/src/main/scala/akka/typed/Extensions.scala b/akka-typed/src/main/scala/akka/typed/Extensions.scala new file mode 100644 index 0000000000..2fdc2914db --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/Extensions.scala @@ -0,0 +1,145 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed + +import akka.annotation.DoNotInherit + +/** + * Marker trait/interface for extensions. An extension can be registered in the ActorSystem and is guaranteed to only + * have one instance per [[ActorSystem]] instance per [[ExtensionId]]. The extension internals must be thread safe. + * For mutable state it should be preferred to use an `Actor` rather than extensions as first choice. + * + * @see [[ExtensionId]] + */ +trait Extension + +/** + * Identifier and factory for an extension. Is used to look up an extension from the `ActorSystem`, and possibly create + * an instance if no instance was already registered. The extension can also be listed in the actor system configuration + * to have it eagerly loaded and registered on actor system startup. + * + * *Scala API* + * + * The `ExtensionId` for an extension written in Scala is best done by letting it be the companion object of the + * extension. If the extension will be used from Java special care needs to be taken to provide a `get` method with the + * concrete extension type as return (as this will not be inferred correctly by the Java compiler with the default + * implementation) + * + * Example: + * + * {{{ + * object MyExt extends ExtensionId[Ext] { + * + * override def createExtension(system: ActorSystem[_]): MyExt = new MyExt(system) + * + * // Java API: retrieve the extension instance for the given system. + * def get(system: ActorSystem[_]): MyExt = apply(system) + * } + * + * class MyExt(system: ActorSystem[_]) extends Extension { + * ... + * } + * + * // can be loaded eagerly on system startup through configuration + * // note that the name is the JVM/Java class name, with a dollar sign in the end + * // and not the Scala object name + * akka.typed.extensions = ["com.example.MyExt$"] + * + * // Allows access like this from Scala + * MyExt().someMethodOnTheExtension() + * // and from Java + * MyExt.get(system).someMethodOnTheExtension() + * }}} + * + * *Java API* + * + * To implement an extension in Java you should first create an `ExtensionId` singleton by implementing a static method + * called `getInstance`, this is needed to be able to list the extension among the `akka.typed.extensions` in the configuration + * and have it loaded when the actor system starts up. + * + * {{{ + * + * public class MyExt extends AbstractExtensionId { + * // single instance of the identifier + * private final static MyExt instance = new MyExt(); + * + * // protect against other instances than the singleton + * private MyExt() {} + * + * // This static method singleton accessor is needed to be able to enable the extension through config when + * // implementing extensions in Java. + * public static MyExt getInstance() { + * return instance; + * } + * + * public MyExtImpl createExtension(ActorSystem system) { + * return new MyExtImpl(); + * } + * + * // convenience accessor + * public static MyExtImpl get(ActorSystem system) { + * return instance.apply(system); + * } + * } + * + * public class MyExtImpl implements Extension { + * ... + * } + * + * // can be loaded eagerly on system startup through configuration + * akka.typed.extensions = ["com.example.MyExt"] + * + * // Allows access like this from Scala + * MyExt.someMethodOnTheExtension() + * // and from Java + * MyExt.get(system).someMethodOnTheExtension() + * }}} + * + * @tparam T The concrete extension type + */ +abstract class ExtensionId[T <: Extension] { + + /** + * Create the extension, will be invoked at most one time per actor system where the extension is registered. + */ + def createExtension(system: ActorSystem[_]): T + + /** + * Lookup or create an instance of the extension identified by this id. + */ + final def apply(system: ActorSystem[_]): T = system.registerExtension(this) + + override final def hashCode: Int = System.identityHashCode(this) + override final def equals(other: Any): Boolean = this eq other.asInstanceOf[AnyRef] +} + +/** + * API for registering and looking up extensions. + * + * Not intended to be extended by user code. + */ +@DoNotInherit +trait Extensions { + + /** + * Registers the provided extension and creates its payload, if this extension isn't already registered + * This method has putIfAbsent-semantics, this method can potentially block, waiting for the initialization + * of the payload, if is in the process of registration from another Thread of execution + */ + def registerExtension[T <: Extension](ext: ExtensionId[T]): T + /** + * Returns the payload that is associated with the provided extension + * throws an IllegalStateException if it is not registered. + * This method can potentially block, waiting for the initialization + * of the payload, if is in the process of registration from another Thread of execution + */ + def extension[T <: Extension](ext: ExtensionId[T]): T + + /** + * Returns whether the specified extension is already registered, this method can potentially block, waiting for the initialization + * of the payload, if is in the process of registration from another Thread of execution + */ + def hasExtension(ext: ExtensionId[_ <: Extension]): Boolean +} + diff --git a/akka-typed/src/main/scala/akka/typed/MessageAndSignals.scala b/akka-typed/src/main/scala/akka/typed/MessageAndSignals.scala index e5b06d7b93..421b5a676d 100644 --- a/akka-typed/src/main/scala/akka/typed/MessageAndSignals.scala +++ b/akka-typed/src/main/scala/akka/typed/MessageAndSignals.scala @@ -25,16 +25,7 @@ final case class DeadLetter(msg: Any) * guaranteed to arrive in contrast to the at-most-once semantics of normal * Actor messages). */ -sealed trait Signal - -/** - * Lifecycle signal that is fired upon creation of the Actor. This will be the - * first message that the actor processes. - */ -sealed abstract class PreStart extends Signal -final case object PreStart extends PreStart { - def instance: PreStart = this -} +trait Signal /** * Lifecycle signal that is fired upon restart of the Actor before replacing @@ -51,10 +42,6 @@ final case object PreRestart extends PreRestart { * Lifecycle signal that is fired after this actor and all its child actors * (transitively) have terminated. The [[Terminated]] signal is only sent to * registered watchers after this signal has been processed. - * - * IMPORTANT NOTE: if the actor terminated by switching to the - * `Stopped` behavior then this signal will be ignored (i.e. the - * Stopped behavior will do nothing in reaction to it). */ sealed abstract class PostStop extends Signal final case object PostStop extends PostStop { @@ -68,8 +55,9 @@ final case object PostStop extends PostStop { * idempotent, meaning that registering twice has the same effect as registering * once. Registration does not need to happen before the Actor terminates, a * notification is guaranteed to arrive after both registration and termination - * have occurred. Termination of a remote Actor can also be effected by declaring - * the Actor’s home system as failed (e.g. as a result of being unreachable). + * have occurred. This message is also sent when the watched actor is on a node + * that has been removed from the cluster when using akka-cluster or has been + * marked unreachable when using akka-remote directly. */ final case class Terminated(ref: ActorRef[Nothing])(failed: Throwable) extends Signal { def wasFailed: Boolean = failed ne null diff --git a/akka-typed/src/main/scala/akka/typed/Props.scala b/akka-typed/src/main/scala/akka/typed/Props.scala new file mode 100644 index 0000000000..fcaa9bba97 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/Props.scala @@ -0,0 +1,270 @@ +/** + * Copyright (C) 2016-2017 Lightbend Inc. + */ +package akka.typed + +import java.util.concurrent.Executor + +import akka.annotation.{ ApiMayChange, DoNotInherit, InternalApi } + +import scala.annotation.tailrec +import scala.concurrent.ExecutionContext +import scala.reflect.ClassTag + +object Props { + + /** + * Empty props instance, should seldom be needed in user code but can be useful as a default props where + * custom configuration of an actor is possible. + */ + val empty: Props = EmptyProps +} + +/** + * Data structure for describing an actor’s props details like which + * executor to run it on. For each type of setting (e.g. [[DispatcherSelector]] + * or [[MailboxCapacity]]) the FIRST occurrence is used when creating the + * actor; this means that adding configuration using the "with" methods + * overrides what was configured previously. + * + * Deliberately not sealed in order to emphasize future extensibility by the + * framework—this is not intended to be extended by user code. + * + */ +@DoNotInherit +@ApiMayChange +abstract class Props private[akka] () extends Product with Serializable { + /** + * Reference to the tail of this Props list. + * + * The `next` reference is here so that it can form an + * internally linked list. Traversal of this list stops when encountering the + * [[EmptyProps]] object. + * + * INTERNAL API + */ + @InternalApi + private[akka] def next: Props + + /** + * Create a copy of this Props node with its `next` reference + * replaced by the given object. This does NOT append the given list + * of configuration nodes to the current list! + * + * INTERNAL API + */ + @InternalApi + private[akka] def withNext(next: Props): Props + + /** + * Prepend a selection of the [[ActorSystem]] default executor to this Props. + */ + def withDispatcherDefault: Props = DispatcherDefault(this) + + /** + * Prepend a selection of the executor found at the given Config path to this Props. + * The path is relative to the configuration root of the [[ActorSystem]] that looks up the + * executor. + */ + def withDispatcherFromConfig(path: String): Props = DispatcherFromConfig(path, this) + + /** + * Prepend a selection of the given executor to this Props. + */ + def withDispatcherFromExecutor(executor: Executor): Props = DispatcherFromExecutor(executor, this) + + /** + * Prepend a selection of the given execution context to this Props. + */ + def withDispatcherFromExecutionContext(ec: ExecutionContext): Props = DispatcherFromExecutionContext(ec, this) + + /** + * Prepend the given mailbox capacity configuration to this Props. + */ + def withMailboxCapacity(capacity: Int): Props = MailboxCapacity(capacity, this) + + /** + * Find the first occurrence of a configuration node of the given type, falling + * back to the provided default if none is found. + * + * INTERNAL API + */ + @InternalApi + private[akka] def firstOrElse[T <: Props: ClassTag](default: T): T = { + @tailrec def rec(d: Props): T = { + d match { + case EmptyProps ⇒ default + case t: T ⇒ t + case _ ⇒ rec(d.next) + } + } + rec(this) + } + + /** + * Retrieve all configuration nodes of a given type in the order that they + * are present in this Props. The `next` reference for all returned + * nodes will be the [[EmptyProps]]. + * + * INTERNAL API + */ + @InternalApi + private[akka] def allOf[T <: Props: ClassTag]: List[Props] = { + @tailrec def select(d: Props, acc: List[Props]): List[Props] = + d match { + case EmptyProps ⇒ acc.reverse + case t: T ⇒ select(d.next, (d withNext EmptyProps) :: acc) + case _ ⇒ select(d.next, acc) + } + select(this, Nil) + } + + /** + * Remove all configuration nodes of a given type and return the resulting + * Props. + */ + @InternalApi + private[akka] def filterNot[T <: Props: ClassTag]: Props = { + @tailrec def select(d: Props, acc: List[Props]): List[Props] = + d match { + case EmptyProps ⇒ acc + case t: T ⇒ select(d.next, acc) + case _ ⇒ select(d.next, d :: acc) + } + @tailrec def link(l: List[Props], acc: Props): Props = + l match { + case d :: ds ⇒ link(ds, d withNext acc) + case Nil ⇒ acc + } + link(select(this, Nil), EmptyProps) + } +} + +/** + * Configure the maximum mailbox capacity for the actor. If more messages are + * enqueued because the actor does not process them quickly enough then further + * messages will be dropped. + * + * The default mailbox capacity that is used when this option is not given is + * taken from the `akka.typed.mailbox-capacity` configuration setting. + */ +@InternalApi +private[akka] final case class MailboxCapacity(capacity: Int, next: Props = Props.empty) extends Props { + private[akka] override def withNext(next: Props): Props = copy(next = next) +} + +/** + * The empty configuration node, used as a terminator for the internally linked + * list of each Props. + */ +@InternalApi +private[akka] case object EmptyProps extends Props { + override def next = throw new NoSuchElementException("EmptyProps has no next") + override def withNext(next: Props): Props = next +} + +/** + * Not intended for user extension. + */ +@DoNotInherit +abstract class DispatcherSelector extends Props + +/** + * Factories for [[DispatcherSelector]]s which describe which thread pool shall be used to run + * the actor to which this configuration is applied. Se the individual factory methods for details + * on the options. + * + * The default configuration if none of these options are present is to run + * the actor on the same executor as its parent. + */ +object DispatcherSelector { + + /** + * Scala API: + * Run the actor on the same executor as its parent. + */ + def default(): DispatcherSelector = DispatcherDefault() + + /** + * Java API: + * Run the actor on the same executor as its parent. + */ + def defaultDispatcher(): DispatcherSelector = default() + + /** + * Look up an executor definition in the [[ActorSystem]] configuration. + * ExecutorServices created in this fashion will be shut down when the + * ActorSystem terminates. + */ + def fromConfig(path: String): DispatcherSelector = DispatcherFromConfig(path) + + /** + * Directly use the given Executor whenever the actor needs to be run. + * No attempt will be made to shut down this thread pool when the [[ActorSystem]] terminates. + */ + def fromExecutor(executor: Executor): DispatcherSelector = DispatcherFromExecutor(executor) + + /** + * Directly use the given ExecutionContext whenever the actor needs to be run. + * No attempt will be made to shut down this thread pool when the [[ActorSystem]] terminates. + */ + def fromExecutionContext(executionContext: ExecutionContext): DispatcherSelector = + DispatcherFromExecutionContext(executionContext) + +} + +/** + * Use the [[ActorSystem]] default executor to run the actor. + * + * INTERNAL API + */ +@DoNotInherit +@InternalApi +private[akka] sealed case class DispatcherDefault(next: Props) extends DispatcherSelector { + @InternalApi + override def withNext(next: Props): Props = copy(next = next) +} +object DispatcherDefault { + // this is hidden in order to avoid having people match on this object + private val empty = DispatcherDefault(EmptyProps) + /** + * Retrieve an instance for this configuration node with empty `next` reference. + */ + def apply(): DispatcherDefault = empty +} + +/** + * Look up an executor definition in the [[ActorSystem]] configuration. + * ExecutorServices created in this fashion will be shut down when the + * ActorSystem terminates. + * + * INTERNAL API + */ +@InternalApi +private[akka] final case class DispatcherFromConfig(path: String, next: Props = Props.empty) extends DispatcherSelector { + override def withNext(next: Props): Props = copy(next = next) +} + +/** + * Directly use the given Executor whenever the actor needs to be run. + * No attempt will be made to shut down this thread pool, even if it is an + * instance of ExecutorService. + * + * INTERNAL API + */ +@InternalApi +private[akka] final case class DispatcherFromExecutor(executor: Executor, next: Props = Props.empty) extends DispatcherSelector { + override def withNext(next: Props): Props = copy(next = next) +} + +/** + * Directly use the given ExecutionContext whenever the actor needs to be run. + * No attempt will be made to shut down this thread pool, even if it is an + * instance of ExecutorService. + * + * INTERNAL API + */ +@InternalApi +private[akka] final case class DispatcherFromExecutionContext(ec: ExecutionContext, next: Props = Props.empty) extends DispatcherSelector { + override def withNext(next: Props): Props = copy(next = next) +} diff --git a/akka-typed/src/main/scala/akka/typed/ScalaDSL.scala b/akka-typed/src/main/scala/akka/typed/ScalaDSL.scala deleted file mode 100644 index bb3b46bca2..0000000000 --- a/akka-typed/src/main/scala/akka/typed/ScalaDSL.scala +++ /dev/null @@ -1,494 +0,0 @@ -/** - * Copyright (C) 2014-2017 Lightbend Inc. - */ -package akka.typed - -import scala.annotation.tailrec -import Behavior._ -import akka.util.LineNumbers - -/** - * This object holds several behavior factories and combinators that can be - * used to construct Behavior instances. - */ -@deprecated("use akka.typed.scaladsl.Actor", "2.5.0") -object ScalaDSL { - - // FIXME check that all behaviors can cope with not getting PreStart as first message - - implicit class BehaviorDecorators[T](val behavior: Behavior[T]) extends AnyVal { - /** - * Widen the type of this Behavior by providing a filter function that permits - * only a subtype of the widened set of messages. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def widen[U >: T](matcher: PartialFunction[U, T]): Behavior[U] = Widened(behavior, matcher) - /** - * Combine the two behaviors such that incoming messages are distributed - * to both of them, each one evolving its state independently. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def &&(other: Behavior[T]): Behavior[T] = And(behavior, other) - /** - * Combine the two behaviors such that incoming messages are given first to - * the left behavior and are then only passed on to the right behavior if - * the left one returned Unhandled. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def ||(other: Behavior[T]): Behavior[T] = Or(behavior, other) - } - - /** - * Widen the wrapped Behavior by placing a funnel in front of it: the supplied - * PartialFunction decides which message to pull in (those that it is defined - * at) and may transform the incoming message to place them into the wrapped - * Behavior’s type hierarchy. Signals are not transformed. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Widened[T, U >: T](behavior: Behavior[T], matcher: PartialFunction[U, T]) extends Behavior[U] { - private def postProcess(ctx: ActorContext[U], behv: Behavior[T]): Behavior[U] = - if (isUnhandled(behv)) Unhandled - else if (isAlive(behv)) { - val next = canonicalize(behv, behavior) - if (next eq behavior) Same else Widened(next, matcher) - } else Stopped - - override def management(ctx: ActorContext[U], msg: Signal): Behavior[U] = - postProcess(ctx, behavior.management(ctx.asInstanceOf[ActorContext[T]], msg)) - - override def message(ctx: ActorContext[U], msg: U): Behavior[U] = - if (matcher.isDefinedAt(msg)) - postProcess(ctx, behavior.message(ctx.asInstanceOf[ActorContext[T]], matcher(msg))) - else Unhandled - - override def toString: String = s"${behavior.toString}.widen(${LineNumbers(matcher)})" - } - - /** - * Wrap a behavior factory so that it runs upon PreStart, i.e. behavior creation - * is deferred to the child actor instead of running within the parent. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Deferred[T](factory: () ⇒ Behavior[T]) extends Behavior[T] { - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = { - if (msg != PreStart) throw new IllegalStateException(s"Deferred must receive PreStart as first message (got $msg)") - Behavior.preStart(factory(), ctx) - } - - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = - throw new IllegalStateException(s"Deferred must receive PreStart as first message (got $msg)") - - override def toString: String = s"Deferred(${LineNumbers(factory)})" - } - - /** - * Return this behavior from message processing in order to advise the - * system to reuse the previous behavior. This is provided in order to - * avoid the allocation overhead of recreating the current behavior where - * that is not necessary. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def Same[T]: Behavior[T] = sameBehavior.asInstanceOf[Behavior[T]] - - /** - * Return this behavior from message processing in order to advise the - * system to reuse the previous behavior, including the hint that the - * message has not been handled. This hint may be used by composite - * behaviors that delegate (partial) handling to other behaviors. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def Unhandled[T]: Behavior[T] = unhandledBehavior.asInstanceOf[Behavior[T]] - - /* - * TODO write a Behavior that waits for all child actors to stop and then - * runs some cleanup before stopping. The factory for this behavior should - * stop and watch all children to get the process started. - */ - - /** - * Return this behavior from message processing to signal that this actor - * shall terminate voluntarily. If this actor has created child actors then - * these will be stopped as part of the shutdown procedure. The PostStop - * signal that results from stopping this actor will NOT be passed to the - * current behavior, it will be effectively ignored. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def Stopped[T]: Behavior[T] = stoppedBehavior.asInstanceOf[Behavior[T]] - - /** - * This behavior does not handle any inputs, it is completely inert. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def Empty[T]: Behavior[T] = emptyBehavior.asInstanceOf[Behavior[T]] - - /** - * This behavior does not handle any inputs, it is completely inert. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def Ignore[T]: Behavior[T] = ignoreBehavior.asInstanceOf[Behavior[T]] - - /** - * Algebraic Data Type modeling either a [[Msg message]] or a - * [[Sig signal]], including the [[ActorContext]]. This type is - * used by several of the behaviors defined in this DSL, see for example - * [[Full]]. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - sealed trait MessageOrSignal[T] - /** - * A message bundled together with the current [[ActorContext]]. - */ - @SerialVersionUID(1L) - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Msg[T](ctx: scaladsl.ActorContext[T], msg: T) extends MessageOrSignal[T] - /** - * A signal bundled together with the current [[ActorContext]]. - */ - @SerialVersionUID(1L) - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Sig[T](ctx: scaladsl.ActorContext[T], signal: Signal) extends MessageOrSignal[T] - - /** - * This type of behavior allows to handle all incoming messages within - * the same user-provided partial function, be that a user message or a system - * signal. For messages that do not match the partial function the same - * behavior is emitted without change. This does entail that unhandled - * failures of child actors will lead to a failure in this actor. - * - * For the lifecycle notifications pertaining to the actor itself this - * behavior includes a fallback mechanism: an unhandled [[PreRestart]] signal - * will terminate all child actors (transitively) and then emit a [[PostStop]] - * signal in addition. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Full[T](behavior: PartialFunction[MessageOrSignal[T], Behavior[T]]) extends Behavior[T] { - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = { - lazy val fallback: (MessageOrSignal[T]) ⇒ Behavior[T] = { - case Sig(context, PreRestart) ⇒ - context.children foreach { child ⇒ - context.unwatch[Nothing](child) - context.stop(child) - } - behavior.applyOrElse(Sig(context, PostStop), fallback) - case _ ⇒ Unhandled - } - behavior.applyOrElse(Sig(ctx, msg), fallback) - } - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = { - behavior.applyOrElse(Msg(ctx, msg), unhandledFunction) - } - override def toString = s"Full(${LineNumbers(behavior)})" - } - - /** - * This type of behavior expects a total function that describes the actor’s - * reaction to all system signals or user messages, without providing a - * fallback mechanism for either. If you use partial function literal syntax - * to create the supplied function then any message not matching the list of - * cases will fail this actor with a [[scala.MatchError]]. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class FullTotal[T](behavior: MessageOrSignal[T] ⇒ Behavior[T]) extends Behavior[T] { - override def management(ctx: ActorContext[T], msg: Signal) = behavior(Sig(ctx, msg)) - override def message(ctx: ActorContext[T], msg: T) = behavior(Msg(ctx, msg)) - override def toString = s"FullTotal(${LineNumbers(behavior)})" - } - - /** - * This type of behavior is created from a total function from the declared - * message type to the next behavior, which means that all possible incoming - * messages for the given type must be handled. All system signals are - * ignored by this behavior, which implies that a failure of a child actor - * will be escalated unconditionally. - * - * This behavior type is most useful for leaf actors that do not create child - * actors themselves. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Total[T](behavior: T ⇒ Behavior[T]) extends Behavior[T] { - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = msg match { - case _ ⇒ Unhandled - } - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = behavior(msg) - override def toString = s"Total(${LineNumbers(behavior)})" - } - - /** - * This type of Behavior is created from a partial function from the declared - * message type to the next behavior, flagging all unmatched messages as - * [[#Unhandled]]. All system signals are - * ignored by this behavior, which implies that a failure of a child actor - * will be escalated unconditionally. - * - * This behavior type is most useful for leaf actors that do not create child - * actors themselves. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Partial[T](behavior: PartialFunction[T, Behavior[T]]) extends Behavior[T] { - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = msg match { - case _ ⇒ Unhandled - } - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = behavior.applyOrElse(msg, unhandledFunction) - override def toString = s"Partial(${LineNumbers(behavior)})" - } - - /** - * This type of Behavior wraps another Behavior while allowing you to perform - * some action upon each received message or signal. It is most commonly used - * for logging or tracing what a certain Actor does. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Tap[T](f: PartialFunction[MessageOrSignal[T], Unit], behavior: Behavior[T]) extends Behavior[T] { - private def canonical(behv: Behavior[T]): Behavior[T] = - if (isUnhandled(behv)) Unhandled - else if (behv eq sameBehavior) Same - else if (isAlive(behv)) Tap(f, behv) - else Stopped - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = { - f.applyOrElse(Sig(ctx, msg), unitFunction) - canonical(behavior.management(ctx, msg)) - } - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = { - f.applyOrElse(Msg(ctx, msg), unitFunction) - canonical(behavior.message(ctx, msg)) - } - override def toString = s"Tap(${LineNumbers(f)},$behavior)" - } - object Tap { - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def monitor[T](monitor: ActorRef[T], behavior: Behavior[T]): Tap[T] = Tap({ case Msg(_, msg) ⇒ monitor ! msg }, behavior) - } - - /** - * This type of behavior is a variant of [[Total]] that does not - * allow the actor to change behavior. It is an efficient choice for stateless - * actors, possibly entering such a behavior after finishing its - * initialization (which may be modeled using any of the other behavior types). - * - * This behavior type is most useful for leaf actors that do not create child - * actors themselves. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Static[T](behavior: T ⇒ Unit) extends Behavior[T] { - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = Unhandled - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = { - behavior(msg) - this - } - override def toString = s"Static(${LineNumbers(behavior)})" - } - - /** - * This behavior allows sending messages to itself without going through the - * Actor’s mailbox. A message sent like this will be processed before the next - * message is taken out of the mailbox. In case of Actor failures outstanding - * messages that were sent to the synchronous self reference will be lost. - * - * This decorator is useful for passing messages between the left and right - * sides of [[And]] and [[Or]] combinators. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class SynchronousSelf[T](f: ActorRef[T] ⇒ Behavior[T]) extends Behavior[T] { - - private class B extends Behavior[T] { - private val inbox = Inbox[T]("synchronousSelf") - private var _behavior = Behavior.validateAsInitial(f(inbox.ref)) - private def behavior = _behavior - private def setBehavior(ctx: ActorContext[T], b: Behavior[T]): Unit = - _behavior = canonicalize(b, _behavior) - - // FIXME should we protect against infinite loops? - @tailrec private def run(ctx: ActorContext[T], next: Behavior[T]): Behavior[T] = { - setBehavior(ctx, next) - if (inbox.hasMessages) run(ctx, behavior.message(ctx, inbox.receiveMsg())) - else if (isUnhandled(next)) Unhandled - else if (isAlive(next)) this - else Stopped - } - - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = - run(ctx, behavior.management(ctx, msg)) - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = - run(ctx, behavior.message(ctx, msg)) - - override def toString: String = s"SynchronousSelf($behavior)" - } - - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = { - if (msg != PreStart) throw new IllegalStateException(s"SynchronousSelf must receive PreStart as first message (got $msg)") - Behavior.preStart(new B(), ctx) - } - - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = - throw new IllegalStateException(s"SynchronousSelf must receive PreStart as first message (got $msg)") - - override def toString: String = s"SynchronousSelf(${LineNumbers(f)})" - } - - /** - * A behavior combinator that feeds incoming messages and signals both into - * the left and right sub-behavior and allows them to evolve independently of - * each other. When one of the sub-behaviors terminates the other takes over - * exclusively. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class And[T](left: Behavior[T], right: Behavior[T]) extends Behavior[T] { - - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = { - val l = left.management(ctx, msg) - val r = right.management(ctx, msg) - if (isUnhandled(l) && isUnhandled(r)) Unhandled - else { - val nextLeft = canonicalize(l, left) - val nextRight = canonicalize(r, right) - val leftAlive = isAlive(nextLeft) - val rightAlive = isAlive(nextRight) - - if (leftAlive && rightAlive) And(nextLeft, nextRight) - else if (leftAlive) nextLeft - else if (rightAlive) nextRight - else Stopped - } - } - - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = { - val l = left.message(ctx, msg) - val r = right.message(ctx, msg) - if (isUnhandled(l) && isUnhandled(r)) Unhandled - else { - val nextLeft = canonicalize(l, left) - val nextRight = canonicalize(r, right) - val leftAlive = isAlive(nextLeft) - val rightAlive = isAlive(nextRight) - - if (leftAlive && rightAlive) And(nextLeft, nextRight) - else if (leftAlive) nextLeft - else if (rightAlive) nextRight - else Stopped - } - } - } - - /** - * A behavior combinator that feeds incoming messages and signals either into - * the left or right sub-behavior and allows them to evolve independently of - * each other. The message or signal is passed first into the left sub-behavior - * and only if that results in [[#Unhandled]] is it passed to the right - * sub-behavior. When one of the sub-behaviors terminates the other takes over - * exclusively. - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - final case class Or[T](left: Behavior[T], right: Behavior[T]) extends Behavior[T] { - - override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = - left.management(ctx, msg) match { - case b if isUnhandled(b) ⇒ - val r = right.management(ctx, msg) - if (isUnhandled(r)) Unhandled - else { - val nr = canonicalize(r, right) - if (isAlive(nr)) Or(left, nr) else left - } - case nl ⇒ - val next = canonicalize(nl, left) - if (isAlive(next)) Or(next, right) else right - } - - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = - left.message(ctx, msg) match { - case b if isUnhandled(b) ⇒ - val r = right.message(ctx, msg) - if (isUnhandled(r)) Unhandled - else { - val nr = canonicalize(r, right) - if (isAlive(nr)) Or(left, nr) else left - } - case nl ⇒ - val next = canonicalize(nl, left) - if (isAlive(next)) Or(next, right) else right - } - } - - // TODO - // final case class Selective[T](timeout: FiniteDuration, selector: PartialFunction[T, Behavior[T]], onTimeout: () ⇒ Behavior[T]) - - /** - * A behavior decorator that extracts the self [[ActorRef]] while receiving the - * the first signal or message and uses that to construct the real behavior - * (which will then also receive that signal or message). - * - * Example: - * {{{ - * SelfAware[MyCommand] { self => - * Simple { - * case cmd => - * } - * } - * }}} - * - * This can also be used together with implicitly sender-capturing message - * types: - * {{{ - * final case class OtherMsg(msg: String)(implicit val replyTo: ActorRef[Reply]) - * - * SelfAware[MyCommand] { implicit self => - * Simple { - * case cmd => - * other ! OtherMsg("hello") // assuming Reply <: MyCommand - * } - * } - * }}} - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def SelfAware[T](behavior: ActorRef[T] ⇒ Behavior[T]): Behavior[T] = - new Behavior[T] { - override def management(ctx: ActorContext[T], sig: Signal): Behavior[T] = - if (sig == PreStart) Behavior.preStart(behavior(ctx.self), ctx) - else throw new IllegalStateException(s"SelfAware must receive PreStart as first message (got $sig)") - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = - throw new IllegalStateException(s"SelfAware must receive PreStart as first message (got $msg)") - } - - /** - * A behavior decorator that extracts the [[ActorContext]] while receiving the - * the first signal or message and uses that to construct the real behavior - * (which will then also receive that signal or message). - * - * Example: - * {{{ - * ContextAware[MyCommand] { ctx => Simple { - * case cmd => - * ... - * } - * } - * }}} - */ - @deprecated("use akka.typed.scaladsl.Actor", "2.5.0") - def ContextAware[T](behavior: scaladsl.ActorContext[T] ⇒ Behavior[T]): Behavior[T] = - new Behavior[T] { - override def management(ctx: ActorContext[T], sig: Signal): Behavior[T] = - if (sig == PreStart) Behavior.preStart(behavior(ctx), ctx) - else throw new IllegalStateException(s"ContextAware must receive PreStart as first message (got $sig)") - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = - throw new IllegalStateException(s"ContextAware must receive PreStart as first message (got ${msg.getClass})") - } - - /** - * INTERNAL API. - */ - private[akka] val _unhandledFunction = (_: Any) ⇒ Unhandled[Nothing] - /** - * INTERNAL API. - */ - private[akka] def unhandledFunction[T, U] = _unhandledFunction.asInstanceOf[(T ⇒ Behavior[U])] - - /** - * INTERNAL API. - */ - private[akka] val _unitFunction = (_: Any) ⇒ () - /** - * INTERNAL API. - */ - private[akka] def unitFunction[T, U] = _unhandledFunction.asInstanceOf[(T ⇒ Behavior[U])] - -} diff --git a/akka-typed/src/main/scala/akka/typed/SupervisorStrategy.scala b/akka-typed/src/main/scala/akka/typed/SupervisorStrategy.scala new file mode 100644 index 0000000000..86d04177d1 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/SupervisorStrategy.scala @@ -0,0 +1,119 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed + +import akka.annotation.InternalApi +import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration.Duration + +object SupervisorStrategy { + + /** + * Resume means keeping the same state as before the exception was + * thrown and is thus less safe than `restart`. + */ + val resume: SupervisorStrategy = Resume(loggingEnabled = true) + + /** + * Restart immediately without any limit on number of restart retries. + */ + val restart: SupervisorStrategy = Restart(-1, Duration.Zero, loggingEnabled = true) + + /** + * Restart with a limit of number of restart retries. + * The number of restarts are limited to a number of restart attempts (`maxNrOfRetries`) + * within a time range (`withinTimeRange`). When the time window has elapsed without reaching + * `maxNrOfRetries` the restart count is reset. + * + * @param maxNrOfRetries the number of times a child actor is allowed to be restarted, + * if the limit is exceeded the child actor is stopped + * @param withinTimeRange duration of the time window for maxNrOfRetries + */ + def restartWithLimit(maxNrOfRetries: Int, withinTimeRange: FiniteDuration): SupervisorStrategy = + Restart(maxNrOfRetries, withinTimeRange, loggingEnabled = true) + + /** + * It supports exponential back-off between the given `minBackoff` and + * `maxBackoff` durations. For example, if `minBackoff` is 3 seconds and + * `maxBackoff` 30 seconds the start attempts will be delayed with + * 3, 6, 12, 24, 30, 30 seconds. The exponential back-off counter is reset + * if the actor is not terminated within the `minBackoff` duration. + * + * In addition to the calculated exponential back-off an additional + * random delay based the given `randomFactor` is added, e.g. 0.2 adds up to 20% + * delay. The reason for adding a random delay is to avoid that all failing + * actors hit the backend resource at the same time. + * + * During the back-off incoming messages are dropped. + * + * If no new exception occurs within the `minBackoff` duration the exponentially + * increased back-off timeout is reset. + * + * @param minBackoff minimum (initial) duration until the child actor will + * started again, if it is terminated + * @param maxBackoff the exponential back-off is capped to this duration + * @param randomFactor after calculation of the exponential back-off an additional + * random delay based on this factor is added, e.g. `0.2` adds up to `20%` delay. + * In order to skip this additional delay pass in `0`. + */ + def restartWithBackoff( + minBackoff: FiniteDuration, + maxBackoff: FiniteDuration, + randomFactor: Double): BackoffSupervisorStrategy = + new Backoff(minBackoff, maxBackoff, randomFactor, resetBackoffAfter = minBackoff, loggingEnabled = true) + + /** + * INTERNAL API + */ + @InternalApi private[akka] case class Resume(loggingEnabled: Boolean) extends SupervisorStrategy { + override def withLoggingEnabled(enabled: Boolean): SupervisorStrategy = + copy(loggingEnabled = enabled) + } + + /** + * INTERNAL API + */ + @InternalApi private[akka] final case class Restart( + maxNrOfRetries: Int, + withinTimeRange: FiniteDuration, + loggingEnabled: Boolean) extends SupervisorStrategy { + + override def withLoggingEnabled(enabled: Boolean): SupervisorStrategy = + copy(loggingEnabled = enabled) + } + + /** + * INTERNAL API + */ + @InternalApi private[akka] final case class Backoff( + minBackoff: FiniteDuration, + maxBackoff: FiniteDuration, + randomFactor: Double, + resetBackoffAfter: FiniteDuration, + loggingEnabled: Boolean) extends BackoffSupervisorStrategy { + + override def withLoggingEnabled(enabled: Boolean): SupervisorStrategy = + copy(loggingEnabled = enabled) + + override def withResetBackoffAfter(timeout: FiniteDuration): BackoffSupervisorStrategy = + copy(resetBackoffAfter = timeout) + } +} + +sealed abstract class SupervisorStrategy { + def loggingEnabled: Boolean + + def withLoggingEnabled(on: Boolean): SupervisorStrategy +} + +sealed abstract class BackoffSupervisorStrategy extends SupervisorStrategy { + def resetBackoffAfter: FiniteDuration + + /** + * The back-off algorithm is reset if the actor does not crash within the + * specified `resetBackoffAfter`. By default, the `resetBackoffAfter` has + * the same value as `minBackoff`. + */ + def withResetBackoffAfter(timeout: FiniteDuration): BackoffSupervisorStrategy +} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/ActorAdapter.scala b/akka-typed/src/main/scala/akka/typed/adapter/ActorAdapter.scala deleted file mode 100644 index bd83b883f4..0000000000 --- a/akka-typed/src/main/scala/akka/typed/adapter/ActorAdapter.scala +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (C) 2016-2017 Lightbend Inc. - */ -package akka.typed -package adapter - -import akka.{ actor ⇒ a } - -private[typed] class ActorAdapter[T](_initialBehavior: Behavior[T]) extends a.Actor { - import Behavior._ - - var behavior: Behavior[T] = _initialBehavior - - if (!isAlive(behavior)) context.stop(self) - - val ctx = new ActorContextAdapter[T](context) - - var failures: Map[a.ActorRef, Throwable] = Map.empty - - def receive = { - case a.Terminated(ref) ⇒ - val msg = - if (failures contains ref) { - val ex = failures(ref) - failures -= ref - Terminated(ActorRefAdapter(ref))(ex) - } else Terminated(ActorRefAdapter(ref))(null) - next(behavior.management(ctx, msg), msg) - case a.ReceiveTimeout ⇒ - next(behavior.message(ctx, ctx.receiveTimeoutMsg), ctx.receiveTimeoutMsg) - case msg: T @unchecked ⇒ - next(behavior.message(ctx, msg), msg) - } - - private def next(b: Behavior[T], msg: Any): Unit = { - if (isUnhandled(b)) unhandled(msg) - behavior = canonicalize(b, behavior) - if (!isAlive(behavior)) context.stop(self) - } - - override def unhandled(msg: Any): Unit = msg match { - case Terminated(ref) ⇒ throw a.DeathPactException(toUntyped(ref)) - case other ⇒ super.unhandled(other) - } - - override val supervisorStrategy = a.OneForOneStrategy() { - case ex ⇒ - val ref = sender() - if (context.asInstanceOf[a.ActorCell].isWatching(ref)) failures = failures.updated(ref, ex) - a.SupervisorStrategy.Stop - } - - override def preStart(): Unit = - next(behavior.management(ctx, PreStart), PreStart) - override def preRestart(reason: Throwable, message: Option[Any]): Unit = - next(behavior.management(ctx, PreRestart), PreRestart) - override def postRestart(reason: Throwable): Unit = - next(behavior.management(ctx, PreStart), PreStart) - override def postStop(): Unit = - next(behavior.management(ctx, PostStop), PostStop) -} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/ActorContextAdapter.scala b/akka-typed/src/main/scala/akka/typed/adapter/ActorContextAdapter.scala deleted file mode 100644 index 129b756a72..0000000000 --- a/akka-typed/src/main/scala/akka/typed/adapter/ActorContextAdapter.scala +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (C) 2016-2017 Lightbend Inc. - */ -package akka.typed -package adapter - -import akka.{ actor ⇒ a } -import scala.concurrent.duration._ -import scala.concurrent.ExecutionContextExecutor - -/** - * INTERNAL API. Wrapping an [[akka.actor.ActorContext]] as an [[ActorContext]]. - */ -private[typed] class ActorContextAdapter[T](ctx: a.ActorContext) extends ActorContext[T] { - - override def self = ActorRefAdapter(ctx.self) - override val system = ActorSystemAdapter(ctx.system) - override def mailboxCapacity = 1 << 29 // FIXME - override def children = ctx.children.map(ActorRefAdapter(_)) - override def child(name: String) = ctx.child(name).map(ActorRefAdapter(_)) - override def spawnAnonymous[U](behavior: Behavior[U], deployment: DeploymentConfig = EmptyDeploymentConfig) = - ctx.spawnAnonymous(behavior, deployment) - override def spawn[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig = EmptyDeploymentConfig) = - ctx.spawn(behavior, name, deployment) - override def stop(child: ActorRef[_]) = - toUntyped(child) match { - case f: akka.actor.FunctionRef ⇒ - val cell = ctx.asInstanceOf[akka.actor.ActorCell] - cell.removeFunctionRef(f) - case untyped ⇒ - ctx.child(child.path.name) match { - case Some(`untyped`) ⇒ - ctx.stop(untyped) - true - case _ ⇒ - false // none of our business - } - } - override def watch[U](other: ActorRef[U]) = { ctx.watch(toUntyped(other)); other } - override def unwatch[U](other: ActorRef[U]) = { ctx.unwatch(toUntyped(other)); other } - var receiveTimeoutMsg: T = null.asInstanceOf[T] - override def setReceiveTimeout(d: FiniteDuration, msg: T) = { - receiveTimeoutMsg = msg - ctx.setReceiveTimeout(d) - } - override def cancelReceiveTimeout(): Unit = { - receiveTimeoutMsg = null.asInstanceOf[T] - ctx.setReceiveTimeout(Duration.Undefined) - } - override def executionContext: ExecutionContextExecutor = ctx.dispatcher - override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): a.Cancellable = { - import ctx.dispatcher - ctx.system.scheduler.scheduleOnce(delay, toUntyped(target), msg) - } - override def spawnAdapter[U](f: U ⇒ T, name: String = ""): ActorRef[U] = { - val cell = ctx.asInstanceOf[akka.actor.ActorCell] - val ref = cell.addFunctionRef((_, msg) ⇒ ctx.self ! f(msg.asInstanceOf[U]), name) - ActorRefAdapter[U](ref) - } - -} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/ActorRefAdapter.scala b/akka-typed/src/main/scala/akka/typed/adapter/ActorRefAdapter.scala deleted file mode 100644 index 3477beca3f..0000000000 --- a/akka-typed/src/main/scala/akka/typed/adapter/ActorRefAdapter.scala +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (C) 2016-2017 Lightbend Inc. - */ -package akka.typed -package adapter - -import akka.{ actor ⇒ a } - -private[typed] class ActorRefAdapter[-T](val untyped: a.InternalActorRef) - extends ActorRef[T](untyped.path) with internal.ActorRefImpl[T] { - - override def tell(msg: T): Unit = untyped ! msg - override def isLocal: Boolean = true - override def sendSystem(signal: internal.SystemMessage): Unit = sendSystemMessage(untyped, signal) -} - -private[typed] object ActorRefAdapter { - def apply[T](untyped: a.ActorRef): ActorRef[T] = new ActorRefAdapter(untyped.asInstanceOf[a.InternalActorRef]) -} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/PropsAdapter.scala b/akka-typed/src/main/scala/akka/typed/adapter/PropsAdapter.scala deleted file mode 100644 index 3ab936ad61..0000000000 --- a/akka-typed/src/main/scala/akka/typed/adapter/PropsAdapter.scala +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (C) 2016-2017 Lightbend Inc. - */ -package akka.typed -package adapter - -import akka.{ actor ⇒ a } - -private[typed] object PropsAdapter { - - // FIXME dispatcher and queue size - def apply(b: Behavior[_], deploy: DeploymentConfig): a.Props = new a.Props(a.Deploy(), classOf[ActorAdapter[_]], (b: AnyRef) :: Nil) - - def apply[T](p: a.Props): Behavior[T] = { - assert(p.clazz == classOf[ActorAdapter[_]], "typed.Actor must have typed.Props") - p.args match { - case (initial: Behavior[_]) :: Nil ⇒ - // FIXME queue size - initial.asInstanceOf[Behavior[T]] - case _ ⇒ throw new AssertionError("typed.Actor args must be right") - } - } - -} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/package.scala b/akka-typed/src/main/scala/akka/typed/adapter/package.scala deleted file mode 100644 index 30222a221d..0000000000 --- a/akka-typed/src/main/scala/akka/typed/adapter/package.scala +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (C) 2016-2017 Lightbend Inc. - */ -package akka.typed - -package object adapter { - - import language.implicitConversions - import akka.dispatch.sysmsg - - implicit class ActorSystemOps(val sys: akka.actor.ActorSystem) extends AnyVal { - def spawnAnonymous[T](behavior: Behavior[T], deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[T] = - ActorRefAdapter(sys.actorOf(PropsAdapter(Behavior.validateAsInitial(behavior), deployment))) - def spawn[T](behavior: Behavior[T], name: String, deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[T] = - ActorRefAdapter(sys.actorOf(PropsAdapter(Behavior.validateAsInitial(behavior), deployment), name)) - } - - implicit class ActorContextOps(val ctx: akka.actor.ActorContext) extends AnyVal { - def spawnAnonymous[T](behavior: Behavior[T], deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[T] = - ActorRefAdapter(ctx.actorOf(PropsAdapter(Behavior.validateAsInitial(behavior), deployment))) - def spawn[T](behavior: Behavior[T], name: String, deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[T] = - ActorRefAdapter(ctx.actorOf(PropsAdapter(Behavior.validateAsInitial(behavior), deployment), name)) - } - - implicit def actorRefAdapter(ref: akka.actor.ActorRef): ActorRef[Any] = ActorRefAdapter(ref) - - private[adapter] def toUntyped[U](ref: ActorRef[U]): akka.actor.InternalActorRef = - ref match { - case adapter: ActorRefAdapter[_] ⇒ adapter.untyped - case _ ⇒ throw new UnsupportedOperationException(s"only adapted untyped ActorRefs permissible ($ref of class ${ref.getClass})") - } - - private[adapter] def sendSystemMessage(untyped: akka.actor.InternalActorRef, signal: internal.SystemMessage): Unit = - signal match { - case internal.Create() ⇒ throw new IllegalStateException("WAT? No, seriously.") - case internal.Terminate() ⇒ untyped.stop() - case internal.Watch(watchee, watcher) ⇒ untyped.sendSystemMessage(sysmsg.Watch(toUntyped(watchee), toUntyped(watcher))) - case internal.Unwatch(watchee, watcher) ⇒ untyped.sendSystemMessage(sysmsg.Unwatch(toUntyped(watchee), toUntyped(watcher))) - case internal.DeathWatchNotification(ref, cause) ⇒ untyped.sendSystemMessage(sysmsg.DeathWatchNotification(toUntyped(ref), true, false)) - case internal.NoMessage ⇒ // just to suppress the warning - } - -} diff --git a/akka-typed/src/main/scala/akka/typed/internal/ActorCell.scala b/akka-typed/src/main/scala/akka/typed/internal/ActorCell.scala index 67cb6ab60a..8f1d2dacb4 100644 --- a/akka-typed/src/main/scala/akka/typed/internal/ActorCell.scala +++ b/akka-typed/src/main/scala/akka/typed/internal/ActorCell.scala @@ -6,18 +6,20 @@ package internal import akka.actor.InvalidActorNameException import akka.util.Helpers -import scala.concurrent.duration.{ Duration, FiniteDuration } +import scala.concurrent.duration.FiniteDuration import akka.dispatch.ExecutionContexts import scala.concurrent.ExecutionContextExecutor import akka.actor.Cancellable import akka.util.Unsafe.{ instance ⇒ unsafe } import java.util.concurrent.ConcurrentLinkedQueue import java.util.Queue -import scala.annotation.{ tailrec, switch } +import scala.annotation.tailrec import scala.util.control.NonFatal import scala.util.control.Exception.Catcher import akka.event.Logging.Error import akka.event.Logging +import akka.typed.Behavior.StoppedBehavior +import akka.util.OptionVal /** * INTERNAL API @@ -58,6 +60,7 @@ object ActorCell { final val SuspendedState = 1 final val SuspendedWaitForChildrenState = 2 + /** compile time constant */ final val Debug = false } @@ -70,7 +73,7 @@ private[typed] class ActorCell[T]( override val executionContext: ExecutionContextExecutor, override val mailboxCapacity: Int, val parent: ActorRefImpl[Nothing]) - extends ActorContext[T] with Runnable with SupervisionMechanics[T] with DeathWatch[T] { + extends ActorContextImpl[T] with Runnable with SupervisionMechanics[T] with DeathWatch[T] { import ActorCell._ /* @@ -100,11 +103,11 @@ private[typed] class ActorCell[T]( protected def ctx: ActorContext[T] = this - override def spawn[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig): ActorRef[U] = { + override def spawn[U](behavior: Behavior[U], name: String, props: Props): ActorRef[U] = { if (childrenMap contains name) throw InvalidActorNameException(s"actor name [$name] is not unique") if (terminatingMap contains name) throw InvalidActorNameException(s"actor name [$name] is not yet free") - val dispatcher = deployment.firstOrElse[DispatcherSelector](DispatcherFromExecutionContext(executionContext)) - val capacity = deployment.firstOrElse(MailboxCapacity(system.settings.DefaultMailboxCapacity)) + val dispatcher = props.firstOrElse[DispatcherSelector](DispatcherFromExecutionContext(executionContext)) + val capacity = props.firstOrElse(MailboxCapacity(system.settings.DefaultMailboxCapacity)) val cell = new ActorCell[U](system, Behavior.validateAsInitial(behavior), system.dispatchers.lookup(dispatcher), capacity.capacity, self) // TODO uid is still needed val ref = new LocalActorRef[U](self.path / name, cell) @@ -115,13 +118,13 @@ private[typed] class ActorCell[T]( } private var nextName = 0L - override def spawnAnonymous[U](behavior: Behavior[U], deployment: DeploymentConfig): ActorRef[U] = { + override def spawnAnonymous[U](behavior: Behavior[U], props: Props): ActorRef[U] = { val name = Helpers.base64(nextName) nextName += 1 - spawn(behavior, name, deployment) + spawn(behavior, name, props) } - override def stop(child: ActorRef[_]): Boolean = { + override def stop[U](child: ActorRef[U]): Boolean = { val name = child.path.name childrenMap.get(name) match { case None ⇒ false @@ -145,7 +148,7 @@ private[typed] class ActorCell[T]( override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): Cancellable = system.scheduler.scheduleOnce(delay)(target ! msg)(ExecutionContexts.sameThreadExecutionContext) - override def spawnAdapter[U](f: U ⇒ T, _name: String = ""): ActorRef[U] = { + override private[akka] def internalSpawnAdapter[U](f: U ⇒ T, _name: String): ActorRef[U] = { val baseName = Helpers.base64(nextName, new java.lang.StringBuilder("$!")) nextName += 1 val name = if (_name != "") s"$baseName-${_name}" else baseName @@ -323,8 +326,25 @@ private[typed] class ActorCell[T]( protected def next(b: Behavior[T], msg: Any): Unit = { if (Behavior.isUnhandled(b)) unhandled(msg) - behavior = Behavior.canonicalize(b, behavior) - if (!Behavior.isAlive(behavior)) self.sendSystem(Terminate()) + else { + b match { + case s: StoppedBehavior[T] ⇒ + // use StoppedBehavior with previous behavior or an explicitly given `postStop` behavior + // until Terminate is received, i.e until finishTerminate is invoked, and there PostStop + // will be signaled to the previous/postStop behavior + s.postStop match { + case OptionVal.None ⇒ + // use previous as the postStop behavior + behavior = new Behavior.StoppedBehavior(OptionVal.Some(behavior)) + case OptionVal.Some(postStop) ⇒ + // use the given postStop behavior, but canonicalize it + behavior = new Behavior.StoppedBehavior(OptionVal.Some(Behavior.canonicalize(postStop, behavior, ctx))) + } + self.sendSystem(Terminate()) + case _ ⇒ + behavior = Behavior.canonicalize(b, behavior, ctx) + } + } } private def unhandled(msg: Any): Unit = msg match { @@ -351,7 +371,7 @@ private[typed] class ActorCell[T]( */ private def processMessage(msg: T): Unit = { if (Debug) println(s"[$thread] $self processing message $msg") - next(behavior.message(this, msg), msg) + next(Behavior.interpretMessage(behavior, this, msg), msg) if (Thread.interrupted()) throw new InterruptedException("Interrupted while processing actor messages") } diff --git a/akka-typed/src/main/scala/akka/typed/internal/ActorContextImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/ActorContextImpl.scala new file mode 100644 index 0000000000..ff03f7bd6a --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/ActorContextImpl.scala @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed +package internal + +import akka.annotation.InternalApi +import java.util.Optional +import java.util.ArrayList +import scala.concurrent.ExecutionContextExecutor + +/** + * INTERNAL API + */ +@InternalApi private[akka] trait ActorContextImpl[T] extends ActorContext[T] with javadsl.ActorContext[T] with scaladsl.ActorContext[T] { + + override def asJava: javadsl.ActorContext[T] = this + + override def asScala: scaladsl.ActorContext[T] = this + + override def getChild(name: String): Optional[ActorRef[Void]] = + child(name) match { + case Some(c) ⇒ Optional.of(c.upcast[Void]) + case None ⇒ Optional.empty() + } + + override def getChildren: java.util.List[akka.typed.ActorRef[Void]] = { + val c = children + val a = new ArrayList[ActorRef[Void]](c.size) + val i = c.iterator + while (i.hasNext) a.add(i.next().upcast[Void]) + a + } + + override def getExecutionContext: ExecutionContextExecutor = + executionContext + + override def getMailboxCapacity: Int = + mailboxCapacity + + override def getSelf: akka.typed.ActorRef[T] = + self + + override def getSystem: akka.typed.ActorSystem[Void] = + system.asInstanceOf[ActorSystem[Void]] + + override def spawn[U](behavior: akka.typed.Behavior[U], name: String): akka.typed.ActorRef[U] = + spawn(behavior, name, Props.empty) + + override def spawnAnonymous[U](behavior: akka.typed.Behavior[U]): akka.typed.ActorRef[U] = + spawnAnonymous(behavior, Props.empty) + + override def spawnAdapter[U](f: U ⇒ T, name: String): ActorRef[U] = + internalSpawnAdapter(f, name) + + override def spawnAdapter[U](f: U ⇒ T): ActorRef[U] = + internalSpawnAdapter(f, "") + + override def spawnAdapter[U](f: java.util.function.Function[U, T]): akka.typed.ActorRef[U] = + internalSpawnAdapter(f.apply _, "") + + override def spawnAdapter[U](f: java.util.function.Function[U, T], name: String): akka.typed.ActorRef[U] = + internalSpawnAdapter(f.apply _, name) + + /** + * INTERNAL API: Needed to make Scala 2.12 compiler happy. + * Otherwise "ambiguous reference to overloaded definition" because Function is lambda. + */ + @InternalApi private[akka] def internalSpawnAdapter[U](f: U ⇒ T, _name: String): ActorRef[U] +} + diff --git a/akka-typed/src/main/scala/akka/typed/internal/ActorRefImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/ActorRefImpl.scala index 0176727070..5f5766925f 100644 --- a/akka-typed/src/main/scala/akka/typed/internal/ActorRefImpl.scala +++ b/akka-typed/src/main/scala/akka/typed/internal/ActorRefImpl.scala @@ -12,6 +12,7 @@ import scala.util.control.NonFatal import scala.concurrent.Future import java.util.ArrayList import scala.util.{ Success, Failure } +import scala.annotation.unchecked.uncheckedVariance /** * Every ActorRef is also an ActorRefImpl, but these two methods shall be @@ -22,13 +23,38 @@ import scala.util.{ Success, Failure } private[typed] trait ActorRefImpl[-T] extends ActorRef[T] { def sendSystem(signal: SystemMessage): Unit def isLocal: Boolean + + final override def narrow[U <: T]: ActorRef[U] = this.asInstanceOf[ActorRef[U]] + + final override def upcast[U >: T @uncheckedVariance]: ActorRef[U] = this.asInstanceOf[ActorRef[U]] + + /** + * Comparison takes path and the unique id of the actor cell into account. + */ + final override def compareTo(other: ActorRef[_]) = { + val x = this.path compareTo other.path + if (x == 0) if (this.path.uid < other.path.uid) -1 else if (this.path.uid == other.path.uid) 0 else 1 + else x + } + + final override def hashCode: Int = path.uid + + /** + * Equals takes path and the unique id of the actor cell into account. + */ + final override def equals(that: Any): Boolean = that match { + case other: ActorRef[_] ⇒ path.uid == other.path.uid && path == other.path + case _ ⇒ false + } + + override def toString: String = s"Actor[${path}#${path.uid}]" } /** * A local ActorRef that is backed by an asynchronous [[ActorCell]]. */ -private[typed] class LocalActorRef[-T](_path: a.ActorPath, cell: ActorCell[T]) - extends ActorRef[T](_path) with ActorRefImpl[T] { +private[typed] class LocalActorRef[-T](override val path: a.ActorPath, cell: ActorCell[T]) + extends ActorRef[T] with ActorRefImpl[T] { override def tell(msg: T): Unit = cell.send(msg) override def sendSystem(signal: SystemMessage): Unit = cell.sendSystem(signal) final override def isLocal: Boolean = true @@ -41,7 +67,8 @@ private[typed] class LocalActorRef[-T](_path: a.ActorPath, cell: ActorCell[T]) * terminates (meaning: no Hawking radiation). */ private[typed] object BlackholeActorRef - extends ActorRef[Any](a.RootActorPath(a.Address("akka.typed.internal", "blackhole"))) with ActorRefImpl[Any] { + extends ActorRef[Any] with ActorRefImpl[Any] { + override val path: a.ActorPath = a.RootActorPath(a.Address("akka.typed.internal", "blackhole")) override def tell(msg: Any): Unit = () override def sendSystem(signal: SystemMessage): Unit = () final override def isLocal: Boolean = true @@ -82,7 +109,7 @@ private[typed] final class FunctionRef[-T]( /** * The mechanics for synthetic ActorRefs that have a lifecycle and support being watched. */ -private[typed] abstract class WatchableRef[-T](_p: a.ActorPath) extends ActorRef[T](_p) with ActorRefImpl[T] { +private[typed] abstract class WatchableRef[-T](override val path: a.ActorPath) extends ActorRef[T] with ActorRefImpl[T] { import WatchableRef._ /** diff --git a/akka-typed/src/main/scala/akka/typed/internal/ActorSystemImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/ActorSystemImpl.scala index d65d0255c6..ee425d16c0 100644 --- a/akka-typed/src/main/scala/akka/typed/internal/ActorSystemImpl.scala +++ b/akka-typed/src/main/scala/akka/typed/internal/ActorSystemImpl.scala @@ -24,21 +24,23 @@ import java.util.concurrent.atomic.AtomicInteger import akka.typed.scaladsl.AskPattern object ActorSystemImpl { - import ScalaDSL._ sealed trait SystemCommand - case class CreateSystemActor[T](behavior: Behavior[T], name: String, deployment: DeploymentConfig)(val replyTo: ActorRef[ActorRef[T]]) extends SystemCommand + case class CreateSystemActor[T](behavior: Behavior[T], name: String, props: Props)(val replyTo: ActorRef[ActorRef[T]]) extends SystemCommand - val systemGuardianBehavior: Behavior[SystemCommand] = - ContextAware { ctx ⇒ + val systemGuardianBehavior: Behavior[SystemCommand] = { + import scaladsl.Actor + Actor.deferred { _ ⇒ var i = 1 - Static { - case create: CreateSystemActor[t] ⇒ + Actor.immutable { + case (ctx, create: CreateSystemActor[t]) ⇒ val name = s"$i-${create.name}" i += 1 - create.replyTo ! ctx.spawn(create.behavior, name, create.deployment) + create.replyTo ! ctx.spawn(create.behavior, name, create.props) + Actor.same } } + } } /* @@ -68,13 +70,13 @@ Distributed Data: */ private[typed] class ActorSystemImpl[-T]( - override val name: String, - _config: Config, - _cl: ClassLoader, - _ec: Option[ExecutionContext], - _userGuardianBehavior: Behavior[T], - _userGuardianDeployment: DeploymentConfig) - extends ActorRef[T](a.RootActorPath(a.Address("akka", name)) / "user") with ActorSystem[T] with ActorRefImpl[T] { + override val name: String, + _config: Config, + _cl: ClassLoader, + _ec: Option[ExecutionContext], + _userGuardianBehavior: Behavior[T], + _userGuardianProps: Props) + extends ActorSystem[T] with ActorRef[T] with ActorRefImpl[T] with ExtensionsImpl { import ActorSystemImpl._ @@ -83,6 +85,8 @@ private[typed] class ActorSystemImpl[-T]( "invalid ActorSystem name [" + name + "], must contain only word characters (i.e. [a-zA-Z0-9] plus non-leading '-' or '_')") + final override val path: a.ActorPath = a.RootActorPath(a.Address("akka", name)) / "user" + override val settings: Settings = new Settings(_cl, _config, name) override def logConfiguration(): Unit = log.info(settings.toString) @@ -160,7 +164,7 @@ private[typed] class ActorSystemImpl[-T]( */ private object eventStreamStub extends e.EventStream(null, false) { override def subscribe(ref: a.ActorRef, ch: Class[_]): Boolean = - throw new UnsupportedOperationException("cannot use this eventstream for subscribing") + throw new UnsupportedOperationException("Cannot use this eventstream for subscribing") override def publish(event: AnyRef): Unit = eventStream.publish(event) } /** @@ -182,13 +186,15 @@ private[typed] class ActorSystemImpl[-T]( private val terminationPromise: Promise[Terminated] = Promise() - private val rootPath: a.ActorPath = a.RootActorPath(a.Address("typed", name)) + private val rootPath: a.ActorPath = a.RootActorPath(a.Address("akka", name)) private val topLevelActors = new ConcurrentSkipListSet[ActorRefImpl[Nothing]] private val terminateTriggered = new AtomicBoolean private val theOneWhoWalksTheBubblesOfSpaceTime: ActorRefImpl[Nothing] = - new ActorRef[Nothing](rootPath) with ActorRefImpl[Nothing] { - override def tell(msg: Nothing): Unit = throw new UnsupportedOperationException("cannot send to theOneWhoWalksTheBubblesOfSpaceTime") + new ActorRef[Nothing] with ActorRefImpl[Nothing] { + override def path: a.ActorPath = rootPath + override def tell(msg: Nothing): Unit = + throw new UnsupportedOperationException("Cannot send to theOneWhoWalksTheBubblesOfSpaceTime") override def sendSystem(signal: SystemMessage): Unit = signal match { case Terminate() ⇒ if (terminateTriggered.compareAndSet(false, true)) @@ -208,9 +214,9 @@ private[typed] class ActorSystemImpl[-T]( override def isLocal: Boolean = true } - private def createTopLevel[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig): ActorRefImpl[U] = { - val dispatcher = deployment.firstOrElse[DispatcherSelector](DispatcherFromExecutionContext(executionContext)) - val capacity = deployment.firstOrElse(MailboxCapacity(settings.DefaultMailboxCapacity)) + private def createTopLevel[U](behavior: Behavior[U], name: String, props: Props): ActorRefImpl[U] = { + val dispatcher = props.firstOrElse[DispatcherSelector](DispatcherFromExecutionContext(executionContext)) + val capacity = props.firstOrElse(MailboxCapacity(settings.DefaultMailboxCapacity)) val cell = new ActorCell(this, behavior, dispatchers.lookup(dispatcher), capacity.capacity, theOneWhoWalksTheBubblesOfSpaceTime) val ref = new LocalActorRef(rootPath / name, cell) cell.setSelf(ref) @@ -219,17 +225,19 @@ private[typed] class ActorSystemImpl[-T]( ref } - private val systemGuardian: ActorRefImpl[SystemCommand] = createTopLevel(systemGuardianBehavior, "system", EmptyDeploymentConfig) + private val systemGuardian: ActorRefImpl[SystemCommand] = createTopLevel(systemGuardianBehavior, "system", EmptyProps) override val receptionist: ActorRef[patterns.Receptionist.Command] = ActorRef(systemActorOf(patterns.Receptionist.behavior, "receptionist")(settings.untyped.CreationTimeout)) - private val userGuardian: ActorRefImpl[T] = createTopLevel(_userGuardianBehavior, "user", _userGuardianDeployment) + private val userGuardian: ActorRefImpl[T] = createTopLevel(_userGuardianBehavior, "user", _userGuardianProps) // now we can start up the loggers eventStream.startUnsubscriber(this) eventStream.startDefaultLoggers(this) + loadExtensions() + override def terminate(): Future[Terminated] = { theOneWhoWalksTheBubblesOfSpaceTime.sendSystem(Terminate()) terminationPromise.future @@ -237,7 +245,8 @@ private[typed] class ActorSystemImpl[-T]( override def whenTerminated: Future[Terminated] = terminationPromise.future override def deadLetters[U]: ActorRefImpl[U] = - new ActorRef[U](rootPath) with ActorRefImpl[U] { + new ActorRef[U] with ActorRefImpl[U] { + override def path: a.ActorPath = rootPath override def tell(msg: U): Unit = eventStream.publish(DeadLetter(msg)) override def sendSystem(signal: SystemMessage): Unit = { signal match { @@ -253,10 +262,10 @@ private[typed] class ActorSystemImpl[-T]( override def sendSystem(msg: SystemMessage): Unit = userGuardian.sendSystem(msg) override def isLocal: Boolean = true - def systemActorOf[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig)(implicit timeout: Timeout): Future[ActorRef[U]] = { + def systemActorOf[U](behavior: Behavior[U], name: String, props: Props)(implicit timeout: Timeout): Future[ActorRef[U]] = { import AskPattern._ implicit val sched = scheduler - systemGuardian ? CreateSystemActor(behavior, name, deployment) + systemGuardian ? CreateSystemActor(behavior, name, props) } def printTree: String = { diff --git a/akka-typed/src/main/scala/akka/typed/internal/BehaviorImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/BehaviorImpl.scala new file mode 100644 index 0000000000..2e0d9d4812 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/BehaviorImpl.scala @@ -0,0 +1,179 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed +package internal + +import akka.util.LineNumbers +import akka.annotation.InternalApi +import akka.typed.{ ActorContext ⇒ AC } +import akka.typed.scaladsl.{ ActorContext ⇒ SAC } +import akka.typed.scaladsl.Actor +import scala.reflect.ClassTag +import scala.annotation.tailrec + +/** + * INTERNAL API + */ +@InternalApi private[akka] object BehaviorImpl { + import Behavior._ + + private val _nullFun = (_: Any) ⇒ null + private def nullFun[T] = _nullFun.asInstanceOf[Any ⇒ T] + + implicit class ContextAs[T](val ctx: AC[T]) extends AnyVal { + def as[U] = ctx.asInstanceOf[AC[U]] + } + + def widened[T, U](behavior: Behavior[T], matcher: PartialFunction[U, T]): Behavior[U] = { + behavior match { + case d: DeferredBehavior[T] ⇒ + DeferredBehavior[U] { ctx ⇒ + val c = ctx.asInstanceOf[akka.typed.ActorContext[T]] + val b = Behavior.validateAsInitial(Behavior.undefer(d, c)) + Widened(b, matcher) + } + case _ ⇒ + Widened(behavior, matcher) + } + } + + private final case class Widened[T, U](behavior: Behavior[T], matcher: PartialFunction[U, T]) extends ExtensibleBehavior[U] { + @tailrec + private def canonical(b: Behavior[T], ctx: AC[T]): Behavior[U] = { + if (isUnhandled(b)) unhandled + else if ((b eq SameBehavior) || (b eq this)) same + else if (!Behavior.isAlive(b)) Behavior.stopped + else { + b match { + case d: DeferredBehavior[T] ⇒ canonical(Behavior.undefer(d, ctx), ctx) + case _ ⇒ Widened(b, matcher) + } + } + } + + override def receiveSignal(ctx: AC[U], signal: Signal): Behavior[U] = + canonical(Behavior.interpretSignal(behavior, ctx.as[T], signal), ctx.as[T]) + + override def receiveMessage(ctx: AC[U], msg: U): Behavior[U] = + matcher.applyOrElse(msg, nullFun) match { + case null ⇒ unhandled + case transformed ⇒ canonical(Behavior.interpretMessage(behavior, ctx.as[T], transformed), ctx.as[T]) + } + + override def toString: String = s"${behavior.toString}.widen(${LineNumbers(matcher)})" + } + + class ImmutableBehavior[T]( + val onMessage: (SAC[T], T) ⇒ Behavior[T], + onSignal: PartialFunction[(SAC[T], Signal), Behavior[T]] = Behavior.unhandledSignal.asInstanceOf[PartialFunction[(SAC[T], Signal), Behavior[T]]]) + extends ExtensibleBehavior[T] { + + override def receiveSignal(ctx: AC[T], msg: Signal): Behavior[T] = + onSignal.applyOrElse((ctx.asScala, msg), Behavior.unhandledSignal.asInstanceOf[PartialFunction[(SAC[T], Signal), Behavior[T]]]) + override def receiveMessage(ctx: AC[T], msg: T) = onMessage(ctx.asScala, msg) + override def toString = s"Immutable(${LineNumbers(onMessage)})" + } + + def tap[T]( + onMessage: Function2[SAC[T], T, _], + onSignal: Function2[SAC[T], Signal, _], + behavior: Behavior[T]): Behavior[T] = { + intercept[T, T]( + beforeMessage = (ctx, msg) ⇒ { + onMessage(ctx, msg) + msg + }, + beforeSignal = (ctx, sig) ⇒ { + onSignal(ctx, sig) + true + }, + afterMessage = (ctx, msg, b) ⇒ b, // TODO optimize by using more ConstantFun + afterSignal = (ctx, sig, b) ⇒ b, + behavior)(ClassTag(classOf[Any])) + } + + /** + * Intercept another `behavior` by invoking `beforeMessage` for + * messages of type `U`. That can be another type than the type of + * the behavior. `beforeMessage` may transform the incoming message, + * or discard it by returning `null`. Note that `beforeMessage` is + * only invoked for messages of type `U`. + * + * Signals can also be intercepted but not transformed. They can + * be discarded by returning `false` from the `beforeOnSignal` function. + * + * The returned behavior from processing messages and signals can also be + * intercepted, e.g. to return another `Behavior`. The passed message to + * `afterMessage` is the message returned from `beforeMessage` (possibly + * different than the incoming message). + */ + def intercept[T, U <: Any: ClassTag]( + beforeMessage: Function2[SAC[U], U, T], + beforeSignal: Function2[SAC[T], Signal, Boolean], + afterMessage: Function3[SAC[T], T, Behavior[T], Behavior[T]], + afterSignal: Function3[SAC[T], Signal, Behavior[T], Behavior[T]], + behavior: Behavior[T], + toStringPrefix: String = "Intercept"): Behavior[T] = { + behavior match { + case d: DeferredBehavior[T] ⇒ + DeferredBehavior[T] { ctx ⇒ + val c = ctx.asInstanceOf[akka.typed.ActorContext[T]] + val b = Behavior.validateAsInitial(Behavior.undefer(d, c)) + Intercept(beforeMessage, beforeSignal, afterMessage, afterSignal, b, toStringPrefix) + } + case _ ⇒ + Intercept(beforeMessage, beforeSignal, afterMessage, afterSignal, behavior, toStringPrefix) + } + } + + private final case class Intercept[T, U <: Any: ClassTag]( + beforeOnMessage: Function2[SAC[U], U, T], + beforeOnSignal: Function2[SAC[T], Signal, Boolean], + afterMessage: Function3[SAC[T], T, Behavior[T], Behavior[T]], + afterSignal: Function3[SAC[T], Signal, Behavior[T], Behavior[T]], + behavior: Behavior[T], + toStringPrefix: String = "Intercept") extends ExtensibleBehavior[T] { + + @tailrec + private def canonical(b: Behavior[T], ctx: ActorContext[T]): Behavior[T] = { + if (isUnhandled(b)) unhandled + else if ((b eq SameBehavior) || (b eq this)) same + else if (!Behavior.isAlive(b)) b + else { + b match { + case d: DeferredBehavior[T] ⇒ canonical(Behavior.undefer(d, ctx), ctx) + case _ ⇒ Intercept(beforeOnMessage, beforeOnSignal, afterMessage, afterSignal, b) + } + } + } + + override def receiveSignal(ctx: AC[T], signal: Signal): Behavior[T] = { + val next: Behavior[T] = + if (beforeOnSignal(ctx.asScala, signal)) + Behavior.interpretSignal(behavior, ctx, signal) + else + same + canonical(afterSignal(ctx.asScala, signal, next), ctx) + } + + override def receiveMessage(ctx: AC[T], msg: T): Behavior[T] = { + msg match { + case m: U ⇒ + val msg2 = beforeOnMessage(ctx.asScala.asInstanceOf[SAC[U]], m) + val next: Behavior[T] = + if (msg2 == null) + same + else + Behavior.interpretMessage(behavior, ctx, msg2) + canonical(afterMessage(ctx.asScala, msg2, next), ctx) + case _ ⇒ + val next: Behavior[T] = Behavior.interpretMessage(behavior, ctx, msg) + canonical(afterMessage(ctx.asScala, msg, next), ctx) + } + } + + override def toString = s"$toStringPrefix(${LineNumbers(beforeOnMessage)},${LineNumbers(beforeOnSignal)},$behavior)" + } + +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/DeathWatch.scala b/akka-typed/src/main/scala/akka/typed/internal/DeathWatch.scala index d5016fb530..90fcfd865e 100644 --- a/akka-typed/src/main/scala/akka/typed/internal/DeathWatch.scala +++ b/akka-typed/src/main/scala/akka/typed/internal/DeathWatch.scala @@ -41,21 +41,34 @@ private[typed] trait DeathWatch[T] { type ARImpl = ActorRefImpl[Nothing] - private var watching = Set.empty[ARImpl] + /** + * This map holds a [[None]] for actors for which we send a [[Terminated]] notification on termination, + * ``Some(message)`` for actors for which we send a custom termination message. + */ + private var watching = Map.empty[ARImpl, Option[T]] private var watchedBy = Set.empty[ARImpl] - final def watch[U](_a: ActorRef[U]): ActorRef[U] = { - val a = _a.sorry + final def watch[U](subject: ActorRef[U]): Unit = { + val a = subject.sorry if (a != self && !watching.contains(a)) { maintainAddressTerminatedSubscription(a) { a.sendSystem(Watch(a, self)) - watching += a + watching = watching.updated(a, None) } } - a } - final def unwatch[U](_a: ActorRef[U]): ActorRef[U] = { + final def watchWith[U](subject: ActorRef[U], msg: T): Unit = { + val a = subject.sorry + if (a != self && !watching.contains(a)) { + maintainAddressTerminatedSubscription(a) { + a.sendSystem(Watch(a, self)) + watching = watching.updated(a, Some(msg)) + } + } + } + + final def unwatch[U](_a: ActorRef[U]): Unit = { val a = _a.sorry if (a != self && watching.contains(a)) { a.sendSystem(Unwatch(a, self)) @@ -63,7 +76,6 @@ private[typed] trait DeathWatch[T] { watching -= a } } - a } /** @@ -72,14 +84,21 @@ private[typed] trait DeathWatch[T] { */ protected def watchedActorTerminated(actor: ARImpl, failure: Throwable): Boolean = { removeChild(actor) - if (watching.contains(actor)) { - maintainAddressTerminatedSubscription(actor) { - watching -= actor - } - if (maySend) { - val t = Terminated(actor)(failure) - next(behavior.management(ctx, t), t) - } + watching.get(actor) match { + case None ⇒ // We're apparently no longer watching this actor. + case Some(optionalMessage) ⇒ + maintainAddressTerminatedSubscription(actor) { + watching -= actor + } + if (maySend) { + optionalMessage match { + case None ⇒ + val t = Terminated(actor)(failure) + next(Behavior.interpretSignal(behavior, ctx, t), t) + case Some(msg) ⇒ + next(Behavior.interpretMessage(behavior, ctx, msg), msg) + } + } } if (isTerminating && terminatingMap.isEmpty) { finishTerminate() @@ -120,9 +139,9 @@ private[typed] trait DeathWatch[T] { if (watching.nonEmpty) { maintainAddressTerminatedSubscription() { try { - watching.foreach(watchee ⇒ watchee.sendSystem(Unwatch(watchee, self))) + watching.foreach { case (watchee, _) ⇒ watchee.sendSystem(Unwatch(watchee, self)) } } finally { - watching = Set.empty + watching = Map.empty } } } @@ -137,7 +156,7 @@ private[typed] trait DeathWatch[T] { if (system.settings.untyped.DebugLifecycle) publish(Debug(self.path.toString, clazz(behavior), s"now watched by $watcher")) } } else if (!watcheeSelf && watcherSelf) { - watch[Nothing](watchee) + watch(watchee) } else { publish(Warning(self.path.toString, clazz(behavior), "BUG: illegal Watch(%s,%s) for %s".format(watchee, watcher, self))) } @@ -153,7 +172,7 @@ private[typed] trait DeathWatch[T] { if (system.settings.untyped.DebugLifecycle) publish(Debug(self.path.toString, clazz(behavior), s"no longer watched by $watcher")) } } else if (!watcheeSelf && watcherSelf) { - unwatch[Nothing](watchee) + unwatch(watchee) } else { publish(Warning(self.path.toString, clazz(behavior), "BUG: illegal Unwatch(%s,%s) for %s".format(watchee, watcher, self))) } @@ -165,7 +184,7 @@ private[typed] trait DeathWatch[T] { for (a ← watchedBy; if a.path.address == address) watchedBy -= a } - for (a ← watching; if a.path.address == address) { + for ((a, _) ← watching; if a.path.address == address) { self.sendSystem(DeathWatchNotification(a, null)) } } @@ -183,7 +202,7 @@ private[typed] trait DeathWatch[T] { } if (isNonLocal(change)) { - def hasNonLocalAddress: Boolean = ((watching exists isNonLocal) || (watchedBy exists isNonLocal)) + def hasNonLocalAddress: Boolean = ((watching.keysIterator exists isNonLocal) || (watchedBy exists isNonLocal)) val had = hasNonLocalAddress val result = block val has = hasNonLocalAddress diff --git a/akka-typed/src/main/scala/akka/typed/internal/EventStreamImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/EventStreamImpl.scala index ccee43cd3d..248b6f216e 100644 --- a/akka-typed/src/main/scala/akka/typed/internal/EventStreamImpl.scala +++ b/akka-typed/src/main/scala/akka/typed/internal/EventStreamImpl.scala @@ -27,7 +27,6 @@ import akka.typed.scaladsl.AskPattern */ private[typed] class EventStreamImpl(private val debug: Boolean)(implicit private val timeout: Timeout) extends EventStream { import e.Logging._ - import ScalaDSL._ import EventStreamImpl._ private val unsubscriberPromise = Promise[ActorRef[Command]] @@ -40,26 +39,32 @@ private[typed] class EventStreamImpl(private val debug: Boolean)(implicit privat def startUnsubscriber(sys: ActorSystem[Nothing]): Unit = unsubscriberPromise.completeWith(sys.systemActorOf(unsubscriberBehavior, "eventStreamUnsubscriber")) - private val unsubscriberBehavior = Deferred { () ⇒ - if (debug) publish(e.Logging.Debug(simpleName(getClass), getClass, s"registering unsubscriber with $this")) - Full[Command] { - case Msg(ctx, Register(actor)) ⇒ - if (debug) publish(e.Logging.Debug(simpleName(getClass), getClass, s"watching $actor in order to unsubscribe from EventStream when it terminates")) - ctx.watch[Nothing](actor) - Same + private val unsubscriberBehavior = { + // TODO avoid depending on dsl here? + import scaladsl.Actor + Actor.deferred[Command] { _ ⇒ + if (debug) publish(e.Logging.Debug(simpleName(getClass), getClass, s"registering unsubscriber with $this")) + Actor.immutable[Command] { (ctx, msg) ⇒ + msg match { + case Register(actor) ⇒ + if (debug) publish(e.Logging.Debug(simpleName(getClass), getClass, s"watching $actor in order to unsubscribe from EventStream when it terminates")) + ctx.watch(actor) + Actor.same - case Msg(ctx, UnregisterIfNoMoreSubscribedChannels(actor)) if hasSubscriptions(actor) ⇒ Same - // hasSubscriptions can be slow, but it's better for this actor to take the hit than the EventStream + case UnregisterIfNoMoreSubscribedChannels(actor) if hasSubscriptions(actor) ⇒ Actor.same + // hasSubscriptions can be slow, but it's better for this actor to take the hit than the EventStream - case Msg(ctx, UnregisterIfNoMoreSubscribedChannels(actor)) ⇒ - if (debug) publish(e.Logging.Debug(simpleName(getClass), getClass, s"unwatching $actor, since has no subscriptions")) - ctx.unwatch[Nothing](actor) - Same - - case Sig(ctx, Terminated(actor)) ⇒ - if (debug) publish(e.Logging.Debug(simpleName(getClass), getClass, s"unsubscribe $actor from $this, because it was terminated")) - unsubscribe(actor) - Same + case UnregisterIfNoMoreSubscribedChannels(actor) ⇒ + if (debug) publish(e.Logging.Debug(simpleName(getClass), getClass, s"unwatching $actor, since has no subscriptions")) + ctx.unwatch(actor) + Actor.same + } + } onSignal { + case (_, Terminated(actor)) ⇒ + if (debug) publish(e.Logging.Debug(simpleName(getClass), getClass, s"unsubscribe $actor from $this, because it was terminated")) + unsubscribe(actor) + Actor.same + } } } @@ -119,7 +124,10 @@ private[typed] class EventStreamImpl(private val debug: Boolean)(implicit privat * started. Its log level can be defined by configuration setting * akka.stdout-loglevel. */ - private[typed] class StandardOutLogger extends ActorRef[LogEvent](StandardOutLoggerPath) with ActorRefImpl[LogEvent] with StdOutLogger { + private[typed] class StandardOutLogger extends ActorRef[LogEvent] with ActorRefImpl[LogEvent] with StdOutLogger { + + override def path: a.ActorPath = StandardOutLoggerPath + override def tell(message: LogEvent): Unit = if (message == null) throw a.InvalidMessageException("Message must not be null") else print(message) @@ -142,9 +150,14 @@ private[typed] class EventStreamImpl(private val debug: Boolean)(implicit privat private val StandardOutLogger = new StandardOutLogger - private val UnhandledMessageForwarder = Static[a.UnhandledMessage] { - case a.UnhandledMessage(msg, sender, rcp) ⇒ - publish(Debug(rcp.path.toString, rcp.getClass, "unhandled message from " + sender + ": " + msg)) + private val UnhandledMessageForwarder = { + // TODO avoid depending on dsl here? + import scaladsl.Actor.{ same, immutable } + immutable[a.UnhandledMessage] { + case (_, a.UnhandledMessage(msg, sender, rcp)) ⇒ + publish(Debug(rcp.path.toString, rcp.getClass, "unhandled message from " + sender + ": " + msg)) + same + } } def startStdoutLogger(settings: Settings) { diff --git a/akka-typed/src/main/scala/akka/typed/internal/ExtensionsImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/ExtensionsImpl.scala new file mode 100644 index 0000000000..9688b2d222 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/ExtensionsImpl.scala @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.internal + +import java.util.concurrent.{ ConcurrentHashMap, CountDownLatch } + +import akka.annotation.InternalApi +import akka.typed.{ ActorSystem, Extension, ExtensionId, Extensions } + +import scala.annotation.tailrec +import scala.util.{ Failure, Success, Try } +import scala.collection.JavaConverters._ + +/** + * Actor system extensions registry + * + * INTERNAL API + */ +@InternalApi +trait ExtensionsImpl extends Extensions { self: ActorSystem[_] ⇒ + + private val extensions = new ConcurrentHashMap[ExtensionId[_], AnyRef] + + /** + * Hook for ActorSystem to load extensions on startup + */ + protected final def loadExtensions() { + /** + * @param throwOnLoadFail Throw exception when an extension fails to load (needed for backwards compatibility) + */ + def loadExtensions(key: String, throwOnLoadFail: Boolean): Unit = { + + settings.config.getStringList(key).asScala.foreach { extensionIdFQCN ⇒ + // it is either a Scala object or it is a Java class with a static singleton accessor + val idTry = dynamicAccess.getObjectFor[AnyRef](extensionIdFQCN) + .recoverWith { case _ ⇒ idFromJavaSingletonAccessor(extensionIdFQCN) } + + idTry match { + case Success(id: ExtensionId[_]) ⇒ registerExtension(id) + case Success(_) ⇒ + if (!throwOnLoadFail) log.error("[{}] is not an 'ExtensionId', skipping...", extensionIdFQCN) + else throw new RuntimeException(s"[$extensionIdFQCN] is not an 'ExtensionId'") + case Failure(problem) ⇒ + if (!throwOnLoadFail) log.error(problem, "While trying to load extension [{}], skipping...", extensionIdFQCN) + else throw new RuntimeException(s"While trying to load extension [$extensionIdFQCN]", problem) + } + } + } + + def idFromJavaSingletonAccessor(extensionIdFQCN: String): Try[ExtensionId[Extension]] = + dynamicAccess.getClassFor[ExtensionId[Extension]](extensionIdFQCN).flatMap[ExtensionId[Extension]] { clazz: Class[_] ⇒ + Try { + + val singletonAccessor = clazz.getDeclaredMethod("getInstance") + singletonAccessor.invoke(null).asInstanceOf[ExtensionId[Extension]] + } + } + + // eager initialization of CoordinatedShutdown + // TODO coordinated shutdown for akka typed + // CoordinatedShutdown(self) + + loadExtensions("akka.typed.library-extensions", throwOnLoadFail = true) + loadExtensions("akka.typed.extensions", throwOnLoadFail = false) + } + + final override def hasExtension(ext: ExtensionId[_ <: Extension]): Boolean = findExtension(ext) != null + + final override def extension[T <: Extension](ext: ExtensionId[T]): T = findExtension(ext) match { + case null ⇒ throw new IllegalArgumentException("Trying to get non-registered extension [" + ext + "]") + case some ⇒ some.asInstanceOf[T] + } + + final override def registerExtension[T <: Extension](ext: ExtensionId[T]): T = + findExtension(ext) match { + case null ⇒ createExtensionInstance(ext) + case existing ⇒ existing.asInstanceOf[T] + } + + private def createExtensionInstance[T <: Extension](ext: ExtensionId[T]): T = { + val inProcessOfRegistration = new CountDownLatch(1) + extensions.putIfAbsent(ext, inProcessOfRegistration) match { // Signal that registration is in process + case null ⇒ try { // Signal was successfully sent + // Create and initialize the extension + ext.createExtension(self) match { + case null ⇒ throw new IllegalStateException("Extension instance created as 'null' for extension [" + ext + "]") + case instance ⇒ + // Replace our in process signal with the initialized extension + extensions.replace(ext, inProcessOfRegistration, instance) + instance + } + } catch { + case t: Throwable ⇒ + //In case shit hits the fan, remove the inProcess signal and escalate to caller + extensions.replace(ext, inProcessOfRegistration, t) + throw t + } finally { + //Always notify listeners of the inProcess signal + inProcessOfRegistration.countDown() + } + case other ⇒ + //Someone else is in process of registering an extension for this Extension, retry + registerExtension(ext) + } + } + + /** + * Returns any extension registered to the specified Extension or returns null if not registered + */ + @tailrec + private def findExtension[T <: Extension](ext: ExtensionId[T]): T = extensions.get(ext) match { + case c: CountDownLatch ⇒ + //Registration in process, await completion and retry + c.await() + findExtension(ext) + case t: Throwable ⇒ throw t //Initialization failed, throw same again + case other ⇒ other.asInstanceOf[T] //could be a T or null, in which case we return the null as T + } +} \ No newline at end of file diff --git a/akka-typed/src/main/scala/akka/typed/internal/Restarter.scala b/akka-typed/src/main/scala/akka/typed/internal/Restarter.scala new file mode 100644 index 0000000000..f233ffbc5d --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/Restarter.scala @@ -0,0 +1,278 @@ +/** + * Copyright (C) 2016-2017 Lightbend Inc. + */ +package akka.typed +package internal + +import java.util.concurrent.ThreadLocalRandom + +import scala.annotation.tailrec +import scala.concurrent.duration.Deadline +import scala.concurrent.duration.FiniteDuration +import scala.reflect.ClassTag +import scala.util.control.Exception.Catcher +import scala.util.control.NonFatal + +import akka.actor.DeadLetterSuppression +import akka.annotation.InternalApi +import akka.event.Logging +import akka.typed.ActorContext +import akka.typed.Behavior +import akka.typed.Behavior.DeferredBehavior +import akka.typed.ExtensibleBehavior +import akka.typed.PreRestart +import akka.typed.Signal +import akka.typed.SupervisorStrategy._ +import akka.typed.scaladsl.Actor._ +import akka.util.OptionVal +import akka.typed.scaladsl.Actor + +/** + * INTERNAL API + */ +@InternalApi private[akka] object Restarter { + def apply[T, Thr <: Throwable: ClassTag](initialBehavior: Behavior[T], strategy: SupervisorStrategy): Behavior[T] = + Actor.deferred[T] { ctx ⇒ + val c = ctx.asInstanceOf[akka.typed.ActorContext[T]] + val startedBehavior = initialUndefer(c, initialBehavior) + strategy match { + case Restart(-1, _, loggingEnabled) ⇒ + new Restarter(initialBehavior, startedBehavior, loggingEnabled) + case r: Restart ⇒ + new LimitedRestarter(initialBehavior, startedBehavior, r, retries = 0, deadline = OptionVal.None) + case Resume(loggingEnabled) ⇒ new Resumer(startedBehavior, loggingEnabled) + case b: Backoff ⇒ + val backoffRestarter = + new BackoffRestarter( + initialBehavior.asInstanceOf[Behavior[Any]], + startedBehavior.asInstanceOf[Behavior[Any]], + b, restartCount = 0, blackhole = false) + backoffRestarter.asInstanceOf[Behavior[T]] + } + } + + def initialUndefer[T](ctx: ActorContext[T], initialBehavior: Behavior[T]): Behavior[T] = + Behavior.validateAsInitial(Behavior.undefer(initialBehavior, ctx)) +} + +/** + * INTERNAL API + */ +@InternalApi private[akka] abstract class Supervisor[T, Thr <: Throwable: ClassTag] extends ExtensibleBehavior[T] { + + protected def loggingEnabled: Boolean + + /** + * Current behavior + */ + protected def behavior: Behavior[T] + + /** + * Wrap next behavior in a concrete restarter again. + */ + protected def wrap(nextBehavior: Behavior[T], afterException: Boolean): Supervisor[T, Thr] + + protected def handleException(ctx: ActorContext[T], startedBehavior: Behavior[T]): Catcher[Supervisor[T, Thr]] + + protected def restart(ctx: ActorContext[T], initialBehavior: Behavior[T], startedBehavior: Behavior[T]): Supervisor[T, Thr] = { + try Behavior.interpretSignal(startedBehavior, ctx, PreRestart) catch { + case NonFatal(ex) ⇒ publish(ctx, Logging.Error(ex, ctx.asScala.self.path.toString, behavior.getClass, + "failure during PreRestart")) + } + // no need to canonicalize, it's done in the calling methods + wrap(Restarter.initialUndefer(ctx, initialBehavior), afterException = true) + } + + @tailrec + protected final def canonical(b: Behavior[T], ctx: ActorContext[T], afterException: Boolean): Behavior[T] = + if (Behavior.isUnhandled(b)) Behavior.unhandled + else if ((b eq Behavior.SameBehavior) || (b eq behavior)) Behavior.same + else if (!Behavior.isAlive(b)) b + else { + b match { + case d: DeferredBehavior[T] ⇒ canonical(Behavior.undefer(d, ctx), ctx, afterException) + case b ⇒ wrap(b, afterException) + } + } + + override def receiveSignal(ctx: ActorContext[T], signal: Signal): Behavior[T] = { + try { + val b = Behavior.interpretSignal(behavior, ctx, signal) + canonical(b, ctx, afterException = false) + } catch handleException(ctx, behavior) + } + + override def receiveMessage(ctx: ActorContext[T], msg: T): Behavior[T] = { + try { + val b = Behavior.interpretMessage(behavior, ctx, msg) + canonical(b, ctx, afterException = false) + } catch handleException(ctx, behavior) + } + + protected def log(ctx: ActorContext[T], ex: Thr): Unit = { + if (loggingEnabled) + publish(ctx, Logging.Error(ex, ctx.asScala.self.toString, behavior.getClass, ex.getMessage)) + } + + protected final def publish(ctx: ActorContext[T], e: Logging.LogEvent): Unit = + try ctx.asScala.system.eventStream.publish(e) catch { case NonFatal(_) ⇒ } +} + +/** + * INTERNAL API + */ +@InternalApi private[akka] final class Resumer[T, Thr <: Throwable: ClassTag]( + override val behavior: Behavior[T], override val loggingEnabled: Boolean) extends Supervisor[T, Thr] { + + override def handleException(ctx: ActorContext[T], startedBehavior: Behavior[T]): Catcher[Supervisor[T, Thr]] = { + case NonFatal(ex: Thr) ⇒ + log(ctx, ex) + wrap(startedBehavior, afterException = true) + } + + override protected def wrap(nextBehavior: Behavior[T], afterException: Boolean): Supervisor[T, Thr] = + new Resumer[T, Thr](nextBehavior, loggingEnabled) + +} + +/** + * INTERNAL API + */ +@InternalApi private[akka] final class Restarter[T, Thr <: Throwable: ClassTag]( + initialBehavior: Behavior[T], override val behavior: Behavior[T], + override val loggingEnabled: Boolean) extends Supervisor[T, Thr] { + + override def handleException(ctx: ActorContext[T], startedBehavior: Behavior[T]): Catcher[Supervisor[T, Thr]] = { + case NonFatal(ex: Thr) ⇒ + log(ctx, ex) + restart(ctx, initialBehavior, startedBehavior) + } + + override protected def wrap(nextBehavior: Behavior[T], afterException: Boolean): Supervisor[T, Thr] = + new Restarter[T, Thr](initialBehavior, nextBehavior, loggingEnabled) +} + +/** + * INTERNAL API + */ +@InternalApi private[akka] final class LimitedRestarter[T, Thr <: Throwable: ClassTag]( + initialBehavior: Behavior[T], override val behavior: Behavior[T], + strategy: Restart, retries: Int, deadline: OptionVal[Deadline]) extends Supervisor[T, Thr] { + + override def loggingEnabled: Boolean = strategy.loggingEnabled + + private def deadlineHasTimeLeft: Boolean = deadline match { + case OptionVal.None ⇒ true + case OptionVal.Some(d) ⇒ d.hasTimeLeft + } + + override def handleException(ctx: ActorContext[T], startedBehavior: Behavior[T]): Catcher[Supervisor[T, Thr]] = { + case NonFatal(ex: Thr) ⇒ + log(ctx, ex) + if (deadlineHasTimeLeft && retries >= strategy.maxNrOfRetries) + throw ex + else + restart(ctx, initialBehavior, startedBehavior) + } + + override protected def wrap(nextBehavior: Behavior[T], afterException: Boolean): Supervisor[T, Thr] = { + if (afterException) { + val timeLeft = deadlineHasTimeLeft + val newRetries = if (timeLeft) retries + 1 else 1 + val newDeadline = if (deadline.isDefined && timeLeft) deadline else OptionVal.Some(Deadline.now + strategy.withinTimeRange) + new LimitedRestarter[T, Thr](initialBehavior, nextBehavior, strategy, newRetries, newDeadline) + } else + new LimitedRestarter[T, Thr](initialBehavior, nextBehavior, strategy, retries, deadline) + } +} + +/** + * INTERNAL API + */ +@InternalApi private[akka] object BackoffRestarter { + /** + * Calculates an exponential back off delay. + */ + def calculateDelay( + restartCount: Int, + minBackoff: FiniteDuration, + maxBackoff: FiniteDuration, + randomFactor: Double): FiniteDuration = { + val rnd = 1.0 + ThreadLocalRandom.current().nextDouble() * randomFactor + if (restartCount >= 30) // Duration overflow protection (> 100 years) + maxBackoff + else + maxBackoff.min(minBackoff * math.pow(2, restartCount)) * rnd match { + case f: FiniteDuration ⇒ f + case _ ⇒ maxBackoff + } + } + + case object ScheduledRestart + final case class ResetRestartCount(current: Int) extends DeadLetterSuppression +} + +/** + * INTERNAL API + */ +@InternalApi private[akka] final class BackoffRestarter[T, Thr <: Throwable: ClassTag]( + initialBehavior: Behavior[Any], override val behavior: Behavior[Any], + strategy: Backoff, restartCount: Int, blackhole: Boolean) extends Supervisor[Any, Thr] { + + // TODO using Any here because the scheduled messages can't be of type T. + + import BackoffRestarter._ + + override def loggingEnabled: Boolean = strategy.loggingEnabled + + override def receiveSignal(ctx: ActorContext[Any], signal: Signal): Behavior[Any] = { + if (blackhole) { + ctx.asScala.system.eventStream.publish(Dropped(signal, ctx.asScala.self)) + Behavior.same + } else + super.receiveSignal(ctx, signal) + } + + override def receiveMessage(ctx: ActorContext[Any], msg: Any): Behavior[Any] = { + // intercept the scheduled messages and drop incoming messages if we are in backoff mode + msg match { + case ScheduledRestart ⇒ + // actual restart after scheduled backoff delay + val restartedBehavior = Restarter.initialUndefer(ctx, initialBehavior) + ctx.asScala.schedule(strategy.resetBackoffAfter, ctx.asScala.self, ResetRestartCount(restartCount)) + new BackoffRestarter[T, Thr](initialBehavior, restartedBehavior, strategy, restartCount, blackhole = false) + case ResetRestartCount(current) ⇒ + if (current == restartCount) + new BackoffRestarter[T, Thr](initialBehavior, behavior, strategy, restartCount = 0, blackhole) + else + Behavior.same + case _ ⇒ + if (blackhole) { + ctx.asScala.system.eventStream.publish(Dropped(msg, ctx.asScala.self)) + Behavior.same + } else + super.receiveMessage(ctx, msg) + } + } + + override def handleException(ctx: ActorContext[Any], startedBehavior: Behavior[Any]): Catcher[Supervisor[Any, Thr]] = { + case NonFatal(ex: Thr) ⇒ + log(ctx, ex) + // actual restart happens after the scheduled backoff delay + try Behavior.interpretSignal(behavior, ctx, PreRestart) catch { + case NonFatal(ex2) ⇒ publish(ctx, Logging.Error(ex2, ctx.asScala.self.path.toString, behavior.getClass, + "failure during PreRestart")) + } + val restartDelay = calculateDelay(restartCount, strategy.minBackoff, strategy.maxBackoff, strategy.randomFactor) + ctx.asScala.schedule(restartDelay, ctx.asScala.self, ScheduledRestart) + new BackoffRestarter[T, Thr](initialBehavior, startedBehavior, strategy, restartCount + 1, blackhole = true) + } + + override protected def wrap(nextBehavior: Behavior[Any], afterException: Boolean): Supervisor[Any, Thr] = { + if (afterException) + throw new IllegalStateException("wrap not expected afterException in BackoffRestarter") + else + new BackoffRestarter[T, Thr](initialBehavior, nextBehavior, strategy, restartCount, blackhole) + } +} + diff --git a/akka-typed/src/main/scala/akka/typed/internal/SupervisionMechanics.scala b/akka-typed/src/main/scala/akka/typed/internal/SupervisionMechanics.scala index 8706565e63..fb9d72df37 100644 --- a/akka-typed/src/main/scala/akka/typed/internal/SupervisionMechanics.scala +++ b/akka-typed/src/main/scala/akka/typed/internal/SupervisionMechanics.scala @@ -4,10 +4,11 @@ package akka.typed package internal -import scala.annotation.{ tailrec, switch } import scala.util.control.NonFatal -import scala.util.control.Exception.Catcher import akka.event.Logging +import akka.typed.Behavior.{ DeferredBehavior, undefer, validateAsInitial } +import akka.typed.Behavior.StoppedBehavior +import akka.util.OptionVal /** * INTERNAL API @@ -69,8 +70,8 @@ private[typed] trait SupervisionMechanics[T] { behavior = initialBehavior if (system.settings.untyped.DebugLifecycle) publish(Logging.Debug(self.path.toString, clazz(behavior), "started")) - if (Behavior.isAlive(behavior)) next(behavior.management(ctx, PreStart), PreStart) - else self.sendSystem(Terminate()) + behavior = validateAsInitial(undefer(behavior, ctx)) + if (!Behavior.isAlive(behavior)) self.sendSystem(Terminate()) true } @@ -88,9 +89,19 @@ private[typed] trait SupervisionMechanics[T] { val a = behavior /* * The following order is crucial for things to work properly. Only change this if you're very confident and lucky. + * + * */ - try if (a ne null) a.management(ctx, PostStop) - catch { case NonFatal(ex) ⇒ publish(Logging.Error(ex, self.path.toString, clazz(a), "failure during PostStop")) } + try a match { + case null ⇒ // skip PostStop + case _: DeferredBehavior[_] ⇒ + // Do not undefer a DeferredBehavior as that may cause creation side-effects, which we do not want on termination. + case s: StoppedBehavior[_] ⇒ s.postStop match { + case OptionVal.Some(postStop) ⇒ Behavior.interpretSignal(postStop, ctx, PostStop) + case OptionVal.None ⇒ // no postStop behavior defined + } + case _ ⇒ Behavior.interpretSignal(a, ctx, PostStop) + } catch { case NonFatal(ex) ⇒ publish(Logging.Error(ex, self.path.toString, clazz(a), "failure during PostStop")) } finally try tellWatchersWeDied() finally try parent.sendSystem(DeathWatchNotification(self, failed)) finally { diff --git a/akka-typed/src/main/scala/akka/typed/internal/TimerSchedulerImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/TimerSchedulerImpl.scala new file mode 100644 index 0000000000..a123f5acfd --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/TimerSchedulerImpl.scala @@ -0,0 +1,147 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed +package internal + +import scala.concurrent.duration.FiniteDuration + +import akka.actor.Cancellable +import akka.annotation.ApiMayChange +import akka.annotation.DoNotInherit +import akka.annotation.InternalApi +import akka.dispatch.ExecutionContexts +import akka.typed.ActorRef +import akka.typed.ActorRef.ActorRefOps +import akka.typed.javadsl +import akka.typed.scaladsl +import akka.typed.scaladsl.ActorContext +import scala.reflect.ClassTag + +/** + * INTERNAL API + */ +@InternalApi private[akka] object TimerSchedulerImpl { + final case class Timer[T](key: Any, msg: T, repeat: Boolean, generation: Int, task: Cancellable) + final case class TimerMsg(key: Any, generation: Int, owner: AnyRef) + + def withTimers[T](factory: TimerSchedulerImpl[T] ⇒ Behavior[T]): Behavior[T] = { + scaladsl.Actor.deferred[T] { ctx ⇒ + val timerScheduler = new TimerSchedulerImpl[T](ctx) + val behavior = factory(timerScheduler) + timerScheduler.intercept(behavior) + } + } +} + +/** + * INTERNAL API + */ +@InternalApi private[akka] class TimerSchedulerImpl[T](ctx: ActorContext[T]) + extends scaladsl.TimerScheduler[T] with javadsl.TimerScheduler[T] { + import TimerSchedulerImpl._ + + // FIXME change to a class specific logger, see issue #21219 + private val log = ctx.system.log + private var timers: Map[Any, Timer[T]] = Map.empty + private val timerGen = Iterator from 1 + + override def startPeriodicTimer(key: Any, msg: T, interval: FiniteDuration): Unit = + startTimer(key, msg, interval, repeat = true) + + override def startSingleTimer(key: Any, msg: T, timeout: FiniteDuration): Unit = + startTimer(key, msg, timeout, repeat = false) + + private def startTimer(key: Any, msg: T, timeout: FiniteDuration, repeat: Boolean): Unit = { + timers.get(key) match { + case Some(t) ⇒ cancelTimer(t) + case None ⇒ + } + val nextGen = timerGen.next() + + val timerMsg = TimerMsg(key, nextGen, this) + val task = + if (repeat) + ctx.system.scheduler.schedule(timeout, timeout) { + ctx.self.upcast ! timerMsg + }(ExecutionContexts.sameThreadExecutionContext) + else + ctx.system.scheduler.scheduleOnce(timeout) { + ctx.self.upcast ! timerMsg + }(ExecutionContexts.sameThreadExecutionContext) + + val nextTimer = Timer(key, msg, repeat, nextGen, task) + log.debug("Start timer [{}] with generation [{}]", key, nextGen) + timers = timers.updated(key, nextTimer) + } + + override def isTimerActive(key: Any): Boolean = + timers.contains(key) + + override def cancel(key: Any): Unit = { + timers.get(key) match { + case None ⇒ // already removed/canceled + case Some(t) ⇒ cancelTimer(t) + } + } + + private def cancelTimer(timer: Timer[T]): Unit = { + log.debug("Cancel timer [{}] with generation [{}]", timer.key, timer.generation) + timer.task.cancel() + timers -= timer.key + } + + override def cancelAll(): Unit = { + log.debug("Cancel all timers") + timers.valuesIterator.foreach { timer ⇒ + timer.task.cancel() + } + timers = Map.empty + } + + private def interceptTimerMsg(ctx: ActorContext[TimerMsg], timerMsg: TimerMsg): T = { + timers.get(timerMsg.key) match { + case None ⇒ + // it was from canceled timer that was already enqueued in mailbox + log.debug("Received timer [{}] that has been removed, discarding", timerMsg.key) + null.asInstanceOf[T] // message should be ignored + case Some(t) ⇒ + if (timerMsg.owner ne this) { + // after restart, it was from an old instance that was enqueued in mailbox before canceled + log.debug("Received timer [{}] from old restarted instance, discarding", timerMsg.key) + null.asInstanceOf[T] // message should be ignored + } else if (timerMsg.generation == t.generation) { + // valid timer + log.debug("Received timer [{}]", timerMsg.key) + if (!t.repeat) + timers -= t.key + t.msg + } else { + // it was from an old timer that was enqueued in mailbox before canceled + log.debug( + "Received timer [{}] from from old generation [{}], expected generation [{}], discarding", + timerMsg.key, timerMsg.generation, t.generation) + null.asInstanceOf[T] // message should be ignored + } + } + } + + def intercept(behavior: Behavior[T]): Behavior[T] = { + // The scheduled TimerMsg is intercepted to guard against old messages enqueued + // in mailbox before timer was canceled. + // Intercept some signals to cancel timers when when restarting and stopping. + BehaviorImpl.intercept[T, TimerMsg]( + beforeMessage = interceptTimerMsg, + beforeSignal = (ctx, sig) ⇒ { + sig match { + case PreRestart | PostStop ⇒ cancelAll() + case _ ⇒ // unhandled + } + true + }, + afterMessage = (ctx, msg, b) ⇒ b, // TODO optimize by using more ConstantFun + afterSignal = (ctx, sig, b) ⇒ b, + behavior)(ClassTag(classOf[TimerSchedulerImpl.TimerMsg])) + } + +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorAdapter.scala b/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorAdapter.scala new file mode 100644 index 0000000000..9056334ade --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorAdapter.scala @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2016-2017 Lightbend Inc. + */ +package akka.typed +package internal +package adapter + +import akka.{ actor ⇒ a } +import akka.annotation.InternalApi +import akka.util.OptionVal + +/** + * INTERNAL API + */ +@InternalApi private[typed] class ActorAdapter[T](_initialBehavior: Behavior[T]) extends a.Actor { + import Behavior._ + import ActorRefAdapter.toUntyped + + var behavior: Behavior[T] = _initialBehavior + + if (!isAlive(behavior)) context.stop(self) + + val ctx = new ActorContextAdapter[T](context) + + var failures: Map[a.ActorRef, Throwable] = Map.empty + + def receive = { + case a.Terminated(ref) ⇒ + val msg = + if (failures contains ref) { + val ex = failures(ref) + failures -= ref + Terminated(ActorRefAdapter(ref))(ex) + } else Terminated(ActorRefAdapter(ref))(null) + next(Behavior.interpretSignal(behavior, ctx, msg), msg) + case a.ReceiveTimeout ⇒ + next(Behavior.interpretMessage(behavior, ctx, ctx.receiveTimeoutMsg), ctx.receiveTimeoutMsg) + case msg: T @unchecked ⇒ + next(Behavior.interpretMessage(behavior, ctx, msg), msg) + } + + private def next(b: Behavior[T], msg: Any): Unit = { + if (Behavior.isUnhandled(b)) unhandled(msg) + else { + b match { + case s: StoppedBehavior[T] ⇒ + // use StoppedBehavior with previous behavior or an explicitly given `postStop` behavior + // until Terminate is received, i.e until postStop is invoked, and there PostStop + // will be signaled to the previous/postStop behavior + s.postStop match { + case OptionVal.None ⇒ + // use previous as the postStop behavior + behavior = new Behavior.StoppedBehavior(OptionVal.Some(behavior)) + case OptionVal.Some(postStop) ⇒ + // use the given postStop behavior, but canonicalize it + behavior = new Behavior.StoppedBehavior(OptionVal.Some(Behavior.canonicalize(postStop, behavior, ctx))) + } + context.stop(self) + case _ ⇒ + behavior = Behavior.canonicalize(b, behavior, ctx) + } + } + } + + override def unhandled(msg: Any): Unit = msg match { + case Terminated(ref) ⇒ throw a.DeathPactException(toUntyped(ref)) + case msg: Signal ⇒ // that's ok + case other ⇒ super.unhandled(other) + } + + override val supervisorStrategy = a.OneForOneStrategy() { + case ex ⇒ + val ref = sender() + if (context.asInstanceOf[a.ActorCell].isWatching(ref)) failures = failures.updated(ref, ex) + a.SupervisorStrategy.Stop + } + + override def preStart(): Unit = { + behavior = validateAsInitial(undefer(behavior, ctx)) + if (!isAlive(behavior)) context.stop(self) + } + + override def preRestart(reason: Throwable, message: Option[Any]): Unit = { + Behavior.interpretSignal(behavior, ctx, PreRestart) + behavior = Behavior.stopped + } + + override def postRestart(reason: Throwable): Unit = { + behavior = validateAsInitial(undefer(behavior, ctx)) + if (!isAlive(behavior)) context.stop(self) + } + + override def postStop(): Unit = { + behavior match { + case null ⇒ // skip PostStop + case _: DeferredBehavior[_] ⇒ + // Do not undefer a DeferredBehavior as that may cause creation side-effects, which we do not want on termination. + case s: StoppedBehavior[_] ⇒ s.postStop match { + case OptionVal.Some(postStop) ⇒ Behavior.interpretSignal(postStop, ctx, PostStop) + case OptionVal.None ⇒ // no postStop behavior defined + } + case b ⇒ Behavior.interpretSignal(b, ctx, PostStop) + } + behavior = Behavior.stopped + } +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorContextAdapter.scala b/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorContextAdapter.scala new file mode 100644 index 0000000000..4fb98a9734 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorContextAdapter.scala @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2016-2017 Lightbend Inc. + */ +package akka.typed +package internal +package adapter + +import akka.{ actor ⇒ a } +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContextExecutor +import akka.annotation.InternalApi + +/** + * INTERNAL API. Wrapping an [[akka.actor.ActorContext]] as an [[ActorContext]]. + */ +@InternalApi private[typed] class ActorContextAdapter[T](val untyped: a.ActorContext) extends ActorContextImpl[T] { + + import ActorRefAdapter.toUntyped + + override def self = ActorRefAdapter(untyped.self) + override val system = ActorSystemAdapter(untyped.system) + override def mailboxCapacity = 1 << 29 // FIXME + override def children = untyped.children.map(ActorRefAdapter(_)) + override def child(name: String) = untyped.child(name).map(ActorRefAdapter(_)) + override def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty) = + ActorContextAdapter.spawnAnonymous(untyped, behavior, props) + override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty) = + ActorContextAdapter.spawn(untyped, behavior, name, props) + override def stop[U](child: ActorRef[U]) = + toUntyped(child) match { + case f: akka.actor.FunctionRef ⇒ + val cell = untyped.asInstanceOf[akka.actor.ActorCell] + cell.removeFunctionRef(f) + case c ⇒ + untyped.child(child.path.name) match { + case Some(`c`) ⇒ + untyped.stop(c) + true + case _ ⇒ + false // none of our business + } + } + override def watch[U](other: ActorRef[U]) = { untyped.watch(toUntyped(other)) } + override def watchWith[U](other: ActorRef[U], msg: T) = { untyped.watchWith(toUntyped(other), msg) } + override def unwatch[U](other: ActorRef[U]) = { untyped.unwatch(toUntyped(other)) } + var receiveTimeoutMsg: T = null.asInstanceOf[T] + override def setReceiveTimeout(d: FiniteDuration, msg: T) = { + receiveTimeoutMsg = msg + untyped.setReceiveTimeout(d) + } + override def cancelReceiveTimeout(): Unit = { + receiveTimeoutMsg = null.asInstanceOf[T] + untyped.setReceiveTimeout(Duration.Undefined) + } + override def executionContext: ExecutionContextExecutor = untyped.dispatcher + override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): a.Cancellable = { + import untyped.dispatcher + untyped.system.scheduler.scheduleOnce(delay, toUntyped(target), msg) + } + override private[akka] def internalSpawnAdapter[U](f: U ⇒ T, _name: String): ActorRef[U] = { + val cell = untyped.asInstanceOf[akka.actor.ActorCell] + val ref = cell.addFunctionRef((_, msg) ⇒ untyped.self ! f(msg.asInstanceOf[U]), _name) + ActorRefAdapter[U](ref) + } + +} + +/** + * INTERNAL API + */ +@InternalApi private[typed] object ActorContextAdapter { + + private def toUntypedImp[U](ctx: ActorContext[_]): a.ActorContext = + ctx match { + case adapter: ActorContextAdapter[_] ⇒ adapter.untyped + case _ ⇒ + throw new UnsupportedOperationException("only adapted untyped ActorContext permissible " + + s"($ctx of class ${ctx.getClass.getName})") + } + + def toUntyped2[U](ctx: ActorContext[_]): a.ActorContext = toUntypedImp(ctx) + + def toUntyped[U](ctx: scaladsl.ActorContext[_]): a.ActorContext = + ctx match { + case c: ActorContext[_] ⇒ toUntypedImp(c) + case _ ⇒ + throw new UnsupportedOperationException("unknown ActorContext type " + + s"($ctx of class ${ctx.getClass.getName})") + } + + def toUntyped[U](ctx: javadsl.ActorContext[_]): a.ActorContext = + ctx match { + case c: ActorContext[_] ⇒ toUntypedImp(c) + case _ ⇒ + throw new UnsupportedOperationException("unknown ActorContext type " + + s"($ctx of class ${ctx.getClass.getName})") + } + + def spawnAnonymous[T](ctx: akka.actor.ActorContext, behavior: Behavior[T], props: Props): ActorRef[T] = { + Behavior.validateAsInitial(behavior) + ActorRefAdapter(ctx.actorOf(PropsAdapter(() ⇒ behavior, props))) + } + + def spawn[T](ctx: akka.actor.ActorContext, behavior: Behavior[T], name: String, props: Props): ActorRef[T] = { + Behavior.validateAsInitial(behavior) + ActorRefAdapter(ctx.actorOf(PropsAdapter(() ⇒ behavior, props), name)) + } +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorRefAdapter.scala b/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorRefAdapter.scala new file mode 100644 index 0000000000..fd151b05de --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorRefAdapter.scala @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2016-2017 Lightbend Inc. + */ +package akka.typed +package internal +package adapter + +import akka.{ actor ⇒ a } +import akka.annotation.InternalApi +import akka.dispatch.sysmsg + +/** + * INTERNAL API + */ +@InternalApi private[typed] class ActorRefAdapter[-T](val untyped: a.InternalActorRef) + extends ActorRef[T] with internal.ActorRefImpl[T] { + + override def path: a.ActorPath = untyped.path + override def tell(msg: T): Unit = untyped ! msg + override def isLocal: Boolean = untyped.isLocal + override def sendSystem(signal: internal.SystemMessage): Unit = + ActorRefAdapter.sendSystemMessage(untyped, signal) +} + +private[typed] object ActorRefAdapter { + def apply[T](untyped: a.ActorRef): ActorRef[T] = new ActorRefAdapter(untyped.asInstanceOf[a.InternalActorRef]) + + def toUntyped[U](ref: ActorRef[U]): akka.actor.InternalActorRef = + ref match { + case adapter: ActorRefAdapter[_] ⇒ adapter.untyped + case _ ⇒ + throw new UnsupportedOperationException("only adapted untyped ActorRefs permissible " + + s"($ref of class ${ref.getClass.getName})") + } + + def sendSystemMessage(untyped: akka.actor.InternalActorRef, signal: internal.SystemMessage): Unit = + signal match { + case internal.Create() ⇒ throw new IllegalStateException("WAT? No, seriously.") + case internal.Terminate() ⇒ untyped.stop() + case internal.Watch(watchee, watcher) ⇒ untyped.sendSystemMessage( + sysmsg.Watch( + toUntyped(watchee), + toUntyped(watcher))) + case internal.Unwatch(watchee, watcher) ⇒ untyped.sendSystemMessage(sysmsg.Unwatch(toUntyped(watchee), toUntyped(watcher))) + case internal.DeathWatchNotification(ref, cause) ⇒ untyped.sendSystemMessage(sysmsg.DeathWatchNotification(toUntyped(ref), true, false)) + case internal.NoMessage ⇒ // just to suppress the warning + } +} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/ActorSystemAdapter.scala b/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorSystemAdapter.scala similarity index 58% rename from akka-typed/src/main/scala/akka/typed/adapter/ActorSystemAdapter.scala rename to akka-typed/src/main/scala/akka/typed/internal/adapter/ActorSystemAdapter.scala index 1fd87b0a03..0aa621f49d 100644 --- a/akka-typed/src/main/scala/akka/typed/adapter/ActorSystemAdapter.scala +++ b/akka-typed/src/main/scala/akka/typed/internal/adapter/ActorSystemAdapter.scala @@ -2,6 +2,7 @@ * Copyright (C) 2016-2017 Lightbend Inc. */ package akka.typed +package internal package adapter import akka.{ actor ⇒ a, dispatch ⇒ d } @@ -9,34 +10,41 @@ import akka.dispatch.sysmsg import scala.concurrent.ExecutionContextExecutor import akka.util.Timeout import scala.concurrent.Future +import akka.annotation.InternalApi +import scala.annotation.unchecked.uncheckedVariance /** - * Lightweight wrapper for presenting an untyped ActorSystem to a Behavior (via the context). + * INTERNAL API. Lightweight wrapper for presenting an untyped ActorSystem to a Behavior (via the context). * Therefore it does not have a lot of vals, only the whenTerminated Future is cached after * its transformation because redoing that every time will add extra objects that persist for * a longer time; in all other cases the wrapper will just be spawned for a single call in * most circumstances. */ -private[typed] class ActorSystemAdapter[-T](val untyped: a.ActorSystemImpl) - extends ActorRef[T](a.RootActorPath(a.Address("akka", untyped.name)) / "user") - with ActorSystem[T] with internal.ActorRefImpl[T] { +@InternalApi private[typed] class ActorSystemAdapter[-T](val untyped: a.ActorSystemImpl) + extends ActorSystem[T] with ActorRef[T] with internal.ActorRefImpl[T] with ExtensionsImpl { import ActorSystemAdapter._ + import ActorRefAdapter.sendSystemMessage // Members declared in akka.typed.ActorRef override def tell(msg: T): Unit = untyped.guardian ! msg override def isLocal: Boolean = true override def sendSystem(signal: internal.SystemMessage): Unit = sendSystemMessage(untyped.guardian, signal) + final override val path: a.ActorPath = a.RootActorPath(a.Address("akka", untyped.name)) / "user" + + override def toString: String = untyped.toString // Members declared in akka.typed.ActorSystem override def deadLetters[U]: ActorRef[U] = ActorRefAdapter(untyped.deadLetters) override def dispatchers: Dispatchers = new Dispatchers { override def lookup(selector: DispatcherSelector): ExecutionContextExecutor = selector match { - case DispatcherDefault(_) ⇒ untyped.dispatcher - case DispatcherFromConfig(str, _) ⇒ untyped.dispatchers.lookup(str) - case DispatcherFromExecutionContext(_, _) ⇒ throw new UnsupportedOperationException("cannot use DispatcherFromExecutionContext with ActorSystemAdapter") - case DispatcherFromExecutor(_, _) ⇒ throw new UnsupportedOperationException("cannot use DispatcherFromExecutor with ActorSystemAdapter") + case DispatcherDefault(_) ⇒ untyped.dispatcher + case DispatcherFromConfig(str, _) ⇒ untyped.dispatchers.lookup(str) + case DispatcherFromExecutionContext(_, _) ⇒ + throw new UnsupportedOperationException("Cannot use DispatcherFromExecutionContext with ActorSystemAdapter") + case DispatcherFromExecutor(_, _) ⇒ + throw new UnsupportedOperationException("Cannot use DispatcherFromExecutor with ActorSystemAdapter") } override def shutdown(): Unit = () // there was no shutdown in untyped Akka } @@ -64,9 +72,9 @@ private[typed] class ActorSystemAdapter[-T](val untyped: a.ActorSystemImpl) override lazy val whenTerminated: scala.concurrent.Future[akka.typed.Terminated] = untyped.whenTerminated.map(t ⇒ Terminated(ActorRefAdapter(t.actor))(null))(sameThreadExecutionContext) - def systemActorOf[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig)(implicit timeout: Timeout): Future[ActorRef[U]] = { - val ref = untyped.systemActorOf(PropsAdapter(behavior, deployment), name) - Future.successful(ref) + def systemActorOf[U](behavior: Behavior[U], name: String, props: Props)(implicit timeout: Timeout): Future[ActorRef[U]] = { + val ref = untyped.systemActorOf(PropsAdapter(() ⇒ behavior, props), name) + Future.successful(ActorRefAdapter(ref)) } } @@ -74,9 +82,24 @@ private[typed] class ActorSystemAdapter[-T](val untyped: a.ActorSystemImpl) private[typed] object ActorSystemAdapter { def apply(untyped: a.ActorSystem): ActorSystem[Nothing] = new ActorSystemAdapter(untyped.asInstanceOf[a.ActorSystemImpl]) - object ReceptionistExtension extends a.ExtensionKey[ReceptionistExtension] + def toUntyped[U](sys: ActorSystem[_]): a.ActorSystem = + sys match { + case adapter: ActorSystemAdapter[_] ⇒ adapter.untyped + case _ ⇒ throw new UnsupportedOperationException("only adapted untyped ActorSystem permissible " + + s"($sys of class ${sys.getClass.getName})") + } + + object ReceptionistExtension extends a.ExtensionId[ReceptionistExtension] with a.ExtensionIdProvider { + override def get(system: a.ActorSystem): ReceptionistExtension = super.get(system) + override def lookup = ReceptionistExtension + override def createExtension(system: a.ExtendedActorSystem): ReceptionistExtension = + new ReceptionistExtension(system) + } + class ReceptionistExtension(system: a.ExtendedActorSystem) extends a.Extension { val receptionist: ActorRef[patterns.Receptionist.Command] = - ActorRefAdapter(system.systemActorOf(PropsAdapter(patterns.Receptionist.behavior, EmptyDeploymentConfig), "receptionist")) + ActorRefAdapter(system.systemActorOf( + PropsAdapter(() ⇒ patterns.Receptionist.behavior, EmptyProps), + "receptionist")) } } diff --git a/akka-typed/src/main/scala/akka/typed/adapter/EventStreamAdapter.scala b/akka-typed/src/main/scala/akka/typed/internal/adapter/EventStreamAdapter.scala similarity index 64% rename from akka-typed/src/main/scala/akka/typed/adapter/EventStreamAdapter.scala rename to akka-typed/src/main/scala/akka/typed/internal/adapter/EventStreamAdapter.scala index f121f85119..91fb378b06 100644 --- a/akka-typed/src/main/scala/akka/typed/adapter/EventStreamAdapter.scala +++ b/akka-typed/src/main/scala/akka/typed/internal/adapter/EventStreamAdapter.scala @@ -2,11 +2,16 @@ * Copyright (C) 2016-2017 Lightbend Inc. */ package akka.typed +package internal package adapter import akka.{ event ⇒ e } +import akka.annotation.InternalApi -class EventStreamAdapter(untyped: e.EventStream) extends EventStream { +/** + * INTERNAL API + */ +@InternalApi private[typed] class EventStreamAdapter(untyped: e.EventStream) extends EventStream { def logLevel: e.Logging.LogLevel = untyped.logLevel def publish[T](event: T): Unit = untyped.publish(event.asInstanceOf[AnyRef]) @@ -16,19 +21,22 @@ class EventStreamAdapter(untyped: e.EventStream) extends EventStream { def subscribe[T](subscriber: ActorRef[T], to: Class[T]): Boolean = subscriber match { case adapter: ActorRefAdapter[_] ⇒ untyped.subscribe(adapter.untyped, to) - case _ ⇒ throw new UnsupportedOperationException("cannot subscribe native typed ActorRef") + case _ ⇒ + throw new UnsupportedOperationException("Cannot subscribe native typed ActorRef") } def unsubscribe[T](subscriber: ActorRef[T]): Unit = subscriber match { case adapter: ActorRefAdapter[_] ⇒ untyped.unsubscribe(adapter.untyped) - case _ ⇒ throw new UnsupportedOperationException("cannot unsubscribe native typed ActorRef") + case _ ⇒ + throw new UnsupportedOperationException("Cannot unsubscribe native typed ActorRef") } def unsubscribe[T](subscriber: ActorRef[T], from: Class[T]): Boolean = subscriber match { case adapter: ActorRefAdapter[_] ⇒ untyped.unsubscribe(adapter.untyped, from) - case _ ⇒ throw new UnsupportedOperationException("cannot unsubscribe native typed ActorRef") + case _ ⇒ + throw new UnsupportedOperationException("Cannot unsubscribe native typed ActorRef") } } diff --git a/akka-typed/src/main/scala/akka/typed/internal/adapter/PropsAdapter.scala b/akka-typed/src/main/scala/akka/typed/internal/adapter/PropsAdapter.scala new file mode 100644 index 0000000000..a3a4d60f9d --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/adapter/PropsAdapter.scala @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed +package internal +package adapter + +import akka.typed.Behavior +import akka.typed.EmptyProps +import akka.typed.Props +import akka.annotation.InternalApi + +/** + * INTERNAL API + */ +@InternalApi private[akka] object PropsAdapter { + def apply[T](behavior: () ⇒ Behavior[T], deploy: Props = Props.empty): akka.actor.Props = { + // FIXME use Props, e.g. dispatcher + akka.actor.Props(new ActorAdapter(behavior())) + } + +} diff --git a/akka-typed/src/main/scala/akka/typed/javadsl/Actor.scala b/akka-typed/src/main/scala/akka/typed/javadsl/Actor.scala new file mode 100644 index 0000000000..ba921ed9d6 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/javadsl/Actor.scala @@ -0,0 +1,280 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.javadsl + +import java.util.function.{ Function ⇒ JFunction } + +import scala.reflect.ClassTag + +import akka.util.OptionVal +import akka.japi.function.{ Function2 ⇒ JapiFunction2 } +import akka.japi.function.Procedure2 +import akka.japi.pf.PFBuilder + +import akka.typed.Behavior +import akka.typed.ExtensibleBehavior +import akka.typed.Signal +import akka.typed.ActorRef +import akka.typed.SupervisorStrategy +import akka.typed.scaladsl.{ ActorContext ⇒ SAC } + +import akka.typed.internal.BehaviorImpl +import akka.typed.internal.Restarter +import akka.typed.internal.TimerSchedulerImpl + +object Actor { + + private val _unitFunction = (_: SAC[Any], _: Any) ⇒ () + private def unitFunction[T] = _unitFunction.asInstanceOf[((SAC[T], Signal) ⇒ Unit)] + + /** + * `deferred` is a factory for a behavior. Creation of the behavior instance is deferred until + * the actor is started, as opposed to `Actor.immutable` that creates the behavior instance + * immediately before the actor is running. The `factory` function pass the `ActorContext` + * as parameter and that can for example be used for spawning child actors. + * + * `deferred` is typically used as the outer most behavior when spawning an actor, but it + * can also be returned as the next behavior when processing a message or signal. In that + * case it will be "undeferred" immediately after it is returned, i.e. next message will be + * processed by the undeferred behavior. + */ + def deferred[T](factory: akka.japi.function.Function[ActorContext[T], Behavior[T]]): Behavior[T] = + Behavior.DeferredBehavior(ctx ⇒ factory.apply(ctx.asJava)) + + /** + * Factory for creating a [[MutableBehavior]] that typically holds mutable state as + * instance variables in the concrete [[MutableBehavior]] implementation class. + * + * Creation of the behavior instance is deferred, i.e. it is created via the `factory` + * function. The reason for the deferred creation is to avoid sharing the same instance in + * multiple actors, and to create a new instance when the actor is restarted. + * + * @param producer + * behavior factory that takes the child actor’s context as argument + * @return the deferred behavior + */ + def mutable[T](factory: akka.japi.function.Function[ActorContext[T], MutableBehavior[T]]): Behavior[T] = + deferred(factory) + + /** + * Mutable behavior can be implemented by extending this class and implement the + * abstract method [[MutableBehavior#onMessage]] and optionally override + * [[MutableBehavior#onSignal]]. + * + * Instances of this behavior should be created via [[Actor#mutable]] and if + * the [[ActorContext]] is needed it can be passed as a constructor parameter + * from the factory function. + * + * @see [[Actor#mutable]] + */ + abstract class MutableBehavior[T] extends ExtensibleBehavior[T] { + private var _receive: OptionVal[Receive[T]] = OptionVal.None + private def receive: Receive[T] = _receive match { + case OptionVal.None ⇒ + val receive = createReceive + _receive = OptionVal.Some(receive) + receive + case OptionVal.Some(r) ⇒ r + } + + @throws(classOf[Exception]) + override final def receiveMessage(ctx: akka.typed.ActorContext[T], msg: T): Behavior[T] = + receive.receiveMessage(msg) + + @throws(classOf[Exception]) + override final def receiveSignal(ctx: akka.typed.ActorContext[T], msg: Signal): Behavior[T] = + receive.receiveSignal(msg) + + def createReceive: Receive[T] + + def receiveBuilder: ReceiveBuilder[T] = ReceiveBuilder.create + } + + /** + * Return this behavior from message processing in order to advise the + * system to reuse the previous behavior. This is provided in order to + * avoid the allocation overhead of recreating the current behavior where + * that is not necessary. + */ + def same[T]: Behavior[T] = Behavior.same + + /** + * Return this behavior from message processing in order to advise the + * system to reuse the previous behavior, including the hint that the + * message has not been handled. This hint may be used by composite + * behaviors that delegate (partial) handling to other behaviors. + */ + def unhandled[T]: Behavior[T] = Behavior.unhandled + + /** + * Return this behavior from message processing to signal that this actor + * shall terminate voluntarily. If this actor has created child actors then + * these will be stopped as part of the shutdown procedure. + * + * The PostStop signal that results from stopping this actor will be passed to the + * current behavior. All other messages and signals will effectively be + * ignored. + */ + def stopped[T]: Behavior[T] = Behavior.stopped + + /** + * Return this behavior from message processing to signal that this actor + * shall terminate voluntarily. If this actor has created child actors then + * these will be stopped as part of the shutdown procedure. + * + * The PostStop signal that results from stopping this actor will be passed to the + * given `postStop` behavior. All other messages and signals will effectively be + * ignored. + */ + def stopped[T](postStop: Behavior[T]): Behavior[T] = Behavior.stopped(postStop) + + /** + * A behavior that treats every incoming message as unhandled. + */ + def empty[T]: Behavior[T] = Behavior.empty + + /** + * A behavior that ignores every incoming message and returns “same”. + */ + def ignore[T]: Behavior[T] = Behavior.ignore + + /** + * Construct an actor behavior that can react to incoming messages but not to + * lifecycle signals. After spawning this actor from another actor (or as the + * guardian of an [[akka.typed.ActorSystem]]) it will be executed within an + * [[ActorContext]] that allows access to the system, spawning and watching + * other actors, etc. + * + * This constructor is called immutable because the behavior instance doesn't + * have or close over any mutable state. Processing the next message + * results in a new behavior that can potentially be different from this one. + * State is updated by returning a new behavior that holds the new immutable + * state. + */ + def immutable[T](onMessage: JapiFunction2[ActorContext[T], T, Behavior[T]]): Behavior[T] = + new BehaviorImpl.ImmutableBehavior((ctx, msg) ⇒ onMessage.apply(ctx.asJava, msg)) + + /** + * Construct an actor behavior that can react to both incoming messages and + * lifecycle signals. After spawning this actor from another actor (or as the + * guardian of an [[akka.typed.ActorSystem]]) it will be executed within an + * [[ActorContext]] that allows access to the system, spawning and watching + * other actors, etc. + * + * This constructor is called immutable because the behavior instance doesn't + * have or close over any mutable state. Processing the next message + * results in a new behavior that can potentially be different from this one. + * State is updated by returning a new behavior that holds the new immutable + * state. + */ + def immutable[T]( + onMessage: JapiFunction2[ActorContext[T], T, Behavior[T]], + onSignal: JapiFunction2[ActorContext[T], Signal, Behavior[T]]): Behavior[T] = { + new BehaviorImpl.ImmutableBehavior( + (ctx, msg) ⇒ onMessage.apply(ctx.asJava, msg), + { case (ctx, sig) ⇒ onSignal.apply(ctx.asJava, sig) }) + } + + /** + * Constructs an actor behavior builder that can build a behavior that can react to both + * incoming messages and lifecycle signals. + * + * This constructor is called immutable because the behavior instance doesn't + * have or close over any mutable state. Processing the next message + * results in a new behavior that can potentially be different from this one. + * State is updated by returning a new behavior that holds the new immutable + * state. If no change is desired, use {@link #same}. + * + * @param type the supertype of all messages accepted by this behavior + * @return the behavior builder + */ + def immutable[T](`type`: Class[T]): BehaviorBuilder[T] = BehaviorBuilder.create[T] + + /** + * This type of Behavior wraps another Behavior while allowing you to perform + * some action upon each received message or signal. It is most commonly used + * for logging or tracing what a certain Actor does. + */ + def tap[T]( + onMessage: Procedure2[ActorContext[T], T], + onSignal: Procedure2[ActorContext[T], Signal], + behavior: Behavior[T]): Behavior[T] = { + BehaviorImpl.tap( + (ctx, msg) ⇒ onMessage.apply(ctx.asJava, msg), + (ctx, sig) ⇒ onSignal.apply(ctx.asJava, sig), + behavior) + } + + /** + * Behavior decorator that copies all received message to the designated + * monitor [[akka.typed.ActorRef]] before invoking the wrapped behavior. The + * wrapped behavior can evolve (i.e. return different behavior) without needing to be + * wrapped in a `monitor` call again. + */ + def monitor[T](monitor: ActorRef[T], behavior: Behavior[T]): Behavior[T] = { + BehaviorImpl.tap( + (ctx, msg) ⇒ monitor ! msg, + unitFunction, + behavior) + } + + /** + * Wrap the given behavior such that it is restarted (i.e. reset to its + * initial state) whenever it throws an exception of the given class or a + * subclass thereof. Exceptions that are not subtypes of `Thr` will not be + * caught and thus lead to the termination of the actor. + * + * It is possible to specify different supervisor strategies, such as restart, + * resume, backoff. + */ + def restarter[T, Thr <: Throwable]( + clazz: Class[Thr], + strategy: SupervisorStrategy, + initialBehavior: Behavior[T]): Behavior[T] = { + Restarter(Behavior.validateAsInitial(initialBehavior), strategy)(ClassTag(clazz)) + } + + /** + * Widen the wrapped Behavior by placing a funnel in front of it: the supplied + * PartialFunction decides which message to pull in (those that it is defined + * at) and may transform the incoming message to place them into the wrapped + * Behavior’s type hierarchy. Signals are not transformed. + * + * Example: + * {{{ + * Behavior<String> s = immutable((ctx, msg) -> { + * System.out.println(msg); + * return same(); + * }); + * Behavior<Number> n = widened(s, pf -> pf. + * match(BigInteger.class, i -> "BigInteger(" + i + ")"). + * match(BigDecimal.class, d -> "BigDecimal(" + d + ")") + * // drop all other kinds of Number + * ); + * }}} + * + * @param behavior + * the behavior that will receive the selected messages + * @param selector + * a partial function builder for describing the selection and + * transformation + * @return a behavior of the widened type + */ + def widened[T, U](behavior: Behavior[T], selector: JFunction[PFBuilder[U, T], PFBuilder[U, T]]): Behavior[U] = + BehaviorImpl.widened(behavior, selector.apply(new PFBuilder).build()) + + /** + * Support for scheduled `self` messages in an actor. + * It takes care of the lifecycle of the timers such as cancelling them when the actor + * is restarted or stopped. + * @see [[TimerScheduler]] + */ + def withTimers[T](factory: akka.japi.function.Function[TimerScheduler[T], Behavior[T]]): Behavior[T] = + TimerSchedulerImpl.withTimers(timers ⇒ factory.apply(timers)) + + trait Receive[T] { + def receiveMessage(msg: T): Behavior[T] + def receiveSignal(msg: Signal): Behavior[T] + } +} diff --git a/akka-typed/src/main/scala/akka/typed/javadsl/ActorContext.scala b/akka-typed/src/main/scala/akka/typed/javadsl/ActorContext.scala new file mode 100644 index 0000000000..f774f85876 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/javadsl/ActorContext.scala @@ -0,0 +1,173 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.javadsl + +import java.util.function.{ Function ⇒ JFunction } +import akka.annotation.DoNotInherit +import akka.annotation.ApiMayChange +import akka.typed.ActorRef +import akka.typed.ActorSystem +import java.util.Optional +import akka.typed.Behavior +import akka.typed.Props +import scala.concurrent.duration.FiniteDuration +import scala.concurrent.ExecutionContextExecutor + +/** + * An Actor is given by the combination of a [[Behavior]] and a context in + * which this behavior is executed. As per the Actor Model an Actor can perform + * the following actions when processing a message: + * + * - send a finite number of messages to other Actors it knows + * - create a finite number of Actors + * - designate the behavior for the next message + * + * In Akka the first capability is accessed by using the `tell` method + * on an [[ActorRef]], the second is provided by [[ActorContext#spawn]] + * and the third is implicit in the signature of [[Behavior]] in that the next + * behavior is always returned from the message processing logic. + * + * An `ActorContext` in addition provides access to the Actor’s own identity (“`getSelf`”), + * the [[ActorSystem]] it is part of, methods for querying the list of child Actors it + * created, access to [[Terminated DeathWatch]] and timed message scheduling. + */ +@DoNotInherit +@ApiMayChange +trait ActorContext[T] { + // this must be a pure interface, i.e. only abstract methods + + /** + * Get the `scaladsl` of this `ActorContext`. + */ + def asScala: akka.typed.scaladsl.ActorContext[T] + + /** + * The identity of this Actor, bound to the lifecycle of this Actor instance. + * An Actor with the same name that lives before or after this instance will + * have a different [[ActorRef]]. + */ + def getSelf: ActorRef[T] + + /** + * Return the mailbox capacity that was configured by the parent for this actor. + */ + def getMailboxCapacity: Int + + /** + * The [[ActorSystem]] to which this Actor belongs. + */ + def getSystem: ActorSystem[Void] + + /** + * The list of child Actors created by this Actor during its lifetime that + * are still alive, in no particular order. + */ + def getChildren: java.util.List[ActorRef[Void]] + + /** + * The named child Actor if it is alive. + */ + def getChild(name: String): Optional[ActorRef[Void]] + + /** + * Create a child Actor from the given [[akka.typed.Behavior]] under a randomly chosen name. + * It is good practice to name Actors wherever practical. + */ + def spawnAnonymous[U](behavior: Behavior[U]): ActorRef[U] + + /** + * Create a child Actor from the given [[akka.typed.Behavior]] under a randomly chosen name. + * It is good practice to name Actors wherever practical. + */ + def spawnAnonymous[U](behavior: Behavior[U], props: Props): ActorRef[U] + + /** + * Create a child Actor from the given [[akka.typed.Behavior]] and with the given name. + */ + def spawn[U](behavior: Behavior[U], name: String): ActorRef[U] + + /** + * Create a child Actor from the given [[akka.typed.Behavior]] and with the given name. + */ + def spawn[U](behavior: Behavior[U], name: String, props: Props): ActorRef[U] + + /** + * Force the child Actor under the given name to terminate after it finishes + * processing its current message. Nothing happens if the ActorRef does not + * refer to a current child actor. + * + * @return whether the passed-in [[ActorRef]] points to a current child Actor + */ + def stop[U](child: ActorRef[U]): Boolean + + /** + * Register for [[Terminated]] notification once the Actor identified by the + * given [[ActorRef]] terminates. This message is also sent when the watched actor + * is on a node that has been removed from the cluster when using akka-cluster + * or has been marked unreachable when using akka-remote directly. + */ + def watch[U](other: ActorRef[U]): Unit + + /** + * Register for termination notification with a custom message once the Actor identified by the + * given [[ActorRef]] terminates. This message is also sent when the watched actor + * is on a node that has been removed from the cluster when using akka-cluster + * or has been marked unreachable when using akka-remote directly. + */ + def watchWith[U](other: ActorRef[U], msg: T): Unit + + /** + * Revoke the registration established by `watch`. A [[Terminated]] + * notification will not subsequently be received for the referenced Actor. + */ + def unwatch[U](other: ActorRef[U]): Unit + + /** + * Schedule the sending of a notification in case no other + * message is received during the given period of time. The timeout starts anew + * with each received message. Provide `Duration.Undefined` to switch off this + * mechanism. + */ + def setReceiveTimeout(d: FiniteDuration, msg: T): Unit + + /** + * Cancel the sending of receive timeout notifications. + */ + def cancelReceiveTimeout(): Unit + + /** + * Schedule the sending of the given message to the given target Actor after + * the given time period has elapsed. The scheduled action can be cancelled + * by invoking [[akka.actor.Cancellable#cancel]] on the returned + * handle. + */ + def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): akka.actor.Cancellable + + /** + * This Actor’s execution context. It can be used to run asynchronous tasks + * like [[scala.concurrent.Future]] combinators. + */ + def getExecutionContext: ExecutionContextExecutor + + /** + * Create a child actor that will wrap messages such that other Actor’s + * protocols can be ingested by this Actor. You are strongly advised to cache + * these ActorRefs or to stop them when no longer needed. + * + * The name of the child actor will be composed of a unique identifier + * starting with a dollar sign to which the given `name` argument is + * appended, with an inserted hyphen between these two parts. Therefore + * the given `name` argument does not need to be unique within the scope + * of the parent actor. + */ + def spawnAdapter[U](f: JFunction[U, T], name: String): ActorRef[U] + + /** + * Create an anonymous child actor that will wrap messages such that other Actor’s + * protocols can be ingested by this Actor. You are strongly advised to cache + * these ActorRefs or to stop them when no longer needed. + */ + def spawnAdapter[U](f: JFunction[U, T]): ActorRef[U] + +} diff --git a/akka-typed/src/main/scala/akka/typed/javadsl/Adapter.scala b/akka-typed/src/main/scala/akka/typed/javadsl/Adapter.scala new file mode 100644 index 0000000000..40f6a780ab --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/javadsl/Adapter.scala @@ -0,0 +1,115 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.javadsl + +import akka.typed.Behavior +import akka.typed.Props +import akka.typed.EmptyProps +import akka.typed.ActorRef +import akka.typed.internal.adapter.ActorRefAdapter +import akka.typed.scaladsl.adapter._ +import akka.typed.ActorSystem +import akka.typed.internal.adapter.ActorContextAdapter +import akka.japi.Creator + +/** + * Java API: Adapters between typed and untyped actors and actor systems. + * The underlying `ActorSystem` is the untyped [[akka.actor.ActorSystem]] + * which runs Akka Typed [[akka.typed.Behavior]] on an emulation layer. In this + * system typed and untyped actors can coexist. + * + * These methods make it possible to create typed child actor from untyped + * parent actor, and the opposite untyped child from typed parent. + * `watch` is also supported in both directions. + * + * There are also converters (`toTyped`, `toUntyped`) between untyped + * [[akka.actor.ActorRef]] and typed [[akka.typed.ActorRef]], and between untyped + * [[akka.actor.ActorSystem]] and typed [[akka.typed.ActorSystem]]. + */ +object Adapter { + + def spawnAnonymous[T](sys: akka.actor.ActorSystem, behavior: Behavior[T]): ActorRef[T] = + spawnAnonymous(sys, behavior, EmptyProps) + + def spawnAnonymous[T](sys: akka.actor.ActorSystem, behavior: Behavior[T], props: Props): ActorRef[T] = + sys.spawnAnonymous(behavior, props) + + def spawn[T](sys: akka.actor.ActorSystem, behavior: Behavior[T], name: String): ActorRef[T] = + spawn(sys, behavior, name, EmptyProps) + + def spawn[T](sys: akka.actor.ActorSystem, behavior: Behavior[T], name: String, props: Props): ActorRef[T] = + sys.spawn(behavior, name, props) + + def spawnAnonymous[T](ctx: akka.actor.ActorContext, behavior: Behavior[T]): ActorRef[T] = + spawnAnonymous(ctx, behavior, EmptyProps) + + def spawnAnonymous[T](ctx: akka.actor.ActorContext, behavior: Behavior[T], props: Props): ActorRef[T] = + ctx.spawnAnonymous(behavior, props) + + def spawn[T](ctx: akka.actor.ActorContext, behavior: Behavior[T], name: String): ActorRef[T] = + spawn(ctx, behavior, name, EmptyProps) + + def spawn[T](ctx: akka.actor.ActorContext, behavior: Behavior[T], name: String, props: Props): ActorRef[T] = + ctx.spawn(behavior, name, props) + + def toTyped(sys: akka.actor.ActorSystem): ActorSystem[Void] = + sys.toTyped.asInstanceOf[ActorSystem[Void]] + + def toUntyped(sys: ActorSystem[_]): akka.actor.ActorSystem = + sys.toUntyped + + def watch[U](ctx: akka.actor.ActorContext, other: ActorRef[U]): Unit = + ctx.watch(other) + + def unwatch[U](ctx: akka.actor.ActorContext, other: ActorRef[U]): Unit = + ctx.unwatch(other) + + def stop(ctx: akka.actor.ActorContext, child: ActorRef[_]): Unit = + ctx.stop(child) + + def watch[U](ctx: ActorContext[_], other: akka.actor.ActorRef): Unit = + ctx.watch(other) + + def unwatch[U](ctx: ActorContext[_], other: akka.actor.ActorRef): Unit = + ctx.unwatch(other) + + def stop(ctx: ActorContext[_], child: akka.actor.ActorRef): Boolean = + ctx.stop(child) + + def actorOf(ctx: ActorContext[_], props: akka.actor.Props): akka.actor.ActorRef = + ActorContextAdapter.toUntyped(ctx).actorOf(props) + + def actorOf(ctx: ActorContext[_], props: akka.actor.Props, name: String): akka.actor.ActorRef = + ActorContextAdapter.toUntyped(ctx).actorOf(props, name) + + def toUntyped(ref: ActorRef[_]): akka.actor.ActorRef = + ref.toUntyped + + def toTyped[T](ref: akka.actor.ActorRef): ActorRef[T] = + ref + + /** + * Wrap [[akka.typed.Behavior]] in an untyped [[akka.actor.Props]], i.e. when + * spawning a typed child actor from an untyped parent actor. + * This is normally not needed because you can use the extension methods + * `spawn` and `spawnAnonymous` with an untyped `ActorContext`, but it's needed + * when using typed actors with an existing library/tool that provides an API that + * takes an untyped [[akka.actor.Props]] parameter. Cluster Sharding is an + * example of that. + */ + def props[T](behavior: Creator[Behavior[T]], deploy: Props): akka.actor.Props = + akka.typed.internal.adapter.PropsAdapter(() ⇒ behavior.create(), deploy) + + /** + * Wrap [[akka.typed.Behavior]] in an untyped [[akka.actor.Props]], i.e. when + * spawning a typed child actor from an untyped parent actor. + * This is normally not needed because you can use the extension methods + * `spawn` and `spawnAnonymous` with an untyped `ActorContext`, but it's needed + * when using typed actors with an existing library/tool that provides an API that + * takes an untyped [[akka.actor.Props]] parameter. Cluster Sharding is an + * example of that. + */ + def props[T](behavior: Creator[Behavior[T]]): akka.actor.Props = + props(behavior, EmptyProps) +} diff --git a/akka-typed/src/main/scala/akka/typed/javadsl/Ask.scala b/akka-typed/src/main/scala/akka/typed/javadsl/Ask.scala new file mode 100644 index 0000000000..d615b072b3 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/javadsl/Ask.scala @@ -0,0 +1,14 @@ +package akka.typed +package javadsl + +import java.util.concurrent.CompletionStage +import scala.compat.java8.FutureConverters +import akka.util.Timeout +import akka.actor.Scheduler +import scaladsl.AskPattern._ +import akka.japi.function.Function + +object AskPattern { + def ask[T, U](actor: ActorRef[T], message: Function[ActorRef[U], T], timeout: Timeout, scheduler: Scheduler): CompletionStage[U] = + FutureConverters.toJava[U](actor.?(message.apply)(timeout, scheduler)) +} \ No newline at end of file diff --git a/akka-typed/src/main/scala/akka/typed/javadsl/BehaviorBuilder.scala b/akka-typed/src/main/scala/akka/typed/javadsl/BehaviorBuilder.scala new file mode 100644 index 0000000000..3c2d22d79c --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/javadsl/BehaviorBuilder.scala @@ -0,0 +1,268 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + */ + +package akka.typed.javadsl + +import scala.annotation.tailrec + +import akka.japi.function.{ Function, Function2, Predicate } +import akka.annotation.InternalApi +import akka.typed +import akka.typed.{ Behavior, ExtensibleBehavior, Signal } + +import akka.typed.Behavior.unhandled + +import BehaviorBuilder._ + +/** + * Used for creating a [[Behavior]] by 'chaining' message and signal handlers. + * + * When handling a message or signal, this [[Behavior]] will consider all handlers in the order they were added, + * looking for the first handler for which both the type and the (optional) predicate match. + * + * @tparam T the common superclass of all supported messages. + */ +class BehaviorBuilder[T] private ( + private val messageHandlers: List[Case[T, T]], + private val signalHandlers: List[Case[T, Signal]] +) { + + def build(): Behavior[T] = new BuiltBehavior(messageHandlers.reverse, signalHandlers.reverse) + + /** + * Add a new case to the message handling. + * + * @param type type of message to match + * @param handler action to apply if the type matches + * @tparam M type of message to match + * @return a new behavior with the specified handling appended + */ + def onMessage[M <: T](`type`: Class[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + withMessage(`type`, None, (i1: ActorContext[T], msg: T) ⇒ handler.apply(i1, msg.asInstanceOf[M])) + + /** + * Add a new predicated case to the message handling. + * + * @param type type of message to match + * @param test a predicate that will be evaluated on the argument if the type matches + * @param handler action to apply if the type matches and the predicate returns true + * @tparam M type of message to match + * @return a new behavior with the specified handling appended + */ + def onMessage[M <: T](`type`: Class[M], test: Predicate[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + withMessage( + `type`, + Some((t: T) ⇒ test.test(t.asInstanceOf[M])), + (i1: ActorContext[T], msg: T) ⇒ handler.apply(i1, msg.asInstanceOf[M]) + ) + + /** + * Add a new case to the message handling without compile time type check. + * + * Should normally not be used, but when matching on class with generic type + * argument it can be useful, e.g. List.class and (List<String> list) -> {...} + * + * @param type type of message to match + * @param handler action to apply when the type matches + * @return a new behavior with the specified handling appended + */ + def onMessageUnchecked[M <: T](`type`: Class[_ <: T], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + withMessage(`type`, None, (i1: ActorContext[T], msg: T) ⇒ handler.apply(i1, msg.asInstanceOf[M])) + + /** + * Add a new case to the message handling matching equal messages. + * + * @param msg the message to compare to + * @param handler action to apply when the message matches + * @return a new behavior with the specified handling appended + */ + def onMessageEquals(msg: T, handler: Function[ActorContext[T], Behavior[T]]): BehaviorBuilder[T] = + withMessage(msg.getClass, Some(_.equals(msg)), (ctx: ActorContext[T], _: T) ⇒ handler.apply(ctx)) + + /** + * Add a new case to the signal handling. + * + * @param type type of signal to match + * @param handler action to apply if the type matches + * @tparam M type of signal to match + * @return a new behavior with the specified handling appended + */ + def onSignal[M <: Signal](`type`: Class[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + withSignal(`type`, None, (ctx: ActorContext[T], signal: Signal) ⇒ handler.apply(ctx, signal.asInstanceOf[M])) + + /** + * Add a new predicated case to the signal handling. + * + * @param type type of signals to match + * @param test a predicate that will be evaluated on the argument if the type matches + * @param handler action to apply if the type matches and the predicate returns true + * @tparam M type of signal to match + * @return a new behavior with the specified handling appended + */ + def onSignal[M <: Signal](`type`: Class[M], test: Predicate[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + withSignal( + `type`, + Some((t: Signal) ⇒ test.test(t.asInstanceOf[M])), + (ctx: ActorContext[T], signal: Signal) ⇒ handler.apply(ctx, signal.asInstanceOf[M]) + ) + + /** + * Add a new case to the signal handling without compile time type check. + * + * Should normally not be used, but when matching on class with generic type + * argument it can be useful, e.g. GenMsg.class and (ActorContext ctx, GenMsg<String> list) -> {...} + * + * @param type type of signal to match + * @param handler action to apply when the type matches + * @return a new behavior with the specified handling appended + */ + def onSignalUnchecked[M <: Signal](`type`: Class[_ <: Signal], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + withSignal(`type`, None, (ctx: ActorContext[T], signal: Signal) ⇒ handler.apply(ctx, signal.asInstanceOf[M])) + + /** + * Add a new case to the signal handling matching equal signals. + * + * @param signal the signal to compare to + * @param handler action to apply when the message matches + * @return a new behavior with the specified handling appended + */ + def onSignalEquals(signal: Signal, handler: Function[ActorContext[T], Behavior[T]]): BehaviorBuilder[T] = + withSignal(signal.getClass, Some(_.equals(signal)), (ctx: ActorContext[T], _: Signal) ⇒ handler.apply(ctx)) + + private def withMessage(`type`: Class[_ <: T], test: Option[T ⇒ Boolean], handler: (ActorContext[T], T) ⇒ Behavior[T]): BehaviorBuilder[T] = + new BehaviorBuilder[T](Case[T, T](`type`, test, handler) +: messageHandlers, signalHandlers) + + private def withSignal[M <: Signal](`type`: Class[M], test: Option[Signal ⇒ Boolean], handler: (ActorContext[T], Signal) ⇒ Behavior[T]): BehaviorBuilder[T] = + new BehaviorBuilder[T](messageHandlers, Case[T, Signal](`type`, test, handler) +: signalHandlers) +} + +object BehaviorBuilder { + def create[T]: BehaviorBuilder[T] = new BehaviorBuilder[T](Nil, Nil) + + import scala.language.existentials + + /** INTERNAL API */ + @InternalApi + private[javadsl] final case class Case[BT, MT](`type`: Class[_ <: MT], test: Option[MT ⇒ Boolean], handler: (ActorContext[BT], MT) ⇒ Behavior[BT]) + + /** + * Start a new behavior chain starting with this case. + * + * @param type type of message to match + * @param handler action to apply if the type matches + * @tparam T type of behavior to create + * @tparam M type of message to match + * @return a new behavior with the specified handling appended + */ + def message[T, M <: T](`type`: Class[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + BehaviorBuilder.create[T].onMessage(`type`, handler) + + /** + * Start a new behavior chain starting with this predicated case. + * + * @param type type of message to match + * @param test a predicate that will be evaluated on the argument if the type matches + * @param handler action to apply if the type matches and the predicate returns true + * @tparam T type of behavior to create + * @tparam M type of message to match + * @return a new behavior with the specified handling appended + */ + def message[T, M <: T](`type`: Class[M], test: Predicate[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + BehaviorBuilder.create[T].onMessage(`type`, test, handler) + + /** + * Start a new behavior chain starting with a handler without compile time type check. + * + * Should normally not be used, but when matching on class with generic type + * argument it can be useful, e.g. List.class and (List<String> list) -> {...} + * + * @param type type of message to match + * @param handler action to apply when the type matches + * @return a new behavior with the specified handling appended + */ + def messageUnchecked[T, M <: T](`type`: Class[_ <: T], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + BehaviorBuilder.create[T].onMessageUnchecked(`type`, handler) + + /** + * Start a new behavior chain starting with a handler for equal messages. + * + * @param msg the message to compare to + * @param handler action to apply when the message matches + * @tparam T type of behavior to create + * @return a new behavior with the specified handling appended + */ + def messageEquals[T](msg: T, handler: Function[ActorContext[T], Behavior[T]]): BehaviorBuilder[T] = + BehaviorBuilder.create[T].onMessageEquals(msg, handler) + + /** + * Start a new behavior chain starting with this signal case. + * + * @param type type of signal to match + * @param handler action to apply if the type matches + * @tparam T type of behavior to create + * @tparam M type of signal to match + * @return a new behavior with the specified handling appended + */ + def signal[T, M <: Signal](`type`: Class[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + BehaviorBuilder.create[T].onSignal(`type`, handler) + + /** + * Start a new behavior chain starting with this predicated signal case. + * + * @param type type of signals to match + * @param test a predicate that will be evaluated on the argument if the type matches + * @param handler action to apply if the type matches and the predicate returns true + * @tparam T type of behavior to create + * @tparam M type of signal to match + * @return a new behavior with the specified handling appended + */ + def signal[T, M <: Signal](`type`: Class[M], test: Predicate[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + BehaviorBuilder.create[T].onSignal(`type`, test, handler) + + /** + * Start a new behavior chain starting with this unchecked signal case. + * + * Should normally not be used, but when matching on class with generic type + * argument it can be useful, e.g. GenMsg.class and (ActorContext ctx, GenMsg<String> list) -> {...} + * + * @param type type of signal to match + * @param handler action to apply when the type matches + * @return a new behavior with the specified handling appended + */ + def signalUnchecked[T, M <: Signal](`type`: Class[_ <: Signal], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = + BehaviorBuilder.create[T].onSignalUnchecked(`type`, handler) + + /** + * Start a new behavior chain starting with a handler for this specific signal. + * + * @param signal the signal to compare to + * @param handler action to apply when the message matches + * @tparam T type of behavior to create + * @return a new behavior with the specified handling appended + */ + def signalEquals[T](signal: Signal, handler: Function[ActorContext[T], Behavior[T]]): BehaviorBuilder[T] = + BehaviorBuilder.create[T].onSignalEquals(signal, handler) + +} + +private class BuiltBehavior[T]( + private val messageHandlers: List[Case[T, T]], + private val signalHandlers: List[Case[T, Signal]] +) extends ExtensibleBehavior[T] { + + override def receiveMessage(ctx: typed.ActorContext[T], msg: T): Behavior[T] = receive[T](ctx.asJava, msg, messageHandlers) + + override def receiveSignal(ctx: typed.ActorContext[T], msg: Signal): Behavior[T] = receive[Signal](ctx.asJava, msg, signalHandlers) + + @tailrec + private def receive[M](ctx: ActorContext[T], msg: M, handlers: List[Case[T, M]]): Behavior[T] = + handlers match { + case Case(cls, predicate, handler) :: tail ⇒ + if (cls.isAssignableFrom(msg.getClass) && (predicate.isEmpty || predicate.get.apply(msg))) handler(ctx, msg) + else receive[M](ctx, msg, tail) + case _ ⇒ + unhandled[T] + } + +} \ No newline at end of file diff --git a/akka-typed/src/main/scala/akka/typed/javadsl/ReceiveBuilder.scala b/akka-typed/src/main/scala/akka/typed/javadsl/ReceiveBuilder.scala new file mode 100644 index 0000000000..7a3beee314 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/javadsl/ReceiveBuilder.scala @@ -0,0 +1,169 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + */ + +package akka.typed.javadsl + +import scala.annotation.tailrec +import akka.japi.function.{ Creator, Function, Predicate } +import akka.typed.javadsl.Actor.Receive +import akka.typed.{ Behavior, Signal } +import ReceiveBuilder._ +import akka.annotation.InternalApi + +/** + * Used when implementing [[Actor.MutableBehavior]]. + * + * When handling a message or signal, this [[Behavior]] will consider all handlers in the order they were added, + * looking for the first handler for which both the type and the (optional) predicate match. + * + * @tparam T the common superclass of all supported messages. + */ +class ReceiveBuilder[T] private ( + private val messageHandlers: List[Case[T, T]], + private val signalHandlers: List[Case[T, Signal]] +) { + + def build(): Receive[T] = new BuiltReceive(messageHandlers.reverse, signalHandlers.reverse) + + /** + * Add a new case to the message handling. + * + * @param type type of message to match + * @param handler action to apply if the type matches + * @tparam M type of message to match + * @return a new behavior with the specified handling appended + */ + def onMessage[M <: T](`type`: Class[M], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] = + withMessage(`type`, None, msg ⇒ handler.apply(msg.asInstanceOf[M])) + + /** + * Add a new predicated case to the message handling. + * + * @param type type of message to match + * @param test a predicate that will be evaluated on the argument if the type matches + * @param handler action to apply if the type matches and the predicate returns true + * @tparam M type of message to match + * @return a new behavior with the specified handling appended + */ + def onMessage[M <: T](`type`: Class[M], test: Predicate[M], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] = + withMessage( + `type`, + Some((t: T) ⇒ test.test(t.asInstanceOf[M])), + msg ⇒ handler.apply(msg.asInstanceOf[M]) + ) + + /** + * Add a new case to the message handling without compile time type check. + * + * Should normally not be used, but when matching on class with generic type + * argument it can be useful, e.g. List.class and (List<String> list) -> {...} + * + * @param type type of message to match + * @param handler action to apply when the type matches + * @return a new behavior with the specified handling appended + */ + def onMessageUnchecked[M <: T](`type`: Class[_ <: T], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] = + withMessage(`type`, None, msg ⇒ handler.apply(msg.asInstanceOf[M])) + + /** + * Add a new case to the message handling matching equal messages. + * + * @param msg the message to compare to + * @param handler action to apply when the message matches + * @return a new behavior with the specified handling appended + */ + def onMessageEquals(msg: T, handler: Creator[Behavior[T]]): ReceiveBuilder[T] = + withMessage(msg.getClass, Some(_.equals(msg)), _ ⇒ handler.create()) + + /** + * Add a new case to the signal handling. + * + * @param type type of signal to match + * @param handler action to apply if the type matches + * @tparam M type of signal to match + * @return a new behavior with the specified handling appended + */ + def onSignal[M <: Signal](`type`: Class[M], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] = + withSignal(`type`, None, signal ⇒ handler.apply(signal.asInstanceOf[M])) + + /** + * Add a new predicated case to the signal handling. + * + * @param type type of signals to match + * @param test a predicate that will be evaluated on the argument if the type matches + * @param handler action to apply if the type matches and the predicate returns true + * @tparam M type of signal to match + * @return a new behavior with the specified handling appended + */ + def onSignal[M <: Signal](`type`: Class[M], test: Predicate[M], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] = + withSignal( + `type`, + Some((t: Signal) ⇒ test.test(t.asInstanceOf[M])), + signal ⇒ handler.apply(signal.asInstanceOf[M]) + ) + + /** + * Add a new case to the signal handling without compile time type check. + * + * Should normally not be used, but when matching on class with generic type + * argument it can be useful, e.g. GenMsg.class and (ActorContext ctx, GenMsg<String> list) -> {...} + * + * @param type type of signal to match + * @param handler action to apply when the type matches + * @return a new behavior with the specified handling appended + */ + def onSignalUnchecked[M <: Signal](`type`: Class[_ <: Signal], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] = + withSignal(`type`, None, signal ⇒ handler.apply(signal.asInstanceOf[M])) + + /** + * Add a new case to the signal handling matching equal signals. + * + * @param signal the signal to compare to + * @param handler action to apply when the message matches + * @return a new behavior with the specified handling appended + */ + def onSignalEquals(signal: Signal, handler: Creator[Behavior[T]]): ReceiveBuilder[T] = + withSignal(signal.getClass, Some(_.equals(signal)), _ ⇒ handler.create()) + + private def withMessage(`type`: Class[_ <: T], test: Option[T ⇒ Boolean], handler: T ⇒ Behavior[T]): ReceiveBuilder[T] = + new ReceiveBuilder[T](Case[T, T](`type`, test, handler) +: messageHandlers, signalHandlers) + + private def withSignal[M <: Signal](`type`: Class[M], test: Option[Signal ⇒ Boolean], handler: Signal ⇒ Behavior[T]): ReceiveBuilder[T] = + new ReceiveBuilder[T](messageHandlers, Case[T, Signal](`type`, test, handler) +: signalHandlers) +} + +object ReceiveBuilder { + def create[T]: ReceiveBuilder[T] = new ReceiveBuilder[T](Nil, Nil) + + import scala.language.existentials + + /** INTERNAL API */ + @InternalApi + private[javadsl] final case class Case[BT, MT](`type`: Class[_ <: MT], test: Option[MT ⇒ Boolean], handler: MT ⇒ Behavior[BT]) + +} + +/** + * Receive type for [[Actor.MutableBehavior]] + */ +private class BuiltReceive[T]( + private val messageHandlers: List[Case[T, T]], + private val signalHandlers: List[Case[T, Signal]] +) extends Receive[T] { + + override def receiveMessage(msg: T): Behavior[T] = receive[T](msg, messageHandlers) + + override def receiveSignal(msg: Signal): Behavior[T] = receive[Signal](msg, signalHandlers) + + @tailrec + private def receive[M](msg: M, handlers: List[Case[T, M]]): Behavior[T] = + handlers match { + case Case(cls, predicate, handler) :: tail ⇒ + if (cls.isAssignableFrom(msg.getClass) && (predicate.isEmpty || predicate.get.apply(msg))) handler(msg) + else receive[M](msg, tail) + case _ ⇒ + Actor.unhandled + } + +} \ No newline at end of file diff --git a/akka-typed/src/main/scala/akka/typed/javadsl/TimerScheduler.scala b/akka-typed/src/main/scala/akka/typed/javadsl/TimerScheduler.scala new file mode 100644 index 0000000000..1412043a29 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/javadsl/TimerScheduler.scala @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.javadsl + +import scala.concurrent.duration.FiniteDuration + +/** + * Support for scheduled `self` messages in an actor. + * It is used with `Actor.withTimers`, which also takes care of the + * lifecycle of the timers such as cancelling them when the actor + * is restarted or stopped. + * + * `TimerScheduler` is not thread-safe, i.e. it must only be used within + * the actor that owns it. + */ +trait TimerScheduler[T] { + + /** + * Start a periodic timer that will send `msg` to the `self` actor at + * a fixed `interval`. + * + * Each timer has a key and if a new timer with same key is started + * the previous is cancelled and it's guaranteed that a message from the + * previous timer is not received, even though it might already be enqueued + * in the mailbox when the new timer is started. + */ + def startPeriodicTimer(key: Any, msg: T, interval: FiniteDuration): Unit + + /** + * * Start a timer that will send `msg` once to the `self` actor after + * the given `timeout`. + * + * Each timer has a key and if a new timer with same key is started + * the previous is cancelled and it's guaranteed that a message from the + * previous timer is not received, even though it might already be enqueued + * in the mailbox when the new timer is started. + */ + def startSingleTimer(key: Any, msg: T, timeout: FiniteDuration): Unit + + /** + * Check if a timer with a given `key` is active. + */ + def isTimerActive(key: Any): Boolean + + /** + * Cancel a timer with a given `key`. + * If canceling a timer that was already canceled, or key never was used to start a timer + * this operation will do nothing. + * + * It is guaranteed that a message from a canceled timer, including its previous incarnation + * for the same key, will not be received by the actor, even though the message might already + * be enqueued in the mailbox when cancel is called. + */ + def cancel(key: Any): Unit + + /** + * Cancel all timers. + */ + def cancelAll(): Unit + +} diff --git a/akka-typed/src/main/scala/akka/typed/patterns/Receptionist.scala b/akka-typed/src/main/scala/akka/typed/patterns/Receptionist.scala index f93db3f075..029dd487ee 100644 --- a/akka-typed/src/main/scala/akka/typed/patterns/Receptionist.scala +++ b/akka-typed/src/main/scala/akka/typed/patterns/Receptionist.scala @@ -3,11 +3,11 @@ */ package akka.typed.patterns -import akka.typed.ScalaDSL._ import akka.typed.ActorRef import akka.typed.Behavior import akka.typed.Terminated import akka.util.TypedMultiMap +import akka.typed.scaladsl.Actor._ /** * A Receptionist is an entry point into an Actor hierarchy where select Actors @@ -72,17 +72,21 @@ object Receptionist { private type KV[K <: AbstractServiceKey] = ActorRef[K#Type] - private def behavior(map: TypedMultiMap[AbstractServiceKey, KV]): Behavior[Command] = Full { - case Msg(ctx, r: Register[t]) ⇒ - ctx.watch(r.address) - r.replyTo ! Registered(r.key, r.address) - behavior(map.inserted(r.key)(r.address)) - case Msg(ctx, f: Find[t]) ⇒ - val set = map get f.key - f.replyTo ! Listing(f.key, set) - Same - case Sig(ctx, Terminated(ref)) ⇒ + private def behavior(map: TypedMultiMap[AbstractServiceKey, KV]): Behavior[Command] = immutable[Command] { (ctx, msg) ⇒ + msg match { + case r: Register[t] ⇒ + ctx.watch(r.address) + r.replyTo ! Registered(r.key, r.address) + behavior(map.inserted(r.key)(r.address)) + case f: Find[t] ⇒ + val set = map get f.key + f.replyTo ! Listing(f.key, set) + same + } + } onSignal { + case (ctx, Terminated(ref)) ⇒ behavior(map valueRemoved ref) + case x ⇒ unhandled } } diff --git a/akka-typed/src/main/scala/akka/typed/patterns/Restarter.scala b/akka-typed/src/main/scala/akka/typed/patterns/Restarter.scala deleted file mode 100644 index 171b5b4875..0000000000 --- a/akka-typed/src/main/scala/akka/typed/patterns/Restarter.scala +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (C) 2016-2017 Lightbend Inc. - */ -package akka.typed -package patterns - -import scala.reflect.ClassTag -import scala.util.control.NonFatal -import akka.event.Logging -import akka.typed.scaladsl.Actor._ - -/** - * Simple supervision strategy that restarts the underlying behavior for all - * failures of type Thr. - * - * FIXME add limited restarts and back-off (with limited buffering or vacation responder) - * FIXME write tests that ensure that restart is canonicalizing PreStart result correctly - */ -final case class Restarter[T, Thr <: Throwable: ClassTag](initialBehavior: Behavior[T], resume: Boolean)( - behavior: Behavior[T] = initialBehavior) extends Behavior[T] { - - private def restart(ctx: ActorContext[T]): Behavior[T] = { - try behavior.management(ctx, PreRestart) catch { case NonFatal(_) ⇒ } - Behavior.canonicalize(initialBehavior.management(ctx, PreStart), initialBehavior) - } - - private def canonical(b: Behavior[T]): Behavior[T] = - if (Behavior.isUnhandled(b)) Unhandled - else if (b eq Behavior.sameBehavior) Same - else if (!Behavior.isAlive(b)) Stopped - else if (b eq behavior) Same - else Restarter[T, Thr](initialBehavior, resume)(b) - - override def management(ctx: ActorContext[T], signal: Signal): Behavior[T] = { - val b = - try behavior.management(ctx, signal) - catch { - case ex: Thr ⇒ - ctx.system.eventStream.publish(Logging.Error(ex, ctx.self.toString, behavior.getClass, ex.getMessage)) - if (resume) behavior else restart(ctx) - } - canonical(b) - } - - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = { - val b = - try behavior.message(ctx, msg) - catch { - case ex: Thr ⇒ - ctx.system.eventStream.publish(Logging.Error(ex, ctx.self.toString, behavior.getClass, ex.getMessage)) - if (resume) behavior else restart(ctx) - } - canonical(b) - } -} - -/** - * Simple supervision strategy that restarts the underlying behavior for all - * failures of type Thr. - * - * FIXME add limited restarts and back-off (with limited buffering or vacation responder) - * FIXME write tests that ensure that all Behaviors are okay with getting PostRestart as first signal - */ -final case class MutableRestarter[T, Thr <: Throwable: ClassTag](initialBehavior: Behavior[T], resume: Boolean) extends Behavior[T] { - - private[this] var current = initialBehavior - - private def restart(ctx: ActorContext[T]): Behavior[T] = { - try current.management(ctx, PreRestart) catch { case NonFatal(_) ⇒ } - current = initialBehavior - current.management(ctx, PreStart) - } - - override def management(ctx: ActorContext[T], signal: Signal): Behavior[T] = { - val b = - try current.management(ctx, signal) - catch { - case ex: Thr ⇒ - ctx.system.eventStream.publish(Logging.Error(ex, ctx.self.toString, current.getClass, ex.getMessage)) - if (resume) current else restart(ctx) - } - current = Behavior.canonicalize(b, current) - if (Behavior.isAlive(current)) this else Stopped - } - - override def message(ctx: ActorContext[T], msg: T): Behavior[T] = { - val b = - try current.message(ctx, msg) - catch { - case ex: Thr ⇒ - ctx.system.eventStream.publish(Logging.Error(ex, ctx.self.toString, current.getClass, ex.getMessage)) - if (resume) current else restart(ctx) - } - current = Behavior.canonicalize(b, current) - if (Behavior.isAlive(current)) this else Stopped - } -} diff --git a/akka-typed/src/main/scala/akka/typed/scaladsl/Actor.scala b/akka-typed/src/main/scala/akka/typed/scaladsl/Actor.scala index fdd5b47cc5..d9c3481677 100644 --- a/akka-typed/src/main/scala/akka/typed/scaladsl/Actor.scala +++ b/akka-typed/src/main/scala/akka/typed/scaladsl/Actor.scala @@ -4,153 +4,19 @@ package akka.typed package scaladsl -import akka.util.LineNumbers import scala.reflect.ClassTag -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.ExecutionContextExecutor -import scala.deprecatedInheritance -import akka.typed.{ ActorContext ⇒ AC } + import akka.annotation.ApiMayChange import akka.annotation.DoNotInherit - -/** - * An Actor is given by the combination of a [[Behavior]] and a context in - * which this behavior is executed. As per the Actor Model an Actor can perform - * the following actions when processing a message: - * - * - send a finite number of messages to other Actors it knows - * - create a finite number of Actors - * - designate the behavior for the next message - * - * In Akka the first capability is accessed by using the `!` or `tell` method - * on an [[ActorRef]], the second is provided by [[ActorContext#spawn]] - * and the third is implicit in the signature of [[Behavior]] in that the next - * behavior is always returned from the message processing logic. - * - * An `ActorContext` in addition provides access to the Actor’s own identity (“`self`”), - * the [[ActorSystem]] it is part of, methods for querying the list of child Actors it - * created, access to [[Terminated DeathWatch]] and timed message scheduling. - */ -@DoNotInherit -@ApiMayChange -trait ActorContext[T] { this: akka.typed.javadsl.ActorContext[T] ⇒ - - /** - * The identity of this Actor, bound to the lifecycle of this Actor instance. - * An Actor with the same name that lives before or after this instance will - * have a different [[ActorRef]]. - */ - def self: ActorRef[T] - - /** - * Return the mailbox capacity that was configured by the parent for this actor. - */ - def mailboxCapacity: Int - - /** - * The [[ActorSystem]] to which this Actor belongs. - */ - def system: ActorSystem[Nothing] - - /** - * The list of child Actors created by this Actor during its lifetime that - * are still alive, in no particular order. - */ - def children: Iterable[ActorRef[Nothing]] - - /** - * The named child Actor if it is alive. - */ - def child(name: String): Option[ActorRef[Nothing]] - - /** - * Create a child Actor from the given [[akka.typed.Behavior]] under a randomly chosen name. - * It is good practice to name Actors wherever practical. - */ - def spawnAnonymous[U](behavior: Behavior[U], deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[U] - - /** - * Create a child Actor from the given [[akka.typed.Behavior]] and with the given name. - */ - def spawn[U](behavior: Behavior[U], name: String, deployment: DeploymentConfig = EmptyDeploymentConfig): ActorRef[U] - - /** - * Force the child Actor under the given name to terminate after it finishes - * processing its current message. Nothing happens if the ActorRef does not - * refer to a current child actor. - * - * @return whether the passed-in [[ActorRef]] points to a current child Actor - */ - def stop(child: ActorRef[_]): Boolean - - /** - * Register for [[Terminated]] notification once the Actor identified by the - * given [[ActorRef]] terminates. This notification is also generated when the - * [[ActorSystem]] to which the referenced Actor belongs is declared as - * failed (e.g. in reaction to being unreachable). - */ - def watch[U](other: ActorRef[U]): ActorRef[U] - - /** - * Revoke the registration established by `watch`. A [[Terminated]] - * notification will not subsequently be received for the referenced Actor. - */ - def unwatch[U](other: ActorRef[U]): ActorRef[U] - - /** - * Schedule the sending of a notification in case no other - * message is received during the given period of time. The timeout starts anew - * with each received message. Provide `Duration.Undefined` to switch off this - * mechanism. - */ - def setReceiveTimeout(d: FiniteDuration, msg: T): Unit - - /** - * Cancel the sending of receive timeout notifications. - */ - def cancelReceiveTimeout(): Unit - - /** - * Schedule the sending of the given message to the given target Actor after - * the given time period has elapsed. The scheduled action can be cancelled - * by invoking [[akka.actor.Cancellable#cancel]] on the returned - * handle. - */ - def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): akka.actor.Cancellable - - /** - * This Actor’s execution context. It can be used to run asynchronous tasks - * like [[scala.concurrent.Future]] combinators. - */ - implicit def executionContext: ExecutionContextExecutor - - /** - * Create a child actor that will wrap messages such that other Actor’s - * protocols can be ingested by this Actor. You are strongly advised to cache - * these ActorRefs or to stop them when no longer needed. - * - * The name of the child actor will be composed of a unique identifier - * starting with a dollar sign to which the given `name` argument is - * appended, with an inserted hyphen between these two parts. Therefore - * the given `name` argument does not need to be unique within the scope - * of the parent actor. - */ - def spawnAdapter[U](f: U ⇒ T, name: String): ActorRef[U] - - /** - * Create an anonymous child actor that will wrap messages such that other Actor’s - * protocols can be ingested by this Actor. You are strongly advised to cache - * these ActorRefs or to stop them when no longer needed. - */ - def spawnAdapter[U](f: U ⇒ T): ActorRef[U] = spawnAdapter(f, "") - -} +import akka.typed.internal.BehaviorImpl +import akka.typed.internal.TimerSchedulerImpl @ApiMayChange object Actor { import Behavior._ - // FIXME check that all behaviors can cope with not getting PreStart as first message + private val _unitFunction = (_: ActorContext[Any], _: Any) ⇒ () + private def unitFunction[T] = _unitFunction.asInstanceOf[((ActorContext[T], Signal) ⇒ Unit)] final implicit class BehaviorDecorators[T](val behavior: Behavior[T]) extends AnyVal { /** @@ -159,66 +25,97 @@ object Actor { * at) and may transform the incoming message to place them into the wrapped * Behavior’s type hierarchy. Signals are not transformed. * - * see also [[Actor.Widened]] + * Example: + * {{{ + * immutable[String] { (ctx, msg) => println(msg); same }.widen[Number] { + * case b: BigDecimal => s"BigDecimal($b)" + * case i: BigInteger => s"BigInteger($i)" + * // drop all other kinds of Number + * } + * }}} */ - def widen[U](matcher: PartialFunction[U, T]): Behavior[U] = Widened(behavior, matcher) - } - - private val _nullFun = (_: Any) ⇒ null - private def nullFun[T] = _nullFun.asInstanceOf[Any ⇒ T] - private implicit class ContextAs[T](val ctx: AC[T]) extends AnyVal { - def as[U] = ctx.asInstanceOf[AC[U]] + def widen[U](matcher: PartialFunction[U, T]): Behavior[U] = + BehaviorImpl.widened(behavior, matcher) } /** - * Widen the wrapped Behavior by placing a funnel in front of it: the supplied - * PartialFunction decides which message to pull in (those that it is defined - * at) and may transform the incoming message to place them into the wrapped - * Behavior’s type hierarchy. Signals are not transformed. + * `deferred` is a factory for a behavior. Creation of the behavior instance is deferred until + * the actor is started, as opposed to `Actor.immutable` that creates the behavior instance + * immediately before the actor is running. The `factory` function pass the `ActorContext` + * as parameter and that can for example be used for spawning child actors. * - * Example: - * {{{ - * Stateless[String]((ctx, msg) => println(msg)).widen[Number] { - * case b: BigDecimal => s"BigDecimal($b)" - * case i: BigInteger => s"BigInteger($i)" - * // drop all other kinds of Number - * } - * }}} + * `deferred` is typically used as the outer most behavior when spawning an actor, but it + * can also be returned as the next behavior when processing a message or signal. In that + * case it will be "undeferred" immediately after it is returned, i.e. next message will be + * processed by the undeferred behavior. */ - final case class Widened[T, U](behavior: Behavior[T], matcher: PartialFunction[U, T]) extends Behavior[U] { - private def postProcess(behv: Behavior[T]): Behavior[U] = - if (isUnhandled(behv)) Unhandled - else if (isAlive(behv)) { - val next = canonicalize(behv, behavior) - if (next eq behavior) Same else Widened(next, matcher) - } else Stopped - - override def management(ctx: AC[U], msg: Signal): Behavior[U] = - postProcess(behavior.management(ctx.as[T], msg)) - - override def message(ctx: AC[U], msg: U): Behavior[U] = - matcher.applyOrElse(msg, nullFun) match { - case null ⇒ Unhandled - case transformed ⇒ postProcess(behavior.message(ctx.as[T], transformed)) - } - - override def toString: String = s"${behavior.toString}.widen(${LineNumbers(matcher)})" - } + def deferred[T](factory: ActorContext[T] ⇒ Behavior[T]): Behavior[T] = + Behavior.DeferredBehavior(factory) /** - * Wrap a behavior factory so that it runs upon PreStart, i.e. behavior creation - * is deferred to the child actor instead of running within the parent. + * Factory for creating a [[MutableBehavior]] that typically holds mutable state as + * instance variables in the concrete [[MutableBehavior]] implementation class. + * + * Creation of the behavior instance is deferred, i.e. it is created via the `factory` + * function. The reason for the deferred creation is to avoid sharing the same instance in + * multiple actors, and to create a new instance when the actor is restarted. + * + * @param producer + * behavior factory that takes the child actor’s context as argument + * @return the deferred behavior */ - final case class Deferred[T](factory: ActorContext[T] ⇒ Behavior[T]) extends Behavior[T] { - override def management(ctx: AC[T], msg: Signal): Behavior[T] = { - if (msg != PreStart) throw new IllegalStateException(s"Deferred must receive PreStart as first message (got $msg)") - Behavior.preStart(factory(ctx), ctx) - } + def mutable[T](factory: ActorContext[T] ⇒ MutableBehavior[T]): Behavior[T] = + deferred(factory) - override def message(ctx: AC[T], msg: T): Behavior[T] = - throw new IllegalStateException(s"Deferred must receive PreStart as first message (got $msg)") + /** + * Mutable behavior can be implemented by extending this class and implement the + * abstract method [[MutableBehavior#onMessage]] and optionally override + * [[MutableBehavior#onSignal]]. + * + * Instances of this behavior should be created via [[Actor#Mutable]] and if + * the [[ActorContext]] is needed it can be passed as a constructor parameter + * from the factory function. + * + * @see [[Actor#Mutable]] + */ + abstract class MutableBehavior[T] extends ExtensibleBehavior[T] { + @throws(classOf[Exception]) + override final def receiveMessage(ctx: akka.typed.ActorContext[T], msg: T): Behavior[T] = + onMessage(msg) - override def toString: String = s"Deferred(${LineNumbers(factory)})" + /** + * Implement this method to process an incoming message and return the next behavior. + * + * The returned behavior can in addition to normal behaviors be one of the canned special objects: + *
    + *
  • returning `stopped` will terminate this Behavior
  • + *
  • returning `this` or `same` designates to reuse the current Behavior
  • + *
  • returning `unhandled` keeps the same Behavior and signals that the message was not yet handled
  • + *
+ * + */ + @throws(classOf[Exception]) + def onMessage(msg: T): Behavior[T] + + @throws(classOf[Exception]) + override final def receiveSignal(ctx: akka.typed.ActorContext[T], msg: Signal): Behavior[T] = + onSignal.applyOrElse(msg, { case _ ⇒ Behavior.unhandled }: PartialFunction[Signal, Behavior[T]]) + + /** + * Override this method to process an incoming [[akka.typed.Signal]] and return the next behavior. + * This means that all lifecycle hooks, ReceiveTimeout, Terminated and Failed messages + * can initiate a behavior change. + * + * The returned behavior can in addition to normal behaviors be one of the canned special objects: + * + * * returning `stopped` will terminate this Behavior + * * returning `this` or `same` designates to reuse the current Behavior + * * returning `unhandled` keeps the same Behavior and signals that the message was not yet handled + * + * By default, partial function is empty and does not handle any signals. + */ + @throws(classOf[Exception]) + def onSignal: PartialFunction[Signal, Behavior[T]] = PartialFunction.empty } /** @@ -227,7 +124,7 @@ object Actor { * avoid the allocation overhead of recreating the current behavior where * that is not necessary. */ - def Same[T]: Behavior[T] = sameBehavior.asInstanceOf[Behavior[T]] + def same[T]: Behavior[T] = Behavior.same /** * Return this behavior from message processing in order to advise the @@ -235,32 +132,39 @@ object Actor { * message has not been handled. This hint may be used by composite * behaviors that delegate (partial) handling to other behaviors. */ - def Unhandled[T]: Behavior[T] = unhandledBehavior.asInstanceOf[Behavior[T]] - - /* - * TODO write a Behavior that waits for all child actors to stop and then - * runs some cleanup before stopping. The factory for this behavior should - * stop and watch all children to get the process started. - */ + def unhandled[T]: Behavior[T] = Behavior.unhandled /** * Return this behavior from message processing to signal that this actor * shall terminate voluntarily. If this actor has created child actors then - * these will be stopped as part of the shutdown procedure. The PostStop - * signal that results from stopping this actor will NOT be passed to the - * current behavior, it will be effectively ignored. + * these will be stopped as part of the shutdown procedure. + * + * The PostStop signal that results from stopping this actor will be passed to the + * current behavior. All other messages and signals will effectively be + * ignored. */ - def Stopped[T]: Behavior[T] = stoppedBehavior.asInstanceOf[Behavior[T]] + def stopped[T]: Behavior[T] = Behavior.stopped + + /** + * Return this behavior from message processing to signal that this actor + * shall terminate voluntarily. If this actor has created child actors then + * these will be stopped as part of the shutdown procedure. + * + * The PostStop signal that results from stopping this actor will be passed to the + * given `postStop` behavior. All other messages and signals will effectively be + * ignored. + */ + def stopped[T](postStop: Behavior[T]): Behavior[T] = Behavior.stopped(postStop) /** * A behavior that treats every incoming message as unhandled. */ - def Empty[T]: Behavior[T] = emptyBehavior.asInstanceOf[Behavior[T]] + def empty[T]: Behavior[T] = Behavior.empty /** * A behavior that ignores every incoming message and returns “same”. */ - def Ignore[T]: Behavior[T] = ignoreBehavior.asInstanceOf[Behavior[T]] + def ignore[T]: Behavior[T] = Behavior.ignore /** * Construct an actor behavior that can react to both incoming messages and @@ -269,36 +173,20 @@ object Actor { * [[ActorContext]] that allows access to the system, spawning and watching * other actors, etc. * - * This constructor is called stateful because processing the next message + * This constructor is called immutable because the behavior instance doesn't + * have or close over any mutable state. Processing the next message * results in a new behavior that can potentially be different from this one. + * State is updated by returning a new behavior that holds the new immutable + * state. */ - final case class Stateful[T]( - behavior: (ActorContext[T], T) ⇒ Behavior[T], - signal: (ActorContext[T], Signal) ⇒ Behavior[T] = Behavior.unhandledSignal.asInstanceOf[(ActorContext[T], Signal) ⇒ Behavior[T]]) - extends Behavior[T] { - override def management(ctx: AC[T], msg: Signal): Behavior[T] = signal(ctx, msg) - override def message(ctx: AC[T], msg: T) = behavior(ctx, msg) - override def toString = s"Stateful(${LineNumbers(behavior)})" - } + def immutable[T](onMessage: (ActorContext[T], T) ⇒ Behavior[T]): Immutable[T] = + new Immutable(onMessage) - /** - * Construct an actor behavior that can react to incoming messages but not to - * lifecycle signals. After spawning this actor from another actor (or as the - * guardian of an [[akka.typed.ActorSystem]]) it will be executed within an - * [[ActorContext]] that allows access to the system, spawning and watching - * other actors, etc. - * - * This constructor is called stateless because it cannot be replaced by - * another one after it has been installed. It is most useful for leaf actors - * that do not create child actors themselves. - */ - final case class Stateless[T](behavior: (ActorContext[T], T) ⇒ Any) extends Behavior[T] { - override def management(ctx: AC[T], msg: Signal): Behavior[T] = Unhandled - override def message(ctx: AC[T], msg: T): Behavior[T] = { - behavior(ctx, msg) - this - } - override def toString = s"Static(${LineNumbers(behavior)})" + final class Immutable[T](onMessage: (ActorContext[T], T) ⇒ Behavior[T]) + extends BehaviorImpl.ImmutableBehavior[T](onMessage) { + + def onSignal(onSignal: PartialFunction[(ActorContext[T], Signal), Behavior[T]]): Behavior[T] = + new BehaviorImpl.ImmutableBehavior(onMessage, onSignal) } /** @@ -306,35 +194,20 @@ object Actor { * some action upon each received message or signal. It is most commonly used * for logging or tracing what a certain Actor does. */ - final case class Tap[T]( - signal: (ActorContext[T], Signal) ⇒ _, - mesg: (ActorContext[T], T) ⇒ _, - behavior: Behavior[T]) extends Behavior[T] { - private def canonical(behv: Behavior[T]): Behavior[T] = - if (isUnhandled(behv)) Unhandled - else if (behv eq sameBehavior) Same - else if (isAlive(behv)) Tap(signal, mesg, behv) - else Stopped - override def management(ctx: AC[T], msg: Signal): Behavior[T] = { - signal(ctx, msg) - canonical(behavior.management(ctx, msg)) - } - override def message(ctx: AC[T], msg: T): Behavior[T] = { - mesg(ctx, msg) - canonical(behavior.message(ctx, msg)) - } - override def toString = s"Tap(${LineNumbers(signal)},${LineNumbers(mesg)},$behavior)" - } + def tap[T]( + onMessage: Function2[ActorContext[T], T, _], + onSignal: Function2[ActorContext[T], Signal, _], // FIXME use partial function here also? + behavior: Behavior[T]): Behavior[T] = + BehaviorImpl.tap(onMessage, onSignal, behavior) /** * Behavior decorator that copies all received message to the designated * monitor [[akka.typed.ActorRef]] before invoking the wrapped behavior. The - * wrapped behavior can evolve (i.e. be stateful) without needing to be + * wrapped behavior can evolve (i.e. return different behavior) without needing to be * wrapped in a `monitor` call again. */ - object Monitor { - def apply[T](monitor: ActorRef[T], behavior: Behavior[T]): Tap[T] = Tap(unitFunction, (_, msg) ⇒ monitor ! msg, behavior) - } + def monitor[T](monitor: ActorRef[T], behavior: Behavior[T]): Behavior[T] = + tap((_, msg) ⇒ monitor ! msg, unitFunction, behavior) /** * Wrap the given behavior such that it is restarted (i.e. reset to its @@ -342,48 +215,32 @@ object Actor { * subclass thereof. Exceptions that are not subtypes of `Thr` will not be * caught and thus lead to the termination of the actor. * - * It is possible to specify that the actor shall not be restarted but - * resumed. This entails keeping the same state as before the exception was - * thrown and is thus less safe. If you use `OnFailure.RESUME` you should at - * least not hold mutable data fields or collections within the actor as those - * might be in an inconsistent state (the exception might have interrupted - * normal processing); avoiding mutable state is possible by returning a fresh - * behavior with the new state after every message. + * It is possible to specify different supervisor strategies, such as restart, + * resume, backoff. * * Example: * {{{ * val dbConnector: Behavior[DbCommand] = ... - * val dbRestarts = Restarter[DbException].wrap(dbConnector) + * val dbRestarts = Restarter[DbException]().wrap(dbConnector) * }}} */ - object Restarter { - class Apply[Thr <: Throwable](c: ClassTag[Thr], resume: Boolean) { - def wrap[T](b: Behavior[T]): Behavior[T] = patterns.Restarter(Behavior.validateAsInitial(b), resume)()(c) - def mutableWrap[T](b: Behavior[T]): Behavior[T] = patterns.MutableRestarter(Behavior.validateAsInitial(b), resume)(c) - } + def restarter[Thr <: Throwable: ClassTag](strategy: SupervisorStrategy = SupervisorStrategy.restart): Restarter[Thr] = + new Restarter(implicitly, strategy) - def apply[Thr <: Throwable: ClassTag](resume: Boolean = false): Apply[Thr] = new Apply(implicitly, resume) + final class Restarter[Thr <: Throwable: ClassTag](c: ClassTag[Thr], strategy: SupervisorStrategy) { + def wrap[T](b: Behavior[T]): Behavior[T] = akka.typed.internal.Restarter(Behavior.validateAsInitial(b), strategy)(c) } + /** + * Support for scheduled `self` messages in an actor. + * It takes care of the lifecycle of the timers such as cancelling them when the actor + * is restarted or stopped. + * @see [[TimerScheduler]] + */ + def withTimers[T](factory: TimerScheduler[T] ⇒ Behavior[T]): Behavior[T] = + TimerSchedulerImpl.withTimers(factory) + // TODO // final case class Selective[T](timeout: FiniteDuration, selector: PartialFunction[T, Behavior[T]], onTimeout: () ⇒ Behavior[T]) - /** - * INTERNAL API. - */ - private[akka] val _unhandledFunction = (_: Any) ⇒ Unhandled[Nothing] - /** - * INTERNAL API. - */ - private[akka] def unhandledFunction[T, U] = _unhandledFunction.asInstanceOf[(T ⇒ Behavior[U])] - - /** - * INTERNAL API. - */ - private[akka] val _unitFunction = (_: ActorContext[Any], _: Any) ⇒ () - /** - * INTERNAL API. - */ - private[akka] def unitFunction[T] = _unitFunction.asInstanceOf[((ActorContext[T], Signal) ⇒ Unit)] - } diff --git a/akka-typed/src/main/scala/akka/typed/scaladsl/ActorContext.scala b/akka-typed/src/main/scala/akka/typed/scaladsl/ActorContext.scala new file mode 100644 index 0000000000..d4a7f13d5b --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/scaladsl/ActorContext.scala @@ -0,0 +1,161 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.scaladsl + +import scala.concurrent.ExecutionContextExecutor +import scala.concurrent.duration.FiniteDuration + +import akka.annotation.ApiMayChange +import akka.annotation.DoNotInherit +import akka.typed.ActorRef +import akka.typed.ActorSystem +import akka.typed.Behavior +import akka.typed.Props +import akka.typed.EmptyProps + +/** + * An Actor is given by the combination of a [[Behavior]] and a context in + * which this behavior is executed. As per the Actor Model an Actor can perform + * the following actions when processing a message: + * + * - send a finite number of messages to other Actors it knows + * - create a finite number of Actors + * - designate the behavior for the next message + * + * In Akka the first capability is accessed by using the `!` or `tell` method + * on an [[ActorRef]], the second is provided by [[ActorContext#spawn]] + * and the third is implicit in the signature of [[Behavior]] in that the next + * behavior is always returned from the message processing logic. + * + * An `ActorContext` in addition provides access to the Actor’s own identity (“`self`”), + * the [[ActorSystem]] it is part of, methods for querying the list of child Actors it + * created, access to [[Terminated DeathWatch]] and timed message scheduling. + */ +@DoNotInherit +@ApiMayChange +trait ActorContext[T] { this: akka.typed.javadsl.ActorContext[T] ⇒ + + /** + * Get the `javadsl` of this `ActorContext`. + */ + def asJava: akka.typed.javadsl.ActorContext[T] + + /** + * The identity of this Actor, bound to the lifecycle of this Actor instance. + * An Actor with the same name that lives before or after this instance will + * have a different [[ActorRef]]. + */ + def self: ActorRef[T] + + /** + * Return the mailbox capacity that was configured by the parent for this actor. + */ + def mailboxCapacity: Int + + /** + * The [[ActorSystem]] to which this Actor belongs. + */ + def system: ActorSystem[Nothing] + + /** + * The list of child Actors created by this Actor during its lifetime that + * are still alive, in no particular order. + */ + def children: Iterable[ActorRef[Nothing]] + + /** + * The named child Actor if it is alive. + */ + def child(name: String): Option[ActorRef[Nothing]] + + /** + * Create a child Actor from the given [[akka.typed.Behavior]] under a randomly chosen name. + * It is good practice to name Actors wherever practical. + */ + def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty): ActorRef[U] + + /** + * Create a child Actor from the given [[akka.typed.Behavior]] and with the given name. + */ + def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] + + /** + * Force the child Actor under the given name to terminate after it finishes + * processing its current message. Nothing happens if the ActorRef does not + * refer to a current child actor. + * + * @return whether the passed-in [[ActorRef]] points to a current child Actor + */ + def stop[U](child: ActorRef[U]): Boolean + + /** + * Register for [[Terminated]] notification once the Actor identified by the + * given [[ActorRef]] terminates. This message is also sent when the watched actor + * is on a node that has been removed from the cluster when using akka-cluster + * or has been marked unreachable when using akka-remote directly + */ + def watch[U](other: ActorRef[U]): Unit + + /** + * Register for termination notification with a custom message once the Actor identified by the + * given [[ActorRef]] terminates. This message is also sent when the watched actor + * is on a node that has been removed from the cluster when using akka-cluster + * or has been marked unreachable when using akka-remote directly. + */ + def watchWith[U](other: ActorRef[U], msg: T): Unit + + /** + * Revoke the registration established by `watch`. A [[Terminated]] + * notification will not subsequently be received for the referenced Actor. + */ + def unwatch[U](other: ActorRef[U]): Unit + + /** + * Schedule the sending of a notification in case no other + * message is received during the given period of time. The timeout starts anew + * with each received message. Provide `Duration.Undefined` to switch off this + * mechanism. + */ + def setReceiveTimeout(d: FiniteDuration, msg: T): Unit + + /** + * Cancel the sending of receive timeout notifications. + */ + def cancelReceiveTimeout(): Unit + + /** + * Schedule the sending of the given message to the given target Actor after + * the given time period has elapsed. The scheduled action can be cancelled + * by invoking [[akka.actor.Cancellable#cancel]] on the returned + * handle. + */ + def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): akka.actor.Cancellable + + /** + * This Actor’s execution context. It can be used to run asynchronous tasks + * like [[scala.concurrent.Future]] combinators. + */ + implicit def executionContext: ExecutionContextExecutor + + /** + * Create a child actor that will wrap messages such that other Actor’s + * protocols can be ingested by this Actor. You are strongly advised to cache + * these ActorRefs or to stop them when no longer needed. + * + * The name of the child actor will be composed of a unique identifier + * starting with a dollar sign to which the given `name` argument is + * appended, with an inserted hyphen between these two parts. Therefore + * the given `name` argument does not need to be unique within the scope + * of the parent actor. + */ + def spawnAdapter[U](f: U ⇒ T, name: String): ActorRef[U] + + /** + * Create an anonymous child actor that will wrap messages such that other Actor’s + * protocols can be ingested by this Actor. You are strongly advised to cache + * these ActorRefs or to stop them when no longer needed. + */ + def spawnAdapter[U](f: U ⇒ T): ActorRef[U] + +} diff --git a/akka-typed/src/main/scala/akka/typed/scaladsl/Ask.scala b/akka-typed/src/main/scala/akka/typed/scaladsl/AskPattern.scala similarity index 87% rename from akka-typed/src/main/scala/akka/typed/scaladsl/Ask.scala rename to akka-typed/src/main/scala/akka/typed/scaladsl/AskPattern.scala index e45b31ce8d..df49e223b2 100644 --- a/akka-typed/src/main/scala/akka/typed/scaladsl/Ask.scala +++ b/akka-typed/src/main/scala/akka/typed/scaladsl/AskPattern.scala @@ -13,7 +13,7 @@ import akka.typed.internal.FunctionRef import akka.actor.RootActorPath import akka.actor.Address import akka.typed.ActorRef -import akka.typed.adapter +import akka.typed.internal.{ adapter ⇒ adapt } /** * The ask-pattern implements the initiator side of a request–reply protocol. @@ -38,9 +38,9 @@ object AskPattern { implicit class Askable[T](val ref: ActorRef[T]) extends AnyVal { def ?[U](f: ActorRef[U] ⇒ T)(implicit timeout: Timeout, scheduler: Scheduler): Future[U] = ref match { - case a: adapter.ActorRefAdapter[_] ⇒ askUntyped(ref, a.untyped, timeout, f) - case a: adapter.ActorSystemAdapter[_] ⇒ askUntyped(ref, a.untyped.guardian, timeout, f) - case _ ⇒ ask(ref, timeout, scheduler, f) + case a: adapt.ActorRefAdapter[_] ⇒ askUntyped(ref, a.untyped, timeout, f) + case a: adapt.ActorSystemAdapter[_] ⇒ askUntyped(ref, a.untyped.guardian, timeout, f) + case _ ⇒ ask(ref, timeout, scheduler, f) } } @@ -50,15 +50,15 @@ object AskPattern { private[this] val (_ref: ActorRef[U], _future: Future[U], _promiseRef) = if (untyped.isTerminated) ( - adapter.ActorRefAdapter[U](untyped.provider.deadLetters), + adapt.ActorRefAdapter[U](untyped.provider.deadLetters), Future.failed[U](new AskTimeoutException(s"Recipient[$target] had already been terminated.")), null) else if (timeout.duration.length <= 0) ( - adapter.ActorRefAdapter[U](untyped.provider.deadLetters), + adapt.ActorRefAdapter[U](untyped.provider.deadLetters), Future.failed[U](new IllegalArgumentException(s"Timeout length must be positive, question not sent to [$target]")), null) else { val a = PromiseActorRef(untyped.provider, timeout, target, "unknown") - val b = adapter.ActorRefAdapter[U](a) + val b = adapt.ActorRefAdapter[U](a) (b, a.result.future.asInstanceOf[Future[U]], a) } diff --git a/akka-typed/src/main/scala/akka/typed/scaladsl/TimerScheduler.scala b/akka-typed/src/main/scala/akka/typed/scaladsl/TimerScheduler.scala new file mode 100644 index 0000000000..9189d66b59 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/scaladsl/TimerScheduler.scala @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.scaladsl + +import scala.concurrent.duration.FiniteDuration + +/** + * Support for scheduled `self` messages in an actor. + * It is used with `Actor.withTimers`. + * Timers are bound to the lifecycle of the actor that owns it, + * and thus are cancelled automatically when it is restarted or stopped. + * + * `TimerScheduler` is not thread-safe, i.e. it must only be used within + * the actor that owns it. + */ +trait TimerScheduler[T] { + + /** + * Start a periodic timer that will send `msg` to the `self` actor at + * a fixed `interval`. + * + * Each timer has a key and if a new timer with same key is started + * the previous is cancelled and it's guaranteed that a message from the + * previous timer is not received, even though it might already be enqueued + * in the mailbox when the new timer is started. + */ + def startPeriodicTimer(key: Any, msg: T, interval: FiniteDuration): Unit + + /** + * Start a timer that will send `msg` once to the `self` actor after + * the given `timeout`. + * + * Each timer has a key and if a new timer with same key is started + * the previous is cancelled and it's guaranteed that a message from the + * previous timer is not received, even though it might already be enqueued + * in the mailbox when the new timer is started. + */ + def startSingleTimer(key: Any, msg: T, timeout: FiniteDuration): Unit + + /** + * Check if a timer with a given `key` is active. + */ + def isTimerActive(key: Any): Boolean + + /** + * Cancel a timer with a given `key`. + * If canceling a timer that was already canceled, or key never was used to start a timer + * this operation will do nothing. + * + * It is guaranteed that a message from a canceled timer, including its previous incarnation + * for the same key, will not be received by the actor, even though the message might already + * be enqueued in the mailbox when cancel is called. + */ + def cancel(key: Any): Unit + + /** + * Cancel all timers. + */ + def cancelAll(): Unit + +} diff --git a/akka-typed/src/main/scala/akka/typed/scaladsl/adapter/PropsAdapter.scala b/akka-typed/src/main/scala/akka/typed/scaladsl/adapter/PropsAdapter.scala new file mode 100644 index 0000000000..2e0e07b5ec --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/scaladsl/adapter/PropsAdapter.scala @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.typed.scaladsl.adapter + +import akka.typed.Behavior +import akka.typed.EmptyProps +import akka.typed.Props +import akka.typed.internal.adapter.ActorAdapter + +/** + * Wrap [[akka.typed.Behavior]] in an untyped [[akka.actor.Props]], i.e. when + * spawning a typed child actor from an untyped parent actor. + * This is normally not needed because you can use the extension methods + * `spawn` and `spawnAnonymous` on an untyped `ActorContext`, but it's needed + * when using typed actors with an existing library/tool that provides an API that + * takes an untyped [[akka.actor.Props]] parameter. Cluster Sharding is an + * example of that. + */ +object PropsAdapter { + def apply[T](behavior: ⇒ Behavior[T], deploy: Props = Props.empty): akka.actor.Props = + akka.typed.internal.adapter.PropsAdapter(() ⇒ behavior, deploy) +} diff --git a/akka-typed/src/main/scala/akka/typed/scaladsl/adapter/package.scala b/akka-typed/src/main/scala/akka/typed/scaladsl/adapter/package.scala new file mode 100644 index 0000000000..73c698ff6b --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/scaladsl/adapter/package.scala @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2016-2017 Lightbend Inc. + */ +package akka.typed +package scaladsl + +import akka.annotation.InternalApi +import akka.typed.internal.adapter._ + +/** + * Scala API: Adapters between typed and untyped actors and actor systems. + * The underlying `ActorSystem` is the untyped [[akka.actor.ActorSystem]] + * which runs Akka Typed [[akka.typed.Behavior]] on an emulation layer. In this + * system typed and untyped actors can coexist. + * + * Use these adapters with `import akka.typed.scaladsl.adapter._`. + * + * Implicit extension methods are added to untyped and typed `ActorSystem`, + * `ActorContext`. Such methods make it possible to create typed child actor + * from untyped parent actor, and the opposite untyped child from typed parent. + * `watch` is also supported in both directions. + * + * There is an implicit conversion from untyped [[akka.actor.ActorRef]] to + * typed [[akka.typed.ActorRef]]. + * + * There are also converters (`toTyped`, `toUntyped`) from typed + * [[akka.typed.ActorRef]] to untyped [[akka.actor.ActorRef]], and between untyped + * [[akka.actor.ActorSystem]] and typed [[akka.typed.ActorSystem]]. + */ +package object adapter { + + import language.implicitConversions + + /** + * Extension methods added to [[akka.actor.ActorSystem]]. + */ + implicit class UntypedActorSystemOps(val sys: akka.actor.ActorSystem) extends AnyVal { + def spawnAnonymous[T](behavior: Behavior[T], props: Props = Props.empty): ActorRef[T] = + ActorRefAdapter(sys.actorOf(PropsAdapter(Behavior.validateAsInitial(behavior), props))) + def spawn[T](behavior: Behavior[T], name: String, props: Props = Props.empty): ActorRef[T] = + ActorRefAdapter(sys.actorOf(PropsAdapter(Behavior.validateAsInitial(behavior), props), name)) + + def toTyped: ActorSystem[Nothing] = ActorSystemAdapter(sys) + } + + /** + * Extension methods added to [[akka.typed.ActorSystem]]. + */ + implicit class TypedActorSystemOps(val sys: ActorSystem[_]) extends AnyVal { + def toUntyped: akka.actor.ActorSystem = ActorSystemAdapter.toUntyped(sys) + } + + /** + * Extension methods added to [[akka.actor.ActorContext]]. + */ + implicit class UntypedActorContextOps(val ctx: akka.actor.ActorContext) extends AnyVal { + def spawnAnonymous[T](behavior: Behavior[T], props: Props = Props.empty): ActorRef[T] = + ActorContextAdapter.spawnAnonymous(ctx, behavior, props) + def spawn[T](behavior: Behavior[T], name: String, props: Props = Props.empty): ActorRef[T] = + ActorContextAdapter.spawn(ctx, behavior, name, props) + + def watch[U](other: ActorRef[U]): Unit = ctx.watch(ActorRefAdapter.toUntyped(other)) + def unwatch[U](other: ActorRef[U]): Unit = ctx.unwatch(ActorRefAdapter.toUntyped(other)) + + def stop(child: ActorRef[_]): Unit = + ctx.stop(ActorRefAdapter.toUntyped(child)) + } + + /** + * Extension methods added to [[akka.typed.scaladsl.ActorContext]]. + */ + implicit class TypedActorContextOps(val ctx: scaladsl.ActorContext[_]) extends AnyVal { + def actorOf(props: akka.actor.Props): akka.actor.ActorRef = + ActorContextAdapter.toUntyped(ctx).actorOf(props) + def actorOf(props: akka.actor.Props, name: String): akka.actor.ActorRef = + ActorContextAdapter.toUntyped(ctx).actorOf(props, name) + + // watch, unwatch and stop not needed here because of the implicit ActorRef conversion + } + + /** + * Extension methods added to [[akka.typed.ActorRef]]. + */ + implicit class TypedActorRefOps(val ref: ActorRef[_]) extends AnyVal { + def toUntyped: akka.actor.ActorRef = ActorRefAdapter.toUntyped(ref) + } + + /** + * Implicit conversion from untyped [[akka.actor.ActorRef]] to typed [[akka.typed.ActorRef]]. + */ + implicit def actorRefAdapter[T](ref: akka.actor.ActorRef): ActorRef[T] = ActorRefAdapter(ref) + +} diff --git a/akka-typed/src/test/java/akka/typed/javadsl/ActorCompile.java b/akka-typed/src/test/java/akka/typed/javadsl/ActorCompile.java deleted file mode 100644 index f59dc63da1..0000000000 --- a/akka-typed/src/test/java/akka/typed/javadsl/ActorCompile.java +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (C) 2017 Lightbend Inc. - */ -package akka.typed.javadsl; - -import akka.typed.*; -import static akka.typed.javadsl.Actor.*; - -public class ActorCompile { - - interface MyMsg {} - - class MyMsgA implements MyMsg { - final ActorRef replyTo; - - public MyMsgA(ActorRef replyTo) { - this.replyTo = replyTo; - } - } - - class MyMsgB implements MyMsg { - final String greeting; - - public MyMsgB(String greeting) { - this.greeting = greeting; - } - } - - Behavior actor1 = stateful((ctx, msg) -> stopped(), (ctx, signal) -> same()); - Behavior actor2 = stateful((ctx, msg) -> unhandled()); - Behavior actor3 = stateless((ctx, msg) -> {}); - Behavior actor4 = empty(); - Behavior actor5 = ignore(); - Behavior actor6 = tap((ctx, signal) -> {}, (ctx, msg) -> {}, actor5); - Behavior actor7 = actor6.narrow(); - Behavior actor8 = deferred(ctx -> { - final ActorRef self = ctx.getSelf(); - return monitor(self, ignore()); - }); - Behavior actor9 = widened(actor7, pf -> pf.match(MyMsgA.class, x -> x)); - - { - Actor.stateful((ctx, msg) -> { - if (msg instanceof MyMsgA) { - return stateless((ctx2, msg2) -> { - if (msg2 instanceof MyMsgB) { - ((MyMsgA) msg).replyTo.tell(((MyMsgB) msg2).greeting); - } - }); - } else return unhandled(); - }); - } -} diff --git a/akka-typed/src/test/scala/akka/typed/BehaviorSpec.scala b/akka-typed/src/test/scala/akka/typed/BehaviorSpec.scala deleted file mode 100644 index f371c9461b..0000000000 --- a/akka-typed/src/test/scala/akka/typed/BehaviorSpec.scala +++ /dev/null @@ -1,841 +0,0 @@ -/** - * Copyright (C) 2014-2017 Lightbend Inc. - */ -package akka.typed - -import akka.typed.scaladsl.{ Actor ⇒ SActor } -import akka.typed.javadsl.{ Actor ⇒ JActor, ActorContext ⇒ JActorContext } -import akka.japi.function.{ Function ⇒ F1e, Function2 ⇒ F2, Procedure2 ⇒ P2 } -import akka.japi.pf.{ FI, PFBuilder } -import java.util.function.{ Function ⇒ F1 } - -class BehaviorSpec extends TypedSpec { - - sealed trait Command { - def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Nil - } - case object GetSelf extends Command { - override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Self(ctx.self) :: Nil - } - // Behavior under test must return Unhandled - case object Miss extends Command { - override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Missed :: Nil - } - // Behavior under test must return Same - case object Ignore extends Command { - override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Ignored :: Nil - } - case object Ping extends Command { - override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Pong :: Nil - } - case object Swap extends Command { - override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Swapped :: Nil - } - case class GetState()(s: State) extends Command { - override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = s :: Nil - } - case class AuxPing(id: Int) extends Command { - override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Pong :: Nil - } - case object Stop extends Command - - sealed trait Event - case class GotSignal(signal: Signal) extends Event - case class Self(self: ActorRef[Command]) extends Event - case object Missed extends Event - case object Ignored extends Event - case object Pong extends Event - case object Swapped extends Event - - sealed trait State extends Event { def next: State } - val StateA: State = new State { override def toString = "StateA"; override def next = StateB } - val StateB: State = new State { override def toString = "StateB"; override def next = StateA } - - trait Common { - type Aux >: Null <: AnyRef - def system: ActorSystem[TypedSpec.Command] - def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) - def checkAux(signal: Signal, aux: Aux): Unit = () - def checkAux(command: Command, aux: Aux): Unit = () - - case class Init(behv: Behavior[Command], inbox: Inbox[Event], aux: Aux) { - def mkCtx(requirePreStart: Boolean = false): Setup = { - val ctx = new EffectfulActorContext("ctx", behv, 1000, system) - val msgs = inbox.receiveAll() - if (requirePreStart) msgs should ===(GotSignal(PreStart) :: Nil) - checkAux(PreStart, aux) - Setup(ctx, inbox, aux) - } - } - case class Setup(ctx: EffectfulActorContext[Command], inbox: Inbox[Event], aux: Aux) - - def init(): Init = { - val inbox = Inbox[Event]("evt") - val (behv, aux) = behavior(inbox.ref) - Init(behv, inbox, aux) - } - - def init(factory: ActorRef[Event] ⇒ (Behavior[Command], Aux)): Init = { - val inbox = Inbox[Event]("evt") - val (behv, aux) = factory(inbox.ref) - Init(behv, inbox, aux) - } - - def mkCtx(requirePreStart: Boolean = false): Setup = - init().mkCtx(requirePreStart) - - implicit class Check(val setup: Setup) { - def check(signal: Signal): Setup = { - setup.ctx.signal(signal) - setup.inbox.receiveAll() should ===(GotSignal(signal) :: Nil) - checkAux(signal, setup.aux) - setup - } - def check(command: Command): Setup = { - setup.ctx.run(command) - setup.inbox.receiveAll() should ===(command.expectedResponse(setup.ctx)) - checkAux(command, setup.aux) - setup - } - def check2(command: Command): Setup = { - setup.ctx.run(command) - val expected = command.expectedResponse(setup.ctx) - setup.inbox.receiveAll() should ===(expected ++ expected) - checkAux(command, setup.aux) - setup - } - } - - val ex = new Exception("mine!") - } - - trait Siphon extends Common { - override type Aux = Inbox[Command] - - override def checkAux(command: Command, aux: Aux): Unit = { - aux.receiveAll() should ===(command :: Nil) - } - } - - trait SignalSiphon extends Common { - override type Aux = Inbox[Either[Signal, Command]] - - override def checkAux(command: Command, aux: Aux): Unit = { - aux.receiveAll() should ===(Right(command) :: Nil) - } - - override def checkAux(signal: Signal, aux: Aux): Unit = { - aux.receiveAll() should ===(Left(signal) :: Nil) - } - } - - trait Lifecycle extends Common { - def `must react to PreStart`(): Unit = { - mkCtx(requirePreStart = true) - } - - def `must react to PostStop`(): Unit = { - mkCtx().check(PostStop) - } - - def `must react to PostStop after a message`(): Unit = { - mkCtx().check(GetSelf).check(PostStop) - } - - def `must react to PreRestart`(): Unit = { - mkCtx().check(PreRestart) - } - - def `must react to PreRestart after a message`(): Unit = { - mkCtx().check(GetSelf).check(PreRestart) - } - - def `must react to Terminated`(): Unit = { - mkCtx().check(Terminated(Inbox("x").ref)(null)) - } - - def `must react to Terminated after a message`(): Unit = { - mkCtx().check(GetSelf).check(Terminated(Inbox("x").ref)(null)) - } - - def `must react to a message after Terminated`(): Unit = { - mkCtx().check(Terminated(Inbox("x").ref)(null)).check(GetSelf) - } - } - - trait Messages extends Common { - def `must react to two messages`(): Unit = { - mkCtx().check(Ping).check(Ping) - } - - def `must react to a message after missing one`(): Unit = { - mkCtx().check(Miss).check(Ping) - } - - def `must react to a message after ignoring one`(): Unit = { - mkCtx().check(Ignore).check(Ping) - } - } - - trait Unhandled extends Common { - def `must return Unhandled`(): Unit = { - val Setup(ctx, inbox, aux) = mkCtx() - ctx.currentBehavior.message(ctx, Miss) should be(Behavior.unhandledBehavior) - inbox.receiveAll() should ===(Missed :: Nil) - checkAux(Miss, aux) - } - } - - trait Stoppable extends Common { - def `must stop`(): Unit = { - val Setup(ctx, inbox, aux) = mkCtx() - ctx.run(Stop) - ctx.currentBehavior should be(Behavior.stoppedBehavior) - checkAux(Stop, aux) - } - } - - trait Become extends Common with Unhandled { - private implicit val inbox = Inbox[State]("state") - - def `must be in state A`(): Unit = { - mkCtx().check(GetState()(StateA)) - } - - def `must switch to state B`(): Unit = { - mkCtx().check(Swap).check(GetState()(StateB)) - } - - def `must switch back to state A`(): Unit = { - mkCtx().check(Swap).check(Swap).check(GetState()(StateA)) - } - } - - trait BecomeWithLifecycle extends Become with Lifecycle { - def `must react to PostStop after swap`(): Unit = { - mkCtx().check(Swap).check(PostStop) - } - - def `must react to PostStop after a message after swap`(): Unit = { - mkCtx().check(Swap).check(GetSelf).check(PostStop) - } - - def `must react to PreRestart after swap`(): Unit = { - mkCtx().check(Swap).check(PreRestart) - } - - def `must react to PreRestart after a message after swap`(): Unit = { - mkCtx().check(Swap).check(GetSelf).check(PreRestart) - } - - def `must react to Terminated after swap`(): Unit = { - mkCtx().check(Swap).check(Terminated(Inbox("x").ref)(null)) - } - - def `must react to Terminated after a message after swap`(): Unit = { - mkCtx().check(Swap).check(GetSelf).check(Terminated(Inbox("x").ref)(null)) - } - - def `must react to a message after Terminated after swap`(): Unit = { - mkCtx().check(Swap).check(Terminated(Inbox("x").ref)(null)).check(GetSelf) - } - } - - /** - * This targets behavior wrappers to ensure that the wrapper does not - * hold on to the changed behavior. Wrappers must be immutable. - */ - trait Reuse extends Common { - def `must be reusable`(): Unit = { - val i = init() - i.mkCtx().check(GetState()(StateA)).check(Swap).check(GetState()(StateB)) - i.mkCtx().check(GetState()(StateA)).check(Swap).check(GetState()(StateB)) - } - } - - private def mkFull(monitor: ActorRef[Event], state: State = StateA): Behavior[Command] = { - import ScalaDSL.{ Full, Msg, Sig, Same, Unhandled, Stopped } - Full { - case Sig(ctx, signal) ⇒ - monitor ! GotSignal(signal) - Same - case Msg(ctx, GetSelf) ⇒ - monitor ! Self(ctx.self) - Same - case Msg(ctx, Miss) ⇒ - monitor ! Missed - Unhandled - case Msg(ctx, Ignore) ⇒ - monitor ! Ignored - Same - case Msg(ctx, Ping) ⇒ - monitor ! Pong - mkFull(monitor, state) - case Msg(ctx, Swap) ⇒ - monitor ! Swapped - mkFull(monitor, state.next) - case Msg(ctx, GetState()) ⇒ - monitor ! state - Same - case Msg(ctx, Stop) ⇒ Stopped - } - } - - trait FullBehavior extends Messages with BecomeWithLifecycle with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = mkFull(monitor) → null - } - object `A Full Behavior (native)` extends FullBehavior with NativeSystem - object `A Full Behavior (adapted)` extends FullBehavior with AdaptedSystem - - trait FullTotalBehavior extends Messages with BecomeWithLifecycle with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor, StateA) → null - private def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = { - import ScalaDSL.{ FullTotal, Msg, Sig, Same, Unhandled, Stopped } - FullTotal { - case Sig(ctx, signal) ⇒ - monitor ! GotSignal(signal) - Same - case Msg(ctx, GetSelf) ⇒ - monitor ! Self(ctx.self) - Same - case Msg(_, Miss) ⇒ - monitor ! Missed - Unhandled - case Msg(_, Ignore) ⇒ - monitor ! Ignored - Same - case Msg(_, Ping) ⇒ - monitor ! Pong - behv(monitor, state) - case Msg(_, Swap) ⇒ - monitor ! Swapped - behv(monitor, state.next) - case Msg(_, GetState()) ⇒ - monitor ! state - Same - case Msg(_, Stop) ⇒ Stopped - case Msg(_, _: AuxPing) ⇒ Unhandled - } - } - } - object `A FullTotal Behavior (native)` extends FullTotalBehavior with NativeSystem - object `A FullTotal Behavior (adapted)` extends FullTotalBehavior with AdaptedSystem - - trait WidenedBehavior extends Messages with BecomeWithLifecycle with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - (ScalaDSL.Widened(mkFull(monitor), { case x ⇒ x }), null) - } - object `A Widened Behavior (native)` extends WidenedBehavior with NativeSystem - object `A Widened Behavior (adapted)` extends WidenedBehavior with AdaptedSystem - - trait ContextAwareBehavior extends Messages with BecomeWithLifecycle with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - (ScalaDSL.ContextAware(ctx ⇒ mkFull(monitor)), null) - } - object `A ContextAware Behavior (native)` extends ContextAwareBehavior with NativeSystem - object `A ContextAware Behavior (adapted)` extends ContextAwareBehavior with AdaptedSystem - - trait SelfAwareBehavior extends Messages with BecomeWithLifecycle with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - (ScalaDSL.SelfAware(self ⇒ mkFull(monitor)), null) - } - object `A SelfAware Behavior (native)` extends SelfAwareBehavior with NativeSystem - object `A SelfAware Behavior (adapted)` extends SelfAwareBehavior with AdaptedSystem - - trait NonMatchingTapBehavior extends Messages with BecomeWithLifecycle with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - (ScalaDSL.Tap({ case null ⇒ }, mkFull(monitor)), null) - } - object `A non-matching Tap Behavior (native)` extends NonMatchingTapBehavior with NativeSystem - object `A non-matching Tap Behavior (adapted)` extends NonMatchingTapBehavior with AdaptedSystem - - trait MatchingTapBehavior extends Messages with BecomeWithLifecycle with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - (ScalaDSL.Tap({ case _ ⇒ }, mkFull(monitor)), null) - } - object `A matching Tap Behavior (native)` extends MatchingTapBehavior with NativeSystem - object `A matching Tap Behavior (adapted)` extends MatchingTapBehavior with AdaptedSystem - - trait SynchronousSelfBehavior extends Messages with BecomeWithLifecycle with Stoppable { - import ScalaDSL._ - - type Aux = Inbox[Command] - - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - (SynchronousSelf(self ⇒ mkFull(monitor)), null) - - private def behavior2(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Command]("syncself") - def first(self: ActorRef[Command]) = Tap.monitor(inbox.ref, Partial[Command] { - case AuxPing(id) ⇒ { self ! AuxPing(0); second(self) } - }) - def second(self: ActorRef[Command]) = Partial[Command] { - case AuxPing(0) ⇒ { self ! AuxPing(1); Same } - case AuxPing(1) ⇒ { self ! AuxPing(2); third(self) } - } - def third(self: ActorRef[Command]) = Partial[Command] { - case AuxPing(2) ⇒ { self ! AuxPing(3); Unhandled } - case AuxPing(3) ⇒ { self ! Ping; Same } - case AuxPing(4) ⇒ { self ! Stop; Stopped } - } - (SynchronousSelf(self ⇒ Or(mkFull(monitor), first(self))), inbox) - } - - override def checkAux(cmd: Command, aux: Aux) = - (cmd, aux) match { - case (AuxPing(42), i: Inbox[_]) ⇒ i.receiveAll() should ===(Seq(42, 0, 1, 2, 3) map AuxPing: Seq[Command]) - case (AuxPing(4), i: Inbox[_]) ⇒ i.receiveAll() should ===(AuxPing(4) :: Nil) - case _ ⇒ // ignore - } - - def `must send messages to itself and stop correctly`(): Unit = { - val Setup(ctx, _, _) = init(behavior2).mkCtx().check(AuxPing(42)) - ctx.run(AuxPing(4)) - ctx.currentBehavior should ===(Stopped[Command]) - } - } - object `A SynchronousSelf Behavior (native)` extends SynchronousSelfBehavior with NativeSystem - object `A SynchronousSelf Behavior (adapted)` extends SynchronousSelfBehavior with AdaptedSystem - - trait And extends Common { - - private def behavior2(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - ScalaDSL.And(mkFull(monitor), mkFull(monitor)) → null - - def `must pass message to both parts`(): Unit = { - init(behavior2).mkCtx().check2(Swap).check2(GetState()(StateB)) - } - - def `must half-terminate`(): Unit = { - val Setup(ctx, inbox, _) = mkCtx() - ctx.run(Stop) - ctx.currentBehavior should ===(ScalaDSL.Empty[Command]) - } - } - - trait BehaviorAndLeft extends Messages with BecomeWithLifecycle with And { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - ScalaDSL.And(mkFull(monitor), ScalaDSL.Empty) → null - } - object `A Behavior combined with And (left, native)` extends BehaviorAndLeft with NativeSystem - object `A Behavior combined with And (left, adapted)` extends BehaviorAndLeft with NativeSystem - - trait BehaviorAndRight extends Messages with BecomeWithLifecycle with And { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - ScalaDSL.And(ScalaDSL.Empty, mkFull(monitor)) → null - } - object `A Behavior combined with And (right, native)` extends BehaviorAndRight with NativeSystem - object `A Behavior combined with And (right, adapted)` extends BehaviorAndRight with NativeSystem - - trait Or extends Common { - private def strange(monitor: ActorRef[Event]): Behavior[Command] = - ScalaDSL.Full { - case ScalaDSL.Msg(_, Ping | AuxPing(_)) ⇒ - monitor ! Pong - ScalaDSL.Unhandled - } - - private def behavior2(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - ScalaDSL.Or(mkFull(monitor), strange(monitor)) → null - - private def behavior3(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - ScalaDSL.Or(strange(monitor), mkFull(monitor)) → null - - def `must pass message only to first interested party`(): Unit = { - init(behavior2).mkCtx().check(Ping).check(AuxPing(0)) - } - - def `must pass message through both if first is uninterested`(): Unit = { - init(behavior3).mkCtx().check2(Ping).check(AuxPing(0)) - } - - def `must half-terminate`(): Unit = { - val Setup(ctx, inbox, _) = mkCtx() - ctx.run(Stop) - ctx.currentBehavior should ===(ScalaDSL.Empty[Command]) - } - } - - trait BehaviorOrLeft extends Messages with BecomeWithLifecycle with Or { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - ScalaDSL.Or(mkFull(monitor), ScalaDSL.Empty) → null - } - object `A Behavior combined with Or (left, native)` extends BehaviorOrLeft with NativeSystem - object `A Behavior combined with Or (left, adapted)` extends BehaviorOrLeft with NativeSystem - - trait BehaviorOrRight extends Messages with BecomeWithLifecycle with Or { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - ScalaDSL.Or(ScalaDSL.Empty, mkFull(monitor)) → null - } - object `A Behavior combined with Or (right, native)` extends BehaviorOrRight with NativeSystem - object `A Behavior combined with Or (right, adapted)` extends BehaviorOrRight with NativeSystem - - trait PartialBehavior extends Messages with Become with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor, StateA) → null - def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = - ScalaDSL.Partial { - case Ping ⇒ - monitor ! Pong - behv(monitor, state) - case Miss ⇒ - monitor ! Missed - ScalaDSL.Unhandled - case Ignore ⇒ - monitor ! Ignored - ScalaDSL.Same - case Swap ⇒ - monitor ! Swapped - behv(monitor, state.next) - case GetState() ⇒ - monitor ! state - ScalaDSL.Same - case Stop ⇒ ScalaDSL.Stopped - } - } - object `A Partial Behavior (native)` extends PartialBehavior with NativeSystem - object `A Partial Behavior (adapted)` extends PartialBehavior with AdaptedSystem - - trait TotalBehavior extends Messages with Become with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor, StateA) → null - def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = - ScalaDSL.Total { - case Ping ⇒ - monitor ! Pong - behv(monitor, state) - case Miss ⇒ - monitor ! Missed - ScalaDSL.Unhandled - case Ignore ⇒ - monitor ! Ignored - ScalaDSL.Same - case GetSelf ⇒ ScalaDSL.Unhandled - case Swap ⇒ - monitor ! Swapped - behv(monitor, state.next) - case GetState() ⇒ - monitor ! state - ScalaDSL.Same - case Stop ⇒ ScalaDSL.Stopped - case _: AuxPing ⇒ ScalaDSL.Unhandled - } - } - object `A Total Behavior (native)` extends TotalBehavior with NativeSystem - object `A Total Behavior (adapted)` extends TotalBehavior with AdaptedSystem - - trait StaticBehavior extends Messages { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - (ScalaDSL.Static { - case Ping ⇒ monitor ! Pong - case Miss ⇒ monitor ! Missed - case Ignore ⇒ monitor ! Ignored - case GetSelf ⇒ - case Swap ⇒ - case GetState() ⇒ - case Stop ⇒ - case _: AuxPing ⇒ - }, null) - } - object `A Static Behavior (native)` extends StaticBehavior with NativeSystem - object `A Static Behavior (adapted)` extends StaticBehavior with AdaptedSystem - - trait StatefulWithSignalScalaBehavior extends Messages with BecomeWithLifecycle with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor) → null - def behv(monitor: ActorRef[Event], state: State = StateA): Behavior[Command] = - SActor.Stateful( - (ctx, msg) ⇒ msg match { - case GetSelf ⇒ - monitor ! Self(ctx.self) - SActor.Same - case Miss ⇒ - monitor ! Missed - SActor.Unhandled - case Ignore ⇒ - monitor ! Ignored - SActor.Same - case Ping ⇒ - monitor ! Pong - behv(monitor, state) - case Swap ⇒ - monitor ! Swapped - behv(monitor, state.next) - case GetState() ⇒ - monitor ! state - SActor.Same - case Stop ⇒ SActor.Stopped - case _: AuxPing ⇒ SActor.Unhandled - }, - (ctx, sig) ⇒ { - monitor ! GotSignal(sig) - SActor.Same - }) - } - object `A StatefulWithSignal Behavior (scala,native)` extends StatefulWithSignalScalaBehavior with NativeSystem - object `A StatefulWithSignal Behavior (scala,adapted)` extends StatefulWithSignalScalaBehavior with AdaptedSystem - - trait StatefulScalaBehavior extends Messages with Become with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor, StateA) → null - def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = - SActor.Stateful { (ctx, msg) ⇒ - msg match { - case GetSelf ⇒ - monitor ! Self(ctx.self) - SActor.Same - case Miss ⇒ - monitor ! Missed - SActor.Unhandled - case Ignore ⇒ - monitor ! Ignored - SActor.Same - case Ping ⇒ - monitor ! Pong - behv(monitor, state) - case Swap ⇒ - monitor ! Swapped - behv(monitor, state.next) - case GetState() ⇒ - monitor ! state - SActor.Same - case Stop ⇒ SActor.Stopped - case _: AuxPing ⇒ SActor.Unhandled - } - } - } - object `A Stateful Behavior (scala,native)` extends StatefulScalaBehavior with NativeSystem - object `A Stateful Behavior (scala,adapted)` extends StatefulScalaBehavior with AdaptedSystem - - trait StatelessScalaBehavior extends Messages { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - (SActor.Stateless { (ctx, msg) ⇒ - msg match { - case GetSelf ⇒ monitor ! Self(ctx.self) - case Miss ⇒ monitor ! Missed - case Ignore ⇒ monitor ! Ignored - case Ping ⇒ monitor ! Pong - case Swap ⇒ monitor ! Swapped - case GetState() ⇒ monitor ! StateA - case Stop ⇒ - case _: AuxPing ⇒ - } - }, null) - } - object `A Stateless Behavior (scala,native)` extends StatelessScalaBehavior with NativeSystem - object `A Stateless Behavior (scala,adapted)` extends StatelessScalaBehavior with AdaptedSystem - - trait WidenedScalaBehavior extends StatefulWithSignalScalaBehavior with Reuse with Siphon { - import SActor.BehaviorDecorators - - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Command]("widenedListener") - super.behavior(monitor)._1.widen[Command] { case c ⇒ inbox.ref ! c; c } → inbox - } - } - object `A widened Behavior (scala,native)` extends WidenedScalaBehavior with NativeSystem - object `A widened Behavior (scala,adapted)` extends WidenedScalaBehavior with AdaptedSystem - - trait DeferredScalaBehavior extends StatefulWithSignalScalaBehavior { - override type Aux = Inbox[PreStart] - - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[PreStart]("deferredListener") - (SActor.Deferred(ctx ⇒ { - inbox.ref ! PreStart - super.behavior(monitor)._1 - }), inbox) - } - - override def checkAux(signal: Signal, aux: Aux): Unit = - signal match { - case PreStart ⇒ aux.receiveAll() should ===(PreStart :: Nil) - case _ ⇒ aux.receiveAll() should ===(Nil) - } - } - object `A deferred Behavior (scala,native)` extends DeferredScalaBehavior with NativeSystem - object `A deferred Behavior (scala,adapted)` extends DeferredScalaBehavior with AdaptedSystem - - trait TapScalaBehavior extends StatefulWithSignalScalaBehavior with Reuse with SignalSiphon { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Either[Signal, Command]]("tapListener") - (SActor.Tap( - (_, sig) ⇒ inbox.ref ! Left(sig), - (_, msg) ⇒ inbox.ref ! Right(msg), - super.behavior(monitor)._1), inbox) - } - } - object `A tap Behavior (scala,native)` extends TapScalaBehavior with NativeSystem - object `A tap Behavior (scala,adapted)` extends TapScalaBehavior with AdaptedSystem - - trait RestarterScalaBehavior extends StatefulWithSignalScalaBehavior with Reuse { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - SActor.Restarter[Exception]().wrap(super.behavior(monitor)._1) → null - } - } - object `A restarter Behavior (scala,native)` extends RestarterScalaBehavior with NativeSystem - object `A restarter Behavior (scala,adapted)` extends RestarterScalaBehavior with AdaptedSystem - - /* - * function converters for Java, to ease the pain on Scala 2.11 - */ - def fs(f: (JActorContext[Command], Signal) ⇒ Behavior[Command]) = - new F2[JActorContext[Command], Signal, Behavior[Command]] { - override def apply(ctx: JActorContext[Command], sig: Signal) = f(ctx, sig) - } - def fc(f: (JActorContext[Command], Command) ⇒ Behavior[Command]) = - new F2[JActorContext[Command], Command, Behavior[Command]] { - override def apply(ctx: JActorContext[Command], command: Command) = f(ctx, command) - } - def ps(f: (JActorContext[Command], Signal) ⇒ Unit) = - new P2[JActorContext[Command], Signal] { - override def apply(ctx: JActorContext[Command], sig: Signal) = f(ctx, sig) - } - def pc(f: (JActorContext[Command], Command) ⇒ Unit) = - new P2[JActorContext[Command], Command] { - override def apply(ctx: JActorContext[Command], command: Command) = f(ctx, command) - } - def pf(f: PFBuilder[Command, Command] ⇒ PFBuilder[Command, Command]) = - new F1[PFBuilder[Command, Command], PFBuilder[Command, Command]] { - override def apply(in: PFBuilder[Command, Command]) = f(in) - } - def fi(f: Command ⇒ Command) = - new FI.Apply[Command, Command] { - override def apply(in: Command) = f(in) - } - def df(f: JActorContext[Command] ⇒ Behavior[Command]) = - new F1e[JActorContext[Command], Behavior[Command]] { - override def apply(in: JActorContext[Command]) = f(in) - } - - trait StatefulWithSignalJavaBehavior extends Messages with BecomeWithLifecycle with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor) → null - def behv(monitor: ActorRef[Event], state: State = StateA): Behavior[Command] = - JActor.stateful( - fc((ctx, msg) ⇒ msg match { - case GetSelf ⇒ - monitor ! Self(ctx.getSelf) - SActor.Same - case Miss ⇒ - monitor ! Missed - SActor.Unhandled - case Ignore ⇒ - monitor ! Ignored - SActor.Same - case Ping ⇒ - monitor ! Pong - behv(monitor, state) - case Swap ⇒ - monitor ! Swapped - behv(monitor, state.next) - case GetState() ⇒ - monitor ! state - SActor.Same - case Stop ⇒ SActor.Stopped - case _: AuxPing ⇒ SActor.Unhandled - }), - fs((ctx, sig) ⇒ { - monitor ! GotSignal(sig) - SActor.Same - })) - } - object `A StatefulWithSignal Behavior (java,native)` extends StatefulWithSignalJavaBehavior with NativeSystem - object `A StatefulWithSignal Behavior (java,adapted)` extends StatefulWithSignalJavaBehavior with AdaptedSystem - - trait StatefulJavaBehavior extends Messages with Become with Stoppable { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = behv(monitor, StateA) → null - def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = - JActor.stateful { - fc((ctx, msg) ⇒ - msg match { - case GetSelf ⇒ - monitor ! Self(ctx.getSelf) - SActor.Same - case Miss ⇒ - monitor ! Missed - SActor.Unhandled - case Ignore ⇒ - monitor ! Ignored - SActor.Same - case Ping ⇒ - monitor ! Pong - behv(monitor, state) - case Swap ⇒ - monitor ! Swapped - behv(monitor, state.next) - case GetState() ⇒ - monitor ! state - SActor.Same - case Stop ⇒ SActor.Stopped - case _: AuxPing ⇒ SActor.Unhandled - }) - } - } - object `A Stateful Behavior (java,native)` extends StatefulJavaBehavior with NativeSystem - object `A Stateful Behavior (java,adapted)` extends StatefulJavaBehavior with AdaptedSystem - - trait StatelessJavaBehavior extends Messages { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = - (JActor.stateless { - pc((ctx, msg) ⇒ - msg match { - case GetSelf ⇒ monitor ! Self(ctx.getSelf) - case Miss ⇒ monitor ! Missed - case Ignore ⇒ monitor ! Ignored - case Ping ⇒ monitor ! Pong - case Swap ⇒ monitor ! Swapped - case GetState() ⇒ monitor ! StateA - case Stop ⇒ - case _: AuxPing ⇒ - }) - }, null) - } - object `A Stateless Behavior (java,native)` extends StatelessJavaBehavior with NativeSystem - object `A Stateless Behavior (java,adapted)` extends StatelessJavaBehavior with AdaptedSystem - - trait WidenedJavaBehavior extends StatefulWithSignalJavaBehavior with Reuse with Siphon { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Command]("widenedListener") - JActor.widened(super.behavior(monitor)._1, pf(_.`match`(classOf[Command], fi(x ⇒ { inbox.ref ! x; x })))) → inbox - } - } - object `A widened Behavior (java,native)` extends WidenedJavaBehavior with NativeSystem - object `A widened Behavior (java,adapted)` extends WidenedJavaBehavior with AdaptedSystem - - trait DeferredJavaBehavior extends StatefulWithSignalJavaBehavior { - override type Aux = Inbox[PreStart] - - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[PreStart]("deferredListener") - (JActor.deferred(df(ctx ⇒ { - inbox.ref ! PreStart - super.behavior(monitor)._1 - })), inbox) - } - - override def checkAux(signal: Signal, aux: Aux): Unit = - signal match { - case PreStart ⇒ aux.receiveAll() should ===(PreStart :: Nil) - case _ ⇒ aux.receiveAll() should ===(Nil) - } - } - object `A deferred Behavior (java,native)` extends DeferredJavaBehavior with NativeSystem - object `A deferred Behavior (java,adapted)` extends DeferredJavaBehavior with AdaptedSystem - - trait TapJavaBehavior extends StatefulWithSignalJavaBehavior with Reuse with SignalSiphon { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Either[Signal, Command]]("tapListener") - (JActor.tap( - ps((_, sig) ⇒ inbox.ref ! Left(sig)), - pc((_, msg) ⇒ inbox.ref ! Right(msg)), - super.behavior(monitor)._1), inbox) - } - } - object `A tap Behavior (java,native)` extends TapJavaBehavior with NativeSystem - object `A tap Behavior (java,adapted)` extends TapJavaBehavior with AdaptedSystem - - trait RestarterJavaBehavior extends StatefulWithSignalJavaBehavior with Reuse { - override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - JActor.restarter(classOf[Exception], JActor.OnFailure.RESTART, super.behavior(monitor)._1) → null - } - } - object `A restarter Behavior (java,native)` extends RestarterJavaBehavior with NativeSystem - object `A restarter Behavior (java,adapted)` extends RestarterJavaBehavior with AdaptedSystem - -} diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index 4cedd48ddd..4402a5ba3d 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -67,7 +67,9 @@ object AkkaBuild extends Build { streamTests, streamTestsTck, testkit, - typed + typed, + typedTests, + typedTestkit ) lazy val root = Project( @@ -94,18 +96,30 @@ object AkkaBuild extends Build { dependencies = Seq(actor) ) - lazy val typed = Project( - id = "akka-typed", - base = file("akka-typed"), - dependencies = Seq(testkit % "compile;test->test") - ) - lazy val actorTests = Project( id = "akka-actor-tests", base = file("akka-actor-tests"), dependencies = Seq(testkit % "compile;test->test") ) + lazy val typed = Project( + id = "akka-typed", + base = file("akka-typed"), + dependencies = Seq(actor) + ) + + lazy val typedTestkit = Project( + id = "akka-typed-testkit", + base = file("akka-typed-testkit"), + dependencies = Seq(typed, testkit % "compile;test->test") + ) + + lazy val typedTests = Project( + id = "akka-typed-tests", + base = file("akka-typed-tests"), + dependencies = Seq(typedTestkit % "compile;test->test") + ) + lazy val benchJmh = Project( id = "akka-bench-jmh", base = file("akka-bench-jmh"), @@ -260,6 +274,7 @@ object AkkaBuild extends Build { remote % "compile;test->test", persistence % "compile;provided->provided;test->test", typed % "compile;test->test", + typedTests % "compile;test->test", streamTestkit % "compile;test->test" ) ) diff --git a/project/MiMa.scala b/project/MiMa.scala index 4780056ad1..7b8135ef97 100644 --- a/project/MiMa.scala +++ b/project/MiMa.scala @@ -1193,6 +1193,12 @@ object MiMa extends AutoPlugin { ), "2.5.1" -> Seq( + // #22794 watchWith + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.ActorContext.watchWith"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.dungeon.DeathWatch.watchWith"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.dungeon.DeathWatch.akka$actor$dungeon$DeathWatch$$watching"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.dungeon.DeathWatch.akka$actor$dungeon$DeathWatch$$watching_="), + // #22868 store shards ProblemFilters.exclude[DirectMissingMethodProblem]("akka.cluster.sharding.DDataShardCoordinator.sendUpdate"), ProblemFilters.exclude[DirectMissingMethodProblem]("akka.cluster.sharding.DDataShardCoordinator.waitingForUpdate"),