summaryrefslogtreecommitdiffstats
path: root/container-di
diff options
context:
space:
mode:
authorgjoranv <gv@verizonmedia.com>2019-07-22 11:34:26 +0200
committergjoranv <gv@verizonmedia.com>2019-07-22 12:42:53 +0200
commit49f480288a0f84034ac204bd52d92177ce19a9d1 (patch)
tree765d6865c57135b4f03a756daf69aa39912799b3 /container-di
parent35ded4c9dd3e6ea9c6ca9738ea1783b8d4ed8bce (diff)
Add utility to find cycles in a directed graph.
- These classes are independent from di classes and can be used as a tool for graphs in general.
Diffstat (limited to 'container-di')
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/CycleFinder.java97
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/Graph.java39
-rw-r--r--container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/CycleFinderTest.java87
-rw-r--r--container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/GraphTest.java69
4 files changed, 292 insertions, 0 deletions
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/CycleFinder.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/CycleFinder.java
new file mode 100644
index 00000000000..14d64dd1863
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/CycleFinder.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+ */
+package com.yahoo.container.di.componentgraph.cycle;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import static com.yahoo.container.di.componentgraph.cycle.CycleFinder.State.BLACK;
+import static com.yahoo.container.di.componentgraph.cycle.CycleFinder.State.GRAY;
+import static com.yahoo.container.di.componentgraph.cycle.CycleFinder.State.WHITE;
+import static com.yahoo.log.LogLevel.DEBUG;
+import static java.util.Collections.singletonList;
+
+
+/**
+ * <p>Applies the
+ * <a href="https://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/"> three-color algorithm</a>
+ * to detect a cycle in a directed graph. If there are multiple cycles, this implementation only detects one
+ * of them and does not guarantee that the shortest cycle is found.
+ * </p>
+ *
+ * @author gjoranv
+ */
+public class CycleFinder<T> {
+ private static final Logger log = Logger.getLogger(CycleFinder.class.getName());
+
+ enum State {
+ WHITE, GRAY, BLACK;
+ }
+
+ private final Graph<T> graph;
+
+ private Map<T, State> colors;
+
+ private List<T> cycle;
+
+ public CycleFinder(Graph<T> graph) {
+ this.graph = graph;
+ }
+
+ private void resetState() {
+ cycle = null;
+ colors = new LinkedHashMap<>();
+ graph.getVertices().forEach(v -> colors.put(v, WHITE));
+ }
+
+ /**
+ * Returns a list of vertices constituting a cycle in the graph, or an empty
+ * list if no cycle was found. Only the first encountered cycle is returned.
+ */
+ public List<T> findCycle() {
+ resetState();
+ for (T vertex : graph.getVertices()) {
+ if (colors.get(vertex) == WHITE) {
+ if (visitDepthFirst(vertex, new ArrayList<>(singletonList(vertex)))) {
+ if (cycle == null) throw new IllegalStateException("Null cycle - this should never happen");
+ if (cycle.isEmpty()) throw new IllegalStateException("Empty cycle - this should never happen");
+ log.log(DEBUG, "Cycle detected: " + cycle);
+ return cycle;
+ }
+ }
+ }
+ return new ArrayList<>();
+ }
+
+ private boolean visitDepthFirst(T vertex, List<T> path) {
+ colors.put(vertex, GRAY);
+ log.log(DEBUG, "Vertex start " + vertex + " - colors: " + colors + " - path: " + path);
+ for (T adjacent : graph.getAdjacent(vertex)) {
+ path.add(adjacent);
+ if (colors.get(adjacent) == GRAY) {
+ cycle = removePathIntoCycle(path);
+ return true;
+ }
+ if (colors.get(adjacent) == WHITE && visitDepthFirst(adjacent, path)) {
+ return true;
+ }
+ path.remove(adjacent);
+ }
+ colors.put(vertex, BLACK);
+ log.log(DEBUG, "Vertex end " + vertex + " - colors: " + colors + " - path: " + path);
+ return false;
+ }
+
+ private List<T> removePathIntoCycle(List<T> pathWithCycle) {
+ T cycleStart = pathWithCycle.get(pathWithCycle.size() - 1);
+ return pathWithCycle.stream()
+ .dropWhile(vertex -> ! vertex.equals(cycleStart))
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/Graph.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/Graph.java
new file mode 100644
index 00000000000..ca1b791f642
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/Graph.java
@@ -0,0 +1,39 @@
+package com.yahoo.container.di.componentgraph.cycle;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Class representing a directed graph.
+ *
+ * @author gjoranv
+ */
+public class Graph<T> {
+
+ private final Map<T, LinkedHashSet<T>> adjMap = new LinkedHashMap<>();
+
+ public void edge(T from, T to) {
+ if (from == null || to == null)
+ throw new IllegalArgumentException("Null vertices are not allowed, edge: " + from + "->" + to);
+
+ adjMap.computeIfAbsent(from, k -> new LinkedHashSet<>()).add(to);
+ adjMap.computeIfAbsent(to, k -> new LinkedHashSet<>());
+ }
+
+ Set<T> getVertices() {
+ return adjMap.keySet();
+ }
+
+ /**
+ * Returns the outgoing edges of the given vertex.
+ */
+ Set<T> getAdjacent(T vertex) {
+ return adjMap.get(vertex);
+ }
+
+ private void throwIfMissingVertex(T vertex) {
+ if (! adjMap.containsKey(vertex)) throw new IllegalArgumentException("No such vertex in the graph: " + vertex);
+ }
+}
diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/CycleFinderTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/CycleFinderTest.java
new file mode 100644
index 00000000000..219aa6b5e8b
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/CycleFinderTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+ */
+
+package com.yahoo.container.di.componentgraph.cycle;
+
+import org.junit.Test;
+
+import static com.yahoo.container.di.componentgraph.cycle.CycleFinderTest.Vertices.A;
+import static com.yahoo.container.di.componentgraph.cycle.CycleFinderTest.Vertices.B;
+import static com.yahoo.container.di.componentgraph.cycle.CycleFinderTest.Vertices.C;
+import static com.yahoo.container.di.componentgraph.cycle.CycleFinderTest.Vertices.D;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ */
+public class CycleFinderTest {
+
+ enum Vertices {A, B, C, D}
+
+ @Test
+ public void graph_without_cycles_returns_no_cycle() {
+ var graph = new Graph<Vertices>();
+ graph.edge(A, B);
+ graph.edge(B, C);
+ graph.edge(A, C);
+ graph.edge(D, A);
+
+ var cycleFinder = new CycleFinder<>(graph);
+ assertThat(cycleFinder.findCycle(), empty());
+ }
+
+ @Test
+ public void graph_with_cycle_returns_cycle() {
+ var graph = new Graph<Vertices>();
+ graph.edge(A, B);
+ graph.edge(B, C);
+ graph.edge(C, A);
+
+ var cycleFinder = new CycleFinder<>(graph);
+ assertThat(cycleFinder.findCycle(), contains(A, B, C, A));
+ }
+
+ @Test
+ public void graph_with_self_referencing_vertex_returns_cycle() {
+ var graph = new Graph<Vertices>();
+ graph.edge(A, A);
+
+ var cycleFinder = new CycleFinder<>(graph);
+ assertThat(cycleFinder.findCycle(), contains(A, A));
+ }
+
+ @Test
+ public void leading_nodes_are_stripped_from_cycle() {
+ var graph = new Graph<Vertices>();
+ graph.edge(A, B);
+ graph.edge(B, C);
+ graph.edge(C, B);
+
+ var cycleFinder = new CycleFinder<>(graph);
+ assertThat(cycleFinder.findCycle(), contains(B, C, B));
+ }
+
+ @Test
+ public void findCycle_is_idempotent_with_cycle() {
+ var graph = new Graph<Vertices>();
+ graph.edge(A, A);
+
+ var cycleFinder = new CycleFinder<>(graph);
+ assertThat(cycleFinder.findCycle(), contains(A, A));
+ assertThat(cycleFinder.findCycle(), contains(A, A));
+ }
+
+ @Test
+ public void findCycle_is_idempotent_without_cycle() {
+ var graph = new Graph<Vertices>();
+ graph.edge(A, B);
+
+ var cycleFinder = new CycleFinder<>(graph);
+ assertThat(cycleFinder.findCycle(), empty());
+ assertThat(cycleFinder.findCycle(), empty());
+ }
+
+}
diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/GraphTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/GraphTest.java
new file mode 100644
index 00000000000..588c1e30ffe
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/GraphTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+ */
+
+package com.yahoo.container.di.componentgraph.cycle;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static com.yahoo.container.di.componentgraph.cycle.GraphTest.Vertices.A;
+import static com.yahoo.container.di.componentgraph.cycle.GraphTest.Vertices.B;
+import static com.yahoo.container.di.componentgraph.cycle.GraphTest.Vertices.C;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.empty;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ */
+public class GraphTest {
+
+ enum Vertices {A, B, C}
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void vertices_and_edges_are_added_and_can_be_retrieved() {
+ var graph = new Graph<Vertices>();
+ graph.edge(A, B);
+ graph.edge(B, C);
+ graph.edge(A, C);
+
+ assertThat(graph.getVertices().size(), is(3));
+ assertThat(graph.getAdjacent(A), containsInAnyOrder(B, C));
+ assertThat(graph.getAdjacent(B), containsInAnyOrder(C));
+ assertThat(graph.getAdjacent(C), empty());
+ }
+
+ @Test
+ public void null_vertices_are_not_allowed() {
+ var graph = new Graph<Vertices>();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Null vertices are not allowed");
+ graph.edge(A, null);
+ }
+
+ @Test
+ public void duplicate_edges_are_ignored() {
+ var graph = new Graph<Vertices>();
+ graph.edge(A, B);
+ graph.edge(A, B);
+
+ assertThat(graph.getAdjacent(A).size(), is(1));
+ }
+
+ @Test
+ public void self_edges_are_allowed() {
+ var graph = new Graph<Vertices>();
+ graph.edge(A, A);
+
+ assertThat(graph.getAdjacent(A), contains(A));
+ }
+
+}