diff --git a/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/JDBCMySQLProcessor.java b/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/JDBCMySQLProcessor.java index bf2c946974638..3455b3b728095 100644 --- a/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/JDBCMySQLProcessor.java +++ b/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/JDBCMySQLProcessor.java @@ -1,5 +1,24 @@ package io.quarkus.jdbc.mysql.deployment; +import java.io.Serializable; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import com.mysql.cj.MysqlConnection; +import com.mysql.cj.WarningListener; +import com.mysql.cj.conf.PropertySet; +import com.mysql.cj.jdbc.JdbcConnection; +import com.mysql.cj.jdbc.JdbcPreparedStatement; +import com.mysql.cj.jdbc.JdbcPropertySet; +import com.mysql.cj.jdbc.JdbcStatement; +import com.mysql.cj.jdbc.ha.LoadBalancedConnection; +import com.mysql.cj.jdbc.ha.ReplicationConnection; +import com.mysql.cj.jdbc.result.ResultSetInternalMethods; +import com.mysql.cj.protocol.Resultset; + import io.quarkus.agroal.deployment.JdbcDriverBuildItem; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.processor.BuiltinScope; @@ -7,12 +26,16 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem; import io.quarkus.deployment.builditem.NativeImageEnableAllTimeZonesBuildItem; import io.quarkus.deployment.builditem.SslNativeConfigBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.jdbc.mysql.runtime.MySQLAgroalConnectionConfigurer; +import io.quarkus.jdbc.mysql.runtime.MySQLRecorder; public class JDBCMySQLProcessor { @@ -39,6 +62,12 @@ void configureAgroalConnection(BuildProducer additional } } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void abandonedConnectionCleanUp(MySQLRecorder recorder) { + recorder.startAbandonedConnectionCleanup(); + } + @BuildStep NativeImageResourceBuildItem resource() { return new NativeImageResourceBuildItem("com/mysql/cj/util/TimeZoneMapping.properties"); @@ -53,4 +82,30 @@ NativeImageEnableAllCharsetsBuildItem enableAllCharsets() { NativeImageEnableAllTimeZonesBuildItem enableAllTimeZones() { return new NativeImageEnableAllTimeZonesBuildItem(); } + + @BuildStep + List registerProxies() { + List proxies = new ArrayList<>(); + proxies.add(new NativeImageProxyDefinitionBuildItem(JdbcConnection.class.getName())); + proxies.add(new NativeImageProxyDefinitionBuildItem(MysqlConnection.class.getName())); + proxies.add(new NativeImageProxyDefinitionBuildItem(Statement.class.getName())); + proxies.add(new NativeImageProxyDefinitionBuildItem(AutoCloseable.class.getName())); + proxies.add(new NativeImageProxyDefinitionBuildItem(JdbcStatement.class.getName())); + proxies.add(new NativeImageProxyDefinitionBuildItem(Connection.class.getName())); + proxies.add(new NativeImageProxyDefinitionBuildItem(ResultSet.class.getName())); + proxies.add( + new NativeImageProxyDefinitionBuildItem(JdbcPreparedStatement.class.getName(), JdbcStatement.class.getName())); + proxies.add(new NativeImageProxyDefinitionBuildItem(JdbcPropertySet.class.getName(), PropertySet.class.getName(), + Serializable.class.getName())); + proxies.add( + new NativeImageProxyDefinitionBuildItem(Resultset.class.getName(), ResultSetInternalMethods.class.getName())); + proxies.add(new NativeImageProxyDefinitionBuildItem(LoadBalancedConnection.class.getName(), + JdbcConnection.class.getName())); + proxies.add( + new NativeImageProxyDefinitionBuildItem(ReplicationConnection.class.getName(), JdbcConnection.class.getName())); + proxies.add( + new NativeImageProxyDefinitionBuildItem(ResultSetInternalMethods.class.getName(), + WarningListener.class.getName(), Resultset.class.getName())); + return proxies; + } } diff --git a/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/MySQLJDBCReflections.java b/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/MySQLJDBCReflections.java index a85da95ca6307..4dab5c595276c 100644 --- a/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/MySQLJDBCReflections.java +++ b/extensions/jdbc/jdbc-mysql/deployment/src/main/java/io/quarkus/jdbc/mysql/deployment/MySQLJDBCReflections.java @@ -1,5 +1,16 @@ package io.quarkus.jdbc.mysql.deployment; +import java.sql.Wrapper; + +import com.mysql.cj.conf.url.FailoverConnectionUrl; +import com.mysql.cj.conf.url.FailoverDnsSrvConnectionUrl; +import com.mysql.cj.conf.url.LoadBalanceConnectionUrl; +import com.mysql.cj.conf.url.LoadBalanceDnsSrvConnectionUrl; +import com.mysql.cj.conf.url.ReplicationConnectionUrl; +import com.mysql.cj.conf.url.ReplicationDnsSrvConnectionUrl; +import com.mysql.cj.conf.url.SingleConnectionUrl; +import com.mysql.cj.conf.url.XDevApiConnectionUrl; +import com.mysql.cj.conf.url.XDevApiDnsSrvConnectionUrl; import com.mysql.cj.exceptions.AssertionFailedException; import com.mysql.cj.exceptions.CJCommunicationsException; import com.mysql.cj.exceptions.CJConnectionFeatureNotAvailableException; @@ -26,6 +37,10 @@ import com.mysql.cj.exceptions.UnableToConnectException; import com.mysql.cj.exceptions.UnsupportedConnectionStringException; import com.mysql.cj.exceptions.WrongArgumentException; +import com.mysql.cj.jdbc.Driver; +import com.mysql.cj.jdbc.ha.NdbLoadBalanceExceptionChecker; +import com.mysql.cj.jdbc.ha.StandardLoadBalanceExceptionChecker; +import com.mysql.cj.log.StandardLogger; import com.mysql.cj.protocol.AsyncSocketFactory; import com.mysql.cj.protocol.NamedPipeSocketFactory; import com.mysql.cj.protocol.SocksProxySocketFactory; @@ -39,28 +54,30 @@ public final class MySQLJDBCReflections { @BuildStep void registerDriverForReflection(BuildProducer reflectiveClass) { - reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, com.mysql.cj.jdbc.Driver.class.getName())); + + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, Driver.class.getName())); reflectiveClass.produce( - new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.FailoverDnsSrvConnectionUrl.class.getName())); + new ReflectiveClassBuildItem(false, false, FailoverDnsSrvConnectionUrl.class.getName())); reflectiveClass.produce( - new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.FailoverConnectionUrl.class.getName())); + new ReflectiveClassBuildItem(false, false, FailoverConnectionUrl.class.getName())); reflectiveClass - .produce(new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.SingleConnectionUrl.class.getName())); + .produce(new ReflectiveClassBuildItem(false, false, SingleConnectionUrl.class.getName())); reflectiveClass.produce( - new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.LoadBalanceConnectionUrl.class.getName())); + new ReflectiveClassBuildItem(false, false, LoadBalanceConnectionUrl.class.getName())); reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, - com.mysql.cj.conf.url.LoadBalanceDnsSrvConnectionUrl.class.getName())); + LoadBalanceDnsSrvConnectionUrl.class.getName())); reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, - com.mysql.cj.conf.url.ReplicationDnsSrvConnectionUrl.class.getName())); + ReplicationDnsSrvConnectionUrl.class.getName())); reflectiveClass.produce( - new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.ReplicationConnectionUrl.class.getName())); + new ReflectiveClassBuildItem(false, false, ReplicationConnectionUrl.class.getName())); reflectiveClass.produce( - new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.XDevApiConnectionUrl.class.getName())); + new ReflectiveClassBuildItem(false, false, XDevApiConnectionUrl.class.getName())); reflectiveClass.produce( - new ReflectiveClassBuildItem(false, false, com.mysql.cj.conf.url.XDevApiDnsSrvConnectionUrl.class.getName())); + new ReflectiveClassBuildItem(false, false, XDevApiDnsSrvConnectionUrl.class.getName())); reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, com.mysql.cj.jdbc.ha.LoadBalancedAutoCommitInterceptor.class.getName())); - reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, com.mysql.cj.log.StandardLogger.class.getName())); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, StandardLogger.class.getName())); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, Wrapper.class.getName())); } @BuildStep @@ -103,5 +120,9 @@ void registerExceptionsForReflection(BuildProducer ref reflectiveClass .produce(new ReflectiveClassBuildItem(false, false, UnsupportedConnectionStringException.class.getName())); reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, WrongArgumentException.class.getName())); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, "com.mysql.cj.jdbc.MysqlXAException")); + reflectiveClass + .produce(new ReflectiveClassBuildItem(false, false, StandardLoadBalanceExceptionChecker.class.getName())); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, NdbLoadBalanceExceptionChecker.class.getName())); } } diff --git a/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/MySQLRecorder.java b/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/MySQLRecorder.java new file mode 100644 index 0000000000000..42f6872c0ef97 --- /dev/null +++ b/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/MySQLRecorder.java @@ -0,0 +1,16 @@ +package io.quarkus.jdbc.mysql.runtime; + +import org.graalvm.nativeimage.ImageInfo; + +import io.quarkus.jdbc.mysql.runtime.graal.AbandonedConnectionCleanupThreadSubstitutions; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class MySQLRecorder { + + public void startAbandonedConnectionCleanup() { + if (ImageInfo.inImageRuntimeCode()) { + AbandonedConnectionCleanupThreadSubstitutions.startCleanUp(); + } + } +} diff --git a/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/AbandonedConnectionCleanupThreadSubstitutions.java b/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/AbandonedConnectionCleanupThreadSubstitutions.java new file mode 100644 index 0000000000000..688b6b2ee8f93 --- /dev/null +++ b/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/AbandonedConnectionCleanupThreadSubstitutions.java @@ -0,0 +1,104 @@ +package io.quarkus.jdbc.mysql.runtime.graal; + +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.mysql.cj.MysqlConnection; +import com.mysql.cj.jdbc.AbandonedConnectionCleanupThread; +import com.mysql.cj.protocol.NetworkResources; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +/** + * This is to disable connection clean up thread {@link AbandonedConnectionCleanupThread} which launches a thread on a static + * block. + * GraalVM is not happy about that. The issue might have been fixed with https://github.com/oracle/graal/pull/1542 but we have + * to wait for a proper GraalVM release, so we substitute the class and start the clean up thread manually when running in + * native image. + */ +@Substitute +@TargetClass(AbandonedConnectionCleanupThread.class) +final public class AbandonedConnectionCleanupThreadSubstitutions implements Runnable { + + private static Set connectionFinalizerPhantomReferences; + private static ReferenceQueue mysqlConnectionReferenceQueue; + + private static ExecutorService executorService; + + @Substitute + private AbandonedConnectionCleanupThreadSubstitutions() { + } + + public static void startCleanUp() { + connectionFinalizerPhantomReferences = ConcurrentHashMap.newKeySet(); + mysqlConnectionReferenceQueue = new ReferenceQueue<>(); + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.execute(new AbandonedConnectionCleanupThreadSubstitutions()); + } + + @Substitute + public void run() { + for (;;) { + try { + Reference reference = mysqlConnectionReferenceQueue.remove(5000); + if (reference != null) { + finalizeResourceAndRemoveReference((ConnectionFinalizerPhantomReference) reference); + } + } catch (InterruptedException e) { + synchronized (mysqlConnectionReferenceQueue) { + Reference reference; + while ((reference = mysqlConnectionReferenceQueue.poll()) != null) { + finalizeResourceAndRemoveReference((ConnectionFinalizerPhantomReference) reference); + } + connectionFinalizerPhantomReferences.clear(); + } + + return; + } catch (Exception ex) { + } + } + } + + @Substitute + protected static void trackConnection(MysqlConnection conn, NetworkResources io) { + synchronized (mysqlConnectionReferenceQueue) { + ConnectionFinalizerPhantomReference reference = new ConnectionFinalizerPhantomReference(conn, io, + mysqlConnectionReferenceQueue); + connectionFinalizerPhantomReferences.add(reference); + } + } + + private static void finalizeResourceAndRemoveReference(ConnectionFinalizerPhantomReference reference) { + try { + reference.finalizeResources(); + reference.clear(); + } finally { + connectionFinalizerPhantomReferences.remove(reference); + } + } + + private static class ConnectionFinalizerPhantomReference extends PhantomReference { + private NetworkResources networkResources; + + ConnectionFinalizerPhantomReference(MysqlConnection conn, NetworkResources networkResources, + ReferenceQueue refQueue) { + super(conn, refQueue); + this.networkResources = networkResources; + } + + void finalizeResources() { + if (this.networkResources != null) { + try { + this.networkResources.forceClose(); + } finally { + this.networkResources = null; + } + } + } + } +} diff --git a/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/AbandonedConnectionCleanupThread_disable.java b/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/AbandonedConnectionCleanupThread_disable.java deleted file mode 100644 index fdfa68145cac4..0000000000000 --- a/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/AbandonedConnectionCleanupThread_disable.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.quarkus.jdbc.mysql.runtime.graal; - -import com.mysql.cj.MysqlConnection; -import com.mysql.cj.jdbc.AbandonedConnectionCleanupThread; -import com.mysql.cj.protocol.NetworkResources; -import com.oracle.svm.core.annotate.Substitute; -import com.oracle.svm.core.annotate.TargetClass; - -/** - * This is to disable connection clean up thread {@link AbandonedConnectionCleanupThread} which launches a thread on a static - * block. - * GraalVM is not happy about that. The issue might have been fixed with https://github.com/oracle/graal/pull/1542 but we have - * to wait for a proper GraalVM release, so we substitute the whole class for now. - * - * There is a PR @see on the mysql repo to enable - * disabling of this class completely by using a system property. Let's see if we can get rid of this once the PR is merged. - */ -@Substitute -@TargetClass(AbandonedConnectionCleanupThread.class) -final public class AbandonedConnectionCleanupThread_disable { - - @Substitute - protected static void trackConnection(MysqlConnection conn, NetworkResources io) { - // do nothing - } -}