in apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/SpanImpl.java [303:383]
protected void afterEnd() {
// capture stack trace when the span ends, relies on this method being called synchronously from the instrumentation
long spanStackTraceMinDurationMs = stacktraceConfiguration.getSpanStackTraceMinDurationMs();
if (spanStackTraceMinDurationMs >= 0 && isSampled() && stackFrames == null) {
if (getDurationMs() >= spanStackTraceMinDurationMs) {
this.stacktrace = new Throwable();
}
}
// Why do we increment references of this span here?
// The only thing preventing the "this"-span from being recycled is the initial reference increment in onAfterStart()
// There are multiple ways in afterEnd() on how this reference may be decremented and therefore potentially causing recycling:
// - we call tracer.endSpan() for this span and the span is dropped / not reported for some reason
// - we call tracer.endSpan() for this span and the span is reported and released afterwards (=> recycled on the reporter thread!)
// - we successfully set parent.bufferedSpan to "this".
// - a span on a different thread with the same parent can now call tracer.endSpan() for parent.bufferedSpan (=this span)
// - the parent span is ended on a different thread and calls tracer.endSpan() for parent.bufferedSpan (=this span)
// By incrementing the reference count here, we guarantee that the "this" span is only recycled AFTER we decrement the reference count again
this.incrementReferences();
try {
if (transaction != null && transaction.isSpanCompressionEnabled() && parent != null) {
SpanImpl parentBuffered = parent.bufferedSpan.incrementReferencesAndGet();
try {
//per the reference, if it is not compression-eligible or if its parent has already ended, it is reported immediately
if (parent.isFinished() || !isCompressionEligible()) {
if (parentBuffered != null) {
if (parent.bufferedSpan.compareAndSet(parentBuffered, null)) {
this.tracer.endSpan(parentBuffered);
logger.trace("parent span compression buffer was set to null and {} was ended", parentBuffered);
}
}
this.tracer.endSpan(this);
return;
}
//since it wasn't reported, this span gets buffered
if (parentBuffered == null) {
if (!parent.bufferedSpan.compareAndSet(null, this)) {
// the failed update would ideally lead to a compression attempt with the new buffer,
// but we're dropping the compression attempt to keep things simple and avoid looping so this stays wait-free
// this doesn't exactly diverge from the spec, but it can lead to non-optimal compression under high load
this.tracer.endSpan(this);
} else {
logger.trace("parent span compression buffer was set to {}", this);
}
return;
}
//still trying to buffer this span
if (!parentBuffered.tryToCompress(this)) {
// we couldn't compress so replace the buffer with this
if (parent.bufferedSpan.compareAndSet(parentBuffered, this)) {
this.tracer.endSpan(parentBuffered);
logger.trace("parent span compression buffer was set to {} and {} was ended", this, parentBuffered);
} else {
// the failed update would ideally lead to a compression attempt with the new buffer,
// but we're dropping the compression attempt to keep things simple and avoid looping so this stays wait-free
// this doesn't exactly diverge from the spec, but it can lead to non-optimal compression under high load
this.tracer.endSpan(this);
}
} else {
if (isSampled() && transaction != null) {
transaction.getSpanCount().getDropped().incrementAndGet();
}
//drop the span by removing the reference allocated in onAfterStart() because it has been compressed
decrementReferences();
}
} finally {
if (parentBuffered != null) {
parentBuffered.decrementReferences();
}
}
} else {
this.tracer.endSpan(this);
}
} finally {
if (parent != null) {
//this needs to happen before this.decrementReferences(), because otherwise "this" might have been recycled already
parent.decrementReferences();
}
this.decrementReferences();
}
}