Merge branch 'optimize-reactor-hooks' of https://github.com/robotmrv/spring-cloud-sleuth into robotmrv-optimize-reactor-hooks
This commit is contained in:
@@ -89,9 +89,7 @@ public class SleuthReactorProperties {
|
||||
/**
|
||||
* Decorates on each operator, will be less performing, but logging will always
|
||||
* contain the tracing entries in each operator.
|
||||
* @deprecated to be removed in Sleuth 4.0.0
|
||||
*/
|
||||
@Deprecated
|
||||
DECORATE_ON_EACH,
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,9 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
import static org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration.SLEUTH_REACTOR_EXECUTOR_SERVICE_KEY;
|
||||
import static org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration.TraceReactorConfiguration.SLEUTH_TRACE_REACTOR_KEY;
|
||||
import static org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth.onLastOperatorForOnEachInstrumentation;
|
||||
|
||||
/**
|
||||
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
@@ -125,12 +127,18 @@ class HooksRefresher implements ApplicationListener<RefreshScopeRefreshedEvent>
|
||||
}
|
||||
Hooks.resetOnEachOperator(SLEUTH_TRACE_REACTOR_KEY);
|
||||
Hooks.resetOnLastOperator(SLEUTH_TRACE_REACTOR_KEY);
|
||||
Hooks.resetOnLastOperator(SLEUTH_REACTOR_EXECUTOR_SERVICE_KEY);
|
||||
switch (this.reactorProperties.getInstrumentationType()) {
|
||||
case DECORATE_ON_EACH:
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Decorating onEach operator instrumentation");
|
||||
}
|
||||
Hooks.onEachOperator(SLEUTH_TRACE_REACTOR_KEY, ReactorSleuth.scopePassingSpanOperator(this.context));
|
||||
Hooks.onEachOperator(SLEUTH_TRACE_REACTOR_KEY,
|
||||
ReactorSleuth.onEachOperatorForOnEachInstrumentation(this.context));
|
||||
Hooks.onLastOperator(SLEUTH_TRACE_REACTOR_KEY,
|
||||
ReactorSleuth.onLastOperatorForOnEachInstrumentation(this.context));
|
||||
Schedulers.onScheduleHook(TraceReactorAutoConfiguration.SLEUTH_REACTOR_EXECUTOR_SERVICE_KEY,
|
||||
ReactorSleuth.scopePassingOnScheduleHook(this.context));
|
||||
break;
|
||||
case DECORATE_ON_LAST:
|
||||
if (log.isTraceEnabled()) {
|
||||
@@ -178,15 +186,23 @@ class HookRegisteringBeanDefinitionRegistryPostProcessor implements BeanDefiniti
|
||||
}
|
||||
else if (property == SleuthReactorProperties.InstrumentationType.DECORATE_ON_EACH) {
|
||||
decorateOnEach(springContext);
|
||||
decorateOnLast(onLastOperatorForOnEachInstrumentation(springContext));
|
||||
decorateScheduler(springContext);
|
||||
}
|
||||
else if (property == SleuthReactorProperties.InstrumentationType.DECORATE_ON_LAST) {
|
||||
decorateOnLast(ReactorSleuth.scopePassingSpanOperator(springContext));
|
||||
decorateScheduler(springContext);
|
||||
}
|
||||
else if (property == SleuthReactorProperties.InstrumentationType.MANUAL) {
|
||||
decorateOnLast(ReactorSleuth.springContextSpanOperator(springContext));
|
||||
}
|
||||
}
|
||||
|
||||
private static void decorateScheduler(ConfigurableApplicationContext springContext) {
|
||||
Schedulers.onScheduleHook(TraceReactorAutoConfiguration.SLEUTH_REACTOR_EXECUTOR_SERVICE_KEY,
|
||||
ReactorSleuth.scopePassingOnScheduleHook(springContext));
|
||||
}
|
||||
|
||||
private static void decorateOnLast(Function<? super Publisher<Object>, ? extends Publisher<Object>> function) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Decorating onLast operator instrumentation");
|
||||
@@ -198,7 +214,8 @@ class HookRegisteringBeanDefinitionRegistryPostProcessor implements BeanDefiniti
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Decorating onEach operator instrumentation");
|
||||
}
|
||||
Hooks.onEachOperator(SLEUTH_TRACE_REACTOR_KEY, ReactorSleuth.scopePassingSpanOperator(springContext));
|
||||
Hooks.onEachOperator(SLEUTH_TRACE_REACTOR_KEY,
|
||||
ReactorSleuth.onEachOperatorForOnEachInstrumentation(springContext));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -208,7 +225,7 @@ class HookRegisteringBeanDefinitionRegistryPostProcessor implements BeanDefiniti
|
||||
}
|
||||
Hooks.resetOnEachOperator(SLEUTH_TRACE_REACTOR_KEY);
|
||||
Hooks.resetOnLastOperator(SLEUTH_TRACE_REACTOR_KEY);
|
||||
Schedulers.removeExecutorServiceDecorator(TraceReactorAutoConfiguration.SLEUTH_REACTOR_EXECUTOR_SERVICE_KEY);
|
||||
Schedulers.resetOnScheduleHook(TraceReactorAutoConfiguration.SLEUTH_REACTOR_EXECUTOR_SERVICE_KEY);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public final class TraceReactorAutoConfigurationAccessorConfiguration {
|
||||
}
|
||||
Hooks.resetOnEachOperator(SLEUTH_TRACE_REACTOR_KEY);
|
||||
Hooks.resetOnLastOperator(SLEUTH_TRACE_REACTOR_KEY);
|
||||
Schedulers.removeExecutorServiceDecorator(SLEUTH_REACTOR_EXECUTOR_SERVICE_KEY);
|
||||
Schedulers.resetOnScheduleHook(SLEUTH_REACTOR_EXECUTOR_SERVICE_KEY);
|
||||
}
|
||||
|
||||
public static void setup(ConfigurableApplicationContext context) {
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.cloud.sleuth.autoconfig.instrument.web;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.cloud.sleuth.CurrentTraceContext;
|
||||
import org.springframework.cloud.sleuth.Tracer;
|
||||
import org.springframework.cloud.sleuth.http.HttpServerHandler;
|
||||
import org.springframework.cloud.sleuth.instrument.web.TraceWebFilter;
|
||||
@@ -38,8 +39,8 @@ class TraceWebFluxConfiguration {
|
||||
|
||||
@Bean
|
||||
TraceWebFilter traceFilter(Tracer tracer, HttpServerHandler httpServerHandler,
|
||||
SleuthWebProperties sleuthWebProperties) {
|
||||
TraceWebFilter traceWebFilter = new TraceWebFilter(tracer, httpServerHandler);
|
||||
CurrentTraceContext currentTraceContext, SleuthWebProperties sleuthWebProperties) {
|
||||
TraceWebFilter traceWebFilter = new TraceWebFilter(tracer, httpServerHandler, currentTraceContext);
|
||||
traceWebFilter.setOrder(sleuthWebProperties.getFilterOrder());
|
||||
return traceWebFilter;
|
||||
}
|
||||
|
||||
@@ -20,10 +20,12 @@ import brave.propagation.CurrentTraceContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
public class LazyBeanTests {
|
||||
@@ -54,6 +56,15 @@ public class LazyBeanTests {
|
||||
then(provider.get()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_throw_error_when_no_basic_type() {
|
||||
context.refresh();
|
||||
|
||||
LazyBean<CurrentTraceContext> provider = LazyBean.create(context, CurrentTraceContext.class);
|
||||
|
||||
assertThatCode(() -> provider.getOrError()).isInstanceOf(NoSuchBeanDefinitionException.class);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class BasicConfig {
|
||||
|
||||
|
||||
@@ -166,6 +166,11 @@
|
||||
<artifactId>archunit-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.springframework.cloud.sleuth.TraceContext;
|
||||
import org.springframework.cloud.sleuth.Tracer;
|
||||
import org.springframework.cloud.sleuth.annotation.ContinueSpan;
|
||||
import org.springframework.cloud.sleuth.annotation.NewSpan;
|
||||
import org.springframework.cloud.sleuth.instrument.reactor.TraceContextPropagator;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -146,7 +147,7 @@ public class ReactorSleuthMethodInvocationProcessor extends AbstractSleuthMethod
|
||||
|
||||
}
|
||||
|
||||
private static final class MonoSpan extends MonoOperator<Object, Object> {
|
||||
private static final class MonoSpan extends MonoOperator<Object, Object> implements TraceContextPropagator {
|
||||
|
||||
final Span span;
|
||||
|
||||
@@ -189,6 +190,14 @@ public class ReactorSleuthMethodInvocationProcessor extends AbstractSleuthMethod
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (key == Attr.RUN_STYLE) {
|
||||
return Attr.RunStyle.SYNC;
|
||||
}
|
||||
return super.scanUnsafe(key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SpanSubscriber implements CoreSubscriber<Object>, Subscription, Scannable {
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.sleuth.instrument.reactor;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.reactivestreams.Processor;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.Fuseable;
|
||||
import reactor.core.Scannable;
|
||||
import reactor.core.publisher.ConnectableFlux;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxOperator;
|
||||
import reactor.core.publisher.GroupedFlux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoOperator;
|
||||
import reactor.core.publisher.Operators;
|
||||
import reactor.core.publisher.ParallelFlux;
|
||||
import reactor.util.annotation.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Helper class for reactor ON_EACH instrumentation. Reduces number of sleuth operators
|
||||
* time in the reactive chain during Assembly by using {@code Scannable.Attr.RUN_STYLE}.
|
||||
* Below is the example of what it does <pre>{@code
|
||||
* Mono.just(0) // (-)
|
||||
* .map(it -> 1) // (-)
|
||||
* .flatMap(it ->
|
||||
* Mono.just(it) // (-)
|
||||
* ) // (-)
|
||||
* .flatMapMany(it ->
|
||||
* asyncPublisher(it) // (+)(*)
|
||||
* ) // (-)
|
||||
* .filter(it -> true) // (-)
|
||||
* .publishOn(Schedulers.single()) //(+)
|
||||
* .map(it -> it) // (-)
|
||||
* .map(it -> it * 10) // (-)
|
||||
* .filter(it -> true) // (-)
|
||||
* .scan((l, r) -> l + r) // (-)
|
||||
* .doOnNext(it -> { // (-)
|
||||
* //log
|
||||
* })
|
||||
* .doFirst(() -> { // (-)
|
||||
* //log
|
||||
* })
|
||||
* .doFinally(signalType -> { // (-)
|
||||
* //log
|
||||
* })
|
||||
* .subscribeOn(Schedulers.parallel()) //(+)
|
||||
* .subscribe();//(*)
|
||||
* (*) - captures tracing context if it differs from what was captured before at subscription and propagates it.
|
||||
* As far as check is performed on Subscriber Context it allocates Publisher to access it.
|
||||
* (+) - is ASYNC need to decorate Publisher
|
||||
* (-) - is SYNC no need to decorate Publisher
|
||||
*}</pre> So it creates 13 (out of 16) less Publisher (+ Subscriber and all their stuff)
|
||||
* on assembly time comparing to the original logic (decorate every Publisher and checking
|
||||
* only at subscription time).
|
||||
* <p/>
|
||||
* It also handles the case when onEachHook was not applied for some operator in the chain
|
||||
* (It could be possible if custom operators or Processor are used) by checkin source
|
||||
* source Publisher. <pre>{@code
|
||||
* processor/customOperator // (?) is async and hook is not applied
|
||||
* .map(it -> ...) // (+) is SYNC but should add hook as previous Processor/operator does not use hooks
|
||||
* .doOnNext(it -> { // (-) is SYNC no need to wrap
|
||||
* //log
|
||||
* })
|
||||
* .subscribe();
|
||||
*}</pre>
|
||||
*/
|
||||
final class ReactorHooksHelper {
|
||||
|
||||
// need a way to determine SYNC sources to not add redundant scope passing decorator
|
||||
// most of reactor-core SYNC sources are marked with SourceProducer interface
|
||||
static final Class<?> sourceProducerClass;
|
||||
static {
|
||||
Class<?> c;
|
||||
try {
|
||||
c = Class.forName("reactor.core.publisher.SourceProducer");
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
c = Void.class;
|
||||
}
|
||||
sourceProducerClass = c;
|
||||
}
|
||||
|
||||
private ReactorHooksHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to decorate input publisher with
|
||||
* {@code ScopePassingSpanOperator}.
|
||||
* @param p publisher to check
|
||||
* @return returns true if input publisher or one of its source publishers is not
|
||||
* {@code RunStyle.SYNC} and there is no {@link TraceContextPropagator} between input
|
||||
* publisher and not SYNC publisher.
|
||||
*/
|
||||
public static boolean shouldDecorate(Publisher<?> p) {
|
||||
Assert.notNull(p, "source Publisher is null");
|
||||
Publisher<?> current = p;
|
||||
while (true) {
|
||||
if (current == null) {
|
||||
// is start of the chain, Publisher without source or foreign Publisher
|
||||
return true;
|
||||
}
|
||||
if (current instanceof Fuseable.ScalarCallable) {
|
||||
return false;
|
||||
}
|
||||
if (isTraceContextPropagator(current)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isSync(current)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isSourceProducer(current)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
current = getParent(current);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isTraceContextPropagator(Publisher<?> current) {
|
||||
return current instanceof TraceContextPropagator;
|
||||
}
|
||||
|
||||
private static boolean isSourceProducer(Publisher<?> p) {
|
||||
return sourceProducerClass.isInstance(p);
|
||||
}
|
||||
|
||||
private static boolean isSync(Publisher<?> p) {
|
||||
return !(p instanceof Processor)
|
||||
&& Scannable.Attr.RunStyle.SYNC == Scannable.from(p).scan(Scannable.Attr.RUN_STYLE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Publisher<?> getParent(Publisher<?> publisher) {
|
||||
Object parent = Scannable.from(publisher).scanUnsafe(Scannable.Attr.PARENT);
|
||||
if (parent instanceof Publisher) {
|
||||
return (Publisher<?>) parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates {@link Publisher} with {@link TraceContextPropagator} {@code Publisher}.
|
||||
* Mostly it is a copy of reactor-core logic from
|
||||
* {@code reactor.core.publisher.Operators#liftPublisher()}.
|
||||
* @param filter the Predicate that the raw Publisher must pass for the transformation
|
||||
* to occur
|
||||
* @param lifter the {@link BiFunction} taking the raw {@link Publisher} and
|
||||
* {@link CoreSubscriber}. The function must return a receiving {@link CoreSubscriber}
|
||||
* that will immediately subscribe to the {@link Publisher}.
|
||||
* @param <O> the input and output type.
|
||||
* @return a new {@link Function}.
|
||||
*/
|
||||
public static <O> Function<? super Publisher<O>, ? extends Publisher<O>> liftPublisher(Predicate<Publisher> filter,
|
||||
BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super O>> lifter) {
|
||||
Assert.notNull(lifter, "lifter is null");
|
||||
return publisher -> {
|
||||
if (filter != null && !filter.test(publisher)) {
|
||||
return (Publisher<O>) publisher;
|
||||
}
|
||||
|
||||
if (publisher instanceof Mono) {
|
||||
return new SleuthMonoLift<>(publisher, lifter);
|
||||
}
|
||||
if (publisher instanceof ParallelFlux) {
|
||||
return new SleuthParallelLift<>((ParallelFlux<O>) publisher, lifter);
|
||||
}
|
||||
if (publisher instanceof ConnectableFlux) {
|
||||
return new SleuthConnectableLift<>((ConnectableFlux<O>) publisher, lifter);
|
||||
}
|
||||
if (publisher instanceof GroupedFlux) {
|
||||
return new SleuthGroupedLift<>((GroupedFlux<?, O>) publisher, lifter);
|
||||
}
|
||||
return new SleuthFluxLift<>(publisher, lifter);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of {@code reactor.core.publisher.MonoLift} which implements
|
||||
* {@link TraceContextPropagator}.
|
||||
*/
|
||||
final class SleuthMonoLift<I, O> extends MonoOperator<I, O> implements TraceContextPropagator {
|
||||
|
||||
final BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter;
|
||||
|
||||
SleuthMonoLift(Publisher<I> p,
|
||||
BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter) {
|
||||
super(Mono.from(p));
|
||||
this.lifter = lifter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(CoreSubscriber<? super O> actual) {
|
||||
CoreSubscriber input = actual;
|
||||
try {
|
||||
input = Objects.requireNonNull(lifter.apply(source, actual), "Lifted subscriber MUST NOT be null");
|
||||
|
||||
source.subscribe(input);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
Operators.reportThrowInSubscribe(input, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (key == Attr.RUN_STYLE) {
|
||||
return Attr.RunStyle.SYNC;
|
||||
}
|
||||
return super.scanUnsafe(key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of {@code reactor.core.publisher.FluxLift} which implements
|
||||
* {@link TraceContextPropagator}.
|
||||
*/
|
||||
final class SleuthFluxLift<I, O> extends FluxOperator<I, O> implements TraceContextPropagator {
|
||||
|
||||
final BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter;
|
||||
|
||||
SleuthFluxLift(Publisher<I> p,
|
||||
BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter) {
|
||||
super(Flux.from(p));
|
||||
this.lifter = lifter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(CoreSubscriber<? super O> actual) {
|
||||
CoreSubscriber input = actual;
|
||||
try {
|
||||
input = Objects.requireNonNull(lifter.apply(source, actual), "Lifted subscriber MUST NOT be null");
|
||||
|
||||
source.subscribe(input);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
Operators.reportThrowInSubscribe(input, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (key == Attr.RUN_STYLE) {
|
||||
return Attr.RunStyle.SYNC;
|
||||
}
|
||||
return super.scanUnsafe(key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of {@code reactor.core.publisher.ConnectableLift} which implements
|
||||
* {@link TraceContextPropagator}.
|
||||
*/
|
||||
final class SleuthConnectableLift<I, O> extends ConnectableFlux<O> implements Scannable, TraceContextPropagator {
|
||||
|
||||
final ConnectableFlux<I> source;
|
||||
|
||||
final BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter;
|
||||
|
||||
SleuthConnectableLift(ConnectableFlux<I> p,
|
||||
BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter) {
|
||||
this.source = Objects.requireNonNull(p, "source");
|
||||
this.lifter = lifter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPrefetch() {
|
||||
return source.getPrefetch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(Consumer<? super Disposable> cancelSupport) {
|
||||
this.source.connect(cancelSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (key == Attr.PREFETCH) {
|
||||
return source.getPrefetch();
|
||||
}
|
||||
if (key == Attr.PARENT) {
|
||||
return source;
|
||||
}
|
||||
if (key == Attr.RUN_STYLE) {
|
||||
return Attr.RunStyle.SYNC;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(CoreSubscriber<? super O> actual) {
|
||||
CoreSubscriber input = actual;
|
||||
try {
|
||||
input = Objects.requireNonNull(lifter.apply(source, actual), "Lifted subscriber MUST NOT be null");
|
||||
|
||||
source.subscribe(input);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
Operators.reportThrowInSubscribe(input, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of {@code reactor.core.publisher.GroupedLift} which implements
|
||||
* {@link TraceContextPropagator}.
|
||||
*/
|
||||
final class SleuthGroupedLift<K, I, O> extends GroupedFlux<K, O> implements Scannable, TraceContextPropagator {
|
||||
|
||||
final BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter;
|
||||
|
||||
final GroupedFlux<K, I> source;
|
||||
|
||||
SleuthGroupedLift(GroupedFlux<K, I> p,
|
||||
BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter) {
|
||||
this.source = Objects.requireNonNull(p, "source");
|
||||
this.lifter = lifter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPrefetch() {
|
||||
return source.getPrefetch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public K key() {
|
||||
return source.key();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (key == Attr.PARENT) {
|
||||
return source;
|
||||
}
|
||||
if (key == Attr.PREFETCH) {
|
||||
return getPrefetch();
|
||||
}
|
||||
if (key == Attr.RUN_STYLE) {
|
||||
return Attr.RunStyle.SYNC;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String stepName() {
|
||||
if (source instanceof Scannable) {
|
||||
return Scannable.from(source).stepName();
|
||||
}
|
||||
return Scannable.super.stepName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(CoreSubscriber<? super O> actual) {
|
||||
CoreSubscriber<? super I> input = lifter.apply(source, actual);
|
||||
|
||||
Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null");
|
||||
|
||||
source.subscribe(input);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of {@code reactor.core.publisher.ParallelLift} which implements
|
||||
* {@link TraceContextPropagator}.
|
||||
*/
|
||||
final class SleuthParallelLift<I, O> extends ParallelFlux<O> implements Scannable, TraceContextPropagator {
|
||||
|
||||
final BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter;
|
||||
|
||||
final ParallelFlux<I> source;
|
||||
|
||||
SleuthParallelLift(ParallelFlux<I> p,
|
||||
BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super I>> lifter) {
|
||||
this.source = Objects.requireNonNull(p, "source");
|
||||
this.lifter = lifter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPrefetch() {
|
||||
return source.getPrefetch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int parallelism() {
|
||||
return source.parallelism();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (key == Attr.PARENT) {
|
||||
return source;
|
||||
}
|
||||
if (key == Attr.PREFETCH) {
|
||||
return getPrefetch();
|
||||
}
|
||||
if (key == Attr.RUN_STYLE) {
|
||||
return Attr.RunStyle.SYNC;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String stepName() {
|
||||
if (source instanceof Scannable) {
|
||||
return Scannable.from(source).stepName();
|
||||
}
|
||||
return Scannable.super.stepName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(CoreSubscriber<? super O>[] s) {
|
||||
@SuppressWarnings("unchecked")
|
||||
CoreSubscriber<? super I>[] subscribers = new CoreSubscriber[parallelism()];
|
||||
|
||||
int i = 0;
|
||||
while (i < subscribers.length) {
|
||||
subscribers[i] = Objects.requireNonNull(lifter.apply(source, s[i]), "Lifted subscriber MUST NOT be null");
|
||||
i++;
|
||||
}
|
||||
|
||||
source.subscribe(subscribers);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.cloud.sleuth.instrument.reactor;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -26,7 +28,9 @@ import org.reactivestreams.Subscription;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.Fuseable;
|
||||
import reactor.core.Scannable;
|
||||
import reactor.core.publisher.Hooks;
|
||||
import reactor.core.publisher.Operators;
|
||||
import reactor.util.annotation.Nullable;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import org.springframework.cloud.sleuth.CurrentTraceContext;
|
||||
@@ -78,13 +82,46 @@ public abstract class ReactorSleuth {
|
||||
LazyBean<CurrentTraceContext> lazyCurrentTraceContext = LazyBean.create(springContext,
|
||||
CurrentTraceContext.class);
|
||||
|
||||
return Operators.liftPublisher((p, sub) -> {
|
||||
// We don't scope scalar results as they happen in an instant. This prevents
|
||||
// excessive overhead when using Flux/Mono #just, #empty, #error, etc.
|
||||
if (p instanceof Fuseable.ScalarCallable) {
|
||||
return sub;
|
||||
}
|
||||
LazyBean<Tracer> lazyTracer = LazyBean.create(springContext, Tracer.class);
|
||||
|
||||
return Operators.liftPublisher(p -> !(p instanceof Fuseable.ScalarCallable),
|
||||
(BiFunction) liftFunction(springContext, lazyCurrentTraceContext, lazyTracer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates scope passing span operator which applies only to not
|
||||
* {@code Scannable.Attr.RunStyle.SYNC} {@code Publisher}s. Used by
|
||||
* {@code InstrumentationType#DECORATE_ON_EACH}
|
||||
* @param springContext the Spring context.
|
||||
* @param <T> an arbitrary type that is left unchanged by the span operator.
|
||||
* @return operator to apply to {@link Hooks#onEachOperator(Function)}.
|
||||
*/
|
||||
public static <T> Function<? super Publisher<T>, ? extends Publisher<T>> onEachOperatorForOnEachInstrumentation(
|
||||
ConfigurableApplicationContext springContext) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Scope passing operator [" + springContext + "]");
|
||||
}
|
||||
|
||||
// keep a reference outside the lambda so that any caching will be visible to
|
||||
// all publishers
|
||||
LazyBean<CurrentTraceContext> lazyCurrentTraceContext = LazyBean.create(springContext,
|
||||
CurrentTraceContext.class);
|
||||
|
||||
LazyBean<Tracer> lazyTracer = LazyBean.create(springContext, Tracer.class);
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
Predicate<Publisher> shouldDecorate = ReactorHooksHelper::shouldDecorate;
|
||||
@SuppressWarnings("rawtypes")
|
||||
BiFunction<Publisher, ? super CoreSubscriber<? super T>, ? extends CoreSubscriber<? super T>> lifter = liftFunction(
|
||||
springContext, lazyCurrentTraceContext, lazyTracer);
|
||||
|
||||
return ReactorHooksHelper.liftPublisher(shouldDecorate, lifter);
|
||||
}
|
||||
|
||||
static <O> BiFunction<Publisher, ? super CoreSubscriber<? super O>, ? extends CoreSubscriber<? super O>> liftFunction(
|
||||
ConfigurableApplicationContext springContext, LazyBean<CurrentTraceContext> lazyCurrentTraceContext,
|
||||
LazyBean<Tracer> lazyTracer) {
|
||||
return (p, sub) -> {
|
||||
if (!springContext.isActive()) {
|
||||
if (log.isTraceEnabled()) {
|
||||
String message = "Spring Context [" + springContext
|
||||
@@ -114,35 +151,41 @@ public abstract class ReactorSleuth {
|
||||
return sub;
|
||||
}
|
||||
|
||||
context = contextWithBeans(context, springContext, sub);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Spring context [" + springContext + "], Reactor context [" + context + "], name ["
|
||||
+ name(sub) + "]");
|
||||
}
|
||||
|
||||
TraceContext parent = traceContext(context, currentTraceContext);
|
||||
if (parent == null) {
|
||||
return sub; // no need to scope a null parent
|
||||
}
|
||||
|
||||
// Handle scenarios such as Mono.defer
|
||||
if (sub instanceof ScopePassingSpanSubscriber) {
|
||||
ScopePassingSpanSubscriber<?> scopePassing = (ScopePassingSpanSubscriber<?>) sub;
|
||||
if (scopePassing.parent.equals(parent)) {
|
||||
return sub; // don't double-wrap
|
||||
}
|
||||
}
|
||||
|
||||
context = contextWithBeans(context, lazyTracer, lazyCurrentTraceContext);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Spring context [" + springContext + "], Reactor context [" + context + "], name ["
|
||||
+ name(sub) + "]");
|
||||
}
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Creating a scope passing span subscriber with Reactor Context " + "[" + context
|
||||
+ "] and name [" + name(sub) + "]");
|
||||
}
|
||||
// if (runStyle == Scannable.Attr.RunStyle.SYNC) {
|
||||
// return sub;
|
||||
// }
|
||||
|
||||
return new ScopePassingSpanSubscriber<>(sub, context, currentTraceContext, parent);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private static <T> Context contextWithBeans(Context context, ConfigurableApplicationContext springContext,
|
||||
CoreSubscriber<? super T> sub) {
|
||||
private static <T> Context contextWithBeans(Context context, LazyBean<Tracer> tracer,
|
||||
LazyBean<CurrentTraceContext> currentTraceContext) {
|
||||
if (!context.hasKey(Tracer.class)) {
|
||||
context = context.put(Tracer.class, springContext.getBean(Tracer.class));
|
||||
context = context.put(Tracer.class, tracer.getOrError());
|
||||
}
|
||||
if (!context.hasKey(CurrentTraceContext.class)) {
|
||||
context = context.put(CurrentTraceContext.class, springContext.getBean(CurrentTraceContext.class));
|
||||
context = context.put(CurrentTraceContext.class, currentTraceContext.getOrError());
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -158,20 +201,71 @@ public abstract class ReactorSleuth {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Spring Context passing operator [" + springContext + "]");
|
||||
}
|
||||
return Operators.liftPublisher((p, sub) -> {
|
||||
|
||||
LazyBean<Tracer> lazyTracer = LazyBean.create(springContext, Tracer.class);
|
||||
LazyBean<CurrentTraceContext> lazyCurrentTraceContext = LazyBean.create(springContext,
|
||||
CurrentTraceContext.class);
|
||||
|
||||
return Operators.liftPublisher(p -> {
|
||||
// We don't scope scalar results as they happen in an instant. This prevents
|
||||
// excessive overhead when using Flux/Mono #just, #empty, #error, etc.
|
||||
if (p instanceof Fuseable.ScalarCallable) {
|
||||
return !(p instanceof Fuseable.ScalarCallable) && springContext.isActive();
|
||||
}, (p, sub) -> {
|
||||
Context ctxBefore = context(sub);
|
||||
Context context = contextWithBeans(ctxBefore, lazyTracer, lazyCurrentTraceContext);
|
||||
if (context == ctxBefore) {
|
||||
return sub;
|
||||
}
|
||||
if (!springContext.isActive()) {
|
||||
return sub;
|
||||
}
|
||||
final Context context = contextWithBeans(context(sub), springContext, sub);
|
||||
return new SleuthContextOperator<>(context, sub);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates tracing context capturing reactor operator. Used by
|
||||
* {@code InstrumentationType#DECORATE_ON_EACH}.
|
||||
* @param springContext the Spring context.
|
||||
* @param <T> an arbitrary type that is left unchanged by the span operator.
|
||||
* @return operator to apply to {@link Hooks#onLastOperator(Function)} for
|
||||
* {@code InstrumentationType#DECORATE_ON_EACH}
|
||||
*/
|
||||
public static <T> Function<? super Publisher<T>, ? extends Publisher<T>> onLastOperatorForOnEachInstrumentation(
|
||||
ConfigurableApplicationContext springContext) {
|
||||
LazyBean<CurrentTraceContext> lazyCurrentTraceContext = LazyBean.create(springContext,
|
||||
CurrentTraceContext.class);
|
||||
LazyBean<Tracer> lazyTracer = LazyBean.create(springContext, Tracer.class);
|
||||
|
||||
BiFunction<Publisher, ? super CoreSubscriber<? super T>, ? extends CoreSubscriber<? super T>> scopePassingSpanSubscriber = liftFunction(
|
||||
springContext, lazyCurrentTraceContext, lazyTracer);
|
||||
|
||||
BiFunction<Publisher, ? super CoreSubscriber<? super T>, ? extends CoreSubscriber<? super T>> skipIfNoTraceCtx = (
|
||||
pub, sub) -> {
|
||||
// lazyCurrentTraceContext.get() is not null here. see predicate bellow
|
||||
TraceContext traceContext = lazyCurrentTraceContext.get().context();
|
||||
if (context(sub).getOrDefault(TraceContext.class, null) == traceContext) {
|
||||
return sub;
|
||||
}
|
||||
return scopePassingSpanSubscriber.apply(pub, sub);
|
||||
};
|
||||
|
||||
return ReactorHooksHelper.liftPublisher(p -> {
|
||||
/*
|
||||
* this prevent double decoration when last operator in the chain is not SYNC
|
||||
* like {@code Mono.fromSuppler(() -> ...).subscribeOn(Schedulers.parallel())}
|
||||
*/
|
||||
if (ReactorHooksHelper.isTraceContextPropagator(p)) {
|
||||
return false;
|
||||
}
|
||||
boolean addContext = !(p instanceof Fuseable.ScalarCallable) && springContext.isActive();
|
||||
if (addContext) {
|
||||
CurrentTraceContext currentTraceContext = lazyCurrentTraceContext.get();
|
||||
if (currentTraceContext != null) {
|
||||
addContext = currentTraceContext.context() != null;
|
||||
}
|
||||
}
|
||||
return addContext;
|
||||
}, skipIfNoTraceCtx);
|
||||
}
|
||||
|
||||
private static <T> Context context(CoreSubscriber<? super T> sub) {
|
||||
try {
|
||||
return sub.currentContext();
|
||||
@@ -199,9 +293,30 @@ public abstract class ReactorSleuth {
|
||||
return fallback.context();
|
||||
}
|
||||
|
||||
public static Function<Runnable, Runnable> scopePassingOnScheduleHook(
|
||||
ConfigurableApplicationContext springContext) {
|
||||
LazyBean<CurrentTraceContext> lazyCurrentTraceContext = LazyBean.create(springContext,
|
||||
CurrentTraceContext.class);
|
||||
return delegate -> {
|
||||
if (springContext.isActive()) {
|
||||
final CurrentTraceContext currentTraceContext = lazyCurrentTraceContext.get();
|
||||
if (currentTraceContext == null) {
|
||||
return delegate;
|
||||
}
|
||||
final TraceContext traceContext = currentTraceContext.context();
|
||||
return () -> {
|
||||
try (CurrentTraceContext.Scope scope = currentTraceContext.maybeScope(traceContext)) {
|
||||
delegate.run();
|
||||
}
|
||||
};
|
||||
}
|
||||
return delegate;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SleuthContextOperator<T> implements Subscription, CoreSubscriber<T> {
|
||||
class SleuthContextOperator<T> implements Subscription, CoreSubscriber<T>, Scannable {
|
||||
|
||||
private final Context context;
|
||||
|
||||
@@ -250,4 +365,13 @@ class SleuthContextOperator<T> implements Subscription, CoreSubscriber<T> {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (key == Attr.RUN_STYLE) {
|
||||
return Attr.RunStyle.SYNC;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ final class ScopePassingSpanSubscriber<T> implements SpanSubscription<T>, Scanna
|
||||
|
||||
private final CurrentTraceContext currentTraceContext;
|
||||
|
||||
private final TraceContext parent;
|
||||
final TraceContext parent;
|
||||
|
||||
private Subscription s;
|
||||
|
||||
@@ -108,11 +108,15 @@ final class ScopePassingSpanSubscriber<T> implements SpanSubscription<T>, Scanna
|
||||
return this.context;
|
||||
}
|
||||
|
||||
@reactor.util.annotation.Nullable
|
||||
@Override
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (key == Attr.PARENT) {
|
||||
return this.s;
|
||||
}
|
||||
else if (key == Attr.RUN_STYLE) {
|
||||
return Attr.RunStyle.SYNC;
|
||||
}
|
||||
else {
|
||||
return key == Attr.ACTUAL ? this.subscriber : null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.sleuth.instrument.reactor;
|
||||
|
||||
/**
|
||||
* Only for internal use. Marker interface for Publisher and Subscriber that propagate
|
||||
* Tracing context into execution Thread.
|
||||
*/
|
||||
public interface TraceContextPropagator {
|
||||
|
||||
}
|
||||
@@ -28,12 +28,14 @@ import reactor.core.publisher.MonoOperator;
|
||||
import reactor.util.annotation.Nullable;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import org.springframework.cloud.sleuth.CurrentTraceContext;
|
||||
import org.springframework.cloud.sleuth.Span;
|
||||
import org.springframework.cloud.sleuth.TraceContext;
|
||||
import org.springframework.cloud.sleuth.Tracer;
|
||||
import org.springframework.cloud.sleuth.http.HttpServerHandler;
|
||||
import org.springframework.cloud.sleuth.http.HttpServerRequest;
|
||||
import org.springframework.cloud.sleuth.http.HttpServerResponse;
|
||||
import org.springframework.cloud.sleuth.instrument.reactor.TraceContextPropagator;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
@@ -70,11 +72,14 @@ public class TraceWebFilter implements WebFilter, Ordered {
|
||||
|
||||
private final HttpServerHandler handler;
|
||||
|
||||
private final CurrentTraceContext currentTraceContext;
|
||||
|
||||
private int order;
|
||||
|
||||
public TraceWebFilter(Tracer tracer, HttpServerHandler handler) {
|
||||
public TraceWebFilter(Tracer tracer, HttpServerHandler handler, CurrentTraceContext currentTraceContext) {
|
||||
this.tracer = tracer;
|
||||
this.handler = handler;
|
||||
this.currentTraceContext = currentTraceContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -106,7 +111,7 @@ public class TraceWebFilter implements WebFilter, Ordered {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
private static class MonoWebFilterTrace extends MonoOperator<Void, Void> {
|
||||
private static class MonoWebFilterTrace extends MonoOperator<Void, Void> implements TraceContextPropagator {
|
||||
|
||||
final ServerWebExchange exchange;
|
||||
|
||||
@@ -120,11 +125,14 @@ public class TraceWebFilter implements WebFilter, Ordered {
|
||||
|
||||
final boolean initialTracePresent;
|
||||
|
||||
final CurrentTraceContext currentTraceContext;
|
||||
|
||||
MonoWebFilterTrace(Mono<? extends Void> source, ServerWebExchange exchange, boolean initialTracePresent,
|
||||
TraceWebFilter parent) {
|
||||
super(source);
|
||||
this.tracer = parent.tracer;
|
||||
this.handler = parent.handler;
|
||||
this.currentTraceContext = parent.currentTraceContext;
|
||||
this.exchange = exchange;
|
||||
this.span = exchange.getAttribute(TRACE_REQUEST_ATTR);
|
||||
this.initialTracePresent = initialTracePresent;
|
||||
@@ -133,7 +141,18 @@ public class TraceWebFilter implements WebFilter, Ordered {
|
||||
@Override
|
||||
public void subscribe(CoreSubscriber<? super Void> subscriber) {
|
||||
Context context = contextWithoutInitialSpan(subscriber.currentContext());
|
||||
this.source.subscribe(new WebFilterTraceSubscriber(subscriber, context, findOrCreateSpan(context), this));
|
||||
Span span = findOrCreateSpan(context);
|
||||
try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(span.context())) {
|
||||
this.source.subscribe(new WebFilterTraceSubscriber(subscriber, context, span, this));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (key == Attr.RUN_STYLE) {
|
||||
return Attr.RunStyle.SYNC;
|
||||
}
|
||||
return super.scanUnsafe(key);
|
||||
}
|
||||
|
||||
private Context contextWithoutInitialSpan(Context context) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.Scannable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.annotation.Nullable;
|
||||
import reactor.util.context.Context;
|
||||
@@ -37,6 +38,7 @@ import org.springframework.cloud.sleuth.TraceContext;
|
||||
import org.springframework.cloud.sleuth.http.HttpClientHandler;
|
||||
import org.springframework.cloud.sleuth.http.HttpClientRequest;
|
||||
import org.springframework.cloud.sleuth.http.HttpClientResponse;
|
||||
import org.springframework.cloud.sleuth.instrument.reactor.TraceContextPropagator;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
@@ -139,7 +141,8 @@ final class TraceExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
private static final class MonoWebClientTrace extends Mono<ClientResponse> {
|
||||
private static final class MonoWebClientTrace extends Mono<ClientResponse>
|
||||
implements Scannable, TraceContextPropagator {
|
||||
|
||||
final ExchangeFunction next;
|
||||
|
||||
@@ -175,13 +178,22 @@ final class TraceExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
.subscribe(new TraceWebClientSubscriber(subscriber, context, span, parent, this));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object scanUnsafe(Scannable.Attr key) {
|
||||
if (key == Scannable.Attr.RUN_STYLE) {
|
||||
return Scannable.Attr.RunStyle.SYNC;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscriber for WebClient.
|
||||
*/
|
||||
static final class TraceWebClientSubscriber extends AtomicReference<Span>
|
||||
implements CoreSubscriber<ClientResponse> {
|
||||
implements CoreSubscriber<ClientResponse>, Scannable {
|
||||
|
||||
final CoreSubscriber<? super ClientResponse> actual;
|
||||
|
||||
@@ -277,6 +289,14 @@ final class TraceExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object scanUnsafe(Scannable.Attr key) {
|
||||
if (key == Scannable.Attr.RUN_STYLE) {
|
||||
return Scannable.Attr.RunStyle.SYNC;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TraceWebClientSubscription implements Subscription {
|
||||
|
||||
@@ -55,12 +55,8 @@ public final class LazyBean<T> {
|
||||
*/
|
||||
@Nullable
|
||||
public T get() {
|
||||
if (this.value != null) {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
try {
|
||||
this.value = springContext.getBean(requiredType);
|
||||
return getOrError();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (log.isDebugEnabled()) {
|
||||
@@ -70,4 +66,19 @@ public final class LazyBean<T> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to provision from the underlying bean factory, if not already provisioned.
|
||||
* @return the bean value. This variant does not catch exception.
|
||||
*/
|
||||
public T getOrError() {
|
||||
T bean = this.value;
|
||||
if (bean != null) {
|
||||
return bean;
|
||||
}
|
||||
|
||||
bean = springContext.getBean(requiredType);
|
||||
this.value = bean;
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.sleuth.instrument.reactor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.Scannable;
|
||||
import reactor.core.publisher.BaseSubscriber;
|
||||
import reactor.core.publisher.ConnectableFlux;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.GroupedFlux;
|
||||
import reactor.core.publisher.Hooks;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoOperator;
|
||||
import reactor.core.publisher.Operators;
|
||||
import reactor.core.publisher.ParallelFlux;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.util.annotation.Nullable;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
class ReactorHooksHelperTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
Hooks.resetOnEachOperator();
|
||||
Hooks.resetOnLastOperator();
|
||||
Schedulers.resetOnScheduleHooks();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDecorateWhenSyncSourceThanShouldNotDecorate() {
|
||||
boolean actual = ReactorHooksHelper.shouldDecorate(Mono.just(1).map(Function.identity()));
|
||||
assertThat(actual).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDecorateWhenSyncSourceProducerThanShouldNotDecorate() {
|
||||
// Mono.fromSupplier() is instance of SourceProduced and is not
|
||||
// Fuseable.ScalarCallable like Mono.just()
|
||||
Mono<Integer> syncSource = Mono.fromSupplier(() -> 1);
|
||||
boolean actual = ReactorHooksHelper.shouldDecorate(syncSource.map(Function.identity()));
|
||||
assertThat(actual).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDecorateWhenNotSyncSourceProducerThanShouldDecorate() {
|
||||
Mono<Long> asyncSource = Mono.delay(Duration.ofMillis(10));
|
||||
boolean actual = ReactorHooksHelper.shouldDecorate(asyncSource.map(Function.identity()));
|
||||
assertThat(actual).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDecorateWhenProcessorSourceThanShouldNotDecorate() {
|
||||
Mono<Long> asyncSource = Sinks.<Long>one().asMono();
|
||||
boolean actual = ReactorHooksHelper.shouldDecorate(asyncSource.map(Function.identity()));
|
||||
assertThat(actual).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDecorateWhenAsyncSourceAndLifterThanShouldDecorate() {
|
||||
Mono<?> asyncSourceWithLifter = Mono.delay(Duration.ofMillis(10)).transform(Operators.liftPublisher(
|
||||
new BiFunction<Publisher, CoreSubscriber<? super Object>, CoreSubscriber<? super Long>>() {
|
||||
@Override
|
||||
public CoreSubscriber<? super Long> apply(Publisher pub, CoreSubscriber<? super Object> sub) {
|
||||
return sub;
|
||||
}
|
||||
}));
|
||||
boolean actual = ReactorHooksHelper.shouldDecorate(asyncSourceWithLifter.map(Function.identity()));
|
||||
assertThat(actual).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDecorateWhenSyncSourceAndLifterThanShouldNotDecorate() {
|
||||
Mono<?> syncSourceWithLifter = Mono.fromSupplier(() -> 1L).transform(Operators.liftPublisher(
|
||||
new BiFunction<Publisher, CoreSubscriber<? super Object>, CoreSubscriber<? super Long>>() {
|
||||
@Override
|
||||
public CoreSubscriber<? super Long> apply(Publisher pub, CoreSubscriber<? super Object> sub) {
|
||||
return sub;
|
||||
}
|
||||
}));
|
||||
boolean actual = ReactorHooksHelper.shouldDecorate(syncSourceWithLifter.map(Function.identity()));
|
||||
assertThat(actual).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDecorateWhenAsyncSourceAndTraceContextPropagatorThanShouldNotDecorate() {
|
||||
Function<? super Publisher<Long>, ? extends Publisher<Long>> traceContextPropagator = ReactorHooksHelper
|
||||
.liftPublisher(publisher -> true, (publisher, coreSubscriber) -> coreSubscriber);
|
||||
Mono<?> syncSourceWithLifter = Mono.delay(Duration.ofMillis(10)).transform(traceContextPropagator);
|
||||
boolean actual = ReactorHooksHelper.shouldDecorate(syncSourceWithLifter.map(Function.identity()));
|
||||
assertThat(actual).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDecorateWhenNullSourceProducerThanError() {
|
||||
assertThatCode(() -> ReactorHooksHelper.shouldDecorate(null)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("source Publisher is null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDecorateWhenOperatorWithoutRunStyleThanShouldDecorate() {
|
||||
Mono<?> source = Mono.just(1).as(CustomMonoWithoutRunStyleOperator::new);
|
||||
boolean actual = ReactorHooksHelper.shouldDecorate(source.map(Function.identity()));
|
||||
assertThat(actual).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liftPublisherWhenFilterDiscardsThenReturnSource() {
|
||||
Mono<Integer> source = Mono.just(1);
|
||||
final Function<? super Publisher<Integer>, ? extends Publisher<Integer>> transformer = ReactorHooksHelper
|
||||
.liftPublisher(p -> false, (p, s) -> s);
|
||||
Mono<Integer> transformed = source.transform(transformer);
|
||||
assertThat(source).isSameAs(transformed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liftPublisherWhenFilterIsNullThenDecorate() {
|
||||
Mono<Integer> source = Mono.just(1);
|
||||
final Function<? super Publisher<Integer>, ? extends Publisher<Integer>> transformer = ReactorHooksHelper
|
||||
.liftPublisher(null, (p, s) -> s);
|
||||
Mono<Integer> transformed = source.transform(transformer);
|
||||
assertThat(source).isNotSameAs(transformed);
|
||||
assertThat(transformed).isInstanceOf(SleuthMonoLift.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liftPublisherWhenSourceMonoThenDecorateWithMono() {
|
||||
Mono<Integer> source = Mono.just(1);
|
||||
final Function<? super Publisher<Integer>, ? extends Publisher<Integer>> transformer = ReactorHooksHelper
|
||||
.liftPublisher(p -> true, (p, s) -> new PlusOneSubscriber(s));
|
||||
Mono<Integer> transformed = source.transform(transformer);
|
||||
assertThat(source).isNotSameAs(transformed);
|
||||
assertThat(transformed).isInstanceOf(SleuthMonoLift.class).isInstanceOf(TraceContextPropagator.class);
|
||||
|
||||
StepVerifier.create(transformed).expectSubscription().expectNext(2).expectComplete()
|
||||
.verify(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liftPublisherWhenSourceFluxThenDecorateWithFlux() {
|
||||
Flux<Integer> source = Flux.just(1, 2, 3);
|
||||
final Function<? super Publisher<Integer>, ? extends Publisher<Integer>> transformer = ReactorHooksHelper
|
||||
.liftPublisher(p -> true, (p, s) -> new PlusOneSubscriber(s));
|
||||
Flux<Integer> transformed = source.transform(transformer);
|
||||
assertThat(source).isNotSameAs(transformed);
|
||||
assertThat(transformed).isInstanceOf(SleuthFluxLift.class).isInstanceOf(TraceContextPropagator.class);
|
||||
|
||||
StepVerifier.create(transformed).expectSubscription().expectNext(2).expectNext(3).expectNext(4).expectComplete()
|
||||
.verify(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Test
|
||||
public void liftPublisherWhenSourceParallelFluxThenDecorateWithParallelFlux() {
|
||||
ParallelFlux<Integer> source = Flux.just(1, 2, 3).parallel();
|
||||
|
||||
Function function = ReactorHooksHelper.liftPublisher(p -> true,
|
||||
(p, s) -> (CoreSubscriber) new PlusOneSubscriber(s));
|
||||
|
||||
ParallelFlux<Integer> transformed = source.transform(function);
|
||||
assertThat(source).isNotSameAs(transformed);
|
||||
assertThat(transformed).isInstanceOf(SleuthParallelLift.class).isInstanceOf(TraceContextPropagator.class);
|
||||
Flux<Integer> sorted = transformed.map(it -> it).sequential().sort();
|
||||
StepVerifier.create(sorted).expectSubscription().expectNext(2).expectNext(3).expectNext(4).expectComplete()
|
||||
.verify(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Test
|
||||
public void liftPublisherWhenSourceConnectableFluxThenDecorateWithConnectableFlux() {
|
||||
final AtomicBoolean sourceCanceled = new AtomicBoolean();
|
||||
ConnectableFlux<Integer> source = Flux.just(1, 2, 3).doOnCancel(() -> sourceCanceled.set(true)).replay();
|
||||
|
||||
Function function = ReactorHooksHelper.liftPublisher(p -> true,
|
||||
(p, s) -> (CoreSubscriber) new PlusOneSubscriber(s));
|
||||
|
||||
Flux<Integer> transformed = source.transform(function);
|
||||
assertThat(source).isNotSameAs(transformed);
|
||||
assertThat(transformed).isInstanceOf(SleuthConnectableLift.class).isInstanceOf(TraceContextPropagator.class);
|
||||
final AtomicReference<Disposable> cancelSource = new AtomicReference<>();
|
||||
Flux<Integer> autoConnect = ((ConnectableFlux<Integer>) transformed).autoConnect(1, cancelSource::set);
|
||||
|
||||
StepVerifier.create(autoConnect).expectSubscription().expectNext(2).expectNext(3).expectNext(4).thenCancel()
|
||||
.verify(Duration.ofSeconds(5));
|
||||
|
||||
assertThat(cancelSource.get()).isNotNull();
|
||||
cancelSource.get().dispose();
|
||||
assertThat(sourceCanceled).isTrue();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Test
|
||||
public void liftPublisherWhenSourceGroupedFluxThenDecorateWithGroupedFlux() {
|
||||
Flux<GroupedFlux<Integer, Integer>> source = Flux.just(1, 2, 3).groupBy(i -> i % 2);
|
||||
|
||||
Function function = ReactorHooksHelper.liftPublisher(p -> true,
|
||||
(p, s) -> (CoreSubscriber) new PlusOneSubscriber(s));
|
||||
|
||||
GroupedFlux<Integer, Integer> grouped = source.filter(it -> it.key() == 1).blockFirst();
|
||||
Flux<Integer> transformed = grouped.transform(function);
|
||||
assertThat(grouped).isNotSameAs(transformed);
|
||||
assertThat(transformed).isInstanceOf(SleuthGroupedLift.class).isInstanceOf(TraceContextPropagator.class);
|
||||
|
||||
assertThat(((GroupedFlux) transformed).key()).isEqualTo(1);
|
||||
|
||||
StepVerifier.create(transformed).expectSubscription().expectNext(2).expectNext(4).expectComplete()
|
||||
.verify(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Test
|
||||
public void sleuthMonoLiftWhenScanRunStyleThenSync() {
|
||||
final Mono source = Mockito.mock(Mono.class);
|
||||
|
||||
SleuthMonoLift lifter = new SleuthMonoLift(source, (p, s) -> s);
|
||||
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.RUN_STYLE)).isEqualTo(Scannable.Attr.RunStyle.SYNC);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PARENT)).isEqualTo(source);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Test
|
||||
public void testSleuthFluxLiftScanUnsafe() {
|
||||
final Flux source = Mockito.mock(Flux.class);
|
||||
|
||||
SleuthFluxLift lifter = new SleuthFluxLift(source, (p, s) -> s);
|
||||
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.RUN_STYLE)).isEqualTo(Scannable.Attr.RunStyle.SYNC);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PARENT)).isEqualTo(source);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PREFETCH)).isEqualTo(lifter.getPrefetch());
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Test
|
||||
public void testSleuthConnectableLiftScanUnsafe() {
|
||||
final ConnectableFlux source = Mockito.mock(ConnectableFlux.class);
|
||||
int sourcePrefetch = 1;
|
||||
Mockito.when(source.getPrefetch()).thenReturn(sourcePrefetch);
|
||||
|
||||
SleuthConnectableLift lifter = new SleuthConnectableLift(source, (p, s) -> s);
|
||||
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.RUN_STYLE)).isEqualTo(Scannable.Attr.RunStyle.SYNC);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PARENT)).isEqualTo(source);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PREFETCH)).isEqualTo(sourcePrefetch);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Test
|
||||
public void testSleuthGroupedLiftFluxScanUnsafe() {
|
||||
final GroupedFlux source = Mockito.mock(GroupedFlux.class);
|
||||
int sourcePrefetch = 1;
|
||||
Mockito.when(source.getPrefetch()).thenReturn(sourcePrefetch);
|
||||
|
||||
SleuthGroupedLift lifter = new SleuthGroupedLift(source, (p, s) -> s);
|
||||
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.RUN_STYLE)).isEqualTo(Scannable.Attr.RunStyle.SYNC);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PARENT)).isEqualTo(source);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PREFETCH)).isEqualTo(sourcePrefetch);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Test
|
||||
public void testSleuthParallelLiftScanUnsafe() {
|
||||
final ParallelFlux source = Mockito.mock(ParallelFlux.class);
|
||||
int sourcePrefetch = 1;
|
||||
Mockito.when(source.getPrefetch()).thenReturn(sourcePrefetch);
|
||||
|
||||
SleuthParallelLift lifter = new SleuthParallelLift(source, (p, s) -> s);
|
||||
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.RUN_STYLE)).isEqualTo(Scannable.Attr.RunStyle.SYNC);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PARENT)).isEqualTo(source);
|
||||
assertThat(lifter.scanUnsafe(Scannable.Attr.PREFETCH)).isEqualTo(sourcePrefetch);
|
||||
}
|
||||
|
||||
static class CustomMonoWithoutRunStyleOperator<O> extends MonoOperator<O, O> {
|
||||
|
||||
/**
|
||||
* Build a {@link MonoOperator} wrapper around the passed parent {@link Publisher}
|
||||
* @param source the {@link Publisher} to decorate
|
||||
*/
|
||||
protected CustomMonoWithoutRunStyleOperator(Mono<? extends O> source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(CoreSubscriber<? super O> actual) {
|
||||
source.subscribe(actual);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object scanUnsafe(Attr key) {
|
||||
if (Attr.RUN_STYLE == key) {
|
||||
return null;
|
||||
}
|
||||
return super.scanUnsafe(key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class PlusOneSubscriber extends BaseSubscriber<Integer> {
|
||||
|
||||
CoreSubscriber<? super Integer> actual;
|
||||
|
||||
PlusOneSubscriber(CoreSubscriber<? super Integer> actual) {
|
||||
this.actual = actual;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context currentContext() {
|
||||
return actual.currentContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookOnSubscribe(Subscription subscription) {
|
||||
actual.onSubscribe(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookOnNext(Integer value) {
|
||||
actual.onNext(value + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookOnComplete() {
|
||||
actual.onComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookOnError(Throwable throwable) {
|
||||
actual.onError(throwable);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,34 +17,46 @@
|
||||
package org.springframework.cloud.sleuth.instrument.reactor;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.assertj.core.presentation.StandardRepresentation;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.Mockito;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.Scannable;
|
||||
import reactor.core.publisher.Hooks;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.cloud.sleuth.CurrentTraceContext;
|
||||
import org.springframework.cloud.sleuth.TraceContext;
|
||||
import org.springframework.cloud.sleuth.Tracer;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
import static org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth.scopePassingSpanOperator;
|
||||
import static org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth.onEachOperatorForOnEachInstrumentation;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
public abstract class FlowsScopePassingSpanSubscriberTests {
|
||||
|
||||
static final String HOOK_KEY = "org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration.TraceReactorConfiguration";
|
||||
|
||||
static {
|
||||
// AssertJ will recognise QueueSubscription implements queue and try to invoke
|
||||
// iterator. That's not allowed, and will cause an exception
|
||||
@@ -61,16 +73,17 @@ public abstract class FlowsScopePassingSpanSubscriberTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
Hooks.resetOnEachOperator(
|
||||
"org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration.TraceReactorConfiguration");
|
||||
Hooks.resetOnLastOperator(
|
||||
"org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration.TraceReactorConfiguration");
|
||||
Hooks.resetOnEachOperator();
|
||||
Hooks.resetOnLastOperator();
|
||||
Schedulers.resetOnScheduleHooks();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void close() {
|
||||
springContext.close();
|
||||
Hooks.resetOnEachOperator();
|
||||
Hooks.resetOnLastOperator();
|
||||
Schedulers.resetOnScheduleHooks();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,7 +91,7 @@ public abstract class FlowsScopePassingSpanSubscriberTests {
|
||||
springContext.registerBean(CurrentTraceContext.class, this::currentTraceContext);
|
||||
springContext.refresh();
|
||||
|
||||
Function<? super Publisher<Integer>, ? extends Publisher<Integer>> transformer = scopePassingSpanOperator(
|
||||
Function<? super Publisher<Integer>, ? extends Publisher<Integer>> transformer = onEachOperatorForOnEachInstrumentation(
|
||||
this.springContext);
|
||||
|
||||
try (CurrentTraceContext.Scope ws = currentTraceContext().newScope(context())) {
|
||||
@@ -144,4 +157,65 @@ public abstract class FlowsScopePassingSpanSubscriberTests {
|
||||
Awaitility.await().untilAsserted(() -> then(currentTraceContext().context()).isNull());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("should_not_double_wrap_async_publisher_Args")
|
||||
public void should_not_double_wrap_async_publisher(String name, Supplier<Mono<Integer>> sourceSupplier) {
|
||||
springContext.registerBean(CurrentTraceContext.class, this::currentTraceContext);
|
||||
springContext.registerBean(Tracer.class, () -> Mockito.mock(Tracer.class));
|
||||
springContext.refresh();
|
||||
|
||||
Hooks.onEachOperator(HOOK_KEY, ReactorSleuth.onEachOperatorForOnEachInstrumentation(springContext));
|
||||
Hooks.onLastOperator(HOOK_KEY, ReactorSleuth.onLastOperatorForOnEachInstrumentation(springContext));
|
||||
|
||||
AtomicBoolean once = new AtomicBoolean();
|
||||
Hooks.onLastOperator("test", p -> {
|
||||
// check only first onLast Hook
|
||||
if (once.compareAndSet(false, true)) {
|
||||
assertThat(p).isInstanceOf(TraceContextPropagator.class);
|
||||
Object parent = Scannable.from(p).scanUnsafe(Scannable.Attr.PARENT);
|
||||
assertThat(parent).isNotInstanceOf(TraceContextPropagator.class);
|
||||
}
|
||||
return p;
|
||||
});
|
||||
try (CurrentTraceContext.Scope ws = currentTraceContext().newScope(context())) {
|
||||
Mono<Integer> source = sourceSupplier.get();
|
||||
|
||||
source.subscribe((Subscriber<? super Integer>) new CoreSubscriber<Integer>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription subscription) {
|
||||
assertThat(subscription).isInstanceOf(ScopePassingSpanSubscriber.class);
|
||||
ScopePassingSpanSubscriber<Integer> spanSubscriber = (ScopePassingSpanSubscriber) subscription;
|
||||
Object parent = spanSubscriber.scanUnsafe(Scannable.Attr.PARENT);
|
||||
|
||||
assertThat(parent).isInstanceOf(Subscriber.class).isNotInstanceOf(ScopePassingSpanSubscriber.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(Integer integer) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
throw Exceptions.propagate(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> should_not_double_wrap_async_publisher_Args() {
|
||||
return Stream.of(
|
||||
// async source is hidden by Mono.defer()
|
||||
Arguments.of("hidden by defer",
|
||||
(Supplier) () -> Mono
|
||||
.defer(() -> Mono.fromSupplier(() -> 1).hide().subscribeOn(Schedulers.parallel()))),
|
||||
// async source is directly accessible during subscription
|
||||
Arguments.of("directly accessible",
|
||||
(Supplier) () -> Mono.fromSupplier(() -> 1).hide().subscribeOn(Schedulers.parallel())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,10 +20,14 @@ import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -45,10 +49,26 @@ public abstract class ScopePassingSpanSubscriberSpringBootTests {
|
||||
@Autowired
|
||||
CurrentTraceContext currentTraceContext;
|
||||
|
||||
private Scheduler subscribeScheduler;
|
||||
|
||||
private Scheduler secondScheduler;
|
||||
|
||||
protected abstract TraceContext context();
|
||||
|
||||
protected abstract TraceContext context2();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
subscribeScheduler = Schedulers.newSingle("subscribeThread2");
|
||||
secondScheduler = Schedulers.newSingle("secondThread");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
subscribeScheduler.dispose();
|
||||
secondScheduler.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_pass_tracing_info_when_using_reactor() {
|
||||
final AtomicReference<TraceContext> spanInOperation = new AtomicReference<>();
|
||||
@@ -88,7 +108,7 @@ public abstract class ScopePassingSpanSubscriberSpringBootTests {
|
||||
|
||||
try (CurrentTraceContext.Scope ws = this.currentTraceContext.newScope(context())) {
|
||||
Flux.just(1, 2, 3).publishOn(Schedulers.single()).log("reactor.1").map(d -> d + 1).map(d -> d + 1)
|
||||
.publishOn(Schedulers.newSingle("secondThread")).log("reactor.2").map((d) -> {
|
||||
.publishOn(secondScheduler).log("reactor.2").map((d) -> {
|
||||
spanInOperation.set(this.currentTraceContext.context());
|
||||
return d + 1;
|
||||
}).map(d -> d + 1).blockLast();
|
||||
@@ -113,6 +133,71 @@ public abstract class ScopePassingSpanSubscriberSpringBootTests {
|
||||
then(this.currentTraceContext.context()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_pass_tracing_info_into_sources_when_using_reactor_async() {
|
||||
final AtomicReference<TraceContext> spanInOperation = new AtomicReference<>();
|
||||
|
||||
try (CurrentTraceContext.Scope ws = this.currentTraceContext.newScope(context())) {
|
||||
Mono.fromSupplier(() -> {
|
||||
spanInOperation.set(this.currentTraceContext.context());
|
||||
return 1;
|
||||
}).publishOn(Schedulers.single()).log("reactor.1").map(d -> d + 1).map(d -> d + 1)
|
||||
.publishOn(secondScheduler).log("reactor.2").map((d) -> d + 1).map(d -> d + 1)
|
||||
.subscribeOn(subscribeScheduler).block();
|
||||
|
||||
Awaitility.await().untilAsserted(() -> then(spanInOperation.get()).isEqualTo(context()));
|
||||
then(this.currentTraceContext.context()).isEqualTo(context());
|
||||
}
|
||||
|
||||
then(this.currentTraceContext.context()).isNull();
|
||||
|
||||
try (CurrentTraceContext.Scope ws = this.currentTraceContext.newScope(context2())) {
|
||||
Mono.fromCallable(() -> {
|
||||
spanInOperation.set(this.currentTraceContext.context());
|
||||
return 1;
|
||||
}).log("reactor.").map(d -> d + 1).map(d -> d + 1).map(d -> d + 1).subscribeOn(subscribeScheduler).block();
|
||||
|
||||
then(this.currentTraceContext.context()).isEqualTo(context2());
|
||||
then(spanInOperation.get()).isEqualTo(context2());
|
||||
}
|
||||
|
||||
then(this.currentTraceContext.context()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_pass_tracing_info_when_using_reactor_async_processor() {
|
||||
final AtomicReference<TraceContext> spanInOperation = new AtomicReference<>();
|
||||
|
||||
Sinks.One<Integer> one = Sinks.one();
|
||||
try (CurrentTraceContext.Scope ws = this.currentTraceContext.newScope(context())) {
|
||||
one.asMono()
|
||||
// hook is not applied for Processors so first operator after it will
|
||||
// not be decorated with brave context
|
||||
.map(d -> d + 1).map((d) -> {
|
||||
spanInOperation.set(this.currentTraceContext.context());
|
||||
return d + 1;
|
||||
}).map(d -> d + 1).doOnSubscribe(subscription -> {
|
||||
final Thread thread = new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(300);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
one.tryEmitValue(0);
|
||||
});
|
||||
thread.setName("async_processor_source");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}).subscribeOn(Schedulers.boundedElastic()).block();
|
||||
|
||||
Awaitility.await().untilAsserted(() -> then(spanInOperation.get()).isEqualTo(context()));
|
||||
then(this.currentTraceContext.context()).isEqualTo(context());
|
||||
}
|
||||
|
||||
then(this.currentTraceContext.context()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyConsidersContextDuringSubscribe() {
|
||||
Mono<TraceContext> fromMono = Mono.fromCallable(this.currentTraceContext::context);
|
||||
|
||||
@@ -143,7 +143,7 @@ public abstract class ScopePassingSpanSubscriberTests {
|
||||
"org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration.TraceReactorConfiguration");
|
||||
Hooks.resetOnLastOperator(
|
||||
"org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration.TraceReactorConfiguration");
|
||||
Schedulers.removeExecutorServiceDecorator("s;euth");
|
||||
Schedulers.resetOnScheduleHook("sleuth");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
||||
Reference in New Issue
Block a user