diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 863ba333eb..0394410e4c 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -256,6 +256,10 @@ task japicmp(type: JapicmpTask) { classExcludes = [ ] methodExcludes = [ + "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function)", + "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function, boolean)", + "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function)", + "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function, boolean)" ] } diff --git a/reactor-core/src/main/java/reactor/core/Exceptions.java b/reactor-core/src/main/java/reactor/core/Exceptions.java index 8328ca05a6..79c18fd462 100644 --- a/reactor-core/src/main/java/reactor/core/Exceptions.java +++ b/reactor-core/src/main/java/reactor/core/Exceptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, 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. @@ -23,6 +23,7 @@ import java.util.Objects; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Consumer; import reactor.core.publisher.Flux; import reactor.util.Logger; @@ -34,6 +35,7 @@ * Global Reactor Core Exception handling and utils to operate on. * * @author Stephane Maldini + * @author Injae Kim * @see Reactive-Streams-Commons */ public abstract class Exceptions { @@ -861,4 +863,16 @@ static final class StaticThrowable extends Error { } } + /** + * A general-purpose {@link Consumer} that closes {@link AutoCloseable} resource. + * If exception is thrown during closing the resource, it will be propagated by {@link Exceptions#propagate(Throwable)}. + */ + public static final Consumer AUTO_CLOSE = resource -> { + try { + resource.close(); + } catch (Throwable t) { + throw Exceptions.propagate(t); + } + }; + } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index b9a3213cb2..e4e9640e66 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, 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. @@ -119,6 +119,7 @@ * @author Stephane Maldini * @author David Karnok * @author Simon Baslé + * @author Injae Kim * * @see Mono */ @@ -2132,6 +2133,64 @@ public static Flux using(Callable resourceSupplier, Funct eager)); } + /** + * Uses an {@link AutoCloseable} resource, generated by a supplier for each individual Subscriber, + * while streaming the values from a Publisher derived from the same resource and makes sure + * the resource is released if the sequence terminates or the Subscriber cancels. + *

+ * Eager {@link AutoCloseable} resource cleanup happens just before the source termination and exceptions raised + * by the cleanup Consumer may override the terminal event. + *

+ * + *

+ * For an asynchronous version of the cleanup, with distinct path for onComplete, onError + * and cancel terminations, see {@link #usingWhen(Publisher, Function, Function, BiFunction, Function)}. + * + * @param resourceSupplier a {@link Callable} that is called on subscribe to generate the resource + * @param sourceSupplier a factory to derive a {@link Publisher} from the supplied resource + * @param emitted type + * @param resource type + * + * @return a new {@link Flux} built around a disposable resource + * @see #usingWhen(Publisher, Function, Function, BiFunction, Function) + * @see #usingWhen(Publisher, Function, Function) + */ + public static Flux using(Callable resourceSupplier, + Function> sourceSupplier) { + return using(resourceSupplier, sourceSupplier, true); + } + + /** + * Uses an {@link AutoCloseable} resource, generated by a supplier for each individual Subscriber, + * while streaming the values from a Publisher derived from the same resource and makes sure + * the resource is released if the sequence terminates or the Subscriber cancels. + *

+ *

+ *

+ * + *

+ * For an asynchronous version of the cleanup, with distinct path for onComplete, onError + * and cancel terminations, see {@link #usingWhen(Publisher, Function, Function, BiFunction, Function)}. + * + * @param resourceSupplier a {@link Callable} that is called on subscribe to generate the resource + * @param sourceSupplier a factory to derive a {@link Publisher} from the supplied resource + * @param eager true to clean before terminating downstream subscribers + * @param emitted type + * @param resource type + * + * @return a new {@link Flux} built around a disposable resource + * @see #usingWhen(Publisher, Function, Function, BiFunction, Function) + * @see #usingWhen(Publisher, Function, Function) + */ + public static Flux using(Callable resourceSupplier, + Function> sourceSupplier, boolean eager) { + return using(resourceSupplier, sourceSupplier, Exceptions.AUTO_CLOSE, eager); + } + /** * Uses a resource, generated by a {@link Publisher} for each individual {@link Subscriber}, * while streaming the values from a {@link Publisher} derived from the same resource. diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 083fe2e6dc..847cf3fcaa 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, 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. @@ -115,6 +115,7 @@ * @author Stephane Maldini * @author David Karnok * @author Simon Baslé + * @author Injae Kim * @see Flux */ public abstract class Mono implements CorePublisher { @@ -912,6 +913,56 @@ public static Mono using(Callable resourceSupplier, return using(resourceSupplier, sourceSupplier, resourceCleanup, true); } + /** + * Uses an {@link AutoCloseable} resource, generated by a supplier for each individual Subscriber, + * while streaming the value from a Mono derived from the same resource and makes sure + * the resource is released if the sequence terminates or the Subscriber cancels. + *

+ * Unlike in {@link Flux#using(Callable, Function, Consumer) Flux}, in the case of a valued {@link Mono} the cleanup + * happens just before passing the value to downstream. In all cases, exceptions raised by the cleanup + * {@link Consumer} may override the terminal event, discarding the element if the derived {@link Mono} was valued. + *

+ * + * + * @param resourceSupplier a {@link Callable} that is called on subscribe to create the resource + * @param sourceSupplier a {@link Mono} factory to create the Mono depending on the created resource + * @param emitted type + * @param resource type + * + * @return new {@link Mono} + */ + public static Mono using(Callable resourceSupplier, + Function> sourceSupplier) { + return using(resourceSupplier, sourceSupplier, true); + } + + /** + * Uses an {@link AutoCloseable} resource, generated by a supplier for each individual Subscriber, + * while streaming the value from a Mono derived from the same resource and makes sure + * the resource is released if the sequence terminates or the Subscriber cancels. + *

+ *

+ *

+ * + * + * @param resourceSupplier a {@link Callable} that is called on subscribe to create the resource + * @param sourceSupplier a {@link Mono} factory to create the Mono depending on the created resource + * @param eager set to true to clean before any signal (including onNext) is passed downstream + * @param emitted type + * @param resource type + * + * @return new {@link Mono} + */ + public static Mono using(Callable resourceSupplier, + Function> sourceSupplier, boolean eager) { + return using(resourceSupplier, sourceSupplier, Exceptions.AUTO_CLOSE, eager); + } /** * Uses a resource, generated by a {@link Publisher} for each individual {@link Subscriber}, diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxUsingTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxUsingTest.java index 31e3e16b42..71ab689ff3 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxUsingTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxUsingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, 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. @@ -20,10 +20,12 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import org.assertj.core.api.Assertions; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.reactivestreams.Subscription; @@ -31,6 +33,7 @@ import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.test.MockUtils; +import reactor.test.ParameterizedTestWithName; import reactor.test.StepVerifier; import reactor.test.publisher.FluxOperatorTest; import reactor.test.subscriber.AssertSubscriber; @@ -41,6 +44,246 @@ public class FluxUsingTest extends FluxOperatorTest { + public static List> sourcesNonEager() { + return Arrays.asList( + new CleanupCase("sourceNonEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set, false); + } + }, + new CleanupCase("autocloseableNonEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.range(1, 10), false); + } + } + ); + } + + public static List> sourcesEager() { + return Arrays.asList( + new CleanupCase("sourceEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set); + } + }, + new CleanupCase("sourceEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set, true); + } + }, + new CleanupCase("autocloseableEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.range(1, 10)); + } + }, + new CleanupCase("autocloseableEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.range(1, 10), true); + } + } + ); + } + + public static List> sourcesFailNonEager() { + return Arrays.asList( + new CleanupCase("sourceFailNonEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.error(new RuntimeException("forced failure")), cleanup::set, false); + } + }, + new CleanupCase("autocloseableFailNonEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.error(new RuntimeException("forced failure")), false); + } + } + ); + } + + public static List> sourcesFailEager() { + return Arrays.asList( + new CleanupCase("sourceFailEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.error(new RuntimeException("forced failure")), cleanup::set); + } + }, + new CleanupCase("sourceFailEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.error(new RuntimeException("forced failure")), cleanup::set, true); + } + }, + new CleanupCase("autocloseableFailEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.error(new RuntimeException("forced failure"))); + } + }, + new CleanupCase("autocloseableFailEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.error(new RuntimeException("forced failure")), true); + } + } + ); + } + + public static List> resourcesThrow() { + return Arrays.asList( + new CleanupCase("resourceThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(r, 10), cleanup::set, false); + } + }, + new CleanupCase("autocloseableResourceThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(1, 10), false); + } + }, + new CleanupCase("resourceThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(r, 10), cleanup::set); + } + }, + new CleanupCase("resourceThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(r, 10), cleanup::set, true); + } + }, + new CleanupCase("autocloseableResourceThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(1, 10)); + } + }, + new CleanupCase("autocloseableResourceThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(1, 10), true); + } + } + ); + } + + public static List> sourcesThrowNonEager() { + return Arrays.asList( + new CleanupCase("sourceThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set, false); + } + }, + new CleanupCase("autocloseableThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }, false); + } + } + ); + } + + public static List> sourcesThrowEager() { + return Arrays.asList( + new CleanupCase("sourceThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set); + } + }, + new CleanupCase("sourceThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set, true); + } + }, + new CleanupCase("autocloseableThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }); + } + }, + new CleanupCase("autocloseableThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }, true); + } + } + ); + } + + public static List> resourcesCleanupThrowNonEager() { + return Arrays.asList( + new CleanupCase("resourceCleanupThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), r -> { throw new IllegalStateException("resourceCleanup"); }, false); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Flux.range(1, 10), false); + } + } + ); + } + + public static List> resourcesCleanupThrowEager() { + return Arrays.asList( + new CleanupCase("resourceCleanupThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), r -> { throw new IllegalStateException("resourceCleanup"); }); + } + }, + new CleanupCase("resourceCleanupThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), r -> { throw new IllegalStateException("resourceCleanup"); }, true); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Flux.range(1, 10)); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Flux.range(1, 10), true); + } + } + ); + } + @Override protected Scenario defaultScenarioOptions(Scenario defaultOptions) { return defaultOptions.fusionMode(Fuseable.ANY) @@ -114,63 +357,55 @@ public void resourceCleanupNull() { }); } - @Test - public void normal() { + @ParameterizedTestWithName + @MethodSource("sourcesNonEager") + public void normal(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set, false) - .subscribe(ts); + cleanupCase.get().doAfterTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(0)).subscribe(ts); ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .assertComplete() .assertNoError(); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); } - @Test - public void normalEager() { + @ParameterizedTestWithName + @MethodSource("sourcesEager") + public void normalEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set) - .subscribe(ts); + cleanupCase.get() + .doFinally(event -> assertThat(cleanupCase.cleanup).hasValue(0)) + .doOnTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(1)) + .subscribe(ts); ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .assertComplete() .assertNoError(); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); } - void checkCleanupExecutionTime(boolean eager, boolean fail) { - AtomicInteger cleanup = new AtomicInteger(); + void checkCleanupExecutionTime(CleanupCase cleanupCase, boolean eager, boolean fail) { AtomicBoolean before = new AtomicBoolean(); AssertSubscriber ts = new AssertSubscriber() { @Override public void onError(Throwable t) { super.onError(t); - before.set(cleanup.get() != 0); + before.set(cleanupCase.cleanup.get() != 0); } @Override public void onComplete() { super.onComplete(); - before.set(cleanup.get() != 0); + before.set(cleanupCase.cleanup.get() != 0); } }; - Flux.using(() -> 1, r -> { - if (fail) { - return Flux.error(new RuntimeException("forced failure")); - } - return Flux.range(r, 10); - }, cleanup::set, eager) - .subscribe(ts); + cleanupCase.get().subscribe(ts); if (fail) { ts.assertNoValues() @@ -184,66 +419,110 @@ public void onComplete() { .assertNoError(); } - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); assertThat(before.get()).isEqualTo(eager); } - @Test - public void checkNonEager() { - checkCleanupExecutionTime(false, false); + @ParameterizedTestWithName + @MethodSource("sourcesNonEager") + public void checkNonEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, false, false); } - @Test - public void checkEager() { - checkCleanupExecutionTime(true, false); + @ParameterizedTestWithName + @MethodSource("sourcesEager") + public void checkEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, true, false); } - @Test - public void checkErrorNonEager() { - checkCleanupExecutionTime(false, true); + @ParameterizedTestWithName + @MethodSource("sourcesFailNonEager") + public void checkErrorNonEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, false, true); } - @Test - public void checkErrorEager() { - checkCleanupExecutionTime(true, true); + @ParameterizedTestWithName + @MethodSource("sourcesFailEager") + public void checkErrorEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, true, true); } - @Test - public void resourceThrowsEager() { + @ParameterizedTestWithName + @MethodSource("resourcesThrow") + public void resourceThrows(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Flux.using(() -> { - throw new RuntimeException("forced failure"); - }, r -> Flux.range(1, 10), cleanup::set, false) - .subscribe(ts); + cleanupCase.get().subscribe(ts); ts.assertNoValues() .assertNotComplete() .assertError(RuntimeException.class) .assertErrorMessage("forced failure"); - assertThat(cleanup).hasValue(0); + assertThat(cleanupCase.cleanup).hasValue(0); } - @Test - public void factoryThrowsEager() { + @ParameterizedTestWithName + @MethodSource("sourcesThrowNonEager") + public void factoryThrowsNonEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); + cleanupCase.get().doAfterTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(0)).subscribe(ts); - Flux.using(() -> 1, r -> { - throw new RuntimeException("forced failure"); - }, cleanup::set, false) - .subscribe(ts); + ts.assertNoValues() + .assertNotComplete() + .assertError(RuntimeException.class) + .assertErrorMessage("forced failure"); + + assertThat(cleanupCase.cleanup).hasValue(1); + } + + @ParameterizedTestWithName + @MethodSource("sourcesThrowEager") + public void factoryThrowsEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get() + .doFinally(event -> assertThat(cleanupCase.cleanup).hasValue(0)) + .doOnTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(1)) + .subscribe(ts); ts.assertNoValues() .assertNotComplete() .assertError(RuntimeException.class) .assertErrorMessage("forced failure"); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); + } + + @ParameterizedTestWithName + @MethodSource("resourcesCleanupThrowNonEager") + public void resourcesCleanupThrowNonEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get().subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertComplete() + .assertNoError(); + + assertThat(cleanupCase.cleanup).hasValue(0); + } + + @ParameterizedTestWithName + @MethodSource("resourcesCleanupThrowEager") + public void resourcesCleanupThrowEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get().subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertErrorWith(e -> { + assertThat(e).hasMessage("resourceCleanup"); + assertThat(e).isExactlyInstanceOf(IllegalStateException.class); + }); + + assertThat(cleanupCase.cleanup).hasValue(0); } @Test @@ -386,4 +665,19 @@ public void scanFuseableSubscriber() { Assertions.assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } + static abstract class CleanupCase implements Supplier> { + + final AtomicInteger cleanup = new AtomicInteger(); + final String name; + + CleanupCase(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + } diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java index 20ba9d5a40..620e85da69 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, 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. @@ -17,16 +17,21 @@ package reactor.core.publisher; import java.time.Duration; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.MethodSource; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Scannable; +import reactor.test.ParameterizedTestWithName; import reactor.test.StepVerifier; import reactor.test.subscriber.AssertSubscriber; @@ -36,6 +41,246 @@ public class MonoUsingTest { + public static List> sourcesNonEager() { + return Arrays.asList( + new CleanupCase("sourceNonEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, cleanup::set, false); + } + }, + new CleanupCase("autocloseableNonEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.just(1), false); + } + } + ); + } + + public static List> sourcesEager() { + return Arrays.asList( + new CleanupCase("sourceEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, cleanup::set); + } + }, + new CleanupCase("sourceEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, cleanup::set, true); + } + }, + new CleanupCase("autocloseableEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.just(1)); + } + }, + new CleanupCase("autocloseableEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.just(1), true); + } + } + ); + } + + public static List> sourcesFailNonEager() { + return Arrays.asList( + new CleanupCase("sourceFailNonEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> Mono.error(new RuntimeException("forced failure")), cleanup::set, false); + } + }, + new CleanupCase("autocloseableFailNonEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.error(new RuntimeException("forced failure")), false); + } + } + ); + } + + public static List> sourcesFailEager() { + return Arrays.asList( + new CleanupCase("sourceFailEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> Mono.error(new RuntimeException("forced failure")), cleanup::set); + } + }, + new CleanupCase("sourceFailEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> Mono.error(new RuntimeException("forced failure")), cleanup::set, true); + } + }, + new CleanupCase("autocloseableFailEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.error(new RuntimeException("forced failure"))); + } + }, + new CleanupCase("autocloseableFailEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.error(new RuntimeException("forced failure")), true); + } + } + ); + } + + public static List> resourcesThrow() { + return Arrays.asList( + new CleanupCase("resourceThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, Mono::just, cleanup::set, false); + } + }, + new CleanupCase("autocloseableResourceThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, r -> Mono.just(1), false); + } + }, + new CleanupCase("resourceThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, Mono::just, cleanup::set); + } + }, + new CleanupCase("resourceThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, Mono::just, cleanup::set, true); + } + }, + new CleanupCase("autocloseableResourceThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, r -> Mono.just(1)); + } + }, + new CleanupCase("autocloseableResourceThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, r -> Mono.just(1), true); + } + } + ); + } + + public static List> sourcesThrowNonEager() { + return Arrays.asList( + new CleanupCase("sourceThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set, false); + } + }, + new CleanupCase("autocloseableThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }, false); + } + } + ); + } + + public static List> sourcesThrowEager() { + return Arrays.asList( + new CleanupCase("sourceThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set); + } + }, + new CleanupCase("sourceThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set, true); + } + }, + new CleanupCase("autocloseableThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }); + } + }, + new CleanupCase("autocloseableThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }, true); + } + } + ); + } + + public static List> resourcesCleanupThrowNonEager() { + return Arrays.asList( + new CleanupCase("resourceCleanupThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, r -> { throw new IllegalStateException("resourceCleanup"); }, false); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Mono.just(1), false); + } + } + ); + } + + public static List> resourcesCleanupThrowEager() { + return Arrays.asList( + new CleanupCase("resourceCleanupThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, r -> { throw new IllegalStateException("resourceCleanup"); }); + } + }, + new CleanupCase("resourceCleanupThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, r -> { throw new IllegalStateException("resourceCleanup"); }, true); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Mono.just(1)); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Mono.just(1), true); + } + } + ); + } + @Test public void resourceSupplierNull() { assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { @@ -59,67 +304,55 @@ public void resourceCleanupNull() { }); } - @Test - public void normal() { + @ParameterizedTestWithName + @MethodSource("sourcesNonEager") + public void normal(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Mono.using(() -> 1, r -> Mono.just(1), cleanup::set, false) - .doAfterTerminate(() -> assertThat(cleanup).hasValue(0)) - .subscribe(ts); + cleanupCase.get().doAfterTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(0)).subscribe(ts); ts.assertValues(1) .assertComplete() .assertNoError(); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); } - @Test - public void normalEager() { + @ParameterizedTestWithName + @MethodSource("sourcesEager") + public void normalEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Mono.using(() -> 1, r -> Mono.just(1) - .doOnTerminate(() -> assertThat(cleanup).hasValue(0)), - cleanup::set, - true) - .subscribe(ts); + cleanupCase.get() + .doFinally(event -> assertThat(cleanupCase.cleanup).hasValue(0)) + .doOnTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(1)) + .subscribe(ts); ts.assertValues(1) .assertComplete() .assertNoError(); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); } - void checkCleanupExecutionTime(boolean eager, boolean fail) { - AtomicInteger cleanup = new AtomicInteger(); + void checkCleanupExecutionTime(CleanupCase cleanupCase, boolean eager, boolean fail) { AtomicBoolean before = new AtomicBoolean(); AssertSubscriber ts = new AssertSubscriber() { @Override public void onError(Throwable t) { super.onError(t); - before.set(cleanup.get() != 0); + before.set(cleanupCase.cleanup.get() != 0); } @Override public void onComplete() { super.onComplete(); - before.set(cleanup.get() != 0); + before.set(cleanupCase.cleanup.get() != 0); } }; - Mono.using(() -> 1, r -> { - if (fail) { - return Mono.error(new RuntimeException("forced failure")); - } - return Mono.just(1); - }, cleanup::set, eager) - .subscribe(ts); + cleanupCase.get().subscribe(ts); if (fail) { ts.assertNoValues() @@ -133,66 +366,110 @@ public void onComplete() { .assertNoError(); } - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); assertThat(before.get()).isEqualTo(eager); } - @Test - public void checkNonEager() { - checkCleanupExecutionTime(false, false); + @ParameterizedTestWithName + @MethodSource("sourcesNonEager") + public void checkNonEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, false, false); } - @Test - public void checkEager() { - checkCleanupExecutionTime(true, false); + @ParameterizedTestWithName + @MethodSource("sourcesEager") + public void checkEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, true, false); } - @Test - public void checkErrorNonEager() { - checkCleanupExecutionTime(false, true); + @ParameterizedTestWithName + @MethodSource("sourcesFailNonEager") + public void checkErrorNonEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, false, true); } - @Test - public void checkErrorEager() { - checkCleanupExecutionTime(true, true); + @ParameterizedTestWithName + @MethodSource("sourcesFailEager") + public void checkErrorEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, true, true); } - @Test - public void resourceThrowsEager() { + @ParameterizedTestWithName + @MethodSource("resourcesThrow") + public void resourceThrowsEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Mono.using(() -> { - throw new RuntimeException("forced failure"); - }, r -> Mono.just(1), cleanup::set, false) - .subscribe(ts); + cleanupCase.get().subscribe(ts); ts.assertNoValues() .assertNotComplete() .assertError(RuntimeException.class) .assertErrorMessage("forced failure"); - assertThat(cleanup).hasValue(0); + assertThat(cleanupCase.cleanup).hasValue(0); } - @Test - public void factoryThrowsEager() { + @ParameterizedTestWithName + @MethodSource("sourcesThrowNonEager") + public void factoryThrowsNonEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); + cleanupCase.get().doAfterTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(0)).subscribe(ts); - Mono.using(() -> 1, r -> { - throw new RuntimeException("forced failure"); - }, cleanup::set, false) - .subscribe(ts); + ts.assertNoValues() + .assertNotComplete() + .assertError(RuntimeException.class) + .assertErrorMessage("forced failure"); + + assertThat(cleanupCase.cleanup).hasValue(1); + } + + @ParameterizedTestWithName + @MethodSource("sourcesThrowEager") + public void factoryThrowsEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get() + .doFinally(event -> assertThat(cleanupCase.cleanup).hasValue(0)) + .doOnTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(1)) + .subscribe(ts); ts.assertNoValues() .assertNotComplete() .assertError(RuntimeException.class) .assertErrorMessage("forced failure"); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); + } + + @ParameterizedTestWithName + @MethodSource("resourcesCleanupThrowNonEager") + public void resourcesCleanupThrowNonEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get().subscribe(ts); + + ts.assertValues(1) + .assertComplete() + .assertNoError(); + + assertThat(cleanupCase.cleanup).hasValue(0); + } + + @ParameterizedTestWithName + @MethodSource("resourcesCleanupThrowEager") + public void resourcesCleanupThrowEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get().subscribe(ts); + + ts.assertNoValues() + .assertErrorWith(e -> { + assertThat(e).hasMessage("resourceCleanup"); + assertThat(e).isExactlyInstanceOf(IllegalStateException.class); + }); + + assertThat(cleanupCase.cleanup).hasValue(0); } @Test @@ -383,4 +660,20 @@ public void scanSubscriber() { test.cancel(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } + + static abstract class CleanupCase implements Supplier> { + + final AtomicInteger cleanup = new AtomicInteger(); + final String name; + + CleanupCase(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + }