Skip to content

Commit 90baeff

Browse files
authored
trace locals (#1007)
Introduce trace locals. Each trace gets its own independently settable copy of the variable.
1 parent 355b224 commit 90baeff

File tree

6 files changed

+264
-0
lines changed

6 files changed

+264
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: improvement
2+
improvement:
3+
description: Introduce trace locals. Each trace gets its own independently settable
4+
copy of the variable.
5+
links:
6+
- https://github.com/palantir/tracing-java/pull/1007
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* (c) Copyright 2023 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.tracing;
18+
19+
import com.palantir.logsafe.Preconditions;
20+
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
21+
import java.util.Map;
22+
import java.util.function.Function;
23+
import java.util.function.Supplier;
24+
import javax.annotation.Nonnull;
25+
import javax.annotation.Nullable;
26+
27+
/**
28+
* Provides trace local variables. Each trace gets its own independent copy of the variable.
29+
*
30+
* A trace local is either set (has a value) or unset (has no value).
31+
*
32+
* Outside of a trace (i.e. when {@link Tracer#hasTraceId()} is false) then the trace local is always unset.
33+
*/
34+
public final class TraceLocal<T> {
35+
36+
@Nullable
37+
private final Function<? super TraceLocal<?>, T> initialValue;
38+
39+
private TraceLocal(@Nullable Supplier<T> initialValue) {
40+
if (initialValue == null) {
41+
this.initialValue = null;
42+
} else {
43+
// eagerly transform supplier to avoid allocation per invocation
44+
// (computeIfAbsent takes a Function)
45+
this.initialValue = _ignored ->
46+
Preconditions.checkNotNull(initialValue.get(), "TraceLocal initial value must not be null");
47+
}
48+
}
49+
50+
public static <T> TraceLocal<T> of() {
51+
return new TraceLocal<>(null);
52+
}
53+
54+
/**
55+
* Creates a trace local variable, with a way of supplying an initial value.
56+
*
57+
* When not already set, and this variable is accessed in a trace with the {@link #get()} method, the supplier
58+
* will be invoked to supply a value (which will then be stored for future accesses).
59+
*
60+
* The supplier is thus normally invoked once per trace, but may be invoked again in case of subsequent
61+
* invocations of {@link #remove()} followed by get.
62+
*/
63+
public static <T> TraceLocal<T> withInitialValue(@Nonnull Supplier<T> initialValue) {
64+
return new TraceLocal<>(Preconditions.checkNotNull(initialValue, "initial value supplier must not be null"));
65+
}
66+
67+
/**
68+
* Retrieve the current value of the trace local, with respect to the current trace.
69+
*
70+
* If there is no current trace, i.e. {@link Tracer#hasTraceId()} is null, then this will return null.
71+
*
72+
* If the value of this trace local has not been set for the current trace, then the supplier passed in the
73+
* constructor will be called to supply a value.
74+
*/
75+
@Nullable
76+
public T get() {
77+
TraceState traceState = Tracer.getTraceState();
78+
79+
if (traceState == null) {
80+
return null;
81+
}
82+
83+
if (initialValue == null) {
84+
// not potentially setting a value, so just grab any current set value
85+
86+
Map<TraceLocal<?>, Object> traceLocals = traceState.getTraceLocals();
87+
88+
if (traceLocals == null) {
89+
return null;
90+
}
91+
92+
return (T) traceLocals.get(this);
93+
} else {
94+
Map<TraceLocal<?>, Object> traceLocals = traceState.getOrCreateTraceLocals();
95+
return (T) traceLocals.computeIfAbsent(this, initialValue);
96+
}
97+
}
98+
99+
/**
100+
* Sets the value of this trace local for the current trace.
101+
*
102+
* Returns the previous value of this trace local if set, or null if the value was previously unset.
103+
*/
104+
@Nullable
105+
public T set(@Nonnull T value) {
106+
if (value == null) {
107+
throw new SafeIllegalArgumentException("value must not be null");
108+
}
109+
110+
TraceState traceState = Tracer.getTraceState();
111+
112+
if (traceState == null) {
113+
return null;
114+
}
115+
116+
return (T) traceState.getOrCreateTraceLocals().put(this, value);
117+
}
118+
119+
/**
120+
* Unsets the value of this trace local.
121+
*
122+
* Returns the previous value of this trace local if set, or null if the value was previously unset.
123+
*/
124+
@Nullable
125+
public T remove() {
126+
TraceState traceState = Tracer.getTraceState();
127+
128+
if (traceState == null) {
129+
return null;
130+
}
131+
132+
Map<TraceLocal<?>, Object> traceLocals = traceState.getTraceLocals();
133+
if (traceLocals == null) {
134+
// no trace locals ever set, short circuit (avoid creating the trace local map)
135+
return null;
136+
}
137+
138+
return (T) traceLocals.remove(this);
139+
}
140+
141+
@Override
142+
public boolean equals(Object obj) {
143+
// identity semantics
144+
return super.equals(obj);
145+
}
146+
147+
@Override
148+
public int hashCode() {
149+
// identity semantics
150+
return super.hashCode();
151+
}
152+
}

tracing/src/main/java/com/palantir/tracing/TraceState.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222
import com.google.common.base.Strings;
2323
import java.io.Serializable;
24+
import java.util.Map;
2425
import java.util.Optional;
26+
import java.util.concurrent.ConcurrentHashMap;
27+
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
2528
import javax.annotation.Nullable;
2629

2730
/**
@@ -39,6 +42,12 @@ final class TraceState implements Serializable {
3942
@Nullable
4043
private final String forUserAgent;
4144

45+
@Nullable
46+
private volatile TraceLocalMap traceLocals;
47+
48+
private static final AtomicReferenceFieldUpdater<TraceState, TraceLocalMap> traceLocalsUpdater =
49+
AtomicReferenceFieldUpdater.newUpdater(TraceState.class, TraceLocalMap.class, "traceLocals");
50+
4251
static TraceState of(String traceId, Optional<String> requestId, Optional<String> forUserAgent) {
4352
checkArgument(!Strings.isNullOrEmpty(traceId), "traceId must be non-empty");
4453
checkNotNull(requestId, "requestId should be not-null");
@@ -50,6 +59,7 @@ private TraceState(String traceId, @Nullable String requestId, @Nullable String
5059
this.traceId = traceId;
5160
this.requestId = requestId;
5261
this.forUserAgent = forUserAgent;
62+
this.traceLocals = null;
5363
}
5464

5565
/**
@@ -79,6 +89,24 @@ String forUserAgent() {
7989
return forUserAgent;
8090
}
8191

92+
Map<TraceLocal<?>, Object> getOrCreateTraceLocals() {
93+
TraceLocalMap result = traceLocalsUpdater.get(this);
94+
95+
if (result == null) {
96+
result = new TraceLocalMap();
97+
if (!traceLocalsUpdater.compareAndSet(this, null, result)) {
98+
return traceLocalsUpdater.get(this);
99+
}
100+
}
101+
102+
return result;
103+
}
104+
105+
@Nullable
106+
Map<TraceLocal<?>, Object> getTraceLocals() {
107+
return traceLocals;
108+
}
109+
82110
@Override
83111
public String toString() {
84112
return "TraceState{"
@@ -87,4 +115,6 @@ public String toString() {
87115
+ "forUserAgent='" + forUserAgent
88116
+ "'}";
89117
}
118+
119+
private static final class TraceLocalMap extends ConcurrentHashMap<TraceLocal<?>, Object> {}
90120
}

tracing/src/main/java/com/palantir/tracing/Tracer.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,17 @@ private static Optional<String> getParentSpanId(@Nullable Trace trace) {
349349
return Optional.empty();
350350
}
351351

352+
@Nullable
353+
static TraceState getTraceState() {
354+
Trace maybeCurrentTrace = currentTrace.get();
355+
356+
if (maybeCurrentTrace == null) {
357+
return null;
358+
}
359+
360+
return maybeCurrentTrace.getTraceState();
361+
}
362+
352363
private static TraceState getTraceState(@Nullable Trace maybeCurrentTrace, SpanType newSpanType) {
353364
if (maybeCurrentTrace != null) {
354365
return maybeCurrentTrace.getTraceState();

tracing/src/test/java/com/palantir/tracing/TraceTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,14 @@ public void testToString() {
4444
.contains(span.getOperation())
4545
.contains(span.getSpanId());
4646
}
47+
48+
@Test
49+
public void testToString_doesNotContainTraceLocals() {
50+
Trace trace = Trace.of(true, TraceState.of("traceId", Optional.empty(), Optional.empty()));
51+
52+
TraceLocal<String> traceLocal = TraceLocal.of();
53+
trace.getTraceState().getOrCreateTraceLocals().put(traceLocal, "secret-value");
54+
55+
assertThat(trace.toString()).doesNotContain("secret");
56+
}
4757
}

tracing/src/test/java/com/palantir/tracing/TracerTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,34 @@ public void testHasUnobservableTrace_unobservableTrace() {
420420
assertThat(Tracer.hasUnobservableTrace()).isFalse();
421421
}
422422

423+
@Test
424+
public void testTraceLocals() {
425+
TraceLocal<String> traceLocal = TraceLocal.withInitialValue(() -> "initial");
426+
427+
Tracer.setSampler(AlwaysSampler.INSTANCE);
428+
Tracer.fastStartSpan("outer");
429+
430+
try (CloseableTracer trace = CloseableTracer.startSpan("operation")) {
431+
assertThat(traceLocal.get()).isEqualTo("initial");
432+
433+
traceLocal.set("some-value");
434+
assertThat(traceLocal.get()).isEqualTo("some-value");
435+
}
436+
437+
try (CloseableTracer trace = CloseableTracer.startSpan("operation")) {
438+
assertThat(traceLocal.get()).isEqualTo("some-value");
439+
traceLocal.set("other-value");
440+
}
441+
442+
assertThat(traceLocal.get()).isEqualTo("other-value");
443+
444+
// outer...
445+
Tracer.fastCompleteSpan();
446+
447+
// outside of a trace, it has no value
448+
assertThat(traceLocal.get()).isEqualTo(null);
449+
}
450+
423451
@Test
424452
public void testSimpleDetachedTrace() {
425453
assertThat(Tracer.hasTraceId()).isFalse();
@@ -671,6 +699,33 @@ public void testNewDetachedTrace_doesNotModifyCurrentState() {
671699
}
672700
}
673701

702+
@Test
703+
public void testDetachedState_traceLocals() {
704+
Tracer.setSampler(AlwaysSampler.INSTANCE);
705+
706+
TraceLocal<String> traceLocal = TraceLocal.withInitialValue(() -> "initial");
707+
708+
try (CloseableTracer ignored = CloseableTracer.startSpan("test")) {
709+
traceLocal.set("outer");
710+
711+
DetachedSpan span =
712+
DetachedSpan.start(Observability.SAMPLE, "12345", Optional.empty(), "op", SpanType.LOCAL);
713+
try (CloseableSpan attached = span.attach()) {
714+
assertThat(traceLocal.get()).isEqualTo("initial");
715+
traceLocal.set("inner");
716+
}
717+
718+
assertThat(traceLocal.get()).isEqualTo("outer");
719+
720+
try (CloseableSpan attached = span.attach()) {
721+
assertThat(traceLocal.get()).isEqualTo("inner");
722+
}
723+
724+
traceLocal.remove();
725+
assertThat(traceLocal.get()).isEqualTo("initial");
726+
}
727+
}
728+
674729
private static void startAndFastCompleteSpan() {
675730
Tracer.fastStartSpan("operation");
676731
Tracer.fastCompleteSpan();

0 commit comments

Comments
 (0)