146 lines
9.6 KiB
Markdown
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)
|