Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent cycles in inferred spans #3588

Merged
merged 3 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:

=== Unreleased

[float]
===== Bug fixes
* Fixed edge case where inferred spans could cause cycles in the trace parent-child relationships, subsequently resulting in the UI crashing - {pull}3588[#3588]

[[release-notes-1.x]]
=== Java Agent version 1.x

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public class TraceContext implements Recyclable, co.elastic.apm.agent.tracer.Tra
public static final String ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME = "elastic-apm-traceparent";
public static final String W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME = "traceparent";
public static final String TRACESTATE_HEADER_NAME = "tracestate";
public static final int SERIALIZED_LENGTH = 42;
public static final int SERIALIZED_LENGTH = 51;
private static final int TEXT_HEADER_EXPECTED_LENGTH = 55;
private static final int TEXT_HEADER_TRACE_ID_OFFSET = 3;
private static final int TEXT_HEADER_PARENT_ID_OFFSET = 36;
Expand Down Expand Up @@ -227,6 +227,7 @@ private TraceContext(ElasticApmTracer tracer, Id id) {
* <p>
* Note: the {@link #traceId} will still be 128 bit
* </p>
*
* @param tracer a valid tracer
*/
public static TraceContext with64BitId(ElasticApmTracer tracer) {
Expand Down Expand Up @@ -621,7 +622,7 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
TraceContext that = (TraceContext) o;
return id.equals(that.id) &&
traceId.equals(that.traceId);
traceId.equals(that.traceId);
}

public boolean idEquals(@Nullable TraceContext o) {
Expand Down Expand Up @@ -650,6 +651,8 @@ public void serialize(byte[] buffer) {
offset = traceId.toBytes(buffer, offset);
offset = id.toBytes(buffer, offset);
offset = transactionId.toBytes(buffer, offset);
buffer[offset++] = parentId.isEmpty() ? (byte) 0 : (byte) 1;
offset = parentId.toBytes(buffer, offset);
buffer[offset++] = flags;
buffer[offset++] = (byte) (discardable ? 1 : 0);
ByteUtils.putLong(buffer, offset, clock.getOffset());
Expand All @@ -660,6 +663,12 @@ public void deserialize(byte[] buffer, @Nullable String serviceName, @Nullable S
offset += traceId.fromBytes(buffer, offset);
offset += id.fromBytes(buffer, offset);
offset += transactionId.fromBytes(buffer, offset);
if (buffer[offset++] != 0) {
offset += parentId.fromBytes(buffer, offset);
} else {
parentId.resetState();
offset += 8;
}
flags = buffer[offset++];
discardable = buffer[offset++] == (byte) 1;
clock.init(ByteUtils.getLong(buffer, offset));
Expand All @@ -672,6 +681,10 @@ public static long getSpanId(byte[] serializedTraceContext) {
return ByteUtils.getLong(serializedTraceContext, 16);
}

public static long getParentId(byte[] serializedTraceContext) {
return ByteUtils.getLong(serializedTraceContext, 33);
}

public boolean traceIdAndIdEquals(byte[] serialized) {
return id.dataEquals(serialized, traceId.getLength()) && traceId.dataEquals(serialized, 0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ public class CallTree implements Recyclable {
* @see co.elastic.apm.agent.impl.transaction.AbstractSpan#childIds
*/
@Nullable
private LongList childIds;
private ChildList childIds;
@Nullable
private LongList maybeChildIds;
private ChildList maybeChildIds;

public CallTree() {
}
Expand Down Expand Up @@ -375,28 +375,29 @@ private void toString(Appendable out, int level) throws IOException {
}
}

int spanify(CallTree.Root root, TraceContext parentContext) {
int spanify(CallTree.Root root, TraceContext parentContext, TraceContext nonInferredParentContext) {
int createdSpans = 0;
if (activeContextOfDirectParent != null) {
parentContext = activeContextOfDirectParent;
nonInferredParentContext = activeContextOfDirectParent;
}
Span span = null;
if (!isPillar() || isLeaf()) {
createdSpans++;
span = asSpan(root, parentContext);
span = asSpan(root, parentContext, nonInferredParentContext);
this.isSpan = true;
}
List<CallTree> children = getChildren();
for (int i = 0, size = children.size(); i < size; i++) {
createdSpans += children.get(i).spanify(root, span != null ? span.getTraceContext() : parentContext);
createdSpans += children.get(i).spanify(root, span != null ? span.getTraceContext() : parentContext, nonInferredParentContext);
}
if (span != null) {
span.end(span.getTimestamp() + getDurationUs());
}
return createdSpans;
}

protected Span asSpan(Root root, TraceContext parentContext) {
protected Span asSpan(Root root, TraceContext parentContext, TraceContext nonInferredParentContext) {
transferMaybeChildIdsToChildIds();
Span span = parentContext.createSpan(root.getEpochMicros(this.start))
.withType("app")
Expand All @@ -410,7 +411,21 @@ protected Span asSpan(Root root, TraceContext parentContext) {
}
span.appendToName("#");
span.appendToName(frame.getMethodName());
span.withChildIds(childIds);

LongList childSpanIds = null;
if (childIds != null) {
long expectedParent = nonInferredParentContext.getId().readLong(0);
childSpanIds = new LongList(childIds.getSize());
for (int i = 0; i < childIds.getSize(); i++) {
// to avoid cycles, we only insert child-ids if the parent of the child is also
// the parent of the stack of inferred spans inserted
if (childIds.getParentId(i) == expectedParent) {
childSpanIds.add(childIds.getId(i));
}
}
}

span.withChildIds(childSpanIds);

// we're not interested in the very bottom of the stack which contains things like accepting and handling connections
if (!root.rootContext.idEquals(parentContext)) {
Expand Down Expand Up @@ -498,19 +513,20 @@ public void resetState() {
* </p>
*
* @param id the child span id to add to this call tree element
* @param parentId the parent id of the span represented by the id parameter
*/
public void addMaybeChildId(long id) {
public void addMaybeChildId(long id, long parentId) {
if (maybeChildIds == null) {
maybeChildIds = new LongList();
maybeChildIds = new ChildList();
}
maybeChildIds.add(id);
maybeChildIds.add(id, parentId);
}

public void addChildId(long id) {
public void addChildId(long id, long parentId) {
if (childIds == null) {
childIds = new LongList();
childIds = new ChildList();
}
childIds.add(id);
childIds.add(id, parentId);
}

public boolean hasChildIds() {
Expand Down Expand Up @@ -541,7 +557,11 @@ void giveChildIdsTo(CallTree giveTo) {

void giveLastChildIdTo(CallTree giveTo) {
if (childIds != null && !childIds.isEmpty()) {
giveTo.addChildId(childIds.remove(childIds.getSize() - 1));
int size = childIds.getSize();
long id = childIds.getId(size - 1);
long parentId = childIds.getParentId(size - 1);
giveTo.addChildId(id, parentId);
childIds.removeLast();
}
}

Expand Down Expand Up @@ -619,7 +639,7 @@ public void onActivation(byte[] active, long timestamp) {
long spanId = TraceContext.getSpanId(active);
activeSet.add(spanId);
if (!isNestedActivation(topOfStack)) {
topOfStack.addMaybeChildId(spanId);
topOfStack.addMaybeChildId(spanId, TraceContext.getParentId(active));
}
}
}
Expand All @@ -628,12 +648,12 @@ private boolean isNestedActivation(CallTree topOfStack) {
return isAnyActive(topOfStack.childIds) || isAnyActive(topOfStack.maybeChildIds);
}

private boolean isAnyActive(@Nullable LongList spanIds) {
private boolean isAnyActive(@Nullable ChildList spanIds) {
if (spanIds == null) {
return false;
}
for (int i = 0, size = spanIds.getSize(); i < size; i++) {
if (activeSet.contains(spanIds.get(i))) {
if (activeSet.contains(spanIds.getId(i))) {
return true;
}
}
Expand Down Expand Up @@ -719,7 +739,7 @@ public int spanify() {
int createdSpans = 0;
List<CallTree> callTrees = getChildren();
for (int i = 0, size = callTrees.size(); i < size; i++) {
createdSpans += callTrees.get(i).spanify(this, rootContext);
createdSpans += callTrees.get(i).spanify(this, rootContext, rootContext);
}
return createdSpans;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.apm.agent.profiler;


import co.elastic.apm.agent.sdk.internal.collections.LongList;

/** List for maintaining pairs of (spanId,parentIds) both represented as longs. */
public class ChildList {

// this list contains the (spanId,parentIds) flattened
private final LongList idsWithParentIds = new LongList();

public void add(long id, long parentId) {
idsWithParentIds.add(id);
idsWithParentIds.add(parentId);
}

public long getId(int index) {
return idsWithParentIds.get(index * 2);
}

public long getParentId(int index) {
return idsWithParentIds.get(index * 2 + 1);
}

public int getSize() {
return idsWithParentIds.getSize() / 2;
}

public void addAll(ChildList other) {
idsWithParentIds.addAll(other.idsWithParentIds);
}

public void clear() {
idsWithParentIds.clear();
}

public boolean isEmpty() {
return getSize() == 0;
}

public void removeLast() {
int size = idsWithParentIds.getSize();
idsWithParentIds.remove(size - 1);
idsWithParentIds.remove(size - 2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
* and at least one stack trace.
* Once {@linkplain ActivationEvent#handleDeactivationEvent(SamplingProfiler) handling the deactivation event} of the root span in a thread
* (after which {@link ElasticApmTracer#getActive()} would return {@code null}),
* the {@link CallTree} is {@linkplain CallTree#spanify(CallTree.Root, TraceContext) converted into regular spans}.
* the {@link CallTree} is {@linkplain CallTree#spanify(CallTree.Root, TraceContext, TraceContext) converted into regular spans}.
* </p>
* <p>
* Overall, the allocation rate does not depend on the number of {@link ActivationEvent}s but only on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.stagemonitor.configuration.ConfigurationRegistry;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -141,8 +142,62 @@ void testCallTreeWithActiveSpan() {
root.spanify();

assertThat(reporter.getSpans()).hasSize(2);
assertThat(reporter.getSpans().get(1).getTraceContext().isChildOf(spanContext));
assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(rootContext));
assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(spanContext)).isTrue();
assertThat(reporter.getSpans().get(1).getTraceContext().isChildOf(rootContext)).isTrue();
}


@Test
void testSpanWithInvertedActivation() {
TraceContext rootContext = CallTreeTest.rootTraceContext(tracer);

TraceContext childSpanContext = TraceContext.with64BitId(tracer);
TraceContext.fromParentContext().asChildOf(childSpanContext, rootContext);

NoopObjectPool<CallTree.Root> rootPool = NoopObjectPool.ofRecyclable(() -> new CallTree.Root(tracer));
NoopObjectPool<CallTree> childPool = NoopObjectPool.ofRecyclable(CallTree::new);

CallTree.Root root = CallTree.createRoot(rootPool, childSpanContext.serialize(), rootContext.getServiceName(), rootContext.getServiceVersion(), 0);
root.addStackTrace(tracer, Collections.singletonList(StackFrame.of("A", "a")), 10_000, childPool, 0);

root.onActivation(rootContext.serialize(), 20_000);
root.onDeactivation(rootContext.serialize(), childSpanContext.serialize(), 30_000);

root.addStackTrace(tracer, Collections.singletonList(StackFrame.of("A", "a")), 40_000, childPool, 0);
root.end(childPool, 0);

root.spanify();

assertThat(reporter.getSpans()).hasSize(1);
assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(childSpanContext)).isTrue();
// the inferred span should not have any span links because this
// span link would cause a cycle in the trace
assertThat(reporter.getSpans().get(0).getChildIds().getSize()).isEqualTo(0L);
}

@Test
void testSpanWithNestedActivation() {
TraceContext rootContext = CallTreeTest.rootTraceContext(tracer);

NoopObjectPool<CallTree.Root> rootPool = NoopObjectPool.ofRecyclable(() -> new CallTree.Root(tracer));
NoopObjectPool<CallTree> childPool = NoopObjectPool.ofRecyclable(CallTree::new);

CallTree.Root root = CallTree.createRoot(rootPool, rootContext.serialize(), rootContext.getServiceName(), rootContext.getServiceVersion(), 0);
root.addStackTrace(tracer, Collections.singletonList(StackFrame.of("A", "a")), 10_000, childPool, 0);

root.onActivation(rootContext.serialize(), 20_000);
root.onDeactivation(rootContext.serialize(), rootContext.serialize(), 30_000);

root.addStackTrace(tracer, Collections.singletonList(StackFrame.of("A", "a")), 40_000, childPool, 0);
root.end(childPool, 0);

root.spanify();

assertThat(reporter.getSpans()).hasSize(1);
assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(rootContext)).isTrue();
// the inferred span should not have any span links because this
// span link would cause a cycle in the trace
assertThat(reporter.getSpans().get(0).getChildIds().getSize()).isEqualTo(0L);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ void testCallTree() {
@Test
void testGiveEmptyChildIdsTo() {
CallTree rich = new CallTree();
rich.addChildId(42);
rich.addChildId(42, 0L);
CallTree robinHood = new CallTree();
CallTree poor = new CallTree();

Expand Down
Loading