Skip to content

Commit d63b1cc

Browse files
authored
Prevent cycles in inferred spans (#3588)
1 parent 750ba4b commit d63b1cc

File tree

7 files changed

+180
-24
lines changed

7 files changed

+180
-24
lines changed

CHANGELOG.asciidoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:
3131
3232
=== Unreleased
3333
34+
[float]
35+
===== Bug fixes
36+
* Fixed edge case where inferred spans could cause cycles in the trace parent-child relationships, subsequently resulting in the UI crashing - {pull}3588[#3588]
37+
3438
[[release-notes-1.x]]
3539
=== Java Agent version 1.x
3640

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public class TraceContext implements Recyclable, co.elastic.apm.agent.tracer.Tra
6464
public static final String ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME = "elastic-apm-traceparent";
6565
public static final String W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME = "traceparent";
6666
public static final String TRACESTATE_HEADER_NAME = "tracestate";
67-
public static final int SERIALIZED_LENGTH = 42;
67+
public static final int SERIALIZED_LENGTH = 51;
6868
private static final int TEXT_HEADER_EXPECTED_LENGTH = 55;
6969
private static final int TEXT_HEADER_TRACE_ID_OFFSET = 3;
7070
private static final int TEXT_HEADER_PARENT_ID_OFFSET = 36;
@@ -227,6 +227,7 @@ private TraceContext(ElasticApmTracer tracer, Id id) {
227227
* <p>
228228
* Note: the {@link #traceId} will still be 128 bit
229229
* </p>
230+
*
230231
* @param tracer a valid tracer
231232
*/
232233
public static TraceContext with64BitId(ElasticApmTracer tracer) {
@@ -621,7 +622,7 @@ public boolean equals(Object o) {
621622
if (o == null || getClass() != o.getClass()) return false;
622623
TraceContext that = (TraceContext) o;
623624
return id.equals(that.id) &&
624-
traceId.equals(that.traceId);
625+
traceId.equals(that.traceId);
625626
}
626627

627628
public boolean idEquals(@Nullable TraceContext o) {
@@ -650,6 +651,8 @@ public void serialize(byte[] buffer) {
650651
offset = traceId.toBytes(buffer, offset);
651652
offset = id.toBytes(buffer, offset);
652653
offset = transactionId.toBytes(buffer, offset);
654+
buffer[offset++] = parentId.isEmpty() ? (byte) 0 : (byte) 1;
655+
offset = parentId.toBytes(buffer, offset);
653656
buffer[offset++] = flags;
654657
buffer[offset++] = (byte) (discardable ? 1 : 0);
655658
ByteUtils.putLong(buffer, offset, clock.getOffset());
@@ -660,6 +663,12 @@ public void deserialize(byte[] buffer, @Nullable String serviceName, @Nullable S
660663
offset += traceId.fromBytes(buffer, offset);
661664
offset += id.fromBytes(buffer, offset);
662665
offset += transactionId.fromBytes(buffer, offset);
666+
if (buffer[offset++] != 0) {
667+
offset += parentId.fromBytes(buffer, offset);
668+
} else {
669+
parentId.resetState();
670+
offset += 8;
671+
}
663672
flags = buffer[offset++];
664673
discardable = buffer[offset++] == (byte) 1;
665674
clock.init(ByteUtils.getLong(buffer, offset));
@@ -672,6 +681,10 @@ public static long getSpanId(byte[] serializedTraceContext) {
672681
return ByteUtils.getLong(serializedTraceContext, 16);
673682
}
674683

684+
public static long getParentId(byte[] serializedTraceContext) {
685+
return ByteUtils.getLong(serializedTraceContext, 33);
686+
}
687+
675688
public boolean traceIdAndIdEquals(byte[] serialized) {
676689
return id.dataEquals(serialized, traceId.getLength()) && traceId.dataEquals(serialized, 0);
677690
}

apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/CallTree.java

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ public class CallTree implements Recyclable {
7979
* @see co.elastic.apm.agent.impl.transaction.AbstractSpan#childIds
8080
*/
8181
@Nullable
82-
private LongList childIds;
82+
private ChildList childIds;
8383
@Nullable
84-
private LongList maybeChildIds;
84+
private ChildList maybeChildIds;
8585

8686
public CallTree() {
8787
}
@@ -375,28 +375,29 @@ private void toString(Appendable out, int level) throws IOException {
375375
}
376376
}
377377

378-
int spanify(CallTree.Root root, TraceContext parentContext) {
378+
int spanify(CallTree.Root root, TraceContext parentContext, TraceContext nonInferredParentContext) {
379379
int createdSpans = 0;
380380
if (activeContextOfDirectParent != null) {
381381
parentContext = activeContextOfDirectParent;
382+
nonInferredParentContext = activeContextOfDirectParent;
382383
}
383384
Span span = null;
384385
if (!isPillar() || isLeaf()) {
385386
createdSpans++;
386-
span = asSpan(root, parentContext);
387+
span = asSpan(root, parentContext, nonInferredParentContext);
387388
this.isSpan = true;
388389
}
389390
List<CallTree> children = getChildren();
390391
for (int i = 0, size = children.size(); i < size; i++) {
391-
createdSpans += children.get(i).spanify(root, span != null ? span.getTraceContext() : parentContext);
392+
createdSpans += children.get(i).spanify(root, span != null ? span.getTraceContext() : parentContext, nonInferredParentContext);
392393
}
393394
if (span != null) {
394395
span.end(span.getTimestamp() + getDurationUs());
395396
}
396397
return createdSpans;
397398
}
398399

399-
protected Span asSpan(Root root, TraceContext parentContext) {
400+
protected Span asSpan(Root root, TraceContext parentContext, TraceContext nonInferredParentContext) {
400401
transferMaybeChildIdsToChildIds();
401402
Span span = parentContext.createSpan(root.getEpochMicros(this.start))
402403
.withType("app")
@@ -410,7 +411,21 @@ protected Span asSpan(Root root, TraceContext parentContext) {
410411
}
411412
span.appendToName("#");
412413
span.appendToName(frame.getMethodName());
413-
span.withChildIds(childIds);
414+
415+
LongList childSpanIds = null;
416+
if (childIds != null) {
417+
long expectedParent = nonInferredParentContext.getId().readLong(0);
418+
childSpanIds = new LongList(childIds.getSize());
419+
for (int i = 0; i < childIds.getSize(); i++) {
420+
// to avoid cycles, we only insert child-ids if the parent of the child is also
421+
// the parent of the stack of inferred spans inserted
422+
if (childIds.getParentId(i) == expectedParent) {
423+
childSpanIds.add(childIds.getId(i));
424+
}
425+
}
426+
}
427+
428+
span.withChildIds(childSpanIds);
414429

415430
// we're not interested in the very bottom of the stack which contains things like accepting and handling connections
416431
if (!root.rootContext.idEquals(parentContext)) {
@@ -498,19 +513,20 @@ public void resetState() {
498513
* </p>
499514
*
500515
* @param id the child span id to add to this call tree element
516+
* @param parentId the parent id of the span represented by the id parameter
501517
*/
502-
public void addMaybeChildId(long id) {
518+
public void addMaybeChildId(long id, long parentId) {
503519
if (maybeChildIds == null) {
504-
maybeChildIds = new LongList();
520+
maybeChildIds = new ChildList();
505521
}
506-
maybeChildIds.add(id);
522+
maybeChildIds.add(id, parentId);
507523
}
508524

509-
public void addChildId(long id) {
525+
public void addChildId(long id, long parentId) {
510526
if (childIds == null) {
511-
childIds = new LongList();
527+
childIds = new ChildList();
512528
}
513-
childIds.add(id);
529+
childIds.add(id, parentId);
514530
}
515531

516532
public boolean hasChildIds() {
@@ -541,7 +557,11 @@ void giveChildIdsTo(CallTree giveTo) {
541557

542558
void giveLastChildIdTo(CallTree giveTo) {
543559
if (childIds != null && !childIds.isEmpty()) {
544-
giveTo.addChildId(childIds.remove(childIds.getSize() - 1));
560+
int size = childIds.getSize();
561+
long id = childIds.getId(size - 1);
562+
long parentId = childIds.getParentId(size - 1);
563+
giveTo.addChildId(id, parentId);
564+
childIds.removeLast();
545565
}
546566
}
547567

@@ -619,7 +639,7 @@ public void onActivation(byte[] active, long timestamp) {
619639
long spanId = TraceContext.getSpanId(active);
620640
activeSet.add(spanId);
621641
if (!isNestedActivation(topOfStack)) {
622-
topOfStack.addMaybeChildId(spanId);
642+
topOfStack.addMaybeChildId(spanId, TraceContext.getParentId(active));
623643
}
624644
}
625645
}
@@ -628,12 +648,12 @@ private boolean isNestedActivation(CallTree topOfStack) {
628648
return isAnyActive(topOfStack.childIds) || isAnyActive(topOfStack.maybeChildIds);
629649
}
630650

631-
private boolean isAnyActive(@Nullable LongList spanIds) {
651+
private boolean isAnyActive(@Nullable ChildList spanIds) {
632652
if (spanIds == null) {
633653
return false;
634654
}
635655
for (int i = 0, size = spanIds.getSize(); i < size; i++) {
636-
if (activeSet.contains(spanIds.get(i))) {
656+
if (activeSet.contains(spanIds.getId(i))) {
637657
return true;
638658
}
639659
}
@@ -719,7 +739,7 @@ public int spanify() {
719739
int createdSpans = 0;
720740
List<CallTree> callTrees = getChildren();
721741
for (int i = 0, size = callTrees.size(); i < size; i++) {
722-
createdSpans += callTrees.get(i).spanify(this, rootContext);
742+
createdSpans += callTrees.get(i).spanify(this, rootContext, rootContext);
723743
}
724744
return createdSpans;
725745
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.profiler;
20+
21+
22+
import co.elastic.apm.agent.sdk.internal.collections.LongList;
23+
24+
/** List for maintaining pairs of (spanId,parentIds) both represented as longs. */
25+
public class ChildList {
26+
27+
// this list contains the (spanId,parentIds) flattened
28+
private final LongList idsWithParentIds = new LongList();
29+
30+
public void add(long id, long parentId) {
31+
idsWithParentIds.add(id);
32+
idsWithParentIds.add(parentId);
33+
}
34+
35+
public long getId(int index) {
36+
return idsWithParentIds.get(index * 2);
37+
}
38+
39+
public long getParentId(int index) {
40+
return idsWithParentIds.get(index * 2 + 1);
41+
}
42+
43+
public int getSize() {
44+
return idsWithParentIds.getSize() / 2;
45+
}
46+
47+
public void addAll(ChildList other) {
48+
idsWithParentIds.addAll(other.idsWithParentIds);
49+
}
50+
51+
public void clear() {
52+
idsWithParentIds.clear();
53+
}
54+
55+
public boolean isEmpty() {
56+
return getSize() == 0;
57+
}
58+
59+
public void removeLast() {
60+
int size = idsWithParentIds.getSize();
61+
idsWithParentIds.remove(size - 1);
62+
idsWithParentIds.remove(size - 2);
63+
}
64+
}

apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/SamplingProfiler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
* and at least one stack trace.
107107
* Once {@linkplain ActivationEvent#handleDeactivationEvent(SamplingProfiler) handling the deactivation event} of the root span in a thread
108108
* (after which {@link ElasticApmTracer#getActive()} would return {@code null}),
109-
* the {@link CallTree} is {@linkplain CallTree#spanify(CallTree.Root, TraceContext) converted into regular spans}.
109+
* the {@link CallTree} is {@linkplain CallTree#spanify(CallTree.Root, TraceContext, TraceContext) converted into regular spans}.
110110
* </p>
111111
* <p>
112112
* Overall, the allocation rate does not depend on the number of {@link ActivationEvent}s but only on

apm-agent-plugins/apm-profiling-plugin/src/test/java/co/elastic/apm/agent/profiler/CallTreeSpanifyTest.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.stagemonitor.configuration.ConfigurationRegistry;
3636

3737
import java.io.IOException;
38+
import java.util.Collections;
3839
import java.util.List;
3940
import java.util.Objects;
4041
import java.util.concurrent.TimeUnit;
@@ -141,8 +142,62 @@ void testCallTreeWithActiveSpan() {
141142
root.spanify();
142143

143144
assertThat(reporter.getSpans()).hasSize(2);
144-
assertThat(reporter.getSpans().get(1).getTraceContext().isChildOf(spanContext));
145-
assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(rootContext));
145+
assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(spanContext)).isTrue();
146+
assertThat(reporter.getSpans().get(1).getTraceContext().isChildOf(rootContext)).isTrue();
147+
}
148+
149+
150+
@Test
151+
void testSpanWithInvertedActivation() {
152+
TraceContext rootContext = CallTreeTest.rootTraceContext(tracer);
153+
154+
TraceContext childSpanContext = TraceContext.with64BitId(tracer);
155+
TraceContext.fromParentContext().asChildOf(childSpanContext, rootContext);
156+
157+
NoopObjectPool<CallTree.Root> rootPool = NoopObjectPool.ofRecyclable(() -> new CallTree.Root(tracer));
158+
NoopObjectPool<CallTree> childPool = NoopObjectPool.ofRecyclable(CallTree::new);
159+
160+
CallTree.Root root = CallTree.createRoot(rootPool, childSpanContext.serialize(), rootContext.getServiceName(), rootContext.getServiceVersion(), 0);
161+
root.addStackTrace(tracer, Collections.singletonList(StackFrame.of("A", "a")), 10_000, childPool, 0);
162+
163+
root.onActivation(rootContext.serialize(), 20_000);
164+
root.onDeactivation(rootContext.serialize(), childSpanContext.serialize(), 30_000);
165+
166+
root.addStackTrace(tracer, Collections.singletonList(StackFrame.of("A", "a")), 40_000, childPool, 0);
167+
root.end(childPool, 0);
168+
169+
root.spanify();
170+
171+
assertThat(reporter.getSpans()).hasSize(1);
172+
assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(childSpanContext)).isTrue();
173+
// the inferred span should not have any span links because this
174+
// span link would cause a cycle in the trace
175+
assertThat(reporter.getSpans().get(0).getChildIds().getSize()).isEqualTo(0L);
176+
}
177+
178+
@Test
179+
void testSpanWithNestedActivation() {
180+
TraceContext rootContext = CallTreeTest.rootTraceContext(tracer);
181+
182+
NoopObjectPool<CallTree.Root> rootPool = NoopObjectPool.ofRecyclable(() -> new CallTree.Root(tracer));
183+
NoopObjectPool<CallTree> childPool = NoopObjectPool.ofRecyclable(CallTree::new);
184+
185+
CallTree.Root root = CallTree.createRoot(rootPool, rootContext.serialize(), rootContext.getServiceName(), rootContext.getServiceVersion(), 0);
186+
root.addStackTrace(tracer, Collections.singletonList(StackFrame.of("A", "a")), 10_000, childPool, 0);
187+
188+
root.onActivation(rootContext.serialize(), 20_000);
189+
root.onDeactivation(rootContext.serialize(), rootContext.serialize(), 30_000);
190+
191+
root.addStackTrace(tracer, Collections.singletonList(StackFrame.of("A", "a")), 40_000, childPool, 0);
192+
root.end(childPool, 0);
193+
194+
root.spanify();
195+
196+
assertThat(reporter.getSpans()).hasSize(1);
197+
assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(rootContext)).isTrue();
198+
// the inferred span should not have any span links because this
199+
// span link would cause a cycle in the trace
200+
assertThat(reporter.getSpans().get(0).getChildIds().getSize()).isEqualTo(0L);
146201
}
147202

148203
}

apm-agent-plugins/apm-profiling-plugin/src/test/java/co/elastic/apm/agent/profiler/CallTreeTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ void testCallTree() {
120120
@Test
121121
void testGiveEmptyChildIdsTo() {
122122
CallTree rich = new CallTree();
123-
rich.addChildId(42);
123+
rich.addChildId(42, 0L);
124124
CallTree robinHood = new CallTree();
125125
CallTree poor = new CallTree();
126126

0 commit comments

Comments
 (0)