Reputation: 1278
We have a Liferay Service Builder module (portlet) that uses a JDBC DataSource
for an external (non-Liferay) database from a HikariCP database connection pool declared in Tomcat using JNDI.
When we first start the server, the JNDI connection works as expected (we're able to retrieve/edit database data), but when we try to redeploy (hot-deploy) the module, Liferay/Spring closes the connection pool during undeployment, so when the module deploys again, we receive errors in the Liferay log (see below) and the module is unable to perform any database operations (resulting in additional exceptions).
2024-10-04 18:41:33.620 ERROR [Refresh Thread: Equinox Container: ce2fdf2e-11b2-49cf-87b1-8d78cc1bcce5][DialectDetector:147] java.sql.SQLException: HikariDataSource HikariDataSource (appServices) has been closed.
java.sql.SQLException: HikariDataSource HikariDataSource (appServices) has been closed.
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:81)
at org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy$LazyConnectionInvocationHandler.getTargetConnection(LazyConnectionDataSourceProxy.java:403)
at org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy$LazyConnectionInvocationHandler.invoke(LazyConnectionDataSourceProxy.java:376)
at com.sun.proxy.$Proxy7.getMetaData(Unknown Source)
at com.liferay.portal.spring.hibernate.DialectDetector.getDialect(DialectDetector.java:56)
at com.liferay.portal.spring.hibernate.PortalHibernateConfiguration.newConfiguration(PortalHibernateConfiguration.java:144)
...
The only known resolution is to restart Tomcat after each module deployment, but this breaks our development workflow (hot-redeploy is a huge time saver) and our CI deployments.
How can we configure HikariCP/Tomcat/Liferay so that it doesn't close the HikariCP connection pools during module redeployment?
We're using Liferay 7.1.3-ga4.
We've "installed" and configured an "external" (non-Liferay) JNDI DataSource in the "global" Tomcat context using HikariCP as the database connection pool per the Liferay documentation at https://learn.liferay.com/w/dxp/installation-and-upgrades/installing-liferay/installing-liferay-on-an-application-server/setting-up-jndi-on-tomcat (also referencing https://liferay.dev/blogs/-/blogs/tomcat-hikaricp and https://github.com/brettwooldridge/HikariCP).
Specifically:
Resource
definitions:
<Server port="8005" shutdown="SHUTDOWN">
...
<GlobalNamingResources>
...
<Resource name="jdbc/appServices"
factory="com.zaxxer.hikari.HikariJNDIFactory"
type="javax.sql.DataSource"
auth="Container"
connectionTimeout="2000"
idleTimeout="60000"
maxLifetime="0"
minimumIdle="5"
maximumPoolSize="20"
poolName="appServices"
registerMbeans="true"
leakDetectionThreshold="30000"
dataSourceClassName="org.postgresql.ds.PGSimpleDataSource"
dataSource.serverName="<host>"
dataSource.databaseName="<database>"
dataSource.portNumber="<port>"
dataSource.user="<username>"
dataSource.password="<password"
dataSource.socketTimeout="45000" />
...
</GlobalNamingResources>
...
</Server>
ResourceLink
definitions:
<Context>
...
<ResourceLink name="jdbc/appServices" global="jdbc/appServices" type="javax.sql.DataSource" />
...
</Context>
Our Service Builder module is configured to use that JNDI DataSource via JDBC per the Liferay documentation at https://help.liferay.com/hc/en-us/articles/360017886812-Connecting-Service-Builder-to-External-Databases and then leverage the Service Builder APIs to perform operations on a model.
Specifically:
Bean
declarations to the DataSource
:
<?xml version="1.0" encoding="UTF-8"?>
<beans
default-destroy-method="destroy"
default-init-method="afterPropertiesSet"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="com.liferay.portal.dao.jdbc.spring.DataSourceFactoryBean" id="liferayDataSourceFactory">
<property name="propertyPrefix" value="app.db." />
<property name="properties">
<props>
<prop key="app.db.jndi.name">jdbc/appServices</prop>
</props>
</property>
</bean>
<bean class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy" id="liferayDataSource">
<property name="targetDataSource" ref="liferayDataSourceFactory" />
</bean>
<alias alias="appDataSource" name="liferayDataSource" />
</beans>
DataSource
specified in ext-spring.xml:
<service-builder package-path="mil.psif.services" auto-namespace-tables="false">
...
<entity name="WeatherConfig" table="weather_config" data-source="appDataSource" local-service="true" remote-service="true">
<column name="id" db-name="id" primary="true" type="long"></column>
<column name="portletId" db-name="portletid" type="String"></column>
<column name="zipCode" db-name="zipcode" type="String"></column>
<column name="longitude" db-name="longitude" type="double"></column>
<column name="latitude" db-name="latitude" type="double"></column>
<column name="createdBy" db-name="created_by" type="long"></column>
<column name="createdDate" db-name="created_date" type="Date"></column>
<column name="lastModifiedBy" db-name="last_modified_by" type="long"></column>
<column name="lastModifiedDate" db-name="last_modified_date" type="Date"></column>
<finder name="portletId" return-type="Collection">
<finder-column name="portletId"></finder-column>
</finder>
</entity>
....
</service-builder>
public class WeatherConfigLocalServiceImpl extends WeatherConfigLocalServiceBaseImpl {
...
public WeatherConfig AddWeatherConfig(String portletId, String zipCode,
double longitude, double latitude, long userId) {
long resourceId = counterLocalService.increment(WeatherConfig.class.getName());
WeatherConfig weatherConfig = weatherConfigPersistence.create(resourceId);
weatherConfig.setPortletId(portletId);
weatherConfig.setZipCode(zipCode);
weatherConfig.setLongitude(longitude);
weatherConfig.setLatitude(latitude);
weatherConfig.setCreatedBy(userId);
weatherConfig.setCreatedDate(new Date());
weatherConfig.setLastModifiedBy(userId);
weatherConfig.setLastModifiedDate(new Date());
WeatherConfig savedWeatherConfig = weatherConfigPersistence.update(weatherConfig);
return savedWeatherConfigHistory;
}
...
}
As a recap from above, the use case is:
blade gw deploy
blade server stop; blade server restart
or restarting the Tomcat Windows service, etc.
blade gw deploy
DataSource
being closedblade server stop; blade server restart
or restarting the Tomcat Windows service, etc.
Some of the many articles I've read that seem related but don't actually solve my problem:
Resources
(only HikariCP)Previously, we used (pure) JDBC Resource
s and Tomcat JDBC as a connection pool. The same use case (hot-redeploying) does not cause the Resource
s nor connection pool to stop, and the service continues to perform "correctly" after the redeployment.
JDBC Resource
config in server.xml:
<Resource
name="jdbc/appServices"
auth="Container"
type="javax.sql.DataSource"
username="<user>"
password="<password>"
url="jdbc:postgresql://<host>:<port>/<database>"
driverClassName="org.postgresql.Driver"
maxActive="20"
maxIdle="10"
maxWait="-1"
/>
Tomcat DBCP Resource
config in server.xml:
<Resource
name="jdbc/appServices"
auth="Container"
type="javax.sql.DataSource"
driverClassName="org.postgresql.Driver"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
url="jdbc:postgresql://<host>:<port>/<database>"
username="<username>"
password="<password>"
maxActive="20"
maxIdle="5"
maxWait="2000"
removeAbandoned="true"
removeAbandonedTimeout="45"
testOnBorrow="true"
validationQuery="SELECT 1"
jdbcInterceptors="ResetAbandonedTimer"
/>
Using jconsole.exe to monitor the HikariCP connection pool shows that when the Service module is redeployed, the connection pool is destroyed.
Again, when using Tomcat DBCP, jconsole shows that the connection pool remains running when the module is redeployed.
DataSource
sWhen a breakpoint is set in com.zaxxer.hikari.HikariDataSource#close
, the breakpoint is triggered when the Service module is redeployed, and the stack trace shows it is called by Liferay's com.liferay.portal.dao.jdbc.DataSourceFactoryImpl#destroyDataSource
.
Notably, it appears that Spring attempts to clean up its beans when the module is undeployed, which calls Liferay's DataSourceFactoryImpl
. When using HikariCP, the provided DataSource
parameter is a com.zaxxer.hikari.HikariDataSource
object, which implements java.io.Closable
, and so the Liferay code calls its close
method, which shuts down the DataSource
(HikariCP pool). When using Tomcat DBCP, however, the provided DataSource parameter is a javax.sql.DataSource
, which doesn't match any of the blocks in Liferay's DataSourceFactoryImpl
code, and so Liferay doesn't close/shutdown the DataSource
.
You made it all the way here! You deserve a plaque!
Based on the above findings about Liferay code calling the DataSource
destroy method, I'm hunting for some Liferay setting or trick to turn off it's kill-the-datasource-when-module-undeploys logic.
But, this feels like I must be missing something obvious, although I've been scouring the confusing and conflicting Liferay documentation and discussing with the Liferay Community Slack for almost a week now, and I'm still at a complete loss as to what is the "right" way to configure Liferay with external databases declared using JNDI and used in a Service Builder using JDBC.
So, please, any help would be greatly appreciated.
Upvotes: 1
Views: 42