+sam #3925 Adding Supervision for Java with Lambda Support Activator template

This commit is contained in:
Björn Antonsson 2014-03-17 14:56:22 +01:00
parent 4b57392143
commit ea6e459adf
13 changed files with 952 additions and 12 deletions

View file

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View file

@ -0,0 +1,10 @@
Activator Template by Typesafe
Licensed under Public Domain (CC0)
To the extent possible under law, the person who associated CC0 with
this Activator Tempate has waived all copyright and related or neighboring
rights to this Activator Template.
You should have received a copy of the CC0 legalcode along with this
work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.

View file

@ -0,0 +1,4 @@
name=akka-supervision-java-lambda
title=Akka Supervision in Java with Lambdas
description=Illustrates supervision in Akka
tags=akka,java,java8,sample

View file

@ -0,0 +1,15 @@
name := "akka-supervision-java-lambda"
version := "1.0"
scalaVersion := "2.10.3"
javacOptions ++= Seq("-source", "1.8", "-target", "1.8", "-Xlint")
testOptions += Tests.Argument(TestFrameworks.JUnit, "-v", "-a")
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % "2.3-SNAPSHOT",
"com.typesafe.akka" %% "akka-testkit" % "2.3-SNAPSHOT" % "test",
"junit" % "junit" % "4.11" % "test",
"com.novocode" % "junit-interface" % "0.10" % "test")

View file

@ -0,0 +1,53 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<groupId>sample</groupId>
<artifactId>akka-supervision-java-lambda</artifactId>
<packaging>jar</packaging>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_2.10</artifactId>
<version>2.3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-testkit_2.10</artifactId>
<version>2.3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<fork>true</fork>
<compilerArgs>
<arg>-Xlint</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1 @@
sbt.version=0.13.1

View file

@ -0,0 +1,75 @@
package supervision;
import akka.actor.*;
import akka.japi.pf.DeciderBuilder;
import akka.japi.pf.ReceiveBuilder;
import scala.PartialFunction;
import scala.runtime.BoxedUnit;
import java.util.HashMap;
import java.util.Map;
import static supervision.FlakyExpressionCalculator.FlakinessException;
import static supervision.FlakyExpressionCalculator.Result;
import static supervision.FlakyExpressionCalculator.Position.Left;
// A very simple service that accepts arithmetic expressions and tries to
// evaluate them. Since the calculation is dangerous (at least for the sake
// of this example) it is delegated to a worker actor of type
// FlakyExpressionCalculator.
class ArithmeticService extends AbstractLoggingActor {
// Map of workers to the original actors requesting the calculation
Map<ActorRef, ActorRef> pendingWorkers = new HashMap<>();
private SupervisorStrategy strategy = new OneForOneStrategy(false, DeciderBuilder.
match(FlakinessException.class, e -> {
log().warning("Evaluation of a top level expression failed, restarting.");
return SupervisorStrategy.restart();
}).
match(ArithmeticException.class, e -> {
log().error("Evaluation failed because of: {}", e.getMessage());
notifyConsumerFailure(sender(), e);
return SupervisorStrategy.stop();
}).
match(Throwable.class, e -> {
log().error("Unexpected failure: {}", e.getMessage());
notifyConsumerFailure(sender(), e);
return SupervisorStrategy.stop();
}).build());
@Override
public SupervisorStrategy supervisorStrategy() {
return strategy;
}
private void notifyConsumerFailure(ActorRef worker, Throwable failure) {
// Status.Failure is a message type provided by the Akka library. The
// reason why it is used is because it is recognized by the "ask" pattern
// and the Future returned by ask will fail with the provided exception.
ActorRef pending = pendingWorkers.get(worker);
if (pending != null) {
pending.tell(new Status.Failure(failure), self());
pendingWorkers.remove(worker);
}
}
private void notifyConsumerSuccess(ActorRef worker, Integer result) {
ActorRef pending = pendingWorkers.get(worker);
if (pending != null) {
pending.tell(result, self());
pendingWorkers.remove(worker);
}
}
@Override
public PartialFunction<Object, BoxedUnit> receive() {
return ReceiveBuilder.
match(Expression.class, expr -> {
// We delegate the dangerous task of calculation to a worker, passing the
// expression as a constructor argument to the actor.
ActorRef worker = context().actorOf(FlakyExpressionCalculator.props(expr, Left));
pendingWorkers.put(worker, sender());
}).
match(Result.class, r -> notifyConsumerSuccess(sender(), r.getValue())).build();
}
}

View file

@ -0,0 +1,122 @@
package supervision;
// Represents an arithmetic expression involving integer numbers
public interface Expression {
public Expression getLeft();
public Expression getRight();
// Basic arithmetic operations that are supported by the ArithmeticService. Every
// operation except the constant value has a left and right side. For example
// the addition in (3 * 2) + (6 * 6) has the left side (3 * 2) and the right
// side (6 * 6).
public static abstract class AbstractExpression implements Expression {
private final Expression left;
private final Expression right;
private final String operator;
protected AbstractExpression(Expression left, Expression right, String operator) {
this.left = left;
this.right = right;
this.operator = operator;
}
public Expression getLeft() {
return left;
}
public Expression getRight() {
return right;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AbstractExpression)) return false;
AbstractExpression that = (AbstractExpression) o;
if (!left.equals(that.left)) return false;
if (!operator.equals(that.operator)) return false;
if (!right.equals(that.right)) return false;
return true;
}
@Override
public int hashCode() {
int result = left.hashCode();
result = 31 * result + right.hashCode();
result = 31 * result + operator.hashCode();
return result;
}
@Override
public String toString() {
return "(" + getLeft() + " " + operator + " " + getRight() + ")";
}
}
public static final class Add extends AbstractExpression {
public Add(Expression left, Expression right) {
super(left, right, "+");
}
}
public static final class Multiply extends AbstractExpression {
public Multiply(Expression left, Expression right) {
super(left, right, "*");
}
}
public static final class Divide extends AbstractExpression {
public Divide(Expression left, Expression right) {
super(left, right, "/");
}
}
public static final class Const implements Expression{
private final int value;
public Const(int value) {
this.value = value;
}
@Override
public Expression getLeft() {
return this;
}
@Override
public Expression getRight() {
return this;
}
public int getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Const)) return false;
Const aConst = (Const) o;
if (value != aConst.value) return false;
return true;
}
@Override
public int hashCode() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
}

View file

@ -0,0 +1,148 @@
package supervision;
import akka.actor.*;
import akka.japi.pf.DeciderBuilder;
import akka.japi.pf.ReceiveBuilder;
import scala.PartialFunction;
import scala.runtime.BoxedUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static supervision.Expression.*;
import static supervision.FlakyExpressionCalculator.Position.*;
public class FlakyExpressionCalculator extends AbstractLoggingActor {
public static Props props(Expression expr, Position position) {
return Props.create(FlakyExpressionCalculator.class, expr, position);
}
// Encodes the original position of a sub-expression in its parent expression
// Example: (4 / 2) has position Left in the original expression (4 / 2) * 3
public static enum Position {
Left, Right
}
public static class Result {
private final Expression originalExpression;
private final Integer value;
private final Position position;
public Result(Expression originalExpression, Integer value, Position position) {
this.originalExpression = originalExpression;
this.value = value;
this.position = position;
}
public Expression getOriginalExpression() {
return originalExpression;
}
public Integer getValue() {
return value;
}
public Position getPosition() {
return position;
}
}
public static class FlakinessException extends RuntimeException {
static final long serialVersionUID = 1;
public FlakinessException() {
super("Flakiness");
}
}
// This actor has the sole purpose of calculating a given expression and
// return the result to its parent. It takes an additional argument,
// myPosition, which is used to signal the parent which side of its
// expression has been calculated.
private final Expression expr;
private final Position myPosition;
public FlakyExpressionCalculator(Expression expr, Position myPosition) {
this.expr = expr;
this.myPosition = myPosition;
}
private Expression getExpr() {
return expr;
}
private SupervisorStrategy strategy = new OneForOneStrategy(false, DeciderBuilder.
match(FlakinessException.class, e -> {
log().warning("Evaluation of {} failed, restarting.", getExpr());
return SupervisorStrategy.restart();
}).
matchAny(e -> SupervisorStrategy.escalate()).build());
@Override
public SupervisorStrategy supervisorStrategy() {
return strategy;
}
// The value of these variables will be reinitialized after every restart.
// The only stable data the actor has during restarts is those embedded in
// the Props when it was created. In this case expr, and myPosition.
Map<Position, Integer> results = new HashMap<>();
Set<Position> expected = Stream.of(Left, Right).collect(Collectors.toSet());
@Override
public void preStart() {
if (expr instanceof Const) {
Integer value = ((Const) expr).getValue();
context().parent().tell(new Result(expr, value, myPosition), self());
// Don't forget to stop the actor after it has nothing more to do
context().stop(self());
}
else {
context().actorOf(FlakyExpressionCalculator.props(expr.getLeft(), Left), "left");
context().actorOf(FlakyExpressionCalculator.props(expr.getRight(), Right), "right");
}
}
@Override
public PartialFunction<Object, BoxedUnit> receive() {
return ReceiveBuilder.
match(Result.class, r -> expected.contains(r.getPosition()), r -> {
expected.remove(r.getPosition());
results.put(r.getPosition(), r.getValue());
if (results.size() == 2) {
// Sometimes we fail to calculate
flakiness();
Integer result = evaluate(expr, results.get(Left),results.get(Right));
log().info("Evaluated expression {} to value {}", expr, result);
context().parent().tell(new Result(expr, result, myPosition), self());
// Don't forget to stop the actor after it has nothing more to do
context().stop(self());
}
}).match(Result.class, r -> {
throw new IllegalStateException("Expected results for positions " +
expected.stream().map(Object::toString).collect(Collectors.joining(", ")) +
" but got position " + r.getPosition());
}).build();
}
private Integer evaluate(Expression expr, Integer left, Integer right) {
if (expr instanceof Add) {
return left + right;
} else if( expr instanceof Multiply) {
return left * right;
} else if (expr instanceof Divide) {
return left / right;
} else {
throw new IllegalStateException("Unknown expression type " + expr.getClass());
}
}
private void flakiness() throws FlakinessException {
if (ThreadLocalRandom.current().nextDouble() < 0.2)
throw new FlakinessException();
}
}

View file

@ -0,0 +1,39 @@
package supervision;
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.actor.ActorSystem;
import akka.util.Timeout;
import scala.concurrent.Await;
import scala.concurrent.duration.Duration;
import scala.concurrent.duration.FiniteDuration;
import java.util.concurrent.TimeUnit;
import static supervision.Expression.*;
import static akka.pattern.Patterns.ask;
import static akka.japi.Util.classTag;
public class Main {
public static void main(String[] args) throws Exception {
ActorSystem system = ActorSystem.create("calculator-system");
ActorRef calculatorService =
system.actorOf(Props.create(ArithmeticService.class), "arithmetic-service");
// (3 + 5) / (2 * (1 + 1))
Expression task = new Divide(
new Add(new Const(3), new Const(5)),
new Multiply(
new Const(2),
new Add(new Const(1), new Const(1))
)
);
FiniteDuration duration = Duration.create(1, TimeUnit.SECONDS);
Integer result = Await.result(ask(calculatorService, task, new Timeout(duration)).mapTo(classTag(Integer.class)), duration);
System.out.println("Got result: " + result);
system.shutdown();
system.awaitTermination();
}
}

View file

@ -0,0 +1,97 @@
package supervision;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.actor.Status;
import akka.testkit.JavaTestKit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.stream.IntStream;
import static supervision.Expression.*;
public class ArithmeticServiceTest {
static ActorSystem system;
@BeforeClass
public static void setup() {
system = ActorSystem.create("BuncherTest");
}
@AfterClass
public static void tearDown() {
JavaTestKit.shutdownActorSystem(system);
system = null;
}
@Test
public void TheArithmeticServiceShouldCalculateConstantExpressionsProperly(){
new JavaTestKit(system) {{
final ActorRef service =
system.actorOf(Props.create(ArithmeticService.class));
final ActorRef probe = getRef();
IntStream.range(-2, 3).forEach(x -> {
service.tell(new Const(x), probe);
expectMsgEquals(x);
});
}};
}
@Test
public void TheArithmeticServiceShouldCalculateAdditionProperly(){
new JavaTestKit(system) {{
final ActorRef service =
system.actorOf(Props.create(ArithmeticService.class));
final ActorRef probe = getRef();
IntStream.range(-2, 3).forEach(x ->
IntStream.range(-2, 3).forEach(y -> {
service.tell(new Add(new Const(x), new Const(y)), probe);
expectMsgEquals(x + y);
})
);
}};
}
@Test
public void TheArithmeticServiceShouldCalculateMultiplicationAndDivisionProperly(){
new JavaTestKit(system) {{
final ActorRef service =
system.actorOf(Props.create(ArithmeticService.class));
final ActorRef probe = getRef();
IntStream.range(-2, 3).forEach(x ->
IntStream.range(-2, 3).forEach(y -> {
service.tell(new Multiply(new Const(x), new Const(y)), probe);
expectMsgEquals(x * y);
})
);
// Skip zero in the second parameter
IntStream.range(-2, 3).forEach(x ->
IntStream.of(-2, -1, 1, 2).forEach(y -> {
service.tell(new Divide(new Const(x), new Const(y)), probe);
expectMsgEquals(x / y);
})
);
}};
}
@Test
public void TheArithmeticServiceShouldSurviveIllegalExpressions(){
new JavaTestKit(system) {{
final ActorRef service =
system.actorOf(Props.create(ArithmeticService.class));
final ActorRef probe = getRef();
service.tell(new Divide(new Const(1), new Const(0)), probe);
expectMsgClass(Status.Failure.class);
service.tell(new Add(null, new Const(0)), probe);
expectMsgClass(Status.Failure.class);
service.tell(new Add(new Const(1), new Const(0)), probe);
expectMsgEquals(1);
}};
}
}

View file

@ -0,0 +1,254 @@
<html>
<head>
<title>Actor Supervision Java with Lambda Support</title>
</head>
<body>
<div>
<h2>Quick Overview</h2>
<p>Congratulations! You have just created your first fault-resilient Akka
application, nice job!</p>
<p>Let's start with an overview and discuss the problem we want
to solve. This tutorial application demonstrates the use of Akka
supervision hierarchies to implement reliable systems. This particular
example demonstrates a calculator service that calculates arithmetic
expressions. We will visit each of the components shortly, but you might
want to take a quick look at the components before we move on.</p>
<ul>
<li><a href="#code/src/main/java/supervision/Expression.java" class="shortcut">Expression.java</a>
contains our "domain model", a very simple representation of
arithmetic expressions
</li>
<li><a href="#code/src/main/java/supervision/ArithmeticService.java"
class="shortcut">ArithmeticService.java</a> is the entry point
for our calculation service
</li>
<li><a href="#code/src/main/java/supervision/FlakyExpressionCalculator.java"
class="shortcut">FlakyExpressionCalculator.java</a> is our
heavy-lifter, a worker actor that can evaluate an expression
concurrently
</li>
<li><a href="#code/src/main/java/supervision/Main.java" class="shortcut">Main.java</a>
example code that starts up the calculator service and sends a few
jobs to it
</li>
</ul>
</div>
<div>
<h2>The Expression Model</h2>
<p>Our service deals with arithmetic expressions on integers involving
addition, multiplication and (integer) division. In
<a href="#code/src/main/java/supervision/Expression.java" class="shortcut">Expression.java</a>
you can see a very simple model of these kind of expressions.</p>
<p>Any arithmetic expression is a descendant of <code>Expression</code>, and
have a left and right side (<code>Const</code> is the only exception)
which is also an <code>Expression</code>.</p>
<p>For example, the expression (3 + 5) / (2 * (1 + 1)) could be constructed
as:</p>
<code><pre>
new Divide(
new Add(
new Const(3),
new Const(5)
), // (3 + 5)
new Multiply(
new Const(2),
new Add(
new Const(1),
new Const(1)
) // (1 + 1)
) // (2 * (1 + 1))
); // (3 + 5) / (2 * (1 + 1))
</pre></code>
<p>Apart from the encoding of an expression and some pretty printing, our
model does not provide other services, so lets move on, and see how we
can calculate the result of such expressions.</p>
</div>
<div>
<h2>Arithmetic Service</h2>
<p>Our entry point is the <a
href="#code/src/main/java/supervision/ArithmeticService.java"
class="shortcut">ArithmeticService</a> actor that accepts arithmetic
expressions, calculates them and returns the result to the original
sender of the <code>Expression</code>.This
logic is implemented in the <code>receive</code> block. The actor
handles <code>Expression</code> messages and starts a worker for them,
carefully recording which worker belongs to which requester in the
<code>pendingWorkers</code> map.
</p>
<p>Who calculates
the expression? As you see, on the reception of an
<code>Expression</code> message we create a <code>FlakyExpressionCalculator</code>
actor and pass the expression as a parameter to its <code>Props</code>.
What happens here is that we delegate the calculation work to a worker
actor because the work can be "dangerous". After the worker
finishes its job, it replies to its parent (in this
case <code>ArithmeticService</code>) with a <code>Result</code>
message. At this point the top level service actor looks up which
actor it needs to send the final result to, and forwards it the value
of the computation.</p>
</div>
<div>
<h2>The Dangers of Arithmetic</h2>
<p>At first, it might feel strange that we don't calculate the result
directly but we delegate it to a new actor. The reason for that, is that
we want to treat the calculation as a dangerous task and isolate its
execution in a different actor to keep the top level service safe.</p>
<p>In our example we will see two kinds of failures</p>
<ul>
<li><code>FlakinessException</code> is a dummy exception that we throw
randomly to simulate transient failures. We will assume that
flakiness is temporary, and retrying the calculation is enough to
eventually get rid of the failure.
</li>
<li>Fatal failures, like <code>ArithmeticException</code> that will not
go away no matter how many times we retry the task. Division by zero
is a good example, since it indicates that the expression is
invalid, and no amount of attempts to calculate it again will fix
it.
</li>
</ul>
<p>To handle these kind of failure modes differently we customized the
supervisor strategy of <a
href="#code/src/main/java/supervision/ArithmeticService.java"
class="shortcut">ArithmeticService</a>. Our strategy here
is to restart the child when a recoverable error is detected (in our
case the dummy <code>FlakinessException</code>), but when arithmetic
errors happen &mdash; like division by zero &mdash; we have no hope to recover
and therefore we stop the worker. In addition,
we have to notify the original requester of the calculation job
about the failure.</p>
<p>We used <code>OneForOneStrategy</code>, since we only want to act on the
failing child, not on all of our children at the same time.</p>
<p>We set <code>loggingEnabled</code> to false, since we wanted to use our
custom logging instead of the built-in reporting.</p>
</div>
<div>
<h2>The Joy of Calculation</h2>
<p>We have now seen our <code>Expression</code> model, our fault modes
and how we deal with them at the top level, delegating the dangerous
work to child workers to isolate the failure, and setting
<code>Stop</code> or <code>Restart</code> directives depending on the
nature of the failure (fatal or transient). Now it's time to
calculate and visit <a href="#code/src/main/java/supervision/FlakyExpressionCalculator.java"
class="shortcut">FlakyExpressionCalculator.java</a>!
</p>
<p>Let's review first our evaluation strategy. When we are facing an
expression like ((4 * 4) / (3 + 1)) we might be tempted to calculate (4
* 4) first, then (3 + 1), and then the final division. We can do better:
Let's calculate the two sides of the division in parallel!</p>
<p>To achieve this, our worker delegates the calculation of the left and
right side of the expression it has been given to two child workers of
the same type (except in the case of constant, where it just sends its
value as <code>Result</code> to its parent.
This logic is in <code>preStart()</code>
since this is the code that will be executed when an actor starts (and
during restarts if the <code>postRestart()</code> is not
overridden).</p>
<p>Since any of the sides of the original expression can finish before the
other, we have to indicate somehow which side has been calculated, that
is why we pass a <code>Position</code> as an argument to workers which
they will put in their <code>Result</code> which they send after the
calculation finished successfully.</p>
</div>
<div>
<h2>Failing Calculations</h2>
<p>As you might have observed, we added a method called
<code>flakiness()</code> that sometimes just misbehaves
(throws a <code>FlakinessException</code>).
This simulates a transient failure. Let's see how our
FlakyExpressionCalculator deals with failure situations.</p>
<p>A supervisor strategy is applied to the children of an actor. Since our
children are actually workers for calculating the left and right side of
our subexpression, we have to think what different failures mean for
us.</p>
<p>If we encounter a <code>FlakinessException</code> it indicates that one
of our workers
just made a hiccup and failed to calculate the answer. Since we know
this failure is recoverable, we just restart the responsible worker.</p>
<p>In case of fatal failures we cannot really do anything ourselves. First
of all, it indicates that the expression is invalid so restart does not
help, second, we are not necessarily the top level worker for the
expression. When an unknown failure is encountered it
is escalated to the parent. The parent of this actor is either another
<code>FlakyExpressionCalculator</code> or the
<code>ArithmeticService </code>. Since the calculators all escalate, no
matter how deep the failure happened, the <code>ArithmeticService</code>
will decide on the fate of the job (in our case, stop it).</p>
</div>
<div>
<h2>When to Split Work? A Small Detour.</h2>
<p>In our example we split expressions recursively and calculated the left
and right sides of each of the expressions. The question naturally
arises: do we gain anything here regarding performance?</p>
<p>In this example more probably not. There is an additional overhead of
splitting up tasks and collecting results, and this case the actual
subtasks consist of simple arithmetic operations which are very fast.
To really gain in performance in practice, the actual subtasks have to
be more heavyweight than this &mdash; but the pattern will be the
same.</p>
</div>
<div>
<h2>Where to go from here?</h2>
<p>After getting comfortable with the code, you can test your
understanding by trying to solve the following small exercises:</p>
<ul>
<li>Add <code>flakiness()</code> to various places in the calculator and
see what happens
</li>
<li>Try devising more calculation intensive nested jobs instead of
arithmetic expressions (for example transformations of a text
document) where parallelism improves performance
</li>
</ul>
<p>You should also visit</p>
<ul>
<li><a href="http://doc.akka.io/docs/akka/2.3-SNAPSHOT/java.html"
target="_blank">The Akka documentation</a></li>
<li><a
href="http://doc.akka.io/docs/akka/2.3-SNAPSHOT/java/lambda-fault-tolerance.html"
target="_blank">Documentation of supervision</a></li>
<li><a href="http://letitcrash.com" target="_blank">The Akka Team blog</a></li>
</ul>
</div>
</body>
</html>