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
new file mode 100644
index 00000000000..8d360ebd2a9
--- /dev/null
+++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java
@@ -0,0 +1,121 @@
+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.
+ *
+ *
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.
+ */
+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");
+
+ 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;
+ this.pendingChecks = pendingChecks;
+ }
+
+ /** Returns the pending checks with one more against {@code sampled} added. */
+ 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, with one more pending check. */
+ InheritedSamplingState withCheck(
+ int samplingPriority,
+ String tracestate,
+ CharSequence decisionMaker,
+ CharSequence knuthSamplingRate,
+ boolean sampled) {
+ return new InheritedSamplingState(
+ samplingPriority,
+ tracestate,
+ decisionMaker,
+ knuthSamplingRate,
+ rawOtelTraceState,
+ otelOriginalSize,
+ withCheck(pendingChecks, sampled));
+ }
+
+ boolean isResolved() {
+ return resolved != null;
+ }
+
+ CharSequence rawOtelTraceState() {
+ return rawOtelTraceState;
+ }
+
+ int otelOriginalSize() {
+ return otelOriginalSize;
+ }
+
+ 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;
+ }
+
+ 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();
+ }
+}
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 fb197efa226..170302bacee 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
@@ -202,10 +202,11 @@ static class PTags extends PropagationTags {
orgPropagationMarkerTagValue,
llmObsTagValues,
null,
- null);
+ null,
+ 0);
}
- // Takes tracestate/otelTraceState up-front to build SamplingState in one allocation.
+ // Takes tracestate and the raw otel member up-front to build SamplingState in one allocation.
PTags(
PTagsFactory factory,
List tagPairs,
@@ -218,7 +219,8 @@ static class PTags extends PropagationTags {
TagValue orgPropagationMarkerTagValue,
@Nonnull LLMObsTagValues llmObsTagValues,
String tracestate,
- OtelTraceState otelTraceState) {
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
assert tagPairs == null || tagPairs.size() % 2 == 0;
this.factory = factory;
this.tagPairs = tagPairs;
@@ -226,7 +228,12 @@ static class PTags extends PropagationTags {
this.traceSource = traceSource;
this.tracestate = tracestate;
this.samplingState =
- initialSamplingState(samplingPriority, tracestate, otelTraceState, decisionMakerTagValue);
+ initialSamplingState(
+ samplingPriority,
+ tracestate,
+ rawOtelTraceState,
+ otelOriginalSize,
+ decisionMakerTagValue);
this.origin = origin;
this.lastParentId = lastParentId;
this.orgPropagationMarkerTagValue = orgPropagationMarkerTagValue;
@@ -255,7 +262,8 @@ static PTags withError(PTagsFactory factory, String error) {
null,
LLMObsTagValues.EMPTY,
null,
- null);
+ null,
+ 0);
pTags.error = error;
return pTags;
}
@@ -265,6 +273,10 @@ public void updateTraceSamplingPriority(int samplingPriority, int samplingMechan
synchronized (samplingStateLock) {
if (samplingPriority != PrioritySampling.UNSET && canChangeDecisionMaker
|| samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) {
+ if (deferOtelCheck(
+ samplingPriority, samplingMechanism, getKnuthSamplingRateTagValue(), true)) {
+ return;
+ }
OtelTraceState nextOtelTraceState =
reconcileOtelTraceState(getOtelTraceState(), samplingPriority, samplingMechanism);
installSamplingState(samplingPriority, samplingMechanism, nextOtelTraceState);
@@ -283,14 +295,21 @@ public boolean tryUpdateTraceSamplingPriority(
if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) {
return false;
}
+ boolean updateDecisionMaker =
+ canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE;
+ TagValue knuthSamplingRate = getKnuthSamplingRateTagValue();
+ if (deferOtelCheck(
+ samplingPriority, samplingMechanism, knuthSamplingRate, updateDecisionMaker)) {
+ return true;
+ }
OtelTraceState nextOtelTraceState =
reconcileOtelTraceState(getOtelTraceState(), samplingPriority, samplingMechanism);
installSamplingState(
samplingPriority,
samplingMechanism,
nextOtelTraceState,
- getKnuthSamplingRateTagValue(),
- canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE);
+ knuthSamplingRate,
+ updateDecisionMaker);
return true;
}
}
@@ -432,16 +451,57 @@ private TagValue nextDecisionMakerTagValue(
private static SamplingState initialSamplingState(
int samplingPriority,
String tracestate,
- OtelTraceState otelTraceState,
+ 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
- && otelTraceState == null
&& decisionMakerTagValue == null) {
return EMPTY_SAMPLING_STATE;
}
- return new SamplingState(
- samplingPriority, tracestate, otelTraceState, decisionMakerTagValue, null);
+ 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
+ * consistency check, which can wait for the first read; a local decision rewrites the member
+ * and parses it. Called with the sampling state lock held.
+ */
+ private boolean deferOtelCheck(
+ int samplingPriority,
+ int samplingMechanism,
+ TagValue knuthSamplingRate,
+ boolean updateDecisionMaker) {
+ SamplingState current = samplingState;
+ if (!(current instanceof InheritedSamplingState)
+ || ((InheritedSamplingState) current).isResolved()
+ || (samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE
+ && samplingMechanism != SamplingMechanism.UNKNOWN)) {
+ return false;
+ }
+ TagValue decisionMaker =
+ nextDecisionMakerTagValue(samplingPriority, samplingMechanism, updateDecisionMaker);
+ clearCachedHeader(W3C);
+ samplingState =
+ ((InheritedSamplingState) current)
+ .withCheck(
+ samplingPriority,
+ tracestate,
+ decisionMaker,
+ knuthSamplingRate,
+ samplingPriority > 0);
+ return true;
}
private static OtelTraceState reconcileOtelTraceState(
@@ -899,6 +959,11 @@ public void updateW3CTracestateFrom(PropagationTags source) {
}
PTags sourcePTags = (PTags) source;
SamplingState sourceState = sourcePTags.samplingState();
+ if (sourceState instanceof InheritedSamplingState
+ && !((InheritedSamplingState) sourceState).isResolved()) {
+ setW3CTracestate(sourceState.getTracestate(), (InheritedSamplingState) sourceState);
+ return;
+ }
CharSequence sourceOtelTraceState = sourceState.getOtelTraceState();
setW3CTracestate(
sourceState.getTracestate(),
@@ -927,6 +992,28 @@ private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState)
}
}
+ /** Takes over an extracted otel member that hasn't been parsed, without parsing it. */
+ private void setW3CTracestate(String tracestate, InheritedSamplingState source) {
+ synchronized (samplingStateLock) {
+ clearCachedHeader(W3C);
+ int samplingPriority = samplingState.getSamplingPriority();
+ byte pendingChecks = source.pendingChecks();
+ if (samplingPriority != PrioritySampling.UNSET) {
+ pendingChecks = InheritedSamplingState.withCheck(pendingChecks, samplingPriority > 0);
+ }
+ this.tracestate = tracestate;
+ this.samplingState =
+ new InheritedSamplingState(
+ samplingPriority,
+ tracestate,
+ getDecisionMakerTagValue(),
+ getKnuthSamplingRateTagValue(),
+ source.rawOtelTraceState(),
+ source.otelOriginalSize(),
+ pendingChecks);
+ }
+ }
+
private OtelTraceState getOtelTraceState() {
return (OtelTraceState) samplingState.getOtelTraceState();
}
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 94c909ff82f..85faf60a0ca 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
@@ -98,18 +98,19 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) {
}
}
- OtelTraceState otelTraceState = null;
+ // The otel member is kept raw and parsed on first read; see InheritedSamplingState
+ CharSequence rawOtelTraceState = null;
+ int otelOriginalSize = 0;
if (otelMemberStart != -1) {
int valueEnd = stripTrailingOWC(value, otelMemberValueStart, otelMemberValueEnd);
- otelTraceState =
- OtelTraceState.parse(
- SubSequence.of(value, otelMemberValueStart, valueEnd),
- memberContributionSize(value, firstMemberStart, otelMemberStart, otelMemberValueEnd));
+ rawOtelTraceState = SubSequence.of(value, otelMemberValueStart, valueEnd);
+ otelOriginalSize =
+ memberContributionSize(value, firstMemberStart, otelMemberStart, otelMemberValueEnd);
}
if (ddMemberIndex == -1) {
// There was no dd member, so create an empty one with the _suffix_
- return empty(tagsFactory, value, otelTraceState);
+ return empty(tagsFactory, value, rawOtelTraceState, otelOriginalSize);
}
List tagPairs = null;
@@ -195,7 +196,8 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) {
firstMemberStart,
ddMemberStart,
ddMemberValueEnd,
- otelTraceState);
+ rawOtelTraceState,
+ otelOriginalSize);
}
if (tagKey.equals(DECISION_MAKER_TAG)) {
decisionMakerTagValue = tagValue;
@@ -264,7 +266,8 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) {
llmObsParentIdTagValue,
llmObsSampleRateTagValue,
llmObsSamplingDecisionTagValue),
- otelTraceState);
+ rawOtelTraceState,
+ otelOriginalSize);
}
@Override
@@ -1004,12 +1007,12 @@ private static int memberContributionSize(
/** Creates tags that preserve unmanaged W3C tracestate members without sampling state. */
static W3CPTags empty(PTagsFactory factory, String original) {
- return empty(factory, original, null);
+ return empty(factory, original, null, 0);
}
private static W3CPTags empty(
- PTagsFactory factory, String original, OtelTraceState otelTraceState) {
- return empty(factory, original, 0, -1, -1, otelTraceState);
+ PTagsFactory factory, String original, CharSequence rawOtelTraceState, int otelOriginalSize) {
+ return empty(factory, original, 0, -1, -1, rawOtelTraceState, otelOriginalSize);
}
private static W3CPTags empty(
@@ -1018,7 +1021,8 @@ private static W3CPTags empty(
int firstMemberStart,
int ddMemberStart,
int ddMemberValueEnd,
- OtelTraceState otelTraceState) {
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
return new W3CPTags(
factory,
null,
@@ -1035,7 +1039,8 @@ private static W3CPTags empty(
null,
null,
LLMObsTagValues.EMPTY,
- otelTraceState);
+ rawOtelTraceState,
+ otelOriginalSize);
}
private static class W3CPTags extends PTags {
@@ -1072,7 +1077,8 @@ public W3CPTags(
CharSequence lastParentId,
TagValue orgPropagationMarkerTagValue,
LLMObsTagValues llmObsTagValues,
- OtelTraceState otelTraceState) {
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
super(
factory,
tagPairs,
@@ -1085,7 +1091,8 @@ public W3CPTags(
orgPropagationMarkerTagValue,
llmObsTagValues,
original,
- otelTraceState);
+ rawOtelTraceState,
+ otelOriginalSize);
this.firstMemberStart = firstMemberStart;
this.ddMemberStart = ddMemberStart;
this.ddMemberValueEnd = ddMemberValueEnd;
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
new file mode 100644
index 00000000000..9de4ac7a74f
--- /dev/null
+++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelExtractionTest.java
@@ -0,0 +1,100 @@
+package datadog.trace.core.propagation.ptags;
+
+import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_X_DATADOG_TAGS_MAX_LENGTH;
+import static datadog.trace.api.TracePropagationStyle.DATADOG;
+import static datadog.trace.api.TracePropagationStyle.TRACECONTEXT;
+import static datadog.trace.bootstrap.instrumentation.api.ContextVisitors.stringValuesMap;
+import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C;
+import static java.util.Arrays.asList;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import datadog.trace.api.Config;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.core.CoreTracer;
+import datadog.trace.core.DDCoreJavaSpecification;
+import datadog.trace.core.propagation.ExtractedContext;
+import datadog.trace.core.propagation.HttpCodec;
+import datadog.trace.core.propagation.PropagationTags;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Real extraction leaves the {@code ot} member unparsed until something reads it. */
+class InheritedOtelExtractionTest extends DDCoreJavaSpecification {
+ private static final String TRACE_PARENT =
+ "00-00000000000000000000000000000001-123456789abcdef0-01";
+ private static final String OTEL_MEMBER = "ot=rv:ef284ace7a91e1;th:e6666666666668";
+
+ @Test
+ void leafFromDatadogUpstreamNeverParses() {
+ Map headers = new HashMap<>();
+ headers.put("x-datadog-trace-id", "1");
+ headers.put("x-datadog-parent-id", "2");
+ headers.put("x-datadog-sampling-priority", "1");
+ headers.put("traceparent", TRACE_PARENT);
+ headers.put("tracestate", "dd=s:1;p:123456789abcdef0," + OTEL_MEMBER);
+
+ PropagationTags tags = extractAndFinishLeaf(headers);
+
+ assertUnparsed(tags);
+ assertTrue(tags.headerValue(W3C).contains(OTEL_MEMBER));
+ }
+
+ @Test
+ void leafFromOtelUpstreamNeverParses() {
+ Map headers = new HashMap<>();
+ headers.put("traceparent", TRACE_PARENT);
+ headers.put("tracestate", OTEL_MEMBER);
+
+ PropagationTags tags = extractAndFinishLeaf(headers);
+
+ assertUnparsed(tags);
+ assertTrue(tags.headerValue(W3C).contains(OTEL_MEMBER));
+ }
+
+ @Test
+ void inconsistentInheritedThresholdIsStillDropped() {
+ // rv < th means drop, but the upstream kept the trace: the threshold can't be forwarded
+ Map headers = new HashMap<>();
+ headers.put("traceparent", TRACE_PARENT);
+ headers.put("tracestate", "ot=rv:0000000000000a;th:8");
+
+ PropagationTags tags = extractAndFinishLeaf(headers);
+
+ assertUnparsed(tags);
+ String outbound = tags.headerValue(W3C);
+ assertTrue(outbound.contains("ot=rv:0000000000000a"));
+ assertFalse(outbound.contains("th:8"));
+ }
+
+ private PropagationTags extractAndFinishLeaf(Map headers) {
+ Config config = mock(Config.class);
+ when(config.getTracePropagationStylesToExtract())
+ .thenReturn(new LinkedHashSet<>(asList(DATADOG, TRACECONTEXT)));
+ when(config.getxDatadogTagsMaxLength()).thenReturn(DEFAULT_TRACE_X_DATADOG_TAGS_MAX_LENGTH);
+ when(config.isApmTracingEnabled()).thenReturn(true);
+
+ CoreTracer tracer = tracerBuilder().build();
+ try {
+ HttpCodec.Extractor extractor = HttpCodec.createExtractor(config, tracer::captureTraceConfig);
+ ExtractedContext extracted =
+ assertInstanceOf(ExtractedContext.class, extractor.extract(headers, stringValuesMap()));
+ AgentSpan span = tracer.buildSpan("test", "leaf").asChildOf(extracted).start();
+ span.finish();
+ return extracted.getPropagationTags();
+ } finally {
+ tracer.close();
+ }
+ }
+
+ 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");
+ }
+}
diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java
new file mode 100644
index 00000000000..9ece70a7a76
--- /dev/null
+++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java
@@ -0,0 +1,97 @@
+package datadog.trace.core.propagation.ptags;
+
+import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP;
+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;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.tabletest.junit.TableTest;
+
+class InheritedSamplingStateTest {
+ private static final int ORIGINAL_SIZE = 40;
+
+ /**
+ * Parsing on first read with pending checks gives what parsing on extract and checking at each
+ * step gave, for every combination of checks and in either order.
+ */
+ @TableTest({
+ "scenario | raw ",
+ "keep consistent | 'rv:ef284ace7a91e1;th:e6666666666668' ",
+ "drop consistent | 'rv:0000000000000a;th:8' ",
+ "random value only | 'rv:ef284ace7a91e1' ",
+ "threshold only | 'th:8' ",
+ "unknown field kept | 'rv:0000000000000a;th:8;future:value' ",
+ "threshold and unknown field | 'th:8;future:value' ",
+ "unknown fields only | 'simplefield;blah;foo' ",
+ "invalid random value | 'rv:zz;th:8;future:value' ",
+ "duplicate random value | 'rv:ef284ace7a91e1;rv:0000000000000a;th:8'",
+ "nothing valid | 'rv:invalid;th:invalid' "
+ })
+ void resolvesAsEagerParseThenChecks(String raw) {
+ for (boolean checkSampled : new boolean[] {false, true}) {
+ for (boolean checkNotSampled : new boolean[] {false, true}) {
+ byte checks = NO_CHECKS;
+ if (checkSampled) {
+ checks = InheritedSamplingState.withCheck(checks, true);
+ }
+ if (checkNotSampled) {
+ checks = InheritedSamplingState.withCheck(checks, false);
+ }
+ InheritedSamplingState state = state(raw, checks);
+
+ 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()));
+ }
+ }
+ }
+
+ @Test
+ void parsesOnlyOnFirstRead() {
+ InheritedSamplingState state = state("rv:ef284ace7a91e1;th:e6666666666668", NO_CHECKS);
+
+ assertFalse(state.isResolved());
+ state.getOtelTraceState();
+ assertTrue(state.isResolved());
+ }
+
+ @Test
+ void repeatedReadsReturnOneInstance() {
+ InheritedSamplingState state = state("rv:ef284ace7a91e1;th:e6666666666668", NO_CHECKS);
+
+ assertSame(state.getOtelTraceState(), state.getOtelTraceState());
+ }
+
+ @Test
+ void keepsOriginalSizeForTheHeaderEstimate() {
+ InheritedSamplingState state = state("rv:ef284ace7a91e1;th:e6666666666668", NO_CHECKS);
+
+ assertEquals(ORIGINAL_SIZE, ((OtelTraceState) state.getOtelTraceState()).getOriginalSize());
+ }
+
+ private static InheritedSamplingState state(String raw, byte checks) {
+ return new InheritedSamplingState(SAMPLER_KEEP, null, null, null, raw, ORIGINAL_SIZE, checks);
+ }
+
+ /** What extraction did before deferring: parse, then check at each step. */
+ private static String eager(
+ String raw, boolean checkSampled, boolean checkNotSampled, boolean sampledFirst) {
+ OtelTraceState state = OtelTraceState.parse(raw, ORIGINAL_SIZE);
+ if (sampledFirst) {
+ state = checkSampled ? check(state, true) : state;
+ state = checkNotSampled ? check(state, false) : state;
+ } else {
+ state = checkNotSampled ? check(state, false) : state;
+ state = checkSampled ? check(state, true) : state;
+ }
+ return String.valueOf(state);
+ }
+
+ private static OtelTraceState check(OtelTraceState state, boolean sampled) {
+ return state == null || state.isConsistentWith(sampled) ? state : state.withoutThreshold();
+ }
+}