From cccf86ee2dea7997d2bcdf4795352f7a1be2081c Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 20:48:39 -0400 Subject: [PATCH 1/5] Share sampling states for decisions with no tracestate or rate A decision with no tracestate, otel member or sampling rate is just a priority and a decision maker, and almost every extracted priority and manual or forced keep is one of a few dozen such pairs. States are immutable, so those decisions now use preallocated shared states instead of building one per trace. Datadog-only extraction no longer builds a sampling state at all, and compound extraction builds one instead of two. Co-Authored-By: Claude Opus 5.5 --- .../core/propagation/ptags/PTagsFactory.java | 100 +++++++++++++++--- 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index d80239d9afa..682f58d6eac 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java @@ -111,9 +111,6 @@ PropagationTags createInvalid(String error) { static class PTags extends PropagationTags { private static final String EMPTY = ""; - private static final SamplingState EMPTY_SAMPLING_STATE = - new SamplingState(PrioritySampling.UNSET, null, null, null, null); - protected final PTagsFactory factory; // tags that don't require any modifications and propagated as-is @@ -167,6 +164,90 @@ static class PTags extends PropagationTags { } } + /** + * Shared states for decisions with no tracestate, otel member or sampling rate, indexed by + * {@link #sharedPriorityIndex} and {@link #sharedDecisionMakerIndex}. States are immutable, so + * a trace whose decision is one of these, as an extracted priority or a manual keep almost + * always is, doesn't build its own. + */ + private static final SamplingState[][] SHARED_SAMPLING_STATES = + new SamplingState[5][DECISION_MAKER_TAG_VALUES.length + 1]; + + static { + int[] priorities = { + PrioritySampling.UNSET, + PrioritySampling.USER_DROP, + PrioritySampling.SAMPLER_DROP, + PrioritySampling.SAMPLER_KEEP, + PrioritySampling.USER_KEEP + }; + for (int priority : priorities) { + SamplingState[] row = SHARED_SAMPLING_STATES[sharedPriorityIndex(priority)]; + row[0] = new SamplingState(priority, null, null, null, null); + for (int mechanism = 0; mechanism < DECISION_MAKER_TAG_VALUES.length; ++mechanism) { + row[mechanism + 1] = + new SamplingState(priority, null, null, DECISION_MAKER_TAG_VALUES[mechanism], null); + } + } + } + + /** + * A state for this decision, shared when it has no tracestate, otel member or sampling rate, + * and a priority and decision maker that {@link #SHARED_SAMPLING_STATES} covers. + */ + static SamplingState samplingState( + int samplingPriority, + String tracestate, + CharSequence otelTraceState, + TagValue decisionMaker, + TagValue knuthSamplingRate) { + if (tracestate == null && otelTraceState == null && knuthSamplingRate == null) { + int priorityIndex = sharedPriorityIndex(samplingPriority); + int decisionMakerIndex = sharedDecisionMakerIndex(decisionMaker); + if (priorityIndex >= 0 && decisionMakerIndex >= 0) { + return SHARED_SAMPLING_STATES[priorityIndex][decisionMakerIndex]; + } + } + return new SamplingState( + samplingPriority, tracestate, otelTraceState, decisionMaker, knuthSamplingRate); + } + + private static int sharedPriorityIndex(int samplingPriority) { + if (samplingPriority == PrioritySampling.UNSET) { + return 0; + } + return samplingPriority >= PrioritySampling.USER_DROP + && samplingPriority <= PrioritySampling.USER_KEEP + ? samplingPriority - PrioritySampling.USER_DROP + 1 + : -1; + } + + /** + * 0 for no decision maker, the mechanism + 1 for a shared {@code -}, otherwise -1. + */ + private static int sharedDecisionMakerIndex(TagValue decisionMaker) { + if (decisionMaker == null) { + return 0; + } + int length = decisionMaker.length(); + if (length < 2 || length > 3 || decisionMaker.charAt(0) != '-') { + return -1; + } + int mechanism = 0; + for (int i = 1; i < length; ++i) { + char c = decisionMaker.charAt(i); + if (c < '0' || c > '9') { + return -1; + } + mechanism = mechanism * 10 + (c - '0'); + } + // equals rejects spellings like -01 that name a mechanism but don't match its value + return mechanism < DECISION_MAKER_TAG_VALUES.length + && DECISION_MAKER_TAG_VALUES[mechanism].equals(decisionMaker) + ? mechanism + 1 + : -1; + } + // Static cache for the most-recently-seen rate → TagValue. In steady state a service uses one // rate, so this eliminates the char[] + String allocation on every new PTags instance. // Writes are benign-racy: two threads computing the same rate produce equal TagValues. @@ -436,7 +517,7 @@ private SamplingState nextSamplingState( return current; } clearCachedHeader(W3C); - return new SamplingState( + return samplingState( samplingPriority, current.getTracestate(), nextOtelTraceState, @@ -497,12 +578,7 @@ private static SamplingState initialSamplingState( otelOriginalSize, InheritedSamplingState.NO_CHECKS); } - if (samplingPriority == PrioritySampling.UNSET - && tracestate == null - && decisionMakerTagValue == null) { - return EMPTY_SAMPLING_STATE; - } - return new SamplingState(samplingPriority, tracestate, null, decisionMakerTagValue, null); + return samplingState(samplingPriority, tracestate, null, decisionMakerTagValue, null); } /** @@ -1043,7 +1119,7 @@ private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) nextOtelTraceState = nextOtelTraceState.withoutThreshold(); } next = - new SamplingState( + samplingState( samplingPriority, tracestate, nextOtelTraceState, @@ -1075,7 +1151,7 @@ public void updateAndLockDecisionMaker(PropagationTags source) { do { current = samplingState; next = - new SamplingState( + samplingState( current.getSamplingPriority(), current.getTracestate(), otelTraceStateOf(current), From 692505281178934dc3fb85d56cd270bc85560516 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 21:02:14 -0400 Subject: [PATCH 2/5] Hold the tracestate apart from the sampling state Only extraction replaces a trace's tracestate, before the trace is shared, so it needn't be published together with the sampling decision. The tracestate now lives only on the propagation tags, and the codec reads it from there. A sampling state changes with the tracestate only when its ot member does, so a tracestate without one keeps the trace's shared state: compound and W3C extraction of such a tracestate build no sampling state at all. Co-Authored-By: Claude Opus 5.5 --- .../core/propagation/PropagationTags.java | 14 ++--- .../ptags/InheritedSamplingState.java | 5 +- .../core/propagation/ptags/PTagsFactory.java | 59 ++++++++----------- .../ptags/ProbabilitySamplingState.java | 3 +- .../core/propagation/ptags/W3CPTagsCodec.java | 25 +++----- .../ptags/InheritedSamplingStateTest.java | 2 +- .../ptags/OtelMemberForWriteTest.java | 1 - 7 files changed, 42 insertions(+), 67 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java index 87c6f7ad12a..fb53bc65e2b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java @@ -35,7 +35,6 @@ public static class SamplingState { private static final byte OTEL_POSITION_REPLACED = 2; private final int samplingPriority; - private final String tracestate; private final CharSequence otelTraceState; private final CharSequence decisionMaker; private final CharSequence knuthSamplingRate; @@ -43,12 +42,10 @@ public static class SamplingState { public SamplingState( int samplingPriority, - String tracestate, CharSequence otelTraceState, CharSequence decisionMaker, CharSequence knuthSamplingRate) { this.samplingPriority = samplingPriority; - this.tracestate = tracestate; this.otelTraceState = otelTraceState; this.decisionMaker = decisionMaker; this.knuthSamplingRate = knuthSamplingRate; @@ -58,10 +55,6 @@ public int getSamplingPriority() { return samplingPriority; } - public String getTracestate() { - return tracestate; - } - /** The {@code ot} tracestate member, which a subclass may build on first read. */ public CharSequence getOtelTraceState() { return otelTraceState; @@ -83,7 +76,12 @@ public CharSequence getKnuthSamplingRate() { return knuthSamplingRate; } - public boolean preservesInheritedOtelPosition() { + /** + * Whether writing the {@code ot} member at its position in {@code tracestate}, the trace's raw + * tracestate, leaves it unchanged. Cached, as a state with an {@code ot} member belongs to one + * trace, whose tracestate doesn't change once the trace is shared. + */ + public boolean preservesInheritedOtelPosition(String tracestate) { if (tracestate == null) { return false; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java index df0de673d59..88bdd66ab1a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java @@ -38,13 +38,12 @@ final class InheritedSamplingState extends SamplingState { InheritedSamplingState( int samplingPriority, - String tracestate, CharSequence decisionMaker, CharSequence knuthSamplingRate, CharSequence rawOtelTraceState, int otelOriginalSize, byte pendingChecks) { - super(samplingPriority, tracestate, null, decisionMaker, knuthSamplingRate); + super(samplingPriority, null, decisionMaker, knuthSamplingRate); this.rawOtelTraceState = rawOtelTraceState; this.otelOriginalSize = otelOriginalSize; this.pendingChecks = pendingChecks; @@ -58,13 +57,11 @@ static byte withCheck(byte pendingChecks, boolean sampled) { /** A copy of this unparsed member under a new decision and {@code pendingChecks}. */ InheritedSamplingState withDecision( int samplingPriority, - String tracestate, CharSequence decisionMaker, CharSequence knuthSamplingRate, byte pendingChecks) { return new InheritedSamplingState( samplingPriority, - tracestate, decisionMaker, knuthSamplingRate, rawOtelTraceState, diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index 682f58d6eac..c7808421909 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java @@ -165,10 +165,10 @@ static class PTags extends PropagationTags { } /** - * Shared states for decisions with no tracestate, otel member or sampling rate, indexed by - * {@link #sharedPriorityIndex} and {@link #sharedDecisionMakerIndex}. States are immutable, so - * a trace whose decision is one of these, as an extracted priority or a manual keep almost - * always is, doesn't build its own. + * Shared states for decisions with no otel member or sampling rate, indexed by {@link + * #sharedPriorityIndex} and {@link #sharedDecisionMakerIndex}. States are immutable, so a trace + * whose decision is one of these, as an extracted priority or a manual keep almost always is, + * doesn't build its own. */ private static final SamplingState[][] SHARED_SAMPLING_STATES = new SamplingState[5][DECISION_MAKER_TAG_VALUES.length + 1]; @@ -183,33 +183,31 @@ static class PTags extends PropagationTags { }; for (int priority : priorities) { SamplingState[] row = SHARED_SAMPLING_STATES[sharedPriorityIndex(priority)]; - row[0] = new SamplingState(priority, null, null, null, null); + row[0] = new SamplingState(priority, null, null, null); for (int mechanism = 0; mechanism < DECISION_MAKER_TAG_VALUES.length; ++mechanism) { row[mechanism + 1] = - new SamplingState(priority, null, null, DECISION_MAKER_TAG_VALUES[mechanism], null); + new SamplingState(priority, null, DECISION_MAKER_TAG_VALUES[mechanism], null); } } } /** - * A state for this decision, shared when it has no tracestate, otel member or sampling rate, - * and a priority and decision maker that {@link #SHARED_SAMPLING_STATES} covers. + * A state for this decision, shared when it has no otel member or sampling rate, and a priority + * and decision maker that {@link #SHARED_SAMPLING_STATES} covers. */ static SamplingState samplingState( int samplingPriority, - String tracestate, CharSequence otelTraceState, TagValue decisionMaker, TagValue knuthSamplingRate) { - if (tracestate == null && otelTraceState == null && knuthSamplingRate == null) { + if (otelTraceState == null && knuthSamplingRate == null) { int priorityIndex = sharedPriorityIndex(samplingPriority); int decisionMakerIndex = sharedDecisionMakerIndex(decisionMaker); if (priorityIndex >= 0 && decisionMakerIndex >= 0) { return SHARED_SAMPLING_STATES[priorityIndex][decisionMakerIndex]; } } - return new SamplingState( - samplingPriority, tracestate, otelTraceState, decisionMaker, knuthSamplingRate); + return new SamplingState(samplingPriority, otelTraceState, decisionMaker, knuthSamplingRate); } private static int sharedPriorityIndex(int samplingPriority) { @@ -313,11 +311,7 @@ private static int sharedDecisionMakerIndex(TagValue decisionMaker) { this.tracestate = tracestate; this.samplingState = initialSamplingState( - samplingPriority, - tracestate, - rawOtelTraceState, - otelOriginalSize, - decisionMakerTagValue); + samplingPriority, rawOtelTraceState, otelOriginalSize, decisionMakerTagValue); this.origin = origin; this.lastParentId = lastParentId; this.orgPropagationMarkerTagValue = orgPropagationMarkerTagValue; @@ -449,7 +443,6 @@ public boolean tryUpdateProbabilitySamplingDecision( next = new ProbabilitySamplingState( samplingPriority, - current.getTracestate(), nextDecisionMakerTagValue, nextKnuthSamplingRate, traceIdLowOrderBits, @@ -519,7 +512,6 @@ private SamplingState nextSamplingState( clearCachedHeader(W3C); return samplingState( samplingPriority, - current.getTracestate(), nextOtelTraceState, nextDecisionMakerTagValue, nextKnuthSamplingRateTagValue); @@ -564,21 +556,19 @@ private TagValue nextDecisionMakerTagValue( private static SamplingState initialSamplingState( int samplingPriority, - String tracestate, CharSequence rawOtelTraceState, int otelOriginalSize, TagValue decisionMakerTagValue) { if (rawOtelTraceState != null) { return new InheritedSamplingState( samplingPriority, - tracestate, decisionMakerTagValue, null, rawOtelTraceState, otelOriginalSize, InheritedSamplingState.NO_CHECKS); } - return samplingState(samplingPriority, tracestate, null, decisionMakerTagValue, null); + return samplingState(samplingPriority, null, decisionMakerTagValue, null); } /** @@ -615,11 +605,7 @@ private SamplingState deferOtelCheck( } clearCachedHeader(W3C); return inherited.withDecision( - samplingPriority, - current.getTracestate(), - decisionMaker, - knuthSamplingRate, - pendingChecks); + samplingPriority, decisionMaker, knuthSamplingRate, pendingChecks); } private static OtelTraceState reconcileOtelTraceState( @@ -1048,7 +1034,8 @@ public String getW3CTracestate(SamplingState samplingState) { TracestateCacheEntry cache = tracestateCache; if (cache == null || cache.samplingState != samplingState) { cache = - new TracestateCacheEntry(samplingState, W3CPTagsCodec.rebuildTracestate(samplingState)); + new TracestateCacheEntry( + samplingState, W3CPTagsCodec.rebuildTracestate(samplingState, tracestate)); tracestateCache = cache; } return cache.tracestate; @@ -1082,6 +1069,7 @@ public void updateW3CTracestateFrom(W3CTracestate source, int overridingPriority // as the trace context's own priority override reconciles its member pendingChecks = InheritedSamplingState.withCheck(pendingChecks, overridingPriority > 0); } + setTracestate(source.tracestate); SamplingState current; SamplingState next; do { @@ -1095,18 +1083,17 @@ public void updateW3CTracestateFrom(W3CTracestate source, int overridingPriority next = new InheritedSamplingState( samplingPriority, - source.tracestate, getDecisionMakerTagValue(current), getKnuthSamplingRateTagValue(current), source.rawOtelTraceState, source.otelOriginalSize, nextPendingChecks); } while (!SAMPLING_STATE_UPDATER.compareAndSet(this, current, next)); - this.tracestate = source.tracestate; clearCachedHeader(W3C); } private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) { + setTracestate(tracestate); SamplingState current; SamplingState next; do { @@ -1121,12 +1108,19 @@ private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) next = samplingState( samplingPriority, - tracestate, nextOtelTraceState, getDecisionMakerTagValue(current), getKnuthSamplingRateTagValue(current)); - } while (!SAMPLING_STATE_UPDATER.compareAndSet(this, current, next)); + } while (next != current && !SAMPLING_STATE_UPDATER.compareAndSet(this, current, next)); + } + + /** + * Replaces the trace's tracestate. Only extraction does this, before the trace is shared, so + * the tracestate needn't be published together with the sampling state. + */ + private void setTracestate(String tracestate) { this.tracestate = tracestate; + tracestateCache = null; clearCachedHeader(W3C); } @@ -1153,7 +1147,6 @@ public void updateAndLockDecisionMaker(PropagationTags source) { next = samplingState( current.getSamplingPriority(), - current.getTracestate(), otelTraceStateOf(current), decisionMakerTagValue, getKnuthSamplingRateTagValue(current)); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/ProbabilitySamplingState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/ProbabilitySamplingState.java index 92c7d55709c..824d5c0412c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/ProbabilitySamplingState.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/ProbabilitySamplingState.java @@ -23,12 +23,11 @@ final class ProbabilitySamplingState extends SamplingState { ProbabilitySamplingState( int samplingPriority, - String tracestate, CharSequence decisionMaker, CharSequence knuthSamplingRate, long traceIdLowOrderBits, double sampleRate) { - super(samplingPriority, tracestate, null, decisionMaker, knuthSamplingRate); + super(samplingPriority, null, decisionMaker, knuthSamplingRate); this.traceIdLowOrderBits = traceIdLowOrderBits; this.sampleRate = sampleRate; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index 96c341a3fdf..883472c57c9 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -11,7 +11,6 @@ import datadog.trace.core.propagation.ptags.PTagsFactory.PTags; import datadog.trace.core.propagation.ptags.TagElement.Encoding; import datadog.trace.util.SubSequence; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -293,12 +292,6 @@ private static R parse(String value, Result result) { } @Override - @SuppressWarnings("StringEquality") - @SuppressFBWarnings( - value = "ES_COMPARING_STRINGS_WITH_EQ", - justification = - "Identity determines whether the sampling state retains this PTags instance's raw " - + "tracestate, allowing its parsed size metadata to be reused.") protected int estimateHeaderSize( PTags pTags, CharSequence lastParentIdOverride, SamplingState samplingState) { int size = EMPTY_SIZE + 1; @@ -314,11 +307,9 @@ protected int estimateHeaderSize( if (lastParent != null) { size += lastParent.length() + 3; } - String originalTracestate = samplingState.getTracestate(); + String originalTracestate = pTags.tracestate; boolean includesOriginalTracestate = false; - if (originalTracestate != null - && pTags instanceof W3CPTags - && originalTracestate == pTags.tracestate) { + if (originalTracestate != null && pTags instanceof W3CPTags) { W3CPTags w3CPTags = (W3CPTags) pTags; size += w3CPTags.maxUnknownSize; if (w3CPTags.ddMemberStart != -1) { @@ -403,7 +394,7 @@ protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingStat size = 0; } if (size == 0) { - String original = samplingState.getTracestate(); + String original = ptags.tracestate; String trimmed = original == null ? null : original.trim(); if (canForwardRawTracestate(trimmed, samplingState.getOtelTraceStateForWrite())) { sb.append(trimmed); @@ -411,7 +402,7 @@ protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingStat } } // Append the managed OTel member and all other non-Datadog list-members - if (appendOtelAndVendorMembers(sb, samplingState, size != 0)) { + if (appendOtelAndVendorMembers(sb, samplingState, ptags.tracestate, size != 0)) { // We don't care about the total size in bytes here, but only the fact that we added something // that should be returned size = Math.max(size, EMPTY_SIZE + 1); @@ -866,12 +857,11 @@ private static int cleanUpAndAppendUnknown(StringBuilder sb, W3CPTags w3CPTags, } private static boolean appendOtelAndVendorMembers( - StringBuilder sb, SamplingState samplingState, boolean hasDatadogMember) { - String original = samplingState.getTracestate(); + StringBuilder sb, SamplingState samplingState, String original, boolean hasDatadogMember) { CharSequence otelTraceState = samplingState.getOtelTraceStateForWrite(); int remainingMembers = MAX_MEMBER_COUNT - (hasDatadogMember ? 1 : 0); boolean memberAppended = false; - boolean preserveOtelPosition = samplingState.preservesInheritedOtelPosition(); + boolean preserveOtelPosition = samplingState.preservesInheritedOtelPosition(original); if (!preserveOtelPosition && otelTraceState != null && remainingMembers > 0) { appendMember(sb, OTEL_MEMBER_KEY, otelTraceState.toString()); remainingMembers--; @@ -924,8 +914,7 @@ public static boolean isUnchangedInheritedOtelMember( return otelMemberCount == 1; } - public static String rebuildTracestate(SamplingState samplingState) { - String original = samplingState.getTracestate(); + public static String rebuildTracestate(SamplingState samplingState, String original) { CharSequence otelTraceState = samplingState.getOtelTraceStateForWrite(); // TODO Consider a raw passthrough for unchanged state after checking dd= is not duplicated. StringBuilder result = new StringBuilder(MAX_HEADER_SIZE); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java index 9ece70a7a76..01d5817ecb6 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java @@ -74,7 +74,7 @@ void keepsOriginalSizeForTheHeaderEstimate() { } private static InheritedSamplingState state(String raw, byte checks) { - return new InheritedSamplingState(SAMPLER_KEEP, null, null, null, raw, ORIGINAL_SIZE, checks); + return new InheritedSamplingState(SAMPLER_KEEP, null, null, raw, ORIGINAL_SIZE, checks); } /** What extraction did before deferring: parse, then check at each step. */ diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java index d44a8eb26be..7cd89679d7c 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java @@ -91,7 +91,6 @@ private static InheritedSamplingState state(String member, byte pending) { int start = tracestate.indexOf("ot=") + 3; return new InheritedSamplingState( SAMPLER_KEEP, - tracestate, null, null, SubSequence.of(tracestate, start, tracestate.length()), From c05e95726464bb8b31935b5e88e4f3a069f4884e Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 21:02:17 -0400 Subject: [PATCH 3/5] Test that a tracestate update only replaces the sampling state's ot member Co-Authored-By: Claude Opus 5.5 --- .../ptags/TracestateUpdateTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/TracestateUpdateTest.java diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/TracestateUpdateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/TracestateUpdateTest.java new file mode 100644 index 00000000000..41611dafc06 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/TracestateUpdateTest.java @@ -0,0 +1,53 @@ +package datadog.trace.core.propagation.ptags; + +import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import datadog.trace.core.propagation.PropagationTags; +import datadog.trace.core.propagation.PropagationTags.SamplingState; +import org.junit.jupiter.api.Test; + +/** + * The tracestate is held apart from the sampling state, which only changes when the tracestate's + * {@code ot} member does. + */ +class TracestateUpdateTest { + private static final PropagationTags.Factory FACTORY = PropagationTags.factory(); + private static final String WITH_OTEL_MEMBER = "dd=s:1,ot=rv:0123456789abcd;th:8"; + + @Test + void tracestateWithoutOtelMemberKeepsSamplingState() { + PropagationTags ptags = FACTORY.empty(); + SamplingState before = ptags.samplingState(); + + ptags.updateW3CTracestate("vendor=state"); + + assertSame(before, ptags.samplingState()); + assertEquals("vendor=state", ptags.getW3CTracestate()); + } + + @Test + void otelMemberOfNewTracestateIsTaken() { + PropagationTags ptags = FACTORY.empty(); + + ptags.updateW3CTracestate(WITH_OTEL_MEMBER); + + CharSequence otel = ptags.samplingState().getOtelTraceState(); + assertNotNull(otel); + assertEquals("rv:0123456789abcd;th:8", otel.toString()); + } + + @Test + void otelMemberIsDroppedWithItsTracestate() { + PropagationTags ptags = FACTORY.fromHeaderValue(W3C, WITH_OTEL_MEMBER); + assertNotNull(ptags.samplingState().getOtelTraceState()); + + ptags.updateW3CTracestate("vendor=state"); + + assertNull(ptags.samplingState().getOtelTraceState()); + assertEquals("vendor=state", ptags.getW3CTracestate()); + } +} From 5f206caf673de2126b1adae3f60cc2b13a1a7594 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 21:21:16 -0400 Subject: [PATCH 4/5] Keep the inherited ot member on the trace, not its sampling state An extracted trace with an ot tracestate member still built a sampling state per trace to hold the member unparsed. The merge already builds a W3CTracestate holding it, so the trace now keeps that as its tracestate and it parses the member on first read. The inherited state records only the decision and its pending checks, so it's shared like the others: extraction with an ot member no longer builds a sampling state. The member is now read through the propagation tags, which resolve an inherited state against the member they hold. Co-Authored-By: Claude Opus 5.5 --- .../core/propagation/PropagationTags.java | 21 ++- .../ptags/InheritedSamplingState.java | 106 +++-------- .../core/propagation/ptags/PTagsFactory.java | 171 +++++++++++++----- .../core/propagation/ptags/W3CPTagsCodec.java | 39 ++-- .../core/propagation/ptags/W3CTracestate.java | 102 +++++++++++ .../OtelTraceStatePropagationTest.java | 14 +- .../ptags/DeferredOtelCheckTest.java | 2 +- .../ptags/InheritedOtelExtractionTest.java | 4 +- ...Test.java => InheritedOtelMemberTest.java} | 29 +-- .../ptags/OtelMemberForWriteTest.java | 37 ++-- .../ptags/ProbabilitySamplingStateTest.java | 2 +- .../ptags/TracestateUpdateTest.java | 6 +- 12 files changed, 334 insertions(+), 199 deletions(-) rename dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/{InheritedSamplingStateTest.java => InheritedOtelMemberTest.java} (76%) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java index fb53bc65e2b..aa728ad858b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java @@ -77,16 +77,13 @@ public CharSequence getKnuthSamplingRate() { } /** - * Whether writing the {@code ot} member at its position in {@code tracestate}, the trace's raw - * tracestate, leaves it unchanged. Cached, as a state with an {@code ot} member belongs to one - * trace, whose tracestate doesn't change once the trace is shared. + * Whether writing {@code otelTraceState}, this state's {@code ot} member as it's written, at + * the member's position in {@code tracestate}, the trace's raw tracestate, leaves it unchanged. + * Cached, as a state with its own {@code ot} member is never shared, and its trace's tracestate + * doesn't change once the trace is. */ - public boolean preservesInheritedOtelPosition(String tracestate) { - if (tracestate == null) { - return false; - } - CharSequence otelTraceState = getOtelTraceStateForWrite(); - if (otelTraceState == null) { + public boolean preservesOtelPosition(String tracestate, CharSequence otelTraceState) { + if (tracestate == null || otelTraceState == null) { return false; } byte position = otelPosition; @@ -167,6 +164,12 @@ public abstract boolean tryUpdateProbabilitySamplingDecision( public abstract SamplingState samplingState(); + /** + * The {@code ot} tracestate member of {@code samplingState}, a state of these tags. Read it here + * rather than from the state, which may defer to a member these tags hold. + */ + public abstract CharSequence getOtelTraceState(SamplingState samplingState); + public abstract void updateTraceOrigin(CharSequence origin); public abstract CharSequence getOrigin(); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java index 88bdd66ab1a..2da72502b4d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java @@ -1,51 +1,39 @@ package datadog.trace.core.propagation.ptags; import datadog.trace.core.propagation.PropagationTags.SamplingState; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; /** * The sampling state for an extracted trace whose OpenTelemetry {@code ot} tracestate member hasn't * been parsed yet. Extraction used to parse the member and check it against the inherited decision * on the request thread, though only W3C injection, OTLP export and a local change of decision ever - * read it. This keeps the raw member and parses it on first read, so a leaf service exporting to - * the Datadog agent never pays for it. + * read it. The member stays raw in the trace's {@link W3CTracestate}, which parses it on first + * read, so a leaf service exporting to the Datadog agent never pays for it. * - *

Consistency checks made before the first read are recorded as pending and applied when the - * member is parsed. Each check drops the threshold when {@code rv >= th} doesn't match the sampling - * decision checked against, and dropping it is idempotent and leaves the state consistent with any - * decision. So any sequence of checks has the same effect as one per decision checked, in any - * order, and two bits record them without changing the output. + *

This state records only the decision and the consistency checks made before the first read, + * which are applied when the member is parsed. Each check drops the threshold when {@code rv >= th} + * doesn't match the sampling decision checked against, and dropping it is idempotent and leaves the + * state consistent with any decision. So any sequence of checks has the same effect as one per + * decision checked, in any order, and two bits record them without changing the output. + * + *

Holding no per-trace data, these states are shared like other states. The member is read + * through the trace's propagation tags; this state can't read it itself. */ final class InheritedSamplingState extends SamplingState { static final byte NO_CHECKS = 0; private static final byte CHECKED_SAMPLED = 1; private static final byte CHECKED_NOT_SAMPLED = 2; - /** Marks a member that parsed to nothing, distinct from not parsed yet. */ - private static final Object NO_OTEL_TRACE_STATE = new Object(); - - private static final AtomicReferenceFieldUpdater - RESOLVED_UPDATER = - AtomicReferenceFieldUpdater.newUpdater( - InheritedSamplingState.class, Object.class, "resolved"); + /** The number of distinct pending check values. */ + static final int CHECK_COMBINATIONS = 4; - private final CharSequence rawOtelTraceState; - private final int otelOriginalSize; private final byte pendingChecks; - /** {@code null} until parsed, then the {@link OtelTraceState} or {@link #NO_OTEL_TRACE_STATE}. */ - private volatile Object resolved; - InheritedSamplingState( int samplingPriority, CharSequence decisionMaker, CharSequence knuthSamplingRate, - CharSequence rawOtelTraceState, - int otelOriginalSize, byte pendingChecks) { super(samplingPriority, null, decisionMaker, knuthSamplingRate); - this.rawOtelTraceState = rawOtelTraceState; - this.otelOriginalSize = otelOriginalSize; this.pendingChecks = pendingChecks; } @@ -54,77 +42,27 @@ static byte withCheck(byte pendingChecks, boolean sampled) { return (byte) (pendingChecks | (sampled ? CHECKED_SAMPLED : CHECKED_NOT_SAMPLED)); } - /** A copy of this unparsed member under a new decision and {@code pendingChecks}. */ - InheritedSamplingState withDecision( - int samplingPriority, - CharSequence decisionMaker, - CharSequence knuthSamplingRate, - byte pendingChecks) { - return new InheritedSamplingState( - samplingPriority, - decisionMaker, - knuthSamplingRate, - rawOtelTraceState, - otelOriginalSize, - pendingChecks); + static boolean checksSampled(byte pendingChecks) { + return (pendingChecks & CHECKED_SAMPLED) != 0; } - boolean isResolved() { - return resolved != null; - } - - /** - * Writes the raw member when parsing it and applying the pending checks would leave it as is, - * which is the usual case when relaying a trace unchanged, so injection doesn't have to parse it. - */ - @Override - public CharSequence getOtelTraceStateForWrite() { - if (resolved == null - && OtelTraceState.isUnchangedByChecks( - rawOtelTraceState, - (pendingChecks & CHECKED_SAMPLED) != 0, - (pendingChecks & CHECKED_NOT_SAMPLED) != 0)) { - return rawOtelTraceState; - } - return getOtelTraceState(); - } - - int otelOriginalSize() { - return otelOriginalSize; + static boolean checksNotSampled(byte pendingChecks) { + return (pendingChecks & CHECKED_NOT_SAMPLED) != 0; } byte pendingChecks() { return pendingChecks; } - /** - * Parses the member and applies the pending checks on first read. Racing readers agree on one - * instance, so identity checks on otel states keep skipping redundant work. - */ @Override public CharSequence getOtelTraceState() { - Object current = resolved; - if (current == null) { - current = resolve(); - } - return current == NO_OTEL_TRACE_STATE ? null : (OtelTraceState) current; + throw new UnsupportedOperationException( + "the ot member is held by the trace; read it through its propagation tags"); } - private Object resolve() { - OtelTraceState otelTraceState = OtelTraceState.parse(rawOtelTraceState, otelOriginalSize); - if ((pendingChecks & CHECKED_SAMPLED) != 0) { - otelTraceState = checked(otelTraceState, true); - } - if ((pendingChecks & CHECKED_NOT_SAMPLED) != 0) { - otelTraceState = checked(otelTraceState, false); - } - Object built = otelTraceState == null ? NO_OTEL_TRACE_STATE : otelTraceState; - return RESOLVED_UPDATER.compareAndSet(this, null, built) ? built : resolved; - } - - private static OtelTraceState checked(OtelTraceState otelTraceState, boolean sampled) { - return otelTraceState == null || otelTraceState.isConsistentWith(sampled) - ? otelTraceState - : otelTraceState.withoutThreshold(); + @Override + public CharSequence getOtelTraceStateForWrite() { + throw new UnsupportedOperationException( + "the ot member is held by the trace; read it through its propagation tags"); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index c7808421909..bbea1d9f89b 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java @@ -210,6 +210,49 @@ static SamplingState samplingState( return new SamplingState(samplingPriority, otelTraceState, decisionMaker, knuthSamplingRate); } + /** + * Shared {@link InheritedSamplingState}s, by pending checks, {@link #sharedPriorityIndex} and + * {@link #sharedDecisionMakerIndex}. Filled on first use; a racing fill builds an equal state. + */ + private static final InheritedSamplingState[] SHARED_INHERITED_SAMPLING_STATES = + new InheritedSamplingState + [InheritedSamplingState.CHECK_COMBINATIONS + * SHARED_SAMPLING_STATES.length + * SHARED_SAMPLING_STATES[0].length]; + + /** An inherited state for this decision, shared when it has no sampling rate. */ + static SamplingState inheritedSamplingState( + int samplingPriority, + TagValue decisionMaker, + TagValue knuthSamplingRate, + byte pendingChecks) { + if (knuthSamplingRate == null) { + int priorityIndex = sharedPriorityIndex(samplingPriority); + int decisionMakerIndex = sharedDecisionMakerIndex(decisionMaker); + if (priorityIndex >= 0 && decisionMakerIndex >= 0) { + int index = + (pendingChecks * SHARED_SAMPLING_STATES.length + priorityIndex) + * SHARED_SAMPLING_STATES[0].length + + decisionMakerIndex; + InheritedSamplingState shared = SHARED_INHERITED_SAMPLING_STATES[index]; + if (shared == null) { + shared = + new InheritedSamplingState( + samplingPriority, + decisionMakerIndex == 0 + ? null + : DECISION_MAKER_TAG_VALUES[decisionMakerIndex - 1], + null, + pendingChecks); + SHARED_INHERITED_SAMPLING_STATES[index] = shared; + } + return shared; + } + } + return new InheritedSamplingState( + samplingPriority, decisionMaker, knuthSamplingRate, pendingChecks); + } + private static int sharedPriorityIndex(int samplingPriority) { if (samplingPriority == PrioritySampling.UNSET) { return 0; @@ -271,10 +314,12 @@ private static int sharedDecisionMakerIndex(TagValue decisionMaker) { private volatile TagValue traceIdHighOrderBitsHexTagValue; /** - * The original W3C tracestate - * header value. + * The trace's W3C tracestate + * header value: null, the value, or, when it has an {@code ot} member, a {@link + * W3CTracestate} that also holds the member for an {@link InheritedSamplingState}. Only + * extraction sets it, before the trace is shared. Read it through {@link #tracestate()}. */ - protected volatile String tracestate; + private volatile Object tracestate; /** * The {@link PTagsFactory#PROPAGATION_ERROR_TAG_KEY propagation tag error} value, {@code null @@ -308,10 +353,17 @@ private static int sharedDecisionMakerIndex(TagValue decisionMaker) { this.tagPairs = tagPairs; this.canChangeDecisionMaker = decisionMakerTagValue == null; this.traceSource = traceSource; - this.tracestate = tracestate; - this.samplingState = - initialSamplingState( - samplingPriority, rawOtelTraceState, otelOriginalSize, decisionMakerTagValue); + if (rawOtelTraceState != null) { + this.tracestate = + new W3CTracestate( + tracestate, rawOtelTraceState, otelOriginalSize, PrioritySampling.UNSET, null); + this.samplingState = + inheritedSamplingState( + samplingPriority, decisionMakerTagValue, null, InheritedSamplingState.NO_CHECKS); + } else { + this.tracestate = tracestate; + this.samplingState = samplingState(samplingPriority, null, decisionMakerTagValue, null); + } this.origin = origin; this.lastParentId = lastParentId; this.orgPropagationMarkerTagValue = orgPropagationMarkerTagValue; @@ -504,7 +556,7 @@ private SamplingState nextSamplingState( nextDecisionMakerTagValue( current, samplingPriority, samplingMechanism, updateDecisionMaker); if (current.getSamplingPriority() == samplingPriority - && current.getOtelTraceState() == nextOtelTraceState + && otelTraceStateOf(current) == nextOtelTraceState && Objects.equals(current.getDecisionMaker(), nextDecisionMakerTagValue) && Objects.equals(current.getKnuthSamplingRate(), nextKnuthSamplingRateTagValue)) { return current; @@ -554,23 +606,6 @@ private TagValue nextDecisionMakerTagValue( return nextDecisionMakerTagValue; } - private static SamplingState initialSamplingState( - int samplingPriority, - CharSequence rawOtelTraceState, - int otelOriginalSize, - TagValue decisionMakerTagValue) { - if (rawOtelTraceState != null) { - return new InheritedSamplingState( - samplingPriority, - decisionMakerTagValue, - null, - rawOtelTraceState, - otelOriginalSize, - InheritedSamplingState.NO_CHECKS); - } - return samplingState(samplingPriority, null, decisionMakerTagValue, null); - } - /** * Records a decision against an extracted otel member that hasn't been parsed, without parsing * it. For an external override or unknown mechanism, reconciling the member is only a @@ -585,7 +620,7 @@ private SamplingState deferOtelCheck( TagValue knuthSamplingRate, boolean updateDecisionMaker) { if (!(current instanceof InheritedSamplingState) - || ((InheritedSamplingState) current).isResolved() + || inheritedOtelMember().isResolved(((InheritedSamplingState) current).pendingChecks()) || (samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE && samplingMechanism != SamplingMechanism.UNKNOWN)) { return null; @@ -604,7 +639,7 @@ private SamplingState deferOtelCheck( return current; } clearCachedHeader(W3C); - return inherited.withDecision( + return inheritedSamplingState( samplingPriority, decisionMaker, knuthSamplingRate, pendingChecks); } @@ -1026,7 +1061,47 @@ private static TagValue asTagValue(CharSequence value) { @Override public String getW3CTracestate() { - return this.tracestate; + return tracestate(); + } + + /** The trace's tracestate header value, or null. */ + final String tracestate() { + Object current = tracestate; + return current instanceof W3CTracestate + ? ((W3CTracestate) current).tracestate + : (String) current; + } + + /** + * The trace's inherited {@code ot} member, which an {@link InheritedSamplingState} defers to. + */ + final W3CTracestate inheritedOtelMember() { + return (W3CTracestate) tracestate; + } + + @Override + public CharSequence getOtelTraceState(SamplingState samplingState) { + return otelTraceStateOf(samplingState); + } + + /** The {@code ot} member of {@code samplingState} as it's written. */ + CharSequence getOtelTraceStateForWrite(SamplingState samplingState) { + if (samplingState instanceof InheritedSamplingState) { + return inheritedOtelMember() + .otelTraceStateForWrite(((InheritedSamplingState) samplingState).pendingChecks()); + } + return samplingState.getOtelTraceStateForWrite(); + } + + /** + * Whether writing {@code otelTraceState}, the {@code ot} member of {@code samplingState} as + * it's written, at the member's position in the tracestate leaves the tracestate unchanged. + */ + boolean preservesOtelPosition(SamplingState samplingState, CharSequence otelTraceState) { + if (samplingState instanceof InheritedSamplingState) { + return inheritedOtelMember().preservesOtelPosition(otelTraceState); + } + return samplingState.preservesOtelPosition(tracestate(), otelTraceState); } @Override @@ -1035,7 +1110,9 @@ public String getW3CTracestate(SamplingState samplingState) { if (cache == null || cache.samplingState != samplingState) { cache = new TracestateCacheEntry( - samplingState, W3CPTagsCodec.rebuildTracestate(samplingState, tracestate)); + samplingState, + W3CPTagsCodec.rebuildTracestate( + tracestate(), getOtelTraceStateForWrite(samplingState))); tracestateCache = cache; } return cache.tracestate; @@ -1069,7 +1146,7 @@ public void updateW3CTracestateFrom(W3CTracestate source, int overridingPriority // as the trace context's own priority override reconciles its member pendingChecks = InheritedSamplingState.withCheck(pendingChecks, overridingPriority > 0); } - setTracestate(source.tracestate); + setTracestate(source); SamplingState current; SamplingState next; do { @@ -1081,12 +1158,10 @@ public void updateW3CTracestateFrom(W3CTracestate source, int overridingPriority InheritedSamplingState.withCheck(nextPendingChecks, samplingPriority > 0); } next = - new InheritedSamplingState( + inheritedSamplingState( samplingPriority, getDecisionMakerTagValue(current), getKnuthSamplingRateTagValue(current), - source.rawOtelTraceState, - source.otelOriginalSize, nextPendingChecks); } while (!SAMPLING_STATE_UPDATER.compareAndSet(this, current, next)); clearCachedHeader(W3C); @@ -1115,16 +1190,22 @@ private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) } /** - * Replaces the trace's tracestate. Only extraction does this, before the trace is shared, so - * the tracestate needn't be published together with the sampling state. + * Replaces the trace's tracestate, a header value or a {@link W3CTracestate} holding its {@code + * ot} member. Only extraction does this, before the trace is shared, so the tracestate needn't + * be published together with the sampling state. */ - private void setTracestate(String tracestate) { + private void setTracestate(Object tracestate) { this.tracestate = tracestate; tracestateCache = null; clearCachedHeader(W3C); } - private static OtelTraceState otelTraceStateOf(SamplingState samplingState) { + /** The {@code ot} member of {@code samplingState}, parsing an inherited one on first read. */ + private OtelTraceState otelTraceStateOf(SamplingState samplingState) { + if (samplingState instanceof InheritedSamplingState) { + return inheritedOtelMember() + .otelTraceState(((InheritedSamplingState) samplingState).pendingChecks()); + } return (OtelTraceState) samplingState.getOtelTraceState(); } @@ -1145,11 +1226,17 @@ public void updateAndLockDecisionMaker(PropagationTags source) { do { current = samplingState; next = - samplingState( - current.getSamplingPriority(), - otelTraceStateOf(current), - decisionMakerTagValue, - getKnuthSamplingRateTagValue(current)); + current instanceof InheritedSamplingState + ? inheritedSamplingState( + current.getSamplingPriority(), + decisionMakerTagValue, + getKnuthSamplingRateTagValue(current), + ((InheritedSamplingState) current).pendingChecks()) + : samplingState( + current.getSamplingPriority(), + otelTraceStateOf(current), + decisionMakerTagValue, + getKnuthSamplingRateTagValue(current)); } while (!SAMPLING_STATE_UPDATER.compareAndSet(this, current, next)); if (decisionMakerTagValue != null) { clearCachedHeader(DATADOG); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index 883472c57c9..e75065747ec 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -115,7 +115,7 @@ private static R parse(String value, Result result) { } } - // The otel member is kept raw and parsed on first read; see InheritedSamplingState + // The otel member is kept raw and parsed on first read; see W3CTracestate CharSequence rawOtelTraceState = null; int otelOriginalSize = 0; if (otelMemberStart != -1) { @@ -307,7 +307,7 @@ protected int estimateHeaderSize( if (lastParent != null) { size += lastParent.length() + 3; } - String originalTracestate = pTags.tracestate; + String originalTracestate = pTags.tracestate(); boolean includesOriginalTracestate = false; if (originalTracestate != null && pTags instanceof W3CPTags) { W3CPTags w3CPTags = (W3CPTags) pTags; @@ -320,13 +320,13 @@ protected int estimateHeaderSize( size += originalTracestate.length(); includesOriginalTracestate = true; } - CharSequence otelTraceState = samplingState.getOtelTraceStateForWrite(); + CharSequence otelTraceState = pTags.getOtelTraceStateForWrite(samplingState); if (otelTraceState != null) { if (includesOriginalTracestate && otelTraceState instanceof OtelTraceState) { size -= ((OtelTraceState) otelTraceState).getOriginalSize(); } else if (includesOriginalTracestate && samplingState instanceof InheritedSamplingState) { // the unparsed member, written as it arrived - size -= ((InheritedSamplingState) samplingState).otelOriginalSize(); + size -= pTags.inheritedOtelMember().otelOriginalSize; } size += OTEL_MEMBER_KEY.length() + otelTraceState.length() + 1; } @@ -394,15 +394,15 @@ protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingStat size = 0; } if (size == 0) { - String original = ptags.tracestate; + String original = ptags.tracestate(); String trimmed = original == null ? null : original.trim(); - if (canForwardRawTracestate(trimmed, samplingState.getOtelTraceStateForWrite())) { + if (canForwardRawTracestate(trimmed, ptags.getOtelTraceStateForWrite(samplingState))) { sb.append(trimmed); return EMPTY_SIZE + 1; } } // Append the managed OTel member and all other non-Datadog list-members - if (appendOtelAndVendorMembers(sb, samplingState, ptags.tracestate, size != 0)) { + if (appendOtelAndVendorMembers(sb, ptags, samplingState, size != 0)) { // We don't care about the total size in bytes here, but only the fact that we added something // that should be returned size = Math.max(size, EMPTY_SIZE + 1); @@ -816,7 +816,7 @@ private static int cleanUpAndAppendUnknown(StringBuilder sb, W3CPTags w3CPTags, || w3CPTags.ddMemberStart >= w3CPTags.ddMemberValueEnd) { return size; } - String original = w3CPTags.tracestate; + String original = w3CPTags.tracestate(); int elementStart = w3CPTags.ddMemberStart + EMPTY_SIZE; // skip over 'dd=' int okSize = size; while (elementStart < w3CPTags.ddMemberValueEnd && size < MAX_HEADER_SIZE) { @@ -857,11 +857,15 @@ private static int cleanUpAndAppendUnknown(StringBuilder sb, W3CPTags w3CPTags, } private static boolean appendOtelAndVendorMembers( - StringBuilder sb, SamplingState samplingState, String original, boolean hasDatadogMember) { - CharSequence otelTraceState = samplingState.getOtelTraceStateForWrite(); + StringBuilder sb, PTags ptags, SamplingState samplingState, boolean hasDatadogMember) { + String original = ptags.tracestate(); + CharSequence otelTraceState = ptags.getOtelTraceStateForWrite(samplingState); int remainingMembers = MAX_MEMBER_COUNT - (hasDatadogMember ? 1 : 0); boolean memberAppended = false; - boolean preserveOtelPosition = samplingState.preservesInheritedOtelPosition(original); + boolean preserveOtelPosition = + original != null + && otelTraceState != null + && ptags.preservesOtelPosition(samplingState, otelTraceState); if (!preserveOtelPosition && otelTraceState != null && remainingMembers > 0) { appendMember(sb, OTEL_MEMBER_KEY, otelTraceState.toString()); remainingMembers--; @@ -914,8 +918,7 @@ public static boolean isUnchangedInheritedOtelMember( return otelMemberCount == 1; } - public static String rebuildTracestate(SamplingState samplingState, String original) { - CharSequence otelTraceState = samplingState.getOtelTraceStateForWrite(); + public static String rebuildTracestate(String original, CharSequence otelTraceState) { // TODO Consider a raw passthrough for unchanged state after checking dd= is not duplicated. StringBuilder result = new StringBuilder(MAX_HEADER_SIZE); int memberCount = 0; @@ -1226,18 +1229,18 @@ private static W3CPTags empty( } private static class W3CPTags extends PTags { - /** The index of the first tracestate list-member position in {@link #tracestate}. */ + /** The index of the first tracestate list-member position in the tracestate. */ private final int firstMemberStart; /** - * The index of the Datadog tracestate list-member (dd=) position in {@link #tracestate}, {@code - * -1 if Datadog list-member not found}. + * The index of the Datadog tracestate list-member (dd=) position in the tracestate, {@code -1 + * if Datadog list-member not found}. */ private final int ddMemberStart; /** - * The index of the end Datadog tracestate list-member (dd=) in {@link #tracestate}, {@code -1 - * if Datadog list-member not found}. + * The index of the end Datadog tracestate list-member (dd=) in the tracestate, {@code -1 if + * Datadog list-member not found}. */ private final int ddMemberValueEnd; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CTracestate.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CTracestate.java index 905a86f4025..939ee0a6df2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CTracestate.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CTracestate.java @@ -1,14 +1,25 @@ package datadog.trace.core.propagation.ptags; import datadog.trace.api.sampling.PrioritySampling; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; /** * What merging a W3C {@code tracestate} into another context's propagation tags reads from it, * without building propagation tags for it. Obtained from {@link #parse(String)}. + * + *

When the tracestate has an {@code ot} member, the trace keeps this as its tracestate, and it + * parses the member on first read under the pending checks of an {@link InheritedSamplingState}. */ public final class W3CTracestate { static final W3CTracestate NONE = new W3CTracestate(null, null, 0, PrioritySampling.UNSET, null); + private static final byte POSITION_UNKNOWN = 0; + private static final byte POSITION_PRESERVED = 1; + private static final byte POSITION_REPLACED = 2; + + private static final AtomicReferenceFieldUpdater RESOLVED_UPDATER = + AtomicReferenceFieldUpdater.newUpdater(W3CTracestate.class, Resolved.class, "resolved"); + /** The header value, or null if it's rejected. */ final String tracestate; @@ -21,6 +32,12 @@ public final class W3CTracestate { private final int samplingPriority; private final CharSequence lastParentId; + /** The member parsed under the checks it was parsed for, or null until it's first read. */ + private volatile Resolved resolved; + + /** Whether the raw member, written at its position, leaves {@link #tracestate} unchanged. */ + private volatile byte rawPosition; + W3CTracestate( String tracestate, CharSequence rawOtelTraceState, @@ -48,4 +65,89 @@ public int getSamplingPriority() { public CharSequence getLastParentId() { return lastParentId; } + + /** + * The {@code ot} member parsed, with {@code pendingChecks} applied. Racing readers agree on one + * instance, so identity checks on otel states keep skipping redundant work. + */ + OtelTraceState otelTraceState(byte pendingChecks) { + Resolved current = resolved; + if (current != null && current.pendingChecks == pendingChecks) { + return current.otelTraceState; + } + OtelTraceState otelTraceState = OtelTraceState.parse(rawOtelTraceState, otelOriginalSize); + if (InheritedSamplingState.checksSampled(pendingChecks)) { + otelTraceState = checked(otelTraceState, true); + } + if (InheritedSamplingState.checksNotSampled(pendingChecks)) { + otelTraceState = checked(otelTraceState, false); + } + if (RESOLVED_UPDATER.compareAndSet( + this, current, new Resolved(pendingChecks, otelTraceState))) { + return otelTraceState; + } + Resolved raced = resolved; + return raced != null && raced.pendingChecks == pendingChecks + ? raced.otelTraceState + : otelTraceState; + } + + /** + * The {@code ot} member as it's written, equal in content to {@link #otelTraceState(byte)}: the + * raw member when parsing it and applying the checks would leave it as is, which is the usual + * case when relaying a trace unchanged, so injection doesn't have to parse it. + */ + CharSequence otelTraceStateForWrite(byte pendingChecks) { + if (!isResolved(pendingChecks) + && OtelTraceState.isUnchangedByChecks( + rawOtelTraceState, + InheritedSamplingState.checksSampled(pendingChecks), + InheritedSamplingState.checksNotSampled(pendingChecks))) { + return rawOtelTraceState; + } + return otelTraceState(pendingChecks); + } + + /** Whether the member has been parsed for {@code pendingChecks}. */ + boolean isResolved(byte pendingChecks) { + Resolved current = resolved; + return current != null && current.pendingChecks == pendingChecks; + } + + /** + * Whether writing {@code otelTraceState} at the {@code ot} member's position leaves the + * tracestate unchanged. Cached for the raw member, which every injection of a relayed trace + * writes. + */ + boolean preservesOtelPosition(CharSequence otelTraceState) { + if (otelTraceState != rawOtelTraceState) { + return W3CPTagsCodec.isUnchangedInheritedOtelMember(tracestate, otelTraceState); + } + byte position = rawPosition; + if (position == POSITION_UNKNOWN) { + position = + W3CPTagsCodec.isUnchangedInheritedOtelMember(tracestate, otelTraceState) + ? POSITION_PRESERVED + : POSITION_REPLACED; + rawPosition = position; + } + return position == POSITION_PRESERVED; + } + + private static OtelTraceState checked(OtelTraceState otelTraceState, boolean sampled) { + return otelTraceState == null || otelTraceState.isConsistentWith(sampled) + ? otelTraceState + : otelTraceState.withoutThreshold(); + } + + /** A parse of the member and the checks it applied. */ + private static final class Resolved { + final byte pendingChecks; + final OtelTraceState otelTraceState; + + Resolved(byte pendingChecks, OtelTraceState otelTraceState) { + this.pendingChecks = pendingChecks; + this.otelTraceState = otelTraceState; + } + } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java index dc6495ff9a9..156cbf1f835 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OtelTraceStatePropagationTest.java @@ -68,7 +68,7 @@ void forwardsOpaqueOtelStateAfterSamplingPriorityChanges() { tags.updateTraceSamplingPriority(USER_KEEP, MANUAL); - assertEquals("simplefield;blah;foo", tags.samplingState().getOtelTraceState().toString()); + assertEquals("simplefield;blah;foo", tags.getOtelTraceState(tags.samplingState()).toString()); assertTrue(tags.headerValue(W3C).contains("ot=simplefield;blah;foo")); assertEquals( "ot=simplefield;blah;foo,vendor=state", tags.getW3CTracestate(tags.samplingState())); @@ -211,7 +211,7 @@ void forceKeepRemovesLocallyGeneratedProbabilityState() { tags.forceKeep(MANUAL); assertEquals(USER_KEEP, tags.samplingState().getSamplingPriority()); - assertNull(tags.samplingState().getOtelTraceState()); + assertNull(tags.getOtelTraceState(tags.samplingState())); } @Test @@ -221,7 +221,7 @@ void limiterDemotionDoesNotFabricateState() { assertTrue( tags.tryUpdateProbabilitySamplingDecision(SAMPLER_DROP, AGENT_RATE, 1.0, true, 1L, false)); - assertNull(tags.samplingState().getOtelTraceState()); + assertNull(tags.getOtelTraceState(tags.samplingState())); } @Test @@ -233,7 +233,7 @@ void localProbabilityDecisionDropsInheritedThresholdAtFullRate() { tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, false, 1L, false)); assertEquals( - "rv:00000000000000;future:value", tags.samplingState().getOtelTraceState().toString()); + "rv:00000000000000;future:value", tags.getOtelTraceState(tags.samplingState()).toString()); } @Test @@ -245,7 +245,7 @@ void localHalfRateDropsInheritedThresholdEvenWhenRandomValueAgrees() { tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, false, 1L, false)); assertEquals( - "rv:ffffffffffffff;future:value", tags.samplingState().getOtelTraceState().toString()); + "rv:ffffffffffffff;future:value", tags.getOtelTraceState(tags.samplingState()).toString()); } @Test @@ -257,7 +257,7 @@ void localProbabilityDecisionDropsThresholdWhenInheritedRandomValueDisagrees() { tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 0.5, false, 1L, false)); assertEquals( - "rv:00000000000000;future:value", tags.samplingState().getOtelTraceState().toString()); + "rv:00000000000000;future:value", tags.getOtelTraceState(tags.samplingState()).toString()); } @Test @@ -267,7 +267,7 @@ void localProbabilityDecisionDropsThresholdWithoutInheritedRandomValue() { assertTrue( tags.tryUpdateProbabilitySamplingDecision(SAMPLER_KEEP, AGENT_RATE, 1.0, false, 1L, false)); - assertEquals("future:value", tags.samplingState().getOtelTraceState().toString()); + assertEquals("future:value", tags.getOtelTraceState(tags.samplingState()).toString()); } @Test diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java index e23ba38ceec..5144e6bcccd 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java @@ -22,7 +22,7 @@ void samePriorityStillRecordsANewCheck() { tags.updateTraceSamplingPriority(SAMPLER_DROP, EXTERNAL_OVERRIDE); - CharSequence otelTraceState = tags.samplingState().getOtelTraceState(); + CharSequence otelTraceState = tags.getOtelTraceState(tags.samplingState()); assertFalse(otelTraceState.toString().contains("th:"), otelTraceState.toString()); } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelExtractionTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelExtractionTest.java index 9de4ac7a74f..2b6b736a74f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelExtractionTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelExtractionTest.java @@ -95,6 +95,8 @@ private PropagationTags extractAndFinishLeaf(Map headers) { private static void assertUnparsed(PropagationTags tags) { InheritedSamplingState state = assertInstanceOf(InheritedSamplingState.class, tags.samplingState()); - assertFalse(state.isResolved(), "a leaf must not parse the inherited otel member"); + assertFalse( + ((PTagsFactory.PTags) tags).inheritedOtelMember().isResolved(state.pendingChecks()), + "a leaf must not parse the inherited otel member"); } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelMemberTest.java similarity index 76% rename from dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java rename to dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelMemberTest.java index 01d5817ecb6..ab324448ffb 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelMemberTest.java @@ -1,6 +1,6 @@ package datadog.trace.core.propagation.ptags; -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.UNSET; import static datadog.trace.core.propagation.ptags.InheritedSamplingState.NO_CHECKS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -10,7 +10,8 @@ import org.junit.jupiter.api.Test; import org.tabletest.junit.TableTest; -class InheritedSamplingStateTest { +/** The trace's inherited {@code ot} member parses on first read under its pending checks. */ +class InheritedOtelMemberTest { private static final int ORIGINAL_SIZE = 40; /** @@ -40,41 +41,41 @@ void resolvesAsEagerParseThenChecks(String raw) { if (checkNotSampled) { checks = InheritedSamplingState.withCheck(checks, false); } - InheritedSamplingState state = state(raw, checks); + W3CTracestate member = member(raw); String sampledFirst = eager(raw, checkSampled, checkNotSampled, true); String notSampledFirst = eager(raw, checkSampled, checkNotSampled, false); assertEquals(sampledFirst, notSampledFirst, "check order mustn't matter"); - assertEquals(sampledFirst, String.valueOf(state.getOtelTraceState())); + assertEquals(sampledFirst, String.valueOf(member.otelTraceState(checks))); } } } @Test void parsesOnlyOnFirstRead() { - InheritedSamplingState state = state("rv:ef284ace7a91e1;th:e6666666666668", NO_CHECKS); + W3CTracestate member = member("rv:ef284ace7a91e1;th:e6666666666668"); - assertFalse(state.isResolved()); - state.getOtelTraceState(); - assertTrue(state.isResolved()); + assertFalse(member.isResolved(NO_CHECKS)); + member.otelTraceState(NO_CHECKS); + assertTrue(member.isResolved(NO_CHECKS)); } @Test void repeatedReadsReturnOneInstance() { - InheritedSamplingState state = state("rv:ef284ace7a91e1;th:e6666666666668", NO_CHECKS); + W3CTracestate member = member("rv:ef284ace7a91e1;th:e6666666666668"); - assertSame(state.getOtelTraceState(), state.getOtelTraceState()); + assertSame(member.otelTraceState(NO_CHECKS), member.otelTraceState(NO_CHECKS)); } @Test void keepsOriginalSizeForTheHeaderEstimate() { - InheritedSamplingState state = state("rv:ef284ace7a91e1;th:e6666666666668", NO_CHECKS); + W3CTracestate member = member("rv:ef284ace7a91e1;th:e6666666666668"); - assertEquals(ORIGINAL_SIZE, ((OtelTraceState) state.getOtelTraceState()).getOriginalSize()); + assertEquals(ORIGINAL_SIZE, member.otelTraceState(NO_CHECKS).getOriginalSize()); } - private static InheritedSamplingState state(String raw, byte checks) { - return new InheritedSamplingState(SAMPLER_KEEP, null, null, raw, ORIGINAL_SIZE, checks); + private static W3CTracestate member(String raw) { + return new W3CTracestate("ot=" + raw, raw, ORIGINAL_SIZE, UNSET, null); } /** What extraction did before deferring: parse, then check at each step. */ diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java index 7cd89679d7c..4f5ee414dbf 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java @@ -1,6 +1,6 @@ package datadog.trace.core.propagation.ptags; -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.UNSET; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; @@ -39,8 +39,8 @@ class OtelMemberForWriteTest { void writtenContentMatchesTheParsedMember() { for (String member : MEMBERS) { for (int checks = 0; checks < 4; ++checks) { - CharSequence parsed = state(member, checks).getOtelTraceState(); - CharSequence written = state(member, checks).getOtelTraceStateForWrite(); + CharSequence parsed = member(member).otelTraceState(pending(checks)); + CharSequence written = member(member).otelTraceStateForWrite(pending(checks)); assertEquals( parsed == null ? null : parsed.toString(), written == null ? null : written.toString(), @@ -51,20 +51,20 @@ void writtenContentMatchesTheParsedMember() { @Test void unchangedMemberIsWrittenRawWithoutParsing() { - InheritedSamplingState consistent = state("rv:c8b1e2d3f4a596;th:8", sampledCheck()); - CharSequence written = consistent.getOtelTraceStateForWrite(); + W3CTracestate consistent = member("rv:c8b1e2d3f4a596;th:8"); + CharSequence written = consistent.otelTraceStateForWrite(sampledCheck()); assertTrue(written instanceof SubSequence, String.valueOf(written)); - assertFalse(consistent.isResolved()); + assertFalse(consistent.isResolved(sampledCheck())); - InheritedSamplingState inconsistent = state("rv:c8b1e2d3f4a596;th:8", notSampledCheck()); - assertTrue(inconsistent.getOtelTraceStateForWrite() instanceof OtelTraceState); + W3CTracestate inconsistent = member("rv:c8b1e2d3f4a596;th:8"); + assertTrue(inconsistent.otelTraceStateForWrite(notSampledCheck()) instanceof OtelTraceState); } @Test void parsedMemberIsWrittenAsParsed() { - InheritedSamplingState state = state("rv:c8b1e2d3f4a596;th:8", sampledCheck()); - CharSequence parsed = state.getOtelTraceState(); - assertSame(parsed, state.getOtelTraceStateForWrite()); + W3CTracestate member = member("rv:c8b1e2d3f4a596;th:8"); + CharSequence parsed = member.otelTraceState(sampledCheck()); + assertSame(parsed, member.otelTraceStateForWrite(sampledCheck())); } private static byte sampledCheck() { @@ -75,7 +75,7 @@ private static byte notSampledCheck() { return InheritedSamplingState.withCheck(InheritedSamplingState.NO_CHECKS, false); } - private static InheritedSamplingState state(String member, int checks) { + private static byte pending(int checks) { byte pending = InheritedSamplingState.NO_CHECKS; if ((checks & 1) != 0) { pending = InheritedSamplingState.withCheck(pending, true); @@ -83,18 +83,17 @@ private static InheritedSamplingState state(String member, int checks) { if ((checks & 2) != 0) { pending = InheritedSamplingState.withCheck(pending, false); } - return state(member, pending); + return pending; } - private static InheritedSamplingState state(String member, byte pending) { + private static W3CTracestate member(String member) { String tracestate = "dd=s:1,ot=" + member; int start = tracestate.indexOf("ot=") + 3; - return new InheritedSamplingState( - SAMPLER_KEEP, - null, - null, + return new W3CTracestate( + tracestate, SubSequence.of(tracestate, start, tracestate.length()), Objects.requireNonNull(member).length() + 4, - pending); + UNSET, + null); } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/ProbabilitySamplingStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/ProbabilitySamplingStateTest.java index b5e0e81f381..9a54330054c 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/ProbabilitySamplingStateTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/ProbabilitySamplingStateTest.java @@ -120,6 +120,6 @@ void laterDecisionStartsFromDeferredOtelState() { assertNull( OtelTraceState.fromProbabilityDecision(TRACE_ID_LOW_ORDER_BITS, 0.1, SAMPLER_KEEP) .forNonProbabilityDecision()); - assertNull(ptags.samplingState().getOtelTraceState()); + assertNull(ptags.getOtelTraceState(ptags.samplingState())); } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/TracestateUpdateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/TracestateUpdateTest.java index 41611dafc06..1b97f102ff6 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/TracestateUpdateTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/TracestateUpdateTest.java @@ -35,7 +35,7 @@ void otelMemberOfNewTracestateIsTaken() { ptags.updateW3CTracestate(WITH_OTEL_MEMBER); - CharSequence otel = ptags.samplingState().getOtelTraceState(); + CharSequence otel = ptags.getOtelTraceState(ptags.samplingState()); assertNotNull(otel); assertEquals("rv:0123456789abcd;th:8", otel.toString()); } @@ -43,11 +43,11 @@ void otelMemberOfNewTracestateIsTaken() { @Test void otelMemberIsDroppedWithItsTracestate() { PropagationTags ptags = FACTORY.fromHeaderValue(W3C, WITH_OTEL_MEMBER); - assertNotNull(ptags.samplingState().getOtelTraceState()); + assertNotNull(ptags.getOtelTraceState(ptags.samplingState())); ptags.updateW3CTracestate("vendor=state"); - assertNull(ptags.samplingState().getOtelTraceState()); + assertNull(ptags.getOtelTraceState(ptags.samplingState())); assertEquals("vendor=state", ptags.getW3CTracestate()); } } From 9d907af7643d2c07f69e26f622baa85ebe248cc1 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 21:21:18 -0400 Subject: [PATCH 5/5] Test reads of the inherited ot member under changed checks Co-Authored-By: Claude Opus 5.5 --- .../propagation/ptags/DeferredOtelCheckTest.java | 13 +++++++++++++ .../propagation/ptags/InheritedOtelMemberTest.java | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java index 5144e6bcccd..03b187ac657 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java @@ -2,6 +2,7 @@ import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; import static datadog.trace.api.sampling.SamplingMechanism.EXTERNAL_OVERRIDE; +import static datadog.trace.core.propagation.PropagationTags.HeaderType.DATADOG; import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -26,6 +27,18 @@ void samePriorityStillRecordsANewCheck() { assertFalse(otelTraceState.toString().contains("th:"), otelTraceState.toString()); } + @Test + void lockingTheDecisionMakerKeepsPendingChecks() { + PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, TRACESTATE); + tags.updateTraceSamplingPriority(SAMPLER_DROP, EXTERNAL_OVERRIDE); + + tags.updateAndLockDecisionMaker( + PropagationTags.factory().fromHeaderValue(DATADOG, "_dd.p.dm=-3")); + + CharSequence otelTraceState = tags.getOtelTraceState(tags.samplingState()); + assertFalse(otelTraceState.toString().contains("th:"), otelTraceState.toString()); + } + @Test void repeatedDecisionKeepsTheState() { PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, TRACESTATE); diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelMemberTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelMemberTest.java index ab324448ffb..b69ef699675 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelMemberTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelMemberTest.java @@ -60,6 +60,18 @@ void parsesOnlyOnFirstRead() { assertTrue(member.isResolved(NO_CHECKS)); } + /** A read under more checks than an earlier one applies them, rather than reusing the parse. */ + @Test + void readUnderOtherChecksAppliesThem() { + // rv >= th, so the member is only consistent with a sampled decision + W3CTracestate member = member("rv:ef284ace7a91e1;th:e6666666666668"); + byte sampled = InheritedSamplingState.withCheck(NO_CHECKS, true); + byte both = InheritedSamplingState.withCheck(sampled, false); + + assertEquals("rv:ef284ace7a91e1;th:e6666666666668", member.otelTraceState(sampled).toString()); + assertEquals("rv:ef284ace7a91e1", member.otelTraceState(both).toString()); + } + @Test void repeatedReadsReturnOneInstance() { W3CTracestate member = member("rv:ef284ace7a91e1;th:e6666666666668");