From 470b6063666d859389480e031ce53e68d3c5c94f Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 21 Oct 2026 16:00:17 -0400 Subject: [PATCH] Defer parsing the extracted OpenTelemetry tracestate member Extraction parsed the inherited ot member and checked it against the inherited decision on the request thread, at extraction, compound merge and root span construction, though only W3C injection, OTLP export and a local change of decision read it. Keep the raw member in an InheritedSamplingState and parse it on first read. Consistency checks made before then are recorded as pending and applied in order when it's parsed, so the output is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../ptags/InheritedSamplingState.java | 121 ++++++++++++++++++ .../core/propagation/ptags/PTagsFactory.java | 109 ++++++++++++++-- .../core/propagation/ptags/W3CPTagsCodec.java | 37 +++--- .../ptags/InheritedOtelExtractionTest.java | 100 +++++++++++++++ .../ptags/InheritedSamplingStateTest.java | 97 ++++++++++++++ 5 files changed, 438 insertions(+), 26 deletions(-) create mode 100644 dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelExtractionTest.java create mode 100644 dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java 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(); + } +}