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_linkexpects. - 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
# 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}
endAnalysis:
- Abstraction: Instead of calling
:poolboy.start_linkdirectly, users call the much cleanerConnectionManager.start_pool(:my_pool, [...]). - Configuration: The
build_poolboy_configfunction acts as a translator. It takesfoundation's simple config format and transforms it into the specific keyword list that:poolboy.start_linkexpects, including enforcing thename: {:local, pool_name}convention. - Validation: Before starting the pool,
ConnectionManagervalidates the configuration (validate_pool_config) and ensures the worker module exists and is compiled (validate_worker_module), providing better error messages thanpoolboymight alone. - Centralization: All pools are managed by the
ConnectionManagerGenServer, 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
# 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/1function is the entry point thatpoolboywill call whenever it needs to create a new worker for the pool. - The
worker_argsfrom theConnectionManager.start_poolconfiguration are passed directly as theconfigargument to thisstart_linkfunction. 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 apoolboyworker.
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:
-
:poolboy.checkout(...)to get a worker. - Use the worker inside a
try/afterblock. -
:poolboy.checkin(...)in theafterclause 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
# 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
endAnalysis:
- Abstraction: The developer calls
ConnectionManager.with_connection/2and simply provides a function that receives the checked-outworkeras an argument. They never see or call:poolboy.checkoutor:poolboy.checkin. - Safety: The
try/afterblock insidedo_with_connectionis the crucial safety mechanism. It guarantees that:poolboy.checkinis always called for the worker, preventing pool resource leakage even if the user's function (fun.()) crashes. - Observability: The wrapper emits
:checkoutand:checkintelemetry events, providing valuable insight into pool usage, checkout times, and potential bottlenecks, which would have to be implemented manually otherwise. - Error Handling: The
catchblock provides standardized error handling for commonpoolboyexit 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.
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.