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()); + } +}