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 */
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;
/** 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;
/**
@ -54,19 +61,19 @@ public class DDSpan implements Span, MutableSpan {
* @param context the context used for the span
*/
DDSpan(final long timestampMicro, final DDSpanContext context) {
this.context = context;
// record the start time in nano (current milli + nano delta)
if (timestampMicro <= 0L) {
this.startTimeMicro = Clock.currentMicroTime();
this.startTimeNano = Clock.currentNanoTicks();
// record the start time
startTimeMicro = Clock.currentMicroTime();
startTimeNano = context.getTrace().getCurrentTimeNano();
} else {
this.startTimeMicro = timestampMicro;
// timestamp might have come from an external clock, so don't bother with nanotime.
this.startTimeNano = 0;
startTimeMicro = timestampMicro;
// Timestamp have come from an external clock, so use startTimeNano as a flag
startTimeNano = 0;
}
this.context.getTrace().registerSpan(this);
context.getTrace().registerSpan(this);
}
@JsonIgnore
@ -74,16 +81,20 @@ public class DDSpan implements Span, MutableSpan {
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
public final void finish() {
if (startTimeNano != 0) {
// 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))) {
context.getTrace().addSpan(this);
} else {
log.debug("{} - already finished!", this);
}
if (startTimeNano > 0) {
// no external clock was used, so we can rely on nano time
finishAndAddToTrace(context.getTrace().getCurrentTimeNano() - startTimeNano);
} else {
finish(Clock.currentMicroTime());
}
@ -91,13 +102,7 @@ public class DDSpan implements Span, MutableSpan {
@Override
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.
if (this.durationNano.compareAndSet(
0, Math.max(1, TimeUnit.MICROSECONDS.toNanos(stoptimeMicros - this.startTimeMicro)))) {
context.getTrace().addSpan(this);
} else {
log.debug("{} - already finished!", this);
}
finishAndAddToTrace(TimeUnit.MICROSECONDS.toNanos(stoptimeMicros - startTimeMicro));
}
/**
@ -288,7 +293,7 @@ public class DDSpan implements Span, MutableSpan {
@Override
@JsonGetter("start")
public long getStartTime() {
return startTimeMicro * 1000L;
return startTimeNano > 0 ? startTimeNano : TimeUnit.MICROSECONDS.toNanos(startTimeMicro);
}
@Override

View File

@ -1,6 +1,7 @@
package datadog.opentracing;
import datadog.opentracing.scopemanager.ContinuableScope;
import datadog.trace.common.util.Clock;
import java.io.Closeable;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
@ -29,6 +30,12 @@ public class PendingTrace extends ConcurrentLinkedDeque<DDSpan> {
private final DDTracer tracer;
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 Set<WeakReference<?>> weakReferences =
Collections.newSetFromMap(new ConcurrentHashMap<WeakReference<?>, Boolean>());
@ -41,9 +48,27 @@ public class PendingTrace extends ConcurrentLinkedDeque<DDSpan> {
PendingTrace(final DDTracer tracer, final long traceId) {
this.tracer = tracer;
this.traceId = traceId;
this.startTimeNano = Clock.currentNanoTime();
this.startNanoTicks = Clock.currentNanoTicks();
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) {
if (span.context().getTraceId() != traceId) {
log.debug("{} - span registered for wrong trace ({})", span, traceId);

View File

@ -26,7 +26,7 @@ public class Clock {
*
* @return The current nanos ticks
*/
public static synchronized long currentNanoTicks() {
public static long currentNanoTicks() {
return System.nanoTime();
}
@ -35,7 +35,17 @@ public class Clock {
*
* @return the current epoch time in micros
*/
public static synchronized long currentMicroTime() {
public static long currentMicroTime() {
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
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 < total
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)
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(total)
span.durationNano % mod == 0 || span.durationNano == 1
@ -138,6 +142,8 @@ class DDSpanTest extends Specification {
def total = System.currentTimeMillis() - start + 1
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(total)
span.durationNano % mod == 0

View File

@ -4,6 +4,8 @@ import datadog.trace.common.writer.ListWriter
import spock.lang.Specification
import spock.lang.Subject
import java.util.concurrent.TimeUnit
class PendingTraceTest extends Specification {
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:
rootSpan.finish()
// this shouldn't happen, but it's possible users of the api
@ -164,4 +166,10 @@ class PendingTraceTest extends Specification {
trace.asList() == [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
}
}