Skip to content

Commit 057a33f

Browse files
dougqhdevflow.devflow-routing-intake
andauthored
Decouple W3C lastParentId from shared propagation-tag mutation (#11702)
Decouple W3C lastParentId from shared propagation-tag mutation Injecting a W3C tracestate wrote the injecting span's id into the (trace-level, shared via getRootSpanContextOrThis) PropagationTags via updateLastParentId, then read it back to build the `p:` member. Concurrent sibling injects race on that shared field -- A writes A, B writes B, A can emit B as its last-parent. Supply the injecting span's id as a parameter instead: PropagationTags.headerValue(HeaderType, lastParentIdOverride). W3CHttpCodec passes context.getSpanId(); the W3C codec uses the override for `p:`, falling back to the stored inbound last-parent-id for non-inject callers (span-link traceState). No inject-time mutation of the shared tags -> no race, no cross-talk between sibling injects, and the inbound last-parent-id is preserved for its ctor/extraction reads. updateLastParentId is now unused and removed (it was the only inject-time mutator; the inbound value is set at construction). OPM stamping is left as-is on purpose: it writes a process-constant local OPM, so concurrent injects are idempotent (no race). Full dd-trace-core propagation + span-build suite passes (1947 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Merge branch 'master' into dougqh/ptags-decouple-lastparentid Merge branch 'master' into dougqh/ptags-decouple-lastparentid Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent a059d47 commit 057a33f

6 files changed

Lines changed: 107 additions & 14 deletions

File tree

dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ public interface Factory {
7979

8080
public abstract CharSequence getLastParentId();
8181

82-
public abstract void updateLastParentId(CharSequence lastParentId);
83-
8482
/**
8583
* Gets the original <a href="https://www.w3.org/TR/trace-context/#tracestate-header">W3C
8684
* tracestate header</a> value.
@@ -104,6 +102,15 @@ public interface Factory {
104102
*/
105103
public abstract String headerValue(HeaderType headerType);
106104

105+
/**
106+
* Like {@link #headerValue(HeaderType)} but uses {@code lastParentIdOverride} for the W3C {@code
107+
* p:} (last-parent-id) instead of the stored {@link #getLastParentId() last-parent-id}. Used at
108+
* inject so the injecting span's id is supplied as a parameter rather than mutated into these
109+
* (possibly trace-level, shared) tags — keeping transient per-injection identity out of shared
110+
* state. A {@code null} override falls back to {@link #headerValue(HeaderType)}.
111+
*/
112+
public abstract String headerValue(HeaderType headerType, CharSequence lastParentIdOverride);
113+
107114
/**
108115
* Fills a provided tagMap with valid propagated _dd.p.* tags and possibly a new sampling decision
109116
* tags _dd.p.dm (root span only) based on the current state, or sets only an error tag if the

dd-trace-core/src/main/java/datadog/trace/core/propagation/W3CHttpCodec.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,11 @@ private <C> void injectTraceParent(DDSpanContext context, C carrier, CarrierSett
8080

8181
private <C> void injectTraceState(DDSpanContext context, C carrier, CarrierSetter<C> setter) {
8282
PropagationTags propagationTags = context.getPropagationTags();
83-
propagationTags.updateLastParentId(DDSpanId.toHexStringPadded(context.getSpanId()));
84-
String tracestate = propagationTags.headerValue(W3C);
83+
// Supply the injecting span's id for the W3C `p:` as a parameter rather than mutating it into
84+
// the (possibly trace-level, shared) tags — keeps transient per-injection identity out of
85+
// shared state, so concurrent sibling injects can't race on it.
86+
String tracestate =
87+
propagationTags.headerValue(W3C, DDSpanId.toHexStringPadded(context.getSpanId()));
8588
if (tracestate != null && !tracestate.isEmpty()) {
8689
setter.set(carrier, TRACE_STATE_KEY, tracestate);
8790
}

dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,18 @@ abstract class PTagsCodec {
2525
protected static final TagKey UPSTREAM_SERVICES_DEPRECATED_TAG = TagKey.from("upstream_services");
2626

2727
static String headerValue(PTagsCodec codec, PTags ptags) {
28+
return headerValue(codec, ptags, null);
29+
}
30+
31+
static String headerValue(PTagsCodec codec, PTags ptags, CharSequence lastParentIdOverride) {
2832
int estimate = codec.estimateHeaderSize(ptags);
2933
if (estimate == 0) {
3034
return "";
3135
}
3236

3337
// No encoding validation here because we don't allow arbitrary tag change
3438
StringBuilder sb = new StringBuilder(estimate);
35-
int size = codec.appendPrefix(sb, ptags);
39+
int size = codec.appendPrefix(sb, ptags, lastParentIdOverride);
3640
if (!ptags.isPropagationTagsDisabled()) {
3741
if (ptags.getDecisionMakerTagValue() != null) {
3842
size = codec.appendTag(sb, DECISION_MAKER_TAG, ptags.getDecisionMakerTagValue(), size);
@@ -174,6 +178,14 @@ static int calcXDatadogTagsSize(int size, TagKey tagKey, TagValue tagValue) {
174178

175179
protected abstract int appendPrefix(StringBuilder sb, PTags ptags);
176180

181+
/**
182+
* Encode the prefix, using {@code lastParentIdOverride} for the W3C {@code p:} when non-null
183+
* (inject-time). Codecs without a last-parent-id (e.g. Datadog) ignore the override.
184+
*/
185+
protected int appendPrefix(StringBuilder sb, PTags ptags, CharSequence lastParentIdOverride) {
186+
return appendPrefix(sb, ptags);
187+
}
188+
177189
protected abstract int appendTag(StringBuilder sb, TagElement key, TagElement value, int size);
178190

179191
protected abstract int appendSuffix(StringBuilder sb, PTags ptags, int size);

dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -420,14 +420,6 @@ public CharSequence getLastParentId() {
420420
return lastParentId;
421421
}
422422

423-
@Override
424-
public void updateLastParentId(CharSequence lastParentId) {
425-
if (!Objects.equals(this.lastParentId, lastParentId)) {
426-
clearCachedHeader(W3C);
427-
this.lastParentId = TagValue.from(lastParentId);
428-
}
429-
}
430-
431423
@Override
432424
@SuppressWarnings("StringEquality")
433425
@SuppressFBWarnings("ES_COMPARING_STRINGS_WITH_EQ")
@@ -448,6 +440,18 @@ public String headerValue(HeaderType headerType) {
448440
return header;
449441
}
450442

443+
@Override
444+
public String headerValue(HeaderType headerType, CharSequence lastParentIdOverride) {
445+
if (lastParentIdOverride == null) {
446+
return headerValue(headerType);
447+
}
448+
// Inject-time path: encode fresh with the override; do NOT cache — the W3C `p:` is
449+
// per-injecting-span and these tags may be shared across sibling spans.
450+
String header =
451+
PTagsCodec.headerValue(factory.getDecoderEncoder(headerType), this, lastParentIdOverride);
452+
return (header == null || header.isEmpty()) ? null : header;
453+
}
454+
451455
@Override
452456
public void fillTagMap(Map<String, String> tagMap) {
453457
PTagsCodec.fillTagMap(this, tagMap);

dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@ protected int estimateHeaderSize(PTags pTags) {
226226

227227
@Override
228228
protected int appendPrefix(StringBuilder sb, PTags ptags) {
229+
return appendPrefix(sb, ptags, null);
230+
}
231+
232+
@Override
233+
protected int appendPrefix(StringBuilder sb, PTags ptags, CharSequence lastParentIdOverride) {
229234
sb.append(DATADOG_MEMBER_KEY);
230235
// Append sampling priority (s)
231236
if (ptags.getSamplingPriority() != PrioritySampling.UNSET) {
@@ -246,7 +251,8 @@ protected int appendPrefix(StringBuilder sb, PTags ptags) {
246251
}
247252
}
248253
// append last ParentId (p)
249-
CharSequence lastParent = ptags.getLastParentId();
254+
CharSequence lastParent =
255+
lastParentIdOverride != null ? lastParentIdOverride : ptags.getLastParentId();
250256
if (lastParent != null) {
251257
if (sb.length() > EMPTY_SIZE) {
252258
sb.append(';');
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package datadog.trace.core.propagation;
2+
3+
import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertFalse;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
/**
11+
* The inject-time W3C last-parent-id ({@code p:}) is supplied as a parameter to {@link
12+
* PropagationTags#headerValue(PropagationTags.HeaderType, CharSequence)} rather than mutated into
13+
* the (possibly trace-level, shared) tags. This keeps transient per-injection identity out of
14+
* shared state: a sibling span's inject can't pollute another span's header, and the stored inbound
15+
* last-parent-id is never overwritten.
16+
*/
17+
class PropagationTagsLastParentIdTest {
18+
19+
private static final String SPAN_A = "00000000000000aa";
20+
private static final String SPAN_B = "00000000000000bb";
21+
22+
private static PropagationTags w3c(String header) {
23+
return PropagationTags.factory().fromHeaderValue(W3C, header);
24+
}
25+
26+
@Test
27+
void overrideSuppliesW3cLastParentId() {
28+
PropagationTags tags = w3c("dd=s:1;o:rum");
29+
assertTrue(tags.headerValue(W3C, SPAN_A).contains("p:" + SPAN_A));
30+
}
31+
32+
@Test
33+
void overrideDoesNotMutateSharedTags_noCrossTalk() {
34+
// One tags instance, two sibling spans injecting through it (the shared-root scenario).
35+
PropagationTags shared = w3c("dd=s:1;o:rum"); // no inbound p:
36+
37+
String headerA = shared.headerValue(W3C, SPAN_A);
38+
String headerB = shared.headerValue(W3C, SPAN_B);
39+
String headerAagain = shared.headerValue(W3C, SPAN_A);
40+
41+
assertTrue(headerA.contains("p:" + SPAN_A));
42+
assertTrue(headerB.contains("p:" + SPAN_B));
43+
// Injecting B did not change what A injects — no shared mutation.
44+
assertEquals(headerA, headerAagain, "a sibling inject must not change another span's header");
45+
// The override is never written into the shared tags (no-override header has no p:).
46+
assertFalse(shared.headerValue(W3C).contains("p:"), "override must not mutate the stored tags");
47+
}
48+
49+
@Test
50+
void inboundLastParentIdPreservedAndUnmutatedByOverride() {
51+
PropagationTags tags = w3c("dd=s:1;p:" + SPAN_A); // arrived carrying a last-parent-id
52+
53+
// No-override path (e.g. span-link traceState) keeps the inbound p:.
54+
assertTrue(tags.headerValue(W3C).contains("p:" + SPAN_A));
55+
// An inject override replaces it for that produced header...
56+
assertTrue(tags.headerValue(W3C, SPAN_B).contains("p:" + SPAN_B));
57+
// ...without mutating the stored inbound value.
58+
assertTrue(
59+
tags.headerValue(W3C).contains("p:" + SPAN_A), "inbound p: must survive override use");
60+
}
61+
}

0 commit comments

Comments
 (0)