From 8d6186e82888bcad4fa2d63747ad984fbbc4b9b1 Mon Sep 17 00:00:00 2001 From: Alex Sweeney Date: Fri, 24 Jun 2022 16:10:35 -0500 Subject: [PATCH] ready for release --- README.md | 15 +++++----- src/main/java/me/alexjs/dag/Dag.java | 2 +- .../java/me/alexjs/dag/DagTraversalTask.java | 26 ++++++++--------- src/main/java/me/alexjs/dag/HashDag.java | 15 +++++++--- src/test/java/me/alexjs/dag/TestDag.java | 12 ++++---- .../java/me/alexjs/dag/TestDagCollection.java | 28 +++++++++++-------- .../java/me/alexjs/dag/TestingHelper.java | 23 +++++++-------- 7 files changed, 66 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 94fccf9..a57d2f2 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It includes a `Dag` interface so you can provide your own implementation. ```java Dag dag = new HashDag<>(); -// Add nodes with (parent, child) relationships to the DAG +// Add nodes with source -> target relationships to the DAG dag.put("Dorothy", "Shelby"); dag.put("Shelby", "Alex"); dag.put("Joe", "Alex"); @@ -24,8 +24,9 @@ dag.put("Joe", "Alex"); dag.add("Clare"); dag.add("Sarah"); -// Find a reverse-topologically sorted list of the nodes +// Find a topologically sorted list of the nodes // Ex: ["Alex", "Joe", "Sarah", "Shelby", "Dorothy", "Clare"] +// Ex: ["Dorothy", "Shelby", "Clare", "Joe", "Alex", "Sarah"] List sorted = dag.sort(); // Find the root nodes of the DAG @@ -36,13 +37,13 @@ Set roots = dag.getRoots(); // Ex: ["Alex", "Clare", "Sarah"] Set leaves = dag.getLeaves(); -// Find the parents of a node +// Find a node's incoming nodes // Ex: ["Joe", "Shelby"] -Set parents = dag.getParents("Alex"); +Set incoming = dag.getIncoming("Alex"); -// Find the children of a node +// Find a node's outgoing nodes // Ex: ["Shelby"] -Set children = dag.getChildren("Dorothy"); +Set outgoing = dag.getOutgoing("Dorothy"); // Find the ancestors of a node // Ex: ["Joe", "Shelby", "Dorothy"] @@ -62,7 +63,7 @@ Dag copy = dag.clone(); ### DAG Traversal You can use a `DagTraversalTask` to run a task on each node in multiple threads. Each node is only visited once all of -its children have been visited. This is useful for running complex multithreaded pipelines on nodes with shared +its incoming nodes have been visited. This is useful for running complex multithreaded pipelines on nodes with shared dependencies. ```java diff --git a/src/main/java/me/alexjs/dag/Dag.java b/src/main/java/me/alexjs/dag/Dag.java index 73c9275..9354f9f 100644 --- a/src/main/java/me/alexjs/dag/Dag.java +++ b/src/main/java/me/alexjs/dag/Dag.java @@ -44,7 +44,7 @@ public interface Dag extends Collection, Cloneable, Serializable { boolean removeEdge(T source, T target); /** - * Orders the nodes of this DAG such that each source node comes before its target nodes in the ordering + * Orders the nodes of this DAG such that each node comes before its outgoing nodes in the ordering * * @return a list of nodes in topological order, or {@code null} if there's a circular dependency * @see https://en.wikipedia.org/wiki/Topological_sorting diff --git a/src/main/java/me/alexjs/dag/DagTraversalTask.java b/src/main/java/me/alexjs/dag/DagTraversalTask.java index 74626bb..e0edf15 100644 --- a/src/main/java/me/alexjs/dag/DagTraversalTask.java +++ b/src/main/java/me/alexjs/dag/DagTraversalTask.java @@ -30,15 +30,15 @@ public class DagTraversalTask { private final Dag dag; private final Consumer task; private final ListeningExecutorService executorService; - private final Map> parents; + private final Map> outgoingNodes; private final Lock lock; private final AtomicBoolean failed; /** * Create a task that traverses a DAG with an {@link java.util.concurrent.ExecutorService} *

- * The nodes will be traversed in reverse-topological order, - * such that no node is visited until all its children have been visited. + * The nodes will be traversed in topological order, + * such that no node is visited until all its incoming nodes have been visited. * * @param dag the DAG to traverse * @param task the task to apply to each node @@ -49,21 +49,21 @@ public DagTraversalTask(Dag dag, Consumer task, ExecutorService executorSe this.dag = dag.clone(); this.task = task; this.executorService = MoreExecutors.listeningDecorator(executorService); - this.parents = new HashMap<>(); + this.outgoingNodes = new HashMap<>(); this.lock = new ReentrantLock(true); this.failed = new AtomicBoolean(); - // Cache the parents of each node for this DAG - this.dag.getNodes().forEach(node -> this.parents.put(node, this.dag.getIncoming(node))); + // Cache each node's outgoing nodes for this DAG + this.dag.getNodes().forEach(node -> this.outgoingNodes.put(node, this.dag.getOutgoing(node))); // Get the set of leaves for this dag - Set leaves = this.dag.getLeaves(); + Set roots = this.dag.getRoots(); // If there are no leaves, then there are no nodes to visit - if (leaves.isEmpty()) { + if (roots.isEmpty()) { executorService.shutdown(); } else { - visit(leaves); + visit(roots); } } @@ -101,13 +101,13 @@ private void visit(Collection nodes) { executorService.shutdown(); } - Set parents = this.parents.get(node); - parents.retainAll(dag.getNodes()); - parents.removeIf(p -> !dag.getOutgoing(p).isEmpty()); + Set outgoing = this.outgoingNodes.get(node); + outgoing.retainAll(dag.getNodes()); + outgoing.removeIf(p -> !dag.getIncoming(p).isEmpty()); lock.unlock(); - visit(parents); + visit(outgoing); }, MoreExecutors.directExecutor()); } catch (Throwable ignore) { diff --git a/src/main/java/me/alexjs/dag/HashDag.java b/src/main/java/me/alexjs/dag/HashDag.java index 826cc1c..9146a5a 100644 --- a/src/main/java/me/alexjs/dag/HashDag.java +++ b/src/main/java/me/alexjs/dag/HashDag.java @@ -81,14 +81,21 @@ public List sort() { List sorted = new LinkedList<>(); Deque s = new LinkedList<>(getRoots()); - Dag copy = clone(); + Map> copy = this.toMap(); while (!s.isEmpty()) { E n = s.pop(); sorted.add(n); - for (E m : copy.getOutgoing(n)) { - copy.removeEdge(n, m); - if (copy.getIncoming(m).isEmpty()) { + + for (E m : copy.remove(n)) { + boolean hasIncoming = false; + for (Collection entry : copy.values()) { + if (entry.contains(m)) { + hasIncoming = true; + break; + } + } + if (!hasIncoming) { s.add(m); } } diff --git a/src/test/java/me/alexjs/dag/TestDag.java b/src/test/java/me/alexjs/dag/TestDag.java index e4e6003..ba1d05c 100644 --- a/src/test/java/me/alexjs/dag/TestDag.java +++ b/src/test/java/me/alexjs/dag/TestDag.java @@ -18,7 +18,7 @@ public static void init() { @RepeatedTest(50) public void testSort() { - Dag dag = helper.populateDag(); + Dag dag = helper.populateDagSimple(); List sorted = dag.sort(); @@ -94,15 +94,15 @@ public void testEmptyDag() { Set leaves = dag.getLeaves(); Set ancestors = dag.getAncestors(0); Set descendants = dag.getDescendants(0); - Set parents = dag.getIncoming(0); - Set children = dag.getOutgoing(0); + Set incoming = dag.getIncoming(0); + Set outgoing = dag.getOutgoing(0); Assertions.assertTrue(roots.isEmpty()); Assertions.assertTrue(leaves.isEmpty()); Assertions.assertTrue(ancestors.isEmpty()); Assertions.assertTrue(descendants.isEmpty()); - Assertions.assertTrue(parents.isEmpty()); - Assertions.assertTrue(children.isEmpty()); + Assertions.assertTrue(incoming.isEmpty()); + Assertions.assertTrue(outgoing.isEmpty()); Iterator it = dag.iterator(); Assertions.assertFalse(it.hasNext()); @@ -110,7 +110,7 @@ public void testEmptyDag() { } @Test - public void testNoChildren() { + public void testNoOutgoing() { // Test with putAll() Dag dag = new HashDag<>(); diff --git a/src/test/java/me/alexjs/dag/TestDagCollection.java b/src/test/java/me/alexjs/dag/TestDagCollection.java index 8025750..526e0c1 100644 --- a/src/test/java/me/alexjs/dag/TestDagCollection.java +++ b/src/test/java/me/alexjs/dag/TestDagCollection.java @@ -38,16 +38,12 @@ public void testRemoveAndRetain() { Assertions.assertTrue(dag.removeAll(List.of(5, 6, 7))); Assertions.assertTrue(dag.isEmpty()); - dag.put(10, 11); - dag.put(10, 12); - dag.put(13, 14); - dag.put(15, 14); - Assertions.assertEquals(2, dag.getOutgoing(10).size()); - Assertions.assertEquals(2, dag.getIncoming(14).size()); - - Assertions.assertTrue(dag.retainAll(List.of(10, 14))); - Assertions.assertEquals(0, dag.getOutgoing(10).size()); - Assertions.assertEquals(0, dag.getIncoming(14).size()); + dag.put(8, 9); + Assertions.assertTrue(dag.removeEdge(8, 9)); + Assertions.assertTrue(dag.contains(8)); + Assertions.assertTrue(dag.contains(9)); + Assertions.assertTrue(dag.getOutgoing(8).isEmpty()); + Assertions.assertTrue(dag.getIncoming(9).isEmpty()); } @@ -58,12 +54,22 @@ public void testSize() { Assertions.assertTrue(dag.size() > 0); Assertions.assertFalse(dag.isEmpty()); - dag.clear(); Assertions.assertEquals(0, dag.size()); Assertions.assertTrue(dag.isEmpty()); + dag.put(1, 2); + dag.put(1, 3); + dag.put(4, 6); + dag.put(5, 6); + Assertions.assertEquals(2, dag.getOutgoing(1).size()); + Assertions.assertEquals(2, dag.getIncoming(6).size()); + + Assertions.assertTrue(dag.retainAll(List.of(1, 6))); + Assertions.assertEquals(0, dag.getOutgoing(1).size()); + Assertions.assertEquals(0, dag.getIncoming(6).size()); + } @Test diff --git a/src/test/java/me/alexjs/dag/TestingHelper.java b/src/test/java/me/alexjs/dag/TestingHelper.java index 1394cdc..37a9982 100644 --- a/src/test/java/me/alexjs/dag/TestingHelper.java +++ b/src/test/java/me/alexjs/dag/TestingHelper.java @@ -25,13 +25,10 @@ public TestingHelper() { public void assertOrder(Dag dag, List sorted) { Assertions.assertEquals(dag.getNodes().size(), sorted.size()); - for (Integer parent : sorted) { - // If a parent comes before any of its children, then fail - for (Integer child : dag.getOutgoing(parent)) { - if (sorted.indexOf(parent) < sorted.indexOf(child)) { - Assertions.fail(); - return; - } + for (Integer node : sorted) { + // If a node comes before any of its outgoing nodes, then fail + for (Integer outgoing : dag.getOutgoing(node)) { + Assertions.assertTrue(sorted.indexOf(node) < sorted.indexOf(outgoing)); } } } @@ -47,17 +44,17 @@ public int getMiddleNode(Dag dag) { public Dag populateDag() { - // Add a ton of parent-child relationships. Many nodes will have multiple children + // Add a ton of source-target relationships. Many nodes will have multiple outgoing edges Dag dag = new HashDag<>(); int nodes = random.nextInt(5000) + 5000; for (int i = 0; i < nodes; i++) { - // A parent will always be strictly less than its children to ensure no circular dependencies - int parent = random.nextInt(500); - int child = parent + random.nextInt(500) + 1; - dag.put(parent, child); + // Each node will always be strictly less than its outgoing nodes to ensure no circular dependencies + int source = random.nextInt(500); + int target = source + random.nextInt(500) + 1; + dag.put(source, target); } - // Nodes that are guaranteed to have no parents or children + // Nodes that are guaranteed to have no incoming or outgoing edges ArrayList orphans = IntStream.generate(() -> random.nextInt(2000) + 1000) .limit(100) .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);