HTRACE-111. HTrace Java client API fixes for 3.2 (cmccabe)
diff --git a/htrace-core/src/main/java/org/apache/htrace/Span.java b/htrace-core/src/main/java/org/apache/htrace/Span.java
index df28630..c6ec37b 100644
--- a/htrace-core/src/main/java/org/apache/htrace/Span.java
+++ b/htrace-core/src/main/java/org/apache/htrace/Span.java
@@ -35,8 +35,6 @@
  */
 @JsonSerialize(using = Span.SpanSerializer.class)
 public interface Span {
-  public static final long ROOT_SPAN_ID = 0x74ace;
-
   /**
    * The block has completed, stop the clock
    */
@@ -88,10 +86,10 @@
   String toString();
 
   /**
-   * Return the pseudo-unique (random) number of the first parent span, returns
-   * ROOT_SPAN_ID if there are no parents.
+   * Returns the parents of the span.
+   * The array will be empty if there are no parents.
    */
-  long getParentId();
+  long[] getParents();
 
   /**
    * Add a data annotation associated with this span
@@ -142,8 +140,8 @@
       jgen.writeStringField("d", span.getDescription());
       jgen.writeStringField("r", span.getProcessId());
       jgen.writeArrayFieldStart("p");
-      if (span.getParentId() != ROOT_SPAN_ID) {
-        jgen.writeString(String.format("%016x", span.getParentId()));
+      for (long parent : span.getParents()) {
+        jgen.writeString(String.format("%016x", parent));
       }
       jgen.writeEndArray();
       Map<String, String> traceInfoMap = span.getKVAnnotations();
diff --git a/htrace-core/src/main/java/org/apache/htrace/Trace.java b/htrace-core/src/main/java/org/apache/htrace/Trace.java
index 353be25..08e215a 100644
--- a/htrace-core/src/main/java/org/apache/htrace/Trace.java
+++ b/htrace-core/src/main/java/org/apache/htrace/Trace.java
@@ -23,8 +23,6 @@
 import org.apache.htrace.wrappers.TraceCallable;
 import org.apache.htrace.wrappers.TraceRunnable;
 
-import java.security.SecureRandom;
-import java.util.Random;
 import java.util.concurrent.Callable;
 
 /**
@@ -62,7 +60,6 @@
  */
 public class Trace {
   private static final Log LOG = LogFactory.getLog(Trace.class);
-  private final static Random random = new SecureRandom();
 
   /**
    * Creates a new trace scope.
@@ -78,6 +75,19 @@
     return startSpan(description, TrueIfTracingSampler.INSTANCE);
   }
 
+  public static TraceScope startSpan(String description, TraceInfo tinfo) {
+    if (tinfo == null) return continueSpan(null);
+    Span newSpan = new MilliSpan.Builder().
+        begin(System.currentTimeMillis()).
+        end(0).
+        description(description).
+        traceId(tinfo.traceId).
+        parents(new long[] { tinfo.spanId }).
+        processId(Tracer.getProcessId()).
+        build();
+    return continueSpan(newSpan);
+  }
+
   /**
    * Creates a new trace scope.
    *
diff --git a/htrace-core/src/main/java/org/apache/htrace/TraceTree.java b/htrace-core/src/main/java/org/apache/htrace/TraceTree.java
index 7d0c368..1f90c7f 100644
--- a/htrace-core/src/main/java/org/apache/htrace/TraceTree.java
+++ b/htrace-core/src/main/java/org/apache/htrace/TraceTree.java
@@ -21,11 +21,14 @@
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.TreeSet;
 
@@ -35,16 +38,16 @@
 public class TraceTree {
   public static final Log LOG = LogFactory.getLog(Tracer.class);
 
+
   public static class SpansByParent {
+    /**
+     * Compare two spans by span ID.
+     */
     private static Comparator<Span> COMPARATOR =
         new Comparator<Span>() {
           @Override
           public int compare(Span a, Span b) {
-            if (a.getParentId() < b.getParentId()) {
-              return -1;
-            } else if (a.getParentId() > b.getParentId()) {
-              return 1;
-            } else if (a.getSpanId() < b.getSpanId()) {
+            if (a.getSpanId() < b.getSpanId()) {
               return -1;
             } else if (a.getSpanId() > b.getSpanId()) {
               return 1;
@@ -56,27 +59,37 @@
 
     private final TreeSet<Span> treeSet;
 
+    private final HashMap<Long, LinkedList<Span>> parentToSpans;
+
     SpansByParent(Collection<Span> spans) {
       TreeSet<Span> treeSet = new TreeSet<Span>(COMPARATOR);
+      parentToSpans = new HashMap<Long, LinkedList<Span>>();
       for (Span span : spans) {
         treeSet.add(span);
+        for (long parent : span.getParents()) {
+          LinkedList<Span> list = parentToSpans.get(Long.valueOf(parent));
+          if (list == null) {
+            list = new LinkedList<Span>();
+            parentToSpans.put(Long.valueOf(parent), list);
+          }
+          list.add(span);
+        }
+        if (span.getParents().length == 0) {
+          LinkedList<Span> list = parentToSpans.get(Long.valueOf(0L));
+          if (list == null) {
+            list = new LinkedList<Span>();
+            parentToSpans.put(Long.valueOf(0L), list);
+          }
+          list.add(span);
+        }
       }
       this.treeSet = treeSet;
     }
 
     public List<Span> find(long parentId) {
-      List<Span> spans = new ArrayList<Span>();
-      Span span = new MilliSpan("", Long.MIN_VALUE,
-          parentId, Long.MIN_VALUE, "");
-      while (true) {
-        span = treeSet.higher(span);
-        if (span == null) {
-          break;
-        }
-        if (span.getParentId() != parentId) {
-          break;
-        }
-        spans.add(span);
+      LinkedList<Span> spans = parentToSpans.get(parentId);
+      if (spans == null) {
+        return new LinkedList<Span>();
       }
       return spans;
     }
@@ -87,6 +100,9 @@
   }
 
   public static class SpansByProcessId {
+    /**
+     * Compare two spans by process ID, and then by span ID.
+     */
     private static Comparator<Span> COMPARATOR =
         new Comparator<Span>() {
           @Override
@@ -116,8 +132,11 @@
 
     public List<Span> find(String processId) {
       List<Span> spans = new ArrayList<Span>();
-      Span span = new MilliSpan("", Long.MIN_VALUE,
-          Long.MIN_VALUE, Long.MIN_VALUE, processId);
+      Span span = new MilliSpan.Builder().
+                    traceId(Long.MIN_VALUE).
+                    spanId(Long.MIN_VALUE).
+                    processId(processId).
+                    build();
       while (true) {
         span = treeSet.higher(span);
         if (span == null) {
@@ -143,8 +162,7 @@
    * Create a new TraceTree
    *
    * @param spans The collection of spans to use to create this TraceTree. Should
-   *              have at least one root span (span with parentId =
-   *              Span.ROOT_SPAN_ID
+   *              have at least one root span.
    */
   public TraceTree(Collection<Span> spans) {
     this.spansByParent = new SpansByParent(spans);
diff --git a/htrace-core/src/main/java/org/apache/htrace/Tracer.java b/htrace-core/src/main/java/org/apache/htrace/Tracer.java
index a214e5a..ddfd25c 100644
--- a/htrace-core/src/main/java/org/apache/htrace/Tracer.java
+++ b/htrace-core/src/main/java/org/apache/htrace/Tracer.java
@@ -20,7 +20,6 @@
 import org.apache.commons.logging.LogFactory;
 import org.apache.htrace.impl.MilliSpan;
 
-import java.security.SecureRandom;
 import java.util.List;
 import java.util.Random;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -31,7 +30,16 @@
  */
 public class Tracer {
   public static final Log LOG = LogFactory.getLog(Tracer.class);
-  private final static Random random = new SecureRandom();
+  private final static Random random = new Random();
+
+  private static long random64() {
+    long id;
+    do {
+      id = random.nextLong();
+    } while (id == 0);
+    return id;
+  }
+
   private final List<SpanReceiver> receivers = new CopyOnWriteArrayList<SpanReceiver>();
   private static final ThreadLocal<Span> currentSpan = new ThreadLocal<Span>() {
     @Override
@@ -40,6 +48,7 @@
     }
   };
   public static final TraceInfo DONT_TRACE = new TraceInfo(-1, -1);
+  private static final long EMPTY_PARENT_ARRAY[] = new long[0];
   protected static String processId = null;
 
   /**
@@ -68,11 +77,15 @@
   protected Span createNew(String description) {
     Span parent = currentSpan.get();
     if (parent == null) {
-      return new MilliSpan(description,
-          /* traceId = */ random.nextLong(),
-          /* parentSpanId = */ Span.ROOT_SPAN_ID,
-          /* spanId = */ random.nextLong(),
-          getProcessId());
+      return new MilliSpan.Builder().
+          begin(System.currentTimeMillis()).
+          end(0).
+          description(description).
+          traceId(random64()).
+          parents(EMPTY_PARENT_ARRAY).
+          spanId(random64()).
+          processId(getProcessId()).
+          build();
     } else {
       return parent.child(description);
     }
diff --git a/htrace-core/src/main/java/org/apache/htrace/impl/MilliSpan.java b/htrace-core/src/main/java/org/apache/htrace/impl/MilliSpan.java
index 4467208..9babfc1 100644
--- a/htrace-core/src/main/java/org/apache/htrace/impl/MilliSpan.java
+++ b/htrace-core/src/main/java/org/apache/htrace/impl/MilliSpan.java
@@ -47,8 +47,6 @@
  */
 @JsonDeserialize(using = MilliSpan.MilliSpanDeserializer.class)
 public class MilliSpan implements Span {
-
-  private static Random rand = new Random();
   private static ObjectWriter JSON_WRITER = new ObjectMapper().writer();
   private static final long EMPTY_PARENT_ARRAY[] = new long[0];
 
@@ -61,10 +59,27 @@
   private Map<String, String> traceInfo = null;
   private final String processId;
   private List<TimelineAnnotation> timeline = null;
+  private final static Random random = new Random();
+
+  private static long random64() {
+    long id;
+    do {
+      id = random.nextLong();
+    } while (id == 0);
+    return id;
+  }
 
   @Override
-  public Span child(String description) {
-    return new MilliSpan(description, traceId, spanId, rand.nextLong(), processId);
+  public Span child(String childDescription) {
+    return new MilliSpan.Builder().
+      begin(System.currentTimeMillis()).
+      end(0).
+      description(childDescription).
+      traceId(traceId).
+      parents(new long[] {spanId}).
+      spanId(random64()).
+      processId(processId).
+      build();
   }
 
   /**
@@ -155,20 +170,6 @@
     this.timeline = builder.timeline;
   }
 
-  public MilliSpan(String description, long traceId, long parentSpanId, long spanId, String processId) {
-    this.description = description;
-    this.traceId = traceId;
-    if (parentSpanId == Span.ROOT_SPAN_ID) {
-      this.parents = new long[0];
-    } else {
-      this.parents = new long[] { parentSpanId };
-    } 
-    this.spanId = spanId;
-    this.begin = System.currentTimeMillis();
-    this.end = 0;
-    this.processId = processId;
-  }
-
   @Override
   public synchronized void stop() {
     if (end == 0) {
@@ -213,14 +214,9 @@
     return spanId;
   }
 
-  // TODO: Fix API callers to deal with multiple parents, and get rid of
-  // Span.ROOT_SPAN_ID.
   @Override
-  public long getParentId() {
-    if (parents.length == 0) {
-      return Span.ROOT_SPAN_ID;
-    }
-    return parents[0];
+  public long[] getParents() {
+    return parents;
   }
 
   @Override
diff --git a/htrace-core/src/test/java/org/apache/htrace/TestHTrace.java b/htrace-core/src/test/java/org/apache/htrace/TestHTrace.java
index 7b058d7..cd4eb28 100644
--- a/htrace-core/src/test/java/org/apache/htrace/TestHTrace.java
+++ b/htrace-core/src/test/java/org/apache/htrace/TestHTrace.java
@@ -77,7 +77,7 @@
 
     Collection<Span> spans = psr.getSpans();
     TraceTree traceTree = new TraceTree(spans);
-    Collection<Span> roots = traceTree.getSpansByParent().find(Span.ROOT_SPAN_ID);
+    Collection<Span> roots = traceTree.getSpansByParent().find(0);
     Assert.assertTrue("Trace tree must have roots", !roots.isEmpty());
     Assert.assertEquals(numTraces, roots.size());
 
diff --git a/htrace-core/src/test/java/org/apache/htrace/impl/TestMilliSpan.java b/htrace-core/src/test/java/org/apache/htrace/impl/TestMilliSpan.java
index 7ad0482..857e9ac 100644
--- a/htrace-core/src/test/java/org/apache/htrace/impl/TestMilliSpan.java
+++ b/htrace-core/src/test/java/org/apache/htrace/impl/TestMilliSpan.java
@@ -19,12 +19,14 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import org.apache.htrace.Span;
 import org.apache.htrace.TimelineAnnotation;
 import org.junit.Test;
 
 import java.security.SecureRandom;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -40,7 +42,7 @@
     assertEquals(expected.getTraceId(), got.getTraceId());
     assertEquals(expected.getSpanId(), got.getSpanId());
     assertEquals(expected.getProcessId(), got.getProcessId());
-    assertEquals(expected.getParentId(), got.getParentId());
+    assertTrue(Arrays.equals(expected.getParents(), got.getParents()));
     Map<String, String> expectedT = expected.getKVAnnotations();
     Map<String, String> gotT = got.getKVAnnotations();
     if (expectedT == null) {
diff --git a/htrace-flume/src/test/java/org/apache/htrace/impl/TestFlumeSpanReceiver.java b/htrace-flume/src/test/java/org/apache/htrace/impl/TestFlumeSpanReceiver.java
index a825690..f7d3840 100644
--- a/htrace-flume/src/test/java/org/apache/htrace/impl/TestFlumeSpanReceiver.java
+++ b/htrace-flume/src/test/java/org/apache/htrace/impl/TestFlumeSpanReceiver.java
@@ -72,7 +72,13 @@
       startReceiver(null, avroHandler);
       
       spans = new ArrayList<Span>();
-      Span rootSpan = new MilliSpan(ROOT_SPAN_DESC, 1, Span.ROOT_SPAN_ID, 100, "test");
+      Span rootSpan = new MilliSpan.Builder().
+                  description(ROOT_SPAN_DESC).
+                  traceId(1).
+                  spanId(100).
+                  processId("test").
+                  begin(System.currentTimeMillis()).
+                  build();
       Span innerOne = rootSpan.child("Some good work");
       Span innerTwo = innerOne.child("Some more good work");
       innerTwo.stop();
diff --git a/htrace-hbase/src/main/java/org/apache/htrace/impl/HBaseSpanReceiver.java b/htrace-hbase/src/main/java/org/apache/htrace/impl/HBaseSpanReceiver.java
index bb9c3a8..7a99366 100644
--- a/htrace-hbase/src/main/java/org/apache/htrace/impl/HBaseSpanReceiver.java
+++ b/htrace-hbase/src/main/java/org/apache/htrace/impl/HBaseSpanReceiver.java
@@ -207,12 +207,22 @@
           for (Span span : dequeuedSpans) {
             sbuilder.clear()
                     .setTraceId(span.getTraceId())
-                    .setParentId(span.getParentId())
                     .setStart(span.getStartTimeMillis())
                     .setStop(span.getStopTimeMillis())
                     .setSpanId(span.getSpanId())
                     .setProcessId(span.getProcessId())
                     .setDescription(span.getDescription());
+
+            if (span.getParents().length == 0) {
+              sbuilder.setParentId(0);
+            } else if (span.getParents().length > 0) {
+              sbuilder.setParentId(span.getParents()[0]);
+              if (span.getParents().length > 1) {
+                LOG.error("error: HBaseSpanReceiver does not support spans " +
+                    "with multiple parents.  Ignoring multiple parents for " +
+                    span);
+              }
+            }
             for (TimelineAnnotation ta : span.getTimelineAnnotations()) {
               sbuilder.addTimeline(tlbuilder.clear()
                                             .setTime(ta.getTime())
@@ -223,7 +233,7 @@
             put.add(HBaseSpanReceiver.this.cf,
                     sbuilder.build().toByteArray(),
                     null);
-            if (span.getParentId() == Span.ROOT_SPAN_ID) {
+            if (span.getParents().length == 0) {
               put.add(HBaseSpanReceiver.this.icf,
                       INDEX_TIME_QUAL,
                       Bytes.toBytes(span.getStartTimeMillis()));
diff --git a/htrace-hbase/src/test/java/org/apache/htrace/impl/TestHBaseSpanReceiver.java b/htrace-hbase/src/test/java/org/apache/htrace/impl/TestHBaseSpanReceiver.java
index c990c18..79d6e9b 100644
--- a/htrace-hbase/src/test/java/org/apache/htrace/impl/TestHBaseSpanReceiver.java
+++ b/htrace-hbase/src/test/java/org/apache/htrace/impl/TestHBaseSpanReceiver.java
@@ -94,7 +94,7 @@
 
     TraceTree traceTree = new TraceTree(spans);
     Collection<Span> roots =
-        traceTree.getSpansByParent().find(Span.ROOT_SPAN_ID);
+        traceTree.getSpansByParent().find(0);
     Assert.assertTrue("Trace tree must have roots", !roots.isEmpty());
     Assert.assertEquals(3, roots.size());
 
@@ -127,8 +127,7 @@
           InputStream in = new ByteArrayInputStream(cell.getValueArray(),
                                                     cell.getValueOffset(),
                                                     cell.getValueLength());
-          Assert.assertEquals(SpanProtos.Span.parseFrom(in).getParentId(),
-                              Span.ROOT_SPAN_ID);
+          Assert.assertEquals(SpanProtos.Span.parseFrom(in).getParentId(), 0);
         }
       }
     } catch (IOException e) {
@@ -149,8 +148,10 @@
     }
 
     @Override
-    public long getParentId() {
-      return span.getParentId();
+    public long[] getParents() {
+      return (span.getParentId() == 0L) ?
+        (new long[] {}) :
+        (new long[] { span.getParentId() });
     }
 
     @Override
@@ -181,7 +182,7 @@
     @Override
     public String toString() {
       return String.format("Span{Id:0x%16x,parentId:0x%16x,pid:%s,desc:%s}",
-                           getSpanId(), getParentId(),
+                           getSpanId(), span.getParentId(),
                            getProcessId(), getDescription());
     }
 
diff --git a/htrace-zipkin/src/main/java/org/apache/htrace/zipkin/HTraceToZipkinConverter.java b/htrace-zipkin/src/main/java/org/apache/htrace/zipkin/HTraceToZipkinConverter.java
index 8bd6442..4154d92 100644
--- a/htrace-zipkin/src/main/java/org/apache/htrace/zipkin/HTraceToZipkinConverter.java
+++ b/htrace-zipkin/src/main/java/org/apache/htrace/zipkin/HTraceToZipkinConverter.java
@@ -111,8 +111,12 @@
     List<Annotation> annotationList = createZipkinAnnotations(hTraceSpan, ep);
     List<BinaryAnnotation> binaryAnnotationList = createZipkinBinaryAnnotations(hTraceSpan, ep);
     zipkinSpan.setTrace_id(hTraceSpan.getTraceId());
-    if (hTraceSpan.getParentId() != org.apache.htrace.Span.ROOT_SPAN_ID) {
-      zipkinSpan.setParent_id(hTraceSpan.getParentId());
+    if (hTraceSpan.getParents().length > 0) {
+      if (hTraceSpan.getParents().length > 1) {
+        LOG.error("zipkin doesn't support spans with multiple parents.  Omitting " +
+            "other parents for " + hTraceSpan);
+      }
+      zipkinSpan.setParent_id(hTraceSpan.getParents()[0]);
     }
     zipkinSpan.setId(hTraceSpan.getSpanId());
     zipkinSpan.setName(hTraceSpan.getDescription());
diff --git a/htrace-zipkin/src/test/java/org/apache/htrace/TestHTraceSpanToZipkinSpan.java b/htrace-zipkin/src/test/java/org/apache/htrace/TestHTraceSpanToZipkinSpan.java
index 07a9cea..de4a8cd 100644
--- a/htrace-zipkin/src/test/java/org/apache/htrace/TestHTraceSpanToZipkinSpan.java
+++ b/htrace-zipkin/src/test/java/org/apache/htrace/TestHTraceSpanToZipkinSpan.java
@@ -45,7 +45,14 @@
     POJOSpanReceiver psr = new POJOSpanReceiver(HTraceConfiguration.EMPTY);
     Trace.addReceiver(psr);
 
-    Span rootSpan = new MilliSpan(ROOT_SPAN_DESC, 1, Span.ROOT_SPAN_ID, 100, "test");
+    Span rootSpan = new MilliSpan.Builder().
+            description(ROOT_SPAN_DESC).
+            traceId(1).
+            parents(new long[] { } ).
+            spanId(100).
+            processId("test").
+            begin(System.currentTimeMillis()).
+            build();
     Span innerOne = rootSpan.child("Some good work");
     Span innerTwo = innerOne.child("Some more good work");
     innerTwo.stop();
@@ -66,7 +73,10 @@
 
     String traceName = "testHTraceAnnotationTimestamp";
     long startTime = System.currentTimeMillis() * 1000;
-    Span ms = new MilliSpan(traceName, 1, Span.ROOT_SPAN_ID, 2, traceName);
+    Span ms = new MilliSpan.Builder().
+        description(traceName).traceId(1).parents(new long[] { }).
+        spanId(2).processId(traceName).begin(System.currentTimeMillis()).
+        build();
 
     Thread.sleep(500);
     long annoStartTime = System.currentTimeMillis() * 1000;
@@ -105,13 +115,20 @@
 
   @Test
   public void testHTraceDefaultPort() throws IOException {
-    MilliSpan ms = new MilliSpan("test", 1, 2, 3, "hmaster");
+    MilliSpan ms = new MilliSpan.Builder().description("test").
+                      traceId(1).parents(new long[] { 2 }).
+                      spanId(3).processId("hmaster").
+                      begin(System.currentTimeMillis()).build();
     com.twitter.zipkin.gen.Span zs = new HTraceToZipkinConverter(12345, (short) -1).convert(ms);
     for (com.twitter.zipkin.gen.Annotation annotation:zs.getAnnotations()) {
       assertEquals((short)60000, annotation.getHost().getPort());
     }
 
-    ms = new MilliSpan("test", 1, 2, 3, "HregIonServer");   // make sure it's all lower cased
+    // make sure it's all lower cased
+    ms = new MilliSpan.Builder().description("test").traceId(1).
+                      parents(new long[] {2}).spanId(3).
+                      processId("HregIonServer").
+                      begin(System.currentTimeMillis()).build();
     zs = new HTraceToZipkinConverter(12345, (short) -1).convert(ms);
     for (com.twitter.zipkin.gen.Annotation annotation:zs.getAnnotations()) {
       assertEquals((short)60020, annotation.getHost().getPort());
@@ -120,8 +137,10 @@
 
   private void assertSpansAreEquivalent(Span s, com.twitter.zipkin.gen.Span zs) {
     assertEquals(s.getTraceId(), zs.getTrace_id());
-    if (s.getParentId() != Span.ROOT_SPAN_ID) {
-      assertEquals(s.getParentId(), zs.getParent_id());
+    assertTrue("zipkin doesn't support multiple parents to a single span.",
+          s.getParents().length <= 1);
+    if (s.getParents().length == 1) {
+      assertEquals(s.getParents()[0], zs.getParent_id());
     }
     assertEquals(s.getSpanId(), zs.getId());
     Assert.assertNotNull(zs.getAnnotations());