From e746d824a5353fbc1277d12998c8f2e3ab1878c9 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 12 Jun 2026 14:45:21 +1200 Subject: [PATCH] docs: Major update, add configuration-reference.md etc --- README.md | 3 + docs/guides/README.md | 3 + docs/guides/configuration-reference.md | 187 +++++++++++++++ docs/guides/create-datasource-pool.md | 7 + docs/guides/monitoring-pool-metrics.md | 167 +++++++++++++ .../troubleshooting-connection-leaks.md | 220 ++++++++++++++++++ 6 files changed, 587 insertions(+) create mode 100644 docs/guides/configuration-reference.md create mode 100644 docs/guides/monitoring-pool-metrics.md create mode 100644 docs/guides/troubleshooting-connection-leaks.md diff --git a/README.md b/README.md index 9a162e6..c3ac1ac 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ This project includes step-by-step guides for common datasource configuration sc **See [docs/guides/README.md](docs/guides/README.md)** for: - [Creating DataSource pools](docs/guides/create-datasource-pool.md) — basic pools, read-only pools, Kubernetes deployment, AWS Lambda, and configuration reference - [Connection Validation Best Practices](docs/guides/connection-validation-best-practices.md) — how to configure heartbeat validation and why explicit heartbeatSql is rarely needed +- [Configuration Reference](docs/guides/configuration-reference.md) — every builder setting, its default, and the equivalent property key for external configuration +- [Troubleshooting Connection Leaks & Pool Exhaustion](docs/guides/troubleshooting-connection-leaks.md) — diagnosing pool exhaustion, leaks, and common warnings +- [Monitoring Pool Metrics](docs/guides/monitoring-pool-metrics.md) — reading PoolStatus metrics, pool state, and outage alerts - [AWS Aurora with dual DataSources](docs/guides/aws-aurora-read-write-split.md) — read-write endpoint separation with Ebean ORM integration - Instructions for AI agents to discover and follow these guides diff --git a/docs/guides/README.md b/docs/guides/README.md index 3ee79b9..0a26add 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -10,6 +10,9 @@ Step-by-step guides covering common scenarios for creating and configuring conne |-------|-------------| | [Create a DataSource Pool](create-datasource-pool.md) | Basic pool creation, read-only pools, Kubernetes configuration, AWS Lambda setup, and configuration reference | | [Connection Validation Best Practices](connection-validation-best-practices.md) | How connection validation works; when and how to configure heartbeat validation; why explicit heartbeatSql is rarely needed | +| [Configuration Reference](configuration-reference.md) | Complete reference of every builder setting, its default, and the equivalent property key for file-based / external configuration | +| [Troubleshooting Connection Leaks & Pool Exhaustion](troubleshooting-connection-leaks.md) | Diagnose `ConnectionPoolExhaustedException`, connection leaks, captureStackTrace, leakTimeMinutes, and common warnings | +| [Monitoring Pool Metrics](monitoring-pool-metrics.md) | Read `PoolStatus` metrics, check pool state, and wire up outage alerts and borrow/return listeners | ## AWS Aurora diff --git a/docs/guides/configuration-reference.md b/docs/guides/configuration-reference.md new file mode 100644 index 0000000..767f975 --- /dev/null +++ b/docs/guides/configuration-reference.md @@ -0,0 +1,187 @@ +# Guide: Configuration Reference + +## Purpose + +A complete reference of every `DataSourceBuilder` setting, its default value, and the equivalent +property key for file-based / external configuration. Use this alongside the task-focused guides +(pool creation, read-only pools, validation, Aurora) when you need to know exactly what a setting +does or what it defaults to. + +--- + +## Two ways to configure + +### 1. Programmatic (builder) + +```java +DataSourcePool pool = DataSourcePool.builder() + .name("mypool") + .url("jdbc:postgresql://localhost:5432/myapp") + .username("app_user") + .password("password") + .minConnections(5) + .maxConnections(50) + .build(); +``` + +### 2. Properties (external configuration) + +The builder can load settings from `java.util.Properties`. There are three entry points: + +```java +Properties props = ...; // loaded from file, env, avaje-config, Spring, etc. + +// (a) no prefix: keys are "username", "url", ... +DataSourcePool pool = DataSourcePool.builder().load(props).build(); + +// (b) custom prefix: keys are "my-db.username", "my-db.url", ... +DataSourcePool pool = DataSourcePool.builder().load(props, "my-db").build(); + +// (c) "datasource.." prefix: keys are "datasource.hr.username", ... +DataSourcePool pool = DataSourcePool.builder().loadSettings(props, "hr").build(); +``` + +Example properties file using the `loadSettings` convention with pool name `hr`: + +```properties +datasource.hr.username=app_user +datasource.hr.password=password +datasource.hr.url=jdbc:postgresql://localhost:5432/myapp +datasource.hr.minConnections=5 +datasource.hr.maxConnections=50 +datasource.hr.leakTimeMinutes=30 +``` + +> When used through **Ebean ORM**, these `datasource..*` properties are typically placed in +> `application.yaml` / `application.properties` and loaded for you via avaje-config. + +Property keys are matched case-insensitively. + +--- + +## Connection settings + +| Builder method | Property key | Default | Description | +|----------------|--------------|---------|-------------| +| `url(String)` | `url` (or `databaseUrl`) | – | JDBC URL. | +| `username(String)` | `username` | – | Database username. | +| `password(String)` | `password` | – | Database password. | +| `readOnlyUrl(String)` | `readOnlyUrl` | – | Optional separate URL for read-only connections. | +| `driver(...)` / `driver(String)` | `driver` (or `databaseDriver`) | auto from URL | JDBC driver class / instance. | +| `schema(String)` | `schema` | driver default | Default schema applied to connections. | +| `catalog(String)` | `catalog` | driver default | Default catalog applied to connections. | +| `isolationLevel(int)` | `isolationLevel` | `READ_COMMITTED` | Transaction isolation level. Property accepts names e.g. `READ_COMMITTED`. | +| `autoCommit(boolean)` | `autoCommit` | `false` | Auto-commit mode for pooled connections. | +| `readOnly(boolean)` | `readOnly` | `false` | Mark connections read-only (optimises read workloads). | +| `applicationName(String)` | `applicationName` | – | Application name reported to the driver where supported. | +| `clientInfo(Properties)` | `clientInfo` | – | Client info properties (semicolon separated `key=value` in properties form). | +| `customProperties(Map)` / `addProperty(...)` | `customProperties` | – | Extra JDBC driver connection properties (semicolon separated `key=value` in properties form). | +| `initSql(List)` | `initSql` | – | SQL run on each new connection (semicolon separated statements in properties form). See per-connection init below. | + +## Pool sizing + +| Builder method | Property key | Default | Description | +|----------------|--------------|---------|-------------| +| `minConnections(int)` | `minConnections` | `2` | Minimum connections maintained in the pool. | +| `initialConnections(int)` | `initialConnections` | = `minConnections` | Connections created on startup. Set higher than min for smooth warm-up (Kubernetes). | +| `maxConnections(int)` | `maxConnections` | `200` | Maximum connections. Threads block (up to `waitTimeout`) when this is reached. | + +## Timeouts, trimming and ageing + +| Builder method | Property key | Default | Description | +|----------------|--------------|---------|-------------| +| `waitTimeoutMillis(int)` | `waitTimeout` | `1000` | Millis a thread waits for a free connection once the pool is at max before throwing `ConnectionPoolExhaustedException`. | +| `maxInactiveTimeSecs(int)` | `maxInactiveTimeSecs` | `300` | Idle seconds after which a free connection can be trimmed back towards `minConnections`. | +| `maxAgeMinutes(int)` | `maxAgeMinutes` | `0` (unlimited) | Maximum age of a connection before it is trimmed regardless of activity. | +| `trimPoolFreqSecs(int)` | `trimPoolFreqSecs` | `59` | How often the background trim check runs. | + +## Health checks / heartbeat + +| Builder method | Property key | Default | Description | +|----------------|--------------|---------|-------------| +| `validateOnHeartbeat(boolean)` | `validateOnHeartbeat` | `true` (`false` in AWS Lambda) | Enable the background heartbeat that validates the pool. | +| `heartbeatFreqSecs(int)` | *(builder only)* | `30` | How often the heartbeat runs. | +| `heartbeatTimeoutSeconds(int)` | `heartbeatTimeoutSeconds` | `30` | Query timeout for the heartbeat validation. | +| `heartbeatSql(String)` | `heartbeatSql` | `Connection.isValid()` / platform default | Explicit validation SQL. Rarely needed — see the validation guide. | +| `heartbeatMaxPoolExhaustedCount(int)` | *(builder only)* | `10` | Consecutive heartbeat pool-exhaustion detections before the pool is reset (leak recovery). | + +See [Connection Validation Best Practices](connection-validation-best-practices.md) for details. + +## Leak detection / diagnostics + +| Builder method | Property key | Default | Description | +|----------------|--------------|---------|-------------| +| `leakTimeMinutes(int)` | `leakTimeMinutes` | `30` | A busy (checked-out) connection older than this is treated as a leak and force-closed during a pool reset. | +| `captureStackTrace(boolean)` | `captureStackTrace` | `false` | Capture the stack trace when a connection is obtained, to locate leaks. Has a performance cost. | +| `maxStackTraceSize(int)` | `maxStackTraceSize` | `5` | Number of stack frames reported for busy connections. | + +See [Troubleshooting Connection Leaks & Pool Exhaustion](troubleshooting-connection-leaks.md). + +## Statement caching + +| Builder method | Property key | Default | Description | +|----------------|--------------|---------|-------------| +| `pstmtCacheSize(int)` | `pstmtCacheSize` | `300` | `PreparedStatement` cache size, per connection. | +| `cstmtCacheSize(int)` | `cstmtCacheSize` | `20` | `CallableStatement` cache size, per connection. | + +## Lifecycle / startup + +| Builder method | Property key | Default | Description | +|----------------|--------------|---------|-------------| +| `failOnStart(boolean)` | `failOnStart` | `true` | When `false`, the pool starts even if the database is unavailable (it recovers later via heartbeat). | +| `offline(boolean)` | `offline` | `false` | Start the pool offline (no connections created until `online()`). | +| `shutdownOnJvmExit(boolean)` | `shutdownOnJvmExit` | `false` | Register a JVM shutdown hook to close the pool on exit. | +| `enforceCleanClose(boolean)` | `enforceCleanClose` | `false` | Throw if a dirty (uncommitted) connection is closed. Recommended in tests. See [issue #116](https://github.com/ebean-orm/ebean-datasource/issues/116). | + +## Hooks and extension points + +| Builder method | Property key | Description | +|----------------|--------------|-------------| +| `connectionInitializer(NewConnectionInitializer)` | *(builder only)* | Hook called when each new connection is created (`preInitialize` / `postInitialize`). | +| `defaultConnectionInitializer(NewConnectionInitializer)` | *(builder only)* | Fallback initializer used only if one is not otherwise set. | +| `listener(DataSourcePoolListener)` | *(builder only)* | Callbacks on borrow (`onAfterBorrowConnection`) and return (`onBeforeReturnConnection`). | +| `poolListener(String)` | `poolListener` | Class name of a `DataSourcePoolListener` to instantiate. | +| `alert(DataSourceAlert)` | *(builder only)* | Callbacks for `dataSourceUp` / `dataSourceDown` (outage alerting). See [Monitoring](monitoring-pool-metrics.md). | + +## Per-connection initialization + +Use `initSql` for simple statements, or a `NewConnectionInitializer` for programmatic control. This +is the right place to set things like a Postgres `search_path` or a per-session `statement_timeout`. + +```java +DataSourcePool pool = DataSourcePool.builder() + .name("mypool") + .url("jdbc:postgresql://localhost:5432/myapp") + .username("app_user") + .password("password") + .initSql(List.of("set search_path to app, public")) + .connectionInitializer(new NewConnectionInitializer() { + @Override + public void postInitialize(Connection connection) { + try (Statement st = connection.createStatement()) { + st.execute("set statement_timeout to '30s'"); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + }) + .build(); +``` + +--- + +## Deprecated `setXxx` methods + +Many settings historically used a `setXxx` name (e.g. `setMinConnections`). These remain for +backwards compatibility but are deprecated — prefer the fluent forms shown above +(`minConnections`, `maxConnections`, `heartbeatFreqSecs`, etc.). + +--- + +## Next Steps + +- [Create a DataSource Pool](create-datasource-pool.md) +- [Connection Validation Best Practices](connection-validation-best-practices.md) +- [Troubleshooting Connection Leaks & Pool Exhaustion](troubleshooting-connection-leaks.md) +- [Monitoring Pool Metrics](monitoring-pool-metrics.md) +- Full Javadoc: [io.ebean:ebean-datasource](https://javadoc.io/doc/io.ebean/ebean-datasource) diff --git a/docs/guides/create-datasource-pool.md b/docs/guides/create-datasource-pool.md index 49f7098..a539bde 100644 --- a/docs/guides/create-datasource-pool.md +++ b/docs/guides/create-datasource-pool.md @@ -164,6 +164,10 @@ DataSourcePool pool = DataSourcePool.builder() ## Configuration Reference +> For the **complete** list of settings, defaults, and property keys see the +> [Configuration Reference](configuration-reference.md). The table below covers the most common +> settings. + ### Common Settings | Setting | Default | Purpose | @@ -217,6 +221,9 @@ DataSourcePool pool = DataSourcePool.builder() ## Next Steps +- Read the [Configuration Reference](configuration-reference.md) for all available settings and property keys +- [Troubleshooting Connection Leaks & Pool Exhaustion](troubleshooting-connection-leaks.md) +- [Monitoring Pool Metrics](monitoring-pool-metrics.md) - Read the [README](../../README.md) for more information about the connection pool - Check the [ebean-datasource GitHub repository](https://github.com/ebean-orm/ebean-datasource) for latest updates - Consult the [DataSourceBuilder API documentation](https://javadoc.io/doc/io.ebean/ebean-datasource) for all available configuration options diff --git a/docs/guides/monitoring-pool-metrics.md b/docs/guides/monitoring-pool-metrics.md new file mode 100644 index 0000000..c8833ca --- /dev/null +++ b/docs/guides/monitoring-pool-metrics.md @@ -0,0 +1,167 @@ +# Guide: Monitoring Pool Metrics + +## Purpose + +This guide explains how to observe the health of a `DataSourcePool` at runtime: reading pool metrics +via `PoolStatus`, checking online/up state, and receiving outage notifications via `DataSourceAlert`. + +--- + +## Reading pool metrics with `PoolStatus` + +Call `status(reset)` on the pool to get a point-in-time snapshot: + +```java +PoolStatus status = pool.status(false); // false = read without resetting counters + +System.out.printf( + "min=%d max=%d free=%d busy=%d waiting=%d highWaterMark=%d hitCount=%d waitCount=%d%n", + status.minSize(), status.maxSize(), status.free(), status.busy(), + status.waiting(), status.highWaterMark(), status.hitCount(), status.waitCount()); +``` + +### Metric meanings + +| Metric | Meaning | +|--------|---------| +| `minSize()` | Configured minimum connections. | +| `maxSize()` | Configured maximum connections. | +| `free()` | Connections currently idle and available. | +| `busy()` | Connections currently checked out (in use). | +| `size()` | `free() + busy()` — total connections in the pool. | +| `waiting()` | Threads currently blocked waiting for a connection. | +| `highWaterMark()` | Peak `busy()` value seen since the last reset. | +| `hitCount()` | Number of times a connection was requested from the pool. | +| `waitCount()` | Number of times a thread had to wait (pool was full). | +| `totalAcquireMicros()` | Total time spent acquiring connections (micros). | +| `totalWaitMicros()` | Total time threads spent waiting when the pool was full (micros). | +| `maxAcquireMicros()` | Slowest single acquire (micros). | +| `meanAcquireNanos()` | Mean acquire time (nanos) — typically in the ballpark of ~150 nanos. | + +### What to watch for + +- **`highWaterMark()` approaching `maxSize()`** — the pool is close to exhaustion; consider raising + `maxConnections` or investigate a leak (see + [Troubleshooting Connection Leaks & Pool Exhaustion](troubleshooting-connection-leaks.md)). +- **Non-zero / rising `waitCount()` and `waiting()`** — threads are blocking on the pool; demand + exceeds capacity. +- **`busy()` not dropping when traffic falls** — a likely connection leak. +- **High `maxAcquireMicros()` / `meanAcquireNanos()`** — acquiring is slow, often a sign of + contention or repeated validation/recreation. + +### Resetting counters for periodic sampling + +Pass `true` to reset the cumulative counters (`highWaterMark`, `hitCount`, `waitCount`, acquire/wait +times) after reading. This is ideal for emitting metrics on a fixed interval: + +```java +// every 60s, e.g. from a scheduled task +PoolStatus status = pool.status(true); // read and reset, so the next window starts clean +meterRegistry.gauge("db.pool.busy", status.busy()); +meterRegistry.gauge("db.pool.free", status.free()); +meterRegistry.gauge("db.pool.highWaterMark", status.highWaterMark()); +meterRegistry.gauge("db.pool.waitCount", status.waitCount()); +``` + +--- + +## Checking pool state + +```java +pool.isOnline(); // true once the pool has been brought online (connections created) +pool.isDataSourceUp(); // false if the pool has detected the database is down +pool.size(); // current number of connections (free + busy) + +SQLException reason = pool.dataSourceDownReason(); // non-null when the pool is down +``` + +- `isOnline()` reflects the lifecycle state (`online()` / `offline()`). +- `isDataSourceUp()` reflects connectivity health detected by the heartbeat. + +--- + +## Outage notifications with `DataSourceAlert` + +Register a `DataSourceAlert` to be notified when the pool detects the database has gone down and when +it recovers. This is useful for wiring pool health into your alerting/paging system. + +```java +DataSourceAlert alert = new DataSourceAlert() { + @Override + public void dataSourceDown(DataSource dataSource, SQLException reason) { + // e.g. send to your alerting system / increment a metric + log.error("Database pool is DOWN", reason); + } + + @Override + public void dataSourceUp(DataSource dataSource) { + log.warn("Database pool is back UP"); + } +}; + +DataSourcePool pool = DataSourcePool.builder() + .name("mypool") + // ... url/username/password ... + .alert(alert) + .build(); +``` + +The pool also logs these transitions itself: + +``` +FATAL: DataSource [mypool] is down or has network error!!! +RESOLVED FATAL: DataSource [mypool] is back up! +``` + +--- + +## Borrow/return hooks with `DataSourcePoolListener` + +For per-connection observation (e.g. timing, tracing, auditing), register a listener: + +```java +DataSourcePoolListener listener = new DataSourcePoolListener() { + @Override + public void onAfterBorrowConnection(Connection connection) { + // called when a connection is handed to the application + } + + @Override + public void onBeforeReturnConnection(Connection connection) { + // called just before a connection is returned to the pool + } +}; + +DataSourcePool pool = DataSourcePool.builder() + .name("mypool") + // ... + .listener(listener) + .build(); +``` + +> Keep listener callbacks fast — they run on the borrow/return path of every connection. + +--- + +## Putting it together: a simple health check + +```java +PoolStatus status = pool.status(false); +boolean healthy = + pool.isDataSourceUp() + && status.waiting() == 0 + && status.busy() < status.maxSize(); + +if (!healthy) { + log.warn("DB pool degraded: up={} busy={}/{} waiting={}", + pool.isDataSourceUp(), status.busy(), status.maxSize(), status.waiting()); +} +``` + +--- + +## Next Steps + +- [Troubleshooting Connection Leaks & Pool Exhaustion](troubleshooting-connection-leaks.md) +- [Configuration Reference](configuration-reference.md) +- [Connection Validation Best Practices](connection-validation-best-practices.md) diff --git a/docs/guides/troubleshooting-connection-leaks.md b/docs/guides/troubleshooting-connection-leaks.md new file mode 100644 index 0000000..da79124 --- /dev/null +++ b/docs/guides/troubleshooting-connection-leaks.md @@ -0,0 +1,220 @@ +# Guide: Troubleshooting Connection Leaks & Pool Exhaustion + +## Purpose + +This guide helps you diagnose and fix the most common connection pool problems: + +- `ConnectionPoolExhaustedException` — no free connections available +- Connection leaks — connections obtained but never returned (not closed) +- Related warnings in the logs and how to interpret them + +--- + +## Symptom 1: `ConnectionPoolExhaustedException` + +You see an exception like: + +``` +Unsuccessfully waited [1000] millis for a connection to be returned. No connections are free. +You need to Increase the max connections of [50] or look for a connection pool leak using +datasource.xxx.capturestacktrace=true +``` + +### What it means + +Every connection in the pool is **busy** (checked out), the pool is already at `maxConnections`, and +a thread waited `waitTimeoutMillis` (default 1000ms) without one being returned. + +There are two root causes: + +1. **Undersized pool** — legitimate concurrent demand exceeds `maxConnections`. +2. **Connection leak** — code obtains connections but does not close them, so they are never + returned to the pool. This is the more common cause and the one to rule out first. + +### Step 1 — Confirm whether it is a leak or genuine load + +Check the pool metrics (see [Monitoring Pool Metrics](monitoring-pool-metrics.md)): + +```java +PoolStatus status = pool.status(false); +System.out.printf("busy=%d free=%d waiting=%d highWaterMark=%d max=%d%n", + status.busy(), status.free(), status.waiting(), status.highWaterMark(), status.maxSize()); +``` + +- If `busy` is pinned at `maxSize` and stays there even when traffic drops → likely a **leak**. +- If `busy` only spikes to max under load and recovers afterwards → likely **undersized**. + +### Step 2 — If it's a leak, capture stack traces + +Enable `captureStackTrace` so the pool records where each connection was obtained. When the pool is +exhausted, it dumps the busy connections including their acquiring stack trace: + +```java +DataSourcePool pool = DataSourcePool.builder() + .name("mypool") + // ... url/username/password ... + .captureStackTrace(true) // records the obtain stack trace + .maxStackTraceSize(20) // number of frames to report (default 5) + .build(); +``` + +Or via properties: + +```properties +datasource.mypool.captureStackTrace=true +datasource.mypool.maxStackTraceSize=20 +``` + +The log will then contain entries like: + +``` +Dumping [50] busy connections: (Use datasource.xxx.captureStackTrace=true ... to get stackTraces) +Busy Connection - name[mypool1] startTime[...] busySeconds[742] stackTrace[...] stmt[select ...] +``` + +The `stackTrace` points at the code path that obtained the connection and never closed it, and +`stmt` shows the last statement it ran. `busySeconds` shows how long it has been checked out. + +> `captureStackTrace` has a performance cost (it captures a stack trace on every `getConnection()`), +> so enable it to diagnose, then turn it off again. + +### Step 3 — Fix the leak + +Always close connections (and statements / result sets) in a `try-with-resources` block: + +```java +try (Connection connection = pool.getConnection()) { + try (PreparedStatement stmt = connection.prepareStatement("select ...")) { + // ... + } +} // connection is returned to the pool here, even on exception +``` + +Common leak causes: + +- A `Connection` stored in a field or returned from a method and never closed. +- An exception thrown between `getConnection()` and `close()` without a `try`/finally or + try-with-resources. +- Closing the `PreparedStatement` / `ResultSet` but forgetting the `Connection`. + +### Step 4 — If it's genuine load, tune sizing + +```java +.maxConnections(100) // raise the ceiling +.waitTimeoutMillis(3000) // allow longer waits before failing (optional) +``` + +Confirm the database can support the connection count, and review `highWaterMark` over time to size +appropriately. See [Configuration Reference](configuration-reference.md). + +--- + +## Symptom 2: leak self-recovery and `leakTimeMinutes` + +A busy connection that has been checked out longer than `leakTimeMinutes` (default 30) is treated as +a leak. Leaked busy connections are **force-closed when the pool resets**. + +The recovery flow is automatic: + +1. Leaks accumulate and the pool eventually exhausts. +2. The background heartbeat fails to obtain a connection. After + `heartbeatMaxPoolExhaustedCount` (default 10) consecutive failures, the pool is **reset**. +3. On reset, busy connections older than `leakTimeMinutes` are force-closed and recreated, restoring + service. + +You will see logs similar to: + +``` +Closing busy connections using leakTimeMinutes 30 +DataSource closing busy connection? name[mypool1] startTime[...] busySeconds[2105] stmt[...] +``` + +This is a safety net, **not** a substitute for fixing the leak. Tune `leakTimeMinutes` to be safely +longer than your longest legitimate transaction so that genuinely-busy connections are never +force-closed: + +```java +.leakTimeMinutes(30) // must exceed your longest expected query/transaction +``` + +--- + +## Symptom 3: `Connection [...] not found in BusyList?` + +``` +Connection [name[mypool1] startTime[...] busySeconds[0] ...] not found in BusyList? +``` + +### What it means + +A connection was returned to the pool but was no longer registered in the busy list. This usually +indicates the connection had already been removed — for example it was force-closed as a suspected +leak (or during a pool reset) while still checked out, and is then returned by the application +afterwards. + +### What to do + +- If you also see leak/reset logs (Symptom 2), the underlying issue is a **leak** or a + `leakTimeMinutes` that is shorter than your longest transaction — address those. +- A low `busySeconds` value in the warning indicates the connection was returned very soon after + being checked out, which points at a reset closing in-use connections rather than a real leak — + ensure `leakTimeMinutes` is not set too low. + +--- + +## Symptom 4: `Tried to close a dirty connection` + +``` +Tried to close a dirty connection at . See https://github.com/ebean-orm/ebean-datasource/issues/116 for details. +``` + +### What it means + +A connection with `autoCommit=false` was closed while it still had uncommitted changes (no `commit()` +or `rollback()` was called). The pool rolls it back for you, but this is a programming error — the +intended transaction outcome is ambiguous. + +### What to do + +Always end transactions explicitly before closing: + +```java +try (Connection connection = pool.getConnection()) { + try { + // ... do work ... + connection.commit(); + } catch (Exception e) { + connection.rollback(); + throw e; + } +} +``` + +To turn this into a hard failure in tests/CI so leaks of transaction discipline are caught early: + +```java +.enforceCleanClose(true) // throws instead of just warning +``` + +This option has no effect on read-only or auto-commit connections. + +--- + +## Quick reference + +| Setting | Default | Use when | +|---------|---------|----------| +| `captureStackTrace` | `false` | Diagnosing where leaked connections were obtained. | +| `maxStackTraceSize` | `5` | Need more stack frames in the leak dump. | +| `leakTimeMinutes` | `30` | Tuning the leak force-close threshold (keep > longest transaction). | +| `waitTimeoutMillis` | `1000` | Tuning how long a thread waits before `ConnectionPoolExhaustedException`. | +| `maxConnections` | `200` | Pool genuinely undersized for concurrent load. | +| `enforceCleanClose` | `false` | Catch dirty-close programming errors (recommended in tests). | + +--- + +## Next Steps + +- [Monitoring Pool Metrics](monitoring-pool-metrics.md) +- [Configuration Reference](configuration-reference.md) +- [Connection Validation Best Practices](connection-validation-best-practices.md)