Merge pull request #25833 from akka/wip-getting-started-typed-patriknw
Translate Getting Started Guide to Typed, #25998
This commit is contained in:
commit
b04accefa0
82 changed files with 6340 additions and 59 deletions
|
|
@ -27,18 +27,26 @@ import scala.util.control.NonFatal
|
|||
private[akka] object TestProbeImpl {
|
||||
private val testActorId = new AtomicInteger(0)
|
||||
|
||||
private case class WatchActor[U](actor: ActorRef[U])
|
||||
private def testActor[M](queue: BlockingDeque[M], terminations: BlockingDeque[Terminated]): Behavior[M] = Behaviors.receive[M] { (context, message) ⇒
|
||||
message match {
|
||||
case WatchActor(ref) ⇒ context.watch(ref)
|
||||
case other ⇒ queue.offerLast(other)
|
||||
private final case class WatchActor[U](actor: ActorRef[U])
|
||||
private case object Stop
|
||||
|
||||
private def testActor[M](queue: BlockingDeque[M], terminations: BlockingDeque[Terminated]): Behavior[M] =
|
||||
Behaviors.receive[M] { (context, msg) ⇒
|
||||
msg match {
|
||||
case WatchActor(ref) ⇒
|
||||
context.watch(ref)
|
||||
Behaviors.same
|
||||
case Stop ⇒
|
||||
Behaviors.stopped
|
||||
case other ⇒
|
||||
queue.offerLast(other)
|
||||
Behaviors.same
|
||||
}
|
||||
}.receiveSignal {
|
||||
case (_, t: Terminated) ⇒
|
||||
terminations.offerLast(t)
|
||||
Behaviors.same
|
||||
}
|
||||
Behaviors.same
|
||||
}.receiveSignal {
|
||||
case (_, t: Terminated) ⇒
|
||||
terminations.offerLast(t)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
||||
@InternalApi
|
||||
|
|
@ -264,16 +272,22 @@ private[akka] final class TestProbeImpl[M](name: String, system: ActorSystem[_])
|
|||
|
||||
@tailrec
|
||||
def poll(t: Duration): A = {
|
||||
// cannot use null-ness of result as signal it failed
|
||||
// because Java API and not wanting to return a value will be "return null"
|
||||
var failed = false
|
||||
val result: A =
|
||||
try {
|
||||
a
|
||||
val aRes = a
|
||||
failed = false
|
||||
aRes
|
||||
} catch {
|
||||
case NonFatal(e) ⇒
|
||||
failed = true
|
||||
if ((now + t) >= stop) throw e
|
||||
else null.asInstanceOf[A]
|
||||
}
|
||||
|
||||
if (result != null) result
|
||||
if (!failed) result
|
||||
else {
|
||||
Thread.sleep(t.toMillis)
|
||||
poll((stop - now) min interval)
|
||||
|
|
@ -290,4 +304,8 @@ private[akka] final class TestProbeImpl[M](name: String, system: ActorSystem[_])
|
|||
|
||||
private def assertFail(msg: String): Nothing = throw new AssertionError(msg)
|
||||
|
||||
override def stop(): Unit = {
|
||||
testActor.asInstanceOf[ActorRef[AnyRef]] ! Stop
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,14 +207,14 @@ abstract class TestProbe[M] {
|
|||
* Same as `expectMessageType(clazz, remainingOrDefault)`,but using the
|
||||
* default timeout as deadline.
|
||||
*/
|
||||
def expectMessageClass[T](clazz: Class[T]): T =
|
||||
def expectMessageClass[T <: M](clazz: Class[T]): T =
|
||||
expectMessageClass_internal(getRemainingOrDefault.asScala, clazz)
|
||||
|
||||
/**
|
||||
* Wait for a message of type M and return it when it arrives, or fail if the `max` timeout is hit.
|
||||
* The timeout is dilated.
|
||||
*/
|
||||
def expectMessageClass[T](clazz: Class[T], max: Duration): T =
|
||||
def expectMessageClass[T <: M](clazz: Class[T], max: Duration): T =
|
||||
expectMessageClass_internal(max.asScala.dilated, clazz)
|
||||
|
||||
/**
|
||||
|
|
@ -269,7 +269,7 @@ abstract class TestProbe[M] {
|
|||
fishForMessage(max, "", fisher)
|
||||
|
||||
/**
|
||||
* Same as the other `fishForMessageJava` but includes the provided hint in all error messages
|
||||
* Same as the other `fishForMessage` but includes the provided hint in all error messages
|
||||
*/
|
||||
def fishForMessage(max: Duration, hint: String, fisher: java.util.function.Function[M, FishingOutcome]): java.util.List[M] =
|
||||
fishForMessage_internal(max.asScala, hint, fisher.apply).asJava
|
||||
|
|
@ -279,4 +279,9 @@ abstract class TestProbe[M] {
|
|||
*/
|
||||
@InternalApi protected def fishForMessage_internal(max: FiniteDuration, hint: String, fisher: M ⇒ FishingOutcome): List[M]
|
||||
|
||||
/**
|
||||
* Stops the [[TestProbe.getRef]], which is useful when testing watch and termination.
|
||||
*/
|
||||
def stop(): Unit
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,4 +224,9 @@ object TestProbe {
|
|||
* which uses the configuration entry "akka.test.timefactor".
|
||||
*/
|
||||
def awaitAssert[A](a: ⇒ A, max: Duration = Duration.Undefined, interval: Duration = 100.millis): A
|
||||
|
||||
/**
|
||||
* Stops the [[TestProbe.ref]], which is useful when testing watch and termination.
|
||||
*/
|
||||
def stop(): Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
package akka.actor.testkit.typed.javadsl;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.TestProbeSpec;
|
||||
|
|
@ -54,4 +55,57 @@ public class TestProbeTest extends JUnitSuite {
|
|||
probe.receiveOne(Duration.ofMillis(100));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAwaitAssert() {
|
||||
TestProbe<String> probe = TestProbe.create(testKit.system());
|
||||
probe.awaitAssert(() -> {
|
||||
// ... something ...
|
||||
return null;
|
||||
});
|
||||
probe.awaitAssert(Duration.ofSeconds(3), () -> {
|
||||
// ... something ...
|
||||
return null;
|
||||
});
|
||||
String awaitAssertResult =
|
||||
probe.awaitAssert(Duration.ofSeconds(3), Duration.ofMillis(100), () -> {
|
||||
// ... something ...
|
||||
return "some result";
|
||||
});
|
||||
assertEquals("some result", awaitAssertResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpectMessage() {
|
||||
TestProbe<String> probe = TestProbe.create(testKit.system());
|
||||
probe.getRef().tell("message");
|
||||
String messageResult = probe.expectMessage("message");
|
||||
probe.getRef().tell("message2");
|
||||
String expectClassResult = probe.expectMessageClass(String.class);
|
||||
probe.expectNoMessage();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFish() {
|
||||
TestProbe<String> probe = TestProbe.create(testKit.system());
|
||||
probe.getRef().tell("one");
|
||||
probe.getRef().tell("one");
|
||||
probe.getRef().tell("two");
|
||||
List<String> results = probe.fishForMessage(Duration.ofSeconds(3), "hint", message -> {
|
||||
if (message.equals("one")) return FishingOutcomes.continueAndIgnore();
|
||||
else if (message.equals("two")) return FishingOutcomes.complete();
|
||||
else return FishingOutcomes.fail("error");
|
||||
});
|
||||
assertEquals(Arrays.asList("two"), results);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithin() {
|
||||
TestProbe<String> probe = TestProbe.create(testKit.system());
|
||||
String withinResult = probe.within(Duration.ofSeconds(3), () -> {
|
||||
// ... something ...
|
||||
return "result";
|
||||
});
|
||||
assertEquals("result", withinResult);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,13 @@ class TestProbeSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
|||
"timeout if expected single message is not received by a provided timeout" in {
|
||||
intercept[AssertionError](createTestProbe[EventT]().receiveOne(100.millis))
|
||||
}
|
||||
|
||||
"support watch and stop of probe" in {
|
||||
val probe1 = TestProbe[String]()
|
||||
val probe2 = TestProbe[String]()
|
||||
probe1.stop()
|
||||
probe2.expectTerminated(probe1.ref, probe2.remainingOrDefault)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ The following capabilities are included with Akka OSS and are introduced later o
|
|||
With a Lightbend subscription, you can use [Enterprise Suite](https://www.lightbend.com/platform/production) in production. Enterprise Suite includes the following extensions to Akka core functionality:
|
||||
|
||||
* [Split Brain Resolver](https://developer.lightbend.com/docs/akka-commercial-addons/current/split-brain-resolver.html) — Detects and recovers from network partitions, eliminating data inconsistencies and possible downtime.
|
||||
* [Multi-DC Persistence](https://developer.lightbend.com/docs/akka-commercial-addons/current/persistence-dc/index.html) — For active-active persistent entities across multiple data centers.
|
||||
* [GDPR for Akka Persistence](https://developer.lightbend.com/docs/akka-commercial-addons/current/gdpr/index.html) — Data shredding can be used to forget information in events.
|
||||
* [Fast Failover](https://developer.lightbend.com/docs/akka-commercial-addons/current/fast-failover.html) — Fast failover for Cluster Sharding.
|
||||
* [Configuration Checker](https://developer.lightbend.com/docs/akka-commercial-addons/current/config-checker.html) — Checks for potential configuration issues and logs suggestions.
|
||||
* [Diagnostics Recorder](https://developer.lightbend.com/docs/akka-commercial-addons/current/diagnostics-recorder.html) — Captures configuration and system information in a format that makes it easy to troubleshoot issues during development and production.
|
||||
* [Thread Starvation Detector](https://developer.lightbend.com/docs/akka-commercial-addons/current/starvation-detector.html) — Monitors an Akka system dispatcher and logs warnings if it becomes unresponsive.
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ immediately after the API has been invoked any of the following can happen:
|
|||
This illustrates that the **guarantee of delivery** does not translate to the **domain level guarantee**. We only want to report success once the order has been actually fully processed and persisted. **The only entity that can report success is the application itself, since only it has any understanding of the domain guarantees required. No generalized framework can figure out the specifics of a particular domain and what is considered a success in that domain**.
|
||||
|
||||
In this particular example, we only want to signal success after a successful database write, where the database acknowledged that the order is now safely stored. **For these reasons Akka lifts the responsibilities of guarantees to the application
|
||||
itself, i.e. you have to implement them yourself. This gives you full control of the guarantees that you want to provide**. Now, let's consider the message ordering that Akka provides to make it easy to reason about application logic.
|
||||
itself, i.e. you have to implement them yourself with the tools that Akka provides. This gives you full control of the guarantees that you want to provide**. Now, let's consider the message ordering that Akka provides to make it easy to reason about application logic.
|
||||
|
||||
### Message Ordering
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ Java
|
|||
|
||||
The Cluster extensions gives you access to:
|
||||
|
||||
* manager: An `ActorRef[ClusterCommand]` where a `ClusterCommand` is a command such as: `Join`, `Leave` and `Down`
|
||||
* manager: An @scala[`ActorRef[ClusterCommand]`]@java[`ActorRef<ClusterCommand>`] where a `ClusterCommand` is a command such as: `Join`, `Leave` and `Down`
|
||||
* subscriptions: An `ActorRef[ClusterStateSubscription]` where a `ClusterStateSubscription` is one of `GetCurrentState` or `Subscribe` and `Unsubscribe` to cluster events like `MemberRemoved`
|
||||
* state: The current `CurrentClusterState`
|
||||
|
||||
|
|
|
|||
99
akka-docs/src/main/paradox/typed/guide/actors-intro.md
Normal file
99
akka-docs/src/main/paradox/typed/guide/actors-intro.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# How the Actor Model Meets the Needs of Modern, Distributed Systems
|
||||
|
||||
As described in the previous topic, common programming practices do not properly
|
||||
address the needs of demanding modern systems. Thankfully, we
|
||||
don't need to scrap everything we know. Instead, the actor model addresses these
|
||||
shortcomings in a principled way, allowing systems to behave in a way that
|
||||
better matches our mental model. The actor model abstraction
|
||||
allows you to think about your code in terms of communication, not unlike the
|
||||
exchanges that occur between people in a large organization.
|
||||
|
||||
Use of actors allows us to:
|
||||
|
||||
* Enforce encapsulation without resorting to locks.
|
||||
* Use the model of cooperative entities reacting to signals, changing state, and sending signals to each other
|
||||
to drive the whole application forward.
|
||||
* Stop worrying about an executing mechanism which is a mismatch to our world view.
|
||||
|
||||
### Usage of message passing avoids locking and blocking
|
||||
|
||||
Instead of calling methods, actors send messages to each other. Sending a message does not transfer the thread
|
||||
of execution from the sender to the destination. An actor can send a message and continue without blocking.
|
||||
Therefore, it can accomplish more in the same amount of time.
|
||||
|
||||
With objects, when a method returns, it releases control of its executing thread. In this respect, actors behave
|
||||
much like objects, they react to messages and return execution when they finish processing the current message.
|
||||
In this way, actors actually achieve the execution we imagined for objects:
|
||||
|
||||

|
||||
|
||||
An important difference between passing messages and calling methods is that messages have no return value.
|
||||
By sending a message, an actor delegates work to another actor. As we saw in @ref:[The illusion of a call stack](actors-motivation.md#the-illusion-of-a-call-stack),
|
||||
if it expected a return value, the sending actor would either need to block or to execute the other actor's work on the same thread.
|
||||
Instead, the receiving actor delivers the results in a reply message.
|
||||
|
||||
The second key change we need in our model is to reinstate encapsulation. Actors react to messages just like objects
|
||||
"react" to methods invoked on them. The difference is that instead of multiple threads "protruding" into our actor and
|
||||
wreaking havoc to internal state and invariants, actors execute independently from the senders of a message, and they
|
||||
react to incoming messages sequentially, one at a time. While each actor processes messages sent to it sequentially,
|
||||
different actors work concurrently with each other so that an actor system can process as many messages simultaneously as the hardware will support.
|
||||
|
||||
Since there is always at most one message being processed per actor,
|
||||
the invariants of an actor can be kept without synchronization. This happens automatically without using locks:
|
||||
|
||||

|
||||
|
||||
In summary, this is what happens when an actor receives a message:
|
||||
|
||||
1. The actor adds the message to the end of a queue.
|
||||
2. If the actor was not scheduled for execution, it is marked as ready to execute.
|
||||
3. A (hidden) scheduler entity takes the actor and starts executing it.
|
||||
4. Actor picks the message from the front of the queue.
|
||||
5. Actor modifies internal state, sends messages to other actors.
|
||||
6. The actor is unscheduled.
|
||||
|
||||
To accomplish this behavior, actors have:
|
||||
|
||||
* A mailbox (the queue where messages end up).
|
||||
* A behavior (the state of the actor, internal variables etc.).
|
||||
* Messages (pieces of data representing a signal, similar to method calls and their parameters).
|
||||
* An execution environment (the machinery that takes actors that have messages to react to and invokes
|
||||
their message handling code).
|
||||
* An address (more on this later).
|
||||
|
||||
Messages go into actor mailboxes. The behavior of the actor describes how the actor responds to
|
||||
messages (like sending more messages and/or changing state). An execution environment orchestrates a pool of threads
|
||||
to drive all these actions completely transparently.
|
||||
|
||||
This is a very simple model and it solves the issues enumerated previously:
|
||||
|
||||
* Encapsulation is preserved by decoupling execution from signaling (method calls transfer execution,
|
||||
message passing does not).
|
||||
* There is no need for locks. Modifying the internal state of an actor is only possible via messages, which are
|
||||
processed one at a time eliminating races when trying to keep invariants.
|
||||
* There are no locks used anywhere, and senders are not blocked. Millions of actors can be efficiently scheduled on a
|
||||
dozen of threads reaching the full potential of modern CPUs. Task delegation is the natural mode of operation for actors.
|
||||
* State of actors is local and not shared, changes and data is propagated via messages, which maps to how modern
|
||||
memory hierarchy actually works. In many cases, this means transferring over only the cache lines that contain the data in the message while keeping local state and data cached at the original core. The same model maps exactly to remote communication where the state is kept in the RAM of machines and changes/data is propagated over the network as packets.
|
||||
|
||||
### Actors handle error situations gracefully
|
||||
|
||||
Since we no longer have a shared call stack between actors that send messages to each other, we need to handle
|
||||
error situations differently. There are two kinds of errors we need to consider:
|
||||
|
||||
* The first case is when the delegated task on the target actor failed due to an error in the task (typically some
|
||||
validation issue, like a non-existent user ID). In this case, the service encapsulated by the target actor is intact,
|
||||
it is only the task that itself is erroneous.
|
||||
The service actor should reply to the sender with a message, presenting the error case. There is nothing special here, errors are part of the domain and hence become ordinary messages.
|
||||
* The second case is when a service itself encounters an internal fault. Akka enforces that all actors are organized
|
||||
into a tree-like hierarchy, i.e. an actor that creates another actor becomes the parent of that new actor. This is very similar how operating systems organize processes into a tree. Just like with processes, when an actor fails,
|
||||
its parent actor can decide how to react to the failure. Also, if the parent actor is stopped,
|
||||
all of its children are recursively stopped, too. This service is called supervision and it is central to Akka.
|
||||
|
||||
A supervisor strategy is typically defined by the parent actor when it is starting a child actor. It can decide
|
||||
to restart the child actor on certain types of failures or stop it completely on others. Children never go silently
|
||||
dead (with the notable exception of entering an infinite loop) instead they are either failing and the supervisor
|
||||
strategy can react to the fault, or they are stopped (in which case interested parties are notified).
|
||||
There is always a responsible entity for managing an actor: its parent. Restarts are not visible from the outside: collaborating actors can keep continuing sending messages while the target actor restarts.
|
||||
|
||||
Now, let's take a short tour of the functionality Akka provides.
|
||||
159
akka-docs/src/main/paradox/typed/guide/actors-motivation.md
Normal file
159
akka-docs/src/main/paradox/typed/guide/actors-motivation.md
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
# Why modern systems need a new programming model
|
||||
|
||||
The actor model was proposed decades ago by @extref[Carl Hewitt](wikipedia:Carl_Hewitt#Actor_model) as a way to handle parallel processing in a high performance network — an environment that was not available at the time. Today, hardware and infrastructure capabilities have caught up with and exceeded Hewitt's vision. Consequently, organizations building distributed systems with demanding requirements encounter challenges that cannot fully be solved with a traditional object-oriented programming (OOP) model, but that can benefit from the actor model.
|
||||
|
||||
Today, the actor model is not only recognized as a highly effective solution — it has been proven in production for some of the world's most demanding applications. To highlight issues that the actor model addresses, this topic discusses the following mismatches between traditional programming assumptions and the reality of modern multi-threaded, multi-CPU architectures:
|
||||
|
||||
* [The challenge of encapsulation](#the-illusion-of-encapsulation)
|
||||
* [The illusion of shared memory on modern computer architectures](#The-illusion-of-shared-memory-on-modern-computer-architectures)
|
||||
* [The illusion of a call stack](#the-illusion-of-a-call-stack)
|
||||
|
||||
|
||||
## The challenge of encapsulation
|
||||
|
||||
A core pillar of OOP is _encapsulation_. Encapsulation dictates that the internal data of an object is not accessible directly from the outside;
|
||||
it can only be modified by invoking a set of curated methods. The object is responsible for exposing safe operations
|
||||
that protect the invariant nature of its encapsulated data.
|
||||
|
||||
For example, operations on an ordered binary tree implementation must not allow violation of the tree ordering
|
||||
invariant. Callers expect the ordering to be intact and when querying the tree for a certain piece of
|
||||
data, they need to be able to rely on this constraint.
|
||||
|
||||
When we analyze OOP runtime behavior, we sometimes draw a message sequence chart showing the interactions of
|
||||
method calls. For example:
|
||||
|
||||

|
||||
|
||||
Unfortunately, the above diagram does not accurately represent the _lifelines_ of the instances during execution.
|
||||
In reality, a _thread_ executes all these calls, and the enforcement of invariants occurs on the same thread from
|
||||
which the method was called. Updating the diagram with the thread of execution, it looks like this:
|
||||
|
||||

|
||||
|
||||
The significance of this clarification becomes clear when you try to model what happens with _multiple threads_.
|
||||
Suddenly, our neatly drawn diagram becomes inadequate. We can try to illustrate multiple threads accessing
|
||||
the same instance:
|
||||
|
||||

|
||||
|
||||
There is a section of execution where two threads enter the same method. Unfortunately, the encapsulation model
|
||||
of objects does not guarantee anything about what happens in that section. Instructions of the two invocations
|
||||
can be interleaved in arbitrary ways which eliminate any hope for keeping the invariants intact without some
|
||||
type of coordination between two threads. Now, imagine this issue compounded by the existence of many threads.
|
||||
|
||||
The common approach to solving this problem is to add a lock around these methods. While this ensures that at most
|
||||
one thread will enter the method at any given time, this is a very costly strategy:
|
||||
|
||||
* Locks _seriously limit_ concurrency, they are very costly on modern CPU architectures,
|
||||
requiring heavy-lifting from the operating system to suspend the thread and restore it later.
|
||||
* The caller thread is now blocked, so it cannot do any other meaningful work. Even in desktop applications this is
|
||||
unacceptable, we want to keep user-facing parts of applications (its UI) to be responsive even when a
|
||||
long background job is running. In the backend, blocking is outright wasteful.
|
||||
One might think that this can be compensated by launching new threads, but threads are also a costly abstraction.
|
||||
* Locks introduce a new menace: deadlocks.
|
||||
|
||||
These realities result in a no-win situation:
|
||||
|
||||
* Without sufficient locks, the state gets corrupted.
|
||||
* With many locks in place, performance suffers and very easily leads to deadlocks.
|
||||
|
||||
Additionally, locks only really work well locally. When it comes to coordinating across multiple machines,
|
||||
the only alternative is distributed locks. Unfortunately, distributed locks are several magnitudes less efficient
|
||||
than local locks and usually impose a hard limit on scaling out. Distributed lock protocols require several
|
||||
communication round-trips over the network across multiple machines, so latency goes through the roof.
|
||||
|
||||
In Object Oriented languages we rarely think about threads or linear execution paths in general.
|
||||
We often envision a system as a network of object instances that react to method calls, modify their internal state,
|
||||
then communicate with each other via method calls driving the whole application state forward:
|
||||
|
||||

|
||||
|
||||
However, in a multi-threaded distributed environment, what actually happens is that threads "traverse" this network of object instances by following method calls.
|
||||
As a result, threads are what really drive execution:
|
||||
|
||||

|
||||
|
||||
**In summary**:
|
||||
|
||||
* **Objects can only guarantee encapsulation (protection of invariants) in the face of single-threaded access,
|
||||
multi-thread execution almost always leads to corrupted internal state. Every invariant can be violated by
|
||||
having two contending threads in the same code segment.**
|
||||
* **While locks seem to be the natural remedy to uphold encapsulation with multiple threads, in practice they
|
||||
are inefficient and easily lead to deadlocks in any application of real-world scale.**
|
||||
* **Locks work locally, attempts to make them distributed exist, but offer limited potential for scaling out.**
|
||||
|
||||
## The illusion of shared memory on modern computer architectures
|
||||
|
||||
Programming models of the 80'-90's conceptualize that writing to a variable means writing to a memory location directly
|
||||
(which somewhat muddies the water that local variables might exist only in registers). On modern architectures -
|
||||
if we simplify things a bit - CPUs are writing to @extref[cache lines](wikipedia:CPU_cache)
|
||||
instead of writing to memory directly. Most of these caches are local to the CPU core, that is, writes by one core
|
||||
are not visible by another. In order to make local changes visible to another core, and hence to another thread,
|
||||
the cache line needs to be shipped to the other core's cache.
|
||||
|
||||
On the JVM, we have to explicitly denote memory locations to be shared across threads by using _volatile_ markers
|
||||
or `Atomic` wrappers. Otherwise, we can access them only in a locked section. Why don't we just mark all variables as
|
||||
volatile? Because shipping cache lines across cores is a very costly operation! Doing so would implicitly stall the cores
|
||||
involved from doing additional work, and result in bottlenecks on the cache coherence protocol (the protocol CPUs
|
||||
use to transfer cache lines between main memory and other CPUs).
|
||||
The result is magnitudes of slowdown.
|
||||
|
||||
Even for developers aware of this situation, figuring out which memory locations should be marked as volatile,
|
||||
or which atomic structures to use is a dark art.
|
||||
|
||||
**In summary**:
|
||||
|
||||
* **There is no real shared memory anymore, CPU cores pass chunks of data (cache lines) explicitly to each other
|
||||
just as computers on a network do. Inter-CPU communication and network communication have more in common than many realize. Passing messages is the norm now be it across CPUs or networked computers.**
|
||||
* **Instead of hiding the message passing aspect through variables marked as shared or using atomic data structures,
|
||||
a more disciplined and principled approach is to keep state local to a concurrent entity and propagate data or events
|
||||
between concurrent entities explicitly via messages.**
|
||||
|
||||
## The illusion of a call stack
|
||||
|
||||
Today, we often take call stacks for granted. But, they were invented in an era where concurrent programming
|
||||
was not as important because multi-CPU systems were not common. Call stacks do not cross threads and hence,
|
||||
do not model asynchronous call chains.
|
||||
|
||||
The problem arises when a thread intends to delegate a task to the "background". In practice, this really means
|
||||
delegating to another thread. This cannot be a simple method/function call because calls are strictly local to the
|
||||
thread. What usually happens, is that the "caller" puts an object into a memory location shared by a worker thread
|
||||
("callee"), which in turn, picks it up in some event loop. This allows the "caller" thread to move on and do other tasks.
|
||||
|
||||
The first issue is, how can the "caller" be notified of the completion of the task? But a more serious issue arises
|
||||
when a task fails with an exception. Where does the exception propagate to? It will propagate to the exception handler
|
||||
of the worker thread completely ignoring who the actual "caller" was:
|
||||
|
||||

|
||||
|
||||
This is a serious problem. How does the worker thread deal with the situation? It likely cannot fix the issue as it is
|
||||
usually oblivious of the purpose of the failed task. The "caller" thread needs to be notified somehow,
|
||||
but there is no call stack to unwind with an exception. Failure notification can only be done via a side-channel,
|
||||
for example putting an error code where the "caller" thread otherwise expects the result once ready.
|
||||
If this notification is not in place, the "caller" never gets notified of a failure and the task is lost!
|
||||
**This is surprisingly similar to how networked systems work where messages/requests can get lost/fail without any
|
||||
notification.**
|
||||
|
||||
This bad situation gets worse when things go really wrong and a worker backed by a thread encounters a bug and ends
|
||||
up in an unrecoverable situation. For example, an internal exception caused by a bug bubbles up to the root of
|
||||
the thread and makes the thread shut down. This immediately raises the question, who should restart the normal operation
|
||||
of the service hosted by the thread, and how should it be restored to a known-good state? At first glance,
|
||||
this might seem manageable, but we are suddenly faced by a new, unexpected phenomena: the actual task,
|
||||
that the thread was currently working on, is no longer in the shared memory location where tasks are taken from
|
||||
(usually a queue). In fact, due to the exception reaching to the top, unwinding all of the call stack,
|
||||
the task state is fully lost! **We have lost a message even though this is local communication with no networking
|
||||
involved (where message losses are to be expected).**
|
||||
|
||||
**In summary:**
|
||||
|
||||
* **To achieve any meaningful concurrency and performance on current systems, threads must delegate tasks among each
|
||||
other in an efficient way without blocking. With this style of task-delegating concurrency
|
||||
(and even more so with networked/distributed computing) call stack-based error handling breaks down and new,
|
||||
explicit error signaling mechanisms need to be introduced. Failures become part of the domain model.**
|
||||
* **Concurrent systems with work delegation needs to handle service faults and have principled means to recover from them.
|
||||
Clients of such services need to be aware that tasks/messages might get lost during restarts.
|
||||
Even if loss does not happen, a response might be delayed arbitrarily due to previously enqueued tasks
|
||||
(a long queue), delays caused by garbage collection, etc. In face of these, concurrent systems should handle response
|
||||
deadlines in the form of timeouts, just like networked/distributed systems.**
|
||||
|
||||
Next, let's see how use of the actor model can overcome these challenges.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
|
|
@ -0,0 +1,365 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 210 297"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
|
||||
sodipodi:docname="actor_top_tree.svg"
|
||||
inkscape:export-filename="C:\Users\Varga\workspace\akka\akka-docs-new\src\main\paradox\guide\diagrams\actor_top_tree.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<defs
|
||||
id="defs2">
|
||||
<marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
id="marker1768"
|
||||
refX="0.0"
|
||||
refY="0.0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Arrow1Lend">
|
||||
<path
|
||||
transform="scale(0.8) rotate(180) translate(12.5,0)"
|
||||
style="fill-rule:evenodd;stroke:#ce5c00;stroke-width:1pt;stroke-opacity:1;fill:#ce5c00;fill-opacity:1"
|
||||
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
|
||||
id="path1766" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Lend"
|
||||
orient="auto"
|
||||
refY="0.0"
|
||||
refX="0.0"
|
||||
id="Arrow1Lend"
|
||||
style="overflow:visible;"
|
||||
inkscape:isstock="true"
|
||||
inkscape:collect="always">
|
||||
<path
|
||||
id="path1451"
|
||||
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
|
||||
style="fill-rule:evenodd;stroke:#ce5c00;stroke-width:1pt;stroke-opacity:1;fill:#ce5c00;fill-opacity:1"
|
||||
transform="scale(0.8) rotate(180) translate(12.5,0)" />
|
||||
</marker>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.2032242"
|
||||
inkscape:cx="360.36313"
|
||||
inkscape:cy="627.74786"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:snap-global="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="855"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
showguides="false" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<flowRoot
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
id="flowRoot208"
|
||||
xml:space="preserve"><flowRegion
|
||||
id="flowRegion210"
|
||||
style="stroke-width:0.26458332"><rect
|
||||
y="98.214661"
|
||||
x="71.68586"
|
||||
height="26.387434"
|
||||
width="98.073303"
|
||||
id="rect212"
|
||||
style="stroke-width:0.26458332" /></flowRegion><flowPara
|
||||
id="flowPara214"
|
||||
style="stroke-width:0.26458332" /></flowRoot> <circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path391"
|
||||
cx="82.744453"
|
||||
cy="104.15183"
|
||||
r="5.2774868" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="83.69178"
|
||||
y="91.837692"
|
||||
id="text395"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan393"
|
||||
x="84.220947"
|
||||
y="91.837692"
|
||||
style="text-align:center;text-anchor:middle;stroke-width:0.26458332">/ </tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="83.69178"
|
||||
y="97.129356"
|
||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332"
|
||||
id="tspan489">(root guardian)</tspan></text>
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="128.56021"
|
||||
cx="66.252312"
|
||||
id="circle397"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle403"
|
||||
cx="121.81485"
|
||||
cy="128.56021"
|
||||
r="5.2774868" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle409"
|
||||
cx="49.848129"
|
||||
cy="152.37239"
|
||||
r="5.2774868" />
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="152.37239"
|
||||
cx="65.193977"
|
||||
id="circle411"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#888a85;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#888a85;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle413"
|
||||
cx="85.30233"
|
||||
cy="152.37239"
|
||||
r="5.2774868" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#888a85;fill-opacity:1;stroke:none;stroke-width:0.26458332;"
|
||||
x="73.728752"
|
||||
y="152.74869"
|
||||
id="text417"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan415"
|
||||
x="73.728752"
|
||||
y="152.74869"
|
||||
style="stroke-width:0.26458332;fill:#888a85;">...</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 69.267016,124.1623 80.041884,108.76963"
|
||||
id="path451"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 87.518324,107.01047 29.905756,18.03141"
|
||||
id="path453"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 52.554973,147.25131 9.895289,-14.73299"
|
||||
id="path455"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#888a85;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 64.869109,147.03141 65.528795,133.6178"
|
||||
id="path457"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#888a85;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 70.366492,132.73822 82.680626,147.6911"
|
||||
id="path459"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#888a85;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 107.74869,148.57068 10.33508,-16.49215"
|
||||
id="path461"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#888a85;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 122.04188,147.91099 0.2199,-14.29319"
|
||||
id="path463"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#888a85;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 126.43979,131.41885 12.31413,17.81151"
|
||||
id="path465"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="41.558144"
|
||||
y="125.85049"
|
||||
id="text395-2"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan393-2"
|
||||
x="41.558144"
|
||||
y="125.85049"
|
||||
style="text-align:center;text-anchor:middle;stroke-width:0.26458332">/user</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="41.558144"
|
||||
y="131.14217"
|
||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332"
|
||||
id="tspan489-1">(user guardian)</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="147.07378"
|
||||
y="126.51018"
|
||||
id="text395-2-5"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan393-2-5"
|
||||
x="147.07378"
|
||||
y="126.51018"
|
||||
style="text-align:center;text-anchor:middle;stroke-width:0.26458332">/system</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="147.07378"
|
||||
y="131.80185"
|
||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332"
|
||||
id="tspan489-1-6">(system guardian)</tspan></text>
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="152.63344"
|
||||
cx="106.36122"
|
||||
id="circle437"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#888a85;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#888a85;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle439"
|
||||
cx="121.70707"
|
||||
cy="152.63344"
|
||||
r="5.2774868" />
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="152.63344"
|
||||
cx="141.81541"
|
||||
id="circle441"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#888a85;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<text
|
||||
id="text445"
|
||||
y="153.00974"
|
||||
x="130.24184"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#888a85;fill-opacity:1;stroke:none;stroke-width:0.26458332;"
|
||||
xml:space="preserve"><tspan
|
||||
style="stroke-width:0.26458332;fill:#888a85;"
|
||||
y="153.00974"
|
||||
x="130.24184"
|
||||
id="tspan443"
|
||||
sodipodi:role="line">...</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332;"
|
||||
x="46.837696"
|
||||
y="163.83282"
|
||||
id="text543"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan541"
|
||||
x="46.837696"
|
||||
y="163.83282"
|
||||
style="stroke-width:0.26458332;fill:#000000;">/user/someActor</tspan></text>
|
||||
<text
|
||||
id="text547"
|
||||
y="163.61293"
|
||||
x="84.082283"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#888a85;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
xml:space="preserve"><tspan
|
||||
style="fill:#888a85;stroke-width:0.26458332"
|
||||
y="163.61293"
|
||||
x="84.082283"
|
||||
id="tspan545"
|
||||
sodipodi:role="line">/user/another</tspan></text>
|
||||
<text
|
||||
id="text551"
|
||||
y="163.48241"
|
||||
x="141.85933"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#888a85;fill-opacity:1;stroke:none;stroke-width:0.26458332;"
|
||||
xml:space="preserve"><tspan
|
||||
style="stroke-width:0.26458332;fill:#888a85;"
|
||||
y="163.48241"
|
||||
x="141.85933"
|
||||
id="tspan549"
|
||||
sodipodi:role="line">/system/someInternalActor</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 49.785712,157.58638 v 2.63875"
|
||||
id="path559"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:1.58999992,1.58999992;stroke-dashoffset:0"
|
||||
d="M 14.732984,141.31413 H 183.39267"
|
||||
id="path627"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 49.696335,165.06282 v 13.41362"
|
||||
id="path1440"
|
||||
inkscape:connector-curvature="0" />
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="176.71373"
|
||||
cx="49.628235"
|
||||
id="circle1438"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<text
|
||||
id="text1444"
|
||||
y="187.11584"
|
||||
x="46.837696"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332;"
|
||||
xml:space="preserve"><tspan
|
||||
style="fill:#000000;stroke-width:0.26458332;"
|
||||
y="187.11584"
|
||||
x="46.837696"
|
||||
id="tspan1442"
|
||||
sodipodi:role="line">/user/someActor/someChild</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#ce5c00;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Lend)"
|
||||
d="M 13.413613,152.52879 H 43.539267"
|
||||
id="path1446"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#ce5c00;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#marker1768)"
|
||||
d="m 31.884815,171.43979 v 5.93717 h 12.314137"
|
||||
id="path1758"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ce5c00;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="24.195574"
|
||||
y="169.90053"
|
||||
id="text1796"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan1794"
|
||||
x="24.195574"
|
||||
y="169.90053"
|
||||
style="fill:#ce5c00;stroke-width:0.26458332">context.actorOf()</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:4.23333311px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ce5c00;fill-opacity:0.94509804;stroke:none;stroke-width:0.26458332;"
|
||||
x="6.3769622"
|
||||
y="150.10995"
|
||||
id="text4164"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4162"
|
||||
x="6.3769622"
|
||||
y="150.10995"
|
||||
style="font-size:4.23333311px;stroke-width:0.26458332;fill:#ce5c00;fill-opacity:0.94509804;">context.actorOf()</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
|
|
@ -0,0 +1,343 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 210 297"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
|
||||
sodipodi:docname="arch_tree_diagram.svg"
|
||||
inkscape:export-filename="arch_tree_diagram.png"
|
||||
inkscape:export-xdpi="96.092979"
|
||||
inkscape:export-ydpi="96.092979">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.2032242"
|
||||
inkscape:cx="427.2667"
|
||||
inkscape:cy="760.72391"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:snap-global="true"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="855"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<flowRoot
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
id="flowRoot208"
|
||||
xml:space="preserve"><flowRegion
|
||||
id="flowRegion210"
|
||||
style="stroke-width:0.26458332"><rect
|
||||
y="98.214661"
|
||||
x="71.68586"
|
||||
height="26.387434"
|
||||
width="98.073303"
|
||||
id="rect212"
|
||||
style="stroke-width:0.26458332" /></flowRegion><flowPara
|
||||
id="flowPara214"
|
||||
style="stroke-width:0.26458332" /></flowRoot> <circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path391"
|
||||
cx="100.77586"
|
||||
cy="79.963348"
|
||||
r="5.2774868" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="108.03242"
|
||||
y="79.743454"
|
||||
id="text395"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan393"
|
||||
x="108.03242"
|
||||
y="79.743454"
|
||||
style="stroke-width:0.26458332">IoTSupervisor</tspan></text>
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="104.37173"
|
||||
cx="84.283722"
|
||||
id="circle397"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="42.503605"
|
||||
y="105.2513"
|
||||
id="text401"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan399"
|
||||
x="42.503605"
|
||||
y="105.2513"
|
||||
style="stroke-width:0.26458332">DeviceManager</tspan></text>
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle403"
|
||||
cx="139.84627"
|
||||
cy="104.37173"
|
||||
r="5.2774868" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="149.07187"
|
||||
y="105.91099"
|
||||
id="text407"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan405"
|
||||
x="149.07187"
|
||||
y="105.91099"
|
||||
style="stroke-width:0.26458332">DashboardManager</tspan></text>
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle409"
|
||||
cx="67.879539"
|
||||
cy="128.18391"
|
||||
r="5.2774868" />
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="128.18391"
|
||||
cx="83.225388"
|
||||
id="circle411"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle413"
|
||||
cx="103.33374"
|
||||
cy="128.18391"
|
||||
r="5.2774868" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="91.760162"
|
||||
y="128.56021"
|
||||
id="text417"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan415"
|
||||
x="91.760162"
|
||||
y="128.56021"
|
||||
style="stroke-width:0.26458332">...</tspan></text>
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="156.75853"
|
||||
cx="51.475361"
|
||||
id="circle419"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle421"
|
||||
cx="66.821205"
|
||||
cy="156.75853"
|
||||
r="5.2774868" />
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="156.75853"
|
||||
cx="86.929558"
|
||||
id="circle423"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<text
|
||||
id="text427"
|
||||
y="157.13483"
|
||||
x="75.35598"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
xml:space="preserve"><tspan
|
||||
style="stroke-width:0.26458332"
|
||||
y="157.13483"
|
||||
x="75.35598"
|
||||
id="tspan425"
|
||||
sodipodi:role="line">...</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="27.926701"
|
||||
y="129.21989"
|
||||
id="text431"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan429"
|
||||
x="27.926701"
|
||||
y="129.21989"
|
||||
style="stroke-width:0.26458332">DeviceGroup(s)</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="23.308899"
|
||||
y="158.24608"
|
||||
id="text435"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan433"
|
||||
x="23.308899"
|
||||
y="158.24608"
|
||||
style="stroke-width:0.26458332">Device(s)</tspan></text>
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="129.50328"
|
||||
cx="124.39263"
|
||||
id="circle437"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle439"
|
||||
cx="139.73848"
|
||||
cy="129.50328"
|
||||
r="5.2774868" />
|
||||
<circle
|
||||
r="5.2774868"
|
||||
cy="129.50328"
|
||||
cx="159.84683"
|
||||
id="circle441"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<text
|
||||
id="text445"
|
||||
y="129.87958"
|
||||
x="148.27325"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
xml:space="preserve"><tspan
|
||||
style="stroke-width:0.26458332"
|
||||
y="129.87958"
|
||||
x="148.27325"
|
||||
id="tspan443"
|
||||
sodipodi:role="line">...</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="169.97905"
|
||||
y="130.53928"
|
||||
id="text449"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan447"
|
||||
x="169.97905"
|
||||
y="130.53928"
|
||||
style="stroke-width:0.26458332">UserDashboard(s)</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 87.29843,99.973818 98.073298,84.581148"
|
||||
id="path451"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 105.54974,82.821988 135.45549,100.8534"
|
||||
id="path453"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 70.586387,123.06283 9.895289,-14.73299"
|
||||
id="path455"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 82.900523,122.84293 0.659686,-13.41361"
|
||||
id="path457"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 88.397906,108.54974 12.314134,14.95288"
|
||||
id="path459"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 125.7801,124.3822 10.33508,-16.49215"
|
||||
id="path461"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 140.07329,123.72251 0.2199,-14.29319"
|
||||
id="path463"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 144.4712,107.23037 12.31413,17.81151"
|
||||
id="path465"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 52.77487,152.089 64.649213,132.07853"
|
||||
id="path467"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 66.848168,151.42932 0.439791,-18.25131"
|
||||
id="path469"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 84.43979,151.42932 71.465967,131.85864"
|
||||
id="path471"
|
||||
inkscape:connector-curvature="0" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#888a85;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="circle475"
|
||||
cx="115.94864"
|
||||
cy="57.534035"
|
||||
r="5.2774868" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#888a85;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="117.36957"
|
||||
y="44.560207"
|
||||
id="text479"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan477"
|
||||
x="117.89874"
|
||||
y="44.560207"
|
||||
style="font-size:4.23333311px;text-align:center;text-anchor:middle;fill:#888a85;stroke-width:0.26458332">/ </tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="117.36957"
|
||||
y="49.851875"
|
||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, Italic';text-align:center;text-anchor:middle;fill:#888a85;stroke-width:0.26458332"
|
||||
id="tspan623">(root guardian)</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:1.25;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, ';letter-spacing:0px;word-spacing:0px;fill:#888a85;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="75.67942"
|
||||
y="77.633873"
|
||||
id="text483"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan481"
|
||||
x="75.67942"
|
||||
y="77.633873"
|
||||
style="text-align:center;text-anchor:middle;fill:#888a85;stroke-width:0.26458332">/user</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="75.67942"
|
||||
y="82.925537"
|
||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Franklin Gothic Medium';-inkscape-font-specification:'Franklin Gothic Medium, Italic';text-align:center;text-anchor:middle;fill:#888a85;stroke-width:0.26458332"
|
||||
id="tspan625">(user guardian)</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#888a85;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 103.4813,74.596486 9.76477,-13.104341"
|
||||
id="path487"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
18
akka-docs/src/main/paradox/typed/guide/index.md
Normal file
18
akka-docs/src/main/paradox/typed/guide/index.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Getting Started Guide
|
||||
|
||||
@@toc { depth=2 }
|
||||
|
||||
@@@ index
|
||||
|
||||
* [introduction](introduction.md)
|
||||
* [actors-motivation](actors-motivation.md)
|
||||
* [actors-intro](actors-intro.md)
|
||||
* [modules](modules.md)
|
||||
* [tutorial](tutorial.md)
|
||||
* [part1](tutorial_1.md)
|
||||
* [part2](tutorial_2.md)
|
||||
* [part3](tutorial_3.md)
|
||||
* [part4](tutorial_4.md)
|
||||
* [part5](tutorial_5.md)
|
||||
|
||||
@@@
|
||||
44
akka-docs/src/main/paradox/typed/guide/introduction.md
Normal file
44
akka-docs/src/main/paradox/typed/guide/introduction.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Introduction to Akka
|
||||
|
||||
Welcome to Akka, a set of open-source libraries for designing scalable, resilient systems that span processor cores and networks. Akka allows you to focus on meeting business needs instead of writing low-level code to provide reliable behavior, fault tolerance, and high performance.
|
||||
|
||||
Many common practices and accepted programming models do not address important challenges
|
||||
inherent in designing systems for modern computer architectures. To be
|
||||
successful, distributed systems must cope in an environment where components
|
||||
crash without responding, messages get lost without a trace on the wire, and
|
||||
network latency fluctuates. These problems occur regularly in carefully managed
|
||||
intra-datacenter environments - even more so in virtualized architectures.
|
||||
|
||||
To help you deal with these realities, Akka provides:
|
||||
|
||||
* Multi-threaded behavior without the use of low-level concurrency constructs like
|
||||
atomics or locks — relieving you from even thinking about memory visibility issues.
|
||||
* Transparent remote communication between systems and their components — relieving you from writing and maintaining difficult networking code.
|
||||
* A clustered, high-availability architecture that is elastic, scales in or out, on demand — enabling you to deliver a truly reactive system.
|
||||
|
||||
Akka's use of the actor model provides a level of abstraction that makes it
|
||||
easier to write correct concurrent, parallel and distributed systems. The actor
|
||||
model spans the full set of Akka libraries, providing you with a consistent way
|
||||
of understanding and using them. Thus, Akka offers a depth of integration that
|
||||
you cannot achieve by picking libraries to solve individual problems and trying
|
||||
to piece them together.
|
||||
|
||||
By learning Akka and how to use the actor model, you will gain access to a vast
|
||||
and deep set of tools that solve difficult distributed/parallel systems problems
|
||||
in a uniform programming model where everything fits together tightly and
|
||||
efficiently.
|
||||
|
||||
## How to get started
|
||||
|
||||
If this is your first experience with Akka, we recommend that you start by
|
||||
running a simple Hello World project. See the @scala[[Quickstart Guide](http://developer.lightbend.com/guides/akka-quickstart-scala)] @java[[Quickstart Guide](http://developer.lightbend.com/guides/akka-quickstart-java)] for
|
||||
instructions on downloading and running the Hello World example. The *Quickstart* guide walks you through example code that introduces how to define actor systems, actors, and messages as well as how to use the test module and logging. Within 30 minutes, you should be able to run the Hello World example and learn how it is constructed.
|
||||
|
||||
FIXME update link to Quickstart to the Typed version, when it's ready, issue https://github.com/akka/akka/issues/25997
|
||||
|
||||
This *Getting Started* guide provides the next level of information. It covers why the actor model fits the needs of modern distributed systems and includes a tutorial that will help further your knowledge of Akka. Topics include:
|
||||
|
||||
* @ref:[Why modern systems need a new programming model](actors-motivation.md)
|
||||
* @ref:[How the actor model meets the needs of concurrent, distributed systems](actors-intro.md)
|
||||
* @ref:[Overview of Akka libraries and modules](modules.md)
|
||||
* A @ref:[more complex example](tutorial.md) that builds on the Hello World example to illustrate common Akka patterns.
|
||||
224
akka-docs/src/main/paradox/typed/guide/modules.md
Normal file
224
akka-docs/src/main/paradox/typed/guide/modules.md
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
# Overview of Akka libraries and modules
|
||||
|
||||
Before delving into some best practices for writing actors, it will be helpful to preview the most commonly used Akka libraries. This will help you start thinking about the functionality you want to use in your system. All core Akka functionality is available as Open Source Software (OSS). Lightbend sponsors Akka development but can also help you with [commercial offerings ](https://www.lightbend.com/platform/subscription) such as training, consulting, support, and [Enterprise Suite](https://www.lightbend.com/platform/production) — a comprehensive set of tools for managing Akka systems.
|
||||
|
||||
The following capabilities are included with Akka OSS and are introduced later on this page:
|
||||
|
||||
* [Actor library](#actor-library)
|
||||
* [Remoting](#remoting)
|
||||
* [Cluster](#cluster)
|
||||
* [Cluster Sharding](#cluster-sharding)
|
||||
* [Cluster Singleton](#cluster-singleton)
|
||||
* [Persistence](#persistence)
|
||||
* [Distributed Data](#distributed-data)
|
||||
* [Streams](#streams)
|
||||
* [HTTP](#http)
|
||||
|
||||
With a Lightbend subscription, you can use [Enterprise Suite](https://www.lightbend.com/platform/production) in production. Enterprise Suite includes the following extensions to Akka core functionality:
|
||||
|
||||
* [Split Brain Resolver](https://developer.lightbend.com/docs/akka-commercial-addons/current/split-brain-resolver.html) — Detects and recovers from network partitions, eliminating data inconsistencies and possible downtime.
|
||||
* [Multi-DC Persistence](https://developer.lightbend.com/docs/akka-commercial-addons/current/persistence-dc/index.html) — For active-active persistent entities across multiple data centers.
|
||||
* [GDPR for Akka Persistence](https://developer.lightbend.com/docs/akka-commercial-addons/current/gdpr/index.html) — Data shredding can be used to forget information in events.
|
||||
* [Fast Failover](https://developer.lightbend.com/docs/akka-commercial-addons/current/fast-failover.html) — Fast failover for Cluster Sharding.
|
||||
* [Configuration Checker](https://developer.lightbend.com/docs/akka-commercial-addons/current/config-checker.html) — Checks for potential configuration issues and logs suggestions.
|
||||
* [Diagnostics Recorder](https://developer.lightbend.com/docs/akka-commercial-addons/current/diagnostics-recorder.html) — Captures configuration and system information in a format that makes it easy to troubleshoot issues during development and production.
|
||||
* [Thread Starvation Detector](https://developer.lightbend.com/docs/akka-commercial-addons/current/starvation-detector.html) — Monitors an Akka system dispatcher and logs warnings if it becomes unresponsive.
|
||||
|
||||
This page does not list all available modules, but overviews the main functionality and gives you an idea of the level of sophistication you can reach when you start building systems on top of Akka.
|
||||
|
||||
### Actor library
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group=com.typesafe.akka
|
||||
artifact=akka-actor-typed_$scala.binary_version$
|
||||
version=$akka.version$
|
||||
}
|
||||
|
||||
The core Akka library is `akka-actor-typed`, but actors are used across Akka libraries, providing a consistent, integrated model that relieves you from individually
|
||||
solving the challenges that arise in concurrent or distributed system design. From a birds-eye view,
|
||||
actors are a programming paradigm that takes encapsulation, one of the pillars of OOP, to its extreme.
|
||||
Unlike objects, actors encapsulate not only their
|
||||
state but their execution. Communication with actors is not via method calls but by passing messages. While this
|
||||
difference may seem minor, it is actually what allows us to break clean from the limitations of OOP when it
|
||||
comes to concurrency and remote communication. Don’t worry if this description feels too high level to fully grasp
|
||||
yet, in the next chapter we will explain actors in detail. For now, the important point is that this is a model that
|
||||
handles concurrency and distribution at the fundamental level instead of ad hoc patched attempts to bring these
|
||||
features to OOP.
|
||||
|
||||
Challenges that actors solve include the following:
|
||||
|
||||
* How to build and design high-performance, concurrent applications.
|
||||
* How to handle errors in a multi-threaded environment.
|
||||
* How to protect my project from the pitfalls of concurrency.
|
||||
|
||||
### Remoting
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group=com.typesafe.akka
|
||||
artifact=akka-remote_$scala.binary_version$
|
||||
version=$akka.version$
|
||||
}
|
||||
|
||||
Remoting enables actors that live on different computers to seamlessly exchange messages.
|
||||
While distributed as a JAR artifact, Remoting resembles a module more than it does a library. You enable it mostly
|
||||
with configuration and it has only a few APIs. Thanks to the actor model, a remote and local message send looks exactly the
|
||||
same. The patterns that you use on local systems translate directly to remote systems.
|
||||
You will rarely need to use Remoting directly, but it provides the foundation on which the Cluster subsystem is built.
|
||||
|
||||
Challenges Remoting solves include the following:
|
||||
|
||||
* How to address actor systems living on remote hosts.
|
||||
* How to address individual actors on remote actor systems.
|
||||
* How to turn messages to bytes on the wire.
|
||||
* How to manage low-level, network connections (and reconnections) between hosts, detect crashed actor systems and hosts,
|
||||
all transparently.
|
||||
* How to multiplex communications from an unrelated set of actors on the same network connection, all transparently.
|
||||
|
||||
### Cluster
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group=com.typesafe.akka
|
||||
artifact=akka-cluster-typed_$scala.binary_version$
|
||||
version=$akka.version$
|
||||
}
|
||||
|
||||
If you have a set of actor systems that cooperate to solve some business problem, then you likely want to manage these set of
|
||||
systems in a disciplined way. While Remoting solves the problem of addressing and communicating with components of
|
||||
remote systems, Clustering gives you the ability to organize these into a "meta-system" tied together by a membership
|
||||
protocol. **In most cases, you want to use the Cluster module instead of using Remoting directly.**
|
||||
Clustering provides an additional set of services on top of Remoting that most real world applications need.
|
||||
|
||||
Challenges the Cluster module solves include the following:
|
||||
|
||||
* How to maintain a set of actor systems (a cluster) that can communicate with each other and consider each other as part of the cluster.
|
||||
* How to introduce a new system safely to the set of already existing members.
|
||||
* How to reliably detect systems that are temporarily unreachable.
|
||||
* How to remove failed hosts/systems (or scale down the system) so that all remaining members agree on the remaining subset of the cluster.
|
||||
* How to distribute computations among the current set of members.
|
||||
* How to designate members of the cluster to a certain role, in other words, to provide certain services and not others.
|
||||
|
||||
### Cluster Sharding
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group=com.typesafe.akka
|
||||
artifact=akka-cluster-sharding-typed_$scala.binary_version$
|
||||
version=$akka.version$
|
||||
}
|
||||
|
||||
Sharding helps to solve the problem of distributing a set of actors among members of an Akka cluster.
|
||||
Sharding is a pattern that mostly used together with Persistence to balance a large set of persistent entities
|
||||
(backed by actors) to members of a cluster and also migrate them to other nodes when members crash or leave.
|
||||
|
||||
Challenges that Sharding solves include the following:
|
||||
|
||||
* How to model and scale out a large set of stateful entities on a set of systems.
|
||||
* How to ensure that entities in the cluster are distributed properly so that load is properly balanced across the machines.
|
||||
* How to ensure migrating entities from a crashed system without losing the state.
|
||||
* How to ensure that an entity does not exist on multiple systems at the same time and hence keeps consistent.
|
||||
|
||||
### Cluster Singleton
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group=com.typesafe.akka
|
||||
artifact=akka-cluster-singleton_$scala.binary_version$
|
||||
version=$akka.version$
|
||||
}
|
||||
|
||||
A common (in fact, a bit too common) use case in distributed systems is to have a single entity responsible
|
||||
for a given task which is shared among other members of the cluster and migrated if the host system fails.
|
||||
While this undeniably introduces a common bottleneck for the whole cluster that limits scaling,
|
||||
there are scenarios where the use of this pattern is unavoidable. Cluster singleton allows a cluster to select an
|
||||
actor system which will host a particular actor while other systems can always access said service independently from
|
||||
where it is.
|
||||
|
||||
The Singleton module can be used to solve these challenges:
|
||||
|
||||
* How to ensure that only one instance of a service is running in the whole cluster.
|
||||
* How to ensure that the service is up even if the system hosting it currently crashes or shuts down during the process of scaling down.
|
||||
* How to reach this instance from any member of the cluster assuming that it can migrate to other systems over time.
|
||||
|
||||
### Persistence
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group=com.typesafe.akka
|
||||
artifact=akka-persistence-typed_$scala.binary_version$
|
||||
version=$akka.version$
|
||||
}
|
||||
|
||||
Just like objects in OOP, actors keep their state in volatile memory. Once the system is shut down, gracefully or
|
||||
because of a crash, all data that was in memory is lost. Persistence provides patterns to enable actors to persist
|
||||
events that lead to their current state. Upon startup, events can be replayed to restore the state of the entity hosted
|
||||
by the actor. The event stream can be queried and fed into additional processing pipelines (an external Big Data
|
||||
cluster for example) or alternate views (like reports).
|
||||
|
||||
Persistence tackles the following challenges:
|
||||
|
||||
* How to restore the state of an entity/actor when system restarts or crashes.
|
||||
* How to implement a [CQRS system](https://msdn.microsoft.com/en-us/library/jj591573.aspx).
|
||||
* How to ensure reliable delivery of messages in face of network errors and system crashes.
|
||||
* How to introspect domain events that have led an entity to its current state.
|
||||
* How to leverage [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html) in your application to support long-running processes while the project continues to evolve.
|
||||
|
||||
### Distributed Data
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group=com.typesafe.akka
|
||||
artifact=akka-cluster-typed_$scala.binary_version$
|
||||
version=$akka.version$
|
||||
}
|
||||
|
||||
In situations where eventual consistency is acceptable, it is possible to share data between nodes in
|
||||
an Akka Cluster and accept both reads and writes even in the face of cluster partitions. This can be
|
||||
achieved using [Conflict Free Replicated Data Types](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) (CRDTs), where writes on different nodes can
|
||||
happen concurrently and are merged in a predictable way afterward. The Distributed Data module
|
||||
provides infrastructure to share data and a number of useful data types.
|
||||
|
||||
Distributed Data is intended to solve the following challenges:
|
||||
|
||||
* How to accept writes even in the face of cluster partitions.
|
||||
* How to share data while at the same time ensuring low-latency local read and write access.
|
||||
|
||||
### Streams
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group=com.typesafe.akka
|
||||
artifact=akka-stream-typed_$scala.binary_version$
|
||||
version=$akka.version$
|
||||
}
|
||||
|
||||
Actors are a fundamental model for concurrency, but there are common patterns where their use requires the user
|
||||
to implement the same pattern over and over. Very common is the scenario where a chain, or graph, of actors, need to
|
||||
process a potentially large, or infinite, stream of sequential events and properly coordinate resource usage so that
|
||||
faster processing stages do not overwhelm slower ones in the chain or graph. Streams provide a higher-level
|
||||
abstraction on top of actors that simplifies writing such processing networks, handling all the fine details in the
|
||||
background and providing a safe, typed, composable programming model. Streams is also an implementation
|
||||
of the [Reactive Streams standard](http://www.reactive-streams.org) which enables integration with all third
|
||||
party implementations of that standard.
|
||||
|
||||
Streams solve the following challenges:
|
||||
|
||||
* How to handle streams of events or large datasets with high performance, exploiting concurrency and keeping resource usage tight.
|
||||
* How to assemble reusable pieces of event/data processing into flexible pipelines.
|
||||
* How to connect asynchronous services in a flexible way to each other with high performance.
|
||||
* How to provide or consume Reactive Streams compliant interfaces to interface with a third party library.
|
||||
|
||||
### HTTP
|
||||
|
||||
[Akka HTTP](https://doc.akka.io/docs/akka-http/current) is a separate module from Akka.
|
||||
|
||||
The de facto standard for providing APIs remotely, internal or external, is [HTTP](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol). Akka provides a library to construct or consume such HTTP services by giving a set of tools to create HTTP services (and serve them) and a client that can be
|
||||
used to consume other services. These tools are particularly suited to streaming in and out a large set of data or real-time events by leveraging the underlying model of Akka Streams.
|
||||
|
||||
Some of the challenges that HTTP tackles:
|
||||
|
||||
* How to expose services of a system or cluster to the external world via an HTTP API in a performant way.
|
||||
* How to stream large datasets in and out of a system using HTTP.
|
||||
* How to stream live events in and out of a system using HTTP.
|
||||
|
||||
### Example of module use
|
||||
|
||||
Akka modules integrate together seamlessly. For example, think of a large set of stateful business objects, such as documents or shopping carts, that website users access. If you model these as sharded entities, using Sharding and Persistence, they will be balanced across a cluster that you can scale out on-demand. They will be available during spikes that come from advertising campaigns or before holidays will be handled, even if some systems crash. You can also take the real-time stream of domain events with Persistence Query and use Streams to pipe them into a streaming Fast Data engine. Then, take the output of that engine as a Stream, manipulate it using Akka Streams
|
||||
operators and expose it as web socket connections served by a load balanced set of HTTP servers hosted by your cluster
|
||||
to power your real-time business analytics tool.
|
||||
|
||||
We hope this preview caught your interest! The next topic introduces the example application we will build in the tutorial portion of this guide.
|
||||
40
akka-docs/src/main/paradox/typed/guide/tutorial.md
Normal file
40
akka-docs/src/main/paradox/typed/guide/tutorial.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Introduction to the Example
|
||||
|
||||
When writing prose, the hardest part is often composing the first few sentences. There is a similar "blank canvas" feeling
|
||||
when starting to build an Akka system. You might wonder: Which should be the first actor? Where should it live? What should it do?
|
||||
Fortunately — unlike with prose — established best practices can guide us through these initial steps. In the remainder of this guide, we examine the core logic of a simple Akka application to introduce you to actors and show you how to formulate solutions with them. The example demonstrates common patterns that will help you kickstart your Akka projects.
|
||||
|
||||
## Prerequisites
|
||||
You should have already followed the instructions in the @scala[[Akka Quickstart with Scala guide](http://developer.lightbend.com/guides/akka-quickstart-scala/)] @java[[Akka Quickstart with Java guide](http://developer.lightbend.com/guides/akka-quickstart-java/)] to download and run the Hello World example. You will use this as a seed project and add the functionality described in this tutorial.
|
||||
|
||||
FIXME update link to Quickstart to the Typed version, when it's ready, issue https://github.com/akka/akka/issues/25997
|
||||
|
||||
## IoT example use case
|
||||
|
||||
In this tutorial, we'll use Akka to build out part of an Internet of Things (IoT) system that reports data from sensor devices installed in customers' homes. The example focuses on temperature readings. The target use case allows customers to log in and view the last reported temperature from different areas of their homes. You can imagine that such sensors could also collect relative humidity or other interesting data and an application would likely support reading and changing device configuration, maybe even alerting home owners when sensor state falls outside of a particular range.
|
||||
|
||||
In a real system, the application would be exposed to customers through a mobile app or browser. This guide concentrates only on the core logic for storing temperatures that would be called over a network protocol, such as HTTP. It also includes writing tests to help you get comfortable and proficient with testing actors.
|
||||
|
||||
The tutorial application consists of two main components:
|
||||
|
||||
* **Device data collection:** — maintains a local representation of the
|
||||
remote devices. Multiple sensor devices for a home are organized into one device group.
|
||||
* **User dashboard:** — periodically collects data from the devices for a
|
||||
logged in user's home and presents the results as a report.
|
||||
|
||||
The following diagram illustrates the example application architecture. Since we are interested in the state of each sensor device, we will model devices as actors. The running application will create as many instances of device actors and device groups as necessary.
|
||||
|
||||

|
||||
|
||||
## What you will learn in this tutorial
|
||||
This tutorial introduces and illustrates:
|
||||
|
||||
* The actor hierarchy and how it influences actor behavior
|
||||
* How to choose the right granularity for actors
|
||||
* How to define protocols as messages
|
||||
* Typical conversational styles
|
||||
|
||||
|
||||
Let's get started by learning more about actors.
|
||||
|
||||
|
||||
152
akka-docs/src/main/paradox/typed/guide/tutorial_1.md
Normal file
152
akka-docs/src/main/paradox/typed/guide/tutorial_1.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Part 1: Actor Architecture
|
||||
|
||||
## Dependency
|
||||
|
||||
Add the following dependency in your project:
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group="com.typesafe.akka"
|
||||
artifact="akka-actor-typed_$scala.binary_version$"
|
||||
version="$akka.version$"
|
||||
}
|
||||
|
||||
## Introduction
|
||||
|
||||
Use of Akka relieves you from creating the infrastructure for an actor system and from writing the low-level code necessary to control basic behavior. To appreciate this, let's look at the relationships between actors you create in your code and those that Akka creates and manages for you internally, the actor lifecycle, and failure handling.
|
||||
|
||||
## The Akka actor hierarchy
|
||||
|
||||
An actor in Akka always belongs to a parent. You create an actor by calling @java[`getContext().spawn()`]@scala[`context.spawn()`]. The creator actor becomes the
|
||||
_parent_ of the newly created _child_ actor. You might ask then, who is the parent of the _first_ actor you create?
|
||||
|
||||
As illustrated below, all actors have a common parent, the user guardian, which is defined and created when you start the `ActorSystem`.
|
||||
As we covered in the @scala[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-scala/)]@java[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-java/)], creation of an actor returns a reference that is a valid URL. So, for example, if we create an actor named `someActor` from the user guardian with `context.spawn(someBehavior, "someActor")`, its reference will include the path `/user/someActor`.
|
||||
|
||||
FIXME update link to Quickstart to the Typed version, when it's ready, issue https://github.com/akka/akka/issues/25997
|
||||
|
||||

|
||||
|
||||
In fact, before you create an actor in your code, Akka has already created three actors in the system. The names of these built-in actors contain _guardian_ because they _supervise_ every child actor in their path. The guardian actors include:
|
||||
|
||||
- `/` the so-called _root guardian_. This is the parent of all actors in the system, and the last one to stop when the system itself is terminated.
|
||||
- `/user` the _guardian_. **This is the parent actor for all user created actors**. Don't let the name `user` confuse
|
||||
you, it has nothing to do with end users, nor with user handling. Every actor you create using the Akka library will have the constant path `/user/` prepended to it.
|
||||
- `/system` the _system guardian_. Akka or other libraries built on top of Akka may create actors in the _system_ namespace.
|
||||
|
||||
The easiest way to see the actor hierarchy in action is to print `ActorRef` instances. In this small experiment, we create an actor, print its reference, create a child of this actor, and print the child's reference. We start with the Hello World project, if you have not downloaded it, download the Quickstart project from the @scala[[Lightbend Tech Hub](http://developer.lightbend.com/start/?group=akka&project=akka-quickstart-scala)]@java[[Lightbend Tech Hub](http://developer.lightbend.com/start/?group=akka&project=akka-quickstart-java)].
|
||||
|
||||
FIXME update link to Quickstart to the Typed version, when it's ready, issue https://github.com/akka/akka/issues/25997
|
||||
|
||||
In your Hello World project, navigate to the `com.lightbend.akka.sample` package and create a new @scala[Scala file called `ActorHierarchyExperiments.scala`]@java[Java file called `ActorHierarchyExperiments.java`] here. Copy and paste the code from the snippet below to this new source file. Save your file and run `sbt "runMain com.lightbend.akka.sample.ActorHierarchyExperiments"` to observe the output.
|
||||
|
||||
Scala
|
||||
: @@snip [ActorHierarchyExperiments.scala](/akka-docs/src/test/scala/typed/tutorial_1/ActorHierarchyExperiments.scala) { #print-refs }
|
||||
|
||||
Java
|
||||
: @@snip [ActorHierarchyExperiments.java](/akka-docs/src/test/java/jdocs/typed/tutorial_1/ActorHierarchyExperiments.java) { #print-refs }
|
||||
|
||||
Note the way a message asked the first actor to do its work. We sent the message by using the parent's reference: @scala[`firstRef ! "printit"`]@java[`firstRef.tell("printit", ActorRef.noSender())`]. When the code executes, the output includes the references for the first actor and the child it created as part of the `printit` case. Your output should look similar to the following:
|
||||
|
||||
```
|
||||
First: Actor[akka://testSystem/user/first-actor#1053618476]
|
||||
Second: Actor[akka://testSystem/user/first-actor/second-actor#-1544706041]
|
||||
```
|
||||
|
||||
Notice the structure of the references:
|
||||
|
||||
* Both paths start with `akka://testSystem/`. Since all actor references are valid URLs, `akka://` is the value of the protocol field.
|
||||
* Next, just like on the World Wide Web, the URL identifies the system. In this example, the system is named `testSystem`, but it could be any other name. If remote communication between multiple systems is enabled, this part of the URL includes the hostname so other systems can find it on the network.
|
||||
* Because the second actor's reference includes the path `/first-actor/`, it identifies it as a child of the first.
|
||||
* The last part of the actor reference, `#1053618476` or `#-1544706041` is a unique identifier that you can ignore in most cases.
|
||||
|
||||
Now that you understand what the actor hierarchy
|
||||
looks like, you might be wondering: _Why do we need this hierarchy? What is it used for?_
|
||||
|
||||
An important role of the hierarchy is to safely manage actor lifecycles. Let's consider this next and see how that knowledge can help us write better code.
|
||||
|
||||
### The actor lifecycle
|
||||
Actors pop into existence when created, then later, at user requests, they are stopped. Whenever an actor is stopped, all of its children are _recursively stopped_ too.
|
||||
This behavior greatly simplifies resource cleanup and helps avoid resource leaks such as those caused by open sockets and files. In fact, a commonly overlooked difficulty when dealing with low-level multi-threaded code is the lifecycle management of various concurrent resources.
|
||||
|
||||
To stop an actor, the recommended pattern is to return `Behaviors.stopped()` inside the actor to stop itself, usually as a response to some user defined stop message or when the actor is done with its job. Stopping a child actor is technically possible by calling `context.stop(childRef)` from the parent, but it's not possible to stop arbitrary (non-child) actors this way.
|
||||
|
||||
The Akka actor API exposes some lifecycle signals, for example `PostStop` is sent just before the actor stops. No messages are processed after this point.
|
||||
|
||||
Let's use the `PostStop` lifecycle signal in a simple experiment to observe the behavior when we stop an actor. First, add the following 2 actor classes to your project:
|
||||
|
||||
Scala
|
||||
: @@snip [ActorHierarchyExperiments.scala](/akka-docs/src/test/scala/typed/tutorial_1/ActorHierarchyExperiments.scala) { #start-stop }
|
||||
|
||||
Java
|
||||
: @@snip [ActorHierarchyExperiments.java](/akka-docs/src/test/java/jdocs/typed/tutorial_1/ActorHierarchyExperiments.java) { #start-stop }
|
||||
|
||||
And create a 'main' class like above to start the actors and then send them a `"stop"` message:
|
||||
|
||||
Scala
|
||||
: @@snip [ActorHierarchyExperiments.scala](/akka-docs/src/test/scala/typed/tutorial_1/ActorHierarchyExperiments.scala) { #start-stop-main }
|
||||
|
||||
Java
|
||||
: @@snip [ActorHierarchyExperiments.java](/akka-docs/src/test/java/jdocs/typed/tutorial_1/ActorHierarchyExperiments.java) { #start-stop-main }
|
||||
|
||||
You can again use `sbt` to start this program. The output should look like this:
|
||||
|
||||
```
|
||||
first started
|
||||
second started
|
||||
second stopped
|
||||
first stopped
|
||||
```
|
||||
|
||||
When we stopped actor `first`, it stopped its child actor, `second`, before stopping itself. This ordering is strict, _all_ `PostStop` signals of the children are processed before the `PostStop` signal of the parent
|
||||
is processed.
|
||||
|
||||
### Failure handling
|
||||
|
||||
Parents and children are connected throughout their lifecycles. Whenever an actor fails (throws an exception or an unhandled exception bubbles out from @scala[`onMessage`]@java[`Receive`]) the failure information is propagated
|
||||
to the supervision strategy, which then decides how to handle the exception caused by the actor. The supervision strategy is typically defined by the parent actor when it spawns a child actor. In this way, parents act as supervisors for their children. The default _supervisor strategy_ is to stop the child. If you don't define the strategy all failures result in a stop.
|
||||
|
||||
Let's observe a restart supervision strategy in a simple experiment. Add the following classes to your project, just as you did with the previous ones:
|
||||
|
||||
Scala
|
||||
: @@snip [ActorHierarchyExperiments.scala](/akka-docs/src/test/scala/typed/tutorial_1/ActorHierarchyExperiments.scala) { #supervise }
|
||||
|
||||
Java
|
||||
: @@snip [ActorHierarchyExperiments.java](/akka-docs/src/test/java/jdocs/typed/tutorial_1/ActorHierarchyExperiments.java) { #supervise }
|
||||
|
||||
And run with:
|
||||
|
||||
Scala
|
||||
: @@snip [ActorHierarchyExperiments.scala](/akka-docs/src/test/scala/typed/tutorial_1/ActorHierarchyExperiments.scala) { #supervise-main }
|
||||
|
||||
Java
|
||||
: @@snip [ActorHierarchyExperiments.java](/akka-docs/src/test/java/jdocs/typed/tutorial_1/ActorHierarchyExperiments.java) { #supervise-main }
|
||||
|
||||
You should see output similar to the following:
|
||||
|
||||
```
|
||||
supervised actor started
|
||||
supervised actor fails now
|
||||
supervised actor will be restarted
|
||||
supervised actor started
|
||||
[ERROR] [11/12/2018 12:03:27.171] [ActorHierarchyExperiments-akka.actor.default-dispatcher-2] [akka://ActorHierarchyExperiments/user/supervising-actor/supervised-actor] Supervisor akka.actor.typed.internal.RestartSupervisor@1c452254 saw failure: I failed!
|
||||
java.lang.Exception: I failed!
|
||||
at typed.tutorial_1.SupervisedActor.onMessage(ActorHierarchyExperiments.scala:113)
|
||||
at typed.tutorial_1.SupervisedActor.onMessage(ActorHierarchyExperiments.scala:106)
|
||||
at akka.actor.typed.scaladsl.AbstractBehavior.receive(AbstractBehavior.scala:59)
|
||||
at akka.actor.typed.Behavior$.interpret(Behavior.scala:395)
|
||||
at akka.actor.typed.Behavior$.interpretMessage(Behavior.scala:369)
|
||||
at akka.actor.typed.internal.InterceptorImpl$$anon$2.apply(InterceptorImpl.scala:49)
|
||||
at akka.actor.typed.internal.SimpleSupervisor.aroundReceive(Supervision.scala:85)
|
||||
at akka.actor.typed.internal.InterceptorImpl.receive(InterceptorImpl.scala:70)
|
||||
at akka.actor.typed.Behavior$.interpret(Behavior.scala:395)
|
||||
at akka.actor.typed.Behavior$.interpretMessage(Behavior.scala:369)
|
||||
```
|
||||
|
||||
We see that after failure the supervised actor is stopped and immediately restarted. We also see a log entry reporting the exception that was handled, in this case, our test exception. In this example we also used the `PreRestart` signal which is processed before restarts.
|
||||
|
||||
For the impatient, we also recommend looking into the @ref:[fault tolerance reference page](../fault-tolerance.md) for more in-depth
|
||||
details.
|
||||
|
||||
# Summary
|
||||
We've learned about how Akka manages actors in hierarchies where parents supervise their children and handle exceptions. We saw how to create a very simple actor and child. Next, we'll apply this knowledge to our example use case by modeling the communication necessary to get information from device actors. Later, we'll deal with how to manage the actors in groups.
|
||||
|
||||
39
akka-docs/src/main/paradox/typed/guide/tutorial_2.md
Normal file
39
akka-docs/src/main/paradox/typed/guide/tutorial_2.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Part 2: Creating the First Actor
|
||||
|
||||
## Introduction
|
||||
|
||||
With an understanding of actor hierarchy and behavior, the remaining question is how to map the top-level components of our IoT system to actors. The _user guardian_ can be an actor that represents the whole application. In other words, we will have a single top-level actor in our IoT system. The components that create and manage devices and dashboards will be children of this actor. This allows us to refactor the example use case architecture diagram into a tree of actors:
|
||||
|
||||

|
||||
|
||||
We can define the first actor, the IotSupervisor, with a few lines of code. To start your tutorial application:
|
||||
|
||||
1. Create a new `IotSupervisor` source file in the `com.lightbend.akka.sample` package.
|
||||
1. Paste the following code into the new file to define the IotSupervisor.
|
||||
|
||||
Scala
|
||||
: @@snip [IotSupervisor.scala](/akka-docs/src/test/scala/typed/tutorial_2/IotSupervisor.scala) { #iot-supervisor }
|
||||
|
||||
Java
|
||||
: @@snip [IotSupervisor.java](/akka-docs/src/test/java/jdocs/typed/tutorial_2/IotSupervisor.java) { #iot-supervisor }
|
||||
|
||||
The code is similar to the actor examples we used in the previous experiments, but notice that instead of `println()` we use Akka's built in logging facility via @scala[context.log]@java[context.getLog()].
|
||||
|
||||
To provide the `main` entry point that creates the actor system, add the following code to the new @scala[`IotApp` object] @java[`IotMain` class].
|
||||
|
||||
Scala
|
||||
: @@snip [IotApp.scala](/akka-docs/src/test/scala/typed/tutorial_2/IotApp.scala) { #iot-app }
|
||||
|
||||
Java
|
||||
: @@snip [IotMain.java](/akka-docs/src/test/java/jdocs/typed/tutorial_2/IotMain.java) { #iot-app }
|
||||
|
||||
The application does little, other than log that it is started. But, we have the first actor in place and we are ready to add other actors.
|
||||
|
||||
## What's next?
|
||||
|
||||
In the following chapters we will grow the application gradually, by:
|
||||
|
||||
1. Creating the representation for a device.
|
||||
2. Creating the device management component.
|
||||
3. Adding query capabilities to device groups.
|
||||
|
||||
193
akka-docs/src/main/paradox/typed/guide/tutorial_3.md
Normal file
193
akka-docs/src/main/paradox/typed/guide/tutorial_3.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# Part 3: Working with Device Actors
|
||||
|
||||
## Introduction
|
||||
|
||||
In the previous topics we explained how to view actor systems _in the large_, that is, how components should be represented, how actors should be arranged in the hierarchy. In this part, we will look at actors _in the small_ by implementing the device actor.
|
||||
|
||||
If we were working with objects, we would typically design the API as _interfaces_, a collection of abstract methods to be filled out by the actual implementation. In the world of actors, protocols take the place of interfaces. While it is not possible to formalize general protocols in the programming language, we can compose their most basic element, messages. So, we will start by identifying the messages we will want to send to device actors.
|
||||
|
||||
Typically, messages fall into categories, or patterns. By identifying these patterns, you will find that it becomes easier to choose between them and to implement them. The first example demonstrates the _request-respond_ message pattern.
|
||||
|
||||
## Identifying messages for devices
|
||||
The tasks of a device actor will be simple:
|
||||
|
||||
* Collect temperature measurements
|
||||
* When asked, report the last measured temperature
|
||||
|
||||
However, a device might start without immediately having a temperature measurement. Hence, we need to account for the case where a temperature is not present. This also allows us to test the query part of the actor without the write part present, as the device actor can report an empty result.
|
||||
|
||||
The protocol for obtaining the current temperature from the device actor is simple. The actor:
|
||||
|
||||
1. Waits for a request for the current temperature.
|
||||
2. Responds to the request with a reply that either:
|
||||
|
||||
* contains the current temperature or,
|
||||
* indicates that a temperature is not yet available.
|
||||
|
||||
We need two messages, one for the request, and one for the reply. Our first attempt might look like the following:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceInProgress.scala](/akka-docs/src/test/scala/typed/tutorial_3/DeviceInProgress.scala) { #read-protocol-1 }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceInProgress.java](/akka-docs/src/test/java/jdocs/typed/tutorial_3/inprogress1/DeviceProtocol.java) { #read-protocol-1 }
|
||||
|
||||
Note that the `ReadTemperature` message contains the @scala[`ActorRef[RespondTemperature]`]@java[`ActorRef<RespondTemperature>`] that the device actor will use when replying to the request.
|
||||
|
||||
These two messages seem to cover the required functionality. However, the approach we choose must take into account the distributed nature of the application. While the basic mechanism is the same for communicating with an actor on the local JVM as with a remote actor, we need to keep the following in mind:
|
||||
|
||||
* There will be observable differences in the latency of delivery between local and remote messages, because factors like network link bandwidth and the message size also come into play.
|
||||
* Reliability is a concern because a remote message send involves more steps, which means that more can go wrong.
|
||||
* A local send will pass a reference to the message inside the same JVM, without any restrictions on the underlying object which is sent, whereas a remote transport will place a limit on the message size.
|
||||
|
||||
In addition, while sending inside the same JVM is significantly more reliable, if an
|
||||
actor fails due to a programmer error while processing the message, the effect is the same as if a remote network request fails due to the remote host crashing while processing the message. Even though in both cases, the service recovers after a while (the actor is restarted by its supervisor, the host is restarted by an operator or by a monitoring system) individual requests are lost during the crash. **Therefore, writing your actors such that every
|
||||
message could possibly be lost is the safe, pessimistic bet.**
|
||||
|
||||
But to further understand the need for flexibility in the protocol, it will help to consider Akka message ordering and message delivery guarantees. Akka provides the following behavior for message sends:
|
||||
|
||||
* At-most-once delivery, that is, no guaranteed delivery.
|
||||
* Message ordering is maintained per sender, receiver pair.
|
||||
|
||||
The following sections discuss this behavior in more detail:
|
||||
|
||||
* [Message delivery](#message-delivery)
|
||||
* [Message ordering](#message-ordering)
|
||||
|
||||
### Message delivery
|
||||
The delivery semantics provided by messaging subsystems typically fall into the following categories:
|
||||
|
||||
* **At-most-once delivery** — each message is delivered zero or one time; in more causal terms it means that messages can be lost, but are never duplicated.
|
||||
* **At-least-once delivery** — potentially multiple attempts are made to deliver each message, until at least one succeeds; again, in more causal terms this means that messages can be duplicated but are never lost.
|
||||
* **Exactly-once delivery** — each message is delivered exactly once to the recipient; the message can neither be lost nor be duplicated.
|
||||
|
||||
The first behavior, the one used by Akka, is the cheapest and results in the highest performance. It has the least implementation overhead because it can be done in a fire-and-forget fashion without keeping the state at the sending end or in the transport mechanism. The second, at-least-once, requires retries to counter transport losses. This adds the overhead of keeping the state at the sending end and having an acknowledgment mechanism at the receiving end. Exactly-once delivery is most expensive, and results in the worst performance: in addition to the overhead added by at-least-once delivery, it requires the state to be kept at the receiving end in order to filter out
|
||||
duplicate deliveries.
|
||||
|
||||
In an actor system, we need to determine exact meaning of a guarantee — at which point does the system consider the delivery as accomplished:
|
||||
|
||||
1. When the message is sent out on the network?
|
||||
2. When the message is received by the target actor's host?
|
||||
3. When the message is put into the target actor's mailbox?
|
||||
4. When the message target actor starts to process the message?
|
||||
5. When the target actor has successfully processed the message?
|
||||
|
||||
Most frameworks and protocols that claim guaranteed delivery actually provide something similar to points 4 and 5. While this sounds reasonable, **is it actually useful?** To understand the implications, consider a simple, practical example: a user attempts to place an order and we only want to claim that it has successfully processed once it is actually on disk in the orders database.
|
||||
|
||||
If we rely on the successful processing of the message, the actor will report success as soon as the order has been submitted to the internal API that has the responsibility to validate it, process it and put it into the database. Unfortunately,
|
||||
immediately after the API has been invoked any of the following can happen:
|
||||
|
||||
* The host can crash.
|
||||
* Deserialization can fail.
|
||||
* Validation can fail.
|
||||
* The database might be unavailable.
|
||||
* A programming error might occur.
|
||||
|
||||
This illustrates that the **guarantee of delivery** does not translate to the **domain level guarantee**. We only want to report success once the order has been actually fully processed and persisted. **The only entity that can report success is the application itself, since only it has any understanding of the domain guarantees required. No generalized framework can figure out the specifics of a particular domain and what is considered a success in that domain**.
|
||||
|
||||
In this particular example, we only want to signal success after a successful database write, where the database acknowledged that the order is now safely stored. **For these reasons Akka lifts the responsibilities of guarantees to the application
|
||||
itself, i.e. you have to implement them yourself with the tools that Akka provides. This gives you full control of the guarantees that you want to provide**. Now, let's consider the message ordering that Akka provides to make it easy to reason about application logic.
|
||||
|
||||
### Message Ordering
|
||||
|
||||
In Akka, for a given pair of actors, messages sent directly from the first to the second will not be received out-of-order. The word directly emphasizes that this guarantee only applies when sending with the tell operator directly to the final destination, but not when employing mediators.
|
||||
|
||||
If:
|
||||
|
||||
* Actor `A1` sends messages `M1`, `M2`, `M3` to `A2`.
|
||||
* Actor `A3` sends messages `M4`, `M5`, `M6` to `A2`.
|
||||
|
||||
This means that, for Akka messages:
|
||||
|
||||
* If `M1` is delivered it must be delivered before `M2` and `M3`.
|
||||
* If `M2` is delivered it must be delivered before `M3`.
|
||||
* If `M4` is delivered it must be delivered before `M5` and `M6`.
|
||||
* If `M5` is delivered it must be delivered before `M6`.
|
||||
* `A2` can see messages from `A1` interleaved with messages from `A3`.
|
||||
* Since there is no guaranteed delivery, any of the messages may be dropped, i.e. not arrive at `A2`.
|
||||
|
||||
These guarantees strike a good balance: having messages from one actor arrive in-order is convenient for building systems that can be easily reasoned about, while on the other hand allowing messages from different actors to arrive interleaved provides sufficient freedom for an efficient implementation of the actor system.
|
||||
|
||||
For the full details on delivery guarantees please refer to the @ref:[reference page](../../general/message-delivery-reliability.md).
|
||||
|
||||
## Adding flexibility to device messages
|
||||
|
||||
Our first query protocol was correct, but did not take into account distributed application execution. If we want to implement resends in the actor that queries a device actor (because of timed out requests), or if we want to query multiple actors, we need to be able to correlate requests and responses. Hence, we add one more field to our messages, so that an ID can be provided by the requester (we will add this code to our app in a later step):
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceInProgress.scala](/akka-docs/src/test/scala/typed/tutorial_3/DeviceInProgress.scala) { #read-protocol-2 }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceInProgress2.java](/akka-docs/src/test/java/jdocs/typed/tutorial_3/inprogress2/DeviceProtocol.java) { #read-protocol-2 }
|
||||
|
||||
## Implementing the device actor and its read protocol
|
||||
|
||||
As we learned in the Hello World example, each actor defines the type of messages it will accept. Our device actor has the responsibility to use the same ID parameter for the response of a given query, which would make it look like the following.
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceInProgress.scala](/akka-docs/src/test/scala/typed/tutorial_3/DeviceInProgress.scala) { #device-with-read }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceInProgress2.java](/akka-docs/src/test/java/jdocs/typed/tutorial_3/inprogress2/Device.java) { #device-with-read }
|
||||
|
||||
Note in the code that:
|
||||
|
||||
* The @scala[apply method in the companion object]@java[static method] defines how to construct the `Behavior` for the `Device` actor. The parameters include an ID for the device and the group to which it belongs, which we will use later.
|
||||
* The messages we reasoned about previously are defined in @scala[the companion object.]@java[DeviceProtocol class that was shown earlier.]
|
||||
* In the `Device` class, the value of `lastTemperatureReading` is initially set to @scala[`None`]@java[`Optional.empty()`], and the actor will report it back if queried.
|
||||
|
||||
## Testing the actor
|
||||
|
||||
Based on the actor above, we could write a test. In the `com.lightbend.akka.sample` package in the test tree of your project, add the following code to a @scala[`DeviceSpec.scala`]@java[`DeviceTest.java`] file.
|
||||
@scala[(We use ScalaTest but any other test framework can be used with the Akka Testkit)].
|
||||
|
||||
You can run this test @scala[by running `test` at the sbt prompt]@java[by running `mvn test`].
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceSpec.scala](/akka-docs/src/test/scala/typed/tutorial_3/DeviceSpec.scala) { #device-read-test }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_3/DeviceTest.java) { #device-read-test }
|
||||
|
||||
Now, the actor needs a way to change the state of the temperature when it receives a message from the sensor.
|
||||
|
||||
## Adding a write protocol
|
||||
|
||||
The purpose of the write protocol is to update the `currentTemperature` field when the actor receives a message that contains the temperature. Again, it is tempting to define the write protocol as a very simple message, something like this:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceInProgress.scala](/akka-docs/src/test/scala/typed/tutorial_3/DeviceInProgress.scala) { #write-protocol-1 }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceInProgress3.java](/akka-docs/src/test/java/jdocs/typed/tutorial_3/inprogress3/DeviceProtocol.java) { #write-protocol-1 }
|
||||
|
||||
However, this approach does not take into account that the sender of the record temperature message can never be sure if the message was processed or not. We have seen that Akka does not guarantee delivery of these messages and leaves it to the application to provide success notifications. In our case, we would like to send an acknowledgment to the sender once we have updated our last temperature recording, e.g. replying with a `TemperatureRecorded` message.
|
||||
Just like in the case of temperature queries and responses, it is also a good idea to include an ID field to provide maximum flexibility.
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceInProgress.scala](/akka-docs/src/test/scala/typed/tutorial_3/Device.scala) { #write-protocol }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceInProgress3.java](/akka-docs/src/test/java/jdocs/typed/tutorial_3/DeviceProtocol.java) { #write-protocol }
|
||||
|
||||
## Actor with read and write messages
|
||||
|
||||
Putting the read and write protocol together, the device actor looks like the following example:
|
||||
|
||||
Scala
|
||||
: @@snip [Device.scala](/akka-docs/src/test/scala/typed/tutorial_3/Device.scala) { #full-device }
|
||||
|
||||
Java
|
||||
: @@snip [Device.java](/akka-docs/src/test/java/jdocs/typed/tutorial_3/Device.java) { #full-device }
|
||||
|
||||
We should also write a new test case now, exercising both the read/query and write/record functionality together:
|
||||
|
||||
Scala:
|
||||
: @@snip [DeviceSpec.scala](/akka-docs/src/test/scala/typed/tutorial_3/DeviceSpec.scala) { #device-write-read-test }
|
||||
|
||||
Java:
|
||||
: @@snip [DeviceTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_3/DeviceTest.java) { #device-write-read-test }
|
||||
|
||||
## What's Next?
|
||||
|
||||
So far, we have started designing our overall architecture, and we wrote the first actor that directly corresponds to the domain. We now have to create the component that is responsible for maintaining groups of devices and the device actors themselves.
|
||||
201
akka-docs/src/main/paradox/typed/guide/tutorial_4.md
Normal file
201
akka-docs/src/main/paradox/typed/guide/tutorial_4.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Part 4: Working with Device Groups
|
||||
|
||||
## Introduction
|
||||
|
||||
Let's take a closer look at the main functionality required by our use case. In a complete IoT system for monitoring home temperatures, the steps for connecting a device sensor to our system might look like this:
|
||||
|
||||
1. A sensor device in the home connects through some protocol.
|
||||
1. The component managing network connections accepts the connection.
|
||||
1. The sensor provides its group and device ID to register with the device manager component of our system.
|
||||
1. The device manager component handles registration by looking up or creating the actor responsible for keeping sensor state.
|
||||
1. The actor responds with an acknowledgement, exposing its `ActorRef`.
|
||||
1. The networking component now uses the `ActorRef` for communication between the sensor and device actor without going through the device manager.
|
||||
|
||||
Steps 1 and 2 take place outside the boundaries of our tutorial system. In this chapter, we will start addressing steps 3-6 and create a way for sensors to register with our system and to communicate with actors. But first, we have another architectural decision — how many levels of actors should we use to represent device groups and device sensors?
|
||||
|
||||
One of the main design challenges for Akka programmers is choosing the best granularity for actors. In practice, depending on the characteristics of the interactions between actors, there are usually several valid ways to organize a system. In our use case, for example, it would be possible to have a single actor maintain all the groups and devices — perhaps using hash maps. It would also be reasonable to have an actor for each group that tracks the state of all devices in the same home.
|
||||
|
||||
The following guidelines help us choose the most appropriate actor hierarchy:
|
||||
|
||||
* In general, prefer larger granularity. Introducing more fine-grained actors than needed causes more problems than it solves.
|
||||
* Add finer granularity when the system requires:
|
||||
* Higher concurrency.
|
||||
* Complex conversations between actors that have many
|
||||
states. We will see a very good example for this in the next chapter.
|
||||
* Sufficient state that it makes sense to divide into smaller
|
||||
actors.
|
||||
* Multiple unrelated responsibilities. Using separate actors allows individuals to fail and be restored with little impact on others.
|
||||
|
||||
## Device manager hierarchy
|
||||
|
||||
Considering the principles outlined in the previous section, We will model the device manager component as an actor tree with three levels:
|
||||
|
||||
* The top level supervisor actor represents the system component for devices. It is also the entry point to look up and create device group and device actors.
|
||||
* At the next level, group actors each supervise the device actors for one group id (e.g. one home). They also provide services, such as querying temperature readings from all of the available devices in their group.
|
||||
* Device actors manage all the interactions with the actual device sensors, such as storing temperature readings.
|
||||
|
||||

|
||||
|
||||
We chose this three-layered architecture for these reasons:
|
||||
|
||||
* Having groups of individual actors:
|
||||
* Isolates failures that occur in a group. If a single actor managed all device groups, an error in one group that causes a restart would wipe out the state of groups that are otherwise non-faulty.
|
||||
* Simplifies the problem of querying all the devices belonging to a group. Each group actor only contains state related to its group.
|
||||
* Increases parallelism in the system. Since each group has a dedicated actor, they run concurrently and we can query multiple groups concurrently.
|
||||
|
||||
|
||||
* Having sensors modeled as individual device actors:
|
||||
* Isolates failures of one device actor from the rest of the devices in the group.
|
||||
* Increases the parallelism of collecting temperature readings. Network connections from different sensors communicate with their individual device actors directly, reducing contention points.
|
||||
|
||||
With the architecture defined, we can start working on the protocol for registering sensors.
|
||||
|
||||
## The Registration Protocol
|
||||
|
||||
As the first step, we need to design the protocol both for registering a device and for creating the group and device actors that will be responsible for it. This protocol will be provided by the `DeviceManager` component itself because that is the only actor that is known and available up front: device groups and device actors are created on-demand.
|
||||
|
||||
Looking at registration in more detail, we can outline the necessary functionality:
|
||||
|
||||
1. When a `DeviceManager` receives a request with a group and device id:
|
||||
* If the manager already has an actor for the device group, it forwards the request to it.
|
||||
* Otherwise, it creates a new device group actor and then forwards the request.
|
||||
1. The `DeviceGroup` actor receives the request to register an actor for the given device:
|
||||
* If the group already has an actor for the device it replies with the `ActorRef` of the existing device actor.
|
||||
* Otherwise, the `DeviceGroup` actor first creates a device actor and replies with the `ActorRef` of the newly created device actor.
|
||||
1. The sensor will now have the `ActorRef` of the device actor to send messages directly to it.
|
||||
|
||||
The messages that we will use to communicate registration requests and their acknowledgement have the definition:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceManager.scala](/akka-docs/src/test/scala/typed/tutorial_4/DeviceManager.scala) { #device-registration-msgs }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceManager.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceManagerProtocol.java) { #device-registration-msgs }
|
||||
|
||||
In this case we have not included a request ID field in the messages. Since registration happens once, when the component connects the system to some network protocol, the ID is not important. However, it is usually a best practice to include a request ID.
|
||||
|
||||
Now, we'll start implementing the protocol from the bottom up. In practice, both a top-down and bottom-up approach can work, but in our case, we benefit from the bottom-up approach as it allows us to immediately write tests for the new features without mocking out parts that we will need to build later.
|
||||
|
||||
## Adding registration support to device group actors
|
||||
|
||||
A group actor has some work to do when it comes to registrations, including:
|
||||
|
||||
* Handling the registration request for existing device actor or by creating a new actor.
|
||||
* Tracking which device actors exist in the group and removing them from the group when they are stopped.
|
||||
|
||||
### Handling the registration request
|
||||
|
||||
A device group actor must either reply to the request with the `ActorRef` of an existing child, or it should create one. To look up child actors by their device IDs we will use a .
|
||||
|
||||
Add the following to your source file:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroup.scala](/akka-docs/src/test/scala/typed/tutorial_4/DeviceGroup.scala) { #device-group-register }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroup.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceGroup.java) { #device-group-register }
|
||||
|
||||
Just as we did with the device, we test this new functionality. We also test that the actors returned for the two different IDs are actually different, and we also attempt to record a temperature reading for each of the devices to see if the actors are responding.
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupSpec.scala](/akka-docs/src/test/scala/typed/tutorial_4/DeviceGroupSpec.scala) { #device-group-test-registration }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceGroupTest.java) { #device-group-test-registration }
|
||||
|
||||
If a device actor already exists for the registration request, we would like to use
|
||||
the existing actor instead of a new one. We have not tested this yet, so we need to fix this:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupSpec.scala](/akka-docs/src/test/scala/typed/tutorial_4/DeviceGroupSpec.scala) { #device-group-test3 }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceGroupTest.java) { #device-group-test3 }
|
||||
|
||||
|
||||
### Keeping track of the device actors in the group
|
||||
|
||||
So far, we have implemented logic for registering device actors in the group. Devices come and go, however, so we will need a way to remove device actors from the @scala[`Map[String, ActorRef[DeviceMessage]]`]@java[`Map<String, ActorRef<DeviceMessage>>`]. We will assume that when a device is removed, its corresponding device actor is stopped. Supervision, as we discussed earlier, only handles error scenarios — not graceful stopping. So we need to notify the parent when one of the device actors is stopped.
|
||||
|
||||
Akka provides a _Death Watch_ feature that allows an actor to _watch_ another actor and be notified if the other actor is stopped. Unlike supervision, watching is not limited to parent-child relationships, any actor can watch any other actor as long as it knows the `ActorRef`. After a watched actor stops, the watcher receives a `Terminated(actorRef)` signal which also contains the reference to the watched actor. The watcher can either handle this message explicitly or will fail with a `DeathPactException`. This latter is useful if the actor can no longer perform its own duties after the watched actor has been stopped. In our case, the group should still function after one device have been stopped, so we need to handle the `Terminated(actorRef)` signal.
|
||||
|
||||
Our device group actor needs to include functionality that:
|
||||
|
||||
1. Starts watching new device actors when they are created.
|
||||
2. Removes a device actor from the @scala[`Map[String, ActorRef[DeviceMessage]]`]@java[`Map<String, ActorRef<DeviceMessage>>`] — which maps devices to device actors — when the notification indicates it has stopped.
|
||||
|
||||
Unfortunately, the `Terminated` signal only contains the `ActorRef` of the child actor. We need the actor's ID to remove it from the map of existing device to device actor mappings. An alternative to the `Terminated` signal is to define a custom message that will be sent when the watched actor is stopped. We will use that here because it gives us the possibility to carry the device ID in that message.
|
||||
|
||||
|
||||
Adding the functionality to identify the actor results in this:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroup.scala](/akka-docs/src/test/scala/typed/tutorial_4/DeviceGroup.scala) { #device-group-remove }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroup.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceGroup.java) { #device-group-remove }
|
||||
|
||||
So far we have no means to get which devices the group device actor keeps track of and, therefore, we cannot test our new functionality yet. To make it testable, we add a new query capability (message `RequestDeviceList`) that lists the currently active device IDs:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceManager.scala](/akka-docs/src/test/scala/typed/tutorial_4/DeviceManager.scala) { #device-list-msgs }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceManager.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceManagerProtocol.java) { #device-list-msgs }
|
||||
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroup.scala](/akka-docs/src/test/scala/typed/tutorial_4/DeviceGroup.scala) { #device-group-full }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroup.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceGroup.java) { #device-group-full }
|
||||
|
||||
We are almost ready to test the removal of devices. But, we still need the following capabilities:
|
||||
|
||||
* To stop a device actor from our test case, from the outside, we must send a message to it. We add a `Passivate` message which instructs the actor to stop.
|
||||
* To be notified once the device actor is stopped. We can use the _Death Watch_ facility for this purpose, too.
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceManager.scala](/akka-docs/src/test/scala/typed/tutorial_4/Device.scala) { #passivate-msg }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceManager.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceProtocol.java) { #passivate-msg }
|
||||
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceManager.scala](/akka-docs/src/test/scala/typed/tutorial_4/Device.scala) { #device-with-passivate }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceManager.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/Device.java) { #device-with-passivate }
|
||||
|
||||
|
||||
We add two more test cases now. In the first, we test that we get back the list of proper IDs once we have added a few devices. The second test case makes sure that the device ID is properly removed after the device actor has been stopped. The `TestProbe` has a `expectTerminated` method that we can easily use to assert that the device actor has been terminated.
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupSpec.scala](/akka-docs/src/test/scala/typed/tutorial_4/DeviceGroupSpec.scala) { #device-group-list-terminate-test }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceGroupTest.java) { #device-group-list-terminate-test }
|
||||
|
||||
## Creating device manager actors
|
||||
|
||||
Going up to the next level in our hierarchy, we need to create the entry point for our device manager component in the `DeviceManager` source file. This actor is very similar to the device group actor, but creates device group actors instead of device actors:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceManager.scala](/akka-docs/src/test/scala/typed/tutorial_4/DeviceManager.scala) { #device-manager-full }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceManager.java](/akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceManager.java) { #device-manager-full }
|
||||
|
||||
We leave tests of the device manager as an exercise for you since it is very similar to the tests we have already written for the group
|
||||
actor.
|
||||
|
||||
## What's next?
|
||||
|
||||
We have now a hierarchical component for registering and tracking devices and recording measurements. We have seen how to implement different types of conversation patterns, such as:
|
||||
|
||||
* Request-respond (for temperature recordings)
|
||||
* Create-on-demand (for registration of devices)
|
||||
* Create-watch-terminate (for creating the group and device actor as children)
|
||||
|
||||
In the next chapter, we will introduce group query capabilities, which will establish a new conversation pattern of scatter-gather. In particular, we will implement the functionality that allows users to query the status of all the devices belonging to a group.
|
||||
222
akka-docs/src/main/paradox/typed/guide/tutorial_5.md
Normal file
222
akka-docs/src/main/paradox/typed/guide/tutorial_5.md
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
# Part 5: Querying Device Groups
|
||||
|
||||
## Introduction
|
||||
|
||||
The conversational patterns that we have seen so far are simple in the sense that they require the actor to keep little or no state. Specifically:
|
||||
|
||||
* Device actors return a reading, which requires no state change
|
||||
* Record a temperature, which updates a single field
|
||||
* Device Group actors maintain group membership by adding or removing entries from a map
|
||||
|
||||
In this part, we will use a more complex example. Since homeowners will be interested in the temperatures throughout their home, our goal is to be able to query all of the device actors in a group. Let us start by investigating how such a query API should behave.
|
||||
|
||||
## Dealing with possible scenarios
|
||||
The very first issue we face is that the membership of a group is dynamic. Each sensor device is represented by an actor that can stop at any time. At the beginning of the query, we can ask all of the existing device actors for the current temperature. However, during the lifecycle of the query:
|
||||
|
||||
* A device actor might stop and not be able to respond back with a temperature reading.
|
||||
* A new device actor might start up and not be included in the query because we weren't aware of it.
|
||||
|
||||
These issues can be addressed in many different ways, but the important point is to settle on the desired behavior. The following works well for our use case:
|
||||
|
||||
* When a query arrives, the group actor takes a _snapshot_ of the existing device actors and will only ask those actors for the temperature.
|
||||
* Actors that start up _after_ the query arrives are ignored.
|
||||
* If an actor in the snapshot stops during the query without answering, we will report the fact that it stopped to the sender of the query message.
|
||||
|
||||
Apart from device actors coming and going dynamically, some actors might take a long time to answer. For example, they could be stuck in an accidental infinite loop, or fail due to a bug and drop our request. We don't want the query to continue indefinitely, so we will consider it complete in either of the following cases:
|
||||
|
||||
* All actors in the snapshot have either responded or have confirmed being stopped.
|
||||
* We reach a pre-defined deadline.
|
||||
|
||||
Given these decisions, along with the fact that a device in the snapshot might have just started and not yet received a temperature to record, we can define four states
|
||||
for each device actor, with respect to a temperature query:
|
||||
|
||||
* It has a temperature available: `Temperature`.
|
||||
* It has responded, but has no temperature available yet: `TemperatureNotAvailable`.
|
||||
* It has stopped before answering: `DeviceNotAvailable`.
|
||||
* It did not respond before the deadline: `DeviceTimedOut`.
|
||||
|
||||
Summarizing these in message types we can add the following to the message protocol:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroup.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceManager.scala) { #query-protocol }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroup.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceManagerProtocol.java) { #query-protocol }
|
||||
|
||||
## Implementing the query
|
||||
|
||||
One approach for implementing the query involves adding code to the group device actor. However, in practice this can be very cumbersome and error prone. Remember that when we start a query, we need to take a snapshot of the devices present and start a timer so that we can enforce the deadline. In the meantime, _another query_ can arrive. For the second query we need to keep track of the exact same information but in isolation from the previous query. This would require us to maintain separate mappings between queries and device actors.
|
||||
|
||||
Instead, we will implement a simpler, and superior approach. We will create an actor that represents a _single query_ and that performs the tasks needed to complete the query on behalf of the group actor. So far we have created actors that belonged to classical domain objects, but now, we will create an
|
||||
actor that represents a process or a task rather than an entity. We benefit by keeping our group device actor simple and being able to better test query capability in isolation.
|
||||
|
||||
### Defining the query actor
|
||||
|
||||
First, we need to design the lifecycle of our query actor. This consists of identifying its initial state, the first action it will take, and the cleanup — if necessary. The query actor will need the following information:
|
||||
|
||||
* The snapshot and IDs of active device actors to query.
|
||||
* The ID of the request that started the query (so that we can include it in the reply).
|
||||
* The reference of the actor who sent the query. We will send the reply to this actor directly.
|
||||
* A deadline that indicates how long the query should wait for replies. Making this a parameter will simplify testing.
|
||||
|
||||
#### Scheduling the query timeout
|
||||
Since we need a way to indicate how long we are willing to wait for responses, it is time to introduce a new Akka feature that we have
|
||||
not used yet, the built-in scheduler facility. Using `Behaviors.withTimers` and `startSingleTimer` to schedule a message that will be sent after a given delay.
|
||||
|
||||
|
||||
We need to create a message that represents the query timeout. We create a simple message `CollectionTimeout` without any parameters for this purpose.
|
||||
|
||||
At the start of the query, we need to ask each of the device actors for the current temperature. To be able to quickly
|
||||
detect devices that stopped before they got the `ReadTemperature` message we will also watch each of the actors. This
|
||||
way, we get `DeviceTerminated` messages for those that stop during the lifetime of the query, so we don't need to wait
|
||||
until the timeout to mark these as not available.
|
||||
|
||||
Putting this together, the outline of our `DeviceGroupQuery` actor looks like this:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupQuery.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuery.scala) { #query-outline }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupQuery.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQuery.java) { #query-outline }
|
||||
|
||||
Note that we have to convert the `RespondTemperature` replies from the device actor to the message protocol that the `DeviceGroupQuery` actor understands, i.e. `DeviceGroupQueryMessage`. For this we use a `messageAdapter` that wraps the `RespondTemperature` in a `WrappedRespondTemperature`, which @scala[extends]@java[implements] `DeviceGroupQueryMessage`.
|
||||
|
||||
#### Tracking actor state
|
||||
|
||||
The query actor, apart from the pending timer, has one stateful aspect, tracking the set of actors that: have replied, have stopped, or have not replied. We track this state @scala[in a `var` field of an immutable `Map`]@java[in a mutable `HashMap`] in the actor.
|
||||
|
||||
For our use case:
|
||||
|
||||
1. We keep track state with:
|
||||
* a `Map` of already received replies
|
||||
* a `Set` of actors that we still wait on
|
||||
2. We have three events to act on:
|
||||
* We can receive a `RespondTemperature` message from one of the devices.
|
||||
* We can receive a `DeviceTerminated` message for a device actor that has been stopped in the meantime.
|
||||
* We can reach the deadline and receive a `CollectionTimeout`.
|
||||
|
||||
To accomplish this, add the following to your `DeviceGroupQuery` source file:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupQuery.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuery.scala) { #query-state }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupQuery.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQuery.java) { #query-state }
|
||||
|
||||
For `RespondTemperature` and `DeviceTerminated` we keep track of the replies by updating `repliesSoFar` and remove the actor from `stillWaiting`, and then delegate to a method `respondWhenAllCollected`, which we will discuss soon.
|
||||
|
||||
In the case of timeout, we need to take all the actors that have not yet replied yet (the members of the set `stillWaiting`) and put a `DeviceTimedOut` as the status in the final reply.
|
||||
|
||||
|
||||
We now have to figure out what to do in `respondWhenAllCollected`. First, we need to record the new result in the map `repliesSoFar` and remove the actor from `stillWaiting`. The next step is to check if there are any remaining actors we are waiting for. If there is none, we send the result of the query to the original requester and stop the query actor. Otherwise, we need to update the `repliesSoFar` and `stillWaiting` structures and wait for more messages.
|
||||
|
||||
With all this knowledge, we can create the `respondWhenAllCollected` method:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupQuery.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuery.scala) { #query-collect-reply }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupQuery.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQuery.java) { #query-collect-reply }
|
||||
|
||||
Our query actor is now done:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupQuery.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuery.scala) { #query-full }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupQuery.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQuery.java) { #query-full }
|
||||
|
||||
### Testing the query actor
|
||||
|
||||
Now let's verify the correctness of the query actor implementation. There are various scenarios we need to test individually to make
|
||||
sure everything works as expected. To be able to do this, we need to simulate the device actors somehow to exercise
|
||||
various normal or failure scenarios. Thankfully we took the list of collaborators (actually a `Map`) as a parameter
|
||||
to the query actor, so we can pass in `TestProbe` references. In our first test, we try out the case when
|
||||
there are two devices and both report a temperature:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupQuerySpec.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuerySpec.scala) { #query-test-normal }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupQueryTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQueryTest.java) { #query-test-normal }
|
||||
|
||||
That was the happy case, but we know that sometimes devices cannot provide a temperature measurement. This
|
||||
scenario is just slightly different from the previous:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupQuerySpec.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuerySpec.scala) { #query-test-no-reading }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupQueryTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQueryTest.java) { #query-test-no-reading }
|
||||
|
||||
We also know, that sometimes device actors stop before answering:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupQuerySpec.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuerySpec.scala) { #query-test-stopped }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupQueryTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQueryTest.java) { #query-test-stopped }
|
||||
|
||||
If you remember, there is another case related to device actors stopping. It is possible that we get a normal reply
|
||||
from a device actor, but then receive a `Terminated` for the same actor later. In this case, we would like to keep
|
||||
the first reply and not mark the device as `DeviceNotAvailable`. We should test this, too:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupQuerySpec.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuerySpec.scala) { #query-test-stopped-later }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupQueryTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQueryTest.java) { #query-test-stopped-later }
|
||||
|
||||
The final case is when not all devices respond in time. To keep our test relatively fast, we will construct the
|
||||
`DeviceGroupQuery` actor with a smaller timeout:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupQuerySpec.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuerySpec.scala) { #query-test-timeout }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupQueryTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQueryTest.java) { #query-test-timeout }
|
||||
|
||||
Our query works as expected now, it is time to include this new functionality in the `DeviceGroup` actor now.
|
||||
|
||||
## Adding query capability to the group
|
||||
|
||||
Including the query feature in the group actor is fairly simple now. We did all the heavy lifting in the query actor
|
||||
itself, the group actor only needs to create it with the right initial parameters and nothing else.
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroup.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroup.scala) { #query-added }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroup.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroup.java) { #query-added }
|
||||
|
||||
It is probably worth restating what we said at the beginning of the chapter. By keeping the temporary state that is only relevant to the query itself in a separate actor we keep the group actor implementation very simple. It delegates
|
||||
everything to child actors and therefore does not have to keep state that is not relevant to its core business. Also, multiple queries can now run parallel to each other, in fact, as many as needed. In our case querying an individual device actor is a fast operation, but if this were not the case, for example, because the remote sensors need to be contacted over the network, this design would significantly improve throughput.
|
||||
|
||||
We close this chapter by testing that everything works together. This test is a variant of the previous ones, now exercising the group query feature:
|
||||
|
||||
Scala
|
||||
: @@snip [DeviceGroupSpec.scala](/akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupSpec.scala) { #group-query-integration-test }
|
||||
|
||||
Java
|
||||
: @@snip [DeviceGroupTest.java](/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupTest.java) { #group-query-integration-test }
|
||||
|
||||
## Summary
|
||||
In the context of the IoT system, this guide introduced the following concepts, among others. You can follow the links to review them if necessary:
|
||||
|
||||
* @ref:[The hierarchy of actors and their lifecycle](tutorial_1.md)
|
||||
* @ref:[The importance of designing messages for flexibility](tutorial_3.md)
|
||||
* @ref:[How to watch and stop actors, if necessary](tutorial_4.md#keeping-track-of-the-device-actors-in-the-group)
|
||||
|
||||
## What's Next?
|
||||
|
||||
To continue your journey with Akka, we recommend:
|
||||
|
||||
* Start building your own applications with Akka, make sure you [get involved in our amazing community](http://akka.io/get-involved) for help if you get stuck.
|
||||
* If you’d like some additional background, and detail, read the rest of the @ref:[reference documentation](../actors.md) and check out some of the @ref:[books and videos](../../additional/books.md) on Akka.
|
||||
* If you are interested in functional programming, read how actors can be defined in a @ref:[functional style](../actors.md#functional-style). In this guide the object-oriented style was used, but you can mix both as you like.
|
||||
|
||||
To get from this guide to a complete application you would likely need to provide either an UI or an API. For this we recommend that you look at the following technologies and see what fits you:
|
||||
|
||||
* [Akka HTTP](https://doc.akka.io/docs/akka-http/current/introduction.html) is a HTTP server and client library, making it possible to publish and consume HTTP endpoints
|
||||
* [Play Framework](https://www.playframework.com) is a full fledged web framework that is built on top of Akka HTTP, it integrates well with Akka and can be used to create a complete modern web UI
|
||||
* [Lagom](https://www.lagomframework.com) is an opinionated microservice framework built on top of Akka, encoding many best practices around Akka and Play
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
@@@ index
|
||||
|
||||
* [guide](guide/index.md)
|
||||
* [actors](actors.md)
|
||||
* [dispatchers](dispatchers.md)
|
||||
* [coexisting](coexisting.md)
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ public class Messages {
|
|||
}
|
||||
}
|
||||
|
||||
public static enum Swap {
|
||||
public enum Swap {
|
||||
Swap
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public class FSMDocTest extends AbstractJavaTest {
|
|||
system = null;
|
||||
}
|
||||
|
||||
public static enum StateType {
|
||||
public enum StateType {
|
||||
SomeState,
|
||||
Processing,
|
||||
Idle,
|
||||
|
|
@ -40,12 +40,12 @@ public class FSMDocTest extends AbstractJavaTest {
|
|||
Error
|
||||
}
|
||||
|
||||
public static enum Messages {
|
||||
public enum Messages {
|
||||
WillDo,
|
||||
Tick
|
||||
}
|
||||
|
||||
public static enum Data {
|
||||
public enum Data {
|
||||
Foo,
|
||||
Bar
|
||||
};
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ public class ClusterShardingTest {
|
|||
static//#counter-actor
|
||||
public class Counter extends AbstractPersistentActor {
|
||||
|
||||
public static enum CounterOp {
|
||||
public enum CounterOp {
|
||||
INCREMENT, DECREMENT
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class Device extends AbstractActor {
|
|||
|
||||
//#read-protocol-2
|
||||
public static final class ReadTemperature {
|
||||
long requestId;
|
||||
final long requestId;
|
||||
|
||||
public ReadTemperature(long requestId) {
|
||||
this.requestId = requestId;
|
||||
|
|
@ -39,8 +39,8 @@ class Device extends AbstractActor {
|
|||
}
|
||||
|
||||
public static final class RespondTemperature {
|
||||
long requestId;
|
||||
Optional<Double> value;
|
||||
final long requestId;
|
||||
final Optional<Double> value;
|
||||
|
||||
public RespondTemperature(long requestId, Optional<Double> value) {
|
||||
this.requestId = requestId;
|
||||
|
|
|
|||
|
|
@ -77,22 +77,47 @@ public class DeviceGroup extends AbstractActor {
|
|||
public Temperature(double value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Temperature that = (Temperature) o;
|
||||
|
||||
return Double.compare(that.value, value) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
long temp = Double.doubleToLongBits(value);
|
||||
return (int) (temp ^ (temp >>> 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Temperature{" +
|
||||
"value=" + value +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TemperatureNotAvailable implements TemperatureReading {
|
||||
public enum TemperatureNotAvailable implements TemperatureReading {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public static final class DeviceNotAvailable implements TemperatureReading {
|
||||
public enum DeviceNotAvailable implements TemperatureReading {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public static final class DeviceTimedOut implements TemperatureReading {
|
||||
public enum DeviceTimedOut implements TemperatureReading {
|
||||
INSTANCE
|
||||
}
|
||||
//#query-protocol
|
||||
|
||||
|
||||
final Map<String, ActorRef> deviceIdToActor = new HashMap<>();
|
||||
final Map<ActorRef, String> actorToDeviceId = new HashMap<>();
|
||||
final long nextCollectionId = 0L;
|
||||
|
||||
@Override
|
||||
public void preStart() {
|
||||
|
|
|
|||
|
|
@ -76,17 +76,17 @@ public class DeviceGroupQuery extends AbstractActor {
|
|||
ActorRef deviceActor = getSender();
|
||||
DeviceGroup.TemperatureReading reading = r.value
|
||||
.map(v -> (DeviceGroup.TemperatureReading) new DeviceGroup.Temperature(v))
|
||||
.orElse(new DeviceGroup.TemperatureNotAvailable());
|
||||
.orElse(DeviceGroup.TemperatureNotAvailable.INSTANCE);
|
||||
receivedResponse(deviceActor, reading, stillWaiting, repliesSoFar);
|
||||
})
|
||||
.match(Terminated.class, t -> {
|
||||
receivedResponse(t.getActor(), new DeviceGroup.DeviceNotAvailable(), stillWaiting, repliesSoFar);
|
||||
receivedResponse(t.getActor(), DeviceGroup.DeviceNotAvailable.INSTANCE, stillWaiting, repliesSoFar);
|
||||
})
|
||||
.match(CollectionTimeout.class, t -> {
|
||||
Map<String, DeviceGroup.TemperatureReading> replies = new HashMap<>(repliesSoFar);
|
||||
for (ActorRef deviceActor : stillWaiting) {
|
||||
String deviceId = actorToDeviceId.get(deviceActor);
|
||||
replies.put(deviceId, new DeviceGroup.DeviceTimedOut());
|
||||
replies.put(deviceId, DeviceGroup.DeviceTimedOut.INSTANCE);
|
||||
}
|
||||
requester.tell(new DeviceGroup.RespondAllTemperatures(requestId, replies), getSelf());
|
||||
getContext().stop(getSelf());
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ public class DeviceGroupQueryTest extends JUnitSuite {
|
|||
expectedTemperatures.put("device1", new DeviceGroup.Temperature(1.0));
|
||||
expectedTemperatures.put("device2", new DeviceGroup.Temperature(2.0));
|
||||
|
||||
assertEqualTemperatures(expectedTemperatures, response.temperatures);
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-normal
|
||||
|
||||
|
|
@ -101,10 +101,10 @@ public class DeviceGroupQueryTest extends JUnitSuite {
|
|||
assertEquals(1L, response.requestId);
|
||||
|
||||
Map<String, DeviceGroup.TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", new DeviceGroup.TemperatureNotAvailable());
|
||||
expectedTemperatures.put("device1", DeviceGroup.TemperatureNotAvailable.INSTANCE);
|
||||
expectedTemperatures.put("device2", new DeviceGroup.Temperature(2.0));
|
||||
|
||||
assertEqualTemperatures(expectedTemperatures, response.temperatures);
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-no-reading
|
||||
|
||||
|
|
@ -137,9 +137,9 @@ public class DeviceGroupQueryTest extends JUnitSuite {
|
|||
|
||||
Map<String, DeviceGroup.TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", new DeviceGroup.Temperature(1.0));
|
||||
expectedTemperatures.put("device2", new DeviceGroup.DeviceNotAvailable());
|
||||
expectedTemperatures.put("device2", DeviceGroup.DeviceNotAvailable.INSTANCE);
|
||||
|
||||
assertEqualTemperatures(expectedTemperatures, response.temperatures);
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-stopped
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ public class DeviceGroupQueryTest extends JUnitSuite {
|
|||
expectedTemperatures.put("device1", new DeviceGroup.Temperature(1.0));
|
||||
expectedTemperatures.put("device2", new DeviceGroup.Temperature(2.0));
|
||||
|
||||
assertEqualTemperatures(expectedTemperatures, response.temperatures);
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-stopped-later
|
||||
|
||||
|
|
@ -209,22 +209,10 @@ public class DeviceGroupQueryTest extends JUnitSuite {
|
|||
|
||||
Map<String, DeviceGroup.TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", new DeviceGroup.Temperature(1.0));
|
||||
expectedTemperatures.put("device2", new DeviceGroup.DeviceTimedOut());
|
||||
expectedTemperatures.put("device2", DeviceGroup.DeviceTimedOut.INSTANCE);
|
||||
|
||||
assertEqualTemperatures(expectedTemperatures, response.temperatures);
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-timeout
|
||||
|
||||
public static void assertEqualTemperatures(Map<String, DeviceGroup.TemperatureReading> expected, Map<String, DeviceGroup.TemperatureReading> actual) {
|
||||
for (Map.Entry<String, DeviceGroup.TemperatureReading> entry : expected.entrySet()) {
|
||||
DeviceGroup.TemperatureReading expectedReading = entry.getValue();
|
||||
DeviceGroup.TemperatureReading actualReading = actual.get(entry.getKey());
|
||||
assertNotNull(actualReading);
|
||||
assertEquals(expectedReading.getClass(), actualReading.getClass());
|
||||
if (expectedReading instanceof DeviceGroup.Temperature) {
|
||||
assertEquals(((DeviceGroup.Temperature) expectedReading).value, ((DeviceGroup.Temperature) actualReading).value, 0.01);
|
||||
}
|
||||
}
|
||||
assertEquals(expected.size(), actual.size());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import static org.junit.Assert.assertNotEquals;
|
|||
|
||||
import org.scalatest.junit.JUnitSuite;
|
||||
|
||||
import static jdocs.tutorial_5.DeviceGroupQueryTest.assertEqualTemperatures;
|
||||
|
||||
public class DeviceGroupTest extends JUnitSuite {
|
||||
|
||||
static ActorSystem system;
|
||||
|
|
@ -166,9 +164,9 @@ public class DeviceGroupTest extends JUnitSuite {
|
|||
Map<String, DeviceGroup.TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", new DeviceGroup.Temperature(1.0));
|
||||
expectedTemperatures.put("device2", new DeviceGroup.Temperature(2.0));
|
||||
expectedTemperatures.put("device3", new DeviceGroup.TemperatureNotAvailable());
|
||||
expectedTemperatures.put("device3", DeviceGroup.TemperatureNotAvailable.INSTANCE);
|
||||
|
||||
assertEqualTemperatures(expectedTemperatures, response.temperatures);
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#group-query-integration-test
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_1;
|
||||
|
||||
import akka.actor.typed.PreRestart;
|
||||
import akka.actor.typed.SupervisorStrategy;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.scalatest.junit.JUnitSuite;
|
||||
|
||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import akka.actor.typed.PostStop;
|
||||
|
||||
//#print-refs
|
||||
import akka.actor.typed.ActorRef;
|
||||
import akka.actor.typed.ActorSystem;
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
class PrintMyActorRefActor extends AbstractBehavior<String> {
|
||||
|
||||
static Behavior<String> createBehavior() {
|
||||
return Behaviors.setup(PrintMyActorRefActor::new);
|
||||
}
|
||||
|
||||
private final ActorContext<String> context;
|
||||
|
||||
private PrintMyActorRefActor(ActorContext<String> context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<String> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessageEquals("printit", this::printIt)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<String> printIt() {
|
||||
ActorRef<String> secondRef = context.spawn(Behaviors.empty(), "second-actor");
|
||||
System.out.println("Second: " + secondRef);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
//#print-refs
|
||||
|
||||
//#start-stop
|
||||
class StartStopActor1 extends AbstractBehavior<String> {
|
||||
|
||||
static Behavior<String> createBehavior() {
|
||||
return Behaviors.setup(context -> new StartStopActor1());
|
||||
}
|
||||
|
||||
private StartStopActor1() {
|
||||
System.out.println("first started");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<String> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessageEquals("stop", Behaviors::stopped)
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<String> postStop() {
|
||||
System.out.println("first stopped");
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StartStopActor2 extends AbstractBehavior<String> {
|
||||
|
||||
static Behavior<String> createBehavior() {
|
||||
return Behaviors.setup(context -> new StartStopActor2());
|
||||
}
|
||||
|
||||
private StartStopActor2() {
|
||||
System.out.println("second started");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<String> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<String> postStop() {
|
||||
System.out.println("second stopped");
|
||||
return this;
|
||||
}
|
||||
}
|
||||
//#start-stop
|
||||
|
||||
//#supervise
|
||||
class SupervisingActor extends AbstractBehavior<String> {
|
||||
|
||||
static Behavior<String> createBehavior() {
|
||||
return Behaviors.setup(SupervisingActor::new);
|
||||
}
|
||||
|
||||
private final ActorRef<String> child;
|
||||
|
||||
private SupervisingActor(ActorContext<String> context) {
|
||||
child = context.spawn(
|
||||
Behaviors.supervise(SupervisedActor.createBehavior()).onFailure(SupervisorStrategy.restart()),
|
||||
"supervised-actor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<String> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessageEquals("failChild", this::failChild)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<String> failChild() {
|
||||
child.tell("fail");
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class SupervisedActor extends AbstractBehavior<String> {
|
||||
|
||||
static Behavior<String> createBehavior() {
|
||||
return Behaviors.setup(context -> new SupervisedActor());
|
||||
}
|
||||
|
||||
private SupervisedActor() {
|
||||
System.out.println("supervised actor started");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<String> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessageEquals("fail", this::fail)
|
||||
.onSignal(PreRestart.class, signal -> preRestart())
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<String> fail() {
|
||||
System.out.println("supervised actor fails now");
|
||||
throw new RuntimeException("I failed!");
|
||||
}
|
||||
|
||||
private Behavior<String> preRestart() {
|
||||
System.out.println("second will be restarted");
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<String> postStop() {
|
||||
System.out.println("second stopped");
|
||||
return this;
|
||||
}
|
||||
}
|
||||
//#supervise
|
||||
|
||||
|
||||
//#print-refs
|
||||
|
||||
class Main extends AbstractBehavior<String> {
|
||||
|
||||
static Behavior<String> createBehavior() {
|
||||
return Behaviors.setup(Main::new);
|
||||
}
|
||||
|
||||
private final ActorContext<String> context;
|
||||
|
||||
private Main(ActorContext<String> context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<String> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessageEquals("start", this::start)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<String> start() {
|
||||
ActorRef<String> firstRef = context.spawn(PrintMyActorRefActor.createBehavior(), "first-actor");
|
||||
|
||||
System.out.println("First: " + firstRef);
|
||||
firstRef.tell("printit");
|
||||
return Behaviors.same();
|
||||
}
|
||||
}
|
||||
|
||||
public class ActorHierarchyExperiments {
|
||||
public static void main(String[] args) {
|
||||
ActorSystem.create(Main.createBehavior(), "testSystem");
|
||||
}
|
||||
}
|
||||
//#print-refs
|
||||
|
||||
|
||||
class ActorHierarchyExperimentsTest extends JUnitSuite {
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit = new TestKitJunitResource();
|
||||
|
||||
@Test
|
||||
public void testStartAndStopActors() {
|
||||
//#start-stop-main
|
||||
ActorRef<String> first = testKit.spawn(StartStopActor1.createBehavior(), "first");
|
||||
first.tell("stop");
|
||||
//#start-stop-main
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuperviseActors() throws Exception {
|
||||
//#supervise-main
|
||||
ActorRef<String> supervisingActor = testKit.spawn(SupervisingActor.createBehavior(), "supervising-actor");
|
||||
supervisingActor.tell("failChild");
|
||||
//#supervise-main
|
||||
Thread.sleep(200); // allow for the println/logging to complete
|
||||
}
|
||||
}
|
||||
18
akka-docs/src/test/java/jdocs/typed/tutorial_2/IotMain.java
Normal file
18
akka-docs/src/test/java/jdocs/typed/tutorial_2/IotMain.java
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_2;
|
||||
|
||||
//#iot-app
|
||||
import akka.actor.typed.ActorSystem;
|
||||
|
||||
public class IotMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// Create ActorSystem and top level supervisor
|
||||
ActorSystem.create(IotSupervisor.createBehavior(), "iot-system");
|
||||
}
|
||||
|
||||
}
|
||||
//#iot-app
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_2;
|
||||
|
||||
//#iot-supervisor
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.PostStop;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
public class IotSupervisor extends AbstractBehavior<Void> {
|
||||
|
||||
public static Behavior<Void> createBehavior() {
|
||||
return Behaviors.setup(IotSupervisor::new);
|
||||
}
|
||||
|
||||
private final ActorContext<Void> context;
|
||||
|
||||
public IotSupervisor(ActorContext<Void> context) {
|
||||
this.context = context;
|
||||
context.getLog().info("IoT Application started");
|
||||
}
|
||||
|
||||
// No need to handle any messages
|
||||
@Override
|
||||
public Receive<Void> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private IotSupervisor postStop() {
|
||||
context.getLog().info("IoT Application stopped");
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
//#iot-supervisor
|
||||
73
akka-docs/src/test/java/jdocs/typed/tutorial_3/Device.java
Normal file
73
akka-docs/src/test/java/jdocs/typed/tutorial_3/Device.java
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_3;
|
||||
|
||||
//#full-device
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.PostStop;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
//#full-device
|
||||
import static jdocs.typed.tutorial_3.DeviceProtocol.*;
|
||||
/*
|
||||
//#full-device
|
||||
import static com.lightbend.akka.sample.DeviceProtocol.*;
|
||||
//#full-device
|
||||
*/
|
||||
//#full-device
|
||||
|
||||
public class Device extends AbstractBehavior<DeviceMessage> {
|
||||
|
||||
public static Behavior<DeviceMessage> createBehavior(String groupId, String deviceId) {
|
||||
return Behaviors.setup(context -> new Device(context, groupId, deviceId));
|
||||
}
|
||||
|
||||
private final ActorContext<DeviceMessage> context;
|
||||
private final String groupId;
|
||||
private final String deviceId;
|
||||
|
||||
private Optional<Double> lastTemperatureReading = Optional.empty();
|
||||
|
||||
public Device(ActorContext<DeviceMessage> context, String groupId, String deviceId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
context.getLog().info("Device actor {}-{} started", groupId, deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<DeviceMessage> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessage(RecordTemperature.class, this::recordTemperature)
|
||||
.onMessage(ReadTemperature.class, this::readTemperature)
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> recordTemperature(RecordTemperature r) {
|
||||
context.getLog().info("Recorded temperature reading {} with {}", r.value, r.requestId);
|
||||
lastTemperatureReading = Optional.of(r.value);
|
||||
r.replyTo.tell(new TemperatureRecorded(r.requestId));
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> readTemperature(ReadTemperature r) {
|
||||
r.replyTo.tell(new RespondTemperature(r.requestId, lastTemperatureReading));
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> postStop() {
|
||||
context.getLog().info("Device actor {}-{} stopped", groupId, deviceId);
|
||||
return Behaviors.stopped();
|
||||
}
|
||||
}
|
||||
//#full-device
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_3;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
abstract class DeviceProtocol {
|
||||
// no instances of DeviceProtocol class
|
||||
private DeviceProtocol() {}
|
||||
|
||||
interface DeviceMessage {}
|
||||
|
||||
//#write-protocol
|
||||
public static final class RecordTemperature implements DeviceMessage {
|
||||
final long requestId;
|
||||
final double value;
|
||||
final ActorRef<TemperatureRecorded> replyTo;
|
||||
|
||||
public RecordTemperature(long requestId, double value, ActorRef<TemperatureRecorded> replyTo){
|
||||
this.requestId = requestId;
|
||||
this.value = value;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TemperatureRecorded {
|
||||
final long requestId;
|
||||
|
||||
public TemperatureRecorded(long requestId) {
|
||||
this.requestId = requestId;
|
||||
}
|
||||
}
|
||||
//#write-protocol
|
||||
|
||||
public static final class ReadTemperature implements DeviceMessage {
|
||||
final long requestId;
|
||||
final ActorRef<RespondTemperature> replyTo;
|
||||
|
||||
public ReadTemperature(long requestId, ActorRef<RespondTemperature> replyTo) {
|
||||
this.requestId = requestId;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RespondTemperature {
|
||||
final long requestId;
|
||||
final Optional<Double> value;
|
||||
|
||||
public RespondTemperature(long requestId, Optional<Double> value) {
|
||||
this.requestId = requestId;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_3;
|
||||
|
||||
//#device-read-test
|
||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||
import akka.actor.typed.ActorRef;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
//#device-read-test
|
||||
import static jdocs.typed.tutorial_3.DeviceProtocol.*;
|
||||
/*
|
||||
//#device-read-test
|
||||
import static com.lightbend.akka.sample.DeviceProtocol.*;
|
||||
|
||||
public class DeviceTest {
|
||||
//#device-read-test
|
||||
*/
|
||||
public class DeviceTest extends org.scalatest.junit.JUnitSuite {
|
||||
//#device-read-test
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit = new TestKitJunitResource();
|
||||
|
||||
@Test
|
||||
public void testReplyWithEmptyReadingIfNoTemperatureIsKnown() {
|
||||
TestProbe<RespondTemperature> probe = testKit.createTestProbe(RespondTemperature.class);
|
||||
ActorRef<DeviceMessage> deviceActor = testKit.spawn(Device.createBehavior("group", "device"));
|
||||
deviceActor.tell(new ReadTemperature(42L, probe.getRef()));
|
||||
RespondTemperature response = probe.expectMessageClass(RespondTemperature.class);
|
||||
assertEquals(42L, response.requestId);
|
||||
assertEquals(Optional.empty(), response.value);
|
||||
}
|
||||
//#device-read-test
|
||||
|
||||
//#device-write-read-test
|
||||
@Test
|
||||
public void testReplyWithLatestTemperatureReading() {
|
||||
TestProbe<TemperatureRecorded> recordProbe = testKit.createTestProbe(TemperatureRecorded.class);
|
||||
TestProbe<RespondTemperature> readProbe = testKit.createTestProbe(RespondTemperature.class);
|
||||
ActorRef<DeviceMessage> deviceActor = testKit.spawn(Device.createBehavior("group", "device"));
|
||||
|
||||
deviceActor.tell(new RecordTemperature(1L, 24.0, recordProbe.getRef()));
|
||||
assertEquals(1L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
|
||||
deviceActor.tell(new ReadTemperature(2L, readProbe.getRef()));
|
||||
RespondTemperature response1 = readProbe.expectMessageClass(RespondTemperature.class);
|
||||
assertEquals(2L, response1.requestId);
|
||||
assertEquals(Optional.of(24.0), response1.value);
|
||||
|
||||
deviceActor.tell(new RecordTemperature(3L, 55.0, recordProbe.getRef()));
|
||||
assertEquals(3L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
|
||||
deviceActor.tell(new ReadTemperature(4L, readProbe.getRef()));
|
||||
RespondTemperature response2 = readProbe.expectMessageClass(RespondTemperature.class);
|
||||
assertEquals(4L, response2.requestId);
|
||||
assertEquals(Optional.of(55.0), response2.value);
|
||||
}
|
||||
//#device-write-read-test
|
||||
|
||||
//#device-read-test
|
||||
}
|
||||
//#device-read-test
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_3.inprogress1;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
//#read-protocol-1
|
||||
abstract class DeviceProtocol {
|
||||
// no instances of DeviceProtocol class
|
||||
private DeviceProtocol() {
|
||||
}
|
||||
|
||||
interface DeviceMessage {}
|
||||
|
||||
public static final class ReadTemperature implements DeviceMessage {
|
||||
final ActorRef<RespondTemperature> replyTo;
|
||||
|
||||
public ReadTemperature(ActorRef<RespondTemperature> replyTo) {
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RespondTemperature {
|
||||
final Optional<Double> value;
|
||||
|
||||
public RespondTemperature(Optional<Double> value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//#read-protocol-1
|
||||
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_3.inprogress2;
|
||||
|
||||
//#device-with-read
|
||||
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.PostStop;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
//#device-with-read
|
||||
import static jdocs.typed.tutorial_3.inprogress2.DeviceProtocol.*;
|
||||
/*
|
||||
//#device-with-read
|
||||
import static com.lightbend.akka.sample.DeviceProtocol.*;
|
||||
//#device-with-read
|
||||
*/
|
||||
//#device-with-read
|
||||
|
||||
public class Device extends AbstractBehavior<DeviceMessage> {
|
||||
|
||||
public static Behavior<DeviceMessage> createBehavior(String groupId, String deviceId) {
|
||||
return Behaviors.setup(context -> new Device(context, groupId, deviceId));
|
||||
}
|
||||
|
||||
private final ActorContext<DeviceMessage> context;
|
||||
private final String groupId;
|
||||
private final String deviceId;
|
||||
|
||||
private Optional<Double> lastTemperatureReading = Optional.empty();
|
||||
|
||||
public Device(ActorContext<DeviceMessage> context, String groupId, String deviceId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
context.getLog().info("Device actor {}-{} started", groupId, deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<DeviceMessage> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessage(ReadTemperature.class, this::readTemperature)
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> readTemperature(ReadTemperature r) {
|
||||
r.replyTo.tell(new RespondTemperature(r.requestId, lastTemperatureReading));
|
||||
return this;
|
||||
}
|
||||
|
||||
private Device postStop() {
|
||||
context.getLog().info("Device actor {}-{} stopped", groupId, deviceId);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//#device-with-read
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_3.inprogress2;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
//#read-protocol-2
|
||||
abstract class DeviceProtocol {
|
||||
// no instances of DeviceProtocol class
|
||||
private DeviceProtocol() {
|
||||
}
|
||||
|
||||
interface DeviceMessage {}
|
||||
|
||||
public static final class ReadTemperature implements DeviceMessage {
|
||||
final long requestId;
|
||||
final ActorRef<RespondTemperature> replyTo;
|
||||
|
||||
public ReadTemperature(long requestId, ActorRef<RespondTemperature> replyTo) {
|
||||
this.requestId = requestId;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RespondTemperature {
|
||||
final long requestId;
|
||||
final Optional<Double> value;
|
||||
|
||||
public RespondTemperature(long requestId, Optional<Double> value) {
|
||||
this.requestId = requestId;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//#read-protocol-2
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_3.inprogress3;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
abstract class DeviceProtocol {
|
||||
// no instances of DeviceProtocol class
|
||||
private DeviceProtocol() {
|
||||
}
|
||||
|
||||
interface DeviceMessage {}
|
||||
|
||||
//#write-protocol-1
|
||||
public static final class RecordTemperature implements DeviceMessage {
|
||||
final double value;
|
||||
|
||||
public RecordTemperature(double value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
//#write-protocol-1
|
||||
}
|
||||
|
||||
74
akka-docs/src/test/java/jdocs/typed/tutorial_4/Device.java
Normal file
74
akka-docs/src/test/java/jdocs/typed/tutorial_4/Device.java
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_4;
|
||||
|
||||
//#device-with-passivate
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.PostStop;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
//#device-with-passivate
|
||||
import static jdocs.typed.tutorial_4.DeviceProtocol.*;
|
||||
/*
|
||||
//#device-with-passivate
|
||||
import static com.lightbend.akka.sample.DeviceProtocol.*;
|
||||
//#device-with-passivate
|
||||
*/
|
||||
//#device-with-passivate
|
||||
|
||||
public class Device extends AbstractBehavior<DeviceMessage> {
|
||||
|
||||
public static Behavior<DeviceMessage> createBehavior(String groupId, String deviceId) {
|
||||
return Behaviors.setup(context -> new Device(context, groupId, deviceId));
|
||||
}
|
||||
|
||||
private final ActorContext<DeviceMessage> context;
|
||||
private final String groupId;
|
||||
private final String deviceId;
|
||||
|
||||
private Optional<Double> lastTemperatureReading = Optional.empty();
|
||||
|
||||
public Device(ActorContext<DeviceMessage> context, String groupId, String deviceId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
context.getLog().info("Device actor {}-{} started", groupId, deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<DeviceMessage> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessage(RecordTemperature.class, this::recordTemperature)
|
||||
.onMessage(ReadTemperature.class, this::readTemperature)
|
||||
.onMessage(Passivate.class, m -> Behaviors.stopped())
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> recordTemperature(RecordTemperature r) {
|
||||
context.getLog().info("Recorded temperature reading {} with {}", r.value, r.requestId);
|
||||
lastTemperatureReading = Optional.of(r.value);
|
||||
r.replyTo.tell(new TemperatureRecorded(r.requestId));
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> readTemperature(ReadTemperature r) {
|
||||
r.replyTo.tell(new RespondTemperature(r.requestId, lastTemperatureReading));
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> postStop() {
|
||||
context.getLog().info("Device actor {}-{} stopped", groupId, deviceId);
|
||||
return Behaviors.stopped();
|
||||
}
|
||||
}
|
||||
//#device-with-passivate
|
||||
114
akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceGroup.java
Normal file
114
akka-docs/src/test/java/jdocs/typed/tutorial_4/DeviceGroup.java
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_4;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.PostStop;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static jdocs.typed.tutorial_4.DeviceManagerProtocol.*;
|
||||
import static jdocs.typed.tutorial_4.DeviceProtocol.DeviceMessage;
|
||||
|
||||
//#device-group-full
|
||||
//#device-group-remove
|
||||
//#device-group-register
|
||||
public class DeviceGroup extends AbstractBehavior<DeviceGroupMessage> {
|
||||
|
||||
public static Behavior<DeviceGroupMessage> createBehavior(String groupId) {
|
||||
return Behaviors.setup(context -> new DeviceGroup(context, groupId));
|
||||
}
|
||||
|
||||
//#device-terminated
|
||||
private class DeviceTerminated implements DeviceGroupMessage{
|
||||
public final ActorRef<DeviceProtocol.DeviceMessage> device;
|
||||
public final String groupId;
|
||||
public final String deviceId;
|
||||
|
||||
DeviceTerminated(ActorRef<DeviceProtocol.DeviceMessage> device, String groupId, String deviceId) {
|
||||
this.device = device;
|
||||
this.groupId = groupId;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
}
|
||||
//#device-terminated
|
||||
|
||||
private final ActorContext<DeviceGroupMessage> context;
|
||||
private final String groupId;
|
||||
private final Map<String, ActorRef<DeviceMessage>> deviceIdToActor = new HashMap<>();
|
||||
|
||||
public DeviceGroup(ActorContext<DeviceGroupMessage> context, String groupId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
context.getLog().info("DeviceGroup {} started", groupId);
|
||||
}
|
||||
|
||||
private DeviceGroup onTrackDevice(RequestTrackDevice trackMsg) {
|
||||
if (this.groupId.equals(trackMsg.groupId)) {
|
||||
ActorRef<DeviceMessage> deviceActor = deviceIdToActor.get(trackMsg.deviceId);
|
||||
if (deviceActor != null) {
|
||||
trackMsg.replyTo.tell(new DeviceRegistered(deviceActor));
|
||||
} else {
|
||||
context.getLog().info("Creating device actor for {}", trackMsg.deviceId);
|
||||
deviceActor = context.spawn(Device.createBehavior(groupId, trackMsg.deviceId), "device-" + trackMsg.deviceId);
|
||||
//#device-group-register
|
||||
context.watchWith(deviceActor, new DeviceTerminated(deviceActor, groupId, trackMsg.deviceId));
|
||||
//#device-group-register
|
||||
deviceIdToActor.put(trackMsg.deviceId, deviceActor);
|
||||
trackMsg.replyTo.tell(new DeviceRegistered(deviceActor));
|
||||
}
|
||||
} else {
|
||||
context.getLog().warning(
|
||||
"Ignoring TrackDevice request for {}. This actor is responsible for {}.",
|
||||
groupId, this.groupId
|
||||
);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
//#device-group-register
|
||||
//#device-group-remove
|
||||
|
||||
private DeviceGroup onDeviceList(RequestDeviceList r) {
|
||||
r.replyTo.tell(new ReplyDeviceList(r.requestId, deviceIdToActor.keySet()));
|
||||
return this;
|
||||
}
|
||||
//#device-group-remove
|
||||
|
||||
private DeviceGroup onTerminated(DeviceTerminated t) {
|
||||
context.getLog().info("Device actor for {} has been terminated", t.deviceId);
|
||||
deviceIdToActor.remove(t.deviceId);
|
||||
return this;
|
||||
}
|
||||
//#device-group-register
|
||||
|
||||
@Override
|
||||
public Receive<DeviceGroupMessage> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessage(RequestTrackDevice.class, this::onTrackDevice)
|
||||
//#device-group-register
|
||||
//#device-group-remove
|
||||
.onMessage(RequestDeviceList.class, r -> r.groupId.equals(groupId), this::onDeviceList)
|
||||
//#device-group-remove
|
||||
.onMessage(DeviceTerminated.class, this::onTerminated)
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
//#device-group-register
|
||||
.build();
|
||||
}
|
||||
|
||||
private DeviceGroup postStop() {
|
||||
context.getLog().info("DeviceGroup {} stopped", groupId);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
//#device-group-register
|
||||
//#device-group-remove
|
||||
//#device-group-full
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_4;
|
||||
|
||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||
import akka.actor.typed.ActorRef;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.scalatest.junit.JUnitSuite;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static jdocs.typed.tutorial_4.DeviceManagerProtocol.*;
|
||||
import static jdocs.typed.tutorial_4.DeviceProtocol.*;
|
||||
|
||||
public class DeviceGroupTest extends JUnitSuite {
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit = new TestKitJunitResource();
|
||||
|
||||
//#device-group-test-registration
|
||||
@Test
|
||||
public void testReplyToRegistrationRequests() {
|
||||
TestProbe<DeviceRegistered> probe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device", probe.getRef()));
|
||||
DeviceRegistered registered1 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
// another deviceId
|
||||
groupActor.tell(new RequestTrackDevice("group", "device3", probe.getRef()));
|
||||
DeviceRegistered registered2 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
assertNotEquals(registered1.device, registered2.device);
|
||||
|
||||
// Check that the device actors are working
|
||||
TestProbe<TemperatureRecorded> recordProbe = testKit.createTestProbe(TemperatureRecorded.class);
|
||||
registered1.device.tell(new RecordTemperature(0L, 1.0, recordProbe.getRef()));
|
||||
assertEquals(0L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
registered2.device.tell(new RecordTemperature(1L, 2.0, recordProbe.getRef()));
|
||||
assertEquals(1L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIgnoreWrongRegistrationRequests() {
|
||||
TestProbe<DeviceRegistered> probe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
groupActor.tell(new RequestTrackDevice("wrongGroup", "device1", probe.getRef()));
|
||||
probe.expectNoMessage();
|
||||
}
|
||||
//#device-group-test-registration
|
||||
|
||||
//#device-group-test3
|
||||
@Test
|
||||
public void testReturnSameActorForSameDeviceId() {
|
||||
TestProbe<DeviceRegistered> probe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device", probe.getRef()));
|
||||
DeviceRegistered registered1 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
// registering same again should be idempotent
|
||||
groupActor.tell(new RequestTrackDevice("group", "device", probe.getRef()));
|
||||
DeviceRegistered registered2 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
assertEquals(registered1.device, registered2.device);
|
||||
}
|
||||
//#device-group-test3
|
||||
|
||||
//#device-group-list-terminate-test
|
||||
@Test
|
||||
public void testListActiveDevices() {
|
||||
TestProbe<DeviceRegistered> registeredProbe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device1", registeredProbe.getRef()));
|
||||
registeredProbe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device2", registeredProbe.getRef()));
|
||||
registeredProbe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
TestProbe<ReplyDeviceList> deviceListProbe = testKit.createTestProbe(ReplyDeviceList.class);
|
||||
|
||||
groupActor.tell(new RequestDeviceList(0L, "group", deviceListProbe.getRef()));
|
||||
ReplyDeviceList reply = deviceListProbe.expectMessageClass(ReplyDeviceList.class);
|
||||
assertEquals(0L, reply.requestId);
|
||||
assertEquals(Stream.of("device1", "device2").collect(Collectors.toSet()), reply.ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListActiveDevicesAfterOneShutsDown() {
|
||||
TestProbe<DeviceRegistered> registeredProbe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device1", registeredProbe.getRef()));
|
||||
DeviceRegistered registered1 = registeredProbe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device2", registeredProbe.getRef()));
|
||||
DeviceRegistered registered2 = registeredProbe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
ActorRef<DeviceMessage> toShutDown = registered1.device;
|
||||
|
||||
TestProbe<ReplyDeviceList> deviceListProbe = testKit.createTestProbe(ReplyDeviceList.class);
|
||||
|
||||
groupActor.tell(new RequestDeviceList(0L, "group", deviceListProbe.getRef()));
|
||||
ReplyDeviceList reply = deviceListProbe.expectMessageClass(ReplyDeviceList.class);
|
||||
assertEquals(0L, reply.requestId);
|
||||
assertEquals(Stream.of("device1", "device2").collect(Collectors.toSet()), reply.ids);
|
||||
|
||||
toShutDown.tell(Passivate.INSTANCE);
|
||||
registeredProbe.expectTerminated(toShutDown, registeredProbe.getRemainingOrDefault());
|
||||
|
||||
// using awaitAssert to retry because it might take longer for the groupActor
|
||||
// to see the Terminated, that order is undefined
|
||||
registeredProbe.awaitAssert(() -> {
|
||||
groupActor.tell(new RequestDeviceList(1L, "group", deviceListProbe.getRef()));
|
||||
ReplyDeviceList r =
|
||||
deviceListProbe.expectMessageClass(ReplyDeviceList.class);
|
||||
assertEquals(1L, r.requestId);
|
||||
assertEquals(Stream.of("device2").collect(Collectors.toSet()), r.ids);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
//#device-group-list-terminate-test
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_4;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.PostStop;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static jdocs.typed.tutorial_4.DeviceManagerProtocol.*;
|
||||
|
||||
//#device-manager-full
|
||||
public class DeviceManager extends AbstractBehavior<DeviceManagerMessage> {
|
||||
|
||||
public static Behavior<DeviceManagerMessage> createBehavior() {
|
||||
return Behaviors.setup(DeviceManager::new);
|
||||
}
|
||||
|
||||
private static class DeviceGroupTerminated implements DeviceManagerMessage{
|
||||
public final String groupId;
|
||||
|
||||
DeviceGroupTerminated(String groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
}
|
||||
|
||||
private final ActorContext<DeviceManagerMessage> context;
|
||||
private final Map<String, ActorRef<DeviceGroupMessage>> groupIdToActor = new HashMap<>();
|
||||
|
||||
public DeviceManager(ActorContext<DeviceManagerMessage> context) {
|
||||
this.context = context;
|
||||
context.getLog().info("DeviceManager started");
|
||||
}
|
||||
|
||||
private DeviceManager onTrackDevice(RequestTrackDevice trackMsg) {
|
||||
String groupId = trackMsg.groupId;
|
||||
ActorRef<DeviceGroupMessage> ref = groupIdToActor.get(groupId);
|
||||
if (ref != null) {
|
||||
ref.tell(trackMsg);
|
||||
} else {
|
||||
context.getLog().info("Creating device group actor for {}", groupId);
|
||||
ActorRef<DeviceGroupMessage> groupActor =
|
||||
context.spawn(DeviceGroup.createBehavior(groupId), "group-" + groupId);
|
||||
context.watchWith(groupActor, new DeviceGroupTerminated(groupId));
|
||||
groupActor.tell(trackMsg);
|
||||
groupIdToActor.put(groupId, groupActor);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private DeviceManager onRequestDeviceList(RequestDeviceList request) {
|
||||
ActorRef<DeviceGroupMessage> ref = groupIdToActor.get(request.groupId);
|
||||
if (ref != null) {
|
||||
ref.tell(request);
|
||||
} else {
|
||||
request.replyTo.tell(new ReplyDeviceList(request.requestId, Collections.emptySet()));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private DeviceManager onTerminated(DeviceGroupTerminated t) {
|
||||
context.getLog().info("Device group actor for {} has been terminated", t.groupId);
|
||||
groupIdToActor.remove(t.groupId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Receive<DeviceManagerMessage> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessage(RequestTrackDevice.class, this::onTrackDevice)
|
||||
.onMessage(RequestDeviceList.class, this::onRequestDeviceList)
|
||||
.onMessage(DeviceGroupTerminated.class, this::onTerminated)
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DeviceManager postStop() {
|
||||
context.getLog().info("DeviceManager stopped");
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
//#device-manager-full
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_4;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
//#device-registration-msgs
|
||||
abstract class DeviceManagerProtocol {
|
||||
// no instances of DeviceManagerProtocol class
|
||||
private DeviceManagerProtocol() {}
|
||||
|
||||
interface DeviceManagerMessage {}
|
||||
|
||||
interface DeviceGroupMessage {}
|
||||
|
||||
public static final class RequestTrackDevice implements DeviceManagerMessage, DeviceGroupMessage {
|
||||
public final String groupId;
|
||||
public final String deviceId;
|
||||
public final ActorRef<DeviceRegistered> replyTo;
|
||||
|
||||
public RequestTrackDevice(String groupId, String deviceId, ActorRef<DeviceRegistered> replyTo) {
|
||||
this.groupId = groupId;
|
||||
this.deviceId = deviceId;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DeviceRegistered {
|
||||
public final ActorRef<DeviceProtocol.DeviceMessage> device;
|
||||
|
||||
public DeviceRegistered(ActorRef<DeviceProtocol.DeviceMessage> device) {
|
||||
this.device = device;
|
||||
}
|
||||
}
|
||||
//#device-registration-msgs
|
||||
|
||||
//#device-list-msgs
|
||||
public static final class RequestDeviceList implements DeviceManagerMessage, DeviceGroupMessage {
|
||||
final long requestId;
|
||||
final String groupId;
|
||||
final ActorRef<ReplyDeviceList> replyTo;
|
||||
|
||||
public RequestDeviceList(long requestId, String groupId, ActorRef<ReplyDeviceList> replyTo) {
|
||||
this.requestId = requestId;
|
||||
this.groupId = groupId;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ReplyDeviceList {
|
||||
final long requestId;
|
||||
final Set<String> ids;
|
||||
|
||||
public ReplyDeviceList(long requestId, Set<String> ids) {
|
||||
this.requestId = requestId;
|
||||
this.ids = ids;
|
||||
}
|
||||
}
|
||||
//#device-list-msgs
|
||||
|
||||
//#device-registration-msgs
|
||||
}
|
||||
//#device-registration-msgs
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_4;
|
||||
|
||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||
import akka.actor.typed.ActorRef;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.scalatest.junit.JUnitSuite;
|
||||
|
||||
import static jdocs.typed.tutorial_4.DeviceManagerProtocol.*;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class DeviceManagerTest extends JUnitSuite {
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit = new TestKitJunitResource();
|
||||
|
||||
@Test
|
||||
public void testReplyToRegistrationRequests() {
|
||||
TestProbe<DeviceRegistered> probe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceManagerMessage> managerActor = testKit.spawn(DeviceManager.createBehavior());
|
||||
|
||||
managerActor.tell(new RequestTrackDevice("group1", "device", probe.getRef()));
|
||||
DeviceRegistered registered1 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
// another group
|
||||
managerActor.tell(new RequestTrackDevice("group2", "device", probe.getRef()));
|
||||
DeviceRegistered registered2 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
assertNotEquals(registered1.device, registered2.device);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_4;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
abstract class DeviceProtocol {
|
||||
// no instances of DeviceProtocol class
|
||||
private DeviceProtocol() {}
|
||||
|
||||
interface DeviceMessage {}
|
||||
|
||||
public static final class RecordTemperature implements DeviceMessage {
|
||||
final long requestId;
|
||||
final double value;
|
||||
final ActorRef<TemperatureRecorded> replyTo;
|
||||
|
||||
public RecordTemperature(long requestId, double value, ActorRef<TemperatureRecorded> replyTo){
|
||||
this.requestId = requestId;
|
||||
this.value = value;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TemperatureRecorded {
|
||||
final long requestId;
|
||||
|
||||
public TemperatureRecorded(long requestId) {
|
||||
this.requestId = requestId;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ReadTemperature implements DeviceMessage {
|
||||
final long requestId;
|
||||
final ActorRef<RespondTemperature> replyTo;
|
||||
|
||||
public ReadTemperature(long requestId, ActorRef<RespondTemperature> replyTo) {
|
||||
this.requestId = requestId;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RespondTemperature {
|
||||
final long requestId;
|
||||
final Optional<Double> value;
|
||||
|
||||
public RespondTemperature(long requestId, Optional<Double> value) {
|
||||
this.requestId = requestId;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
//#passivate-msg
|
||||
static enum Passivate implements DeviceMessage {
|
||||
INSTANCE
|
||||
}
|
||||
//#passivate-msg
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_4;
|
||||
|
||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||
import akka.actor.typed.ActorRef;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.scalatest.junit.JUnitSuite;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static jdocs.typed.tutorial_4.DeviceProtocol.*;
|
||||
|
||||
public class DeviceTest extends JUnitSuite {
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit = new TestKitJunitResource();
|
||||
|
||||
//#device-read-test
|
||||
@Test
|
||||
public void testReplyWithEmptyReadingIfNoTemperatureIsKnown() {
|
||||
TestProbe<RespondTemperature> probe = testKit.createTestProbe(RespondTemperature.class);
|
||||
ActorRef<DeviceMessage> deviceActor = testKit.spawn(Device.createBehavior("group", "device"));
|
||||
deviceActor.tell(new ReadTemperature(42L, probe.getRef()));
|
||||
RespondTemperature response = probe.expectMessageClass(RespondTemperature.class);
|
||||
assertEquals(42L, response.requestId);
|
||||
assertEquals(Optional.empty(), response.value);
|
||||
}
|
||||
//#device-read-test
|
||||
|
||||
//#device-write-read-test
|
||||
@Test
|
||||
public void testReplyWithLatestTemperatureReading() {
|
||||
TestProbe<TemperatureRecorded> recordProbe = testKit.createTestProbe(TemperatureRecorded.class);
|
||||
TestProbe<RespondTemperature> readProbe = testKit.createTestProbe(RespondTemperature.class);
|
||||
ActorRef<DeviceMessage> deviceActor = testKit.spawn(Device.createBehavior("group", "device"));
|
||||
|
||||
deviceActor.tell(new RecordTemperature(1L, 24.0, recordProbe.getRef()));
|
||||
assertEquals(1L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
|
||||
deviceActor.tell(new ReadTemperature(2L, readProbe.getRef()));
|
||||
RespondTemperature response1 = readProbe.expectMessageClass(RespondTemperature.class);
|
||||
assertEquals(2L, response1.requestId);
|
||||
assertEquals(Optional.of(24.0), response1.value);
|
||||
|
||||
deviceActor.tell(new RecordTemperature(3L, 55.0, recordProbe.getRef()));
|
||||
assertEquals(3L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
|
||||
deviceActor.tell(new ReadTemperature(4L, readProbe.getRef()));
|
||||
RespondTemperature response2 = readProbe.expectMessageClass(RespondTemperature.class);
|
||||
assertEquals(4L, response2.requestId);
|
||||
assertEquals(Optional.of(55.0), response2.value);
|
||||
}
|
||||
//#device-write-read-test
|
||||
|
||||
}
|
||||
64
akka-docs/src/test/java/jdocs/typed/tutorial_5/Device.java
Normal file
64
akka-docs/src/test/java/jdocs/typed/tutorial_5/Device.java
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.PostStop;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
import static jdocs.typed.tutorial_5.DeviceProtocol.*;
|
||||
|
||||
public class Device extends AbstractBehavior<DeviceMessage> {
|
||||
|
||||
public static Behavior<DeviceMessage> createBehavior(String groupId, String deviceId) {
|
||||
return Behaviors.setup(context -> new Device(context, groupId, deviceId));
|
||||
}
|
||||
|
||||
private final ActorContext<DeviceMessage> context;
|
||||
private final String groupId;
|
||||
private final String deviceId;
|
||||
|
||||
private Optional<Double> lastTemperatureReading = Optional.empty();
|
||||
|
||||
public Device(ActorContext<DeviceMessage> context, String groupId, String deviceId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
context.getLog().info("Device actor {}-{} started", groupId, deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<DeviceMessage> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessage(RecordTemperature.class, this::recordTemperature)
|
||||
.onMessage(ReadTemperature.class, this::readTemperature)
|
||||
.onMessage(Passivate.class, m -> Behaviors.stopped())
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> recordTemperature(RecordTemperature r) {
|
||||
context.getLog().info("Recorded temperature reading {} with {}", r.value, r.requestId);
|
||||
lastTemperatureReading = Optional.of(r.value);
|
||||
r.replyTo.tell(new TemperatureRecorded(r.requestId));
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> readTemperature(ReadTemperature r) {
|
||||
r.replyTo.tell(new RespondTemperature(r.requestId, deviceId, lastTemperatureReading));
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<DeviceMessage> postStop() {
|
||||
context.getLog().info("Device actor {}-{} stopped", groupId, deviceId);
|
||||
return Behaviors.stopped();
|
||||
}
|
||||
}
|
||||
118
akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroup.java
Normal file
118
akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroup.java
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.PostStop;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static jdocs.typed.tutorial_5.DeviceManagerProtocol.*;
|
||||
import static jdocs.typed.tutorial_5.DeviceProtocol.DeviceMessage;
|
||||
|
||||
//#query-added
|
||||
public class DeviceGroup extends AbstractBehavior<DeviceGroupMessage> {
|
||||
|
||||
public static Behavior<DeviceGroupMessage> createBehavior(String groupId) {
|
||||
return Behaviors.setup(context -> new DeviceGroup(context, groupId));
|
||||
}
|
||||
|
||||
private class DeviceTerminated implements DeviceGroupMessage{
|
||||
public final ActorRef<DeviceProtocol.DeviceMessage> device;
|
||||
public final String groupId;
|
||||
public final String deviceId;
|
||||
|
||||
DeviceTerminated(ActorRef<DeviceProtocol.DeviceMessage> device, String groupId, String deviceId) {
|
||||
this.device = device;
|
||||
this.groupId = groupId;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
private final ActorContext<DeviceGroupMessage> context;
|
||||
private final String groupId;
|
||||
private final Map<String, ActorRef<DeviceMessage>> deviceIdToActor = new HashMap<>();
|
||||
|
||||
public DeviceGroup(ActorContext<DeviceGroupMessage> context, String groupId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
context.getLog().info("DeviceGroup {} started", groupId);
|
||||
}
|
||||
|
||||
//#query-added
|
||||
private DeviceGroup onTrackDevice(RequestTrackDevice trackMsg) {
|
||||
if (this.groupId.equals(trackMsg.groupId)) {
|
||||
ActorRef<DeviceMessage> deviceActor = deviceIdToActor.get(trackMsg.deviceId);
|
||||
if (deviceActor != null) {
|
||||
trackMsg.replyTo.tell(new DeviceRegistered(deviceActor));
|
||||
} else {
|
||||
context.getLog().info("Creating device actor for {}", trackMsg.deviceId);
|
||||
deviceActor = context.spawn(Device.createBehavior(groupId, trackMsg.deviceId), "device-" + trackMsg.deviceId);
|
||||
context.watchWith(deviceActor, new DeviceTerminated(deviceActor, groupId, trackMsg.deviceId));
|
||||
deviceIdToActor.put(trackMsg.deviceId, deviceActor);
|
||||
trackMsg.replyTo.tell(new DeviceRegistered(deviceActor));
|
||||
}
|
||||
} else {
|
||||
context.getLog().warning(
|
||||
"Ignoring TrackDevice request for {}. This actor is responsible for {}.",
|
||||
groupId, this.groupId
|
||||
);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private DeviceGroup onDeviceList(RequestDeviceList r) {
|
||||
r.replyTo.tell(new ReplyDeviceList(r.requestId, deviceIdToActor.keySet()));
|
||||
return this;
|
||||
}
|
||||
|
||||
private DeviceGroup onTerminated(DeviceTerminated t) {
|
||||
context.getLog().info("Device actor for {} has been terminated", t.deviceId);
|
||||
deviceIdToActor.remove(t.deviceId);
|
||||
return this;
|
||||
}
|
||||
|
||||
private DeviceGroup postStop() {
|
||||
context.getLog().info("DeviceGroup {} stopped", groupId);
|
||||
return this;
|
||||
}
|
||||
|
||||
//#query-added
|
||||
|
||||
private DeviceGroup onAllTemperatures(RequestAllTemperatures r) {
|
||||
// since Java collections are mutable, we want to avoid sharing them between actors (since multiple Actors (threads)
|
||||
// modifying the same mutable data-structure is not safe), and perform a defensive copy of the mutable map:
|
||||
//
|
||||
// Feel free to use your favourite immutable data-structures library with Akka in Java applications!
|
||||
Map<String, ActorRef<DeviceMessage>> deviceIdToActorCopy = new HashMap<>(this.deviceIdToActor);
|
||||
|
||||
context.spawnAnonymous(DeviceGroupQuery.createBehavior(
|
||||
deviceIdToActorCopy, r.requestId, r.replyTo, Duration.ofSeconds(3)));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<DeviceGroupMessage> createReceive() {
|
||||
return receiveBuilder()
|
||||
//#query-added
|
||||
.onMessage(RequestTrackDevice.class, this::onTrackDevice)
|
||||
.onMessage(RequestDeviceList.class, r -> r.groupId.equals(groupId), this::onDeviceList)
|
||||
.onMessage(DeviceTerminated.class, this::onTerminated)
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
//#query-added
|
||||
// ... other cases omitted
|
||||
.onMessage(RequestAllTemperatures.class, r -> r.groupId.equals(groupId), this::onAllTemperatures)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
//#query-added
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
import akka.actor.typed.javadsl.TimerScheduler;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static jdocs.typed.tutorial_5.DeviceManagerProtocol.*;
|
||||
|
||||
//#query-full
|
||||
//#query-outline
|
||||
public class DeviceGroupQuery extends AbstractBehavior<DeviceGroupQueryMessage> {
|
||||
|
||||
public static Behavior<DeviceGroupQueryMessage> createBehavior(
|
||||
Map<String, ActorRef<DeviceProtocol.DeviceMessage>> deviceIdToActor,
|
||||
long requestId,
|
||||
ActorRef<RespondAllTemperatures> requester,
|
||||
Duration timeout) {
|
||||
return
|
||||
Behaviors.setup(context ->
|
||||
Behaviors.withTimers(timers -> new DeviceGroupQuery(
|
||||
deviceIdToActor, requestId, requester, timeout, context, timers)));
|
||||
}
|
||||
|
||||
private static enum CollectionTimeout implements DeviceGroupQueryMessage {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
static class WrappedRespondTemperature implements DeviceGroupQueryMessage {
|
||||
final DeviceProtocol.RespondTemperature response;
|
||||
|
||||
WrappedRespondTemperature(DeviceProtocol.RespondTemperature response) {
|
||||
this.response = response;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeviceTerminated implements DeviceGroupQueryMessage {
|
||||
final String deviceId;
|
||||
|
||||
private DeviceTerminated(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
private final long requestId;
|
||||
private final ActorRef<RespondAllTemperatures> requester;
|
||||
//#query-outline
|
||||
//#query-state
|
||||
private Map<String, TemperatureReading> repliesSoFar = new HashMap<>();
|
||||
private final Set<String> stillWaiting;
|
||||
|
||||
//#query-state
|
||||
//#query-outline
|
||||
|
||||
public DeviceGroupQuery(
|
||||
Map<String, ActorRef<DeviceProtocol.DeviceMessage>> deviceIdToActor,
|
||||
long requestId,
|
||||
ActorRef<RespondAllTemperatures> requester,
|
||||
Duration timeout,
|
||||
ActorContext<DeviceGroupQueryMessage> context, TimerScheduler<DeviceGroupQueryMessage> timers) {
|
||||
this.requestId = requestId;
|
||||
this.requester = requester;
|
||||
|
||||
timers.startSingleTimer(CollectionTimeout.class, CollectionTimeout.INSTANCE, timeout);
|
||||
|
||||
ActorRef<DeviceProtocol.RespondTemperature> respondTemperatureAdapter =
|
||||
context.messageAdapter(DeviceProtocol.RespondTemperature.class, WrappedRespondTemperature::new);
|
||||
|
||||
for (Map.Entry<String, ActorRef<DeviceProtocol.DeviceMessage>> entry : deviceIdToActor.entrySet()) {
|
||||
context.watchWith(entry.getValue(), new DeviceTerminated(entry.getKey()));
|
||||
entry.getValue().tell(new DeviceProtocol.ReadTemperature(0L, respondTemperatureAdapter));
|
||||
}
|
||||
stillWaiting = new HashSet<>(deviceIdToActor.keySet());
|
||||
}
|
||||
|
||||
//#query-outline
|
||||
//#query-state
|
||||
@Override
|
||||
public Receive<DeviceGroupQueryMessage> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessage(WrappedRespondTemperature.class, this::onRespondTemperature)
|
||||
.onMessage(DeviceTerminated.class, this::onDeviceTerminated)
|
||||
.onMessage(CollectionTimeout.class, this::onCollectionTimeout)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<DeviceGroupQueryMessage> onRespondTemperature(WrappedRespondTemperature r) {
|
||||
TemperatureReading reading = r.response.value
|
||||
.map(v -> (TemperatureReading) new Temperature(v))
|
||||
.orElse(TemperatureNotAvailable.INSTANCE);
|
||||
|
||||
String deviceId = r.response.deviceId;
|
||||
repliesSoFar.put(deviceId, reading);
|
||||
stillWaiting.remove(deviceId);
|
||||
|
||||
return respondWhenAllCollected();
|
||||
}
|
||||
|
||||
private Behavior<DeviceGroupQueryMessage> onDeviceTerminated(DeviceTerminated terminated) {
|
||||
if (stillWaiting.contains(terminated.deviceId)) {
|
||||
repliesSoFar.put(terminated.deviceId, DeviceNotAvailable.INSTANCE);
|
||||
stillWaiting.remove(terminated.deviceId);
|
||||
}
|
||||
return respondWhenAllCollected();
|
||||
}
|
||||
|
||||
private Behavior<DeviceGroupQueryMessage> onCollectionTimeout(CollectionTimeout timeout) {
|
||||
for (String deviceId : stillWaiting) {
|
||||
repliesSoFar.put(deviceId, DeviceTimedOut.INSTANCE);
|
||||
}
|
||||
stillWaiting.clear();
|
||||
return respondWhenAllCollected();
|
||||
}
|
||||
//#query-state
|
||||
|
||||
//#query-collect-reply
|
||||
private Behavior<DeviceGroupQueryMessage> respondWhenAllCollected() {
|
||||
if (stillWaiting.isEmpty()) {
|
||||
requester.tell(new RespondAllTemperatures(requestId, repliesSoFar));
|
||||
return Behaviors.stopped();
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
//#query-collect-reply
|
||||
//#query-outline
|
||||
|
||||
}
|
||||
//#query-outline
|
||||
//#query-full
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||
import akka.actor.typed.ActorRef;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.scalatest.junit.JUnitSuite;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static jdocs.typed.tutorial_5.DeviceManagerProtocol.*;
|
||||
import static jdocs.typed.tutorial_5.DeviceProtocol.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class DeviceGroupQueryTest extends JUnitSuite {
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit = new TestKitJunitResource();
|
||||
|
||||
//#query-test-normal
|
||||
@Test
|
||||
public void testReturnTemperatureValueForWorkingDevices() {
|
||||
TestProbe<RespondAllTemperatures> requester = testKit.createTestProbe(RespondAllTemperatures.class);
|
||||
TestProbe<DeviceMessage> device1 = testKit.createTestProbe(DeviceMessage.class);
|
||||
TestProbe<DeviceMessage> device2 = testKit.createTestProbe(DeviceMessage.class);
|
||||
|
||||
Map<String, ActorRef<DeviceProtocol.DeviceMessage>> deviceIdToActor = new HashMap<>();
|
||||
deviceIdToActor.put("device1", device1.getRef());
|
||||
deviceIdToActor.put("device2", device2.getRef());
|
||||
|
||||
ActorRef<DeviceGroupQueryMessage> queryActor = testKit.spawn(DeviceGroupQuery.createBehavior(
|
||||
deviceIdToActor,
|
||||
1L,
|
||||
requester.getRef(),
|
||||
Duration.ofSeconds(3)));
|
||||
|
||||
device1.expectMessageClass(ReadTemperature.class);
|
||||
device2.expectMessageClass(ReadTemperature.class);
|
||||
|
||||
queryActor.tell(new DeviceGroupQuery.WrappedRespondTemperature(
|
||||
new RespondTemperature(0L, "device1", Optional.of(1.0))));
|
||||
|
||||
queryActor.tell(new DeviceGroupQuery.WrappedRespondTemperature(
|
||||
new RespondTemperature(0L, "device2", Optional.of(2.0))));
|
||||
|
||||
RespondAllTemperatures response = requester.expectMessageClass(RespondAllTemperatures.class);
|
||||
assertEquals(1L, response.requestId);
|
||||
|
||||
Map<String, TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", new Temperature(1.0));
|
||||
expectedTemperatures.put("device2", new Temperature(2.0));
|
||||
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-normal
|
||||
|
||||
//#query-test-no-reading
|
||||
@Test
|
||||
public void testReturnTemperatureNotAvailableForDevicesWithNoReadings() {
|
||||
TestProbe<RespondAllTemperatures> requester = testKit.createTestProbe(RespondAllTemperatures.class);
|
||||
TestProbe<DeviceMessage> device1 = testKit.createTestProbe(DeviceMessage.class);
|
||||
TestProbe<DeviceMessage> device2 = testKit.createTestProbe(DeviceMessage.class);
|
||||
|
||||
Map<String, ActorRef<DeviceProtocol.DeviceMessage>> deviceIdToActor = new HashMap<>();
|
||||
deviceIdToActor.put("device1", device1.getRef());
|
||||
deviceIdToActor.put("device2", device2.getRef());
|
||||
|
||||
ActorRef<DeviceGroupQueryMessage> queryActor = testKit.spawn(DeviceGroupQuery.createBehavior(
|
||||
deviceIdToActor,
|
||||
1L,
|
||||
requester.getRef(),
|
||||
Duration.ofSeconds(3)));
|
||||
|
||||
assertEquals(0L, device1.expectMessageClass(ReadTemperature.class).requestId);
|
||||
assertEquals(0L, device2.expectMessageClass(ReadTemperature.class).requestId);
|
||||
|
||||
queryActor.tell(new DeviceGroupQuery.WrappedRespondTemperature(
|
||||
new RespondTemperature(0L, "device1", Optional.empty())));
|
||||
|
||||
queryActor.tell(new DeviceGroupQuery.WrappedRespondTemperature(
|
||||
new RespondTemperature(0L, "device2", Optional.of(2.0))));
|
||||
|
||||
RespondAllTemperatures response = requester.expectMessageClass(RespondAllTemperatures.class);
|
||||
assertEquals(1L, response.requestId);
|
||||
|
||||
Map<String, TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", TemperatureNotAvailable.INSTANCE);
|
||||
expectedTemperatures.put("device2", new Temperature(2.0));
|
||||
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-no-reading
|
||||
|
||||
//#query-test-stopped
|
||||
@Test
|
||||
public void testReturnDeviceNotAvailableIfDeviceStopsBeforeAnswering() {
|
||||
TestProbe<RespondAllTemperatures> requester = testKit.createTestProbe(RespondAllTemperatures.class);
|
||||
TestProbe<DeviceMessage> device1 = testKit.createTestProbe(DeviceMessage.class);
|
||||
TestProbe<DeviceMessage> device2 = testKit.createTestProbe(DeviceMessage.class);
|
||||
|
||||
Map<String, ActorRef<DeviceProtocol.DeviceMessage>> deviceIdToActor = new HashMap<>();
|
||||
deviceIdToActor.put("device1", device1.getRef());
|
||||
deviceIdToActor.put("device2", device2.getRef());
|
||||
|
||||
ActorRef<DeviceGroupQueryMessage> queryActor = testKit.spawn(DeviceGroupQuery.createBehavior(
|
||||
deviceIdToActor,
|
||||
1L,
|
||||
requester.getRef(),
|
||||
Duration.ofSeconds(3)));
|
||||
|
||||
assertEquals(0L, device1.expectMessageClass(ReadTemperature.class).requestId);
|
||||
assertEquals(0L, device2.expectMessageClass(ReadTemperature.class).requestId);
|
||||
|
||||
queryActor.tell(new DeviceGroupQuery.WrappedRespondTemperature(
|
||||
new RespondTemperature(0L, "device1", Optional.of(1.0))));
|
||||
|
||||
device2.stop();
|
||||
|
||||
RespondAllTemperatures response = requester.expectMessageClass(RespondAllTemperatures.class);
|
||||
assertEquals(1L, response.requestId);
|
||||
|
||||
Map<String, TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", new Temperature(1.0));
|
||||
expectedTemperatures.put("device2", DeviceNotAvailable.INSTANCE);
|
||||
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-stopped
|
||||
|
||||
//#query-test-stopped-later
|
||||
@Test
|
||||
public void testReturnTemperatureReadingEvenIfDeviceStopsAfterAnswering() {
|
||||
TestProbe<RespondAllTemperatures> requester = testKit.createTestProbe(RespondAllTemperatures.class);
|
||||
TestProbe<DeviceMessage> device1 = testKit.createTestProbe(DeviceMessage.class);
|
||||
TestProbe<DeviceMessage> device2 = testKit.createTestProbe(DeviceMessage.class);
|
||||
|
||||
Map<String, ActorRef<DeviceProtocol.DeviceMessage>> deviceIdToActor = new HashMap<>();
|
||||
deviceIdToActor.put("device1", device1.getRef());
|
||||
deviceIdToActor.put("device2", device2.getRef());
|
||||
|
||||
ActorRef<DeviceGroupQueryMessage> queryActor = testKit.spawn(DeviceGroupQuery.createBehavior(
|
||||
deviceIdToActor,
|
||||
1L,
|
||||
requester.getRef(),
|
||||
Duration.ofSeconds(3)));
|
||||
|
||||
assertEquals(0L, device1.expectMessageClass(ReadTemperature.class).requestId);
|
||||
assertEquals(0L, device2.expectMessageClass(ReadTemperature.class).requestId);
|
||||
|
||||
queryActor.tell(new DeviceGroupQuery.WrappedRespondTemperature(
|
||||
new RespondTemperature(0L, "device1", Optional.of(1.0))));
|
||||
|
||||
queryActor.tell(new DeviceGroupQuery.WrappedRespondTemperature(
|
||||
new RespondTemperature(0L, "device2", Optional.of(2.0))));
|
||||
|
||||
device2.stop();
|
||||
|
||||
RespondAllTemperatures response = requester.expectMessageClass(RespondAllTemperatures.class);
|
||||
assertEquals(1L, response.requestId);
|
||||
|
||||
Map<String, TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", new Temperature(1.0));
|
||||
expectedTemperatures.put("device2", new Temperature(2.0));
|
||||
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-stopped-later
|
||||
|
||||
//#query-test-timeout
|
||||
@Test
|
||||
public void testReturnDeviceTimedOutIfDeviceDoesNotAnswerInTime() {
|
||||
TestProbe<RespondAllTemperatures> requester = testKit.createTestProbe(RespondAllTemperatures.class);
|
||||
TestProbe<DeviceMessage> device1 = testKit.createTestProbe(DeviceMessage.class);
|
||||
TestProbe<DeviceMessage> device2 = testKit.createTestProbe(DeviceMessage.class);
|
||||
|
||||
Map<String, ActorRef<DeviceProtocol.DeviceMessage>> deviceIdToActor = new HashMap<>();
|
||||
deviceIdToActor.put("device1", device1.getRef());
|
||||
deviceIdToActor.put("device2", device2.getRef());
|
||||
|
||||
ActorRef<DeviceGroupQueryMessage> queryActor = testKit.spawn(DeviceGroupQuery.createBehavior(
|
||||
deviceIdToActor,
|
||||
1L,
|
||||
requester.getRef(),
|
||||
Duration.ofMillis(200)));
|
||||
|
||||
assertEquals(0L, device1.expectMessageClass(ReadTemperature.class).requestId);
|
||||
assertEquals(0L, device2.expectMessageClass(ReadTemperature.class).requestId);
|
||||
|
||||
queryActor.tell(new DeviceGroupQuery.WrappedRespondTemperature(
|
||||
new RespondTemperature(0L, "device1", Optional.of(1.0))));
|
||||
|
||||
// no reply from device2
|
||||
|
||||
RespondAllTemperatures response = requester.expectMessageClass(RespondAllTemperatures.class);
|
||||
assertEquals(1L, response.requestId);
|
||||
|
||||
Map<String, TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", new Temperature(1.0));
|
||||
expectedTemperatures.put("device2", DeviceTimedOut.INSTANCE);
|
||||
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#query-test-timeout
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||
import akka.actor.typed.ActorRef;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.scalatest.junit.JUnitSuite;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static jdocs.typed.tutorial_5.DeviceManagerProtocol.*;
|
||||
import static jdocs.typed.tutorial_5.DeviceProtocol.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class DeviceGroupTest extends JUnitSuite {
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit = new TestKitJunitResource();
|
||||
|
||||
@Test
|
||||
public void testReplyToRegistrationRequests() {
|
||||
TestProbe<DeviceRegistered> probe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device", probe.getRef()));
|
||||
DeviceRegistered registered1 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
// another deviceId
|
||||
groupActor.tell(new RequestTrackDevice("group", "device3", probe.getRef()));
|
||||
DeviceRegistered registered2 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
assertNotEquals(registered1.device, registered2.device);
|
||||
|
||||
// Check that the device actors are working
|
||||
TestProbe<TemperatureRecorded> recordProbe = testKit.createTestProbe(TemperatureRecorded.class);
|
||||
registered1.device.tell(new RecordTemperature(0L, 1.0, recordProbe.getRef()));
|
||||
assertEquals(0L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
registered2.device.tell(new RecordTemperature(1L, 2.0, recordProbe.getRef()));
|
||||
assertEquals(1L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIgnoreWrongRegistrationRequests() {
|
||||
TestProbe<DeviceRegistered> probe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
groupActor.tell(new RequestTrackDevice("wrongGroup", "device1", probe.getRef()));
|
||||
probe.expectNoMessage();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnSameActorForSameDeviceId() {
|
||||
TestProbe<DeviceRegistered> probe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device", probe.getRef()));
|
||||
DeviceRegistered registered1 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
// registering same again should be idempotent
|
||||
groupActor.tell(new RequestTrackDevice("group", "device", probe.getRef()));
|
||||
DeviceRegistered registered2 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
assertEquals(registered1.device, registered2.device);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListActiveDevices() {
|
||||
TestProbe<DeviceRegistered> registeredProbe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device1", registeredProbe.getRef()));
|
||||
registeredProbe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device2", registeredProbe.getRef()));
|
||||
registeredProbe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
TestProbe<ReplyDeviceList> deviceListProbe = testKit.createTestProbe(ReplyDeviceList.class);
|
||||
|
||||
groupActor.tell(new RequestDeviceList(0L, "group", deviceListProbe.getRef()));
|
||||
ReplyDeviceList reply = deviceListProbe.expectMessageClass(ReplyDeviceList.class);
|
||||
assertEquals(0L, reply.requestId);
|
||||
assertEquals(Stream.of("device1", "device2").collect(Collectors.toSet()), reply.ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListActiveDevicesAfterOneShutsDown() {
|
||||
TestProbe<DeviceRegistered> registeredProbe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device1", registeredProbe.getRef()));
|
||||
DeviceRegistered registered1 = registeredProbe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device2", registeredProbe.getRef()));
|
||||
registeredProbe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
ActorRef<DeviceMessage> toShutDown = registered1.device;
|
||||
|
||||
TestProbe<ReplyDeviceList> deviceListProbe = testKit.createTestProbe(ReplyDeviceList.class);
|
||||
|
||||
groupActor.tell(new RequestDeviceList(0L, "group", deviceListProbe.getRef()));
|
||||
ReplyDeviceList reply = deviceListProbe.expectMessageClass(ReplyDeviceList.class);
|
||||
assertEquals(0L, reply.requestId);
|
||||
assertEquals(Stream.of("device1", "device2").collect(Collectors.toSet()), reply.ids);
|
||||
|
||||
toShutDown.tell(Passivate.INSTANCE);
|
||||
registeredProbe.expectTerminated(toShutDown, registeredProbe.getRemainingOrDefault());
|
||||
|
||||
// using awaitAssert to retry because it might take longer for the groupActor
|
||||
// to see the Terminated, that order is undefined
|
||||
registeredProbe.awaitAssert(() -> {
|
||||
groupActor.tell(new RequestDeviceList(1L, "group", deviceListProbe.getRef()));
|
||||
ReplyDeviceList r =
|
||||
deviceListProbe.expectMessageClass(ReplyDeviceList.class);
|
||||
assertEquals(1L, r.requestId);
|
||||
assertEquals(Stream.of("device2").collect(Collectors.toSet()), r.ids);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
//#group-query-integration-test
|
||||
@Test
|
||||
public void testCollectTemperaturesFromAllActiveDevices() {
|
||||
TestProbe<DeviceRegistered> registeredProbe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceGroupMessage> groupActor = testKit.spawn(DeviceGroup.createBehavior("group"));
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device1", registeredProbe.getRef()));
|
||||
ActorRef<DeviceMessage> deviceActor1 = registeredProbe.expectMessageClass(DeviceRegistered.class).device;
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device2", registeredProbe.getRef()));
|
||||
ActorRef<DeviceMessage> deviceActor2 = registeredProbe.expectMessageClass(DeviceRegistered.class).device;
|
||||
|
||||
groupActor.tell(new RequestTrackDevice("group", "device3", registeredProbe.getRef()));
|
||||
ActorRef<DeviceMessage> deviceActor3 = registeredProbe.expectMessageClass(DeviceRegistered.class).device;
|
||||
|
||||
// Check that the device actors are working
|
||||
TestProbe<TemperatureRecorded> recordProbe = testKit.createTestProbe(TemperatureRecorded.class);
|
||||
deviceActor1.tell(new RecordTemperature(0L, 1.0, recordProbe.getRef()));
|
||||
assertEquals(0L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
deviceActor2.tell(new RecordTemperature(1L, 2.0, recordProbe.getRef()));
|
||||
assertEquals(1L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
// No temperature for device 3
|
||||
|
||||
TestProbe<RespondAllTemperatures> allTempProbe = testKit.createTestProbe(RespondAllTemperatures.class);
|
||||
groupActor.tell(new RequestAllTemperatures(0L, "group", allTempProbe.getRef()));
|
||||
RespondAllTemperatures response = allTempProbe.expectMessageClass(RespondAllTemperatures.class);
|
||||
assertEquals(0L, response.requestId);
|
||||
|
||||
Map<String, TemperatureReading> expectedTemperatures = new HashMap<>();
|
||||
expectedTemperatures.put("device1", new Temperature(1.0));
|
||||
expectedTemperatures.put("device2", new Temperature(2.0));
|
||||
expectedTemperatures.put("device3", TemperatureNotAvailable.INSTANCE);
|
||||
|
||||
assertEquals(expectedTemperatures, response.temperatures);
|
||||
}
|
||||
//#group-query-integration-test
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.PostStop;
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static jdocs.typed.tutorial_5.DeviceManagerProtocol.*;
|
||||
|
||||
public class DeviceManager extends AbstractBehavior<DeviceManagerMessage> {
|
||||
|
||||
public static Behavior<DeviceManagerMessage> createBehavior() {
|
||||
return Behaviors.setup(DeviceManager::new);
|
||||
}
|
||||
|
||||
private static class DeviceGroupTerminated implements DeviceManagerMessage{
|
||||
public final String groupId;
|
||||
|
||||
DeviceGroupTerminated(String groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
}
|
||||
|
||||
private final ActorContext<DeviceManagerMessage> context;
|
||||
private final Map<String, ActorRef<DeviceGroupMessage>> groupIdToActor = new HashMap<>();
|
||||
|
||||
public DeviceManager(ActorContext<DeviceManagerMessage> context) {
|
||||
this.context = context;
|
||||
context.getLog().info("DeviceManager started");
|
||||
}
|
||||
|
||||
private DeviceManager onTrackDevice(RequestTrackDevice trackMsg) {
|
||||
String groupId = trackMsg.groupId;
|
||||
ActorRef<DeviceGroupMessage> ref = groupIdToActor.get(groupId);
|
||||
if (ref != null) {
|
||||
ref.tell(trackMsg);
|
||||
} else {
|
||||
context.getLog().info("Creating device group actor for {}", groupId);
|
||||
ActorRef<DeviceGroupMessage> groupActor =
|
||||
context.spawn(DeviceGroup.createBehavior(groupId), "group-" + groupId);
|
||||
context.watchWith(groupActor, new DeviceGroupTerminated(groupId));
|
||||
groupActor.tell(trackMsg);
|
||||
groupIdToActor.put(groupId, groupActor);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private DeviceManager onRequestDeviceList(RequestDeviceList request) {
|
||||
ActorRef<DeviceGroupMessage> ref = groupIdToActor.get(request.groupId);
|
||||
if (ref != null) {
|
||||
ref.tell(request);
|
||||
} else {
|
||||
request.replyTo.tell(new ReplyDeviceList(request.requestId, Collections.emptySet()));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private DeviceManager onRequestAllTemperatures(RequestAllTemperatures request) {
|
||||
ActorRef<DeviceGroupMessage> ref = groupIdToActor.get(request.groupId);
|
||||
if (ref != null) {
|
||||
ref.tell(request);
|
||||
} else {
|
||||
request.replyTo.tell(new RespondAllTemperatures(request.requestId, Collections.emptyMap()));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private DeviceManager onTerminated(DeviceGroupTerminated t) {
|
||||
context.getLog().info("Device group actor for {} has been terminated", t.groupId);
|
||||
groupIdToActor.remove(t.groupId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Receive<DeviceManagerMessage> createReceive() {
|
||||
return receiveBuilder()
|
||||
.onMessage(RequestTrackDevice.class, this::onTrackDevice)
|
||||
.onMessage(RequestDeviceList.class, this::onRequestDeviceList)
|
||||
.onMessage(RequestAllTemperatures.class, this::onRequestAllTemperatures)
|
||||
.onMessage(DeviceGroupTerminated.class, this::onTerminated)
|
||||
.onSignal(PostStop.class, signal -> postStop())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DeviceManager postStop() {
|
||||
context.getLog().info("DeviceManager stopped");
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
abstract class DeviceManagerProtocol {
|
||||
// no instances of DeviceManagerProtocol class
|
||||
private DeviceManagerProtocol() {}
|
||||
|
||||
interface DeviceManagerMessage {}
|
||||
|
||||
interface DeviceGroupMessage {}
|
||||
|
||||
public static final class RequestTrackDevice implements DeviceManagerMessage, DeviceGroupMessage {
|
||||
public final String groupId;
|
||||
public final String deviceId;
|
||||
public final ActorRef<DeviceRegistered> replyTo;
|
||||
|
||||
public RequestTrackDevice(String groupId, String deviceId, ActorRef<DeviceRegistered> replyTo) {
|
||||
this.groupId = groupId;
|
||||
this.deviceId = deviceId;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DeviceRegistered {
|
||||
public final ActorRef<DeviceProtocol.DeviceMessage> device;
|
||||
|
||||
public DeviceRegistered(ActorRef<DeviceProtocol.DeviceMessage> device) {
|
||||
this.device = device;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RequestDeviceList implements DeviceManagerMessage, DeviceGroupMessage {
|
||||
final long requestId;
|
||||
final String groupId;
|
||||
final ActorRef<ReplyDeviceList> replyTo;
|
||||
|
||||
public RequestDeviceList(long requestId, String groupId, ActorRef<ReplyDeviceList> replyTo) {
|
||||
this.requestId = requestId;
|
||||
this.groupId = groupId;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ReplyDeviceList {
|
||||
final long requestId;
|
||||
final Set<String> ids;
|
||||
|
||||
public ReplyDeviceList(long requestId, Set<String> ids) {
|
||||
this.requestId = requestId;
|
||||
this.ids = ids;
|
||||
}
|
||||
}
|
||||
|
||||
//#query-protocol
|
||||
interface DeviceGroupQueryMessage {}
|
||||
|
||||
public static final class RequestAllTemperatures
|
||||
implements DeviceGroupQueryMessage, DeviceGroupMessage, DeviceManagerMessage {
|
||||
|
||||
final long requestId;
|
||||
final String groupId;
|
||||
final ActorRef<RespondAllTemperatures> replyTo;
|
||||
|
||||
public RequestAllTemperatures(long requestId, String groupId, ActorRef<RespondAllTemperatures> replyTo) {
|
||||
this.requestId = requestId;
|
||||
this.groupId = groupId;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RespondAllTemperatures {
|
||||
final long requestId;
|
||||
final Map<String, TemperatureReading> temperatures;
|
||||
|
||||
public RespondAllTemperatures(long requestId, Map<String, TemperatureReading> temperatures) {
|
||||
this.requestId = requestId;
|
||||
this.temperatures = temperatures;
|
||||
}
|
||||
}
|
||||
|
||||
public static interface TemperatureReading {
|
||||
}
|
||||
|
||||
public static final class Temperature implements TemperatureReading {
|
||||
public final double value;
|
||||
|
||||
public Temperature(double value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Temperature that = (Temperature) o;
|
||||
|
||||
return Double.compare(that.value, value) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
long temp = Double.doubleToLongBits(value);
|
||||
return (int) (temp ^ (temp >>> 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Temperature{" +
|
||||
"value=" + value +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public enum TemperatureNotAvailable implements TemperatureReading {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public enum DeviceNotAvailable implements TemperatureReading {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public enum DeviceTimedOut implements TemperatureReading {
|
||||
INSTANCE
|
||||
}
|
||||
//#query-protocol
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||
import akka.actor.typed.ActorRef;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.scalatest.junit.JUnitSuite;
|
||||
|
||||
import static jdocs.typed.tutorial_5.DeviceManagerProtocol.*;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class DeviceManagerTest extends JUnitSuite {
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit = new TestKitJunitResource();
|
||||
|
||||
@Test
|
||||
public void testReplyToRegistrationRequests() {
|
||||
TestProbe<DeviceRegistered> probe = testKit.createTestProbe(DeviceRegistered.class);
|
||||
ActorRef<DeviceManagerMessage> managerActor = testKit.spawn(DeviceManager.createBehavior());
|
||||
|
||||
managerActor.tell(new RequestTrackDevice("group1", "device", probe.getRef()));
|
||||
DeviceRegistered registered1 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
|
||||
// another group
|
||||
managerActor.tell(new RequestTrackDevice("group2", "device", probe.getRef()));
|
||||
DeviceRegistered registered2 = probe.expectMessageClass(DeviceRegistered.class);
|
||||
assertNotEquals(registered1.device, registered2.device);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
abstract class DeviceProtocol {
|
||||
// no instances of DeviceProtocol class
|
||||
private DeviceProtocol() {}
|
||||
|
||||
interface DeviceMessage {}
|
||||
|
||||
public static final class RecordTemperature implements DeviceMessage {
|
||||
final long requestId;
|
||||
final double value;
|
||||
final ActorRef<TemperatureRecorded> replyTo;
|
||||
|
||||
public RecordTemperature(long requestId, double value, ActorRef<TemperatureRecorded> replyTo){
|
||||
this.requestId = requestId;
|
||||
this.value = value;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TemperatureRecorded {
|
||||
final long requestId;
|
||||
|
||||
public TemperatureRecorded(long requestId) {
|
||||
this.requestId = requestId;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ReadTemperature implements DeviceMessage {
|
||||
final long requestId;
|
||||
final ActorRef<RespondTemperature> replyTo;
|
||||
|
||||
public ReadTemperature(long requestId, ActorRef<RespondTemperature> replyTo) {
|
||||
this.requestId = requestId;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RespondTemperature {
|
||||
final long requestId;
|
||||
final String deviceId;
|
||||
final Optional<Double> value;
|
||||
|
||||
public RespondTemperature(long requestId, String deviceId, Optional<Double> value) {
|
||||
this.requestId = requestId;
|
||||
this.deviceId = deviceId;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
static enum Passivate implements DeviceMessage {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdocs.typed.tutorial_5;
|
||||
|
||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||
import akka.actor.testkit.typed.javadsl.TestProbe;
|
||||
import akka.actor.typed.ActorRef;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.scalatest.junit.JUnitSuite;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static jdocs.typed.tutorial_5.DeviceManagerProtocol.*;
|
||||
import static jdocs.typed.tutorial_5.DeviceProtocol.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class DeviceTest extends JUnitSuite {
|
||||
|
||||
@ClassRule
|
||||
public static final TestKitJunitResource testKit = new TestKitJunitResource();
|
||||
|
||||
@Test
|
||||
public void testReplyWithEmptyReadingIfNoTemperatureIsKnown() {
|
||||
TestProbe<RespondTemperature> probe = testKit.createTestProbe(RespondTemperature.class);
|
||||
ActorRef<DeviceMessage> deviceActor = testKit.spawn(Device.createBehavior("group", "device"));
|
||||
deviceActor.tell(new ReadTemperature(42L, probe.getRef()));
|
||||
RespondTemperature response = probe.expectMessageClass(RespondTemperature.class);
|
||||
assertEquals(42L, response.requestId);
|
||||
assertEquals(Optional.empty(), response.value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplyWithLatestTemperatureReading() {
|
||||
TestProbe<TemperatureRecorded> recordProbe = testKit.createTestProbe(TemperatureRecorded.class);
|
||||
TestProbe<RespondTemperature> readProbe = testKit.createTestProbe(RespondTemperature.class);
|
||||
ActorRef<DeviceMessage> deviceActor = testKit.spawn(Device.createBehavior("group", "device"));
|
||||
|
||||
deviceActor.tell(new RecordTemperature(1L, 24.0, recordProbe.getRef()));
|
||||
assertEquals(1L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
|
||||
deviceActor.tell(new ReadTemperature(2L, readProbe.getRef()));
|
||||
RespondTemperature response1 = readProbe.expectMessageClass(RespondTemperature.class);
|
||||
assertEquals(2L, response1.requestId);
|
||||
assertEquals(Optional.of(24.0), response1.value);
|
||||
|
||||
deviceActor.tell(new RecordTemperature(3L, 55.0, recordProbe.getRef()));
|
||||
assertEquals(3L, recordProbe.expectMessageClass(TemperatureRecorded.class).requestId);
|
||||
|
||||
deviceActor.tell(new ReadTemperature(4L, readProbe.getRef()));
|
||||
RespondTemperature response2 = readProbe.expectMessageClass(RespondTemperature.class);
|
||||
assertEquals(4L, response2.requestId);
|
||||
assertEquals(Optional.of(55.0), response2.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_1
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.WordSpecLike
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.PreRestart
|
||||
import akka.actor.typed.Signal
|
||||
import akka.actor.typed.SupervisorStrategy
|
||||
|
||||
//#print-refs
|
||||
import akka.actor.typed.ActorSystem
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
object PrintMyActorRefActor {
|
||||
def apply(): Behavior[String] =
|
||||
Behaviors.setup(context ⇒ new PrintMyActorRefActor(context))
|
||||
}
|
||||
|
||||
class PrintMyActorRefActor(context: ActorContext[String]) extends AbstractBehavior[String] {
|
||||
|
||||
override def onMessage(msg: String): Behavior[String] =
|
||||
msg match {
|
||||
case "printit" ⇒
|
||||
val secondRef = context.spawn(Behaviors.empty[String], "second-actor")
|
||||
println(s"Second: $secondRef")
|
||||
this
|
||||
}
|
||||
}
|
||||
//#print-refs
|
||||
|
||||
//#start-stop
|
||||
object StartStopActor1 {
|
||||
def apply(): Behavior[String] =
|
||||
Behaviors.setup(context ⇒ new StartStopActor1(context))
|
||||
}
|
||||
|
||||
class StartStopActor1(context: ActorContext[String]) extends AbstractBehavior[String] {
|
||||
println("first started")
|
||||
context.spawn(StartStopActor2(), "second")
|
||||
|
||||
override def onMessage(msg: String): Behavior[String] =
|
||||
msg match {
|
||||
case "stop" ⇒ Behaviors.stopped
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[String]] = {
|
||||
case PostStop ⇒
|
||||
println("first stopped")
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object StartStopActor2 {
|
||||
def apply(): Behavior[String] =
|
||||
Behaviors.setup(_ ⇒ new StartStopActor2)
|
||||
}
|
||||
|
||||
class StartStopActor2 extends AbstractBehavior[String] {
|
||||
println("second started")
|
||||
|
||||
override def onMessage(msg: String): Behavior[String] = {
|
||||
// no messages handled by this actor
|
||||
Behaviors.unhandled
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[String]] = {
|
||||
case PostStop ⇒
|
||||
println("second stopped")
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
//#start-stop
|
||||
|
||||
//#supervise
|
||||
object SupervisingActor {
|
||||
def apply(): Behavior[String] =
|
||||
Behaviors.setup(context ⇒ new SupervisingActor(context))
|
||||
}
|
||||
|
||||
class SupervisingActor(context: ActorContext[String]) extends AbstractBehavior[String] {
|
||||
private val child = context.spawn(
|
||||
Behaviors.supervise(SupervisedActor()).onFailure(SupervisorStrategy.restart),
|
||||
name = "supervised-actor")
|
||||
|
||||
override def onMessage(msg: String): Behavior[String] =
|
||||
msg match {
|
||||
case "failChild" ⇒
|
||||
child ! "fail"
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
object SupervisedActor {
|
||||
def apply(): Behavior[String] =
|
||||
Behaviors.setup(_ ⇒ new SupervisedActor)
|
||||
}
|
||||
|
||||
class SupervisedActor extends AbstractBehavior[String] {
|
||||
println("supervised actor started")
|
||||
|
||||
override def onMessage(msg: String): Behavior[String] =
|
||||
msg match {
|
||||
case "fail" ⇒
|
||||
println("supervised actor fails now")
|
||||
throw new Exception("I failed!")
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[String]] = {
|
||||
case PreRestart ⇒
|
||||
println("supervised actor will be restarted")
|
||||
this
|
||||
case PostStop ⇒
|
||||
println("supervised actor stopped")
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
//#supervise
|
||||
|
||||
//#print-refs
|
||||
|
||||
object Main {
|
||||
def apply(): Behavior[String] =
|
||||
Behaviors.setup(context ⇒ new Main(context))
|
||||
|
||||
}
|
||||
|
||||
class Main(context: ActorContext[String]) extends AbstractBehavior[String] {
|
||||
override def onMessage(msg: String): Behavior[String] =
|
||||
msg match {
|
||||
case "start" ⇒
|
||||
val firstRef = context.spawn(PrintMyActorRefActor(), "first-actor")
|
||||
println(s"First: $firstRef")
|
||||
firstRef ! "printit"
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
object ActorHierarchyExperiments extends App {
|
||||
ActorSystem(Main(), "testSystem")
|
||||
}
|
||||
//#print-refs
|
||||
|
||||
class ActorHierarchyExperiments extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||
|
||||
"ActorHierarchyExperiments" must {
|
||||
|
||||
def context = this
|
||||
|
||||
"start and stop actors" in {
|
||||
//#start-stop-main
|
||||
val first = context.spawn(StartStopActor1(), "first")
|
||||
first ! "stop"
|
||||
//#start-stop-main
|
||||
}
|
||||
|
||||
"supervise actors" in {
|
||||
//#supervise-main
|
||||
val supervisingActor = context.spawn(SupervisingActor(), "supervising-actor")
|
||||
supervisingActor ! "failChild"
|
||||
//#supervise-main
|
||||
Thread.sleep(200) // allow for the println/logging to complete
|
||||
}
|
||||
}
|
||||
}
|
||||
18
akka-docs/src/test/scala/typed/tutorial_2/IotApp.scala
Normal file
18
akka-docs/src/test/scala/typed/tutorial_2/IotApp.scala
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_2
|
||||
|
||||
//#iot-app
|
||||
import akka.actor.typed.ActorSystem
|
||||
|
||||
object IotApp {
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
// Create ActorSystem and top level supervisor
|
||||
ActorSystem[Nothing](IotSupervisor(), "iot-system")
|
||||
}
|
||||
|
||||
}
|
||||
//#iot-app
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_2
|
||||
|
||||
//#iot-supervisor
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.Signal
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
object IotSupervisor {
|
||||
def apply(): Behavior[Nothing] =
|
||||
Behaviors.setup[Nothing](context ⇒ new IotSupervisor(context))
|
||||
}
|
||||
|
||||
class IotSupervisor(context: ActorContext[Nothing]) extends AbstractBehavior[Nothing] {
|
||||
context.log.info("IoT Application started")
|
||||
|
||||
override def onMessage(msg: Nothing): Behavior[Nothing] = {
|
||||
// No need to handle any messages
|
||||
Behaviors.unhandled
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[Nothing]] = {
|
||||
case PostStop ⇒
|
||||
context.log.info("IoT Application stopped")
|
||||
this
|
||||
}
|
||||
}
|
||||
//#iot-supervisor
|
||||
62
akka-docs/src/test/scala/typed/tutorial_3/Device.scala
Normal file
62
akka-docs/src/test/scala/typed/tutorial_3/Device.scala
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_3
|
||||
|
||||
//#full-device
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.Signal
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
object Device {
|
||||
def apply(groupId: String, deviceId: String): Behavior[DeviceMessage] =
|
||||
Behaviors.setup(context ⇒ new Device(context, groupId, deviceId))
|
||||
|
||||
sealed trait DeviceMessage
|
||||
|
||||
final case class ReadTemperature(requestId: Long, replyTo: ActorRef[RespondTemperature])
|
||||
extends DeviceMessage
|
||||
final case class RespondTemperature(requestId: Long, value: Option[Double])
|
||||
|
||||
//#write-protocol
|
||||
final case class RecordTemperature(requestId: Long, value: Double, replyTo: ActorRef[TemperatureRecorded])
|
||||
extends DeviceMessage
|
||||
final case class TemperatureRecorded(requestId: Long)
|
||||
//#write-protocol
|
||||
}
|
||||
|
||||
class Device(context: ActorContext[Device.DeviceMessage], groupId: String, deviceId: String)
|
||||
extends AbstractBehavior[Device.DeviceMessage] {
|
||||
import Device._
|
||||
|
||||
var lastTemperatureReading: Option[Double] = None
|
||||
|
||||
context.log.info("Device actor {}-{} started", groupId, deviceId)
|
||||
|
||||
override def onMessage(msg: DeviceMessage): Behavior[DeviceMessage] = {
|
||||
msg match {
|
||||
case RecordTemperature(id, value, replyTo) ⇒
|
||||
context.log.info("Recorded temperature reading {} with {}", value, id)
|
||||
lastTemperatureReading = Some(value)
|
||||
replyTo ! TemperatureRecorded(id)
|
||||
this
|
||||
|
||||
case ReadTemperature(id, replyTo) ⇒
|
||||
replyTo ! RespondTemperature(id, lastTemperatureReading)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[DeviceMessage]] = {
|
||||
case PostStop ⇒
|
||||
context.log.info("Device actor {}-{} stopped", groupId, deviceId)
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
//#full-device
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_3
|
||||
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.Signal
|
||||
|
||||
object DeviceInProgress1 {
|
||||
|
||||
//#read-protocol-1
|
||||
import akka.actor.typed.ActorRef
|
||||
|
||||
object Device {
|
||||
sealed trait DeviceMessage
|
||||
final case class ReadTemperature(replyTo: ActorRef[RespondTemperature]) extends DeviceMessage
|
||||
final case class RespondTemperature(value: Option[Double])
|
||||
}
|
||||
//#read-protocol-1
|
||||
|
||||
}
|
||||
|
||||
object DeviceInProgress2 {
|
||||
import akka.actor.typed.ActorRef
|
||||
|
||||
//#device-with-read
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
object Device {
|
||||
def apply(groupId: String, deviceId: String): Behavior[DeviceMessage] =
|
||||
Behaviors.setup(context ⇒ new Device(context, groupId, deviceId))
|
||||
|
||||
//#read-protocol-2
|
||||
sealed trait DeviceMessage
|
||||
final case class ReadTemperature(requestId: Long, replyTo: ActorRef[RespondTemperature])
|
||||
extends DeviceMessage
|
||||
final case class RespondTemperature(requestId: Long, value: Option[Double])
|
||||
//#read-protocol-2
|
||||
}
|
||||
|
||||
class Device(context: ActorContext[Device.DeviceMessage], groupId: String, deviceId: String)
|
||||
extends AbstractBehavior[Device.DeviceMessage] {
|
||||
import Device._
|
||||
|
||||
var lastTemperatureReading: Option[Double] = None
|
||||
|
||||
context.log.info("Device actor {}-{} started", groupId, deviceId)
|
||||
|
||||
override def onMessage(msg: DeviceMessage): Behavior[DeviceMessage] = {
|
||||
msg match {
|
||||
case ReadTemperature(id, replyTo) ⇒
|
||||
replyTo ! RespondTemperature(id, lastTemperatureReading)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[DeviceMessage]] = {
|
||||
case PostStop ⇒
|
||||
context.log.info("Device actor {}-{} stopped", groupId, deviceId)
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
//#device-with-read
|
||||
|
||||
}
|
||||
|
||||
object DeviceInProgress3 {
|
||||
|
||||
object Device {
|
||||
//#write-protocol-1
|
||||
sealed trait DeviceMessage
|
||||
final case class RecordTemperature(value: Double)
|
||||
extends DeviceMessage
|
||||
//#write-protocol-1
|
||||
}
|
||||
}
|
||||
54
akka-docs/src/test/scala/typed/tutorial_3/DeviceSpec.scala
Normal file
54
akka-docs/src/test/scala/typed/tutorial_3/DeviceSpec.scala
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_3
|
||||
|
||||
//#device-read-test
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.WordSpecLike
|
||||
|
||||
class DeviceSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||
import Device._
|
||||
|
||||
"Device actor" must {
|
||||
|
||||
"reply with empty reading if no temperature is known" in {
|
||||
val probe = createTestProbe[RespondTemperature]()
|
||||
val deviceActor = spawn(Device("group", "device"))
|
||||
|
||||
deviceActor ! Device.ReadTemperature(requestId = 42, probe.ref)
|
||||
val response = probe.expectMessageType[Device.RespondTemperature]
|
||||
response.requestId should ===(42)
|
||||
response.value should ===(None)
|
||||
}
|
||||
//#device-read-test
|
||||
|
||||
//#device-write-read-test
|
||||
"reply with latest temperature reading" in {
|
||||
val recordProbe = createTestProbe[TemperatureRecorded]()
|
||||
val readProbe = createTestProbe[RespondTemperature]()
|
||||
val deviceActor = spawn(Device("group", "device"))
|
||||
|
||||
deviceActor ! Device.RecordTemperature(requestId = 1, 24.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(Device.TemperatureRecorded(requestId = 1))
|
||||
|
||||
deviceActor ! Device.ReadTemperature(requestId = 2, readProbe.ref)
|
||||
val response1 = readProbe.expectMessageType[RespondTemperature]
|
||||
response1.requestId should ===(2)
|
||||
response1.value should ===(Some(24.0))
|
||||
|
||||
deviceActor ! Device.RecordTemperature(requestId = 3, 55.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(Device.TemperatureRecorded(requestId = 3))
|
||||
|
||||
deviceActor ! Device.ReadTemperature(requestId = 4, readProbe.ref)
|
||||
val response2 = readProbe.expectMessageType[RespondTemperature]
|
||||
response2.requestId should ===(4)
|
||||
response2.value should ===(Some(55.0))
|
||||
}
|
||||
//#device-write-read-test
|
||||
|
||||
}
|
||||
//#device-read-test
|
||||
}
|
||||
//#device-read-test
|
||||
67
akka-docs/src/test/scala/typed/tutorial_4/Device.scala
Normal file
67
akka-docs/src/test/scala/typed/tutorial_4/Device.scala
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_4
|
||||
|
||||
//#device-with-passivate
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.Signal
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
object Device {
|
||||
def apply(groupId: String, deviceId: String): Behavior[DeviceMessage] =
|
||||
Behaviors.setup(context ⇒ new Device(context, groupId, deviceId))
|
||||
|
||||
sealed trait DeviceMessage
|
||||
|
||||
final case class ReadTemperature(requestId: Long, replyTo: ActorRef[RespondTemperature])
|
||||
extends DeviceMessage
|
||||
final case class RespondTemperature(requestId: Long, value: Option[Double])
|
||||
|
||||
final case class RecordTemperature(requestId: Long, value: Double, replyTo: ActorRef[TemperatureRecorded])
|
||||
extends DeviceMessage
|
||||
final case class TemperatureRecorded(requestId: Long)
|
||||
|
||||
//#passivate-msg
|
||||
case object Passivate extends DeviceMessage
|
||||
//#passivate-msg
|
||||
}
|
||||
|
||||
class Device(context: ActorContext[Device.DeviceMessage], groupId: String, deviceId: String)
|
||||
extends AbstractBehavior[Device.DeviceMessage] {
|
||||
import Device._
|
||||
|
||||
var lastTemperatureReading: Option[Double] = None
|
||||
|
||||
context.log.info("Device actor {}-{} started", groupId, deviceId)
|
||||
|
||||
override def onMessage(msg: DeviceMessage): Behavior[DeviceMessage] = {
|
||||
msg match {
|
||||
case RecordTemperature(id, value, replyTo) ⇒
|
||||
context.log.info("Recorded temperature reading {} with {}", value, id)
|
||||
lastTemperatureReading = Some(value)
|
||||
replyTo ! TemperatureRecorded(id)
|
||||
this
|
||||
|
||||
case ReadTemperature(id, replyTo) ⇒
|
||||
replyTo ! RespondTemperature(id, lastTemperatureReading)
|
||||
this
|
||||
|
||||
case Passivate ⇒
|
||||
Behaviors.stopped
|
||||
}
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[DeviceMessage]] = {
|
||||
case PostStop ⇒
|
||||
context.log.info("Device actor {}-{} stopped", groupId, deviceId)
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
//#device-with-passivate
|
||||
92
akka-docs/src/test/scala/typed/tutorial_4/DeviceGroup.scala
Normal file
92
akka-docs/src/test/scala/typed/tutorial_4/DeviceGroup.scala
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_4
|
||||
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.Signal
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
//#device-group-full
|
||||
//#device-group-register
|
||||
object DeviceGroup {
|
||||
def apply(groupId: String): Behavior[DeviceGroupMessage] =
|
||||
Behaviors.setup(context ⇒ new DeviceGroup(context, groupId))
|
||||
|
||||
trait DeviceGroupMessage
|
||||
|
||||
private final case class DeviceTerminated(
|
||||
device: ActorRef[Device.DeviceMessage],
|
||||
groupId: String,
|
||||
deviceId: String) extends DeviceGroupMessage
|
||||
|
||||
}
|
||||
//#device-group-register
|
||||
//#device-group-register
|
||||
//#device-group-remove
|
||||
|
||||
class DeviceGroup(context: ActorContext[DeviceGroup.DeviceGroupMessage], groupId: String)
|
||||
extends AbstractBehavior[DeviceGroup.DeviceGroupMessage] {
|
||||
import DeviceGroup._
|
||||
import DeviceManager._
|
||||
|
||||
private var deviceIdToActor = Map.empty[String, ActorRef[Device.DeviceMessage]]
|
||||
|
||||
context.log.info("DeviceGroup {} started", groupId)
|
||||
|
||||
override def onMessage(msg: DeviceGroupMessage): Behavior[DeviceGroupMessage] =
|
||||
msg match {
|
||||
case trackMsg @ RequestTrackDevice(`groupId`, deviceId, replyTo) ⇒
|
||||
deviceIdToActor.get(deviceId) match {
|
||||
case Some(deviceActor) ⇒
|
||||
replyTo ! DeviceRegistered(deviceActor)
|
||||
case None ⇒
|
||||
context.log.info("Creating device actor for {}", trackMsg.deviceId)
|
||||
val deviceActor = context.spawn(Device(groupId, deviceId), s"device-$deviceId")
|
||||
//#device-group-register
|
||||
context.watchWith(deviceActor, DeviceTerminated(deviceActor, groupId, deviceId))
|
||||
//#device-group-register
|
||||
deviceIdToActor += deviceId -> deviceActor
|
||||
replyTo ! DeviceRegistered(deviceActor)
|
||||
}
|
||||
this
|
||||
|
||||
case RequestTrackDevice(gId, _, _) ⇒
|
||||
context.log.warning(
|
||||
"Ignoring TrackDevice request for {}. This actor is responsible for {}.",
|
||||
gId, groupId
|
||||
)
|
||||
this
|
||||
//#device-group-register
|
||||
//#device-group-remove
|
||||
|
||||
case RequestDeviceList(requestId, gId, replyTo) ⇒
|
||||
if (gId == groupId) {
|
||||
replyTo ! ReplyDeviceList(requestId, deviceIdToActor.keySet)
|
||||
this
|
||||
} else
|
||||
Behaviors.unhandled
|
||||
//#device-group-remove
|
||||
|
||||
case DeviceTerminated(_, _, deviceId) ⇒
|
||||
context.log.info("Device actor for {} has been terminated", deviceId)
|
||||
deviceIdToActor -= deviceId
|
||||
this
|
||||
|
||||
//#device-group-register
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[DeviceGroupMessage]] = {
|
||||
case PostStop ⇒
|
||||
context.log.info("DeviceGroup {} stopped", groupId)
|
||||
this
|
||||
}
|
||||
}
|
||||
//#device-group-remove
|
||||
//#device-group-register
|
||||
//#device-group-full
|
||||
111
akka-docs/src/test/scala/typed/tutorial_4/DeviceGroupSpec.scala
Normal file
111
akka-docs/src/test/scala/typed/tutorial_4/DeviceGroupSpec.scala
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_4
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.WordSpecLike
|
||||
import typed.tutorial_4.Device._
|
||||
import typed.tutorial_4.DeviceManager._
|
||||
|
||||
class DeviceGroupSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||
|
||||
"DeviceGroup actor" must {
|
||||
|
||||
//#device-group-test-registration
|
||||
"be able to register a device actor" in {
|
||||
val probe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device1", probe.ref)
|
||||
val registered1 = probe.expectMessageType[DeviceRegistered]
|
||||
val deviceActor1 = registered1.device
|
||||
|
||||
// another deviceId
|
||||
groupActor ! RequestTrackDevice("group", "device2", probe.ref)
|
||||
val registered2 = probe.expectMessageType[DeviceRegistered]
|
||||
val deviceActor2 = registered2.device
|
||||
deviceActor1 should !==(deviceActor2)
|
||||
|
||||
// Check that the device actors are working
|
||||
val recordProbe = createTestProbe[TemperatureRecorded]()
|
||||
deviceActor1 ! RecordTemperature(requestId = 0, 1.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(TemperatureRecorded(requestId = 0))
|
||||
deviceActor2 ! Device.RecordTemperature(requestId = 1, 2.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(Device.TemperatureRecorded(requestId = 1))
|
||||
}
|
||||
|
||||
"ignore requests for wrong groupId" in {
|
||||
val probe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("wrongGroup", "device1", probe.ref)
|
||||
probe.expectNoMessage(500.milliseconds)
|
||||
}
|
||||
//#device-group-test-registration
|
||||
|
||||
//#device-group-test3
|
||||
"return same actor for same deviceId" in {
|
||||
val probe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device1", probe.ref)
|
||||
val registered1 = probe.expectMessageType[DeviceRegistered]
|
||||
|
||||
// registering same again should be idempotent
|
||||
groupActor ! RequestTrackDevice("group", "device1", probe.ref)
|
||||
val registered2 = probe.expectMessageType[DeviceRegistered]
|
||||
|
||||
registered1.device should ===(registered2.device)
|
||||
}
|
||||
//#device-group-test3
|
||||
|
||||
//#device-group-list-terminate-test
|
||||
"be able to list active devices" in {
|
||||
val registeredProbe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device1", registeredProbe.ref)
|
||||
registeredProbe.expectMessageType[DeviceRegistered]
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device2", registeredProbe.ref)
|
||||
registeredProbe.expectMessageType[DeviceRegistered]
|
||||
|
||||
val deviceListProbe = createTestProbe[ReplyDeviceList]()
|
||||
groupActor ! RequestDeviceList(requestId = 0, groupId = "group", deviceListProbe.ref)
|
||||
deviceListProbe.expectMessage(ReplyDeviceList(requestId = 0, Set("device1", "device2")))
|
||||
}
|
||||
|
||||
"be able to list active devices after one shuts down" in {
|
||||
val registeredProbe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device1", registeredProbe.ref)
|
||||
val registered1 = registeredProbe.expectMessageType[DeviceRegistered]
|
||||
val toShutDown = registered1.device
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device2", registeredProbe.ref)
|
||||
registeredProbe.expectMessageType[DeviceRegistered]
|
||||
|
||||
val deviceListProbe = createTestProbe[ReplyDeviceList]()
|
||||
groupActor ! RequestDeviceList(requestId = 0, groupId = "group", deviceListProbe.ref)
|
||||
deviceListProbe.expectMessage(ReplyDeviceList(requestId = 0, Set("device1", "device2")))
|
||||
|
||||
toShutDown ! Passivate
|
||||
registeredProbe.expectTerminated(toShutDown, registeredProbe.remainingOrDefault)
|
||||
|
||||
// using awaitAssert to retry because it might take longer for the groupActor
|
||||
// to see the Terminated, that order is undefined
|
||||
registeredProbe.awaitAssert {
|
||||
groupActor ! RequestDeviceList(requestId = 1, groupId = "group", deviceListProbe.ref)
|
||||
deviceListProbe.expectMessage(ReplyDeviceList(requestId = 1, Set("device2")))
|
||||
}
|
||||
}
|
||||
//#device-group-list-terminate-test
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_4
|
||||
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.Signal
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
//#device-manager-full
|
||||
object DeviceManager {
|
||||
def apply(): Behavior[DeviceManagerMessage] =
|
||||
Behaviors.setup(context ⇒ new DeviceManager(context))
|
||||
|
||||
//#device-manager-msgs
|
||||
import DeviceGroup.DeviceGroupMessage
|
||||
|
||||
sealed trait DeviceManagerMessage
|
||||
|
||||
//#device-registration-msgs
|
||||
final case class RequestTrackDevice(groupId: String, deviceId: String, replyTo: ActorRef[DeviceRegistered])
|
||||
extends DeviceManagerMessage with DeviceGroupMessage
|
||||
|
||||
final case class DeviceRegistered(device: ActorRef[Device.DeviceMessage])
|
||||
//#device-registration-msgs
|
||||
|
||||
//#device-list-msgs
|
||||
final case class RequestDeviceList(requestId: Long, groupId: String, replyTo: ActorRef[ReplyDeviceList])
|
||||
extends DeviceManagerMessage with DeviceGroupMessage
|
||||
|
||||
final case class ReplyDeviceList(requestId: Long, ids: Set[String])
|
||||
//#device-list-msgs
|
||||
|
||||
private final case class DeviceGroupTerminated(groupId: String) extends DeviceManagerMessage
|
||||
//#device-manager-msgs
|
||||
}
|
||||
|
||||
class DeviceManager(context: ActorContext[DeviceManager.DeviceManagerMessage])
|
||||
extends AbstractBehavior[DeviceManager.DeviceManagerMessage] {
|
||||
import DeviceManager._
|
||||
import DeviceGroup.DeviceGroupMessage
|
||||
|
||||
var groupIdToActor = Map.empty[String, ActorRef[DeviceGroupMessage]]
|
||||
|
||||
context.log.info("DeviceManager started")
|
||||
|
||||
override def onMessage(msg: DeviceManagerMessage): Behavior[DeviceManagerMessage] =
|
||||
msg match {
|
||||
case trackMsg @ RequestTrackDevice(groupId, _, replyTo) ⇒
|
||||
groupIdToActor.get(groupId) match {
|
||||
case Some(ref) ⇒
|
||||
ref ! trackMsg
|
||||
case None ⇒
|
||||
context.log.info("Creating device group actor for {}", groupId)
|
||||
val groupActor = context.spawn(DeviceGroup(groupId), "group-" + groupId)
|
||||
context.watchWith(groupActor, DeviceGroupTerminated(groupId))
|
||||
groupActor ! trackMsg
|
||||
groupIdToActor += groupId -> groupActor
|
||||
}
|
||||
this
|
||||
|
||||
case req @ RequestDeviceList(requestId, groupId, replyTo) ⇒
|
||||
groupIdToActor.get(groupId) match {
|
||||
case Some(ref) ⇒
|
||||
ref ! req
|
||||
case None ⇒
|
||||
replyTo ! ReplyDeviceList(requestId, Set.empty)
|
||||
}
|
||||
this
|
||||
|
||||
case DeviceGroupTerminated(groupId) ⇒
|
||||
context.log.info("Device group actor for {} has been terminated", groupId)
|
||||
groupIdToActor -= groupId
|
||||
this
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[DeviceManagerMessage]] = {
|
||||
case PostStop ⇒
|
||||
context.log.info("DeviceManager stopped")
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
//#device-manager-full
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_4
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.WordSpecLike
|
||||
import typed.tutorial_4.DeviceManager._
|
||||
|
||||
class DeviceManagerSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||
|
||||
"DeviceManager actor" must {
|
||||
|
||||
"reply to registration requests" in {
|
||||
val probe = createTestProbe[DeviceRegistered]()
|
||||
val managerActor = spawn(DeviceManager())
|
||||
|
||||
managerActor ! RequestTrackDevice("group1", "device", probe.ref)
|
||||
val registered1 = probe.expectMessageType[DeviceRegistered]
|
||||
|
||||
// another group
|
||||
managerActor ! RequestTrackDevice("group2", "device", probe.ref)
|
||||
val registered2 = probe.expectMessageType[DeviceRegistered]
|
||||
|
||||
registered1.device should !==(registered2.device)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
53
akka-docs/src/test/scala/typed/tutorial_4/DeviceSpec.scala
Normal file
53
akka-docs/src/test/scala/typed/tutorial_4/DeviceSpec.scala
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_4
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.WordSpecLike
|
||||
|
||||
class DeviceSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||
import Device._
|
||||
|
||||
"Device actor" must {
|
||||
|
||||
//#device-read-test
|
||||
"reply with empty reading if no temperature is known" in {
|
||||
val probe = createTestProbe[RespondTemperature]()
|
||||
val deviceActor = spawn(Device("group", "device"))
|
||||
|
||||
deviceActor ! Device.ReadTemperature(requestId = 42, probe.ref)
|
||||
val response = probe.expectMessageType[Device.RespondTemperature]
|
||||
response.requestId should ===(42)
|
||||
response.value should ===(None)
|
||||
}
|
||||
//#device-read-test
|
||||
|
||||
//#device-write-read-test
|
||||
"reply with latest temperature reading" in {
|
||||
val recordProbe = createTestProbe[TemperatureRecorded]()
|
||||
val readProbe = createTestProbe[RespondTemperature]()
|
||||
val deviceActor = spawn(Device("group", "device"))
|
||||
|
||||
deviceActor ! Device.RecordTemperature(requestId = 1, 24.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(Device.TemperatureRecorded(requestId = 1))
|
||||
|
||||
deviceActor ! Device.ReadTemperature(requestId = 2, readProbe.ref)
|
||||
val response1 = readProbe.expectMessageType[RespondTemperature]
|
||||
response1.requestId should ===(2)
|
||||
response1.value should ===(Some(24.0))
|
||||
|
||||
deviceActor ! Device.RecordTemperature(requestId = 3, 55.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(Device.TemperatureRecorded(requestId = 3))
|
||||
|
||||
deviceActor ! Device.ReadTemperature(requestId = 4, readProbe.ref)
|
||||
val response2 = readProbe.expectMessageType[RespondTemperature]
|
||||
response2.requestId should ===(4)
|
||||
response2.value should ===(Some(55.0))
|
||||
}
|
||||
//#device-write-read-test
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
65
akka-docs/src/test/scala/typed/tutorial_5/Device.scala
Normal file
65
akka-docs/src/test/scala/typed/tutorial_5/Device.scala
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_5
|
||||
|
||||
//#device-with-passivate
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.Signal
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
object Device {
|
||||
def apply(groupId: String, deviceId: String): Behavior[DeviceMessage] =
|
||||
Behaviors.setup(context ⇒ new Device(context, groupId, deviceId))
|
||||
|
||||
sealed trait DeviceMessage
|
||||
|
||||
final case class ReadTemperature(requestId: Long, replyTo: ActorRef[RespondTemperature])
|
||||
extends DeviceMessage
|
||||
final case class RespondTemperature(requestId: Long, deviceId: String, value: Option[Double])
|
||||
|
||||
final case class RecordTemperature(requestId: Long, value: Double, replyTo: ActorRef[TemperatureRecorded])
|
||||
extends DeviceMessage
|
||||
final case class TemperatureRecorded(requestId: Long)
|
||||
|
||||
case object Passivate extends DeviceMessage
|
||||
}
|
||||
|
||||
class Device(context: ActorContext[Device.DeviceMessage], groupId: String, deviceId: String)
|
||||
extends AbstractBehavior[Device.DeviceMessage] {
|
||||
import Device._
|
||||
|
||||
var lastTemperatureReading: Option[Double] = None
|
||||
|
||||
context.log.info("Device actor {}-{} started", groupId, deviceId)
|
||||
|
||||
override def onMessage(msg: DeviceMessage): Behavior[DeviceMessage] = {
|
||||
msg match {
|
||||
case RecordTemperature(id, value, replyTo) ⇒
|
||||
context.log.info("Recorded temperature reading {} with {}", value, id)
|
||||
lastTemperatureReading = Some(value)
|
||||
replyTo ! TemperatureRecorded(id)
|
||||
this
|
||||
|
||||
case ReadTemperature(id, replyTo) ⇒
|
||||
replyTo ! RespondTemperature(id, deviceId, lastTemperatureReading)
|
||||
this
|
||||
|
||||
case Passivate ⇒
|
||||
Behaviors.stopped
|
||||
}
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[DeviceMessage]] = {
|
||||
case PostStop ⇒
|
||||
context.log.info("Device actor {}-{} stopped", groupId, deviceId)
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
//#device-with-passivate
|
||||
99
akka-docs/src/test/scala/typed/tutorial_5/DeviceGroup.scala
Normal file
99
akka-docs/src/test/scala/typed/tutorial_5/DeviceGroup.scala
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_5
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.Signal
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
object DeviceGroup {
|
||||
def apply(groupId: String): Behavior[DeviceGroupMessage] =
|
||||
Behaviors.setup(context ⇒ new DeviceGroup(context, groupId))
|
||||
|
||||
trait DeviceGroupMessage
|
||||
|
||||
private final case class DeviceTerminated(
|
||||
device: ActorRef[Device.DeviceMessage],
|
||||
groupId: String,
|
||||
deviceId: String) extends DeviceGroupMessage
|
||||
|
||||
}
|
||||
|
||||
//#query-added
|
||||
class DeviceGroup(context: ActorContext[DeviceGroup.DeviceGroupMessage], groupId: String)
|
||||
extends AbstractBehavior[DeviceGroup.DeviceGroupMessage] {
|
||||
import DeviceGroup._
|
||||
import DeviceManager._
|
||||
|
||||
private var deviceIdToActor = Map.empty[String, ActorRef[Device.DeviceMessage]]
|
||||
|
||||
context.log.info("DeviceGroup {} started", groupId)
|
||||
|
||||
override def onMessage(msg: DeviceGroupMessage): Behavior[DeviceGroupMessage] =
|
||||
msg match {
|
||||
//#query-added
|
||||
case trackMsg @ RequestTrackDevice(`groupId`, deviceId, replyTo) ⇒
|
||||
deviceIdToActor.get(deviceId) match {
|
||||
case Some(deviceActor) ⇒
|
||||
replyTo ! DeviceRegistered(deviceActor)
|
||||
case None ⇒
|
||||
context.log.info("Creating device actor for {}", trackMsg.deviceId)
|
||||
val deviceActor = context.spawn(Device(groupId, deviceId), s"device-$deviceId")
|
||||
//#device-group-register
|
||||
context.watchWith(deviceActor, DeviceTerminated(deviceActor, groupId, deviceId))
|
||||
//#device-group-register
|
||||
deviceIdToActor += deviceId -> deviceActor
|
||||
replyTo ! DeviceRegistered(deviceActor)
|
||||
}
|
||||
this
|
||||
|
||||
case RequestTrackDevice(gId, _, _) ⇒
|
||||
context.log.warning(
|
||||
"Ignoring TrackDevice request for {}. This actor is responsible for {}.",
|
||||
gId, groupId
|
||||
)
|
||||
this
|
||||
|
||||
case RequestDeviceList(requestId, gId, replyTo) ⇒
|
||||
if (gId == groupId) {
|
||||
replyTo ! ReplyDeviceList(requestId, deviceIdToActor.keySet)
|
||||
this
|
||||
} else
|
||||
Behaviors.unhandled
|
||||
//#device-group-remove
|
||||
|
||||
case DeviceTerminated(_, _, deviceId) ⇒
|
||||
context.log.info("Device actor for {} has been terminated", deviceId)
|
||||
deviceIdToActor -= deviceId
|
||||
this
|
||||
|
||||
//#query-added
|
||||
// ... other cases omitted
|
||||
|
||||
case RequestAllTemperatures(requestId, gId, replyTo) ⇒
|
||||
if (gId == groupId) {
|
||||
context.spawnAnonymous(DeviceGroupQuery(
|
||||
deviceIdToActor,
|
||||
requestId = requestId,
|
||||
requester = replyTo,
|
||||
3.seconds
|
||||
))
|
||||
this
|
||||
} else
|
||||
Behaviors.unhandled
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[DeviceGroupMessage]] = {
|
||||
case PostStop ⇒
|
||||
context.log.info("DeviceGroup {} stopped", groupId)
|
||||
this
|
||||
}
|
||||
}
|
||||
//#query-added
|
||||
129
akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuery.scala
Normal file
129
akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupQuery.scala
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_5
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import akka.actor.typed.scaladsl.TimerScheduler
|
||||
import typed.tutorial_5.Device.DeviceMessage
|
||||
import typed.tutorial_5.Device.ReadTemperature
|
||||
import typed.tutorial_5.Device.RespondTemperature
|
||||
import typed.tutorial_5.DeviceManager.DeviceNotAvailable
|
||||
import typed.tutorial_5.DeviceManager.DeviceTimedOut
|
||||
import typed.tutorial_5.DeviceManager.RespondAllTemperatures
|
||||
import typed.tutorial_5.DeviceManager.Temperature
|
||||
import typed.tutorial_5.DeviceManager.TemperatureNotAvailable
|
||||
import typed.tutorial_5.DeviceManager.TemperatureReading
|
||||
|
||||
//#query-full
|
||||
//#query-outline
|
||||
object DeviceGroupQuery {
|
||||
|
||||
def apply(
|
||||
deviceIdToActor: Map[String, ActorRef[Device.DeviceMessage]],
|
||||
requestId: Long,
|
||||
requester: ActorRef[RespondAllTemperatures],
|
||||
timeout: FiniteDuration
|
||||
): Behavior[DeviceGroupQueryMessage] = {
|
||||
Behaviors.setup { context ⇒
|
||||
Behaviors.withTimers { timers ⇒
|
||||
new DeviceGroupQuery(deviceIdToActor, requestId, requester, timeout, context, timers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait DeviceGroupQueryMessage
|
||||
|
||||
private case object CollectionTimeout extends DeviceGroupQueryMessage
|
||||
|
||||
final case class WrappedRespondTemperature(response: RespondTemperature) extends DeviceGroupQueryMessage
|
||||
|
||||
private final case class DeviceTerminated(deviceId: String) extends DeviceGroupQueryMessage
|
||||
}
|
||||
|
||||
class DeviceGroupQuery(
|
||||
deviceIdToActor: Map[String, ActorRef[DeviceMessage]],
|
||||
requestId: Long,
|
||||
requester: ActorRef[RespondAllTemperatures],
|
||||
timeout: FiniteDuration,
|
||||
context: ActorContext[DeviceGroupQuery.DeviceGroupQueryMessage],
|
||||
timers: TimerScheduler[DeviceGroupQuery.DeviceGroupQueryMessage])
|
||||
extends AbstractBehavior[DeviceGroupQuery.DeviceGroupQueryMessage] {
|
||||
|
||||
import DeviceGroupQuery._
|
||||
timers.startSingleTimer(CollectionTimeout, CollectionTimeout, timeout)
|
||||
|
||||
private val respondTemperatureAdapter = context.messageAdapter(WrappedRespondTemperature.apply)
|
||||
|
||||
//#query-outline
|
||||
//#query-state
|
||||
private var repliesSoFar = Map.empty[String, TemperatureReading]
|
||||
private var stillWaiting = deviceIdToActor.keySet
|
||||
|
||||
//#query-state
|
||||
//#query-outline
|
||||
|
||||
deviceIdToActor.foreach {
|
||||
case (deviceId, device) ⇒
|
||||
context.watchWith(device, DeviceTerminated(deviceId))
|
||||
device ! ReadTemperature(0, respondTemperatureAdapter)
|
||||
}
|
||||
|
||||
//#query-outline
|
||||
//#query-state
|
||||
override def onMessage(msg: DeviceGroupQueryMessage): Behavior[DeviceGroupQueryMessage] =
|
||||
msg match {
|
||||
case WrappedRespondTemperature(response) ⇒ onRespondTemperature(response)
|
||||
case DeviceTerminated(deviceId) ⇒ onDeviceTerminated(deviceId)
|
||||
case CollectionTimeout ⇒ onCollectionTimout()
|
||||
}
|
||||
|
||||
private def onRespondTemperature(response: RespondTemperature): Behavior[DeviceGroupQueryMessage] = {
|
||||
val reading = response.value match {
|
||||
case Some(value) ⇒ Temperature(value)
|
||||
case None ⇒ TemperatureNotAvailable
|
||||
}
|
||||
|
||||
val deviceId = response.deviceId
|
||||
repliesSoFar += (deviceId -> reading)
|
||||
stillWaiting -= deviceId
|
||||
|
||||
respondWhenAllCollected()
|
||||
}
|
||||
|
||||
private def onDeviceTerminated(deviceId: String): Behavior[DeviceGroupQueryMessage] = {
|
||||
if (stillWaiting(deviceId)) {
|
||||
repliesSoFar += (deviceId -> DeviceNotAvailable)
|
||||
stillWaiting -= deviceId
|
||||
}
|
||||
respondWhenAllCollected()
|
||||
}
|
||||
|
||||
private def onCollectionTimout(): Behavior[DeviceGroupQueryMessage] = {
|
||||
repliesSoFar ++= stillWaiting.map(deviceId ⇒ deviceId -> DeviceTimedOut)
|
||||
stillWaiting = Set.empty
|
||||
respondWhenAllCollected()
|
||||
}
|
||||
//#query-state
|
||||
|
||||
//#query-collect-reply
|
||||
private def respondWhenAllCollected(): Behavior[DeviceGroupQueryMessage] = {
|
||||
if (stillWaiting.isEmpty) {
|
||||
requester ! RespondAllTemperatures(requestId, repliesSoFar)
|
||||
Behaviors.stopped
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
//#query-collect-reply
|
||||
//#query-outline
|
||||
}
|
||||
//#query-outline
|
||||
//#query-full
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_5
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.WordSpecLike
|
||||
import typed.tutorial_5.Device.DeviceMessage
|
||||
import typed.tutorial_5.DeviceGroupQuery.WrappedRespondTemperature
|
||||
import typed.tutorial_5.DeviceManager.DeviceNotAvailable
|
||||
import typed.tutorial_5.DeviceManager.DeviceTimedOut
|
||||
import typed.tutorial_5.DeviceManager.RespondAllTemperatures
|
||||
import typed.tutorial_5.DeviceManager.Temperature
|
||||
import typed.tutorial_5.DeviceManager.TemperatureNotAvailable
|
||||
|
||||
class DeviceGroupQuerySpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||
|
||||
"DeviceGroupQuery" must {
|
||||
|
||||
//#query-test-normal
|
||||
"return temperature value for working devices" in {
|
||||
val requester = createTestProbe[RespondAllTemperatures]()
|
||||
|
||||
val device1 = createTestProbe[DeviceMessage]()
|
||||
val device2 = createTestProbe[DeviceMessage]()
|
||||
|
||||
val deviceIdToActor = Map("device1" -> device1.ref, "device2" -> device2.ref)
|
||||
|
||||
val queryActor = spawn(DeviceGroupQuery(
|
||||
deviceIdToActor,
|
||||
requestId = 1,
|
||||
requester = requester.ref,
|
||||
timeout = 3.seconds
|
||||
))
|
||||
|
||||
device1.expectMessageType[Device.ReadTemperature]
|
||||
device2.expectMessageType[Device.ReadTemperature]
|
||||
|
||||
queryActor ! WrappedRespondTemperature(Device.RespondTemperature(requestId = 0, "device1", Some(1.0)))
|
||||
queryActor ! WrappedRespondTemperature(Device.RespondTemperature(requestId = 0, "device2", Some(2.0)))
|
||||
|
||||
requester.expectMessage(RespondAllTemperatures(
|
||||
requestId = 1,
|
||||
temperatures = Map(
|
||||
"device1" -> Temperature(1.0),
|
||||
"device2" -> Temperature(2.0)
|
||||
)
|
||||
))
|
||||
}
|
||||
//#query-test-normal
|
||||
|
||||
//#query-test-no-reading
|
||||
"return TemperatureNotAvailable for devices with no readings" in {
|
||||
val requester = createTestProbe[RespondAllTemperatures]()
|
||||
|
||||
val device1 = createTestProbe[DeviceMessage]()
|
||||
val device2 = createTestProbe[DeviceMessage]()
|
||||
|
||||
val deviceIdToActor = Map("device1" -> device1.ref, "device2" -> device2.ref)
|
||||
|
||||
val queryActor = spawn(DeviceGroupQuery(
|
||||
deviceIdToActor,
|
||||
requestId = 1,
|
||||
requester = requester.ref,
|
||||
timeout = 3.seconds
|
||||
))
|
||||
|
||||
device1.expectMessageType[Device.ReadTemperature]
|
||||
device2.expectMessageType[Device.ReadTemperature]
|
||||
|
||||
queryActor ! WrappedRespondTemperature(Device.RespondTemperature(requestId = 0, "device1", None))
|
||||
queryActor ! WrappedRespondTemperature(Device.RespondTemperature(requestId = 0, "device2", Some(2.0)))
|
||||
|
||||
requester.expectMessage(RespondAllTemperatures(
|
||||
requestId = 1,
|
||||
temperatures = Map(
|
||||
"device1" -> TemperatureNotAvailable,
|
||||
"device2" -> Temperature(2.0)
|
||||
)
|
||||
))
|
||||
}
|
||||
//#query-test-no-reading
|
||||
|
||||
//#query-test-stopped
|
||||
"return DeviceNotAvailable if device stops before answering" in {
|
||||
val requester = createTestProbe[RespondAllTemperatures]()
|
||||
|
||||
val device1 = createTestProbe[DeviceMessage]()
|
||||
val device2 = createTestProbe[DeviceMessage]()
|
||||
|
||||
val deviceIdToActor = Map("device1" -> device1.ref, "device2" -> device2.ref)
|
||||
|
||||
val queryActor = spawn(DeviceGroupQuery(
|
||||
deviceIdToActor,
|
||||
requestId = 1,
|
||||
requester = requester.ref,
|
||||
timeout = 3.seconds
|
||||
))
|
||||
|
||||
device1.expectMessageType[Device.ReadTemperature]
|
||||
device2.expectMessageType[Device.ReadTemperature]
|
||||
|
||||
queryActor ! WrappedRespondTemperature(Device.RespondTemperature(requestId = 0, "device1", Some(2.0)))
|
||||
|
||||
device2.stop()
|
||||
|
||||
requester.expectMessage(RespondAllTemperatures(
|
||||
requestId = 1,
|
||||
temperatures = Map(
|
||||
"device1" -> Temperature(2.0),
|
||||
"device2" -> DeviceNotAvailable
|
||||
)
|
||||
))
|
||||
}
|
||||
//#query-test-stopped
|
||||
|
||||
//#query-test-stopped-later
|
||||
"return temperature reading even if device stops after answering" in {
|
||||
val requester = createTestProbe[RespondAllTemperatures]()
|
||||
|
||||
val device1 = createTestProbe[DeviceMessage]()
|
||||
val device2 = createTestProbe[DeviceMessage]()
|
||||
|
||||
val deviceIdToActor = Map("device1" -> device1.ref, "device2" -> device2.ref)
|
||||
|
||||
val queryActor = spawn(DeviceGroupQuery(
|
||||
deviceIdToActor,
|
||||
requestId = 1,
|
||||
requester = requester.ref,
|
||||
timeout = 3.seconds
|
||||
))
|
||||
|
||||
device1.expectMessageType[Device.ReadTemperature]
|
||||
device2.expectMessageType[Device.ReadTemperature]
|
||||
|
||||
queryActor ! WrappedRespondTemperature(Device.RespondTemperature(requestId = 0, "device1", Some(1.0)))
|
||||
queryActor ! WrappedRespondTemperature(Device.RespondTemperature(requestId = 0, "device2", Some(2.0)))
|
||||
|
||||
device2.stop()
|
||||
|
||||
requester.expectMessage(RespondAllTemperatures(
|
||||
requestId = 1,
|
||||
temperatures = Map(
|
||||
"device1" -> Temperature(1.0),
|
||||
"device2" -> Temperature(2.0)
|
||||
)
|
||||
))
|
||||
}
|
||||
//#query-test-stopped-later
|
||||
|
||||
//#query-test-timeout
|
||||
"return DeviceTimedOut if device does not answer in time" in {
|
||||
val requester = createTestProbe[RespondAllTemperatures]()
|
||||
|
||||
val device1 = createTestProbe[DeviceMessage]()
|
||||
val device2 = createTestProbe[DeviceMessage]()
|
||||
|
||||
val deviceIdToActor = Map("device1" -> device1.ref, "device2" -> device2.ref)
|
||||
|
||||
val queryActor = spawn(DeviceGroupQuery(
|
||||
deviceIdToActor,
|
||||
requestId = 1,
|
||||
requester = requester.ref,
|
||||
timeout = 200.millis
|
||||
))
|
||||
|
||||
device1.expectMessageType[Device.ReadTemperature]
|
||||
device2.expectMessageType[Device.ReadTemperature]
|
||||
|
||||
queryActor ! WrappedRespondTemperature(Device.RespondTemperature(requestId = 0, "device1", Some(1.0)))
|
||||
|
||||
// no reply from device2
|
||||
|
||||
requester.expectMessage(RespondAllTemperatures(
|
||||
requestId = 1,
|
||||
temperatures = Map(
|
||||
"device1" -> Temperature(1.0),
|
||||
"device2" -> DeviceTimedOut
|
||||
)
|
||||
))
|
||||
}
|
||||
//#query-test-timeout
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
145
akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupSpec.scala
Normal file
145
akka-docs/src/test/scala/typed/tutorial_5/DeviceGroupSpec.scala
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_5
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.WordSpecLike
|
||||
import typed.tutorial_5.Device._
|
||||
import typed.tutorial_5.DeviceManager._
|
||||
|
||||
class DeviceGroupSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||
|
||||
"DeviceGroup actor" must {
|
||||
|
||||
//#device-group-test-registration
|
||||
"be able to register a device actor" in {
|
||||
val probe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device1", probe.ref)
|
||||
val registered1 = probe.expectMessageType[DeviceRegistered]
|
||||
val deviceActor1 = registered1.device
|
||||
|
||||
// another deviceId
|
||||
groupActor ! RequestTrackDevice("group", "device2", probe.ref)
|
||||
val registered2 = probe.expectMessageType[DeviceRegistered]
|
||||
val deviceActor2 = registered2.device
|
||||
deviceActor1 should !==(deviceActor2)
|
||||
|
||||
// Check that the device actors are working
|
||||
val recordProbe = createTestProbe[TemperatureRecorded]()
|
||||
deviceActor1 ! RecordTemperature(requestId = 0, 1.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(TemperatureRecorded(requestId = 0))
|
||||
deviceActor2 ! Device.RecordTemperature(requestId = 1, 2.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(Device.TemperatureRecorded(requestId = 1))
|
||||
}
|
||||
|
||||
"ignore requests for wrong groupId" in {
|
||||
val probe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("wrongGroup", "device1", probe.ref)
|
||||
probe.expectNoMessage(500.milliseconds)
|
||||
}
|
||||
//#device-group-test-registration
|
||||
|
||||
//#device-group-test3
|
||||
"return same actor for same deviceId" in {
|
||||
val probe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device1", probe.ref)
|
||||
val registered1 = probe.expectMessageType[DeviceRegistered]
|
||||
|
||||
// registering same again should be idempotent
|
||||
groupActor ! RequestTrackDevice("group", "device1", probe.ref)
|
||||
val registered2 = probe.expectMessageType[DeviceRegistered]
|
||||
|
||||
registered1.device should ===(registered2.device)
|
||||
}
|
||||
//#device-group-test3
|
||||
|
||||
//#device-group-list-terminate-test
|
||||
"be able to list active devices" in {
|
||||
val registeredProbe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device1", registeredProbe.ref)
|
||||
registeredProbe.expectMessageType[DeviceRegistered]
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device2", registeredProbe.ref)
|
||||
registeredProbe.expectMessageType[DeviceRegistered]
|
||||
|
||||
val deviceListProbe = createTestProbe[ReplyDeviceList]()
|
||||
groupActor ! RequestDeviceList(requestId = 0, groupId = "group", deviceListProbe.ref)
|
||||
deviceListProbe.expectMessage(ReplyDeviceList(requestId = 0, Set("device1", "device2")))
|
||||
}
|
||||
|
||||
"be able to list active devices after one shuts down" in {
|
||||
val registeredProbe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device1", registeredProbe.ref)
|
||||
val registered1 = registeredProbe.expectMessageType[DeviceRegistered]
|
||||
val toShutDown = registered1.device
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device2", registeredProbe.ref)
|
||||
registeredProbe.expectMessageType[DeviceRegistered]
|
||||
|
||||
val deviceListProbe = createTestProbe[ReplyDeviceList]()
|
||||
groupActor ! RequestDeviceList(requestId = 0, groupId = "group", deviceListProbe.ref)
|
||||
deviceListProbe.expectMessage(ReplyDeviceList(requestId = 0, Set("device1", "device2")))
|
||||
|
||||
toShutDown ! Passivate
|
||||
registeredProbe.expectTerminated(toShutDown, registeredProbe.remainingOrDefault)
|
||||
|
||||
// using awaitAssert to retry because it might take longer for the groupActor
|
||||
// to see the Terminated, that order is undefined
|
||||
registeredProbe.awaitAssert {
|
||||
groupActor ! RequestDeviceList(requestId = 1, groupId = "group", deviceListProbe.ref)
|
||||
deviceListProbe.expectMessage(ReplyDeviceList(requestId = 1, Set("device2")))
|
||||
}
|
||||
}
|
||||
//#device-group-list-terminate-test
|
||||
|
||||
//#group-query-integration-test
|
||||
"be able to collect temperatures from all active devices" in {
|
||||
val registeredProbe = createTestProbe[DeviceRegistered]()
|
||||
val groupActor = spawn(DeviceGroup("group"))
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device1", registeredProbe.ref)
|
||||
val deviceActor1 = registeredProbe.expectMessageType[DeviceRegistered].device
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device2", registeredProbe.ref)
|
||||
val deviceActor2 = registeredProbe.expectMessageType[DeviceRegistered].device
|
||||
|
||||
groupActor ! RequestTrackDevice("group", "device3", registeredProbe.ref)
|
||||
registeredProbe.expectMessageType[DeviceRegistered]
|
||||
|
||||
// Check that the device actors are working
|
||||
val recordProbe = createTestProbe[TemperatureRecorded]()
|
||||
deviceActor1 ! RecordTemperature(requestId = 0, 1.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(TemperatureRecorded(requestId = 0))
|
||||
deviceActor2 ! RecordTemperature(requestId = 1, 2.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(TemperatureRecorded(requestId = 1))
|
||||
// No temperature for device3
|
||||
|
||||
val allTempProbe = createTestProbe[RespondAllTemperatures]()
|
||||
groupActor ! RequestAllTemperatures(requestId = 0, groupId = "group", allTempProbe.ref)
|
||||
allTempProbe.expectMessage(
|
||||
RespondAllTemperatures(
|
||||
requestId = 0,
|
||||
temperatures = Map(
|
||||
"device1" -> Temperature(1.0),
|
||||
"device2" -> Temperature(2.0),
|
||||
"device3" -> TemperatureNotAvailable)))
|
||||
}
|
||||
//#group-query-integration-test
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
109
akka-docs/src/test/scala/typed/tutorial_5/DeviceManager.scala
Normal file
109
akka-docs/src/test/scala/typed/tutorial_5/DeviceManager.scala
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_5
|
||||
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.PostStop
|
||||
import akka.actor.typed.Signal
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
|
||||
//#device-manager-full
|
||||
object DeviceManager {
|
||||
def apply(): Behavior[DeviceManagerMessage] =
|
||||
Behaviors.setup(context ⇒ new DeviceManager(context))
|
||||
|
||||
//#device-manager-msgs
|
||||
import DeviceGroup.DeviceGroupMessage
|
||||
|
||||
sealed trait DeviceManagerMessage
|
||||
|
||||
final case class RequestTrackDevice(groupId: String, deviceId: String, replyTo: ActorRef[DeviceRegistered])
|
||||
extends DeviceManagerMessage with DeviceGroupMessage
|
||||
|
||||
final case class DeviceRegistered(device: ActorRef[Device.DeviceMessage])
|
||||
|
||||
final case class RequestDeviceList(requestId: Long, groupId: String, replyTo: ActorRef[ReplyDeviceList])
|
||||
extends DeviceManagerMessage with DeviceGroupMessage
|
||||
|
||||
final case class ReplyDeviceList(requestId: Long, ids: Set[String])
|
||||
|
||||
private final case class DeviceGroupTerminated(groupId: String) extends DeviceManagerMessage
|
||||
//#device-manager-msgs
|
||||
|
||||
//#query-protocol
|
||||
import DeviceGroupQuery.DeviceGroupQueryMessage
|
||||
|
||||
final case class RequestAllTemperatures(requestId: Long, groupId: String, replyTo: ActorRef[RespondAllTemperatures])
|
||||
extends DeviceGroupQueryMessage with DeviceGroupMessage with DeviceManagerMessage
|
||||
|
||||
final case class RespondAllTemperatures(requestId: Long, temperatures: Map[String, TemperatureReading])
|
||||
|
||||
sealed trait TemperatureReading
|
||||
final case class Temperature(value: Double) extends TemperatureReading
|
||||
case object TemperatureNotAvailable extends TemperatureReading
|
||||
case object DeviceNotAvailable extends TemperatureReading
|
||||
case object DeviceTimedOut extends TemperatureReading
|
||||
//#query-protocol
|
||||
}
|
||||
|
||||
class DeviceManager(context: ActorContext[DeviceManager.DeviceManagerMessage])
|
||||
extends AbstractBehavior[DeviceManager.DeviceManagerMessage] {
|
||||
import DeviceManager._
|
||||
import DeviceGroup.DeviceGroupMessage
|
||||
|
||||
var groupIdToActor = Map.empty[String, ActorRef[DeviceGroupMessage]]
|
||||
|
||||
context.log.info("DeviceManager started")
|
||||
|
||||
override def onMessage(msg: DeviceManagerMessage): Behavior[DeviceManagerMessage] =
|
||||
msg match {
|
||||
case trackMsg @ RequestTrackDevice(groupId, _, replyTo) ⇒
|
||||
groupIdToActor.get(groupId) match {
|
||||
case Some(ref) ⇒
|
||||
ref ! trackMsg
|
||||
case None ⇒
|
||||
context.log.info("Creating device group actor for {}", groupId)
|
||||
val groupActor = context.spawn(DeviceGroup(groupId), "group-" + groupId)
|
||||
context.watchWith(groupActor, DeviceGroupTerminated(groupId))
|
||||
groupActor ! trackMsg
|
||||
groupIdToActor += groupId -> groupActor
|
||||
}
|
||||
this
|
||||
|
||||
case req @ RequestDeviceList(requestId, groupId, replyTo) ⇒
|
||||
groupIdToActor.get(groupId) match {
|
||||
case Some(ref) ⇒
|
||||
ref ! req
|
||||
case None ⇒
|
||||
replyTo ! ReplyDeviceList(requestId, Set.empty)
|
||||
}
|
||||
this
|
||||
|
||||
case req @ RequestAllTemperatures(requestId, groupId, replyTo) ⇒
|
||||
groupIdToActor.get(groupId) match {
|
||||
case Some(ref) ⇒
|
||||
ref ! req
|
||||
case None ⇒
|
||||
replyTo ! RespondAllTemperatures(requestId, Map.empty)
|
||||
}
|
||||
this
|
||||
|
||||
case DeviceGroupTerminated(groupId) ⇒
|
||||
context.log.info("Device group actor for {} has been terminated", groupId)
|
||||
groupIdToActor -= groupId
|
||||
this
|
||||
}
|
||||
|
||||
override def onSignal: PartialFunction[Signal, Behavior[DeviceManagerMessage]] = {
|
||||
case PostStop ⇒
|
||||
context.log.info("DeviceManager stopped")
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
//#device-manager-full
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_5
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.WordSpecLike
|
||||
import typed.tutorial_5.DeviceManager._
|
||||
|
||||
class DeviceManagerSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||
|
||||
"DeviceManager actor" must {
|
||||
|
||||
"reply to registration requests" in {
|
||||
val probe = createTestProbe[DeviceRegistered]()
|
||||
val managerActor = spawn(DeviceManager())
|
||||
|
||||
managerActor ! RequestTrackDevice("group1", "device", probe.ref)
|
||||
val registered1 = probe.expectMessageType[DeviceRegistered]
|
||||
|
||||
// another group
|
||||
managerActor ! RequestTrackDevice("group2", "device", probe.ref)
|
||||
val registered2 = probe.expectMessageType[DeviceRegistered]
|
||||
|
||||
registered1.device should !==(registered2.device)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
53
akka-docs/src/test/scala/typed/tutorial_5/DeviceSpec.scala
Normal file
53
akka-docs/src/test/scala/typed/tutorial_5/DeviceSpec.scala
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package typed.tutorial_5
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.WordSpecLike
|
||||
|
||||
class DeviceSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||
import Device._
|
||||
|
||||
"Device actor" must {
|
||||
|
||||
//#device-read-test
|
||||
"reply with empty reading if no temperature is known" in {
|
||||
val probe = createTestProbe[RespondTemperature]()
|
||||
val deviceActor = spawn(Device("group", "device"))
|
||||
|
||||
deviceActor ! Device.ReadTemperature(requestId = 42, probe.ref)
|
||||
val response = probe.expectMessageType[Device.RespondTemperature]
|
||||
response.requestId should ===(42)
|
||||
response.value should ===(None)
|
||||
}
|
||||
//#device-read-test
|
||||
|
||||
//#device-write-read-test
|
||||
"reply with latest temperature reading" in {
|
||||
val recordProbe = createTestProbe[TemperatureRecorded]()
|
||||
val readProbe = createTestProbe[RespondTemperature]()
|
||||
val deviceActor = spawn(Device("group", "device"))
|
||||
|
||||
deviceActor ! Device.RecordTemperature(requestId = 1, 24.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(Device.TemperatureRecorded(requestId = 1))
|
||||
|
||||
deviceActor ! Device.ReadTemperature(requestId = 2, readProbe.ref)
|
||||
val response1 = readProbe.expectMessageType[RespondTemperature]
|
||||
response1.requestId should ===(2)
|
||||
response1.value should ===(Some(24.0))
|
||||
|
||||
deviceActor ! Device.RecordTemperature(requestId = 3, 55.0, recordProbe.ref)
|
||||
recordProbe.expectMessage(Device.TemperatureRecorded(requestId = 3))
|
||||
|
||||
deviceActor ! Device.ReadTemperature(requestId = 4, readProbe.ref)
|
||||
val response2 = readProbe.expectMessageType[RespondTemperature]
|
||||
response2.requestId should ===(4)
|
||||
response2.value should ===(Some(55.0))
|
||||
}
|
||||
//#device-write-read-test
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ public class BlogPostExample {
|
|||
//#state
|
||||
interface BlogState {}
|
||||
|
||||
public static enum BlankState implements BlogState {
|
||||
public enum BlankState implements BlogState {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ lazy val distributedData = akkaModule("akka-distributed-data")
|
|||
|
||||
lazy val docs = akkaModule("akka-docs")
|
||||
.dependsOn(
|
||||
actor, cluster, clusterMetrics, slf4j, agent, osgi, persistenceTck, persistenceQuery, distributedData, stream,
|
||||
actor, cluster, clusterMetrics, slf4j, agent, osgi, persistenceTck, persistenceQuery, distributedData, stream, actorTyped,
|
||||
camel % "compile->compile;test->test",
|
||||
clusterTools % "compile->compile;test->test",
|
||||
clusterSharding % "compile->compile;test->test",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue