azkaban-aplcache

Check circular dependency (#1760) * Check circular dependency Throws

5/17/2018 2:31:08 PM

Details

diff --git a/azkaban-exec-server/src/main/java/azkaban/dag/DagBuilder.java b/azkaban-exec-server/src/main/java/azkaban/dag/DagBuilder.java
index fed5a75..d920594 100644
--- a/azkaban-exec-server/src/main/java/azkaban/dag/DagBuilder.java
+++ b/azkaban-exec-server/src/main/java/azkaban/dag/DagBuilder.java
@@ -21,6 +21,7 @@ import static java.util.Objects.requireNonNull;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -69,14 +70,100 @@ public class DagBuilder {
    * @return the Dag reflecting the current state of the DagBuilder
    */
   public Dag build() {
+    checkCircularDependencies();
     final Dag dag = new Dag(this.name, this.dagProcessor);
     final Map<NodeBuilder, Node> builderNodeMap = createBuilderToNodeMap(dag);
     updateNodesRelationships(builderNodeMap);
-    // todo HappyRay: circular dependency detection.
     return dag;
   }
 
   /**
+   * Checks if the builder contains nodes that form a circular dependency ring.
+   *
+   * <p>The depth first algorithm is described in this article
+   * <a href="https://en.wikipedia.org/wiki/Topological_sorting">https://en.wikipedia.org/wiki/Topological_sorting</a>
+   * </p>
+   *
+   * @throws DagException if true
+   */
+  private void checkCircularDependencies() {
+    class CircularDependencyChecker {
+
+      // The nodes that need to be visited
+      private final Set<NodeBuilder> toVisit = new HashSet<>(DagBuilder.this.builders);
+
+      // The nodes that have finished traversing all their parent nodes
+      private final Set<NodeBuilder> finished = new HashSet<>();
+
+      // The nodes that are waiting for their parent nodes to finish visit.
+      private final Set<NodeBuilder> ongoing = new HashSet<>();
+
+      // One sample of nodes that form a circular dependency
+      private final List<NodeBuilder> sampleCircularNodes = new ArrayList<>();
+
+      /**
+       * Checks if the builder contains nodes that form a circular dependency ring.
+       *
+       * @throws DagException if true
+       */
+      private void check() {
+        while (!this.toVisit.isEmpty()) {
+          final NodeBuilder node = removeOneNodeFromToVisitSet();
+          if (checkNode(node)) {
+            final String msg = String.format("Circular dependency detected. Sample: %s",
+                this.sampleCircularNodes);
+            throw new DagException(msg);
+          }
+        }
+      }
+
+      /**
+       * Removes one node from the toVisit set and returns that node.
+       *
+       * @return a node
+       */
+      private NodeBuilder removeOneNodeFromToVisitSet() {
+        final Iterator<NodeBuilder> iterator = this.toVisit.iterator();
+        final NodeBuilder node = iterator.next();
+        iterator.remove();
+        return node;
+      }
+
+      /**
+       * Checks if the node is part of a group of nodes that form a circular dependency ring.
+       *
+       * <p>If true, the node will be added to the sampleCircularNodes list</p>
+       *
+       * @param node node to check
+       * @return true if it is
+       */
+      private boolean checkNode(final NodeBuilder node) {
+        if (this.finished.contains(node)) {
+          return false;
+        }
+        if (this.ongoing.contains(node)) {
+          this.sampleCircularNodes.add(node);
+          return true;
+        }
+        this.toVisit.remove(node);
+        this.ongoing.add(node);
+        for (final NodeBuilder parent : node.getParents()) {
+          if (checkNode(parent)) {
+            this.sampleCircularNodes.add(node);
+            return true;
+          }
+        }
+        this.ongoing.remove(node);
+        this.finished.add(node);
+        return false;
+      }
+    }
+
+    final CircularDependencyChecker checker = new CircularDependencyChecker();
+    checker.check();
+  }
+
+  /**
    * Creates nodes using information stored in the current list of builders.
    *
    * <p>New nodes are created here to ensure they don't change even if their corresponding
diff --git a/azkaban-exec-server/src/test/java/azkaban/dag/DagBuilderTest.java b/azkaban-exec-server/src/test/java/azkaban/dag/DagBuilderTest.java
index 132aa83..2bc1b29 100644
--- a/azkaban-exec-server/src/test/java/azkaban/dag/DagBuilderTest.java
+++ b/azkaban-exec-server/src/test/java/azkaban/dag/DagBuilderTest.java
@@ -80,6 +80,27 @@ public class DagBuilderTest {
   }
 
   @Test
+  public void build_should_throw_exception_when_circular_dependency_is_detected() {
+    // given
+    final NodeBuilder nodeBuilder1 = createNodeBuilder("nb1");
+    final NodeBuilder nodeBuilder2 = createNodeBuilder("nb2");
+    final NodeBuilder nodeBuilder3 = createNodeBuilder("nb3");
+    nodeBuilder2.addParents(nodeBuilder1);
+    nodeBuilder3.addParents(nodeBuilder2);
+    nodeBuilder1.addParents(nodeBuilder3);
+
+    // when
+    final Throwable thrown = catchThrowable(() -> {
+      this.dagBuilder.build();
+    });
+
+    // then
+    // Expect the exception message to show the loop: nb1 -> nb2 -> nb3 -> nb1.
+    System.out.println("Expect exception: " + thrown);
+    assertThat(thrown).isInstanceOf(DagException.class);
+  }
+
+  @Test
   public void add_dependency_should_not_affect_dag_already_built() {
     // given
     final Dag dag = this.dagBuilder.build();