package dev.caskeleton.adapter.outbound.persistence.platform.pool; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import dev.caskeleton.adapter.outbound.persistence.testkit.pool.PoolMeasurement; import dev.caskeleton.adapter.outbound.persistence.testkit.postgresql.PostgreSqlContainerFactory; import dev.caskeleton.adapter.outbound.persistence.testkit.postgresql.PostgreSqlVersion; import java.sql.Connection; import java.time.Duration; import java.time.Instant; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.postgresql.PostgreSQLContainer; /** Analysis-only probe: what a real saturated Hikari pool reports, and how long acquisition waits. */ class AnalysisPoolSaturationProbe { private static PostgreSQLContainer container; @BeforeAll static void startServer() { container = PostgreSqlContainerFactory.create(PostgreSqlVersion.PG_16); container.start(); } @AfterAll static void stopServer() { if (container != null) { container.stop(); } } @Test void realSaturatedPoolPendingCount() throws Exception { try (HikariDataSource pool = pool(2, 3000L)) { Connection first = pool.getConnection(); Connection second = pool.getConnection(); CountDownLatch waiterStarted = new CountDownLatch(1); AtomicReference waiterOutcome = new AtomicReference<>("none"); Thread waiter = new Thread( () -> { waiterStarted.countDown(); try (Connection blocked = pool.getConnection()) { waiterOutcome.set("acquired"); } catch (Exception refused) { waiterOutcome.set(refused.getClass().getSimpleName()); } }); waiter.start(); waiterStarted.await(5, TimeUnit.SECONDS); Thread.sleep(500L); var bean = pool.getHikariPoolMXBean(); PoolMeasurement measurement = new PoolMeasurement( bean.getActiveConnections(), bean.getIdleConnections(), bean.getThreadsAwaitingConnection(), Duration.ZERO); System.out.println("realPool.active=" + measurement.active()); System.out.println("realPool.idle=" + measurement.idle()); System.out.println("realPool.pending=" + measurement.pending()); System.out.println("realPool.saturated=" + measurement.saturated()); first.close(); waiter.join(10_000L); System.out.println("realPool.waiterOutcome=" + waiterOutcome.get()); second.close(); } } @Test void acquireWaitAgainstTheConfiguredTimeout() throws Exception { long configuredTimeoutMillis = 500L; try (HikariDataSource pool = pool(2, configuredTimeoutMillis)) { Connection first = pool.getConnection(); Connection second = pool.getConnection(); Instant startedAt = Instant.now(); String outcome; try (Connection refused = pool.getConnection()) { outcome = "acquired"; } catch (Exception failure) { outcome = failure.getClass().getSimpleName(); } Duration waited = Duration.between(startedAt, Instant.now()); System.out.println("acquire.configuredTimeoutMillis=" + configuredTimeoutMillis); System.out.println("acquire.observedWaitMillis=" + waited.toMillis()); System.out.println("acquire.outcome=" + outcome); System.out.println( "acquire.assertedUpperBoundMillis=" + (configuredTimeoutMillis + 2000L)); first.close(); second.close(); } } @Test void handBuiltFixtureStateVersusRealPoolState() { PoolMeasurement fixture = new PoolMeasurement(4, 2, 3, Duration.ofMillis(80)); System.out.println("fixture.active=" + fixture.active()); System.out.println("fixture.idle=" + fixture.idle()); System.out.println("fixture.pending=" + fixture.pending()); System.out.println("fixture.saturated=" + fixture.saturated()); System.out.println("fixture.total=" + fixture.total()); int concurrentThreads = 8; int maxRequiresNewDepth = 1; System.out.println( "formula.required=" + (concurrentThreads * (1 + maxRequiresNewDepth) + 1)); } private static HikariDataSource pool(int size, long connectionTimeoutMillis) { HikariConfig config = new HikariConfig(); config.setJdbcUrl(container.getJdbcUrl()); config.setUsername(container.getUsername()); config.setPassword(container.getPassword()); config.setMaximumPoolSize(size); config.setConnectionTimeout(connectionTimeoutMillis); return new HikariDataSource(config); } }