shawmanz32na
shawmanz32na

Reputation: 1278

How to prevent Liferay from closing external (non-Liferay) JNDI resources when redeploying modules?

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?

Installation / configuration / usage details

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:

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:

Troubleshooting and findings

As a recap from above, the use case is:

Some of the many articles I've read that seem related but don't actually solve my problem:

Does not affect Tomcat DBCP nor pure JDBC Resources (only HikariCP)

Previously, we used (pure) JDBC Resources and Tomcat JDBC as a connection pool. The same use case (hot-redeploying) does not cause the Resources 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"
  />

JMX indicates redeployment closes/destroys the Connection Pool

Using jconsole.exe to monitor the HikariCP connection pool shows that when the Service module is redeployed, the connection pool is destroyed.

Before redeployment: jconsole screenshot before redeployment

After redeployment: jconsole screenshot after redeployment

Again, when using Tomcat DBCP, jconsole shows that the connection pool remains running when the module is redeployed.

Live debugging indicates Liferay is closing the DataSources

When 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.

Live debugging showing HikariDataSource#close being called by Liferay code Liferay's DataSourceFactoryImpl#destroyDataSource implementation

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.

Final thoughts

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

Answers (0)

Related Questions