/* * Copyright (c) 2011-2018 Pivotal Software Inc, All Rights Reserved. * * 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 reactor.core.publisher; import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.LongAdder; import java.util.logging.Level; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.Fuseable; import reactor.core.Scannable.Attr; import reactor.core.publisher.FluxUsingWhen.ResourceSubscriber; import reactor.core.publisher.FluxUsingWhen.UsingWhenSubscriber; import reactor.test.StepVerifier; import reactor.test.publisher.PublisherProbe; import reactor.test.publisher.TestPublisher; import reactor.test.util.TestLogger; import reactor.util.Loggers; import reactor.util.annotation.Nullable; import reactor.util.context.Context; import reactor.util.function.Tuple2; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; public class FluxUsingWhenTest { @Test public void nullResourcePublisherRejected() { assertThatNullPointerException() .isThrownBy(() -> Flux.usingWhen(null, tr -> Mono.empty(), tr -> Mono.empty(), (tr, err) -> Mono.empty(), tr -> Mono.empty())) .withMessage("resourceSupplier") .withNoCause(); } @Test public void emptyResourcePublisherDoesntApplyCallback() { AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); Flux<String> test = Flux.usingWhen(Flux.empty().hide(), tr -> Mono.just("unexpected"), tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> rollbackDone.set(true))); StepVerifier.create(test) .verifyComplete(); assertThat(commitDone).isFalse(); assertThat(rollbackDone).isFalse(); } @Test public void emptyResourceCallableDoesntApplyCallback() { AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); Flux<String> test = Flux.usingWhen(Flux.empty(), tr -> Mono.just("unexpected"), tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> rollbackDone.set(true))); StepVerifier.create(test) .verifyComplete(); assertThat(commitDone).isFalse(); assertThat(rollbackDone).isFalse(); } @Test public void errorResourcePublisherDoesntApplyCallback() { AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); Flux<String> test = Flux.usingWhen(Flux.error(new IllegalStateException("boom")).hide(), tr -> Mono.just("unexpected"), tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> rollbackDone.set(true))); StepVerifier.create(test) .verifyErrorSatisfies(e -> assertThat(e) .isInstanceOf(IllegalStateException.class) .hasMessage("boom") .hasNoCause() .hasNoSuppressedExceptions() ); assertThat(commitDone).isFalse(); assertThat(rollbackDone).isFalse(); } @Test public void errorResourceCallableDoesntApplyCallback() { AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); Flux<String> test = Flux.usingWhen(Flux.error(new IllegalStateException("boom")), tr -> Mono.just("unexpected"), tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> rollbackDone.set(true))); StepVerifier.create(test) .verifyErrorSatisfies(e -> assertThat(e) .isInstanceOf(IllegalStateException.class) .hasMessage("boom") .hasNoCause() .hasNoSuppressedExceptions() ); assertThat(commitDone).isFalse(); assertThat(rollbackDone).isFalse(); } @Test public void errorResourcePublisherAfterEmitIsDropped() { AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); TestPublisher<String> testPublisher = TestPublisher.createCold(); testPublisher.next("Resource").error(new IllegalStateException("boom")); Flux<String> test = Flux.usingWhen(testPublisher, Mono::just, tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> rollbackDone.set(true))); StepVerifier.create(test) .expectNext("Resource") .expectComplete() .verifyThenAssertThat(Duration.ofSeconds(2)) .hasDroppedErrorWithMessage("boom") .hasNotDroppedElements(); assertThat(commitDone).isTrue(); assertThat(rollbackDone).isFalse(); testPublisher.assertCancelled(); } @Test public void secondResourceInPublisherIsDropped() { AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); TestPublisher<String> testPublisher = TestPublisher.createCold(); testPublisher.emit("Resource", "boom"); Flux<String> test = Flux.usingWhen(testPublisher, Mono::just, tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> rollbackDone.set(true))); StepVerifier.create(test) .expectNext("Resource") .expectComplete() .verifyThenAssertThat(Duration.ofSeconds(2)) .hasDropped("boom") .hasNotDroppedErrors(); assertThat(commitDone).isTrue(); assertThat(rollbackDone).isFalse(); testPublisher.assertCancelled(); } @Test public void fluxResourcePublisherIsCancelled() { AtomicBoolean cancelled = new AtomicBoolean(); AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); Flux<String> resourcePublisher = Flux.just("Resource", "Something Else") .doOnCancel(() -> cancelled.set(true)); Flux<String> test = Flux.usingWhen(resourcePublisher, Mono::just, tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> rollbackDone.set(true))); StepVerifier.create(test) .expectNext("Resource") .expectComplete() .verifyThenAssertThat() .hasNotDroppedErrors(); assertThat(commitDone).isTrue(); assertThat(rollbackDone).isFalse(); assertThat(cancelled).as("resource publisher was cancelled").isTrue(); } @Test public void monoResourcePublisherIsNotCancelled() { AtomicBoolean cancelled = new AtomicBoolean(); AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); Mono<String> resourcePublisher = Mono.just("Resource") .doOnCancel(() -> cancelled.set(true)); Flux<String> test = Flux.usingWhen(resourcePublisher, Flux::just, tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> rollbackDone.set(true))); StepVerifier.create(test) .expectNext("Resource") .expectComplete() .verifyThenAssertThat() .hasNotDroppedErrors(); assertThat(commitDone).isTrue(); assertThat(rollbackDone).isFalse(); assertThat(cancelled).as("resource publisher was not cancelled").isFalse(); } @Test public void lateFluxResourcePublisherIsCancelledOnCancel() { AtomicBoolean resourceCancelled = new AtomicBoolean(); AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); AtomicBoolean cancelDone = new AtomicBoolean(); Flux<String> resourcePublisher = Flux.<String>never() .doOnCancel(() -> resourceCancelled.set(true)); StepVerifier.create(Flux.usingWhen(resourcePublisher, Flux::just, tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> cancelDone.set(true)))) .expectSubscription() .expectNoEvent(Duration.ofMillis(100)) .thenCancel() .verify(Duration.ofSeconds(1)); assertThat(commitDone).as("commitDone").isFalse(); assertThat(rollbackDone).as("rollbackDone").isFalse(); assertThat(cancelDone).as("cancelDone").isFalse(); assertThat(resourceCancelled).as("resource cancelled").isTrue(); } @Test public void lateMonoResourcePublisherIsCancelledOnCancel() { AtomicBoolean resourceCancelled = new AtomicBoolean(); AtomicBoolean commitDone = new AtomicBoolean(); AtomicBoolean rollbackDone = new AtomicBoolean(); AtomicBoolean cancelDone = new AtomicBoolean(); Mono<String> resourcePublisher = Mono.<String>never() .doOnCancel(() -> resourceCancelled.set(true)); Mono<String> usingWhen = Mono.usingWhen(resourcePublisher, Mono::just, tr -> Mono.fromRunnable(() -> commitDone.set(true)), (tr, err) -> Mono.fromRunnable(() -> rollbackDone.set(true)), tr -> Mono.fromRunnable(() -> cancelDone.set(true))); StepVerifier.create(usingWhen) .expectSubscription() .expectNoEvent(Duration.ofMillis(100)) .thenCancel() .verify(Duration.ofSeconds(1)); assertThat(commitDone).as("commitDone").isFalse(); assertThat(rollbackDone).as("rollbackDone").isFalse(); assertThat(cancelDone).as("cancelDone").isFalse(); assertThat(resourceCancelled).as("resource cancelled").isTrue(); } @Test public void blockOnNeverResourceCanBeCancelled() throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); Disposable disposable = Flux.usingWhen(Flux.<String>never(), Flux::just, Flux::just, (res, err) -> Flux.just(res), Flux::just) .doFinally(f -> latch.countDown()) .subscribe(); assertThat(latch.await(500, TimeUnit.MILLISECONDS)) .as("hangs before dispose").isFalse(); disposable.dispose(); assertThat(latch.await(100, TimeUnit.MILLISECONDS)) .as("terminates after dispose").isTrue(); } @Test public void failToGenerateClosureAppliesRollback() { TestResource testResource = new TestResource(); Flux<String> test = Flux.usingWhen(Mono.just(testResource), tr -> { throw new UnsupportedOperationException("boom"); }, TestResource::commit, TestResource::rollback, TestResource::cancel); StepVerifier.create(test) .verifyErrorSatisfies(e -> assertThat(e).hasMessage("boom")); testResource.commitProbe.assertWasNotSubscribed(); testResource.cancelProbe.assertWasNotSubscribed(); testResource.rollbackProbe.assertWasSubscribed(); } @Test public void nullClosureAppliesRollback() { TestResource testResource = new TestResource(); Flux<String> test = Flux.usingWhen(Mono.just(testResource), tr -> null, TestResource::commit, TestResource::rollback, TestResource::cancel); StepVerifier.create(test) .verifyErrorSatisfies(e -> assertThat(e) .isInstanceOf(NullPointerException.class) .hasMessage("The resourceClosure function returned a null value")); testResource.commitProbe.assertWasNotSubscribed(); testResource.cancelProbe.assertWasNotSubscribed(); testResource.rollbackProbe.assertWasSubscribed(); } @ParameterizedTest @MethodSource("sources01") public void cancelWithHandler(Flux<String> source) { TestResource testResource = new TestResource(); Flux<String> test = Flux.usingWhen(Mono.just(testResource), tr -> source, TestResource::commit, TestResource::rollback, TestResource::cancel) .take(2); StepVerifier.create(test) .expectNext("0", "1") .verifyComplete(); testResource.commitProbe.assertWasNotSubscribed(); testResource.rollbackProbe.assertWasNotSubscribed(); testResource.cancelProbe.assertWasSubscribed(); } @ParameterizedTest @MethodSource("sources01") public void cancelWithHandlerFailure(Flux<String> source) { TestResource testResource = new TestResource(); final TestLogger tl = new TestLogger(); Loggers.useCustomLoggers(name -> tl); try { Flux<String> test = Flux.usingWhen(Mono.just(testResource), tr -> source, TestResource::commit, TestResource::rollback, r -> r.cancel() //immediate error to trigger the logging within the test .concatWith(Mono.error(new IllegalStateException("cancel error"))) ) .take(2); StepVerifier.create(test) .expectNext("0", "1") .verifyComplete(); testResource.commitProbe.assertWasNotSubscribed(); testResource.rollbackProbe.assertWasNotSubscribed(); testResource.cancelProbe.assertWasSubscribed(); } finally { Loggers.resetLoggerFactory(); } assertThat(tl.getErrContent()) .contains("Async resource cleanup failed after cancel") .contains("java.lang.IllegalStateException: cancel error"); } @ParameterizedTest @MethodSource("sources01") public void cancelWithHandlerGenerationFailureLogs(Flux<String> source) throws InterruptedException { TestLogger tl = new TestLogger(); Loggers.useCustomLoggers(name -> tl); TestResource testResource = new TestResource(); try { Flux<String> test = Flux.usingWhen(Mono.just(testResource), tr -> source, TestResource::commit, TestResource::rollback, r -> null) .take(2); StepVerifier.create(test) .expectNext("0", "1") .verifyComplete(); testResource.commitProbe.assertWasNotSubscribed(); testResource.cancelProbe.assertWasNotSubscribed(); testResource.rollbackProbe.assertWasNotSubscribed(); } finally { Loggers.resetLoggerFactory(); } assertThat(tl.getErrContent()) .contains("Error generating async resource cleanup during onCancel") .contains("java.lang.NullPointerException"); } @ParameterizedTest @MethodSource("sources01") @Deprecated public void cancelWithoutHandlerAppliesCommit(Flux<String> source) { TestResource testResource = new TestResource(); Flux<String> test = Flux .usingWhen(Mono.just(testResource).hide(), tr -> source, TestResource::commit, tr -> tr.rollback(new RuntimeException("placeholder rollback exception"))) .take(2); StepVerifier.create(test) .expectNext("0", "1") .verifyComplete(); testResource.commitProbe.assertWasSubscribed(); testResource.cancelProbe.assertWasNotSubscribed(); testResource.rollbackProbe.assertWasNotSubscribed(); } @ParameterizedTest @MethodSource("sources01") @Deprecated public void cancelDefaultHandlerFailure(Flux<String> source) { TestResource testResource = new TestResource(); final TestLogger tl = new TestLogger(); Loggers.useCustomLoggers(name -> tl); try { Flux<String> test = Flux.usingWhen(Mono.just(testResource), tr -> source, r -> r.commit() //immediate error to trigger the logging within the test .concatWith(Mono.error(new IllegalStateException("commit error"))), r -> r.rollback(new RuntimeException("placeholder ignored rollback exception")) ) .take(2); StepVerifier.create(test) .expectNext("0", "1") .verifyComplete(); testResource.commitProbe.assertWasSubscribed(); testResource.cancelProbe.assertWasNotSubscribed(); testResource.rollbackProbe.assertWasNotSubscribed(); } finally { Loggers.resetLoggerFactory(); } assertThat(tl.getErrContent()) .contains("Async resource cleanup failed after cancel") .contains("java.lang.IllegalStateException: commit error"); } @ParameterizedTest @MethodSource("sourcesFullTransaction") public void apiCommit(Flux<String> fullTransaction) { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return fullTransaction; }, TestResource::commit, TestResource::rollback, TestResource::cancel); StepVerifier.create(flux) .expectNext("Transaction started") .expectNext("work in transaction") .expectNext("more work in transaction") .expectComplete() .verify(); assertThat(ref.get()) .isNotNull() .matches(tr -> tr.commitProbe.wasSubscribed(), "commit method used") .matches(tr -> !tr.cancelProbe.wasSubscribed(), "no cancel") .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback"); } @ParameterizedTest @MethodSource("sourcesFullTransaction") public void apiCommitFailure(Flux<String> fullTransaction) { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return fullTransaction; }, TestResource::commitError, TestResource::rollback, TestResource::cancel); StepVerifier.create(flux) .expectNext("Transaction started") .expectNext("work in transaction") .expectNext("more work in transaction") .verifyErrorSatisfies(e -> assertThat(e) .hasMessage("Async resource cleanup failed after onComplete") .hasCauseInstanceOf(ArithmeticException.class)); assertThat(ref.get()) .isNotNull() .matches(tr -> tr.commitProbe.wasSubscribed(), "commit method used") .matches(tr -> !tr.cancelProbe.wasSubscribed(), "no cancel") .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback"); } @ParameterizedTest @MethodSource("sourcesFullTransaction") public void commitGeneratingNull(Flux<String> fullTransaction) { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return fullTransaction; }, TestResource::commitNull, TestResource::rollback, TestResource::cancel); StepVerifier.create(flux) .expectNext("Transaction started") .expectNext("work in transaction") .expectNext("more work in transaction") .verifyErrorSatisfies(e -> assertThat(e) .hasMessage("The asyncComplete returned a null Publisher") .isInstanceOf(NullPointerException.class) .hasNoCause()); assertThat(ref.get()) .isNotNull() .matches(tr -> !tr.commitProbe.wasSubscribed(), "commit method short-circuited") .matches(tr -> !tr.cancelProbe.wasSubscribed(), "no cancel") .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback"); } @ParameterizedTest @MethodSource("sourcesTransactionError") public void apiRollback(Flux<String> transactionWithError) { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return transactionWithError; }, TestResource::commitError, TestResource::rollback, TestResource::cancel); StepVerifier.create(flux) .expectNext("Transaction started") .expectNext("work in transaction") .verifyErrorSatisfies(e -> assertThat(e) .hasMessage("boom") .hasNoCause() .hasNoSuppressedExceptions()); assertThat(ref.get()) .isNotNull() .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") .matches(tr -> !tr.cancelProbe.wasSubscribed(), "no cancel") .matches(tr -> tr.rollbackProbe.wasSubscribed(), "rollback method used"); } @ParameterizedTest @MethodSource("sourcesTransactionError") public void apiRollbackFailure(Flux<String> transactionWithError) { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return transactionWithError; }, TestResource::commitError, TestResource::rollbackError, TestResource::cancel); StepVerifier.create(flux) .expectNext("Transaction started") .expectNext("work in transaction") .verifyErrorSatisfies(e -> assertThat(e) .hasMessage("Async resource cleanup failed after onError") .hasCauseInstanceOf(ArithmeticException.class) .hasSuppressedException(new IllegalStateException("boom"))); assertThat(ref.get()) .isNotNull() .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") .matches(tr -> !tr.cancelProbe.wasSubscribed(), "no cancel") .matches(tr -> tr.rollbackProbe.wasSubscribed(), "rollback method used"); } @ParameterizedTest @MethodSource("sourcesTransactionError") public void apiRollbackGeneratingNull(Flux<String> transactionWithError) { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return transactionWithError; }, TestResource::commitError, TestResource::rollbackNull, TestResource::cancel); StepVerifier.create(flux) .expectNext("Transaction started") .expectNext("work in transaction") .verifyErrorSatisfies(e -> assertThat(e) .hasMessage("The asyncError returned a null Publisher") .isInstanceOf(NullPointerException.class) .hasSuppressedException(new IllegalStateException("boom"))); assertThat(ref.get()) .isNotNull() .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") .matches(tr -> !tr.cancelProbe.wasSubscribed(), "no cancel") .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "rollback method short-circuited"); } @ParameterizedTest @MethodSource("sourcesFullTransaction") public void apiCancel(Flux<String> transactionWithError) { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return transactionWithError; }, TestResource::commit, TestResource::rollback, TestResource::cancel); StepVerifier.create(flux.take(1), 1) .expectNext("Transaction started") .verifyComplete(); assertThat(ref.get()) .isNotNull() .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback") .matches(tr -> tr.cancelProbe.wasSubscribed(), "cancel method used"); } @ParameterizedTest @MethodSource("sourcesFullTransaction") public void apiCancelFailure(Flux<String> transaction) { TestLogger testLogger = new TestLogger(); Loggers.useCustomLoggers(s -> testLogger); try { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return transaction; }, TestResource::commit, TestResource::rollback, TestResource::cancelError); StepVerifier.create(flux.take(1), 1) .expectNext("Transaction started") .verifyComplete(); assertThat(ref.get()) .isNotNull() .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback") .matches(tr -> tr.cancelProbe.wasSubscribed(), "cancel method used"); //since the CancelInner is subscribed in a fire-and-forget fashion, the log comes later //the test must be done before the finally, lest the error message be printed too late for TestLogger to catch it Awaitility.await().atMost(1, TimeUnit.SECONDS) .untilAsserted(() -> assertThat(testLogger.getErrContent()) .startsWith("[ WARN]") .contains("Async resource cleanup failed after cancel - java.lang.ArithmeticException: / by zero")); } finally { Loggers.resetLoggerFactory(); } } @ParameterizedTest @MethodSource("sourcesFullTransaction") public void apiCancelGeneratingNullLogs(Flux<String> transactionWithError) { TestLogger testLogger = new TestLogger(); Loggers.useCustomLoggers(s -> testLogger); try { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return transactionWithError; }, TestResource::commit, TestResource::rollback, TestResource::cancelNull); StepVerifier.create(flux.take(1), 1) .expectNext("Transaction started") .verifyComplete(); assertThat(ref.get()) .isNotNull() .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback") .matches(tr -> !tr.cancelProbe.wasSubscribed(), "cancel method short-circuited"); } finally { Loggers.resetLoggerFactory(); } assertThat(testLogger.getErrContent()) .contains("[ WARN] (" + Thread.currentThread().getName() + ") " + "Error generating async resource cleanup during onCancel - java.lang.NullPointerException"); } @Test @Deprecated public void apiSingleAsyncCleanup() { final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return d.data().concatWithValues("work in transaction"); }, TestResource::commit); StepVerifier.create(flux) .expectNext("Transaction started") .expectNext("work in transaction") .verifyComplete(); assertThat(ref.get()) .isNotNull() .matches(tr -> tr.commitProbe.wasSubscribed(), "commit method used") .matches(tr -> !tr.cancelProbe.wasSubscribed(), "no cancel") .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback"); } @Test @Deprecated public void apiSingleAsyncCleanupFailure() { final RuntimeException rollbackCause = new IllegalStateException("boom"); final AtomicReference<TestResource> ref = new AtomicReference<>(); Flux<String> flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), d -> { ref.set(d); return d.data().concatWithValues("work in transaction") .concatWith(Mono.error(rollbackCause)); }, TestResource::commitError); StepVerifier.create(flux) .expectNext("Transaction started") .expectNext("work in transaction") .verifyErrorSatisfies(e -> assertThat(e) .hasMessage("Async resource cleanup failed after onError") .hasCauseInstanceOf(ArithmeticException.class) .hasSuppressedException(rollbackCause)); assertThat(ref.get()) .isNotNull() .matches(tr -> tr.commitProbe.wasSubscribed(), "commit method used despite error") .matches(tr -> !tr.cancelProbe.wasSubscribed(), "no cancel") .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback"); } @Test public void normalHasNoQueueOperations() { final FluxPeekFuseableTest.AssertQueueSubscription<String> assertQueueSubscription = new FluxPeekFuseableTest.AssertQueueSubscription<>(); assertQueueSubscription.offer("foo"); UsingWhenSubscriber<String, String> test = new UsingWhenSubscriber<>(new LambdaSubscriber<>(null, null, null, null), "resource", it -> Mono.empty(), (it, err) -> Mono.empty(), null, Mockito.mock(Operators.DeferredSubscription.class)); test.onSubscribe(assertQueueSubscription); assertThat(test).isNotInstanceOf(Fuseable.QueueSubscription.class); } @ParameterizedTest @MethodSource("sourcesContext") public void contextPropagationOnCommit(Mono<String> source) { AtomicReference<String> probeContextValue = new AtomicReference<>(); AtomicReference<String> resourceContextValue = new AtomicReference<>(); TestResource testResource = new TestResource(); PublisherProbe<String> probe = PublisherProbe.of( Mono.subscriberContext() .map(it -> it.get(String.class)) .doOnNext(probeContextValue::set) .onErrorReturn("fail") ); Mono<String> contextHandler = probe.mono(); Mono<TestResource> resourceProvider = Mono.just(testResource) .zipWith(Mono.subscriberContext()) .doOnNext(it -> resourceContextValue.set(it.getT2().get(String.class))) .map(Tuple2::getT1); Flux.usingWhen(resourceProvider, r -> source, r -> contextHandler, TestResource::rollback, TestResource::cancel) .subscriberContext(Context.of(String.class, "contextual")) .as(StepVerifier::create) .expectAccessibleContext().contains(String.class, "contextual") .then() .expectNext("contextual") .verifyComplete(); testResource.commitProbe.assertWasNotSubscribed(); testResource.cancelProbe.assertWasNotSubscribed(); testResource.rollbackProbe.assertWasNotSubscribed(); probe.assertWasSubscribed(); assertThat(probeContextValue).hasValue("contextual"); assertThat(resourceContextValue).hasValue("contextual"); } @ParameterizedTest @MethodSource("sourcesContextError") public void contextPropagationOnRollback(Mono<String> source) { AtomicReference<String> probeContextValue = new AtomicReference<>(); AtomicReference<String> resourceContextValue = new AtomicReference<>(); TestResource testResource = new TestResource(); PublisherProbe<String> probe = PublisherProbe.of( Mono.subscriberContext() .map(it -> it.get(String.class)) .doOnNext(probeContextValue::set) .onErrorReturn("fail") ); Mono<String> contextHandler = probe.mono(); Mono<TestResource> resourceProvider = Mono.just(testResource) .zipWith(Mono.subscriberContext()) .doOnNext(it -> resourceContextValue.set(it.getT2().get(String.class))) .map(Tuple2::getT1); Flux.usingWhen(resourceProvider, r -> source, TestResource::commit, (r, err) -> contextHandler, TestResource::cancel) .subscriberContext(Context.of(String.class, "contextual")) .as(StepVerifier::create) .expectAccessibleContext().contains(String.class, "contextual") .then() .verifyErrorMessage("boom"); testResource.commitProbe.assertWasNotSubscribed(); testResource.cancelProbe.assertWasNotSubscribed(); testResource.rollbackProbe.assertWasNotSubscribed(); probe.assertWasSubscribed(); assertThat(probeContextValue).hasValue("contextual"); assertThat(resourceContextValue).hasValue("contextual"); } @ParameterizedTest @MethodSource("sources01") public void contextPropagationOnCancel(Flux<String> source) { TestResource testResource = new TestResource(); AtomicReference<Throwable> errorRef = new AtomicReference<>(); PublisherProbe<String> probe = PublisherProbe.of( Mono.subscriberContext() .map(it -> it.get(String.class)) .doOnError(errorRef::set) .onErrorReturn("fail") ); Mono<String> cancelHandler = probe.mono(); Flux.usingWhen(Mono.just(testResource), r -> source, TestResource::commit, TestResource::rollback, cancel -> cancelHandler) .subscriberContext(Context.of(String.class, "contextual")) .take(1) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); testResource.rollbackProbe.assertWasNotSubscribed(); testResource.commitProbe.assertWasNotSubscribed(); probe.assertWasSubscribed(); assertThat(errorRef).hasValue(null); } @ParameterizedTest @MethodSource("sources01") public void contextPropagationOnCancelWithNoHandler(Flux<String> source) { TestResource testResource = new TestResource(); AtomicReference<Throwable> errorRef = new AtomicReference<>(); PublisherProbe<String> probe = PublisherProbe.of( Mono.subscriberContext() .map(it -> it.get(String.class)) .doOnError(errorRef::set) .onErrorReturn("fail") ); Mono<String> cancelHandler = probe.mono(); new FluxUsingWhen<>(Mono.just(testResource), r -> source, commit -> cancelHandler, TestResource::rollback, null) .subscriberContext(Context.of(String.class, "contextual")) .take(1) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); testResource.rollbackProbe.assertWasNotSubscribed(); testResource.commitProbe.assertWasNotSubscribed(); probe.assertWasSubscribed(); assertThat(errorRef).hasValue(null); } // == tests checking callbacks don't pile up == @Test public void noCancelCallbackAfterComplete() { LongAdder cleanupCount = new LongAdder(); Flux<String> flux = Flux.usingWhen(Mono.defer(() -> Mono.just("foo")), Mono::just, s -> Mono.fromRunnable(() -> cleanupCount.add(10)), //10 for completion (s, err) -> Mono.fromRunnable(() -> cleanupCount.add(100)), //100 for error s -> Mono.fromRunnable(() -> cleanupCount.add(1000)) //1000 for cancel ); flux.subscribe(new CoreSubscriber<Object>() { Subscription subscription; @Override public void onSubscribe(Subscription s) { s.request(1); subscription = s; } @Override public void onNext(Object o) {} @Override public void onError(Throwable t) {} @Override public void onComplete() { subscription.cancel(); } }); assertThat(cleanupCount.sum()).isEqualTo(10); } @Test public void noCancelCallbackAfterError() { LongAdder cleanupCount = new LongAdder(); Flux<String> flux = Flux.usingWhen(Mono.just("foo"), v -> Mono.error(new IllegalStateException("boom")), s -> Mono.fromRunnable(() -> cleanupCount.add(10)), //10 for completion (s, err) -> Mono.fromRunnable(() -> cleanupCount.add(100)), //100 for error s -> Mono.fromRunnable(() -> cleanupCount.add(1000)) //1000 for cancel ); flux.subscribe(new CoreSubscriber<Object>() { Subscription subscription; @Override public void onSubscribe(Subscription s) { s.request(1); subscription = s; } @Override public void onNext(Object o) {} @Override public void onError(Throwable t) { subscription.cancel(); } @Override public void onComplete() {} }); assertThat(cleanupCount.sum()).isEqualTo(100); } @Test public void noCompleteCallbackAfterCancel() throws InterruptedException { AtomicBoolean cancelled = new AtomicBoolean(); LongAdder cleanupCount = new LongAdder(); Publisher<String> badPublisher = s -> s.onSubscribe(new Subscription() { @Override public void request(long n) { new Thread(() -> { s.onNext("foo1"); try { Thread.sleep(100); } catch (InterruptedException e) {} s.onComplete(); }).start(); } @Override public void cancel() { cancelled.set(true); } }); Flux<String> flux = Flux.usingWhen(Mono.just("foo"), v -> badPublisher, s -> Mono.fromRunnable(() -> cleanupCount.add(10)), //10 for completion (s, err) -> Mono.fromRunnable(() -> cleanupCount.add(100)), //100 for error s -> Mono.fromRunnable(() -> cleanupCount.add(1000)) //1000 for cancel ); flux.subscribe(new CoreSubscriber<String>() { Subscription subscription; @Override public void onSubscribe(Subscription s) { s.request(1); subscription = s; } @Override public void onNext(String o) { subscription.cancel(); } @Override public void onError(Throwable t) {} @Override public void onComplete() {} }); Thread.sleep(300); assertThat(cleanupCount.sum()).isEqualTo(1000); assertThat(cancelled).as("source cancelled").isTrue(); } @Test public void noErrorCallbackAfterCancel() throws InterruptedException { AtomicBoolean cancelled = new AtomicBoolean(); LongAdder cleanupCount = new LongAdder(); Publisher<String> badPublisher = s -> s.onSubscribe(new Subscription() { @Override public void request(long n) { new Thread(() -> { s.onNext("foo1"); try { Thread.sleep(100); } catch (InterruptedException e) {} s.onError(new IllegalStateException("boom")); }).start(); } @Override public void cancel() { cancelled.set(true); } }); Flux<String> flux = Flux.usingWhen(Mono.just("foo"), v -> badPublisher, s -> Mono.fromRunnable(() -> cleanupCount.add(10)), //10 for completion (s, err) -> Mono.fromRunnable(() -> cleanupCount.add(100)), //100 for error s -> Mono.fromRunnable(() -> cleanupCount.add(1000)) //1000 for cancel ); flux.subscribe(new CoreSubscriber<String>() { Subscription subscription; @Override public void onSubscribe(Subscription s) { s.request(1); subscription = s; } @Override public void onNext(String o) { subscription.cancel(); } @Override public void onError(Throwable t) {} @Override public void onComplete() {} }); Thread.sleep(300); assertThat(cleanupCount.sum()).isEqualTo(1000); assertThat(cancelled).as("source cancelled").isTrue(); } @Test public void errorCallbackReceivesCause() { AtomicReference<Throwable> errorRef = new AtomicReference<>(); NullPointerException npe = new NullPointerException("original error"); Flux.usingWhen(Flux.just("ignored"), s -> Flux.concat(Flux.just("ignored1", "ignored2"), Flux.error(npe)), Mono::just, (res, err) -> Mono.fromRunnable(() -> errorRef.set(err)), Mono::just) .as(StepVerifier::create) .expectNext("ignored1", "ignored2") .verifyErrorSatisfies(e -> assertThat(e).isSameAs(npe) .hasNoCause() .hasNoSuppressedExceptions()); assertThat(errorRef).hasValue(npe); } // == scanUnsafe tests == @Test public void scanOperator() { FluxUsingWhen<Object, Object> op = new FluxUsingWhen<>(Mono.empty(), Mono::just, Mono::just, (s, err) -> Mono.just(s), Mono::just); assertThat(op.scanUnsafe(Attr.ACTUAL)) .isSameAs(op.scanUnsafe(Attr.ACTUAL_METADATA)) .isSameAs(op.scanUnsafe(Attr.BUFFERED)) .isSameAs(op.scanUnsafe(Attr.CAPACITY)) .isSameAs(op.scanUnsafe(Attr.CANCELLED)) .isSameAs(op.scanUnsafe(Attr.DELAY_ERROR)) .isSameAs(op.scanUnsafe(Attr.ERROR)) .isSameAs(op.scanUnsafe(Attr.LARGE_BUFFERED)) .isSameAs(op.scanUnsafe(Attr.NAME)) .isSameAs(op.scanUnsafe(Attr.PARENT)) .isSameAs(op.scanUnsafe(Attr.RUN_ON)) .isSameAs(op.scanUnsafe(Attr.PREFETCH)) .isSameAs(op.scanUnsafe(Attr.REQUESTED_FROM_DOWNSTREAM)) .isSameAs(op.scanUnsafe(Attr.TERMINATED)) .isSameAs(op.scanUnsafe(Attr.TAGS)) .isNull(); } @Test public void scanResourceSubscriber() { CoreSubscriber<Integer> actual = new LambdaSubscriber<>(null, e -> {}, null, null); ResourceSubscriber<String, Integer> op = new ResourceSubscriber<>(actual, s -> Flux.just(s.length()), Mono::just, (s, err) -> Mono.just(s), Mono::just, true); final Subscription parent = Operators.emptySubscription(); op.onSubscribe(parent); assertThat(op.scan(Attr.PARENT)).as("PARENT").isSameAs(parent); assertThat(op.scan(Attr.ACTUAL)).as("ACTUAL").isSameAs(actual); assertThat(op.scan(Attr.PREFETCH)).as("PREFETCH").isEqualTo(Integer.MAX_VALUE); assertThat(op.scan(Attr.TERMINATED)).as("TERMINATED").isFalse(); op.resourceProvided = true; assertThat(op.scan(Attr.TERMINATED)).as("TERMINATED resourceProvided").isTrue(); assertThat(op.scanUnsafe(Attr.CANCELLED)).as("CANCELLED not supported").isNull(); } @Test public void scanUsingWhenSubscriber() { CoreSubscriber<? super Integer> actual = new LambdaSubscriber<>(null, e -> {}, null, null); UsingWhenSubscriber<Integer, String> op = new UsingWhenSubscriber<>(actual, "RESOURCE", Mono::just, (s, err) -> Mono.just(s), Mono::just, null); final Subscription parent = Operators.emptySubscription(); op.onSubscribe(parent); assertThat(op.scan(Attr.PARENT)).as("PARENT").isSameAs(parent); assertThat(op.scan(Attr.ACTUAL)).as("ACTUAL") .isSameAs(actual) .isSameAs(op.actual()); assertThat(op.scan(Attr.TERMINATED)).as("pre TERMINATED").isFalse(); assertThat(op.scan(Attr.CANCELLED)).as("pre CANCELLED").isFalse(); op.deferredError(new IllegalStateException("boom")); assertThat(op.scan(Attr.TERMINATED)).as("TERMINATED with error").isTrue(); assertThat(op.scan(Attr.ERROR)).as("ERROR").hasMessage("boom"); op.cancel(); assertThat(op.scan(Attr.CANCELLED)).as("CANCELLED").isTrue(); } @Test public void scanCommitInner() { CoreSubscriber<? super Integer> actual = new LambdaSubscriber<>(null, e -> {}, null, null); UsingWhenSubscriber<Integer, String> up = new UsingWhenSubscriber<>(actual, "RESOURCE", Mono::just, (s, err) -> Mono.just(s), Mono::just, null); final Subscription parent = Operators.emptySubscription(); up.onSubscribe(parent); FluxUsingWhen.CommitInner op = new FluxUsingWhen.CommitInner(up); assertThat(op.scan(Attr.PARENT)).as("PARENT").isSameAs(up); assertThat(op.scan(Attr.ACTUAL)).as("ACTUAL").isSameAs(up.actual); assertThat(op.scan(Attr.TERMINATED)).as("TERMINATED before").isFalse(); op.onError(new IllegalStateException("boom")); assertThat(op.scan(Attr.TERMINATED)) .as("TERMINATED by error") .isSameAs(up.scan(Attr.TERMINATED)) .isTrue(); assertThat(up.scan(Attr.ERROR)).as("parent ERROR") .hasMessage("Async resource cleanup failed after onComplete") .hasCause(new IllegalStateException("boom")); assertThat(op.scanUnsafe(Attr.PREFETCH)).as("PREFETCH not supported").isNull(); } @Test public void scanRollbackInner() { CoreSubscriber<? super Integer> actual = new LambdaSubscriber<>(null, e -> {}, null, null); UsingWhenSubscriber<Integer, String> up = new UsingWhenSubscriber<>(actual, "RESOURCE", Mono::just, (s, err) -> Mono.just(s), Mono::just, null); final Subscription parent = Operators.emptySubscription(); up.onSubscribe(parent); FluxUsingWhen.RollbackInner op = new FluxUsingWhen.RollbackInner(up, new IllegalStateException("rollback cause")); assertThat(op.scan(Attr.PARENT)).as("PARENT").isSameAs(up); assertThat(op.scan(Attr.ACTUAL)).as("ACTUAL").isSameAs(up.actual); assertThat(op.scan(Attr.TERMINATED)).as("TERMINATED before").isFalse(); op.onComplete(); assertThat(op.scan(Attr.TERMINATED)) .as("TERMINATED by complete") .isSameAs(up.scan(Attr.TERMINATED)) .isTrue(); assertThat(up.scan(Attr.ERROR)).as("parent ERROR").hasMessage("rollback cause"); assertThat(op.scanUnsafe(Attr.PREFETCH)).as("PREFETCH not supported").isNull(); } @Test public void scanCancelInner() { CoreSubscriber<? super Integer> actual = new LambdaSubscriber<>(null, e -> {}, null, null); UsingWhenSubscriber<Integer, String> up = new UsingWhenSubscriber<>(actual, "RESOURCE", Mono::just, (s, err) -> Mono.just(s), Mono::just, null); final Subscription parent = Operators.emptySubscription(); up.onSubscribe(parent); FluxUsingWhen.CancelInner op = new FluxUsingWhen.CancelInner(up); assertThat(op.scan(Attr.PARENT)).as("PARENT").isSameAs(up); assertThat(op.scan(Attr.ACTUAL)).as("ACTUAL").isSameAs(up.actual); assertThat(op.scanUnsafe(Attr.PREFETCH)).as("PREFETCH not supported").isNull(); } // == utility test classes == static class TestResource { private static final Duration DELAY = Duration.ofMillis(100); final Level level; PublisherProbe<Integer> commitProbe = PublisherProbe.empty(); PublisherProbe<Integer> rollbackProbe = PublisherProbe.empty(); PublisherProbe<Integer> cancelProbe = PublisherProbe.empty(); TestResource() { this.level = Level.FINE; } TestResource(Level level) { this.level = level; } public Flux<String> data() { return Flux.just("Transaction started"); } public Flux<Integer> commit() { this.commitProbe = PublisherProbe.of( Flux.just(3, 2, 1) .log("commit method used", level, SignalType.ON_NEXT, SignalType.ON_COMPLETE)); return commitProbe.flux(); } public Flux<Integer> commitDelay() { this.commitProbe = PublisherProbe.of( Flux.just(3, 2, 1) .delayElements(DELAY) .log("commit method used", level, SignalType.ON_NEXT, SignalType.ON_COMPLETE)); return commitProbe.flux(); } public Flux<Integer> commitError() { this.commitProbe = PublisherProbe.of( Flux.just(3, 2, 1) .delayElements(DELAY) .map(i -> 100 / (i - 1)) //results in divide by 0 .log("commit method used", level, SignalType.ON_NEXT, SignalType.ON_COMPLETE)); return commitProbe.flux(); } @Nullable public Flux<Integer> commitNull() { return null; } public Flux<Integer> rollback(Throwable error) { this.rollbackProbe = PublisherProbe.of( Flux.just(5, 4, 3, 2, 1) .log("rollback me thod used on: " + error, level, SignalType.ON_NEXT, SignalType.ON_COMPLETE)); return rollbackProbe.flux(); } public Flux<Integer> rollbackDelay(Throwable error) { this.rollbackProbe = PublisherProbe.of( Flux.just(5, 4, 3, 2, 1) .delayElements(DELAY) .log("rollback method used on: " + error, level, SignalType.ON_NEXT, SignalType.ON_COMPLETE)); return rollbackProbe.flux(); } public Flux<Integer> rollbackError(Throwable error) { this.rollbackProbe = PublisherProbe.of( Flux.just(5, 4, 3, 2, 1) .delayElements(DELAY) .map(i -> 100 / (i - 1)) //results in divide by 0 .log("rollback method used on: " + error, level, SignalType.ON_NEXT, SignalType.ON_COMPLETE)); return rollbackProbe.flux(); } @Nullable public Flux<Integer> rollbackNull(Throwable error) { return null; } public Flux<Integer> cancel() { this.cancelProbe = PublisherProbe.of( Flux.just(5, 4, 3, 2, 1) .log("cancel method used", level, SignalType.ON_NEXT, SignalType.ON_COMPLETE)); return cancelProbe.flux(); } public Flux<Integer> cancelError() { this.cancelProbe = PublisherProbe.of( Flux.just(5, 4, 3, 2, 1) .delayElements(DELAY) .map(i -> 100 / (i - 1)) //results in divide by 0 .log("cancel method used", level, SignalType.ON_NEXT, SignalType.ON_COMPLETE)); return cancelProbe.flux(); } @Nullable public Flux<Integer> cancelNull() { return null; } } //unit test parameter providers private static Object[] sources01() { return new Object[] { new Object[] { Flux.interval(Duration.ofMillis(100)).map(String::valueOf) }, new Object[] { Flux.range(0, 2).map(String::valueOf) } }; } private static Object[] sourcesFullTransaction() { return new Object[] { new Object[] { Flux.just("Transaction started", "work in transaction", "more work in transaction").hide() }, new Object[] { Flux.just("Transaction started", "work in transaction", "more work in transaction") } }; } private static Object[] sourcesTransactionError() { return new Object[] { new Object[] { Flux.just("Transaction started", "work in transaction") .concatWith(Mono.error(new IllegalStateException("boom"))) }, new Object[] { Flux.just("Transaction started", "work in transaction", "boom") .map(v -> { if (v.length() > 4) return v; else throw new IllegalStateException("boom"); } ) } }; } private static Object[] sourcesContext() { return new Object[] { new Object[] { Mono.subscriberContext().map(it -> it.get(String.class)).hide() }, new Object[] { Mono.subscriberContext().map(it -> it.get(String.class)) } }; } private static Object[] sourcesContextError() { return new Object[] { new Object[] { Mono .subscriberContext() .map(it -> it.get(String.class)) .hide() .map(it -> { throw new IllegalStateException("boom"); }) }, new Object[] { Mono .subscriberContext() .map(it -> it.get(String.class)) .map(it -> { throw new IllegalStateException("boom"); }) } }; } }