protected void afterEnd()

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();
        }
    }