diff --git a/akka-samples/akka-sample-supervision-java-lambda/COPYING b/akka-samples/akka-sample-supervision-java-lambda/COPYING new file mode 100644 index 0000000000..0e259d42c9 --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/akka-samples/akka-sample-supervision-java-lambda/LICENSE b/akka-samples/akka-sample-supervision-java-lambda/LICENSE new file mode 100644 index 0000000000..287f8dd7fa --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/LICENSE @@ -0,0 +1,10 @@ +Activator Template by Typesafe + +Licensed under Public Domain (CC0) + +To the extent possible under law, the person who associated CC0 with +this Activator Tempate has waived all copyright and related or neighboring +rights to this Activator Template. + +You should have received a copy of the CC0 legalcode along with this +work. If not, see . diff --git a/akka-samples/akka-sample-supervision-java-lambda/activator.properties b/akka-samples/akka-sample-supervision-java-lambda/activator.properties new file mode 100644 index 0000000000..083d7ca33a --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/activator.properties @@ -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 diff --git a/akka-samples/akka-sample-supervision-java-lambda/build.sbt b/akka-samples/akka-sample-supervision-java-lambda/build.sbt new file mode 100644 index 0000000000..b8ae5c2d48 --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/build.sbt @@ -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") diff --git a/akka-samples/akka-sample-supervision-java-lambda/pom.xml b/akka-samples/akka-sample-supervision-java-lambda/pom.xml new file mode 100644 index 0000000000..8d639b4649 --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/pom.xml @@ -0,0 +1,53 @@ + + 4.0.0 + + + UTF-8 + + + sample + akka-supervision-java-lambda + jar + 1.0 + + + + com.typesafe.akka + akka-actor_2.10 + 2.3-SNAPSHOT + + + com.typesafe.akka + akka-testkit_2.10 + 2.3-SNAPSHOT + + + junit + junit + 4.11 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + true + + -Xlint + + + + + + + diff --git a/akka-samples/akka-sample-supervision-java-lambda/project/build.properties b/akka-samples/akka-sample-supervision-java-lambda/project/build.properties new file mode 100644 index 0000000000..37b489cb6e --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.1 diff --git a/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/ArithmeticService.java b/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/ArithmeticService.java new file mode 100644 index 0000000000..9e982076e3 --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/ArithmeticService.java @@ -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 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 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(); + } +} \ No newline at end of file diff --git a/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/Expression.java b/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/Expression.java new file mode 100644 index 0000000000..73760d1c38 --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/Expression.java @@ -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); + } + } +} diff --git a/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/FlakyExpressionCalculator.java b/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/FlakyExpressionCalculator.java new file mode 100644 index 0000000000..3afd7201a7 --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/FlakyExpressionCalculator.java @@ -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 results = new HashMap<>(); + Set 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 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(); + } +} diff --git a/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/Main.java b/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/Main.java new file mode 100644 index 0000000000..ca0fd13099 --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/src/main/java/supervision/Main.java @@ -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(); + } +} diff --git a/akka-samples/akka-sample-supervision-java-lambda/src/test/java/supervision/ArithmeticServiceTest.java b/akka-samples/akka-sample-supervision-java-lambda/src/test/java/supervision/ArithmeticServiceTest.java new file mode 100644 index 0000000000..af721e8682 --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/src/test/java/supervision/ArithmeticServiceTest.java @@ -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); + }}; + } +} diff --git a/akka-samples/akka-sample-supervision-java-lambda/tutorial/index.html b/akka-samples/akka-sample-supervision-java-lambda/tutorial/index.html new file mode 100644 index 0000000000..c5057b0586 --- /dev/null +++ b/akka-samples/akka-sample-supervision-java-lambda/tutorial/index.html @@ -0,0 +1,254 @@ + + + Actor Supervision Java with Lambda Support + + +
+

Quick Overview

+ +

Congratulations! You have just created your first fault-resilient Akka + application, nice job!

+ +

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.

+ +
    +
  • Expression.java + contains our "domain model", a very simple representation of + arithmetic expressions +
  • +
  • ArithmeticService.java is the entry point + for our calculation service +
  • +
  • FlakyExpressionCalculator.java is our + heavy-lifter, a worker actor that can evaluate an expression + concurrently +
  • +
  • Main.java + example code that starts up the calculator service and sends a few + jobs to it +
  • +
+
+
+

The Expression Model

+ +

Our service deals with arithmetic expressions on integers involving + addition, multiplication and (integer) division. In + Expression.java + you can see a very simple model of these kind of expressions.

+ +

Any arithmetic expression is a descendant of Expression, and + have a left and right side (Const is the only exception) + which is also an Expression.

+ +

For example, the expression (3 + 5) / (2 * (1 + 1)) could be constructed + as:

+ + +
+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))
+
+ +

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.

+
+
+

Arithmetic Service

+ +

Our entry point is the ArithmeticService actor that accepts arithmetic + expressions, calculates them and returns the result to the original + sender of the Expression.This + logic is implemented in the receive block. The actor + handles Expression messages and starts a worker for them, + carefully recording which worker belongs to which requester in the + pendingWorkers map. +

+ +

Who calculates + the expression? As you see, on the reception of an + Expression message we create a FlakyExpressionCalculator + actor and pass the expression as a parameter to its Props. + 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 ArithmeticService) with a Result + 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.

+
+ +
+

The Dangers of Arithmetic

+ +

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.

+ +

In our example we will see two kinds of failures

+
    +
  • FlakinessException 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. +
  • +
  • Fatal failures, like ArithmeticException 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. +
  • +
+ +

To handle these kind of failure modes differently we customized the + supervisor strategy of ArithmeticService. Our strategy here + is to restart the child when a recoverable error is detected (in our + case the dummy FlakinessException), but when arithmetic + errors happen — like division by zero — 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.

+ +

We used OneForOneStrategy, since we only want to act on the + failing child, not on all of our children at the same time.

+ +

We set loggingEnabled to false, since we wanted to use our + custom logging instead of the built-in reporting.

+ + + +
+ +
+ +

The Joy of Calculation

+ +

We have now seen our Expression 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 + Stop or Restart directives depending on the + nature of the failure (fatal or transient). Now it's time to + calculate and visit FlakyExpressionCalculator.java! +

+ +

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!

+ +

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 Result to its parent. + This logic is in preStart() + since this is the code that will be executed when an actor starts (and + during restarts if the postRestart() is not + overridden).

+ +

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 Position as an argument to workers which + they will put in their Result which they send after the + calculation finished successfully.

+ +
+
+ + +

Failing Calculations

+ +

As you might have observed, we added a method called + flakiness() that sometimes just misbehaves + (throws a FlakinessException). + This simulates a transient failure. Let's see how our + FlakyExpressionCalculator deals with failure situations.

+ +

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.

+ +

If we encounter a FlakinessException 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.

+ +

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 + FlakyExpressionCalculator or the + ArithmeticService . Since the calculators all escalate, no + matter how deep the failure happened, the ArithmeticService + will decide on the fate of the job (in our case, stop it).

+
+
+

When to Split Work? A Small Detour.

+ +

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?

+ +

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 — but the pattern will be the + same.

+ +
+
+

Where to go from here?

+ +

After getting comfortable with the code, you can test your + understanding by trying to solve the following small exercises:

+
    +
  • Add flakiness() to various places in the calculator and + see what happens +
  • +
  • Try devising more calculation intensive nested jobs instead of + arithmetic expressions (for example transformations of a text + document) where parallelism improves performance +
  • +
+ +

You should also visit

+ +
+ + + + diff --git a/scripts/build/extra-build-steps.sh b/scripts/build/extra-build-steps.sh index f5e1e1fe0d..56da1ab8c0 100755 --- a/scripts/build/extra-build-steps.sh +++ b/scripts/build/extra-build-steps.sh @@ -56,6 +56,14 @@ function check { type -P "$@" &> /dev/null || fail "command not found: $@" } +# run mvn clean test using the specified java home in the specified directory +function mvncleantest { + tmp="$script_dir/../../$2" + try cd "$tmp" "can't step into project directory: $tmp" + export JAVA_HOME="$1" + try mvn clean test "mvn execution in $2 failed" +} + # initialize variables with defaults and override from environment declare java_home="$default_java_home" if [ $AKKA_BUILD_JAVA_HOME ]; then @@ -88,17 +96,10 @@ check "$java8_path" check mvn # now do some work -tmp="$script_dir/../../akka-samples/akka-docs-java-lambda" -try cd "$tmp" "can't step into project directory: $tmp" -export JAVA_HOME="$java8_home" -try mvn clean test "mvn execution in akka-docs-java-lambda failed" +mvncleantest "$java8_home" "akka-samples/akka-docs-java-lambda" -tmp="$script_dir/../../akka-samples/akka-sample-fsm-java-lambda" -try cd "$tmp" "can't step into project directory: $tmp" -export JAVA_HOME="$java8_home" -try mvn clean test "mvn execution in akka-sample-fsm-java-lambda failed" +mvncleantest "$java8_home" "akka-samples/akka-sample-fsm-java-lambda" -tmp="$script_dir/../../akka-samples/akka-sample-persistence-java8" -try cd "$tmp" "can't step into project directory: $tmp" -export JAVA_HOME="$java8_home" -try mvn clean test "mvn execution in akka-sample-fsm-java-lambda failed" +mvncleantest "$java8_home" "akka-samples/akka-sample-persistence-java8" + +mvncleantest "$java8_home" "akka-samples/akka-sample-supervision-java-lambda"