Skip to content

fix(db): open the pub/sub LISTEN connection outside the Hikari pool (#36934) - #36935

Open
fabrizzio-dotCMS wants to merge 1 commit into
mainfrom
issue-36934-pubsub-unpooled-listen-connection
Open

fix(db): open the pub/sub LISTEN connection outside the Hikari pool (#36934)#36935
fabrizzio-dotCMS wants to merge 1 commit into
mainfrom
issue-36934-pubsub-unpooled-listen-connection

Conversation

@fabrizzio-dotCMS

Copy link
Copy Markdown
Member

Fixes #36934

What

PGListener borrowed a connection from the Hikari request pool and held it for the lifetime of the JVM:

// JDBCPubSubImpl.java:121 — before
private final Lazy<Connection> connection =
        Lazy.of(() -> Try.of(() -> DbConnectionFactory.getDataSource().getConnection())…);

Holding it is correct — a Postgres LISTEN needs a persistent connection — but taking it from the request pool is not. Two consequences:

  1. The pool silently loses a slot. It is withdrawn for the lifetime of the JVM and never shows up as in-use work, so nobody accounts for it against max_connections.
  2. Every boot logs Apparent connection leak detected with a stack trace rooted at PGListener.<init>. It is a false positive — stopListening() does release the connection — but it reads like a bug, and it trains operators and support to ignore leak warnings, including real ones.

How

The listener builds its own connection from the pool's JDBC coordinates through DriverManager, so the pool keeps its full capacity and the detector has nothing to report.

When the datasource exposes no JDBC URL — JNDI-provided or otherwise wrapped — it falls back to the previous behaviour and says so in the log. A listener that works while logging a spurious warning beats no listener at all.

Lifecycle is unchanged: stopListening() already closes the connection, so start/stop cycles do not accumulate connections.

Testing

./mvnw test -pl :dotcms-core -Dmaven.build.cache.enabled=false \
    -Dtest=JDBCPubSubListenerConnectionTest

Tests run: 2, Failures: 0, Errors: 0 — pure unit tests, in no suite. They pin both branches: the fallback returns the pooled connection, and a datasource that does expose coordinates is never asked for one (verify(hikari, never()).getConnection()), which is the property that actually matters.

Verified at runtime against PostgreSQL 18 + dotCMS in Docker:

INFO dotpubsub.JDBCPubSubImpl - Opening a dedicated connection for the Postgres pub/sub
                                listener, outside the Hikari pool.
INFO dotpubsub.JDBCPubSubImpl - Starting to listen for Postgres notifications.

A controlled first-boot run on an empty database — the path where the warning was originally observed, during the starter load — is in progress; I will post the result as a comment. What is already confirmed is the positive signal above: the dedicated path is taken and the listener starts and runs normally.

Context

Found incidentally while reproducing #36222 (TC-056) on a local ES→OS migration stack. Unrelated subsystem, so it is filed and fixed separately rather than bundled into that fix.

🤖 Generated with Claude Code

…36934)

`PGListener` borrowed a connection from the request pool and held it for the
lifetime of the JVM, which is what a Postgres `LISTEN` needs and what a request
pool must never hand out. Two consequences: the pool silently loses a slot that
never shows up as in-use work, and HikariCP reports the hold as
`Apparent connection leak detected` — a stack trace on every boot that reads
like a bug and is not one, training everyone to ignore leak warnings.

The listener now builds its own connection from the pool's JDBC coordinates via
`DriverManager`, so the pool keeps its full capacity and the detector has
nothing to report. When the datasource exposes no JDBC URL — JNDI-provided or
otherwise wrapped — it falls back to the previous behaviour: a listener that
works while logging a spurious warning beats no listener at all.

Lifecycle is unchanged: `stopListening()` already closes the connection, so
start/stop cycles do not accumulate connections.

Tests: `JDBCPubSubListenerConnectionTest` pins both branches — the fallback
returns the pooled connection, and a datasource with coordinates is never asked
for one. 2/2 green, pure unit tests in no suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @fabrizzio-dotCMS's task in 2m 27s —— View job


Code Review

I reviewed the diff against origin/main, the four HikariDataSource strategies in com.dotmarketing.db, and the listener lifecycle.

The approach is sound: opening a dedicated DriverManager connection from the pool's coordinates keeps the pool at full capacity and silences the false-positive leak warning. Lifecycle is unchanged — stopListening() closes the connection and each reconnect builds a fresh PGListener, so start/stop cycles don't accumulate connections. The two unit tests pin both branches correctly, and the fallback for non-URL datasources (the JNDI/Tomcat strategy, which sets config.setDataSource(...) and leaves getJdbcUrl() null) is the right call.

New Issues

  • 🟡 Medium: dotCMS/src/main/java/com/dotcms/dotpubsub/JDBCPubSubImpl.java:171 — The dedicated connection is built from only jdbcUrl, username, and password, so any connection configuration the pool carries outside the URL is silently dropped for the listener. HikariCP can hold settings via dataSourceProperties / driverClassName, connectionInitSql, transactionIsolation, etc. For DBPropertiesDataSourceStrategy (new HikariConfig(propertiesFile.getPath()), line 81) a db.properties file can define dataSourceProperties (e.g. ssl, sslmode, sslrootcert, applicationName, socketTimeout) that are not part of the JDBC URL. When such properties exist, the pooled connections would use them but the DriverManager listener connection would not — e.g. a deployment that enforces SSL via dataSource.sslmode=require rather than a ?sslmode= URL param would get a listener connection that skips SSL, or fails to connect while pooled connections succeed.
    • Assumption: production deployments put all connection params (SSL, timeouts) in the JDBC URL, in which case this is a non-issue.
    • What to verify: whether any supported deployment path (db.properties, env-based) sets Postgres connection properties via dataSourceProperties instead of the URL. If so, consider carrying hikari.getDataSourceProperties() into the DriverManager.getConnection(url, Properties) overload, or unwrapping the underlying driver config, rather than passing only user/password.

Not blocking — the URL-only path is correct for the standard dotCMS Docker/env configuration confirmed in the PR's runtime test.

Notes (non-blocking)

  • JDBCPubSubImpl.java:171DriverManager.getConnection applies no socket/login connect timeout unless it's encoded in the URL. This matches the prior pooled behavior (Hikari's connectionTimeout governs pool-checkout wait, not TCP connect), so it's not a regression — just worth being aware that a listener reconnect against an unreachable DB can block the daemon thread until the OS TCP timeout if the URL carries no connectTimeout.
  • The Postgres driver auto-registers with DriverManager via SPI and is already loaded (the class imports org.postgresql.PGConnection), so the DriverManager lookup will resolve the driver. ✅

Overall this is a clean, well-scoped fix with good test coverage of the branch that matters. The only thing worth confirming before merge is the connection-properties question above.
· issue-36934-pubsub-unpooled-listen-connection

@fabrizzio-dotCMS

Copy link
Copy Markdown
Member Author

Controlled first-boot verification — clean

Re-ran the exact path where the warning was originally observed: empty database, so Task00004LoadStarter runs and the starter load drives OSGIUtilQueuingPubSubWrapper.startPGListener.<init>. The patched classes were placed in the container before it was started, so the very first boot ran with the fix.

23:35:40  INFO  startup.StartupTasksExecutor  - Running Startup Tasks : Task00004LoadStarter
23:35:43  INFO  dotpubsub.JDBCPubSubImpl      - Opening a dedicated connection for the Postgres
                                                pub/sub listener, outside the Hikari pool.
23:35:53  INFO  startup.Catalina              - Server startup in [29611] milliseconds

After 8 minutes of uptime — well past the 300 s default DB_LEAK_DETECTION_THRESHOLD that produced the original warning at roughly the 5-minute mark:

Signal Count
Starter load actually ran (so the case was exercised) 1
Apparent connection leak detected 0
Dedicated connection taken 1

Before the fix, the same stack logged the warning once per starter-load boot, with the stack trace rooted at JDBCPubSubImpl$PGListener.<init>.

Environment: PostgreSQL 18 (pgvector/pgvector:pg18), dotCMS built from this branch, default leak-detection threshold — no tuning to make the result look better.

@fabrizzio-dotCMS
fabrizzio-dotCMS requested a review from wezell August 6, 2026 23:54
@wezell

wezell commented Aug 7, 2026

Copy link
Copy Markdown
Member

Why are we doing this? The Hikari warning is noise. It’s not a real connection leak; it’s saying a possible connection leak.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Pub/sub PGListener holds a pooled Hikari connection for the JVM lifetime, triggering a false leak warning on every boot

2 participants