Files
hibernate-demo/docs/19-hikaricp-connection-pooling.md

146 lines
9.6 KiB
Markdown

# 19 — HikariCP connection pooling
[← Previous: 18 — Ehcache 3 L2 cache configuration](18-ehcache-l2-configuration.md) | [Back to README →](../README.md)
Backs the rewrite of ankurm.com post 4886 (HikariCP connection pooling).
## The claim that needs no repo at all to check, and is still worth checking
"Spring Boot uses HikariCP by default" is one of those facts everyone repeats and almost no one
verifies against their own project. This repo's own `application.yml` names no connection pool
at all -- no `spring.datasource.type`, no `spring.datasource.hikari.*` block -- and the `DataSource`
bean Spring Boot 4.1.1 hands out anyway really is a `HikariDataSource`:
```
RESULT[hikari-spring-default]: dataSource class=com.zaxxer.hikari.HikariDataSource | pool name=HikariPool-1 | maximumPoolSize=10 | minimumIdle=10 | connectionTimeout=30000ms | idleTimeout=600000ms -- these are HikariCP's own built-in defaults (maximumPoolSize=10, minimumIdle defaults to maximumPoolSize), not anything this project set.
```
[(full transcript)](output/19-spring-default-hikari.txt), source:
[`SpringAutoConfiguredHikariTest`](../src/test/java/com/ankurm/hibernatedemo/hikari/SpringAutoConfiguredHikariTest.java)
Confirmed against `HikariConfig.class` itself (disassembled from `HikariCP-7.0.2.jar`):
`maxPoolSize` defaults to `10`, `minIdle` defaults to `-1` (meaning "unset"), and `validate()`
resolves an unset `minIdle` up to whatever `maxPoolSize` ends up being -- which is exactly the
`minimumIdle=10` this test observes with nothing configured.
## Bootstrapping HikariCP with zero Spring involved
The original article's own example is plain Hibernate, no Spring Boot. Reproducing that
faithfully needed one more dependency this repo didn't already have:
[`org.hibernate.orm:hibernate-hikaricp`](../pom.xml), the integration jar that teaches a raw
`StandardServiceRegistry` how to talk to HikariCP at all -- test-scoped, since the Spring-managed
chapters never need it.
```java
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.provider_class", "hikari")
.applySetting("hibernate.hikari.maximumPoolSize", "7")
.applySetting("hibernate.hikari.poolName", "hibernate-demo-ch19-pool")
.applySetting("hibernate.hikari.connectionTimeout", "5000")
// ... driver_class, url, username as usual
.build();
```
[`HikariRawBootstrapTest.java`](../src/test/java/com/ankurm/hibernatedemo/hikari/HikariRawBootstrapTest.java)
```
RESULT[hikari-raw-bootstrap]: ConnectionProvider class=org.hibernate.hikaricp.internal.HikariCPConnectionProvider | isUnwrappableAs(HikariDataSource)=true
RESULT[hikari-raw-bootstrap-config]: poolName=hibernate-demo-ch19-pool | maximumPoolSize=7 | connectionTimeout=5000ms -- every value traces back to a hibernate.hikari.* setting passed into StandardServiceRegistryBuilder, with zero Spring involved.
```
[(full transcript)](output/19-raw-bootstrap.txt)
Two things confirmed by disassembling `hibernate-hikaricp-7.4.5.Final.jar` rather than assumed
from the property name: `hibernate.connection.provider_class` accepts the short aliases `hikari`
or `hikaricp`, not only the fully-qualified class name (both are registered in
`StrategyRegistrationProviderImpl`); and every `hibernate.hikari.*`-prefixed setting has that
prefix stripped and is handed to `new HikariConfig(properties)` as-is -- `HikariConfigurationUtil`
declares the prefix itself as the public constant `CONFIG_PREFIX = "hibernate.hikari."`. That
means any `HikariConfig` setter is reachable this way, not just the handful mentioned here.
<blockquote style="border-left:4px solid #d97757;background:#fdf3ee;padding:0.75rem 1.25rem;margin:1.5rem 0;">
<strong>Trap:</strong> case matters. <code>hibernate.hikari.maximumPoolSize</code> maps to
<code>HikariConfig#setMaximumPoolSize</code> because HikariCP's own property loader does exact,
case-sensitive bean-property matching -- <code>hibernate.hikari.maximumpoolsize</code> (all
lowercase) silently does nothing rather than failing loudly.
</blockquote>
- Going deeper: [`hibernate.javax.cache.uri`'s own classpath-prefix trap](18-ehcache-l2-configuration.md) is a different config-loading mechanism worth contrasting with this one -- one strips a prefix and applies bean-property reflection, the other passes a raw string straight to a resource loader.
## Failure mode 1: pool exhaustion is a named exception, not a hang
Lead with what actually breaks. Every connection in the pool checked out, and one more request
comes in:
```java
config.setMaximumPoolSize(1);
config.setConnectionTimeout(1000);
Connection held = dataSource.getConnection();
// a second dataSource.getConnection() from here...
```
[`HikariPoolExhaustionTest.java`](../src/test/java/com/ankurm/hibernatedemo/hikari/HikariPoolExhaustionTest.java)
```
RESULT[hikari-pool-exhaustion]: maximumPoolSize=1, connectionTimeout=1000ms | second getConnection() waited=1004ms before throwing java.sql.SQLTransientConnectionException: exhaustion-pool - Connection is not available, request timed out after 1002ms (total=1, active=1, idle=0, waiting=0)
```
[(full transcript)](output/19-pool-exhaustion.txt)
This is worth knowing verbatim because it's exactly what you'll grep application logs for: a
`SQLTransientConnectionException` (a real, distinct exception type -- catchable separately from
other SQL errors), naming the pool by name, stating the timeout, and reporting the pool's live
`total`/`active`/`idle`/`waiting` counts at the moment it gave up. `SQLTransientConnectionException`
being a `java.sql.SQLTransientException` also means a retry framework that specifically retries
transient SQL errors will treat this one as retryable by default -- worth confirming that's
actually what you want under sustained load, rather than assuming it.
- Going deeper: raising `maximumPoolSize` is the obvious fix, but it isn't free -- each connection is a real OS thread and socket on both ends; [HikariCP's own sizing guidance](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing) (`rel="nofollow"`) argues for pool sizes much smaller than most defaults assume, based on `connections = ((core_count * 2) + effective_spindle_count)`.
## Failure mode 2: leak detection has a floor the article never mentioned
"Set `leakDetectionThreshold`" is the standard advice for catching code that checks out a
connection and forgets to close it. What isn't standard advice: HikariCP enforces a hard floor on
that setting, and violating it fails silently rather than loudly.
```java
config.setLeakDetectionThreshold(500); // under the floor
HikariDataSource ds = new HikariDataSource(config); // validate() runs right here
```
[`HikariLeakDetectionTest.java`](../src/test/java/com/ankurm/hibernatedemo/hikari/HikariLeakDetectionTest.java)
```
RESULT[hikari-leak-threshold-floor]: requested leakDetectionThreshold=500ms | actual leakDetectionThreshold after construction=0ms | logged warnings=1 | message=HikariPool-1 - leakDetectionThreshold is less than 2000ms or more than maxLifetime, disabling it. -- HikariCP does not clamp 500ms up to 2000ms, it disables leak detection entirely and logs a WARN naming the reason.
```
[(full transcript)](output/19-leak-detection.txt)
Confirmed by disassembling `HikariConfig.class`: any value under 2000ms (and any value above
`maxLifetime`, when `maxLifetime` is set) is reset straight to `0` -- disabled -- inside
`validate()`, which runs synchronously the moment `new HikariDataSource(config)` is called. It
does not round up to 2000ms and it does not throw; it just quietly turns the feature off and logs
one WARN through `HikariConfig`'s own logger. A value at or above the floor behaves as documented:
```
RESULT[hikari-leak-detection]: leakDetectionThreshold=2000ms | logger=com.zaxxer.hikari.pool.ProxyLeakTask | level=WARN | message=Connection leak detection triggered for conn1: url=jdbc:h2:mem:hikarileak user=SA on thread main, stack trace follows
```
[(same transcript)](output/19-leak-detection.txt)
<blockquote style="border-left:4px solid #d97757;background:#fdf3ee;padding:0.75rem 1.25rem;margin:1.5rem 0;">
<strong>Trap:</strong> if you copy a "fast" leak-detection setting from a unit test into
production config -- 200ms, 500ms, anything under two seconds -- HikariCP accepts it without
error and simply never checks for leaks at all. The only sign is a single WARN logged once, at
startup, that's easy to miss in a noisy boot log.
</blockquote>
- Going deeper: the leaked-connection warning includes a captured stack trace of the original checkout site (visible in [the full transcript](output/19-leak-detection.txt)) -- that's what makes this setting worth turning on in staging even when it costs a little overhead, since it points straight at the offending code path rather than just the symptom.
## Production checklist
- Don't trust "HikariCP is the default" from memory for a project you haven't checked -- confirm
the actual `DataSource` bean type, the way `SpringAutoConfiguredHikariTest` does here.
- Size the pool from real concurrency needs, not a round number -- HikariCP's own guidance argues
for pools smaller than most defaults assume.
- Set `connectionTimeout` deliberately: it's what turns pool exhaustion from "the caller hangs"
into "the caller gets a typed, catchable exception in a bounded time."
- Set `leakDetectionThreshold` to at least 2000ms if you set it at all -- anything lower is
silently a no-op, not a more sensitive check.
- In a non-Spring bootstrap, remember `hibernate.hikari.*` property names are case-sensitive bean
property names, not free-form config keys.
[← Previous: 18 — Ehcache 3 L2 cache configuration](18-ehcache-l2-configuration.md) | [Back to README →](../README.md)