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..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
@@ -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,12 +76,14 @@ public CharSequence getKnuthSamplingRate() {
return knuthSamplingRate;
}
- public boolean preservesInheritedOtelPosition() {
- if (tracestate == null) {
- return false;
- }
- CharSequence otelTraceState = getOtelTraceStateForWrite();
- if (otelTraceState == null) {
+ /**
+ * 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 preservesOtelPosition(String tracestate, CharSequence otelTraceState) {
+ if (tracestate == null || otelTraceState == null) {
return false;
}
byte position = otelPosition;
@@ -169,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 df0de673d59..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,52 +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,
- String tracestate,
CharSequence decisionMaker,
CharSequence knuthSamplingRate,
- CharSequence rawOtelTraceState,
- int otelOriginalSize,
byte pendingChecks) {
- super(samplingPriority, tracestate, null, decisionMaker, knuthSamplingRate);
- this.rawOtelTraceState = rawOtelTraceState;
- this.otelOriginalSize = otelOriginalSize;
+ super(samplingPriority, null, decisionMaker, knuthSamplingRate);
this.pendingChecks = pendingChecks;
}
@@ -55,79 +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,
- String tracestate,
- CharSequence decisionMaker,
- CharSequence knuthSamplingRate,
- byte pendingChecks) {
- return new InheritedSamplingState(
- samplingPriority,
- tracestate,
- 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 d80239d9afa..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
@@ -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,131 @@ static class PTags extends PropagationTags {
}
}
+ /**
+ * 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];
+
+ 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);
+ for (int mechanism = 0; mechanism < DECISION_MAKER_TAG_VALUES.length; ++mechanism) {
+ row[mechanism + 1] =
+ new SamplingState(priority, null, DECISION_MAKER_TAG_VALUES[mechanism], null);
+ }
+ }
+ }
+
+ /**
+ * 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,
+ CharSequence otelTraceState,
+ TagValue decisionMaker,
+ TagValue knuthSamplingRate) {
+ 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, 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;
+ }
+ 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.
@@ -192,10 +314,12 @@ static class PTags extends PropagationTags {
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
@@ -229,14 +353,17 @@ static class PTags extends PropagationTags {
this.tagPairs = tagPairs;
this.canChangeDecisionMaker = decisionMakerTagValue == null;
this.traceSource = traceSource;
- this.tracestate = tracestate;
- this.samplingState =
- initialSamplingState(
- samplingPriority,
- tracestate,
- 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;
@@ -368,7 +495,6 @@ public boolean tryUpdateProbabilitySamplingDecision(
next =
new ProbabilitySamplingState(
samplingPriority,
- current.getTracestate(),
nextDecisionMakerTagValue,
nextKnuthSamplingRate,
traceIdLowOrderBits,
@@ -430,15 +556,14 @@ 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;
}
clearCachedHeader(W3C);
- return new SamplingState(
+ return samplingState(
samplingPriority,
- current.getTracestate(),
nextOtelTraceState,
nextDecisionMakerTagValue,
nextKnuthSamplingRateTagValue);
@@ -481,30 +606,6 @@ private TagValue nextDecisionMakerTagValue(
return 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);
- }
- if (samplingPriority == PrioritySampling.UNSET
- && tracestate == null
- && decisionMakerTagValue == null) {
- return EMPTY_SAMPLING_STATE;
- }
- return new SamplingState(samplingPriority, tracestate, 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
@@ -519,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;
@@ -538,12 +639,8 @@ private SamplingState deferOtelCheck(
return current;
}
clearCachedHeader(W3C);
- return inherited.withDecision(
- samplingPriority,
- current.getTracestate(),
- decisionMaker,
- knuthSamplingRate,
- pendingChecks);
+ return inheritedSamplingState(
+ samplingPriority, decisionMaker, knuthSamplingRate, pendingChecks);
}
private static OtelTraceState reconcileOtelTraceState(
@@ -964,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
@@ -972,7 +1109,10 @@ 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(
+ tracestate(), getOtelTraceStateForWrite(samplingState)));
tracestateCache = cache;
}
return cache.tracestate;
@@ -1006,6 +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);
SamplingState current;
SamplingState next;
do {
@@ -1017,20 +1158,17 @@ public void updateW3CTracestateFrom(W3CTracestate source, int overridingPriority
InheritedSamplingState.withCheck(nextPendingChecks, samplingPriority > 0);
}
next =
- new InheritedSamplingState(
+ 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 {
@@ -1043,18 +1181,31 @@ private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState)
nextOtelTraceState = nextOtelTraceState.withoutThreshold();
}
next =
- new SamplingState(
+ 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, 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(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();
}
@@ -1075,12 +1226,17 @@ public void updateAndLockDecisionMaker(PropagationTags source) {
do {
current = samplingState;
next =
- new SamplingState(
- current.getSamplingPriority(),
- current.getTracestate(),
- 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/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..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
@@ -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;
@@ -116,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) {
@@ -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) {
@@ -329,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;
}
@@ -403,15 +394,15 @@ 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())) {
+ 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, 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);
@@ -825,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) {
@@ -866,12 +857,15 @@ private static int cleanUpAndAppendUnknown(StringBuilder sb, W3CPTags w3CPTags,
}
private static boolean appendOtelAndVendorMembers(
- StringBuilder sb, SamplingState samplingState, boolean hasDatadogMember) {
- String original = samplingState.getTracestate();
- 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();
+ boolean preserveOtelPosition =
+ original != null
+ && otelTraceState != null
+ && ptags.preservesOtelPosition(samplingState, otelTraceState);
if (!preserveOtelPosition && otelTraceState != null && remainingMembers > 0) {
appendMember(sb, OTEL_MEMBER_KEY, otelTraceState.toString());
remainingMembers--;
@@ -924,9 +918,7 @@ public static boolean isUnchangedInheritedOtelMember(
return otelMemberCount == 1;
}
- public static String rebuildTracestate(SamplingState samplingState) {
- String original = samplingState.getTracestate();
- 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;
@@ -1237,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..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;
@@ -22,7 +23,19 @@ 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());
+ }
+
+ @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());
}
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 66%
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 9ece70a7a76..b69ef699675 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,53 @@ 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));
+ }
+
+ /** 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() {
- 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, 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 d44a8eb26be..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,19 +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,
+ return new W3CTracestate(
tracestate,
- null,
- null,
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
new file mode 100644
index 00000000000..1b97f102ff6
--- /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.getOtelTraceState(ptags.samplingState());
+ assertNotNull(otel);
+ assertEquals("rv:0123456789abcd;th:8", otel.toString());
+ }
+
+ @Test
+ void otelMemberIsDroppedWithItsTracestate() {
+ PropagationTags ptags = FACTORY.fromHeaderValue(W3C, WITH_OTEL_MEMBER);
+ assertNotNull(ptags.getOtelTraceState(ptags.samplingState()));
+
+ ptags.updateW3CTracestate("vendor=state");
+
+ assertNull(ptags.getOtelTraceState(ptags.samplingState()));
+ assertEquals("vendor=state", ptags.getW3CTracestate());
+ }
+}