Decouple trace flush from collection
Wait until all spans are finished or garbage collected before reporting trace.
MODIFY
dd-java-agent-ittests/src/test/java/datadog/trace/agent/instrumentation/annotation/TraceAnnotationsTest.java
dd-java-agent-ittests/src/test/java/datadog/trace/agent/instrumentation/annotation/TraceAnnotationsTest.java
@@ -49,13 +49,13 @@ public class TraceAnnotationsTest {
assertThat(writer.firstTrace().get(0).context().getParentId()).isEqualTo(0);
assertThat(writer.firstTrace().get(0).getServiceName()).isEqualTo("test2");
- assertThat(writer.firstTrace().get(1).getOperationName()).isEqualTo("SayTracedHello.sayHello");
- assertThat(writer.firstTrace().get(1).getServiceName()).isEqualTo("test");
+ assertThat(writer.firstTrace().get(1).getOperationName()).isEqualTo("SAY_HA");
assertThat(writer.firstTrace().get(1).getParentId()).isEqualTo(parentId);
+ assertThat(writer.firstTrace().get(1).context().getSpanType()).isEqualTo("DB");
- assertThat(writer.firstTrace().get(2).getOperationName()).isEqualTo("SAY_HA");
+ assertThat(writer.firstTrace().get(2).getOperationName()).isEqualTo("SayTracedHello.sayHello");
+ assertThat(writer.firstTrace().get(2).getServiceName()).isEqualTo("test");
assertThat(writer.firstTrace().get(2).getParentId()).isEqualTo(parentId);
- assertThat(writer.firstTrace().get(2).context().getSpanType()).isEqualTo("DB");
}
@Test

MODIFY
dd-java-agent/instrumentation/kafka-streams-0.11/src/test/groovy/KafkaStreamsTest.groovy
dd-java-agent/instrumentation/kafka-streams-0.11/src/test/groovy/KafkaStreamsTest.groovy
@@ -25,7 +25,7 @@ import spock.lang.Timeout
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
-@Timeout(5)
+@Timeout(15)
class KafkaStreamsTest extends AgentTestRunner {
static final STREAM_PENDING = "test.pending"
static final STREAM_PROCESSED = "test.processed"
@@ -135,41 +135,41 @@ class KafkaStreamsTest extends AgentTestRunner {
and: // STREAMING span 0
def t2span1 = t2[0]
- t2span1.context().operationName == "kafka.consume"
+ t2span1.context().operationName == "kafka.produce"
t2span1.serviceName == "kafka"
- t2span1.resourceName == "Consume Topic $STREAM_PENDING"
+ t2span1.resourceName == "Produce Topic $STREAM_PROCESSED"
t2span1.type == "queue"
!t2span1.context().getErrorFlag()
- t2span1.context().parentId == t1span1.context().spanId
def t2tags1 = t2span1.context().tags
t2tags1["component"] == "java-kafka"
- t2tags1["span.kind"] == "consumer"
- t1tags1["span.type"] == "queue"
- t2tags1["partition"] >= 0
- t2tags1["offset"] == 0
+ t2tags1["span.kind"] == "producer"
+ t2tags1["span.type"] == "queue"
t2tags1["thread.name"] != null
t2tags1["thread.id"] != null
- t2tags1["asdf"] == "testing"
- t2tags1.size() == 8
+ t2tags1.size() == 5
and: // STREAMING span 1
def t2span2 = t2[1]
+ t2span1.context().parentId == t2span2.context().spanId
- t2span2.context().operationName == "kafka.produce"
+ t2span2.context().operationName == "kafka.consume"
t2span2.serviceName == "kafka"
- t2span2.resourceName == "Produce Topic $STREAM_PROCESSED"
+ t2span2.resourceName == "Consume Topic $STREAM_PENDING"
t2span2.type == "queue"
!t2span2.context().getErrorFlag()
- t2span2.context().parentId == t2span1.context().spanId
+ t2span2.context().parentId == t1span1.context().spanId
def t2tags2 = t2span2.context().tags
t2tags2["component"] == "java-kafka"
- t2tags2["span.kind"] == "producer"
- t2tags2["span.type"] == "queue"
+ t2tags2["span.kind"] == "consumer"
+ t1tags1["span.type"] == "queue"
+ t2tags2["partition"] >= 0
+ t2tags2["offset"] == 0
t2tags2["thread.name"] != null
t2tags2["thread.id"] != null
- t2tags2.size() == 5
+ t2tags2["asdf"] == "testing"
+ t2tags2.size() == 8
and: // CONSUMER span 0
def t3span1 = t3[0]
@@ -179,7 +179,7 @@ class KafkaStreamsTest extends AgentTestRunner {
t3span1.resourceName == "Consume Topic $STREAM_PROCESSED"
t3span1.type == "queue"
!t3span1.context().getErrorFlag()
- t3span1.context().parentId == t2span2.context().spanId
+ t3span1.context().parentId == t2span1.context().spanId
def t3tags1 = t3span1.context().tags
t3tags1["component"] == "java-kafka"
@@ -194,8 +194,8 @@ class KafkaStreamsTest extends AgentTestRunner {
def headers = received.headers()
headers.iterator().hasNext()
- new String(headers.headers("x-datadog-trace-id").iterator().next().value()) == "$t2span2.traceId"
- new String(headers.headers("x-datadog-parent-id").iterator().next().value()) == "$t2span2.spanId"
+ new String(headers.headers("x-datadog-trace-id").iterator().next().value()) == "$t2span1.traceId"
+ new String(headers.headers("x-datadog-parent-id").iterator().next().value()) == "$t2span1.spanId"
cleanup:

MODIFY
dd-trace-ot/src/main/java/datadog/opentracing/DDSpan.java
dd-trace-ot/src/main/java/datadog/opentracing/DDSpan.java
@@ -12,6 +12,7 @@ import datadog.trace.common.util.Clock;
import io.opentracing.Span;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -42,6 +43,9 @@ public class DDSpan implements Span {
*/
private final AtomicLong durationNano = new AtomicLong();
+ /** Implementation detail. Stores the weak reference to this span. Used by TraceCollection. */
+ volatile WeakReference<DDSpan> ref;
+
/**
* Spans should be constructed using the builder, not by calling the constructor directly.
*
@@ -61,9 +65,7 @@ public class DDSpan implements Span {
// timestamp might have come from an external clock, so don't bother with nanotime.
this.startTimeNano = 0;
}
-
- // track each span of the trace
- this.context.getTrace().add(this);
+ this.context.getTrace().registerSpan(this);
}
@Override
@@ -72,7 +74,7 @@ public class DDSpan implements Span {
// no external clock was used, so we can rely on nanotime, but still ensure a min duration of 1.
if (this.durationNano.compareAndSet(
0, Math.max(1, Clock.currentNanoTicks() - startTimeNano))) {
- writeTraceIfRootSpan();
+ context.getTrace().addSpan(this);
} else {
log.debug("{} - already finished!", this);
}
@@ -86,18 +88,12 @@ public class DDSpan implements Span {
// Ensure that duration is at least 1. Less than 1 is possible due to our use of system clock instead of nano time.
if (this.durationNano.compareAndSet(
0, Math.max(1, TimeUnit.MICROSECONDS.toNanos(stoptimeMicros - this.startTimeMicro)))) {
- writeTraceIfRootSpan();
+ context.getTrace().addSpan(this);
} else {
log.debug("{} - already finished!", this);
}
}
- private void writeTraceIfRootSpan() {
- if (this.isRootSpan()) {
- this.context.getTrace().write();
- }
- }
-
/**
* Check if the span is the root parent. It means that the traceId is the same as the spanId
*
@@ -105,14 +101,7 @@ public class DDSpan implements Span {
*/
@JsonIgnore
public final boolean isRootSpan() {
-
- if (context().getTrace().isEmpty()) {
- return false;
- }
- // First item of the array AND tracer set
- final DDSpan first = context().getTrace().peek();
- return first.context().getSpanId() == this.context().getSpanId()
- && this.context.getTracer() != null;
+ return context.getParentId() == 0;
}
public void setErrorMeta(final Throwable error) {

MODIFY
dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java
dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java
@@ -30,9 +30,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
-import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ThreadLocalRandom;
import lombok.extern.slf4j.Slf4j;
@@ -58,8 +56,6 @@ public class DDTracer implements io.opentracing.Tracer {
private final Map<String, List<AbstractDecorator>> spanContextDecorators =
new ConcurrentHashMap<>();
- private final Set<ScopeContext> contextualScopeManagers = new ConcurrentSkipListSet<>();
-
private final CodecRegistry registry;
private final Map<String, Service> services = new HashMap<>();
@@ -446,7 +442,7 @@ public class DDTracer implements io.opentracing.Tracer {
traceId = ddsc.getTraceId();
parentSpanId = ddsc.getSpanId();
baggage = ddsc.getBaggage();
- parentTrace = new TraceCollection(DDTracer.this);
+ parentTrace = new TraceCollection(DDTracer.this, traceId);
samplingPriority = ddsc.getSamplingPriority();
// Start a new trace
@@ -454,7 +450,7 @@ public class DDTracer implements io.opentracing.Tracer {
traceId = generateNewId();
parentSpanId = 0L;
baggage = null;
- parentTrace = new TraceCollection(DDTracer.this);
+ parentTrace = new TraceCollection(DDTracer.this, traceId);
samplingPriority = PrioritySampling.UNSET;
}

MODIFY
dd-trace-ot/src/main/java/datadog/opentracing/TraceCollection.java
dd-trace-ot/src/main/java/datadog/opentracing/TraceCollection.java
package datadog.opentracing;
-import java.util.concurrent.ConcurrentLinkedQueue;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
@Slf4j
-// TODO: figure out better concurrency model.
-class TraceCollection extends ConcurrentLinkedQueue<DDSpan> {
+class TraceCollection extends ConcurrentLinkedDeque<DDSpan> {
+ static {
+ SpanCleaner.start();
+ }
+
private final DDTracer tracer;
+ private final long traceId;
+
+ private final ReferenceQueue refQueue = new ReferenceQueue();
+ private final Set<WeakReference<?>> weakReferences = Sets.newConcurrentHashSet();
+
+ private final AtomicInteger unfinishedSpanCount = new AtomicInteger(0);
/** Ensure a trace is never written multiple times */
private final AtomicBoolean isWritten = new AtomicBoolean(false);
- TraceCollection(final DDTracer tracer) {
+ TraceCollection(final DDTracer tracer, final long traceId) {
this.tracer = tracer;
+ this.traceId = traceId;
+ SpanCleaner.pendingTraces.add(this);
}
- public void write() {
- if (isWritten.compareAndSet(false, true)) {
- for (final DDSpan span : this) {
- if (span.getDurationNano() == 0L) {
- log.warn(
- "{} - The trace is being written but this span isn't finished. You have to close each children.",
- this);
+ public void registerSpan(final DDSpan span) {
+ if (span.context().getTraceId() != traceId) {
+ log.warn("{} - span registered for wrong trace ({})", span, traceId);
+ return;
+ }
+ span.ref = new WeakReference<DDSpan>(span, refQueue);
+ weakReferences.add(span.ref);
+ unfinishedSpanCount.incrementAndGet();
+ }
+
+ private void expireSpan(final DDSpan span) {
+ if (span.context().getTraceId() != traceId) {
+ log.warn("{} - span expired for wrong trace ({})", span, traceId);
+ return;
+ }
+ weakReferences.remove(span.ref);
+ span.ref.clear();
+ span.ref = null;
+ expireSpan();
}
+
+ private void expireSpan() {
+ if (unfinishedSpanCount.decrementAndGet() == 0) {
+ write();
+ }
+ }
+
+ public void addSpan(final DDSpan span) {
+ if (span.getDurationNano() == 0) {
+ log.warn("{} - added to trace, but not complete.", span);
+ return;
+ }
+ if (traceId != span.getTraceId()) {
+ log.warn("{} - added to a mismatched trace.", span);
+ return;
}
- log.debug("{} - Writing to {}.", this, tracer.writer);
+
+ if (!isWritten.get()) {
+ addFirst(span);
+ expireSpan(span);
+ } else {
+ log.warn("{} - finished after trace reported.", span);
+ }
+ }
+
+ private void write() {
+ if (isWritten.compareAndSet(false, true)) {
+ SpanCleaner.pendingTraces.remove(this);
+ if (!isEmpty()) {
+ log.debug("Writing {} spans to {}.", this.size(), tracer.writer);
tracer.write(this);
}
}
}
+
+ public synchronized boolean clean() {
+ Reference ref;
+ int count = 0;
+ while ((ref = refQueue.poll()) != null) {
+ weakReferences.remove(ref);
+ count++;
+ expireSpan();
+ }
+ if (count > 0) {
+ log.debug("{} unfinished spans garbage collected!", count);
+ }
+ return count > 0;
+ }
+
+ /**
+ * This method ensures that garbage collection takes place, unlike <code>{@link System#gc()}
+ * </code>. Useful for testing.
+ */
+ public static void awaitGC() {
+ System.gc(); // For good measure.
+ Object obj = new Object();
+ final WeakReference ref = new WeakReference<>(obj);
+ obj = null;
+ while (ref.get() != null) {
+ System.gc();
+ }
+ }
+
+ private static class SpanCleaner implements Runnable {
+ private static final long CLEAN_FREQUENCY = 1;
+ private static final ThreadFactory agentWriterThreadFactory =
+ new ThreadFactoryBuilder().setNameFormat("dd-span-cleaner-%d").setDaemon(true).build();
+
+ private static final ScheduledExecutorService scheduledExecutor =
+ Executors.newScheduledThreadPool(1, agentWriterThreadFactory);
+
+ static final Set<TraceCollection> pendingTraces = Sets.newConcurrentHashSet();
+
+ static void start() {
+ scheduledExecutor.scheduleAtFixedRate(
+ new SpanCleaner(), 0, CLEAN_FREQUENCY, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void run() {
+ for (final TraceCollection trace : pendingTraces) {
+ trace.clean();
+ }
+ }
+ }
+}

MODIFY
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanBuilderTest.groovy
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanBuilderTest.groovy
@@ -48,9 +48,10 @@ class DDSpanBuilderTest extends Specification {
span = tracer.buildSpan(expectedName).withServiceName("foo").start()
then:
- span.getTags() == [(DDTags.THREAD_NAME): Thread.currentThread().getName(),
+ span.getTags() == [
+ (DDTags.THREAD_NAME): Thread.currentThread().getName(),
(DDTags.THREAD_ID) : Thread.currentThread().getId(),
- (DDTags.SPAN_TYPE) : null]
+ ]
when:
// with all custom fields provided
@@ -118,6 +119,7 @@ class DDSpanBuilderTest extends Specification {
when(mockedContext.getSpanId()).thenReturn(spanId)
when(mockedContext.getServiceName()).thenReturn("foo")
+ when(mockedContext.getTrace()).thenReturn(new TraceCollection(tracer, 1L))
final String expectedName = "fakeName"
@@ -204,13 +206,15 @@ class DDSpanBuilderTest extends Specification {
final long tickEnd = System.currentTimeMillis()
for (int i = 1; i <= 10; i++) {
- spans.add(tracer
+ def span = tracer
.buildSpan("fake_" + i)
.withServiceName("foo")
.asChildOf(spans.get(i - 1))
- .start())
+ .start()
+ spans.add(span)
+ span.finish()
}
- spans.get(1).finish(tickEnd)
+ root.finish(tickEnd)
expect:
root.context().getTrace().size() == nbSamples + 1
@@ -223,9 +227,10 @@ class DDSpanBuilderTest extends Specification {
System.setProperty("dd.trace.span.tags", tagString)
tracer = new DDTracer(writer)
def span = tracer.buildSpan("op name").withServiceName("foo").start()
- tags.putAll([(DDTags.THREAD_NAME): Thread.currentThread().getName(),
+ tags.putAll([
+ (DDTags.THREAD_NAME): Thread.currentThread().getName(),
(DDTags.THREAD_ID) : Thread.currentThread().getId(),
- (DDTags.SPAN_TYPE) : null])
+ ])
expect:
span.tags == tags

MODIFY
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanSerializationTest.groovy
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanSerializationTest.groovy
@@ -51,7 +51,7 @@ class DDSpanSerializationTest extends Specification {
false,
"type",
tags,
- new TraceCollection(tracer),
+ new TraceCollection(tracer, 1L),
tracer)
baggage.put(DDTags.THREAD_NAME, Thread.currentThread().getName())

MODIFY
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanTest.groovy
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanTest.groovy
@@ -27,7 +27,7 @@ class DDSpanTest extends Specification {
false,
"fakeType",
null,
- new TraceCollection(tracer),
+ new TraceCollection(tracer, 1L),
tracer)
final DDSpan span = new DDSpan(1L, context)

ADD
dd-trace-ot/src/test/groovy/datadog/opentracing/TraceCollectionTest.groovy
+package datadog.opentracing
+
+import datadog.trace.common.writer.ListWriter
+import spock.lang.Specification
+import spock.lang.Subject
+
+
+class TraceCollectionTest extends Specification {
+ def writer = new ListWriter()
+ def tracer = new DDTracer(writer)
+
+ def traceId = System.identityHashCode(this)
+
+ @Subject
+ TraceCollection trace = new TraceCollection(tracer, traceId)
+
+ DDSpan rootSpan = SpanFactory.newSpanOf(trace)
+
+ def setup() {
+ assert trace.size() == 0
+ assert trace.unfinishedSpanCount.get() == 1
+ assert trace.weakReferences.size() == 1
+ assert trace.isWritten.get() == false
+ }
+
+ def "single span gets added to trace and written when finished"() {
+ setup:
+ rootSpan.finish()
+
+ expect:
+ trace.asList() == [rootSpan]
+ writer == [[rootSpan]]
+ }
+
+ def "child finishes before parent"() {
+ when:
+ def child = tracer.buildSpan("child").asChildOf(rootSpan).start()
+
+ then:
+ trace.unfinishedSpanCount.get() == 2
+ trace.weakReferences.size() == 2
+
+ when:
+ child.finish()
+
+ then:
+ trace.unfinishedSpanCount.get() == 1
+ trace.weakReferences.size() == 1
+ trace.asList() == [child]
+ writer == []
+
+ when:
+ rootSpan.finish()
+
+ then:
+ trace.unfinishedSpanCount.get() == 0
+ trace.weakReferences.size() == 0
+ trace.asList() == [rootSpan, child]
+ writer == [[rootSpan, child]]
+ }
+
+ def "parent finishes before child which holds up trace"() {
+ when:
+ def child = tracer.buildSpan("child").asChildOf(rootSpan).start()
+
+ then:
+ trace.unfinishedSpanCount.get() == 2
+ trace.weakReferences.size() == 2
+
+ when:
+ rootSpan.finish()
+
+ then:
+ trace.unfinishedSpanCount.get() == 1
+ trace.weakReferences.size() == 1
+ trace.asList() == [rootSpan]
+ writer == []
+
+ when:
+ child.finish()
+
+ then:
+ trace.unfinishedSpanCount.get() == 0
+ trace.weakReferences.size() == 0
+ trace.asList() == [child, rootSpan]
+ writer == [[child, rootSpan]]
+ }
+
+ def "trace reported when unfinished child discarded"() {
+ when:
+ def child = tracer.buildSpan("child").asChildOf(rootSpan).start()
+ rootSpan.finish()
+
+ then:
+ trace.unfinishedSpanCount.get() == 1
+ trace.weakReferences.size() == 1
+ trace.asList() == [rootSpan]
+ writer == []
+
+ when:
+ child = null
+ while (!trace.clean()) {
+ trace.awaitGC()
+ }
+
+ then:
+ trace.unfinishedSpanCount.get() == 0
+ trace.weakReferences.size() == 0
+ trace.asList() == [rootSpan]
+ writer == [[rootSpan]]
+ }
+
+ def "add unfinished span to trace fails"() {
+ setup:
+ trace.addSpan(rootSpan)
+
+ expect:
+ trace.unfinishedSpanCount.get() == 1
+ trace.weakReferences.size() == 1
+ trace.asList() == []
+ }
+
+ def "register span to wrong trace fails"() {
+ setup:
+ def otherTrace = new TraceCollection(tracer, traceId - 10)
+ otherTrace.registerSpan(new DDSpan(0, rootSpan.context()))
+
+ expect:
+ otherTrace.unfinishedSpanCount.get() == 0
+ otherTrace.weakReferences.size() == 0
+ otherTrace.asList() == []
+ }
+
+ def "add span to wrong trace fails"() {
+ setup:
+ def otherTrace = new TraceCollection(tracer, traceId - 10)
+ rootSpan.finish()
+ otherTrace.addSpan(rootSpan)
+
+ expect:
+ otherTrace.unfinishedSpanCount.get() == 0
+ otherTrace.weakReferences.size() == 0
+ otherTrace.asList() == []
+ }
+}

MODIFY
dd-trace-ot/src/test/groovy/datadog/opentracing/decorators/URLAsResourceNameTest.groovy
dd-trace-ot/src/test/groovy/datadog/opentracing/decorators/URLAsResourceNameTest.groovy
@@ -100,7 +100,7 @@ class URLAsResourceNameTest extends Specification {
false,
"fakeType",
tags,
- new TraceCollection(tracer),
+ new TraceCollection(tracer, 1L),
tracer)
then:

MODIFY
dd-trace-ot/src/test/groovy/datadog/opentracing/propagation/HTTPCodecTest.groovy
dd-trace-ot/src/test/groovy/datadog/opentracing/propagation/HTTPCodecTest.groovy
@@ -46,7 +46,7 @@ class HTTPCodecTest extends Specification {
false,
"fakeType",
null,
- new TraceCollection(tracer),
+ new TraceCollection(tracer, 1L),
tracer)
final Map<String, String> carrier = new HashMap<>()

MODIFY
dd-trace-ot/src/test/groovy/datadog/opentracing/scopemanager/ScopeManagerTest.groovy
dd-trace-ot/src/test/groovy/datadog/opentracing/scopemanager/ScopeManagerTest.groovy
@@ -126,7 +126,7 @@ class ScopeManagerTest extends Specification {
def "continuation restores trace"() {
setup:
- def parentScope = tracer.buildSpan("parent").startActive(false) //false or trace is reported early
+ def parentScope = tracer.buildSpan("parent").startActive(true)
def parentSpan = parentScope.span()
ContinuableScope childScope = (ContinuableScope) tracer.buildSpan("parent").startActive(true)
def childSpan = childScope.span()
@@ -142,12 +142,12 @@ class ScopeManagerTest extends Specification {
when:
parentScope.close()
- // If finishSpanOnClose was true for the parent, the trace would get reported even though the child was not done.
+ // parent span is finished, but trace is not reported
then:
scopeManager.active() == null
!spanReported(childSpan)
- !spanReported(parentSpan)
+ spanReported(parentSpan)
writer == []
when:
@@ -158,7 +158,7 @@ class ScopeManagerTest extends Specification {
newScope != childScope && newScope != parentScope
newScope.span() == childSpan
!spanReported(childSpan)
- !spanReported(parentSpan)
+ spanReported(parentSpan)
writer == []
when:
@@ -167,17 +167,8 @@ class ScopeManagerTest extends Specification {
then:
scopeManager.active() == null
spanReported(childSpan)
- !spanReported(parentSpan)
- writer == []
-
- when:
- // Since finishSpanOnClose was false, we must manually finish the span.
- parentSpan.finish()
-
- then:
- spanReported(childSpan)
spanReported(parentSpan)
- writer == [[parentSpan, childSpan]]
+ writer == [[childSpan, parentSpan]]
}
@Unroll

Decouple trace flush from collection

Wait until all spans are finished or garbage collected before reporting trace.