mansart-pool is a JDBC connection pool implemented in pure Java 25, with no external dependency, with a lock-free Semaphore + ConcurrentLinkedDeque and a housekeeper on a virtual thread. It is fully decoupled from the rest of Mansart: its only contract is javax.sql.DataSource.
Mission
-
Provide a performant, simple, configurable
DataSource. -
Be virtual-thread-native — no
synchronizedcontention on the hot path. -
Stay optional — the user can plug
mansart-jakarta-dataonto anyDataSource(Hikari, Tomcat-JDBC, c3p0).
Position in the workspace
mansart-pool is a standalone peer of the other Mansart modules. It depends neither on mansart-jakarta-data nor on mansart-transactions. Conversely, mansart-jakarta-data and mansart-transactions may use it but do not depend on it at compile time.
Modules
| Sub-module | Role |
|---|---|
|
|
|
|
Public API
PoolConfig config = PoolConfig.builder()
.jdbcUrl("jdbc:postgresql://db.local:5432/shop")
.username("shop").password("...")
.minIdle(4).maxSize(32)
.acquireTimeout(Duration.ofSeconds(5))
.idleTimeout(Duration.ofMinutes(10))
.maxLifetime(Duration.ofMinutes(30))
.validation(ValidationMode.ON_BORROW)
.leakDetectionThreshold(Duration.ofSeconds(30))
.build();
DataSource ds = MansartDataSource.of(config);
PoolMetrics metrics = ((MansartDataSource) ds).snapshot();
See Reference for the full property table.
Runtime architecture
MansartDataSource (impl DataSource)
│
├── ConcurrentLinkedDeque<PooledEntry> idle (lock-free LIFO)
├── Set<PooledEntry> inUse (ConcurrentHashMap.newKeySet)
├── Semaphore permits (size = maxSize, fair=false)
└── housekeeper (virtual thread)
Acquire (getConnection()):
-
permits.acquire(acquireTimeout)— hard bound. -
idle.poll()— hot LIFO. -
If valid (
ON_BORROWmode:validationQuerywhen set, elseConnection.isValid) →inUse.add; otherwise → close physically + create new. -
Return: the
PooledConnectionwrapper, whoseclose()returns to the pool.
Release (Connection.close() on the wrapper):
-
Restore defaults — rollback if dirty,
autoCommit=true, default isolation. -
inUse.remove(entry). -
Still valid and younger than
maxLifetime→idle.offerFirst(entry); otherwise → close physically. -
permits.release().
The wrapper is a plain hand-written PooledConnection implements Connection — not java.lang.reflect.Proxy (no per-call invoke overhead), not generated bytecode. close() returns the connection to the pool instead of closing it. See Internals.
Transactions integration
The pool carries no transactional context itself: JTA enlistment is the job of mansart-transactions (its JDBC bridge enlists the connection with the transaction manager). See mansart-transactions.
Metrics
PoolMetrics (lock-free snapshot via MansartDataSource.snapshot()):
-
active(),idle(),waiting(). -
totalBorrows(),totalTimeouts(),totalLeaks(). -
meanBorrowDuration().
Micrometer / Prometheus bridge on the backlog (MP3).
Leak detection
leakDetectionThreshold(Duration) records, on acquire, a stack trace inside the PooledConnection. If the connection stays borrowed longer than the threshold, the housekeeper logs a WARNING with the stack — immediate diagnosis of the offending code path. Enable in staging, disable in production if the overhead is measured.
Comparison vs HikariCP
-
Threading — Mansart virtual-thread-native, no pinning under Loom load. HikariCP uses
synchronizedblocks that pin. -
Dependencies — Mansart zero-dep. HikariCP depends on SLF4J + javassist (transitively via micrometer).
-
AOT — Mansart compatible with GraalVM native-image and Leyden CDS without configuration. HikariCP needs
--initialize-at-build-timefor javassist. -
API — very close (
HikariConfig→PoolConfig). See Migration.
Numbers: see mansart-pool BENCH.md (2026-05-06 baseline vs HikariCP).
Roadmap
-
✅ MP1 — API + skeleton.
-
⏳ MP2 — Core implementation (
MansartDataSource, semaphore, deque, validation). -
⏳ MP3 — Metrics + leak detection.
-
⏳ MP4 —
mansart-jakarta-dataintegration (transparent for the user).
See mansart-pool PLAN.md.