From 9d435b726d897e0b12ad242d2e97d5de028afcba Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 21 Feb 2023 10:15:37 +0200 Subject: [PATCH] Add a way for RESTEasy Reactive to close arbitrary Closeable services Closes: #31213 --- .../ResteasyReactiveCDIProcessor.java | 3 +- .../reactive/server/test/CloserTest.java | 212 ++++++++++++++++++ .../resteasy/reactive/server/Closer.java | 17 ++ .../reactive/server/runtime/CloserImpl.java | 31 +++ .../runtime/QuarkusContextProducers.java | 12 + 5 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/CloserTest.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/Closer.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/CloserImpl.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveCDIProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveCDIProcessor.java index c6a3ab0ec1d87..7fbff58e48950 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveCDIProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveCDIProcessor.java @@ -35,7 +35,8 @@ public class ResteasyReactiveCDIProcessor { AutoInjectAnnotationBuildItem contextInjection( BuildProducer additionalBeanBuildItemBuildProducer) { additionalBeanBuildItemBuildProducer - .produce(AdditionalBeanBuildItem.builder().addBeanClasses(ContextProducers.class, QuarkusContextProducers.class) + .produce(AdditionalBeanBuildItem.builder() + .addBeanClasses(ContextProducers.class, QuarkusContextProducers.class) .build()); return new AutoInjectAnnotationBuildItem(ResteasyReactiveServerDotNames.CONTEXT, DotName.createSimple(BeanParam.class.getName())); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/CloserTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/CloserTest.java new file mode 100644 index 0000000000000..e37433f36d65e --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/CloserTest.java @@ -0,0 +1,212 @@ +package io.quarkus.resteasy.reactive.server.test; + +import static io.restassured.RestAssured.get; +import static org.hamcrest.Matchers.equalTo; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.resteasy.reactive.server.Closer; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Uni; + +public class CloserTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(PerRequestResource.class, SingletonResource.class, CounterResource.class, + Counter.class); + } + }); + + @Test + public void test() { + get("/counter/singleton") + .then() + .body(equalTo("0")); + get("/counter/uni-singleton") + .then() + .body(equalTo("0")); + get("/counter/per-request") + .then() + .body(equalTo("0")); + + get("/singleton") + .then() + .statusCode(200) + .body(equalTo("0")); + get("/singleton") + .then() + .statusCode(200) + .body(equalTo("1")); + get("/counter/singleton") + .then() + .body(equalTo("2")); + get("/counter/uni-singleton") + .then() + .body(equalTo("0")); + get("/counter/per-request") + .then() + .body(equalTo("0")); + + get("/uni-singleton") + .then() + .statusCode(200) + .body(equalTo("0")); + get("/uni-singleton") + .then() + .statusCode(200) + .body(equalTo("1")); + get("/counter/singleton") + .then() + .body(equalTo("2")); + get("/counter/uni-singleton") + .then() + .body(equalTo("2")); + get("/counter/per-request") + .then() + .body(equalTo("0")); + + get("/per-request") + .then() + .statusCode(200) + .body(equalTo("0")); + get("/per-request") + .then() + .statusCode(200) + .body(equalTo("1")); + get("/counter/singleton") + .then() + .body(equalTo("2")); + get("/counter/uni-singleton") + .then() + .body(equalTo("2")); + get("/counter/per-request") + .then() + .body(equalTo("2")); + } + + @Path("per-request") + @RequestScoped + public static class PerRequestResource implements Closeable { + private final Closer closer; + private final Counter counter; + + public PerRequestResource(Closer closer, Counter counter) { + this.closer = closer; + this.counter = counter; + } + + @GET + public int get() { + closer.add(this); + return counter.perRequest.get(); + } + + public void close() throws IOException { + counter.perRequest.incrementAndGet(); + } + } + + @Path("singleton") + public static class SingletonResource implements Closeable { + + private final Counter counter; + + public SingletonResource(Counter counter) { + this.counter = counter; + } + + @GET + public int get(@Context Closer closer) { + closer.add(this); + return counter.singleton.get(); + } + + @Override + public void close() { + counter.singleton.incrementAndGet(); + } + } + + @Path("uni-singleton") + public static class UniSingletonResource implements Closeable { + + @Inject + Counter counter; + + @Inject + Closer closer; + + public UniSingletonResource(Counter counter) { + this.counter = counter; + } + + @GET + public Uni get() { + return Uni.createFrom().completionStage(() -> CompletableFuture.completedStage(null)) + .invoke(() -> closer.add(UniSingletonResource.this)) + .map(v -> counter.uniSingleton.get()); + } + + @Override + public void close() { + counter.uniSingleton.incrementAndGet(); + } + } + + @Path("counter") + public static class CounterResource { + + private final Counter counter; + + public CounterResource(Counter counter) { + this.counter = counter; + } + + @Path("singleton") + @GET + public int singletonCount() { + return counter.singleton.get(); + } + + @Path("uni-singleton") + @GET + public int uniSingleton() { + return counter.uniSingleton.get(); + } + + @Path("per-request") + @GET + public int perRequestCount() { + return counter.perRequest.get(); + } + } + + @Singleton + public static class Counter { + public final AtomicInteger perRequest = new AtomicInteger(0); + public final AtomicInteger singleton = new AtomicInteger(0); + public final AtomicInteger uniSingleton = new AtomicInteger(0); + + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/Closer.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/Closer.java new file mode 100644 index 0000000000000..791493cdf00f5 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/Closer.java @@ -0,0 +1,17 @@ +package io.quarkus.resteasy.reactive.server; + +import java.io.Closeable; + +/** + * A service that allows users to close any {@link Closeable} that + * when the request completes. + *

+ * Meant to be used a Resource Method parameter using {@link jakarta.ws.rs.core.Context} + */ +public interface Closer { + + /** + * Register a new {@link Closeable} that is to be closed when the request completes. + */ + void add(Closeable c); +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/CloserImpl.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/CloserImpl.java new file mode 100644 index 0000000000000..33aff0ebee2dc --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/CloserImpl.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.reactive.server.runtime; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; + +import org.jboss.logging.Logger; + +import io.quarkus.resteasy.reactive.server.Closer; + +public class CloserImpl implements Closer { + + private static final Logger log = Logger.getLogger(CloserImpl.class); + + private final List closables = new ArrayList<>(); + + @Override + public void add(Closeable c) { + closables.add(c); + } + + void close() { + for (Closeable closable : closables) { + try { + closable.close(); + } catch (Exception e) { + log.warn("Unable to perform close operation", e); + } + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusContextProducers.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusContextProducers.java index f23cd1e048b4b..be5c7df207c22 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusContextProducers.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusContextProducers.java @@ -2,6 +2,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Disposes; import jakarta.enterprise.inject.Produces; import jakarta.inject.Singleton; import jakarta.ws.rs.ext.Providers; @@ -30,4 +31,15 @@ HttpServerResponse httpServerResponse() { Providers providers() { return new ProvidersImpl(ResteasyReactiveRecorder.getCurrentDeployment()); } + + @RequestScoped + @Produces + CloserImpl closer() { + return new CloserImpl(); + } + + void closeCloser(@Disposes CloserImpl closer) { + closer.close(); + } + }