diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index 92583127f3f65..98c7fbed5f234 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -702,6 +702,58 @@ For example, you could expire idle connections after 60 minutes: quarkus.datasource.reactive.idle-timeout=PT60M ---- +== Customizing pool creation + +Sometimes, the database connection pool cannot be configured only by declaration. + +You may need to read a specific file only present in production, or retrieve configuration data from a proprietary configuration server. + +In this case, you can customize pool creation by creating a class implementing an interface which depends on the target database: + +[cols="30,70"] +|=== +|Database |Pool creator class name + +|IBM Db2 +|`io.quarkus.reactive.db2.client.DB2PoolCreator` + +|MariaDB/MySQL +|`io.quarkus.reactive.mysql.client.MySQLPoolCreator` + +|Microsoft SQL Server +|`io.quarkus.reactive.mssql.client.MSSQLPoolCreator` + +|Oracle +|`io.quarkus.reactive.oracle.client.OraclePoolCreator` + +|PostgreSQL +|`io.quarkus.reactive.pg.client.PgPoolCreator` +|=== + +Here's an example for PostgreSQL: + +[source,java] +---- +import javax.inject.Singleton; + +import io.quarkus.reactive.pg.client.PgPoolCreator; +import io.vertx.pgclient.PgConnectOptions; +import io.vertx.pgclient.PgPool; +import io.vertx.sqlclient.PoolOptions; + +@Singleton +public class CustomPgPoolCreator implements PgPoolCreator { + + @Override + public PgPool create(Input input) { + PgConnectOptions connectOptions = input.pgConnectOptions(); + PoolOptions poolOptions = input.poolOptions(); + // Customize connectOptions, poolOptions or both, as required + return PgPool.pool(input.vertx(), connectOptions, poolOptions); + } +} +---- + == Configuration Reference === Common Datasource diff --git a/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java b/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java index ca2b618b18433..10e5ddb2cf151 100644 --- a/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java +++ b/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java @@ -1,12 +1,26 @@ package io.quarkus.reactive.db2.client.deployment; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; + import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.arc.deployment.devconsole.Name; +import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.DotNames; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.common.runtime.DatabaseKind; @@ -33,6 +47,7 @@ import io.quarkus.reactive.datasource.runtime.DataSourceReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.db2.client.DB2PoolCreator; import io.quarkus.reactive.db2.client.runtime.DB2PoolRecorder; import io.quarkus.reactive.db2.client.runtime.DB2ServiceBindingConverter; import io.quarkus.reactive.db2.client.runtime.DataSourcesReactiveDB2Config; @@ -88,6 +103,34 @@ DevServicesDatasourceConfigurationHandlerBuildItem devDbHandler() { return DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.DB2); } + @BuildStep + void unremoveableBeans(BuildProducer producer) { + producer.produce(UnremovableBeanBuildItem.beanTypes(DB2PoolCreator.class)); + } + + @BuildStep + void validateBeans(ValidationPhaseBuildItem validationPhase, + BuildProducer errors) { + // no two Db2PoolCreator beans can be associated with the same datasource + Map seen = new HashMap<>(); + for (BeanInfo beanInfo : validationPhase.getContext().beans() + .matchBeanTypes(new DB2PoolCreatorBeanClassPredicate())) { + Set qualifiers = new TreeSet<>(); // use a TreeSet in order to get a predictable iteration order + for (AnnotationInstance qualifier : beanInfo.getQualifiers()) { + qualifiers.add(Name.from(qualifier)); + } + String qualifiersStr = qualifiers.stream().map(Name::toString).collect(Collectors.joining("_")); + if (seen.getOrDefault(qualifiersStr, false)) { + errors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem( + new IllegalStateException( + "There can be at most one bean of type '" + DB2PoolCreator.class.getName() + + "' for each datasource."))); + } else { + seen.put(qualifiersStr, true); + } + } + } + @BuildStep void registerServiceBinding(Capabilities capabilities, BuildProducer serviceProvider, BuildProducer dbKind) { @@ -236,4 +279,14 @@ private static void addQualifiers(ExtendedBeanConfigurator configurator, String .done(); } } + + private static class DB2PoolCreatorBeanClassPredicate implements Predicate> { + private static final Type DB2_POOL_CREATOR = Type.create(DotName.createSimple(DB2PoolCreator.class.getName()), + Type.Kind.CLASS); + + @Override + public boolean test(Set types) { + return types.contains(DB2_POOL_CREATOR); + } + } } diff --git a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/DB2PoolCreator.java b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/DB2PoolCreator.java new file mode 100644 index 0000000000000..3538d678e8670 --- /dev/null +++ b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/DB2PoolCreator.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.db2.client; + +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.vertx.core.Vertx; +import io.vertx.db2client.DB2ConnectOptions; +import io.vertx.db2client.DB2Pool; +import io.vertx.sqlclient.PoolOptions; + +/** + * This interface is an integration point that allows users to use the {@link Vertx}, {@link PoolOptions} and + * {@link DB2ConnectOptions} objects configured automatically by Quarkus, in addition to a custom strategy + * for creating the final {@link DB2Pool}. + * + * Implementations of this class are meant to be used as CDI beans. + * If a bean of this type is used without a {@link ReactiveDataSource} qualifier, then it's applied to the default datasource, + * otherwise it applies to the datasource matching the name of the annotation. + */ +public interface DB2PoolCreator { + + DB2Pool create(Input input); + + interface Input { + + Vertx vertx(); + + PoolOptions poolOptions(); + + DB2ConnectOptions db2ConnectOptions(); + } +} diff --git a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java index 420a286b2dc7f..ff9a794e19ef9 100644 --- a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java +++ b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java @@ -13,14 +13,20 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import javax.enterprise.inject.Instance; + import org.jboss.logging.Logger; +import io.quarkus.arc.Arc; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; +import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; +import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.DataSourceReactiveRuntimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.db2.client.DB2PoolCreator; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; @@ -78,7 +84,7 @@ private DB2Pool initialize(Vertx vertx, log.warn( "Configuration element 'thread-local' on Reactive datasource connections is deprecated and will be ignored. The started pool will always be based on a per-thread separate pool now."); } - return DB2Pool.pool(vertx, connectOptions, poolOptions); + return createPool(vertx, poolOptions, connectOptions, dataSourceName); } private PoolOptions toPoolOptions(Integer eventLoopCount, @@ -186,4 +192,47 @@ private DB2ConnectOptions toConnectOptions(DataSourceRuntimeConfig dataSourceRun return connectOptions; } + + private DB2Pool createPool(Vertx vertx, PoolOptions poolOptions, DB2ConnectOptions dB2ConnectOptions, + String dataSourceName) { + Instance instance; + if (DataSourceUtil.isDefault(dataSourceName)) { + instance = Arc.container().select(DB2PoolCreator.class); + } else { + instance = Arc.container().select(DB2PoolCreator.class, + new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); + } + if (instance.isResolvable()) { + DB2PoolCreator.Input input = new DefaultInput(vertx, poolOptions, dB2ConnectOptions); + return instance.get().create(input); + } + return DB2Pool.pool(vertx, dB2ConnectOptions, poolOptions); + } + + private static class DefaultInput implements DB2PoolCreator.Input { + private final Vertx vertx; + private final PoolOptions poolOptions; + private final DB2ConnectOptions dB2ConnectOptions; + + public DefaultInput(Vertx vertx, PoolOptions poolOptions, DB2ConnectOptions dB2ConnectOptions) { + this.vertx = vertx; + this.poolOptions = poolOptions; + this.dB2ConnectOptions = dB2ConnectOptions; + } + + @Override + public Vertx vertx() { + return vertx; + } + + @Override + public PoolOptions poolOptions() { + return poolOptions; + } + + @Override + public DB2ConnectOptions db2ConnectOptions() { + return dB2ConnectOptions; + } + } } diff --git a/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java b/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java index 3482082072658..15d22dc1a515d 100644 --- a/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java +++ b/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java @@ -1,12 +1,26 @@ package io.quarkus.reactive.mssql.client.deployment; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; + import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.arc.deployment.devconsole.Name; +import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.DotNames; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.common.runtime.DatabaseKind; @@ -33,6 +47,7 @@ import io.quarkus.reactive.datasource.runtime.DataSourceReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.mssql.client.MSSQLPoolCreator; import io.quarkus.reactive.mssql.client.runtime.DataSourcesReactiveMSSQLConfig; import io.quarkus.reactive.mssql.client.runtime.MSSQLPoolRecorder; import io.quarkus.reactive.mssql.client.runtime.MsSQLServiceBindingConverter; @@ -88,6 +103,34 @@ DevServicesDatasourceConfigurationHandlerBuildItem devDbHandler() { return DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.MSSQL); } + @BuildStep + void unremoveableBeans(BuildProducer producer) { + producer.produce(UnremovableBeanBuildItem.beanTypes(MSSQLPoolCreator.class)); + } + + @BuildStep + void validateBeans(ValidationPhaseBuildItem validationPhase, + BuildProducer errors) { + // no two MssqlPoolCreator beans can be associated with the same datasource + Map seen = new HashMap<>(); + for (BeanInfo beanInfo : validationPhase.getContext().beans() + .matchBeanTypes(new MSSQLPoolCreatorBeanClassPredicate())) { + Set qualifiers = new TreeSet<>(); // use a TreeSet in order to get a predictable iteration order + for (AnnotationInstance qualifier : beanInfo.getQualifiers()) { + qualifiers.add(Name.from(qualifier)); + } + String qualifiersStr = qualifiers.stream().map(Name::toString).collect(Collectors.joining("_")); + if (seen.getOrDefault(qualifiersStr, false)) { + errors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem( + new IllegalStateException( + "There can be at most one bean of type '" + MSSQLPoolCreator.class.getName() + + "' for each datasource."))); + } else { + seen.put(qualifiersStr, true); + } + } + } + @BuildStep void registerServiceBinding(Capabilities capabilities, BuildProducer serviceProvider, BuildProducer dbKind) { @@ -235,4 +278,14 @@ private static void addQualifiers(ExtendedBeanConfigurator configurator, String .done(); } } + + private static class MSSQLPoolCreatorBeanClassPredicate implements Predicate> { + private static final Type MSSQL_POOL_CREATOR = Type.create(DotName.createSimple(MSSQLPoolCreator.class.getName()), + Type.Kind.CLASS); + + @Override + public boolean test(Set types) { + return types.contains(MSSQL_POOL_CREATOR); + } + } } diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/LocalhostMSSQLPoolCreator.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/LocalhostMSSQLPoolCreator.java new file mode 100644 index 0000000000000..b4a045f6255de --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/LocalhostMSSQLPoolCreator.java @@ -0,0 +1,15 @@ +package io.quarkus.reactive.mssql.client; + +import javax.inject.Singleton; + +import io.vertx.mssqlclient.MSSQLPool; + +@Singleton +public class LocalhostMSSQLPoolCreator implements MSSQLPoolCreator { + + @Override + public MSSQLPool create(Input input) { + return MSSQLPool.pool(input.vertx(), input.msSQLConnectOptions().setHost("localhost").setPort(1435), + input.poolOptions()); + } +} diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/MSSQLPoolCreatorTest.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/MSSQLPoolCreatorTest.java new file mode 100644 index 0000000000000..c80375688833b --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/MSSQLPoolCreatorTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.mssql.client; + +import static io.restassured.RestAssured.given; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class MSSQLPoolCreatorTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(CustomCredentialsProvider.class) + .addClass(CredentialsTestResource.class) + .addClass(LocalhostMSSQLPoolCreator.class) + .addAsResource("application-credentials-with-erroneous-url.properties", "application.properties")); + + @Test + public void testConnect() { + given() + .when().get("/test") + .then() + .statusCode(200) + .body(CoreMatchers.equalTo("OK")); + } + +} diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/MultipleDataSourcesAndMSSQLPoolCreatorsTest.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/MultipleDataSourcesAndMSSQLPoolCreatorsTest.java new file mode 100644 index 0000000000000..f89109ab4c95f --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/MultipleDataSourcesAndMSSQLPoolCreatorsTest.java @@ -0,0 +1,105 @@ +package io.quarkus.reactive.mssql.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mssqlclient.MSSQLPool; + +public class MultipleDataSourcesAndMSSQLPoolCreatorsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-multiple-datasources-with-erroneous-url.properties") + .withApplicationRoot((jar) -> jar + .addClasses(BeanUsingDefaultDataSource.class) + .addClass(BeanUsingHibernateDataSource.class) + .addClass(DefaultMSSQLPoolCreator.class) + .addClass(HibernateMSSQLPoolCreator.class)); + + @Inject + BeanUsingDefaultDataSource beanUsingDefaultDataSource; + + @Inject + BeanUsingHibernateDataSource beanUsingHibernateDataSource; + + @Test + public void testMultipleDataSources() { + beanUsingDefaultDataSource.verify() + .thenCompose(v -> beanUsingHibernateDataSource.verify()) + .toCompletableFuture() + .join(); + } + + @ApplicationScoped + static class BeanUsingDefaultDataSource { + + @Inject + MSSQLPool mSSQLClient; + + public CompletionStage verify() { + CompletableFuture cf = new CompletableFuture<>(); + mSSQLClient.query("SELECT 1").execute(ar -> { + if (ar.failed()) { + cf.completeExceptionally(ar.cause()); + } else { + cf.complete(null); + } + }); + return cf; + } + } + + @ApplicationScoped + static class BeanUsingHibernateDataSource { + + @Inject + @ReactiveDataSource("hibernate") + MSSQLPool mSSQLClient; + + public CompletionStage verify() { + CompletableFuture cf = new CompletableFuture<>(); + mSSQLClient.query("SELECT 1").execute(ar -> { + if (ar.failed()) { + cf.completeExceptionally(ar.cause()); + } else { + cf.complete(null); + } + }); + return cf; + } + } + + @Singleton + public static class DefaultMSSQLPoolCreator implements MSSQLPoolCreator { + + @Override + public MSSQLPool create(Input input) { + assertEquals(12345, input.msSQLConnectOptions().getPort()); // validate that the bean has been called for the proper datasource + return MSSQLPool.pool(input.vertx(), input.msSQLConnectOptions().setHost("localhost").setPort(1435), + input.poolOptions()); + } + } + + @Singleton + @ReactiveDataSource("hibernate") + public static class HibernateMSSQLPoolCreator implements MSSQLPoolCreator { + + @Override + public MSSQLPool create(Input input) { + assertEquals(55555, input.msSQLConnectOptions().getPort()); // validate that the bean has been called for the proper datasource + return MSSQLPool.pool(input.vertx(), input.msSQLConnectOptions().setHost("localhost").setPort(1435), + input.poolOptions()); + } + } +} diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/MultipleMSSQLPoolCreatorsForSameDatasourceTest.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/MultipleMSSQLPoolCreatorsForSameDatasourceTest.java new file mode 100644 index 0000000000000..1fb1e478b4f99 --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/MultipleMSSQLPoolCreatorsForSameDatasourceTest.java @@ -0,0 +1,40 @@ +package io.quarkus.reactive.mssql.client; + +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DeploymentException; +import javax.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mssqlclient.MSSQLPool; + +public class MultipleMSSQLPoolCreatorsForSameDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(CustomCredentialsProvider.class) + .addClass(CredentialsTestResource.class) + .addClass(LocalhostMSSQLPoolCreator.class) + .addClass(AnotherMSSQLPoolCreator.class) + .addAsResource("application-credentials-with-erroneous-url.properties", "application.properties")) + .setExpectedException(DeploymentException.class); + + @Test + public void test() { + fail("Should never have been called"); + } + + @Singleton + public static class AnotherMSSQLPoolCreator implements MSSQLPoolCreator { + + @Override + public MSSQLPool create(Input input) { + return MSSQLPool.pool(input.vertx(), input.msSQLConnectOptions(), input.poolOptions()); + } + } + +} diff --git a/extensions/reactive-mssql-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties b/extensions/reactive-mssql-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties new file mode 100644 index 0000000000000..60e6e5bcaabdc --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties @@ -0,0 +1,3 @@ +quarkus.datasource.db-kind=mssql +quarkus.datasource.credentials-provider=custom +quarkus.datasource.reactive.url=vertx-reactive:sqlserver://test:12345 diff --git a/extensions/reactive-mssql-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties b/extensions/reactive-mssql-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties new file mode 100644 index 0000000000000..106938c672363 --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties @@ -0,0 +1,9 @@ +quarkus.datasource.db-kind=mssql +quarkus.datasource.username=sa +quarkus.datasource.password=A_Str0ng_Required_Password +quarkus.datasource.reactive.url=vertx-reactive:sqlserver://test:12345 + +quarkus.datasource."hibernate".db-kind=mssql +quarkus.datasource."hibernate".username=sa +quarkus.datasource."hibernate".password=A_Str0ng_Required_Password +quarkus.datasource."hibernate".reactive.url=vertx-reactive:sqlserver://test:55555 diff --git a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/MSSQLPoolCreator.java b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/MSSQLPoolCreator.java new file mode 100644 index 0000000000000..dc2da326413e2 --- /dev/null +++ b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/MSSQLPoolCreator.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.mssql.client; + +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.vertx.core.Vertx; +import io.vertx.mssqlclient.MSSQLConnectOptions; +import io.vertx.mssqlclient.MSSQLPool; +import io.vertx.sqlclient.PoolOptions; + +/** + * This interface is an integration point that allows users to use the {@link Vertx}, {@link PoolOptions} and + * {@link MSSQLConnectOptions} objects configured automatically by Quarkus, in addition to a custom strategy + * for creating the final {@link MSSQLPool}. + * + * Implementations of this class are meant to be used as CDI beans. + * If a bean of this type is used without a {@link ReactiveDataSource} qualifier, then it's applied to the default datasource, + * otherwise it applies to the datasource matching the name of the annotation. + */ +public interface MSSQLPoolCreator { + + MSSQLPool create(Input input); + + interface Input { + + Vertx vertx(); + + PoolOptions poolOptions(); + + MSSQLConnectOptions msSQLConnectOptions(); + } +} diff --git a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java index 4828688ed77c9..7c14613815975 100644 --- a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java +++ b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java @@ -13,14 +13,20 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import javax.enterprise.inject.Instance; + import org.jboss.logging.Logger; +import io.quarkus.arc.Arc; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; +import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; +import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.DataSourceReactiveRuntimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.mssql.client.MSSQLPoolCreator; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; @@ -78,7 +84,7 @@ private MSSQLPool initialize(Vertx vertx, log.warn( "Configuration element 'thread-local' on Reactive datasource connections is deprecated and will be ignored. The started pool will always be based on a per-thread separate pool now."); } - return MSSQLPool.pool(vertx, mssqlConnectOptions, poolOptions); + return createPool(vertx, poolOptions, mssqlConnectOptions, dataSourceName); } private PoolOptions toPoolOptions(Integer eventLoopCount, @@ -182,4 +188,46 @@ private MSSQLConnectOptions toMSSQLConnectOptions(DataSourceRuntimeConfig dataSo return mssqlConnectOptions; } + private MSSQLPool createPool(Vertx vertx, PoolOptions poolOptions, MSSQLConnectOptions mSSQLConnectOptions, + String dataSourceName) { + Instance instance; + if (DataSourceUtil.isDefault(dataSourceName)) { + instance = Arc.container().select(MSSQLPoolCreator.class); + } else { + instance = Arc.container().select(MSSQLPoolCreator.class, + new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); + } + if (instance.isResolvable()) { + MSSQLPoolCreator.Input input = new DefaultInput(vertx, poolOptions, mSSQLConnectOptions); + return instance.get().create(input); + } + return MSSQLPool.pool(vertx, mSSQLConnectOptions, poolOptions); + } + + private static class DefaultInput implements MSSQLPoolCreator.Input { + private final Vertx vertx; + private final PoolOptions poolOptions; + private final MSSQLConnectOptions mSSQLConnectOptions; + + public DefaultInput(Vertx vertx, PoolOptions poolOptions, MSSQLConnectOptions mSSQLConnectOptions) { + this.vertx = vertx; + this.poolOptions = poolOptions; + this.mSSQLConnectOptions = mSSQLConnectOptions; + } + + @Override + public Vertx vertx() { + return vertx; + } + + @Override + public PoolOptions poolOptions() { + return poolOptions; + } + + @Override + public MSSQLConnectOptions msSQLConnectOptions() { + return mSSQLConnectOptions; + } + } } diff --git a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java index 2354842e7865c..42266feac61df 100644 --- a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java +++ b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java @@ -1,12 +1,26 @@ package io.quarkus.reactive.mysql.client.deployment; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; + import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.arc.deployment.devconsole.Name; +import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.DotNames; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.common.runtime.DatabaseKind; @@ -33,6 +47,7 @@ import io.quarkus.reactive.datasource.runtime.DataSourceReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.mysql.client.MySQLPoolCreator; import io.quarkus.reactive.mysql.client.runtime.DataSourcesReactiveMySQLConfig; import io.quarkus.reactive.mysql.client.runtime.MySQLPoolRecorder; import io.quarkus.reactive.mysql.client.runtime.MySQLServiceBindingConverter; @@ -89,6 +104,34 @@ List devDbHandler() { DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.MARIADB)); } + @BuildStep + void unremoveableBeans(BuildProducer producer) { + producer.produce(UnremovableBeanBuildItem.beanTypes(MySQLPoolCreator.class)); + } + + @BuildStep + void validateBeans(ValidationPhaseBuildItem validationPhase, + BuildProducer errors) { + // no two MySQLPoolCreator beans can be associated with the same datasource + Map seen = new HashMap<>(); + for (BeanInfo beanInfo : validationPhase.getContext().beans() + .matchBeanTypes(new MySQLPoolCreatorBeanClassPredicate())) { + Set qualifiers = new TreeSet<>(); // use a TreeSet in order to get a predictable iteration order + for (AnnotationInstance qualifier : beanInfo.getQualifiers()) { + qualifiers.add(Name.from(qualifier)); + } + String qualifiersStr = qualifiers.stream().map(Name::toString).collect(Collectors.joining("_")); + if (seen.getOrDefault(qualifiersStr, false)) { + errors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem( + new IllegalStateException( + "There can be at most one bean of type '" + MySQLPoolCreator.class.getName() + + "' for each datasource."))); + } else { + seen.put(qualifiersStr, true); + } + } + } + @BuildStep void registerServiceBinding(Capabilities capabilities, BuildProducer serviceProvider, BuildProducer dbKind) { @@ -237,4 +280,14 @@ private static void addQualifiers(ExtendedBeanConfigurator configurator, String .done(); } } + + private static class MySQLPoolCreatorBeanClassPredicate implements Predicate> { + private static final Type MYSQL_POOL_CREATOR = Type.create(DotName.createSimple(MySQLPoolCreator.class.getName()), + Type.Kind.CLASS); + + @Override + public boolean test(Set types) { + return types.contains(MYSQL_POOL_CREATOR); + } + } } diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/LocalhostMySQLPoolCreator.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/LocalhostMySQLPoolCreator.java new file mode 100644 index 0000000000000..ec8c7a798d0a2 --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/LocalhostMySQLPoolCreator.java @@ -0,0 +1,15 @@ +package io.quarkus.reactive.mysql.client; + +import javax.inject.Singleton; + +import io.vertx.mysqlclient.MySQLPool; + +@Singleton +public class LocalhostMySQLPoolCreator implements MySQLPoolCreator { + + @Override + public MySQLPool create(Input input) { + return MySQLPool.pool(input.vertx(), input.mySQLConnectOptions().setHost("localhost").setPort(3308), + input.poolOptions()); + } +} diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/MultipleDataSourcesAndMySQLPoolCreatorsTest.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/MultipleDataSourcesAndMySQLPoolCreatorsTest.java new file mode 100644 index 0000000000000..e0b5a88258f4f --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/MultipleDataSourcesAndMySQLPoolCreatorsTest.java @@ -0,0 +1,105 @@ +package io.quarkus.reactive.mysql.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mysqlclient.MySQLPool; + +public class MultipleDataSourcesAndMySQLPoolCreatorsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-multiple-datasources-with-erroneous-url.properties") + .withApplicationRoot((jar) -> jar + .addClasses(BeanUsingDefaultDataSource.class) + .addClass(BeanUsingHibernateDataSource.class) + .addClass(DefaultMySQLPoolCreator.class) + .addClass(HibernateMySQLPoolCreator.class)); + + @Inject + BeanUsingDefaultDataSource beanUsingDefaultDataSource; + + @Inject + BeanUsingHibernateDataSource beanUsingHibernateDataSource; + + @Test + public void testMultipleDataSources() { + beanUsingDefaultDataSource.verify() + .thenCompose(v -> beanUsingHibernateDataSource.verify()) + .toCompletableFuture() + .join(); + } + + @ApplicationScoped + static class BeanUsingDefaultDataSource { + + @Inject + MySQLPool mySQLClient; + + public CompletionStage verify() { + CompletableFuture cf = new CompletableFuture<>(); + mySQLClient.query("SELECT 1").execute(ar -> { + if (ar.failed()) { + cf.completeExceptionally(ar.cause()); + } else { + cf.complete(null); + } + }); + return cf; + } + } + + @ApplicationScoped + static class BeanUsingHibernateDataSource { + + @Inject + @ReactiveDataSource("hibernate") + MySQLPool mySQLClient; + + public CompletionStage verify() { + CompletableFuture cf = new CompletableFuture<>(); + mySQLClient.query("SELECT 1").execute(ar -> { + if (ar.failed()) { + cf.completeExceptionally(ar.cause()); + } else { + cf.complete(null); + } + }); + return cf; + } + } + + @Singleton + public static class DefaultMySQLPoolCreator implements MySQLPoolCreator { + + @Override + public MySQLPool create(Input input) { + assertEquals(12345, input.mySQLConnectOptions().getPort()); // validate that the bean has been called for the proper datasource + return MySQLPool.pool(input.vertx(), input.mySQLConnectOptions().setHost("localhost").setPort(3308), + input.poolOptions()); + } + } + + @Singleton + @ReactiveDataSource("hibernate") + public static class HibernateMySQLPoolCreator implements MySQLPoolCreator { + + @Override + public MySQLPool create(Input input) { + assertEquals(55555, input.mySQLConnectOptions().getPort()); // validate that the bean has been called for the proper datasource + return MySQLPool.pool(input.vertx(), input.mySQLConnectOptions().setHost("localhost").setPort(3308), + input.poolOptions()); + } + } +} diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/MultipleMySQLPoolCreatorsForSameDatasourceTest.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/MultipleMySQLPoolCreatorsForSameDatasourceTest.java new file mode 100644 index 0000000000000..6fd00cf2d7748 --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/MultipleMySQLPoolCreatorsForSameDatasourceTest.java @@ -0,0 +1,40 @@ +package io.quarkus.reactive.mysql.client; + +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DeploymentException; +import javax.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mysqlclient.MySQLPool; + +public class MultipleMySQLPoolCreatorsForSameDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(CustomCredentialsProvider.class) + .addClass(CredentialsTestResource.class) + .addClass(LocalhostMySQLPoolCreator.class) + .addClass(AnotherMySQLPoolCreator.class) + .addAsResource("application-credentials-with-erroneous-url.properties", "application.properties")) + .setExpectedException(DeploymentException.class); + + @Test + public void test() { + fail("Should never have been called"); + } + + @Singleton + public static class AnotherMySQLPoolCreator implements MySQLPoolCreator { + + @Override + public MySQLPool create(Input input) { + return MySQLPool.pool(input.vertx(), input.mySQLConnectOptions(), input.poolOptions()); + } + } + +} diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/MySQLPoolCreatorTest.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/MySQLPoolCreatorTest.java new file mode 100644 index 0000000000000..d3787cfac2901 --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/MySQLPoolCreatorTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.mysql.client; + +import static io.restassured.RestAssured.given; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class MySQLPoolCreatorTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(CustomCredentialsProvider.class) + .addClass(CredentialsTestResource.class) + .addClass(LocalhostMySQLPoolCreator.class) + .addAsResource("application-credentials-with-erroneous-url.properties", "application.properties")); + + @Test + public void testConnect() { + given() + .when().get("/test") + .then() + .statusCode(200) + .body(CoreMatchers.equalTo("OK")); + } + +} diff --git a/extensions/reactive-mysql-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties b/extensions/reactive-mysql-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties new file mode 100644 index 0000000000000..bb13d3858c6f1 --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties @@ -0,0 +1,3 @@ +quarkus.datasource.db-kind=mysql +quarkus.datasource.credentials-provider=custom +quarkus.datasource.reactive.url=vertx-reactive:mysql://test:12345/hibernate_orm_test diff --git a/extensions/reactive-mysql-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties b/extensions/reactive-mysql-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties new file mode 100644 index 0000000000000..194f07190b185 --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties @@ -0,0 +1,9 @@ +quarkus.datasource.db-kind=mysql +quarkus.datasource.username=hibernate_orm_test +quarkus.datasource.password=hibernate_orm_test +quarkus.datasource.reactive.url=vertx-reactive:mysql://test:12345/hibernate_orm_test + +quarkus.datasource."hibernate".db-kind=mysql +quarkus.datasource."hibernate".username=hibernate_orm_test +quarkus.datasource."hibernate".password=hibernate_orm_test +quarkus.datasource."hibernate".reactive.url=vertx-reactive:mysql://test:55555/hibernate_orm_test diff --git a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/MySQLPoolCreator.java b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/MySQLPoolCreator.java new file mode 100644 index 0000000000000..43bfc3db99755 --- /dev/null +++ b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/MySQLPoolCreator.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.mysql.client; + +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.vertx.core.Vertx; +import io.vertx.mysqlclient.MySQLConnectOptions; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.sqlclient.PoolOptions; + +/** + * This interface is an integration point that allows users to use the {@link Vertx}, {@link PoolOptions} and + * {@link MySQLConnectOptions} objects configured automatically by Quarkus, in addition to a custom strategy + * for creating the final {@link MySQLPool}. + * + * Implementations of this class are meant to be used as CDI beans. + * If a bean of this type is used without a {@link ReactiveDataSource} qualifier, then it's applied to the default datasource, + * otherwise it applies to the datasource matching the name of the annotation. + */ +public interface MySQLPoolCreator { + + MySQLPool create(Input input); + + interface Input { + + Vertx vertx(); + + PoolOptions poolOptions(); + + MySQLConnectOptions mySQLConnectOptions(); + } +} diff --git a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java index e9662b066db8d..95eb1aad8645f 100644 --- a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java +++ b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java @@ -13,14 +13,20 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import javax.enterprise.inject.Instance; + import org.jboss.logging.Logger; +import io.quarkus.arc.Arc; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; +import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; +import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.DataSourceReactiveRuntimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.mysql.client.MySQLPoolCreator; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; @@ -79,7 +85,7 @@ private MySQLPool initialize(Vertx vertx, log.warn( "Configuration element 'thread-local' on Reactive datasource connections is deprecated and will be ignored. The started pool will always be based on a per-thread separate pool now."); } - return MySQLPool.pool(vertx, mysqlConnectOptions, poolOptions); + return createPool(vertx, poolOptions, mysqlConnectOptions, dataSourceName); } private PoolOptions toPoolOptions(Integer eventLoopCount, @@ -221,4 +227,46 @@ private MySQLConnectOptions toMySQLConnectOptions(DataSourceRuntimeConfig dataSo return mysqlConnectOptions; } + private MySQLPool createPool(Vertx vertx, PoolOptions poolOptions, MySQLConnectOptions mySQLConnectOptions, + String dataSourceName) { + Instance instance; + if (DataSourceUtil.isDefault(dataSourceName)) { + instance = Arc.container().select(MySQLPoolCreator.class); + } else { + instance = Arc.container().select(MySQLPoolCreator.class, + new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); + } + if (instance.isResolvable()) { + MySQLPoolCreator.Input input = new DefaultInput(vertx, poolOptions, mySQLConnectOptions); + return instance.get().create(input); + } + return MySQLPool.pool(vertx, mySQLConnectOptions, poolOptions); + } + + private static class DefaultInput implements MySQLPoolCreator.Input { + private final Vertx vertx; + private final PoolOptions poolOptions; + private final MySQLConnectOptions mySQLConnectOptions; + + public DefaultInput(Vertx vertx, PoolOptions poolOptions, MySQLConnectOptions mySQLConnectOptions) { + this.vertx = vertx; + this.poolOptions = poolOptions; + this.mySQLConnectOptions = mySQLConnectOptions; + } + + @Override + public Vertx vertx() { + return vertx; + } + + @Override + public PoolOptions poolOptions() { + return poolOptions; + } + + @Override + public MySQLConnectOptions mySQLConnectOptions() { + return mySQLConnectOptions; + } + } } diff --git a/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java b/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java index cb5afbd310af4..1385081e1d037 100644 --- a/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java +++ b/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java @@ -1,12 +1,26 @@ package io.quarkus.reactive.oracle.client.deployment; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; + import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.arc.deployment.devconsole.Name; +import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.DotNames; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.common.runtime.DatabaseKind; @@ -33,6 +47,7 @@ import io.quarkus.reactive.datasource.runtime.DataSourceReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.oracle.client.OraclePoolCreator; import io.quarkus.reactive.oracle.client.runtime.DataSourcesReactiveOracleConfig; import io.quarkus.reactive.oracle.client.runtime.OraclePoolRecorder; import io.quarkus.reactive.oracle.client.runtime.OracleServiceBindingConverter; @@ -89,6 +104,34 @@ DevServicesDatasourceConfigurationHandlerBuildItem devDbHandler() { return DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.ORACLE); } + @BuildStep + void unremoveableBeans(BuildProducer producer) { + producer.produce(UnremovableBeanBuildItem.beanTypes(OraclePoolCreator.class)); + } + + @BuildStep + void validateBeans(ValidationPhaseBuildItem validationPhase, + BuildProducer errors) { + // no two OraclePoolCreator beans can be associated with the same datasource + Map seen = new HashMap<>(); + for (BeanInfo beanInfo : validationPhase.getContext().beans() + .matchBeanTypes(new OraclePoolCreatorBeanClassPredicate())) { + Set qualifiers = new TreeSet<>(); // use a TreeSet in order to get a predictable iteration order + for (AnnotationInstance qualifier : beanInfo.getQualifiers()) { + qualifiers.add(Name.from(qualifier)); + } + String qualifiersStr = qualifiers.stream().map(Name::toString).collect(Collectors.joining("_")); + if (seen.getOrDefault(qualifiersStr, false)) { + errors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem( + new IllegalStateException( + "There can be at most one bean of type '" + OraclePoolCreator.class.getName() + + "' for each datasource."))); + } else { + seen.put(qualifiersStr, true); + } + } + } + @BuildStep void registerServiceBinding(Capabilities capabilities, BuildProducer serviceProvider, BuildProducer dbKind) { @@ -236,4 +279,14 @@ private static void addQualifiers(ExtendedBeanConfigurator configurator, String .done(); } } + + private static class OraclePoolCreatorBeanClassPredicate implements Predicate> { + private static final Type ORACLE_POOL_CREATOR = Type.create(DotName.createSimple(OraclePoolCreator.class.getName()), + Type.Kind.CLASS); + + @Override + public boolean test(Set types) { + return types.contains(ORACLE_POOL_CREATOR); + } + } } diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/LocalhostOraclePoolCreator.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/LocalhostOraclePoolCreator.java new file mode 100644 index 0000000000000..ab4b483255b6a --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/LocalhostOraclePoolCreator.java @@ -0,0 +1,15 @@ +package io.quarkus.reactive.oracle.client; + +import javax.inject.Singleton; + +import io.vertx.oracleclient.OraclePool; + +@Singleton +public class LocalhostOraclePoolCreator implements OraclePoolCreator { + + @Override + public OraclePool create(Input input) { + return OraclePool.pool(input.vertx(), input.oracleConnectOptions().setHost("localhost").setPort(1521), + input.poolOptions()); + } +} diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/MultipleDataSourcesAndOraclePoolCreatorsTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/MultipleDataSourcesAndOraclePoolCreatorsTest.java new file mode 100644 index 0000000000000..fd0cae86bc907 --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/MultipleDataSourcesAndOraclePoolCreatorsTest.java @@ -0,0 +1,105 @@ +package io.quarkus.reactive.oracle.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.oracleclient.OraclePool; + +public class MultipleDataSourcesAndOraclePoolCreatorsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-multiple-datasources-with-erroneous-url.properties") + .withApplicationRoot((jar) -> jar + .addClasses(BeanUsingDefaultDataSource.class) + .addClass(BeanUsingHibernateDataSource.class) + .addClass(DefaultOraclePoolCreator.class) + .addClass(HibernateOraclePoolCreator.class)); + + @Inject + BeanUsingDefaultDataSource beanUsingDefaultDataSource; + + @Inject + BeanUsingHibernateDataSource beanUsingHibernateDataSource; + + @Test + public void testMultipleDataSources() { + beanUsingDefaultDataSource.verify() + .thenCompose(v -> beanUsingHibernateDataSource.verify()) + .toCompletableFuture() + .join(); + } + + @ApplicationScoped + static class BeanUsingDefaultDataSource { + + @Inject + OraclePool oracleClient; + + public CompletionStage verify() { + CompletableFuture cf = new CompletableFuture<>(); + oracleClient.query("SELECT 1 FROM DUAL").execute(ar -> { + if (ar.failed()) { + cf.completeExceptionally(ar.cause()); + } else { + cf.complete(null); + } + }); + return cf; + } + } + + @ApplicationScoped + static class BeanUsingHibernateDataSource { + + @Inject + @ReactiveDataSource("hibernate") + OraclePool oracleClient; + + public CompletionStage verify() { + CompletableFuture cf = new CompletableFuture<>(); + oracleClient.query("SELECT 1 FROM DUAL").execute(ar -> { + if (ar.failed()) { + cf.completeExceptionally(ar.cause()); + } else { + cf.complete(null); + } + }); + return cf; + } + } + + @Singleton + public static class DefaultOraclePoolCreator implements OraclePoolCreator { + + @Override + public OraclePool create(Input input) { + assertEquals(12345, input.oracleConnectOptions().getPort()); // validate that the bean has been called for the proper datasource + return OraclePool.pool(input.vertx(), input.oracleConnectOptions().setHost("localhost").setPort(1521), + input.poolOptions()); + } + } + + @Singleton + @ReactiveDataSource("hibernate") + public static class HibernateOraclePoolCreator implements OraclePoolCreator { + + @Override + public OraclePool create(Input input) { + assertEquals(55555, input.oracleConnectOptions().getPort()); // validate that the bean has been called for the proper datasource + return OraclePool.pool(input.vertx(), input.oracleConnectOptions().setHost("localhost").setPort(1521), + input.poolOptions()); + } + } +} diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/MultipleOraclePoolCreatorsForSameDatasourceTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/MultipleOraclePoolCreatorsForSameDatasourceTest.java new file mode 100644 index 0000000000000..8e0987f9ba5fa --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/MultipleOraclePoolCreatorsForSameDatasourceTest.java @@ -0,0 +1,40 @@ +package io.quarkus.reactive.oracle.client; + +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DeploymentException; +import javax.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.oracleclient.OraclePool; + +public class MultipleOraclePoolCreatorsForSameDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(CustomCredentialsProvider.class) + .addClass(CredentialsTestResource.class) + .addClass(LocalhostOraclePoolCreator.class) + .addClass(AnotherOraclePoolCreator.class) + .addAsResource("application-credentials-with-erroneous-url.properties", "application.properties")) + .setExpectedException(DeploymentException.class); + + @Test + public void test() { + fail("Should never have been called"); + } + + @Singleton + public static class AnotherOraclePoolCreator implements OraclePoolCreator { + + @Override + public OraclePool create(Input input) { + return OraclePool.pool(input.vertx(), input.oracleConnectOptions(), input.poolOptions()); + } + } + +} diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/OraclePoolCreatorTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/OraclePoolCreatorTest.java new file mode 100644 index 0000000000000..c8f268e68b435 --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/OraclePoolCreatorTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.oracle.client; + +import static io.restassured.RestAssured.given; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class OraclePoolCreatorTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(CustomCredentialsProvider.class) + .addClass(CredentialsTestResource.class) + .addClass(LocalhostOraclePoolCreator.class) + .addAsResource("application-credentials-with-erroneous-url.properties", "application.properties")); + + @Test + public void testConnect() { + given() + .when().get("/test") + .then() + .statusCode(200) + .body(CoreMatchers.equalTo("OK")); + } + +} diff --git a/extensions/reactive-oracle-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties b/extensions/reactive-oracle-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties new file mode 100644 index 0000000000000..df109c4000943 --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties @@ -0,0 +1,3 @@ +quarkus.datasource.db-kind=oracle +quarkus.datasource.credentials-provider=custom +quarkus.datasource.reactive.url=vertx-reactive:oracle:thin:@test:12345:XE diff --git a/extensions/reactive-oracle-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties b/extensions/reactive-oracle-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties new file mode 100644 index 0000000000000..b1461c2cd52e3 --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties @@ -0,0 +1,9 @@ +quarkus.datasource.db-kind=oracle +quarkus.datasource.username=SYSTEM +quarkus.datasource.password=hibernate_orm_test +quarkus.datasource.reactive.url=vertx-reactive:oracle:thin:@test:12345:XE + +quarkus.datasource."hibernate".db-kind=oracle +quarkus.datasource."hibernate".username=SYSTEM +quarkus.datasource."hibernate".password=hibernate_orm_test +quarkus.datasource."hibernate".reactive.url=vertx-reactive:oracle:thin:@test:55555:XE diff --git a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/OraclePoolCreator.java b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/OraclePoolCreator.java new file mode 100644 index 0000000000000..de2441d5dda83 --- /dev/null +++ b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/OraclePoolCreator.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.oracle.client; + +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.vertx.core.Vertx; +import io.vertx.oracleclient.OracleConnectOptions; +import io.vertx.oracleclient.OraclePool; +import io.vertx.sqlclient.PoolOptions; + +/** + * This interface is an integration point that allows users to use the {@link Vertx}, {@link PoolOptions} and + * {@link OracleConnectOptions} objects configured automatically by Quarkus, in addition to a custom strategy + * for creating the final {@link OraclePool}. + * + * Implementations of this class are meant to be used as CDI beans. + * If a bean of this type is used without a {@link ReactiveDataSource} qualifier, then it's applied to the default datasource, + * otherwise it applies to the datasource matching the name of the annotation. + */ +public interface OraclePoolCreator { + + OraclePool create(Input input); + + interface Input { + + Vertx vertx(); + + PoolOptions poolOptions(); + + OracleConnectOptions oracleConnectOptions(); + } +} diff --git a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java index 316338e7b6d32..a24a80f7997bb 100644 --- a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java +++ b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java @@ -7,14 +7,20 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import javax.enterprise.inject.Instance; + import org.jboss.logging.Logger; +import io.quarkus.arc.Arc; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; +import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; +import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.DataSourceReactiveRuntimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.oracle.client.OraclePoolCreator; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; @@ -72,7 +78,7 @@ private OraclePool initialize(Vertx vertx, log.warn( "Configuration element 'thread-local' on Reactive datasource connections is deprecated and will be ignored. The started pool will always be based on a per-thread separate pool now."); } - return OraclePool.pool(vertx, oracleConnectOptions, poolOptions); + return createPool(vertx, poolOptions, oracleConnectOptions, dataSourceName); } private PoolOptions toPoolOptions(Integer eventLoopCount, @@ -151,4 +157,46 @@ private OracleConnectOptions toOracleConnectOptions(DataSourceRuntimeConfig data return oracleConnectOptions; } + private OraclePool createPool(Vertx vertx, PoolOptions poolOptions, OracleConnectOptions oracleConnectOptions, + String dataSourceName) { + Instance instance; + if (DataSourceUtil.isDefault(dataSourceName)) { + instance = Arc.container().select(OraclePoolCreator.class); + } else { + instance = Arc.container().select(OraclePoolCreator.class, + new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); + } + if (instance.isResolvable()) { + OraclePoolCreator.Input input = new DefaultInput(vertx, poolOptions, oracleConnectOptions); + return instance.get().create(input); + } + return OraclePool.pool(vertx, oracleConnectOptions, poolOptions); + } + + private static class DefaultInput implements OraclePoolCreator.Input { + private final Vertx vertx; + private final PoolOptions poolOptions; + private final OracleConnectOptions oracleConnectOptions; + + public DefaultInput(Vertx vertx, PoolOptions poolOptions, OracleConnectOptions oracleConnectOptions) { + this.vertx = vertx; + this.poolOptions = poolOptions; + this.oracleConnectOptions = oracleConnectOptions; + } + + @Override + public Vertx vertx() { + return vertx; + } + + @Override + public PoolOptions poolOptions() { + return poolOptions; + } + + @Override + public OracleConnectOptions oracleConnectOptions() { + return oracleConnectOptions; + } + } } diff --git a/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java b/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java index 12463073eb480..1eff23617ecfd 100644 --- a/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java +++ b/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java @@ -1,12 +1,26 @@ package io.quarkus.reactive.pg.client.deployment; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; + import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.arc.deployment.devconsole.Name; +import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.DotNames; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.common.runtime.DatabaseKind; @@ -34,6 +48,7 @@ import io.quarkus.reactive.datasource.runtime.DataSourceReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveBuildTimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.pg.client.PgPoolCreator; import io.quarkus.reactive.pg.client.runtime.DataSourcesReactivePostgreSQLConfig; import io.quarkus.reactive.pg.client.runtime.PgPoolRecorder; import io.quarkus.reactive.pg.client.runtime.PostgreSQLServiceBindingConverter; @@ -121,6 +136,33 @@ void addHealthCheck( dataSourcesBuildTimeConfig.healthEnabled)); } + @BuildStep + void unremoveableBeans(BuildProducer producer) { + producer.produce(UnremovableBeanBuildItem.beanTypes(PgPoolCreator.class)); + } + + @BuildStep + void validateBeans(ValidationPhaseBuildItem validationPhase, + BuildProducer errors) { + // no two PgPoolCreator beans can be associated with the same datasource + Map seen = new HashMap<>(); + for (BeanInfo beanInfo : validationPhase.getContext().beans().matchBeanTypes(new PgPoolCreatorBeanClassPredicate())) { + Set qualifiers = new TreeSet<>(); // use a TreeSet in order to get a predictable iteration order + for (AnnotationInstance qualifier : beanInfo.getQualifiers()) { + qualifiers.add(Name.from(qualifier)); + } + String qualifiersStr = qualifiers.stream().map(Name::toString).collect(Collectors.joining("_")); + if (seen.getOrDefault(qualifiersStr, false)) { + errors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem( + new IllegalStateException( + "There can be at most one bean of type '" + PgPoolCreator.class.getName() + + "' for each datasource."))); + } else { + seen.put(qualifiersStr, true); + } + } + } + @BuildStep void registerServiceBinding(Capabilities capabilities, BuildProducer serviceProvider, BuildProducer dbKind) { @@ -243,4 +285,14 @@ private static void addQualifiers(ExtendedBeanConfigurator configurator, String .done(); } } + + private static class PgPoolCreatorBeanClassPredicate implements Predicate> { + private static final Type PG_POOL_CREATOR = Type.create(DotName.createSimple(PgPoolCreator.class.getName()), + Type.Kind.CLASS); + + @Override + public boolean test(Set types) { + return types.contains(PG_POOL_CREATOR); + } + } } diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/LocalhostPgPoolCreator.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/LocalhostPgPoolCreator.java new file mode 100644 index 0000000000000..4df42b98dde69 --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/LocalhostPgPoolCreator.java @@ -0,0 +1,14 @@ +package io.quarkus.reactive.pg.client; + +import javax.inject.Singleton; + +import io.vertx.pgclient.PgPool; + +@Singleton +public class LocalhostPgPoolCreator implements PgPoolCreator { + + @Override + public PgPool create(Input input) { + return PgPool.pool(input.vertx(), input.pgConnectOptions().setHost("localhost").setPort(5431), input.poolOptions()); + } +} diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/MultipleDataSourcesAndPgPoolCreatorsTest.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/MultipleDataSourcesAndPgPoolCreatorsTest.java new file mode 100644 index 0000000000000..bc5b7bfbcb5c8 --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/MultipleDataSourcesAndPgPoolCreatorsTest.java @@ -0,0 +1,103 @@ +package io.quarkus.reactive.pg.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.pgclient.PgPool; + +public class MultipleDataSourcesAndPgPoolCreatorsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-multiple-datasources-with-erroneous-url.properties") + .withApplicationRoot((jar) -> jar + .addClasses(BeanUsingDefaultDataSource.class) + .addClass(BeanUsingHibernateDataSource.class) + .addClass(DefaultPgPoolCreator.class) + .addClass(HibernatePgPoolCreator.class)); + + @Inject + BeanUsingDefaultDataSource beanUsingDefaultDataSource; + + @Inject + BeanUsingHibernateDataSource beanUsingHibernateDataSource; + + @Test + public void testMultipleDataSources() { + beanUsingDefaultDataSource.verify() + .thenCompose(v -> beanUsingHibernateDataSource.verify()) + .toCompletableFuture() + .join(); + } + + @ApplicationScoped + static class BeanUsingDefaultDataSource { + + @Inject + PgPool pgClient; + + public CompletionStage verify() { + CompletableFuture cf = new CompletableFuture<>(); + pgClient.query("SELECT 1").execute(ar -> { + if (ar.failed()) { + cf.completeExceptionally(ar.cause()); + } else { + cf.complete(null); + } + }); + return cf; + } + } + + @ApplicationScoped + static class BeanUsingHibernateDataSource { + + @Inject + @ReactiveDataSource("hibernate") + PgPool pgClient; + + public CompletionStage verify() { + CompletableFuture cf = new CompletableFuture<>(); + pgClient.query("SELECT 1").execute(ar -> { + if (ar.failed()) { + cf.completeExceptionally(ar.cause()); + } else { + cf.complete(null); + } + }); + return cf; + } + } + + @Singleton + public static class DefaultPgPoolCreator implements PgPoolCreator { + + @Override + public PgPool create(Input input) { + assertEquals(10, input.pgConnectOptions().getPipeliningLimit()); // validate that the bean has been called for the proper datasource + return PgPool.pool(input.vertx(), input.pgConnectOptions().setHost("localhost").setPort(5431), input.poolOptions()); + } + } + + @Singleton + @ReactiveDataSource("hibernate") + public static class HibernatePgPoolCreator implements PgPoolCreator { + + @Override + public PgPool create(Input input) { + assertEquals(7, input.pgConnectOptions().getPipeliningLimit()); // validate that the bean has been called for the proper datasource + return PgPool.pool(input.vertx(), input.pgConnectOptions().setHost("localhost").setPort(5431), input.poolOptions()); + } + } +} diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/MultiplePgPoolCreatorsForSameDatasourceTest.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/MultiplePgPoolCreatorsForSameDatasourceTest.java new file mode 100644 index 0000000000000..325e6865a84ca --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/MultiplePgPoolCreatorsForSameDatasourceTest.java @@ -0,0 +1,40 @@ +package io.quarkus.reactive.pg.client; + +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DeploymentException; +import javax.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.pgclient.PgPool; + +public class MultiplePgPoolCreatorsForSameDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(CustomCredentialsProvider.class) + .addClass(CredentialsTestResource.class) + .addClass(LocalhostPgPoolCreator.class) + .addClass(AnotherPgPoolCreator.class) + .addAsResource("application-credentials-with-erroneous-url.properties", "application.properties")) + .setExpectedException(DeploymentException.class); + + @Test + public void test() { + fail("Should never have been called"); + } + + @Singleton + public static class AnotherPgPoolCreator implements PgPoolCreator { + + @Override + public PgPool create(Input input) { + return PgPool.pool(input.vertx(), input.pgConnectOptions(), input.poolOptions()); + } + } + +} diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/PgPoolCreatorTest.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/PgPoolCreatorTest.java new file mode 100644 index 0000000000000..bc16ade4ee22b --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/PgPoolCreatorTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.pg.client; + +import static io.restassured.RestAssured.given; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class PgPoolCreatorTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(CustomCredentialsProvider.class) + .addClass(CredentialsTestResource.class) + .addClass(LocalhostPgPoolCreator.class) + .addAsResource("application-credentials-with-erroneous-url.properties", "application.properties")); + + @Test + public void testConnect() { + given() + .when().get("/test") + .then() + .statusCode(200) + .body(CoreMatchers.equalTo("OK")); + } + +} diff --git a/extensions/reactive-pg-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties b/extensions/reactive-pg-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties new file mode 100644 index 0000000000000..2784de89ee990 --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/resources/application-credentials-with-erroneous-url.properties @@ -0,0 +1,3 @@ +quarkus.datasource.db-kind=postgresql +quarkus.datasource.credentials-provider=custom +quarkus.datasource.reactive.url=vertx-reactive:postgresql://test:12345/hibernate_orm_test diff --git a/extensions/reactive-pg-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties b/extensions/reactive-pg-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties new file mode 100644 index 0000000000000..1c74fbf5ecc20 --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/resources/application-multiple-datasources-with-erroneous-url.properties @@ -0,0 +1,11 @@ +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=hibernate_orm_test +quarkus.datasource.password=hibernate_orm_test +quarkus.datasource.reactive.url=vertx-reactive:postgresql://test:12345/hibernate_orm_test +quarkus.datasource.reactive.postgresql.pipelining-limit=10 + +quarkus.datasource."hibernate".db-kind=postgresql +quarkus.datasource."hibernate".username=hibernate_orm_test +quarkus.datasource."hibernate".password=hibernate_orm_test +quarkus.datasource."hibernate".reactive.url=vertx-reactive:postgresql://test:55555/hibernate_orm_test +quarkus.datasource."hibernate".reactive.postgresql.pipelining-limit=7 diff --git a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/PgPoolCreator.java b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/PgPoolCreator.java new file mode 100644 index 0000000000000..ca40189b97b6e --- /dev/null +++ b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/PgPoolCreator.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.pg.client; + +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.vertx.core.Vertx; +import io.vertx.pgclient.PgConnectOptions; +import io.vertx.pgclient.PgPool; +import io.vertx.sqlclient.PoolOptions; + +/** + * This interface is an integration point that allows users to use the {@link Vertx}, {@link PoolOptions} and + * {@link PgConnectOptions} objects configured automatically by Quarkus, in addition to a custom strategy + * for creating the final {@link PgPool}. + * + * Implementations of this class are meant to be used as CDI beans. + * If a bean of this type is used without a {@link ReactiveDataSource} qualifier, then it's applied to the default datasource, + * otherwise it applies to the datasource matching the name of the annotation. + */ +public interface PgPoolCreator { + + PgPool create(Input input); + + interface Input { + + Vertx vertx(); + + PoolOptions poolOptions(); + + PgConnectOptions pgConnectOptions(); + } +} diff --git a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java index 31a5405003d3d..9c5708b440de5 100644 --- a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java +++ b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java @@ -13,14 +13,20 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import javax.enterprise.inject.Instance; + import org.jboss.logging.Logger; +import io.quarkus.arc.Arc; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; +import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; +import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.DataSourceReactiveRuntimeConfig; import io.quarkus.reactive.datasource.runtime.DataSourcesReactiveRuntimeConfig; +import io.quarkus.reactive.pg.client.PgPoolCreator; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; @@ -78,7 +84,7 @@ private PgPool initialize(Vertx vertx, log.warn( "Configuration element 'thread-local' on Reactive datasource connections is deprecated and will be ignored. The started pool will always be based on a per-thread separate pool now."); } - return PgPool.pool(vertx, pgConnectOptions, poolOptions); + return createPool(vertx, poolOptions, pgConnectOptions, dataSourceName); } private PoolOptions toPoolOptions(Integer eventLoopCount, @@ -201,4 +207,47 @@ private PgConnectOptions toPgConnectOptions(DataSourceRuntimeConfig dataSourceRu return pgConnectOptions; } + + private PgPool createPool(Vertx vertx, PoolOptions poolOptions, PgConnectOptions pgConnectOptions, + String dataSourceName) { + Instance instance; + if (DataSourceUtil.isDefault(dataSourceName)) { + instance = Arc.container().select(PgPoolCreator.class); + } else { + instance = Arc.container().select(PgPoolCreator.class, + new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); + } + if (instance.isResolvable()) { + PgPoolCreator.Input input = new DefaultInput(vertx, poolOptions, pgConnectOptions); + return instance.get().create(input); + } + return PgPool.pool(vertx, pgConnectOptions, poolOptions); + } + + private static class DefaultInput implements PgPoolCreator.Input { + private final Vertx vertx; + private final PoolOptions poolOptions; + private final PgConnectOptions pgConnectOptions; + + public DefaultInput(Vertx vertx, PoolOptions poolOptions, PgConnectOptions pgConnectOptions) { + this.vertx = vertx; + this.poolOptions = poolOptions; + this.pgConnectOptions = pgConnectOptions; + } + + @Override + public Vertx vertx() { + return vertx; + } + + @Override + public PoolOptions poolOptions() { + return poolOptions; + } + + @Override + public PgConnectOptions pgConnectOptions() { + return pgConnectOptions; + } + } }