This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new ce173b09cf NIFI-15383 Ensure JDBC Driver deregistered from
HikariCPConnectionPool (#10692)
ce173b09cf is described below
commit ce173b09cffd588bf9fee7b370e7600a3556e183
Author: Bob Paulin <[email protected]>
AuthorDate: Mon Dec 29 09:35:32 2025 -0600
NIFI-15383 Ensure JDBC Driver deregistered from HikariCPConnectionPool
(#10692)
Signed-off-by: David Handermann <[email protected]>
---
.../nifi-hikari-dbcp-service/pom.xml | 6 ++
.../apache/nifi/dbcp/HikariCPConnectionPool.java | 82 ++++++++++++++++++++++
.../nifi/dbcp/HikariCPConnectionPoolTest.java | 20 ++++++
3 files changed, 108 insertions(+)
diff --git
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/pom.xml
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/pom.xml
index 42ccf64935..271d54c7d9 100644
---
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/pom.xml
+++
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/pom.xml
@@ -53,5 +53,11 @@
<artifactId>nifi-dbcp-utils</artifactId>
<version>2.8.0-SNAPSHOT</version>
</dependency>
+ <dependency>
+ <groupId>org.hsqldb</groupId>
+ <artifactId>hsqldb</artifactId>
+ <version>2.7.4</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/main/java/org/apache/nifi/dbcp/HikariCPConnectionPool.java
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/main/java/org/apache/nifi/dbcp/HikariCPConnectionPool.java
index 3c51c2a2eb..319b64eebc 100644
---
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/main/java/org/apache/nifi/dbcp/HikariCPConnectionPool.java
+++
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/main/java/org/apache/nifi/dbcp/HikariCPConnectionPool.java
@@ -27,11 +27,13 @@ import
org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnDisabled;
import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.annotation.lifecycle.OnRemoved;
import org.apache.nifi.components.ConfigVerificationResult;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.PropertyValue;
import org.apache.nifi.components.RequiredPermission;
import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceReference;
import org.apache.nifi.components.resource.ResourceReferences;
import org.apache.nifi.components.resource.ResourceType;
import org.apache.nifi.controller.AbstractControllerService;
@@ -52,6 +54,8 @@ import org.apache.nifi.security.krb.KerberosUser;
import javax.security.auth.login.LoginException;
import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@@ -213,6 +217,8 @@ public class HikariCPConnectionPool extends
AbstractControllerService implements
private volatile HikariDataSource dataSource;
private volatile KerberosUser kerberosUser;
+ // Hold an instance of the driver so we can properly de-register it.
+ private volatile Driver registeredDriver;
@Override
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
@@ -423,6 +429,9 @@ public class HikariCPConnectionPool extends
AbstractControllerService implements
}
}
+ // We load the driver here so we can keep a reference to it if it was
loaded in the InstanceClassLoader.
+ // This allows us to deregister it to prevent a memory leak.
+ loadDriver(driverName, dburl);
dataSource.setConnectionTimeout(maxWaitMillis);
dataSource.setValidationTimeout(Math.max(maxWaitMillis,
DEFAULT_MIN_VALIDATION_TIMEOUT));
dataSource.setMaximumPoolSize(maxTotal);
@@ -494,6 +503,79 @@ public class HikariCPConnectionPool extends
AbstractControllerService implements
}
}
+ protected void loadDriver(final String driverClassName, final String url) {
+ final Class<?> clazz;
+
+ try {
+ clazz = Class.forName(driverClassName);
+ } catch (final ClassNotFoundException e) {
+ // Enhanced error message with discovery
+ ResourceReferences driverResources = null;
+ try {
+ // Try to get driver resources from current context if
available
+ if (getConfigurationContext() != null) {
+ driverResources =
getConfigurationContext().getProperty(DB_DRIVER_LOCATION).evaluateAttributeExpressions().asResources();
+ }
+ } catch (Exception ignored) {
+ // Context might not be available, continue without it
+ }
+
+ final List<String> availableDrivers = (driverResources != null &&
driverResources.getCount() != 0) ?
DriverUtils.discoverDriverClasses(driverResources) : List.of();
+
+ StringBuilder errorMessage = new StringBuilder("JDBC driver class
'%s' not found.".formatted(driverClassName));
+
+ if (!availableDrivers.isEmpty()) {
+ errorMessage.append(" Available driver classes found in
resources: %s.".formatted(String.join(", ", availableDrivers)));
+ } else if (driverResources != null && driverResources.getCount()
!= 0) {
+ final List<ResourceReference> resourcesList =
driverResources.asList();
+ if (resourcesList.stream().filter(r -> r.getResourceType() !=
ResourceType.URL).count() != 0) {
+ errorMessage.append(" No JDBC driver classes found in the
provided resources.");
+ }
+ } else if (driverResources == null) {
+ errorMessage.append(" The property 'Database Driver
Location(s)' should be set.");
+ }
+
+ throw new IllegalStateException(errorMessage.toString(), e);
+ }
+
+ try {
+ final Driver driver = DriverManager.getDriver(url);
+ // Ensure drivers that register themselves during class loading
can be set as the registeredDriver.
+ // This ensures drivers that register themselves can be
deregisterd when the componet is removed.
+ // These drivers should be loaded in the same InstanceClassloader
that load this component
+ if (driver != registeredDriver
+ &&
driver.getClass().getClassLoader().equals(getClass().getClassLoader())) {
+ DriverManager.deregisterDriver(registeredDriver);
+ registeredDriver = driver;
+ }
+ } catch (final SQLException e) {
+ // In case the driver is not registered by the implementation, we
explicitly try to register it.
+ try {
+ if (registeredDriver != null) {
+ DriverManager.deregisterDriver(registeredDriver);
+ }
+ registeredDriver = (Driver)
clazz.getDeclaredConstructor().newInstance();
+
+ DriverManager.registerDriver(registeredDriver);
+ DriverManager.getDriver(url);
+ } catch (final SQLException e2) {
+ throw new IllegalStateException("No suitable driver for the
given Database Connection URL", e2);
+ } catch (final Exception e2) {
+ throw new IllegalStateException("Creating driver instance is
failed", e2);
+ }
+ }
+ }
+
+ @OnRemoved
+ public void onRemove() {
+ try {
+ // We need to deregister the driver to allow the
InstanceClassLoader to be garbage collected.
+ DriverManager.deregisterDriver(registeredDriver);
+ } catch (final SQLException e) {
+ getLogger().warn("Failed to deregister driver [{}]",
registeredDriver, e);
+ }
+ }
+
@Override
public String toString() {
return String.format("%s[id=%s]", getClass().getSimpleName(),
getIdentifier());
diff --git
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/test/java/org/apache/nifi/dbcp/HikariCPConnectionPoolTest.java
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/test/java/org/apache/nifi/dbcp/HikariCPConnectionPoolTest.java
index 5d2f925a0a..f7cbe7cea8 100644
---
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/test/java/org/apache/nifi/dbcp/HikariCPConnectionPoolTest.java
+++
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/test/java/org/apache/nifi/dbcp/HikariCPConnectionPoolTest.java
@@ -29,6 +29,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
+import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Iterator;
@@ -166,6 +167,25 @@ public class HikariCPConnectionPoolTest {
assertOutcomeSuccessful(results);
}
+ @Test
+ public void testDeregisterDriver() throws Exception {
+ final HikariCPConnectionPool service = new HikariCPConnectionPool();
+ runner.addControllerService(SERVICE_ID, service);
+ final String url = "jdbc:hsqldb:mem:test";
+ runner.setProperty(service, HikariCPConnectionPool.DATABASE_URL, url);
+ runner.setProperty(service, HikariCPConnectionPool.DB_USER,
String.class.getSimpleName());
+ runner.setProperty(service, HikariCPConnectionPool.DB_PASSWORD,
String.class.getName());
+ runner.setProperty(service, HikariCPConnectionPool.DB_DRIVERNAME,
"org.hsqldb.jdbc.JDBCDriver");
+ runner.setProperty(service,
HikariCPConnectionPool.MAX_TOTAL_CONNECTIONS, "2");
+ runner.enableControllerService(service);
+ runner.assertValid(service);
+ final int serviceRunningNumberOfDrivers =
Collections.list(DriverManager.getDrivers()).size();
+ runner.disableControllerService(service);
+ runner.removeControllerService(service);
+ final int expectedDriversAfterRemove = serviceRunningNumberOfDrivers -
1;
+ assertEquals(expectedDriversAfterRemove,
Collections.list(DriverManager.getDrivers()).size(), "Driver should be
deregistered on remove");
+ }
+
private void setDatabaseProperties(final HikariCPConnectionPool service) {
runner.setProperty(service, HikariCPConnectionPool.DATABASE_URL,
DB_DRIVERNAME_VALUE);
runner.setProperty(service, HikariCPConnectionPool.DB_DRIVERNAME,
MockDriver.class.getName());