Uses ConfigurableApplicationContext consistently in Reactor (#1545)

Before, we used ConfigurableApplicationContext or BeanFactory eventhough
we already had a reference to ConfigurableApplicationContext. This uses
the latter consistently, avoiding a state condition that caused more
code.

This also corrects some misnamed tests and adjusts them to verify only
what they are responsible for.
This commit is contained in:
Adrian Cole
2020-02-03 19:26:38 +08:00
committed by GitHub
parent 4acbaf38f3
commit 241f536aef
12 changed files with 384 additions and 625 deletions

View File

@@ -0,0 +1,204 @@
/*
* Copyright 2013-2019 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.AtomicReference;
import brave.propagation.CurrentTraceContext;
import brave.propagation.CurrentTraceContext.Scope;
import brave.propagation.TraceContext;
import brave.sampler.Sampler;
import org.awaitility.Awaitility;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.BDDAssertions.then;
/**
* Like {@link ScopePassingSpanSubscriberTests}, except this tests wiring with spring boot
* config.
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ScopePassingSpanSubscriberSpringBootTests.Config.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class ScopePassingSpanSubscriberSpringBootTests {
@Autowired
CurrentTraceContext currentTraceContext;
TraceContext context = TraceContext.newBuilder().traceId(1).spanId(1).sampled(true)
.build();
TraceContext context2 = TraceContext.newBuilder().traceId(1).spanId(2).sampled(true)
.build();
@Test
public void should_pass_tracing_info_when_using_reactor() {
final AtomicReference<TraceContext> spanInOperation = new AtomicReference<>();
Publisher<Integer> traced = Flux.just(1, 2, 3);
try (Scope ws = this.currentTraceContext.newScope(context)) {
Flux.from(traced).map(d -> d + 1).map(d -> d + 1).map((d) -> {
spanInOperation.set(this.currentTraceContext.get());
return d + 1;
}).map(d -> d + 1).subscribe(d -> {
});
}
then(this.currentTraceContext.get()).isNull();
then(spanInOperation.get()).isEqualTo(context);
}
@Test
public void should_support_reactor_fusion_optimization() {
final AtomicReference<TraceContext> spanInOperation = new AtomicReference<>();
try (Scope ws = this.currentTraceContext.newScope(context)) {
Mono.just(1).flatMap(d -> Flux.just(d + 1).collectList().map(p -> p.get(0)))
.map(d -> d + 1).map((d) -> {
spanInOperation.set(this.currentTraceContext.get());
return d + 1;
}).map(d -> d + 1).subscribe(d -> {
});
}
then(this.currentTraceContext.get()).isNull();
then(spanInOperation.get()).isEqualTo(context);
}
@Test
public void should_pass_tracing_info_when_using_reactor_async() {
final AtomicReference<TraceContext> spanInOperation = new AtomicReference<>();
try (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) -> {
spanInOperation.set(this.currentTraceContext.get());
return d + 1;
}).map(d -> d + 1).blockLast();
Awaitility.await()
.untilAsserted(() -> then(spanInOperation.get()).isEqualTo(context));
then(this.currentTraceContext.get()).isEqualTo(context);
}
then(this.currentTraceContext.get()).isNull();
try (Scope ws = this.currentTraceContext.newScope(context2)) {
Flux.just(1, 2, 3).publishOn(Schedulers.single()).log("reactor.")
.map(d -> d + 1).map(d -> d + 1).map((d) -> {
spanInOperation.set(this.currentTraceContext.get());
return d + 1;
}).map(d -> d + 1).blockLast();
then(this.currentTraceContext.get()).isEqualTo(context2);
then(spanInOperation.get()).isEqualTo(context2);
}
then(this.currentTraceContext.get()).isNull();
}
@Test
public void onlyConsidersContextDuringSubscribe() {
Mono<TraceContext> fromMono = Mono.fromCallable(this.currentTraceContext::get);
try (Scope ws = this.currentTraceContext.newScope(context)) {
then(fromMono.map(context -> context).block()).isNotNull();
}
}
@Test
public void checkTraceIdDuringZipOperation() {
final AtomicReference<TraceContext> spanInOperation = new AtomicReference<>();
final AtomicReference<TraceContext> spanInZipOperation = new AtomicReference<>();
try (Scope ws = this.currentTraceContext.newScope(context)) {
Mono.fromCallable(this.currentTraceContext::get).map(span -> span)
.doOnNext(spanInOperation::set)
.zipWith(Mono.fromCallable(this.currentTraceContext::get)
.map(span -> span).doOnNext(spanInZipOperation::set))
.block();
}
then(spanInZipOperation).hasValue(context);
then(spanInOperation).hasValue(context);
}
// #646
@Test
public void should_work_for_mono_just_with_flat_map() {
try (Scope ws = this.currentTraceContext.newScope(context)) {
Mono.just("value1")
.flatMap(request -> Mono.just("value2").then(Mono.just("foo")))
.map(a -> "qwe").block();
}
}
// #1030
@Test
public void checkTraceIdFromSubscriberContext() {
final AtomicReference<TraceContext> spanInSubscriberContext = new AtomicReference<>();
try (Scope ws = this.currentTraceContext.newScope(context)) {
Mono.subscriberContext().map(context -> this.currentTraceContext.get())
.doOnNext(spanInSubscriberContext::set).block();
}
then(spanInSubscriberContext).hasValue(context); // ok here
}
@Test
public void should_pass_tracing_info_into_inner_publishers() {
final AtomicReference<TraceContext> spanInOperation = new AtomicReference<>();
try (Scope ws = this.currentTraceContext.newScope(context)) {
Flux.range(0, 5)
.flatMap(it -> Mono.delay(Duration.ofMillis(1))
.map(context -> this.currentTraceContext.get())
.doOnNext(spanInOperation::set))
.blockFirst();
}
then(spanInOperation.get()).isEqualTo(context);
}
@EnableAutoConfiguration
@Configuration
static class Config {
@Bean
Sampler sampler() {
return Sampler.ALWAYS_SAMPLE;
}
}
}

View File

@@ -16,32 +16,59 @@
package org.springframework.cloud.sleuth.instrument.reactor;
import java.util.Objects;
import java.util.function.Function;
import brave.propagation.CurrentTraceContext;
import brave.propagation.CurrentTraceContext.Scope;
import brave.propagation.TraceContext;
import org.assertj.core.presentation.StandardRepresentation;
import org.awaitility.Awaitility;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.BDDMockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import org.springframework.beans.factory.BeanFactory;
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;
/**
* @author Marcin Grzejszczak
*/
@RunWith(MockitoJUnitRunner.class)
public class ScopePassingSpanSubscriberTests {
CurrentTraceContext currentTraceContext = CurrentTraceContext.Default.create();
static {
// AssertJ will recognise QueueSubscription implements queue and try to invoke
// iterator. That's not allowed, and will cause an exception
// Fuseable$QueueSubscription.NOT_SUPPORTED_MESSAGE.
// This ensures AssertJ uses normal toString.
StandardRepresentation.registerFormatterForType(ScopePassingSpanSubscriber.class,
Objects::toString);
}
final CurrentTraceContext currentTraceContext = CurrentTraceContext.Default.create();
TraceContext context = TraceContext.newBuilder().traceId(1).spanId(1).sampled(true)
.build();
TraceContext context2 = TraceContext.newBuilder().traceId(1).spanId(2).sampled(true)
.build();
AnnotationConfigApplicationContext springContext = new AnnotationConfigApplicationContext();
@After
public void close() {
springContext.close();
}
@Test
public void should_propagate_current_context() {
ScopePassingSpanSubscriber<?> subscriber = new ScopePassingSpanSubscriber<>(null,
@@ -53,28 +80,96 @@ public class ScopePassingSpanSubscriberTests {
@Test
public void should_set_empty_context_when_context_is_null() {
ScopePassingSpanSubscriber<?> subscriber = new ScopePassingSpanSubscriber<>(null,
null, this.currentTraceContext, null);
Context.empty(), this.currentTraceContext, null);
then(subscriber.currentContext().isEmpty()).isTrue();
}
@Test
public void should_put_current_span_to_context() {
try (Scope ws = this.currentTraceContext.newScope(context)) {
CoreSubscriber<?> subscriber = ReactorSleuth.scopePassingSpanSubscription(
beanFactory(), new BaseSubscriber<Object>() {
});
try (Scope ws = this.currentTraceContext.newScope(context2)) {
CoreSubscriber<?> subscriber = new ScopePassingSpanSubscriber<>(
new BaseSubscriber<Object>() {
}, Context.empty(), currentTraceContext, context);
then(subscriber.currentContext().get(TraceContext.class)).isEqualTo(context);
}
}
private BeanFactory beanFactory() {
BeanFactory beanFactory = BDDMockito.mock(BeanFactory.class);
BDDMockito.given(beanFactory.getBean(CurrentTraceContext.class))
.willReturn(this.currentTraceContext);
return beanFactory;
@Test
public void should_not_trace_scalar_flows() {
springContext.registerBean(CurrentTraceContext.class, () -> currentTraceContext);
springContext.refresh();
Function<? super Publisher<Integer>, ? extends Publisher<Integer>> transformer = scopePassingSpanOperator(
this.springContext);
try (Scope ws = this.currentTraceContext.newScope(context)) {
Subscriber<Object> assertNoSpanSubscriber = new CoreSubscriber<Object>() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
assertThat(s).isNotInstanceOf(ScopePassingSpanSubscriber.class);
}
@Override
public void onNext(Object o) {
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
};
Subscriber<Object> assertSpanSubscriber = new CoreSubscriber<Object>() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
assertThat(s).isInstanceOf(ScopePassingSpanSubscriber.class);
}
@Override
public void onNext(Object o) {
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
};
transformer.apply(Mono.just(1).hide()).subscribe(assertSpanSubscriber);
transformer.apply(Mono.just(1)).subscribe(assertNoSpanSubscriber);
transformer.apply(Mono.<Integer>error(new Exception()).hide())
.subscribe(assertSpanSubscriber);
transformer.apply(Mono.error(new Exception()))
.subscribe(assertNoSpanSubscriber);
transformer.apply(Mono.<Integer>empty().hide())
.subscribe(assertSpanSubscriber);
transformer.apply(Mono.empty()).subscribe(assertNoSpanSubscriber);
}
Awaitility.await().untilAsserted(() -> {
then(this.currentTraceContext.get()).isNull();
});
}
}

View File

@@ -1,333 +0,0 @@
/*
* Copyright 2013-2019 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.AtomicReference;
import java.util.function.Function;
import brave.Span;
import brave.Tracer;
import brave.sampler.Sampler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.awaitility.Awaitility;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.BDDAssertions.then;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpanSubscriberTests.Config.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class SpanSubscriberTests {
private static final Log log = LogFactory.getLog(SpanSubscriberTests.class);
@Autowired
Tracer tracer;
@Autowired
ConfigurableApplicationContext factory;
@Test
public void should_pass_tracing_info_when_using_reactor() {
Span span = this.tracer.nextSpan().name("foo").start();
final AtomicReference<Span> spanInOperation = new AtomicReference<>();
Publisher<Integer> traced = Flux.just(1, 2, 3);
log.info("Hello");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
Flux.from(traced).map(d -> d + 1).map(d -> d + 1).map((d) -> {
spanInOperation.set(this.tracer.currentSpan());
return d + 1;
}).map(d -> d + 1).subscribe(System.out::println);
}
finally {
span.finish();
}
then(this.tracer.currentSpan()).isNull();
then(spanInOperation.get().context().spanId()).isEqualTo(span.context().spanId());
}
@Test
public void should_support_reactor_fusion_optimization() {
Span span = this.tracer.nextSpan().name("foo").start();
final AtomicReference<Span> spanInOperation = new AtomicReference<>();
log.info("Hello");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
Mono.just(1).flatMap(d -> Flux.just(d + 1).collectList().map(p -> p.get(0)))
.map(d -> d + 1).map((d) -> {
spanInOperation.set(this.tracer.currentSpan());
return d + 1;
}).map(d -> d + 1).subscribe(System.out::println);
}
finally {
span.finish();
}
then(this.tracer.currentSpan()).isNull();
then(spanInOperation.get().context().spanId()).isEqualTo(span.context().spanId());
}
@Test
public void should_not_trace_scalar_flows() {
Span span = this.tracer.nextSpan().name("foo").start();
log.info("Hello");
// Disable global hooks for local hook testing
TraceReactorAutoConfigurationAccessorConfiguration.close();
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
Function<? super Publisher<Integer>, ? extends Publisher<Integer>> transformer = ReactorSleuth
.scopePassingSpanOperator(this.factory);
Subscriber<Object> assertNoSpanSubscriber = new CoreSubscriber<Object>() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
assertThat(s).isNotInstanceOf(ScopePassingSpanSubscriber.class);
}
@Override
public void onNext(Object o) {
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
};
Subscriber<Object> assertSpanSubscriber = new CoreSubscriber<Object>() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
assertThat(s).isInstanceOf(ScopePassingSpanSubscriber.class);
}
@Override
public void onNext(Object o) {
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
};
transformer.apply(Mono.just(1).hide()).subscribe(assertSpanSubscriber);
transformer.apply(Mono.just(1)).subscribe(assertNoSpanSubscriber);
transformer.apply(Mono.<Integer>error(new Exception()).hide())
.subscribe(assertSpanSubscriber);
transformer.apply(Mono.error(new Exception()))
.subscribe(assertNoSpanSubscriber);
transformer.apply(Mono.<Integer>empty().hide())
.subscribe(assertSpanSubscriber);
transformer.apply(Mono.empty()).subscribe(assertNoSpanSubscriber);
}
finally {
span.finish();
}
Awaitility.await().untilAsserted(() -> {
then(this.tracer.currentSpan()).isNull();
});
TraceReactorAutoConfigurationAccessorConfiguration.setup(this.factory);
}
@Test
public void should_pass_tracing_info_when_using_reactor_async() {
Span span = this.tracer.nextSpan().name("foo").start();
final AtomicReference<Span> spanInOperation = new AtomicReference<>();
log.info("Hello");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
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) -> {
spanInOperation.set(this.tracer.currentSpan());
return d + 1;
}).map(d -> d + 1).blockLast();
Awaitility.await().untilAsserted(() -> {
then(spanInOperation.get().context().traceId())
.isEqualTo(span.context().traceId());
});
then(this.tracer.currentSpan()).isEqualTo(span);
}
finally {
span.finish();
}
then(this.tracer.currentSpan()).isNull();
Span foo2 = this.tracer.nextSpan().name("foo").start();
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(foo2)) {
Flux.just(1, 2, 3).publishOn(Schedulers.single()).log("reactor.")
.map(d -> d + 1).map(d -> d + 1).map((d) -> {
spanInOperation.set(this.tracer.currentSpan());
return d + 1;
}).map(d -> d + 1).blockLast();
then(this.tracer.currentSpan()).isEqualTo(foo2);
// parent cause there's an async span in the meantime
then(spanInOperation.get().context().traceId())
.isEqualTo(foo2.context().traceId());
}
finally {
foo2.finish();
}
then(this.tracer.currentSpan()).isNull();
}
@Test
public void checkSequenceOfOperations() {
Span parentSpan = this.tracer.nextSpan().name("foo").start();
log.info("Hello");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(parentSpan)) {
final Long spanId = Mono.fromCallable(this.tracer::currentSpan)
.map(span -> span.context().spanId()).block();
then(spanId).isNotNull();
final Long secondSpanId = Mono.fromCallable(this.tracer::currentSpan)
.map(span -> span.context().spanId()).block();
then(secondSpanId).isEqualTo(spanId); // different trace ids here
}
}
@Test
public void checkTraceIdDuringZipOperation() {
Span initSpan = this.tracer.nextSpan().name("foo").start();
final AtomicReference<Long> spanInOperation = new AtomicReference<>();
final AtomicReference<Long> spanInZipOperation = new AtomicReference<>();
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(initSpan)) {
Mono.fromCallable(this.tracer::currentSpan)
.map(span -> span.context().spanId()).doOnNext(spanInOperation::set)
.zipWith(Mono.fromCallable(this.tracer::currentSpan)
.map(span -> span.context().spanId())
.doOnNext(spanInZipOperation::set))
.block();
}
then(spanInZipOperation).hasValue(initSpan.context().spanId()); // ok here
then(spanInOperation).hasValue(initSpan.context().spanId()); // Expecting
// <AtomicReference[null]>
// to have value:
// <1L> but did
// not.
}
// #646
@Test
public void should_work_for_mono_just_with_flat_map() {
Span initSpan = this.tracer.nextSpan().name("foo").start();
log.info("Hello");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(initSpan)) {
Mono.just("value1")
.flatMap(request -> Mono.just("value2").then(Mono.just("foo")))
.map(a -> "qwe").block();
}
}
// #1030
@Test
public void checkTraceIdFromSubscriberContext() {
Span initSpan = this.tracer.nextSpan().name("foo").start();
final AtomicReference<Long> spanInSubscriberContext = new AtomicReference<>();
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(initSpan)) {
Mono.subscriberContext()
.map(context -> this.tracer.currentSpan().context().spanId())
.doOnNext(spanInSubscriberContext::set).block();
}
then(spanInSubscriberContext).hasValue(initSpan.context().spanId()); // ok here
}
@Test
public void should_pass_tracing_info_into_inner_publishers() {
Span span = this.tracer.nextSpan().name("foo").start();
final AtomicReference<Span> spanInOperation = new AtomicReference<>();
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
Flux.range(0, 5)
.flatMap(it -> Mono.delay(Duration.ofMillis(1))
.map(context -> this.tracer.currentSpan())
.doOnNext(spanInOperation::set))
.blockFirst();
}
finally {
span.finish();
}
then(spanInOperation.get().context().spanId()).isEqualTo(span.context().spanId());
}
@EnableAutoConfiguration
@Configuration
static class Config {
@Bean
Sampler sampler() {
return Sampler.ALWAYS_SAMPLE;
}
}
}