9.6 KiB
19 — HikariCP connection pooling
← Previous: 18 — Ehcache 3 L2 cache configuration | Back to README →
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), source:
SpringAutoConfiguredHikariTest
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, 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.
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();
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.
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.
Trap: case matters.hibernate.hikari.maximumPoolSizemaps toHikariConfig#setMaximumPoolSizebecause HikariCP's own property loader does exact, case-sensitive bean-property matching --hibernate.hikari.maximumpoolsize(all lowercase) silently does nothing rather than failing loudly.
- Going deeper:
hibernate.javax.cache.uri's own classpath-prefix trap 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:
config.setMaximumPoolSize(1);
config.setConnectionTimeout(1000);
Connection held = dataSource.getConnection();
// a second dataSource.getConnection() from here...
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)
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
maximumPoolSizeis 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 (rel="nofollow") argues for pool sizes much smaller than most defaults assume, based onconnections = ((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.
config.setLeakDetectionThreshold(500); // under the floor
HikariDataSource ds = new HikariDataSource(config); // validate() runs right here
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.
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
Trap: 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.
- Going deeper: the leaked-connection warning includes a captured stack trace of the original checkout site (visible in the full transcript) -- 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
DataSourcebean type, the waySpringAutoConfiguredHikariTestdoes 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
connectionTimeoutdeliberately: it's what turns pool exhaustion from "the caller hangs" into "the caller gets a typed, catchable exception in a bounded time." - Set
leakDetectionThresholdto 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 | Back to README →