Merge pull request #358 from DataDog/mar-kolya/fix-span-time-boundaries-poc

Fix span time boundaries
This commit is contained in:
Nikolay Martynov 2018-06-26 12:48:49 -04:00 committed by GitHub
commit 088402beed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 83 additions and 29 deletions

View File

@ -32,10 +32,17 @@ public class DDSpan implements Span, MutableSpan {
/** The context attached to the span */ /** The context attached to the span */
private final DDSpanContext context; private final DDSpanContext context;
/** Creation time of the span in microseconds. Must be greater than zero. */ /**
* Creation time of the span in microseconds provided by external clock. Must be greater than
* zero.
*/
private final long startTimeMicro; private final long startTimeMicro;
/** Creation time of span in system relative nanotime (may be negative) */ /**
* Creation time of span in nanoseconds. We use combination of millisecond-precision clock and
* nanosecond-precision offset from start of the trace. See {@link PendingTrace} for details. Must
* be greater than zero.
*/
private final long startTimeNano; private final long startTimeNano;
/** /**
@ -54,19 +61,19 @@ public class DDSpan implements Span, MutableSpan {
* @param context the context used for the span * @param context the context used for the span
*/ */
DDSpan(final long timestampMicro, final DDSpanContext context) { DDSpan(final long timestampMicro, final DDSpanContext context) {
this.context = context; this.context = context;
// record the start time in nano (current milli + nano delta)
if (timestampMicro <= 0L) { if (timestampMicro <= 0L) {
this.startTimeMicro = Clock.currentMicroTime(); // record the start time
this.startTimeNano = Clock.currentNanoTicks(); startTimeMicro = Clock.currentMicroTime();
startTimeNano = context.getTrace().getCurrentTimeNano();
} else { } else {
this.startTimeMicro = timestampMicro; startTimeMicro = timestampMicro;
// timestamp might have come from an external clock, so don't bother with nanotime. // Timestamp have come from an external clock, so use startTimeNano as a flag
this.startTimeNano = 0; startTimeNano = 0;
} }
this.context.getTrace().registerSpan(this);
context.getTrace().registerSpan(this);
} }
@JsonIgnore @JsonIgnore
@ -74,16 +81,20 @@ public class DDSpan implements Span, MutableSpan {
return durationNano.get() != 0; return durationNano.get() != 0;
} }
private void finishAndAddToTrace(final long durationNano) {
// ensure a min duration of 1
if (this.durationNano.compareAndSet(0, Math.max(1, durationNano))) {
context.getTrace().addSpan(this);
} else {
log.debug("{} - already finished!", this);
}
}
@Override @Override
public final void finish() { public final void finish() {
if (startTimeNano != 0) { if (startTimeNano > 0) {
// no external clock was used, so we can rely on nanotime, but still ensure a min duration of 1. // no external clock was used, so we can rely on nano time
if (this.durationNano.compareAndSet( finishAndAddToTrace(context.getTrace().getCurrentTimeNano() - startTimeNano);
0, Math.max(1, Clock.currentNanoTicks() - startTimeNano))) {
context.getTrace().addSpan(this);
} else {
log.debug("{} - already finished!", this);
}
} else { } else {
finish(Clock.currentMicroTime()); finish(Clock.currentMicroTime());
} }
@ -91,13 +102,7 @@ public class DDSpan implements Span, MutableSpan {
@Override @Override
public final void finish(final long stoptimeMicros) { public final void finish(final long stoptimeMicros) {
// Ensure that duration is at least 1. Less than 1 is possible due to our use of system clock instead of nano time. finishAndAddToTrace(TimeUnit.MICROSECONDS.toNanos(stoptimeMicros - startTimeMicro));
if (this.durationNano.compareAndSet(
0, Math.max(1, TimeUnit.MICROSECONDS.toNanos(stoptimeMicros - this.startTimeMicro)))) {
context.getTrace().addSpan(this);
} else {
log.debug("{} - already finished!", this);
}
} }
/** /**
@ -288,7 +293,7 @@ public class DDSpan implements Span, MutableSpan {
@Override @Override
@JsonGetter("start") @JsonGetter("start")
public long getStartTime() { public long getStartTime() {
return startTimeMicro * 1000L; return startTimeNano > 0 ? startTimeNano : TimeUnit.MICROSECONDS.toNanos(startTimeMicro);
} }
@Override @Override

View File

@ -1,6 +1,7 @@
package datadog.opentracing; package datadog.opentracing;
import datadog.opentracing.scopemanager.ContinuableScope; import datadog.opentracing.scopemanager.ContinuableScope;
import datadog.trace.common.util.Clock;
import java.io.Closeable; import java.io.Closeable;
import java.lang.ref.Reference; import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue; import java.lang.ref.ReferenceQueue;
@ -29,6 +30,12 @@ public class PendingTrace extends ConcurrentLinkedDeque<DDSpan> {
private final DDTracer tracer; private final DDTracer tracer;
private final long traceId; private final long traceId;
// TODO: consider moving these time fields into DDTracer to ensure that traces have precise relative time
/** Trace start time in nano seconds measured up to a millisecond accuracy */
private final long startTimeNano;
/** Nano second ticks value at trace start */
private final long startNanoTicks;
private final ReferenceQueue referenceQueue = new ReferenceQueue(); private final ReferenceQueue referenceQueue = new ReferenceQueue();
private final Set<WeakReference<?>> weakReferences = private final Set<WeakReference<?>> weakReferences =
Collections.newSetFromMap(new ConcurrentHashMap<WeakReference<?>, Boolean>()); Collections.newSetFromMap(new ConcurrentHashMap<WeakReference<?>, Boolean>());
@ -41,9 +48,27 @@ public class PendingTrace extends ConcurrentLinkedDeque<DDSpan> {
PendingTrace(final DDTracer tracer, final long traceId) { PendingTrace(final DDTracer tracer, final long traceId) {
this.tracer = tracer; this.tracer = tracer;
this.traceId = traceId; this.traceId = traceId;
this.startTimeNano = Clock.currentNanoTime();
this.startNanoTicks = Clock.currentNanoTicks();
SPAN_CLEANER.pendingTraces.add(this); SPAN_CLEANER.pendingTraces.add(this);
} }
/**
* Current timestamp in nanoseconds.
*
* <p>Note: it is not possible to get 'real' nanosecond time. This method uses trace start time
* (which has millisecond precision) as a reference and it gets time with nanosecond precision
* after that. This means time measured within same Trace in different Spans is relatively correct
* with nanosecond precision.
*
* @return timestamp in nanoseconds
*/
public long getCurrentTimeNano() {
return startTimeNano + Math.max(0, Clock.currentNanoTicks() - startNanoTicks);
}
public void registerSpan(final DDSpan span) { public void registerSpan(final DDSpan span) {
if (span.context().getTraceId() != traceId) { if (span.context().getTraceId() != traceId) {
log.debug("{} - span registered for wrong trace ({})", span, traceId); log.debug("{} - span registered for wrong trace ({})", span, traceId);

View File

@ -26,7 +26,7 @@ public class Clock {
* *
* @return The current nanos ticks * @return The current nanos ticks
*/ */
public static synchronized long currentNanoTicks() { public static long currentNanoTicks() {
return System.nanoTime(); return System.nanoTime();
} }
@ -35,7 +35,17 @@ public class Clock {
* *
* @return the current epoch time in micros * @return the current epoch time in micros
*/ */
public static synchronized long currentMicroTime() { public static long currentMicroTime() {
return TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); return TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
} }
/**
* Get the current time in nanos. The actual precision is the millis Note: this will overflow in
* ~290 years after epoch
*
* @return the current epoch time in nanos
*/
public static long currentNanoTime() {
return TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis());
}
} }

View File

@ -103,6 +103,8 @@ class DDSpanTest extends Specification {
def total = System.nanoTime() - start def total = System.nanoTime() - start
expect: expect:
// Generous 5 seconds to execute this test
Math.abs(TimeUnit.NANOSECONDS.toSeconds(span.startTime) - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())) < 5
span.durationNano > betweenDur span.durationNano > betweenDur
span.durationNano < total span.durationNano < total
span.durationNano % mod > 0 // Very slim chance of a false negative. span.durationNano % mod > 0 // Very slim chance of a false negative.
@ -121,6 +123,8 @@ class DDSpanTest extends Specification {
def total = Math.max(1, System.currentTimeMillis() - start) def total = Math.max(1, System.currentTimeMillis() - start)
expect: expect:
// Generous 5 seconds to execute this test
Math.abs(TimeUnit.NANOSECONDS.toSeconds(span.startTime) - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())) < 5
span.durationNano >= TimeUnit.MILLISECONDS.toNanos(betweenDur) span.durationNano >= TimeUnit.MILLISECONDS.toNanos(betweenDur)
span.durationNano <= TimeUnit.MILLISECONDS.toNanos(total) span.durationNano <= TimeUnit.MILLISECONDS.toNanos(total)
span.durationNano % mod == 0 || span.durationNano == 1 span.durationNano % mod == 0 || span.durationNano == 1
@ -138,6 +142,8 @@ class DDSpanTest extends Specification {
def total = System.currentTimeMillis() - start + 1 def total = System.currentTimeMillis() - start + 1
expect: expect:
// Generous 5 seconds to execute this test
Math.abs(TimeUnit.NANOSECONDS.toSeconds(span.startTime) - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())) < 5
span.durationNano >= TimeUnit.MILLISECONDS.toNanos(betweenDur) span.durationNano >= TimeUnit.MILLISECONDS.toNanos(betweenDur)
span.durationNano <= TimeUnit.MILLISECONDS.toNanos(total) span.durationNano <= TimeUnit.MILLISECONDS.toNanos(total)
span.durationNano % mod == 0 span.durationNano % mod == 0

View File

@ -4,6 +4,8 @@ import datadog.trace.common.writer.ListWriter
import spock.lang.Specification import spock.lang.Specification
import spock.lang.Subject import spock.lang.Subject
import java.util.concurrent.TimeUnit
class PendingTraceTest extends Specification { class PendingTraceTest extends Specification {
def writer = new ListWriter() def writer = new ListWriter()
@ -150,7 +152,7 @@ class PendingTraceTest extends Specification {
} }
def "child spans created after trace written" () { def "child spans created after trace written"() {
setup: setup:
rootSpan.finish() rootSpan.finish()
// this shouldn't happen, but it's possible users of the api // this shouldn't happen, but it's possible users of the api
@ -164,4 +166,10 @@ class PendingTraceTest extends Specification {
trace.asList() == [rootSpan] trace.asList() == [rootSpan]
writer == [[rootSpan]] writer == [[rootSpan]]
} }
def "test getCurrentTimeNano"() {
expect:
// Generous 5 seconds to execute this test
Math.abs(TimeUnit.NANOSECONDS.toSeconds(trace.currentTimeNano) - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())) < 5
}
} }