# Infrastructure - Poolboy Integration
This document provides a detailed breakdown of how the `foundation` library integrates `poolboy` for connection pooling, contrasting the direct `poolboy` pattern with the abstractions provided by the `ConnectionManager`.
## Overview
The standard `poolboy` integration pattern involves directly calling `:poolboy.start_link/2` with a specific configuration keyword list. The `foundation` library abstracts this process within the **`ConnectionManager`** module to provide validation, logging, and a more user-friendly configuration format.
The `foundation` library adheres to this pattern but provides a robust abstraction layer through its `Infrastructure` and `ConnectionManager` modules. This abstraction offers several advantages:
- **Centralized Control:** Simplifies management of multiple connection pools.
- **Configuration Translation:** Converts simple config formats into the specific keyword lists that `:poolboy.start_link` expects.
- **Safety:** Wraps the checkout/checkin lifecycle in safe functions that prevent resource leakage.
- **Observability:** Automatically emits telemetry events for pool operations and performance metrics.
## The Integration in Detail
### 1. Starting a Pool: `poolboy:start_link`
The standard `poolboy` pattern involves directly calling `:poolboy.start_link/2` with a specific configuration keyword list. The `foundation` library abstracts this process within the **`ConnectionManager`** module to provide validation, logging, and a more user-friendly configuration format.
**File:** `foundation/infrastructure/connection_manager.ex`
**Functions:** `start_pool/2`, `do_start_pool/2`, `build_poolboy_config/2`
```elixir
# foundation/infrastructure/connection_manager.ex
# --- The Public API ---
@spec start_pool(pool_name(), pool_config()) :: {:ok, pid()} | {:error, term()}
def start_pool(pool_name, config) do
GenServer.call(__MODULE__, {:start_pool, pool_name, config})
end
# --- The Private Implementation ---
@spec do_start_pool(pool_name(), pool_config()) :: {:ok, pid()} | {:error, term()}
defp do_start_pool(pool_name, config) do
# ... (validation logic) ...
{poolboy_config, worker_args} = build_poolboy_config(pool_name, config)
# --- THIS IS THE KEY LINE ---
case :poolboy.start_link(poolboy_config, worker_args) do
{:ok, pid} -> {:ok, pid}
{:error, reason} -> {:error, reason}
end
# ...
end
# --- Configuration Translation ---
@spec build_poolboy_config(pool_name(), pool_config()) :: {keyword(), keyword()}
defp build_poolboy_config(pool_name, config) do
merged_config = Keyword.merge(@default_config, config)
poolboy_config = [
name: {:local, pool_name}, # `poolboy` requires the {:local, name} format
worker_module: Keyword.fetch!(merged_config, :worker_module),
size: Keyword.get(merged_config, :size),
max_overflow: Keyword.get(merged_config, :max_overflow),
strategy: Keyword.get(merged_config, :strategy)
]
worker_args = Keyword.get(merged_config, :worker_args, [])
{poolboy_config, worker_args}
end
```
**Analysis:**
* **Abstraction:** Instead of calling `:poolboy.start_link` directly, users call the much cleaner `ConnectionManager.start_pool(:my_pool, [...])`.
* **Configuration:** The `build_poolboy_config` function acts as a translator. It takes `foundation`'s simple config format and transforms it into the specific keyword list that `:poolboy.start_link` expects, including enforcing the `name: {:local, pool_name}` convention.
* **Validation:** Before starting the pool, `ConnectionManager` validates the configuration (`validate_pool_config`) and ensures the worker module exists and is compiled (`validate_worker_module`), providing better error messages than `poolboy` might alone.
* **Centralization:** All pools are managed by the `ConnectionManager` GenServer, which keeps track of their state and configuration in a centralized location.
---
### 2. The Worker Module Concept
`Poolboy` requires a "worker" module—a `GenServer` that implements a `start_link/1` function. The `foundation` library fully adheres to this pattern and provides a sample implementation with `HttpWorker`.
**File:** `foundation/infrastructure/pool_workers/http_worker.ex`
**Function:** `start_link/1`
```elixir
# foundation/infrastructure/pool_workers/http_worker.ex
@doc """
Starts an HTTP worker with the given configuration.
This function is called by Poolboy to create worker instances.
"""
@spec start_link(worker_config()) :: GenServer.on_start()
def start_link(config) do
GenServer.start_link(__MODULE__, config)
end
# ... (GenServer implementation for the worker)
```
**Analysis:**
* The `HttpWorker.start_link/1` function is the entry point that `poolboy` will call whenever it needs to create a new worker for the pool.
* The `worker_args` from the `ConnectionManager.start_pool` configuration are passed directly as the `config` argument to this `start_link` function. This is how you provide initial state (like a base URL or API keys) to each worker.
* This demonstrates that `foundation`'s abstraction does not change the fundamental contract required of a `poolboy` worker.
---
### 3. Using a Pooled Resource: `checkout` and `checkin`
The most significant abstraction `ConnectionManager` provides is wrapping the `checkout`/`checkin` lifecycle. The standard `poolboy` pattern requires developers to manually:
1. `:poolboy.checkout(...)` to get a worker.
2. Use the worker inside a `try/after` block.
3. `:poolboy.checkin(...)` in the `after` clause to guarantee the worker is returned to the pool, even if the main operation fails.
`ConnectionManager` encapsulates this entire error-prone process in a single, safe function.
**File:** `foundation/infrastructure/connection_manager.ex`
**Functions:** `with_connection/3`, `do_with_connection/4`
```elixir
# foundation/infrastructure/connection_manager.ex
# --- The Public API ---
@spec with_connection(pool_name(), (pid() -> term()), timeout()) ::
{:ok, term()} | {:error, term()}
def with_connection(pool_name, fun, timeout \\ @default_checkout_timeout) do
# ... (timeout logic) ...
GenServer.call(__MODULE__, {:with_connection, pool_name, fun, timeout}, genserver_timeout)
end
# --- The Private Implementation ---
@spec do_with_connection(pool_name(), pid(), (pid() -> term()), timeout()) ::
{:ok, term()} | {:error, term()}
defp do_with_connection(pool_name, pool_pid, fun, timeout) do
start_time = System.monotonic_time()
try do
# --- CHECKOUT ---
worker = :poolboy.checkout(pool_pid, true, timeout)
emit_telemetry(
:checkout,
%{ checkout_time: System.monotonic_time() - start_time },
%{pool_name: pool_name}
)
try do
# --- EXECUTE USER'S FUNCTION ---
result = fun.(worker)
{:ok, result}
# ... (error handling) ...
after
# --- CHECKIN (GUARANTEED) ---
:poolboy.checkin(pool_pid, worker)
emit_telemetry(:checkin, %{}, %{pool_name: pool_name})
end
catch
# ... (timeout and other exit handling) ...
end
end
```
**Analysis:**
* **Abstraction:** The developer calls `ConnectionManager.with_connection/2` and simply provides a function that receives the checked-out `worker` as an argument. They never see or call `:poolboy.checkout` or `:poolboy.checkin`.
* **Safety:** The `try/after` block inside `do_with_connection` is the crucial safety mechanism. It guarantees that `:poolboy.checkin` is *always* called for the worker, preventing pool resource leakage even if the user's function (`fun.()`) crashes.
* **Observability:** The wrapper emits `:checkout` and `:checkin` telemetry events, providing valuable insight into pool usage, checkout times, and potential bottlenecks, which would have to be implemented manually otherwise.
* **Error Handling:** The `catch` block provides standardized error handling for common `poolboy` exit reasons, like `:timeout`, translating them into clean `{:error, reason}` tuples.
---
## End-to-End Workflow
This diagram visualizes the end-to-end flow, from starting a pool to using it via the `ConnectionManager`.
```mermaid
sequenceDiagram
participant App as "Application Code"
participant CM as "ConnectionManager"
participant Poolboy as ":poolboy"
participant Worker as "Worker Module"
App->>+CM: start_pool(:my_pool, config)
CM->>CM: build_poolboy_config(config)
CM->>+Poolboy: :poolboy.start_link(poolboy_config, worker_args)
Poolboy->>+Worker: start_link(worker_args)
Worker-->>-Poolboy: {:ok, worker_pid}
Poolboy-->>-CM: {:ok, pool_pid}
CM-->>-App: {:ok, pool_pid}
App->>+CM: with_connection(:my_pool, fun)
CM->>+Poolboy: checkout()
Poolboy-->>-CM: worker_pid
Note right of CM: Emits :checkout telemetry
CM->>+Worker: fun(worker_pid)
Worker-->>-CM: result
CM->>+Poolboy: checkin(worker_pid)
Note right of CM: Emits :checkin telemetry
Poolboy-->>-CM: :ok
CM-->>-App: {:ok, result}
```
---
## Comparison Summary
This table summarizes how `foundation` implements the standard `poolboy` patterns.
| `poolboy` Documentation Pattern | `foundation` Library Implementation |
| :--- | :--- |
| Call `:poolboy.start_link` with specific config. | Call **`ConnectionManager.start_pool`**. It handles config translation, validation, and logging. |
| Implement a `worker_module` with `start_link/1`. | This is the same. The contract for the worker is unchanged. `HttpWorker` is provided as an example. |
| Manually wrap work in `try...after` with `:poolboy.checkout` and `:poolboy.checkin`. | Call **`ConnectionManager.with_connection`** and provide a function. The wrapper handles the entire `checkout`/`checkin` lifecycle and associated error handling. |
| Implement telemetry and logging for pool events manually. | Telemetry for checkouts, check-ins, and timeouts is **built into** the `ConnectionManager` wrapper. |
| Manage multiple pools manually. | `ConnectionManager` is a **centralized GenServer** that manages the state and configuration of all active pools. |
By using these abstractions, `foundation` provides a more integrated, observable, and developer-friendly way to leverage the power of `poolboy`'s connection pooling capabilities.