diff --git a/zipkin/src/main/java/zipkin/internal/CorrectForClockSkew.java b/zipkin/src/main/java/zipkin/internal/CorrectForClockSkew.java index a5679551f23..ec997572fec 100644 --- a/zipkin/src/main/java/zipkin/internal/CorrectForClockSkew.java +++ b/zipkin/src/main/java/zipkin/internal/CorrectForClockSkew.java @@ -13,6 +13,8 @@ */ package zipkin.internal; +import java.util.ArrayList; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -39,9 +41,13 @@ public ClockSkew(Endpoint endpoint, long skew) { public static List<Span> apply(List<Span> spans) { for (Span s : spans) { if (s.parentId == null) { - SpanNode tree = SpanNode.create(s, spans); + Node<Span> tree = Node.constructTree(spans); adjust(tree, null); - return tree.toSpans(); + List<Span> result = new ArrayList<>(spans.size()); + for (Iterator<Node<Span>> i = tree.traverse(); i.hasNext();) { + result.add(i.next().value()); + } + return result; } } return spans; @@ -51,20 +57,20 @@ public static List<Span> apply(List<Span> spans) { * Recursively adjust the timestamps on the span tree. Root span is the reference point, all * children's timestamps gets adjusted based on that span's timestamps. */ - private static void adjust(SpanNode node, @Nullable ClockSkew skewFromParent) { + private static void adjust(Node<Span> node, @Nullable ClockSkew skewFromParent) { // adjust skew for the endpoint brought over from the parent span if (skewFromParent != null) { - node.span = adjustTimestamps(node.span, skewFromParent); + node.value(adjustTimestamps(node.value(), skewFromParent)); } // Is there any skew in the current span? - ClockSkew skew = getClockSkew(node.span); + ClockSkew skew = getClockSkew(node.value()); if (skew != null) { // the current span's skew may be a different endpoint than skewFromParent, adjust again. - node.span = adjustTimestamps(node.span, skew); + node.value(adjustTimestamps(node.value(), skew)); // propagate skew to any children - for (SpanNode child : node.children) { + for (Node<Span> child : node.children()) { adjust(child, skew); } } diff --git a/zipkin/src/main/java/zipkin/internal/Node.java b/zipkin/src/main/java/zipkin/internal/Node.java new file mode 100644 index 00000000000..4bbfce05f50 --- /dev/null +++ b/zipkin/src/main/java/zipkin/internal/Node.java @@ -0,0 +1,152 @@ +/** + * Copyright 2015-2016 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.internal; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import zipkin.Span; + +/** + * Convenience type representing a tree. This is here because multiple facets in zipkin require + * traversing the trace tree. For example, looking at network boundaries to correct clock skew, or + * counting requests imply visiting the tree. + * + * @param <V> the node's value. Ex a full span or a tuple like {@code (serviceName, isLocal)} + */ +public final class Node<V> { + + /** Set via {@link #addChild(Node)} */ + private Node<V> parent; + /** mutable as some transformations, such as clock skew, adjust this. */ + private V value; + /** mutable to avoid allocating lists for childless nodes */ + private List<Node<V>> children = Collections.emptyList(); + + /** Returns the parent, or null if root */ + @Nullable + public Node<V> parent() { + return parent; + } + + public V value() { + return value; + } + + public Node<V> value(V newValue) { + this.value = newValue; + return this; + } + + public Node<V> addChild(Node<V> child) { + child.parent = this; + if (children.equals(Collections.emptyList())) children = new LinkedList<>(); + children.add(child); + return this; + } + + /** Returns the children of this node. */ + public Collection<Node<V>> children() { + return children; + } + + /** Traverses the tree, breadth-first. */ + public Iterator<Node<V>> traverse() { + return new BreadthFirstIterator<>(this); + } + + static final class BreadthFirstIterator<V> implements Iterator<Node<V>> { + private final Queue<Node<V>> queue = new ArrayDeque<>(); + + BreadthFirstIterator(Node<V> root) { + queue.add(root); + } + + @Override + public boolean hasNext() { + return !queue.isEmpty(); + } + + @Override + public Node<V> next() { + Node<V> result = queue.remove(); + queue.addAll(result.children); + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + } + + /** + * @param trace spans that belong to the same {@link Span#traceId trace}, in any order. + */ + static Node<Span> constructTree(List<Span> trace) { + TreeBuilder<Span> treeBuilder = new TreeBuilder<>(); + for (Span s : trace) { + treeBuilder.addNode(s.parentId, s.id, s); + } + return treeBuilder.build(); + } + + /** + * Some operations do not require the entire span object. This creates a tree given (parent id, + * id) pairs. + * + * @param <V> same type as {@link Node#value} + */ + public static final class TreeBuilder<V> { + Node<V> rootNode = null; + + // Nodes representing the trace tree + Map<Long, Node<V>> idToNode = new LinkedHashMap<>(); + // Collect the parent-child relationships between all spans. + Map<Long, Long> idToParent = new LinkedHashMap<>(idToNode.size()); + + public void addNode(Long parentId, long id, @Nullable V value) { + Node<V> node = new Node<V>().value(value); + if (parentId == null) { // special-case root + rootNode = node; + } else { + idToNode.put(id, node); + idToParent.put(id, parentId); + } + } + + /** Builds a tree from calls to {@link #addNode}, or returns an empty tree. */ + public Node<V> build() { + // Materialize the tree using parent - child relationships + for (Map.Entry<Long, Long> entry : idToParent.entrySet()) { + Node<V> node = idToNode.get(entry.getKey()); + Node<V> parent = idToNode.get(entry.getValue()); + if (parent == null && rootNode == null) { // handle headless trace + rootNode = node; + } else if (parent == null) { // attribute missing parents to root + rootNode.addChild(node); + } else { + parent.addChild(node); + } + } + return rootNode != null ? rootNode : new Node<V>(); + } + } +} diff --git a/zipkin/src/main/java/zipkin/internal/SpanNode.java b/zipkin/src/main/java/zipkin/internal/SpanNode.java deleted file mode 100644 index c077ce43399..00000000000 --- a/zipkin/src/main/java/zipkin/internal/SpanNode.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright 2015-2016 The OpenZipkin Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ -package zipkin.internal; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import zipkin.Span; - -import static zipkin.internal.Util.checkNotNull; -import static zipkin.internal.Util.sortedList; - -final class SpanNode { - /** mutable to avoid allocating lists for no reason */ - Span span; - List<SpanNode> children = Collections.emptyList(); - - private SpanNode(Span span) { - this.span = checkNotNull(span, "span"); - } - - private void addChild(SpanNode node) { - if (children.equals(Collections.emptyList())) children = new LinkedList<>(); - children.add(node); - } - - static SpanNode create(Span span, List<Span> spans) { - SpanNode rootNode = new SpanNode(span); - - // Initialize nodes representing the trace tree - Map<Long, SpanNode> idToNode = new LinkedHashMap<>(); - for (Span s : spans) { - if (s.parentId == null) continue; // special-case root - idToNode.put(s.id, new SpanNode(s)); - } - - // Collect the parent-child relationships between all spans. - Map<Long, Long> idToParent = new LinkedHashMap<>(); - for (Map.Entry<Long, SpanNode> entry : idToNode.entrySet()) { - idToParent.put(entry.getKey(), entry.getValue().span.parentId); - } - - // Materialize the tree using parent - child relationships - for (Map.Entry<Long, Long> entry : idToParent.entrySet()) { - SpanNode node = idToNode.get(entry.getKey()); - SpanNode parent = idToNode.get(entry.getValue()); - if (parent == null) { // attribute missing parents to root - rootNode.addChild(node); - } else { - parent.addChild(node); - } - } - return rootNode; - } - - List<Span> toSpans() { - if (children.isEmpty()) { - return Collections.singletonList(span); - } - List<Span> result = new LinkedList<>(); - result.add(span); - for (SpanNode child : children) { - result.addAll(child.toSpans()); - } - return sortedList(result); - } -} diff --git a/zipkin/src/test/java/zipkin/internal/NodeTest.java b/zipkin/src/test/java/zipkin/internal/NodeTest.java new file mode 100644 index 00000000000..767e976be5d --- /dev/null +++ b/zipkin/src/test/java/zipkin/internal/NodeTest.java @@ -0,0 +1,81 @@ +/** + * Copyright 2015-2016 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import zipkin.Span; +import zipkin.TestObjects; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NodeTest { + + /** + * <p>The following tree should traverse in alphabetical order <pre>{@code + * + * a + * / | \ + * b c d + * /|\ \ + * e f g h + * }</pre> + */ + @Test + public void traversesBreadthFirst() { + Node<Character> a = new Node<Character>().value('a'); + Node<Character> b = new Node<Character>().value('b'); + Node<Character> c = new Node<Character>().value('c'); + Node<Character> d = new Node<Character>().value('d'); + // root(a) has children b, c, d + a.addChild(b).addChild(c).addChild(d); + Node<Character> e = new Node<Character>().value('e'); + Node<Character> f = new Node<Character>().value('f'); + Node<Character> g = new Node<Character>().value('g'); + // child(b) has children e, f, g + b.addChild(e).addChild(f).addChild(g); + Node<Character> h = new Node<Character>().value('h'); + // f has no children + // child(g) has child h + g.addChild(h); + + assertThat(a.traverse()).extracting(Node::value) + .containsExactly('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + } + + /** + * Makes sure that the trace tree is constructed based on parent-child, not by parameter order. + */ + @Test + public void constructsTraceTree() { + // TRACE is sorted with root span first, lets shuffle them to make + // sure the trace is stitched together by id. + List<Span> copy = new ArrayList<>(TestObjects.TRACE); + + Collections.shuffle(copy); + + Node<Span> root = Node.constructTree(copy); + assertThat(root.value()) + .isEqualTo(TestObjects.TRACE.get(0)); + + assertThat(root.children()).extracting(Node::value) + .containsExactly(TestObjects.TRACE.get(1)); + + Node<Span> child = root.children().iterator().next(); + assertThat(child.children()).extracting(Node::value) + .containsExactly(TestObjects.TRACE.get(2)); + } +}