Ryan Malloy 5e26b34564 Phase 15: connection pool
Thread-safe connection pool with min/max sizing, lazy growth,
idle recycling, and per-acquire health-check.

API:
  pool = informix_db.create_pool(host=..., min_size=1, max_size=10)
  with pool.connection() as conn:
      ...
  pool.close()

Design choices:

* Lazy growth from min_size — pre-opens min_size on construction,
  grows to max_size on demand. Pay-nothing startup with burst capacity.

* Health-check on acquire, not release. Sends a trivial SELECT 1
  round-trip before yielding. Dead idle connections (server-side
  timeout, network drop) are silently replaced. The cost is ~1ms
  per acquire, bought at the price of "users never see a stale-
  connection error". Check-on-release is wrong because idle time
  is when connections actually die.

* Eviction on OperationalError/InterfaceError only. The "with
  pool.connection()" context manager retains the connection on
  application-level errors (ValueError, IntegrityError, etc.).
  Avoids the "every constraint violation evicts a healthy connection"
  pitfall.

* Releases the pool lock during connect() — the slow handshake
  (50-100ms) doesn't serialize other threads' acquires.

Tests: 15 integration tests in test_pool.py covering:
* API & lifecycle (pre-open, lazy growth, context-manager, LIFO)
* Exhaustion (timeout when full, per-acquire override, unblock-on-release)
* Eviction (explicit broken, auto on OperationalError, retain on
  application errors)
* Health-check (dead idle silently replaced)
* Shutdown (close drains, idempotent, context-manager)
* Multi-thread safety (8 workers × 3 queries each, no leaks)

Total: 69 unit + 154 integration = 223 tests.

With Phase 14 (TLS) and Phase 15 (pool), the project covers the
three things a typical Python web/API workload needs from a
database driver: PEP 249 surface, TLS transport, connection pool.
Only async (informix_db.aio) remains in the backlog.
2026-05-04 14:50:27 -06:00
..
2026-05-04 14:50:27 -06:00