Add Hibernate 7 batches 2-6, batch 7, and batch 8: mapping styles, JPA annotations, natural IDs, @Immutable, stored procedures, in-memory test databases, JNDI mocking, proxies, associations, temporal mapping, named queries, HQL, Criteria API, EntityManager bootstrapping, Ehcache 3 L2 cache configuration, HikariCP connection pooling, Hibernate Validator CDI integration, aggregate functions, sorting, pagination, interceptors, and Hibernate Search 8 (Hibernate 7.4.5.Final + Spring Boot 4.1.1 + JDK 25)
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user