Skip to content

Commit 45bdd9a

Browse files
Makes ScopePassingSpanSubscriber aware of pending spans
without this change e.g. Reactor Netty spans are not treated as current in the reactor flow with this change once Reactor Netty instrumentation creates a pending span it will be treated as a parent one in the reactor flow fixes gh-2282
1 parent 3d19830 commit 45bdd9a

File tree

4 files changed

+74
-12
lines changed

4 files changed

+74
-12
lines changed

spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java

+44
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.AbstractQueue;
2020
import java.util.Iterator;
2121
import java.util.Queue;
22+
import java.util.concurrent.atomic.AtomicReference;
2223
import java.util.function.BiConsumer;
2324
import java.util.function.BiFunction;
2425
import java.util.function.Function;
@@ -64,6 +65,8 @@ public abstract class ReactorSleuth {
6465

6566
private static final Log log = LogFactory.getLog(ReactorSleuth.class);
6667

68+
private static final String PENDING_SPAN_KEY = "sleuth.pending-span";
69+
6770
private ReactorSleuth() {
6871
}
6972

@@ -607,6 +610,47 @@ public static Context wrapContext(Context context) {
607610
return contextWrappingFunction.apply(context);
608611
}
609612

613+
/**
614+
* Retreives the {@link TraceContext} from the current context.
615+
* @param context Reactor context
616+
* @return {@link TraceContext} or {@code null} if none present
617+
*/
618+
@SuppressWarnings("unchecked")
619+
public static TraceContext getParentTraceContext(Context context, TraceContext fallback) {
620+
AtomicReference<Span> pendingSpanRef = getPendingSpan(context);
621+
if (pendingSpanRef == null || pendingSpanRef.get() == null) {
622+
return fallback;
623+
}
624+
return pendingSpanRef.get().context();
625+
}
626+
627+
/**
628+
* Retreives the pending span from the current context.
629+
* @param context Reactor context
630+
* @return {@code AtomicReference} to span or {@code null} if none present
631+
* @see ReactorSleuth#putPendingSpan(Context, AtomicReference)
632+
*/
633+
@SuppressWarnings("unchecked")
634+
public static AtomicReference<Span> getPendingSpan(ContextView context) {
635+
Object objectSpan = context.getOrDefault(ReactorSleuth.PENDING_SPAN_KEY, null);
636+
if ((objectSpan instanceof AtomicReference)) {
637+
return ((AtomicReference<Span>) objectSpan);
638+
}
639+
return null;
640+
}
641+
642+
/**
643+
* Mutates the {@link Context} to include a mutable reference to a span. Can be used
644+
* when you need to mutate the parent operator context with a span created by a child
645+
* operator.
646+
* @param context Reactor context
647+
* @param span atomic reference of a span
648+
* @return mutated context
649+
*/
650+
public static Context putPendingSpan(Context context, AtomicReference<Span> span) {
651+
return context.put(PENDING_SPAN_KEY, span);
652+
}
653+
610654
/**
611655
* Retrieves span from Reactor context.
612656
* @param tracer tracer

spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ScopePassingSpanSubscriber.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ final class ScopePassingSpanSubscriber<T> implements SpanSubscription<T>, Scanna
5252
@Nullable TraceContext parent) {
5353
this.subscriber = subscriber;
5454
this.currentTraceContext = currentTraceContext;
55-
this.parent = parent;
56-
Context context = parent != null && !parent.equals(ctx.getOrDefault(TraceContext.class, null))
57-
? ctx.put(TraceContext.class, parent) : ctx;
55+
this.parent = ReactorSleuth.getParentTraceContext(ctx, parent);
56+
Context context = this.parent != null && !this.parent.equals(ctx.getOrDefault(TraceContext.class, null))
57+
? ctx.put(TraceContext.class, this.parent) : ctx;
5858
this.context = ReactorSleuth.wrapContext(context);
5959
if (log.isTraceEnabled()) {
60-
log.trace("Parent span [" + parent + "], context [" + this.context + "]");
60+
log.trace("Parent span [" + this.parent + "], context [" + this.context + "]");
6161
}
6262
}
6363

spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/HttpClientBeanPostProcessor.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public Mono<? extends Connection> apply(Mono<? extends Connection> mono) {
115115
// Read in this processor and also in ScopePassingSpanSubscriber
116116
context = ReactorSleuth.wrapContext(context.put(TraceContext.class, invocationContext));
117117
}
118-
return context.put(PendingSpan.class, pendingSpan);
118+
return ReactorSleuth.putPendingSpan(context, pendingSpan);
119119
}).doOnCancel(() -> {
120120
// Check to see if Subscription.cancel() happened before another signal,
121121
// like onComplete() completed the span (clearing the reference).
@@ -153,7 +153,7 @@ HttpClientHandler handler() {
153153

154154
@Override
155155
public void accept(HttpClientRequest req, Connection connection) {
156-
PendingSpan pendingSpan = req.currentContextView().getOrDefault(PendingSpan.class, null);
156+
AtomicReference<Span> pendingSpan = ReactorSleuth.getPendingSpan(req.currentContextView());
157157
if (pendingSpan == null) {
158158
return; // Somehow TracingMapConnect was not invoked.. skip out
159159
}
@@ -240,7 +240,7 @@ HttpClientHandler handler() {
240240
}
241241

242242
void handle(Context context, @Nullable HttpClientResponse resp, @Nullable Throwable error) {
243-
PendingSpan pendingSpan = context.getOrDefault(PendingSpan.class, null);
243+
AtomicReference<Span> pendingSpan = ReactorSleuth.getPendingSpan(context);
244244
if (pendingSpan == null) {
245245
return; // Somehow TracingMapConnect was not invoked.. skip out
246246
}

tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/HttpClientBeanPostProcessorTest.java

+23-5
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616

1717
package org.springframework.cloud.sleuth.instrument.web.client;
1818

19+
import java.util.concurrent.TimeUnit;
20+
import java.util.concurrent.atomic.AtomicBoolean;
1921
import java.util.function.BiConsumer;
2022

2123
import io.netty.bootstrap.Bootstrap;
2224
import org.assertj.core.api.Assertions;
25+
import org.awaitility.Awaitility;
2326
import org.junit.jupiter.api.BeforeEach;
2427
import org.junit.jupiter.api.Test;
2528
import org.junit.jupiter.api.extension.ExtendWith;
@@ -32,7 +35,6 @@
3235
import reactor.netty.Connection;
3336

3437
import org.springframework.cloud.sleuth.TraceContext;
35-
import org.springframework.cloud.sleuth.instrument.web.client.HttpClientBeanPostProcessor.PendingSpan;
3638
import org.springframework.cloud.sleuth.instrument.web.client.HttpClientBeanPostProcessor.TracingMapConnect;
3739

3840
@ExtendWith(MockitoExtension.class)
@@ -58,35 +60,51 @@ public void setup() {
5860
@Test
5961
void mapConnect_should_setup_reactor_context_currentTraceContext() {
6062
TracingMapConnect tracingMapConnect = new TracingMapConnect(() -> traceContext);
63+
AtomicBoolean assertionPassed = new AtomicBoolean();
6164

6265
Mono<Connection> original = Mono.just(connection)
6366
.handle(new BiConsumer<Connection, SynchronousSink<Connection>>() {
6467
@Override
6568
public void accept(Connection t, SynchronousSink<Connection> ctx) {
66-
Assertions.assertThat(ctx.currentContext().get(TraceContext.class)).isSameAs(traceContext);
67-
Assertions.assertThat(ctx.currentContext().get(PendingSpan.class)).isNotNull();
69+
try {
70+
Assertions.assertThat(ctx.currentContext().get(TraceContext.class)).isSameAs(traceContext);
71+
Assertions.assertThat((Object) ctx.currentContext().get("sleuth.pending-span")).isNotNull();
72+
assertionPassed.set(true);
73+
}
74+
catch (AssertionError ae) {
75+
}
6876
}
6977
});
7078

7179
// Wrap and run the assertions
7280
tracingMapConnect.apply(original).log().subscribe();
81+
82+
Awaitility.await().atMost(1, TimeUnit.SECONDS).untilTrue(assertionPassed);
7383
}
7484

7585
@Test
7686
void mapConnect_should_setup_reactor_context_no_currentTraceContext() {
7787
TracingMapConnect tracingMapConnect = new TracingMapConnect(() -> null);
88+
AtomicBoolean assertionPassed = new AtomicBoolean();
7889

7990
Mono<Connection> original = Mono.just(connection)
8091
.handle(new BiConsumer<Connection, SynchronousSink<Connection>>() {
8192
@Override
8293
public void accept(Connection t, SynchronousSink<Connection> ctx) {
83-
Assertions.assertThat(ctx.currentContext().getOrEmpty(TraceContext.class)).isEmpty();
84-
Assertions.assertThat(ctx.currentContext().get(PendingSpan.class)).isNotNull();
94+
try {
95+
Assertions.assertThat(ctx.currentContext().getOrEmpty(TraceContext.class)).isEmpty();
96+
Assertions.assertThat((Object) ctx.currentContext().get("sleuth.pending-span")).isNotNull();
97+
assertionPassed.set(true);
98+
}
99+
catch (AssertionError ae) {
100+
}
85101
}
86102
});
87103

88104
// Wrap and run the assertions
89105
tracingMapConnect.apply(original).log().subscribe();
106+
107+
Awaitility.await().atMost(1, TimeUnit.SECONDS).untilTrue(assertionPassed);
90108
}
91109

92110
}

0 commit comments

Comments
 (0)