|
5 | 5 |
|
6 | 6 | package io.opentelemetry.instrumentation.reactor.v3_1;
|
7 | 7 |
|
| 8 | +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; |
8 | 9 | import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
|
9 | 10 | import static java.lang.invoke.MethodType.methodType;
|
10 | 11 | import static org.assertj.core.api.Assertions.assertThat;
|
|
20 | 21 | import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
|
21 | 22 | import java.lang.invoke.MethodHandle;
|
22 | 23 | import java.lang.invoke.MethodHandles;
|
| 24 | +import java.lang.reflect.Method; |
23 | 25 | import java.time.Duration;
|
| 26 | +import java.util.concurrent.atomic.AtomicBoolean; |
24 | 27 | import java.util.function.Function;
|
25 | 28 | import org.junit.jupiter.api.AfterAll;
|
26 | 29 | import org.junit.jupiter.api.BeforeAll;
|
27 | 30 | import org.junit.jupiter.api.Test;
|
28 | 31 | import org.junit.jupiter.api.extension.RegisterExtension;
|
| 32 | +import org.junit.jupiter.params.ParameterizedTest; |
| 33 | +import org.junit.jupiter.params.provider.ValueSource; |
| 34 | +import org.reactivestreams.Publisher; |
29 | 35 | import reactor.core.Scannable;
|
30 | 36 | import reactor.core.publisher.Flux;
|
31 | 37 | import reactor.core.publisher.Mono;
|
@@ -406,6 +412,133 @@ void doesNotOverrideInnerCurrentSpansWithThereIsOuterCurrent() {
|
406 | 412 | .hasAttributes(attributeEntry("onNext", true))));
|
407 | 413 | }
|
408 | 414 |
|
| 415 | + @ParameterizedTest |
| 416 | + @ValueSource(strings = {"retry", "retryWhen"}) |
| 417 | + void doesNotLeakContextOnRetry(String retryKind) { |
| 418 | + // retry calls subscribe again from onError where we have active context, check that this |
| 419 | + // context is not used as parent for retried operations |
| 420 | + AtomicBoolean beforeRetry = new AtomicBoolean(true); |
| 421 | + Flux<Integer> publish = |
| 422 | + Flux.create( |
| 423 | + sink -> { |
| 424 | + for (int i = 0; i < 2; i++) { |
| 425 | + int index = i; |
| 426 | + testing.runWithSpan( |
| 427 | + "produce " + (beforeRetry.get() ? "before" : "after") + " retry " + i, |
| 428 | + () -> sink.next(index)); |
| 429 | + } |
| 430 | + }); |
| 431 | + |
| 432 | + Flux<Integer> flux = |
| 433 | + Flux.defer(() -> publish.delaySubscription(Duration.ofMillis(1))) |
| 434 | + .doOnNext(message -> testing.runWithSpan("process " + message, () -> {})) |
| 435 | + .handle( |
| 436 | + (message, sink) -> { |
| 437 | + if (message == 1 && beforeRetry.compareAndSet(true, false)) { |
| 438 | + sink.error(new RuntimeException("Message has error")); |
| 439 | + } else { |
| 440 | + sink.next(message); |
| 441 | + } |
| 442 | + }); |
| 443 | + |
| 444 | + switch (retryKind) { |
| 445 | + case "retry": |
| 446 | + flux = flux.retry(); |
| 447 | + break; |
| 448 | + case "retryWhen": |
| 449 | + flux = retryWhen(flux); |
| 450 | + break; |
| 451 | + default: |
| 452 | + throw new IllegalStateException("Unsupported retry kind " + retryKind); |
| 453 | + } |
| 454 | + |
| 455 | + flux.subscribe(); |
| 456 | + |
| 457 | + testing.waitAndAssertSortedTraces( |
| 458 | + orderByRootSpanName( |
| 459 | + "produce before retry 0", |
| 460 | + "produce before retry 1", |
| 461 | + "produce after retry 0", |
| 462 | + "produce after retry 1"), |
| 463 | + trace -> |
| 464 | + trace.hasSpansSatisfyingExactly( |
| 465 | + span -> span.hasName("produce before retry 0").hasNoParent(), |
| 466 | + span -> span.hasName("process 0").hasParent(trace.getSpan(0))), |
| 467 | + trace -> |
| 468 | + trace.hasSpansSatisfyingExactly( |
| 469 | + span -> span.hasName("produce before retry 1").hasNoParent(), |
| 470 | + span -> span.hasName("process 1").hasParent(trace.getSpan(0))), |
| 471 | + trace -> |
| 472 | + trace.hasSpansSatisfyingExactly( |
| 473 | + span -> span.hasName("produce after retry 0").hasNoParent(), |
| 474 | + span -> span.hasName("process 0").hasParent(trace.getSpan(0))), |
| 475 | + trace -> |
| 476 | + trace.hasSpansSatisfyingExactly( |
| 477 | + span -> span.hasName("produce after retry 1").hasNoParent(), |
| 478 | + span -> span.hasName("process 1").hasParent(trace.getSpan(0)))); |
| 479 | + } |
| 480 | + |
| 481 | + @Test |
| 482 | + void retryWithParentSpan() { |
| 483 | + AtomicBoolean beforeRetry = new AtomicBoolean(true); |
| 484 | + Flux<Integer> publish = |
| 485 | + Flux.create( |
| 486 | + sink -> |
| 487 | + testing.runWithSpan( |
| 488 | + "produce " + (beforeRetry.get() ? "before" : "after") + " retry", |
| 489 | + () -> sink.next(0))); |
| 490 | + |
| 491 | + Flux<Object> flux = |
| 492 | + Flux.defer(() -> publish.delaySubscription(Duration.ofMillis(1))) |
| 493 | + .doOnNext(message -> testing.runWithSpan("process", () -> {})) |
| 494 | + .handle( |
| 495 | + (message, sink) -> { |
| 496 | + if (beforeRetry.compareAndSet(true, false)) { |
| 497 | + sink.error(new RuntimeException("Message has error")); |
| 498 | + } else { |
| 499 | + sink.next(message); |
| 500 | + } |
| 501 | + }) |
| 502 | + .retry(); |
| 503 | + |
| 504 | + testing.runWithSpan("parent", () -> flux.subscribe()); |
| 505 | + |
| 506 | + testing.waitAndAssertTraces( |
| 507 | + trace -> |
| 508 | + trace.hasSpansSatisfyingExactly( |
| 509 | + span -> span.hasName("parent").hasNoParent(), |
| 510 | + span -> span.hasName("produce before retry").hasParent(trace.getSpan(0)), |
| 511 | + span -> span.hasName("process").hasParent(trace.getSpan(0)), |
| 512 | + span -> span.hasName("produce after retry").hasParent(trace.getSpan(0)), |
| 513 | + span -> span.hasName("process").hasParent(trace.getSpan(0)))); |
| 514 | + } |
| 515 | + |
| 516 | + @SuppressWarnings("unchecked") |
| 517 | + private static <T> Flux<T> retryWhen(Flux<T> flux) { |
| 518 | + try { |
| 519 | + Method method = Flux.class.getMethod("retryWhen", Function.class); |
| 520 | + Function<Flux<Throwable>, ? extends Publisher<?>> function = |
| 521 | + err -> Flux.create(sink -> sink.next(-1)); |
| 522 | + return (Flux<T>) method.invoke(flux, function); |
| 523 | + } catch (NoSuchMethodException exception) { |
| 524 | + // ignore |
| 525 | + } catch (Exception exception) { |
| 526 | + throw new IllegalStateException(exception); |
| 527 | + } |
| 528 | + |
| 529 | + try { |
| 530 | + Class<?> retryClass = Class.forName("reactor.util.retry.Retry"); |
| 531 | + Method retryWhenMethod = Flux.class.getMethod("retryWhen", retryClass); |
| 532 | + Method retrySpecMethod = retryClass.getMethod("indefinitely"); |
| 533 | + return (Flux<T>) retryWhenMethod.invoke(flux, retrySpecMethod.invoke(retryClass)); |
| 534 | + } catch (ClassNotFoundException | NoSuchMethodException exception) { |
| 535 | + // ignore |
| 536 | + } catch (Exception exception) { |
| 537 | + throw new IllegalStateException(exception); |
| 538 | + } |
| 539 | + throw new IllegalStateException("Could not find retryWhen method"); |
| 540 | + } |
| 541 | + |
409 | 542 | @SuppressWarnings("unchecked")
|
410 | 543 | private <T> Mono<T> monoSpan(Mono<T> mono, String spanName) {
|
411 | 544 |
|
|
0 commit comments