+ * 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 extends D> resourceSupplier,
+ Function super D, ? extends Publisher extends T>> 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.
+ *
+ *
+ *
Eager {@link AutoCloseable} resource cleanup happens just before the source termination and exceptions raised
+ * by the cleanup Consumer may override the terminal event.
+ * 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 extends D> resourceSupplier,
+ Function super D, ? extends Mono extends T>> 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.
+ *
+ *
+ *
For eager cleanup, 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.
+ *
Non-eager cleanup will drop any exception.
+ *
+ *
+ *
+ *
+ * @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 extends D> resourceSupplier,
+ Function super D, ? extends Mono extends T>> 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