Merge branch 'optimize-reactor-hooks' of https://github.com/robotmrv/spring-cloud-sleuth into robotmrv-optimize-reactor-hooks

This commit is contained in:
Marcin Grzejszczak
2021-02-19 10:59:17 +01:00
18 changed files with 1288 additions and 55 deletions

View File

@@ -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,
/**

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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 {
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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())));
}
}

View File

@@ -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);

View File

@@ -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