Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,17 @@ 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;
private volatile byte otelPosition;

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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
* <p>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.
*
* <p>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<InheritedSamplingState, Object>
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;
}

Expand All @@ -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");
}
}
Loading
Loading