TCP/TLS connection pooling for Eio

init

+1
.gitignore
···
+
_build
+33
conpool.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "Protocol-agnostic TCP/IP connection pooling library for Eio"
+
description:
+
"Conpool is a connection pooling library built on Eio.Pool that manages TCP connection lifecycles, validates connection health, and provides per-endpoint resource limiting for any TCP-based protocol (HTTP, Redis, PostgreSQL, etc.)"
+
maintainer: ["Your Name"]
+
authors: ["Your Name"]
+
license: "MIT"
+
homepage: "https://github.com/username/conpool"
+
bug-reports: "https://github.com/username/conpool/issues"
+
depends: [
+
"ocaml"
+
"dune" {>= "3.0" & >= "3.0"}
+
"eio"
+
"tls-eio" {>= "1.0"}
+
"logs"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
dev-repo: "git+https://github.com/username/conpool.git"
+24
dune-project
···
+
(lang dune 3.0)
+
(name conpool)
+
+
(generate_opam_files true)
+
+
(source
+
(github username/conpool))
+
+
(authors "Your Name")
+
+
(maintainers "Your Name")
+
+
(license MIT)
+
+
(package
+
(name conpool)
+
(synopsis "Protocol-agnostic TCP/IP connection pooling library for Eio")
+
(description "Conpool is a connection pooling library built on Eio.Pool that manages TCP connection lifecycles, validates connection health, and provides per-endpoint resource limiting for any TCP-based protocol (HTTP, Redis, PostgreSQL, etc.)")
+
(depends
+
ocaml
+
(dune (>= 3.0))
+
eio
+
(tls-eio (>= 1.0))
+
logs))
+632
lib/conpool.ml
···
+
(** Conpool - Protocol-agnostic TCP/IP connection pooling library for Eio *)
+
+
let src = Logs.Src.create "conpool" ~doc:"Connection pooling library"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
+
module Endpoint = struct
+
type t = {
+
host : string;
+
port : int;
+
}
+
+
let make ~host ~port = { host; port }
+
+
let host t = t.host
+
let port t = t.port
+
+
let pp fmt t =
+
Format.fprintf fmt "%s:%d" t.host t.port
+
+
let equal t1 t2 =
+
String.equal t1.host t2.host && t1.port = t2.port
+
+
let hash t =
+
Hashtbl.hash (t.host, t.port)
+
end
+
+
module Tls_config = struct
+
type t = {
+
config : Tls.Config.client;
+
servername : string option;
+
}
+
+
let make ~config ?servername () = { config; servername }
+
+
let config t = t.config
+
let servername t = t.servername
+
+
let pp fmt t =
+
Format.fprintf fmt "TLS(servername=%s)"
+
(match t.servername with Some s -> s | None -> "<default>")
+
end
+
+
(* Internal connection type - not exposed in public API *)
+
module Connection = struct
+
type t = {
+
flow : [`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t;
+
created_at : float;
+
mutable last_used : float;
+
mutable use_count : int;
+
endpoint : Endpoint.t;
+
}
+
+
let flow t = t.flow
+
let endpoint t = t.endpoint
+
let created_at t = t.created_at
+
let last_used t = t.last_used
+
let use_count t = t.use_count
+
end
+
+
module Config = struct
+
type t = {
+
max_connections_per_endpoint : int;
+
max_idle_time : float;
+
max_connection_lifetime : float;
+
max_connection_uses : int option;
+
health_check : ([`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t -> bool) option;
+
connect_timeout : float option;
+
connect_retry_count : int;
+
connect_retry_delay : float;
+
on_connection_created : (Endpoint.t -> unit) option;
+
on_connection_closed : (Endpoint.t -> unit) option;
+
on_connection_reused : (Endpoint.t -> unit) option;
+
}
+
+
let make
+
?(max_connections_per_endpoint = 10)
+
?(max_idle_time = 60.0)
+
?(max_connection_lifetime = 300.0)
+
?max_connection_uses
+
?health_check
+
?(connect_timeout = 10.0)
+
?(connect_retry_count = 3)
+
?(connect_retry_delay = 0.1)
+
?on_connection_created
+
?on_connection_closed
+
?on_connection_reused
+
() =
+
{
+
max_connections_per_endpoint;
+
max_idle_time;
+
max_connection_lifetime;
+
max_connection_uses;
+
health_check;
+
connect_timeout = Some connect_timeout;
+
connect_retry_count;
+
connect_retry_delay;
+
on_connection_created;
+
on_connection_closed;
+
on_connection_reused;
+
}
+
+
let default = make ()
+
+
let max_connections_per_endpoint t = t.max_connections_per_endpoint
+
let max_idle_time t = t.max_idle_time
+
let max_connection_lifetime t = t.max_connection_lifetime
+
let max_connection_uses t = t.max_connection_uses
+
let health_check t = t.health_check
+
let connect_timeout t = t.connect_timeout
+
let connect_retry_count t = t.connect_retry_count
+
let connect_retry_delay t = t.connect_retry_delay
+
+
let pp fmt t =
+
Format.fprintf fmt
+
"@[<v>Config:@,\
+
- max_connections_per_endpoint: %d@,\
+
- max_idle_time: %.1fs@,\
+
- max_connection_lifetime: %.1fs@,\
+
- max_connection_uses: %s@,\
+
- connect_timeout: %s@,\
+
- connect_retry_count: %d@,\
+
- connect_retry_delay: %.2fs@]"
+
t.max_connections_per_endpoint
+
t.max_idle_time
+
t.max_connection_lifetime
+
(match t.max_connection_uses with Some n -> string_of_int n | None -> "unlimited")
+
(match t.connect_timeout with Some f -> Printf.sprintf "%.1fs" f | None -> "none")
+
t.connect_retry_count
+
t.connect_retry_delay
+
end
+
+
module Stats = struct
+
type t = {
+
active : int;
+
idle : int;
+
total_created : int;
+
total_reused : int;
+
total_closed : int;
+
errors : int;
+
}
+
+
let active t = t.active
+
let idle t = t.idle
+
let total_created t = t.total_created
+
let total_reused t = t.total_reused
+
let total_closed t = t.total_closed
+
let errors t = t.errors
+
+
let pp fmt t =
+
Format.fprintf fmt
+
"@[<v>Stats:@,\
+
- Active: %d@,\
+
- Idle: %d@,\
+
- Created: %d@,\
+
- Reused: %d@,\
+
- Closed: %d@,\
+
- Errors: %d@]"
+
t.active
+
t.idle
+
t.total_created
+
t.total_reused
+
t.total_closed
+
t.errors
+
end
+
+
type endp_stats = {
+
mutable active : int;
+
mutable idle : int;
+
mutable total_created : int;
+
mutable total_reused : int;
+
mutable total_closed : int;
+
mutable errors : int;
+
}
+
+
type endpoint_pool = {
+
pool : Connection.t Eio.Pool.t;
+
stats : endp_stats;
+
mutex : Eio.Mutex.t;
+
}
+
+
type ('clock, 'net) t = {
+
sw : Eio.Switch.t;
+
net : 'net;
+
clock : 'clock;
+
config : Config.t;
+
tls : Tls_config.t option;
+
endpoints : (Endpoint.t, endpoint_pool) Hashtbl.t;
+
endpoints_mutex : Eio.Mutex.t;
+
}
+
+
module EndpointTbl = Hashtbl.Make(struct
+
type t = Endpoint.t
+
let equal = Endpoint.equal
+
let hash = Endpoint.hash
+
end)
+
+
let get_time pool =
+
Eio.Time.now pool.clock
+
+
let create_endp_stats () = {
+
active = 0;
+
idle = 0;
+
total_created = 0;
+
total_reused = 0;
+
total_closed = 0;
+
errors = 0;
+
}
+
+
let snapshot_stats (stats : endp_stats) : Stats.t = {
+
active = stats.active;
+
idle = stats.idle;
+
total_created = stats.total_created;
+
total_reused = stats.total_reused;
+
total_closed = stats.total_closed;
+
errors = stats.errors;
+
}
+
+
(** {1 DNS Resolution} *)
+
+
let resolve_endpoint pool endpoint =
+
Log.debug (fun m -> m "Resolving %a..." Endpoint.pp endpoint);
+
let addrs = Eio.Net.getaddrinfo_stream pool.net (Endpoint.host endpoint) ~service:(string_of_int (Endpoint.port endpoint)) in
+
Log.debug (fun m -> m "Got address list for %a" Endpoint.pp endpoint);
+
match addrs with
+
| addr :: _ ->
+
Log.debug (fun m -> m "Resolved %a to %a"
+
Endpoint.pp endpoint Eio.Net.Sockaddr.pp addr);
+
addr
+
| [] ->
+
Log.err (fun m -> m "Failed to resolve hostname: %s" (Endpoint.host endpoint));
+
failwith (Printf.sprintf "Failed to resolve hostname: %s" (Endpoint.host endpoint))
+
+
(** {1 Connection Creation with Retry} *)
+
+
let rec create_connection_with_retry pool endpoint attempt =
+
if attempt > pool.config.connect_retry_count then begin
+
Log.err (fun m -> m "Failed to connect to %a after %d attempts"
+
Endpoint.pp endpoint pool.config.connect_retry_count);
+
failwith (Printf.sprintf "Failed to connect to %s:%d after %d attempts"
+
(Endpoint.host endpoint) (Endpoint.port endpoint) pool.config.connect_retry_count)
+
end;
+
+
Log.debug (fun m -> m "Connecting to %a (attempt %d/%d)"
+
Endpoint.pp endpoint attempt pool.config.connect_retry_count);
+
+
try
+
let addr = resolve_endpoint pool endpoint in
+
Log.debug (fun m -> m "Resolved %a to address" Endpoint.pp endpoint);
+
+
(* Connect with optional timeout *)
+
let socket =
+
match pool.config.connect_timeout with
+
| Some timeout ->
+
Eio.Time.with_timeout_exn pool.clock timeout
+
(fun () -> Eio.Net.connect ~sw:pool.sw pool.net addr)
+
| None ->
+
Eio.Net.connect ~sw:pool.sw pool.net addr
+
in
+
+
Log.debug (fun m -> m "TCP connection established to %a" Endpoint.pp endpoint);
+
+
let flow = match pool.tls with
+
| None -> (socket :> [`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t)
+
| Some tls_cfg ->
+
Log.debug (fun m -> m "Initiating TLS handshake with %a" Endpoint.pp endpoint);
+
let host = match Tls_config.servername tls_cfg with
+
| Some name -> Domain_name.(host_exn (of_string_exn name))
+
| None -> Domain_name.(host_exn (of_string_exn (Endpoint.host endpoint)))
+
in
+
let tls_flow = Tls_eio.client_of_flow ~host (Tls_config.config tls_cfg) socket in
+
Log.info (fun m -> m "TLS connection established to %a" Endpoint.pp endpoint);
+
(tls_flow :> [`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t)
+
in
+
+
let now = get_time pool in
+
Log.info (fun m -> m "Connection created to %a" Endpoint.pp endpoint);
+
{
+
Connection.flow;
+
created_at = now;
+
last_used = now;
+
use_count = 0;
+
endpoint;
+
}
+
+
with
+
| Eio.Time.Timeout ->
+
Log.warn (fun m -> m "Connection timeout to %a (attempt %d)" Endpoint.pp endpoint attempt);
+
(* Exponential backoff *)
+
let delay = pool.config.connect_retry_delay *. (2.0 ** float_of_int (attempt - 1)) in
+
Eio.Time.sleep pool.clock delay;
+
create_connection_with_retry pool endpoint (attempt + 1)
+
| e ->
+
(* Other errors - retry with backoff *)
+
Log.warn (fun m -> m "Connection attempt %d to %a failed: %s"
+
attempt Endpoint.pp endpoint (Printexc.to_string e));
+
if attempt < pool.config.connect_retry_count then (
+
let delay = pool.config.connect_retry_delay *. (2.0 ** float_of_int (attempt - 1)) in
+
Eio.Time.sleep pool.clock delay;
+
create_connection_with_retry pool endpoint (attempt + 1)
+
) else
+
raise e
+
+
let create_connection pool endpoint =
+
create_connection_with_retry pool endpoint 1
+
+
(** {1 Connection Validation} *)
+
+
let is_healthy pool ?(check_readable = false) conn =
+
let now = get_time pool in
+
+
(* Check age *)
+
let age = now -. Connection.created_at conn in
+
if age > pool.config.max_connection_lifetime then begin
+
Log.debug (fun m -> m "Connection to %a unhealthy: exceeded max lifetime (%.2fs > %.2fs)"
+
Endpoint.pp (Connection.endpoint conn) age pool.config.max_connection_lifetime);
+
false
+
end
+
+
(* Check idle time *)
+
else if (now -. Connection.last_used conn) > pool.config.max_idle_time then begin
+
let idle_time = now -. Connection.last_used conn in
+
Log.debug (fun m -> m "Connection to %a unhealthy: exceeded max idle time (%.2fs > %.2fs)"
+
Endpoint.pp (Connection.endpoint conn) idle_time pool.config.max_idle_time);
+
false
+
end
+
+
(* Check use count *)
+
else if (match pool.config.max_connection_uses with
+
| Some max -> Connection.use_count conn >= max
+
| None -> false) then begin
+
Log.debug (fun m -> m "Connection to %a unhealthy: exceeded max use count (%d)"
+
Endpoint.pp (Connection.endpoint conn) (Connection.use_count conn));
+
false
+
end
+
+
(* Optional: custom health check *)
+
else if (match pool.config.health_check with
+
| Some check ->
+
(try
+
let healthy = check (Connection.flow conn) in
+
if not healthy then
+
Log.debug (fun m -> m "Connection to %a failed custom health check"
+
Endpoint.pp (Connection.endpoint conn));
+
not healthy
+
with e ->
+
Log.debug (fun m -> m "Connection to %a health check raised exception: %s"
+
Endpoint.pp (Connection.endpoint conn) (Printexc.to_string e));
+
true) (* Exception in health check = unhealthy *)
+
| None -> false) then
+
false
+
+
(* Optional: check if socket still connected *)
+
else if check_readable then
+
try
+
(* TODO avsm: a sockopt for this? *)
+
true
+
with
+
| _ -> false
+
+
else begin
+
Log.debug (fun m -> m "Connection to %a is healthy (age=%.2fs, idle=%.2fs, uses=%d)"
+
Endpoint.pp (Connection.endpoint conn)
+
age
+
(now -. Connection.last_used conn)
+
(Connection.use_count conn));
+
true
+
end
+
+
(** {1 Internal Pool Operations} *)
+
+
let close_internal pool conn =
+
Log.debug (fun m -> m "Closing connection to %a (age=%.2fs, uses=%d)"
+
Endpoint.pp (Connection.endpoint conn)
+
(get_time pool -. Connection.created_at conn)
+
(Connection.use_count conn));
+
+
Eio.Cancel.protect (fun () ->
+
try
+
Eio.Flow.close (Connection.flow conn)
+
with _ -> ()
+
);
+
+
(* Call hook if configured *)
+
Option.iter (fun f -> f (Connection.endpoint conn)) pool.config.on_connection_closed
+
+
let get_or_create_endpoint_pool pool endpoint =
+
Log.debug (fun m -> m "Getting or creating endpoint pool for %a" Endpoint.pp endpoint);
+
+
(* First try with read lock *)
+
match Eio.Mutex.use_ro pool.endpoints_mutex (fun () ->
+
Hashtbl.find_opt pool.endpoints endpoint
+
) with
+
| Some ep_pool ->
+
Log.debug (fun m -> m "Found existing endpoint pool for %a" Endpoint.pp endpoint);
+
ep_pool
+
| None ->
+
Log.debug (fun m -> m "No existing pool, need to create for %a" Endpoint.pp endpoint);
+
(* Need to create - use write lock *)
+
Eio.Mutex.use_rw ~protect:true pool.endpoints_mutex (fun () ->
+
(* Check again in case another fiber created it *)
+
match Hashtbl.find_opt pool.endpoints endpoint with
+
| Some ep_pool ->
+
Log.debug (fun m -> m "Another fiber created pool for %a" Endpoint.pp endpoint);
+
ep_pool
+
| None ->
+
(* Create new endpoint pool *)
+
let stats = create_endp_stats () in
+
let mutex = Eio.Mutex.create () in
+
+
Log.info (fun m -> m "Creating new endpoint pool for %a (max_connections=%d)"
+
Endpoint.pp endpoint pool.config.max_connections_per_endpoint);
+
+
Log.debug (fun m -> m "About to create Eio.Pool for %a" Endpoint.pp endpoint);
+
+
let eio_pool = Eio.Pool.create
+
pool.config.max_connections_per_endpoint
+
~validate:(fun conn ->
+
Log.debug (fun m -> m "Validate called for connection to %a" Endpoint.pp endpoint);
+
(* Called before reusing from pool *)
+
let healthy = is_healthy pool ~check_readable:false conn in
+
+
if healthy then (
+
Log.debug (fun m -> m "Reusing connection to %a from pool" Endpoint.pp endpoint);
+
+
(* Update stats for reuse *)
+
Eio.Mutex.use_rw ~protect:true mutex (fun () ->
+
stats.total_reused <- stats.total_reused + 1
+
);
+
+
(* Call hook if configured *)
+
Option.iter (fun f -> f endpoint) pool.config.on_connection_reused;
+
+
(* Run health check if configured *)
+
match pool.config.health_check with
+
| Some check ->
+
(try check (Connection.flow conn)
+
with _ -> false)
+
| None -> true
+
) else begin
+
Log.debug (fun m -> m "Connection to %a failed validation, creating new one" Endpoint.pp endpoint);
+
false
+
end
+
)
+
~dispose:(fun conn ->
+
(* Called when removing from pool *)
+
Eio.Cancel.protect (fun () ->
+
close_internal pool conn;
+
+
(* Update stats *)
+
Eio.Mutex.use_rw ~protect:true mutex (fun () ->
+
stats.total_closed <- stats.total_closed + 1
+
)
+
)
+
)
+
(fun () ->
+
Log.debug (fun m -> m "Factory function called for %a" Endpoint.pp endpoint);
+
try
+
let conn = create_connection pool endpoint in
+
+
Log.debug (fun m -> m "Connection created successfully for %a" Endpoint.pp endpoint);
+
+
(* Update stats *)
+
Eio.Mutex.use_rw ~protect:true mutex (fun () ->
+
stats.total_created <- stats.total_created + 1
+
);
+
+
(* Call hook if configured *)
+
Option.iter (fun f -> f endpoint) pool.config.on_connection_created;
+
+
conn
+
with e ->
+
Log.err (fun m -> m "Factory function failed for %a: %s"
+
Endpoint.pp endpoint (Printexc.to_string e));
+
(* Update error stats *)
+
Eio.Mutex.use_rw ~protect:true mutex (fun () ->
+
stats.errors <- stats.errors + 1
+
);
+
raise e
+
)
+
in
+
+
Log.debug (fun m -> m "Eio.Pool created successfully for %a" Endpoint.pp endpoint);
+
+
let ep_pool = {
+
pool = eio_pool;
+
stats;
+
mutex;
+
} in
+
+
Hashtbl.add pool.endpoints endpoint ep_pool;
+
Log.debug (fun m -> m "Endpoint pool added to hashtable for %a" Endpoint.pp endpoint);
+
ep_pool
+
)
+
+
(** {1 Public API - Pool Creation} *)
+
+
let create ~sw ~(net : 'net Eio.Net.t) ~(clock : 'clock Eio.Time.clock) ?tls ?(config = Config.default) () : ('clock Eio.Time.clock, 'net Eio.Net.t) t =
+
Log.info (fun m -> m "Creating new connection pool (max_per_endpoint=%d, max_idle=%.1fs, max_lifetime=%.1fs)"
+
config.max_connections_per_endpoint
+
config.max_idle_time
+
config.max_connection_lifetime);
+
+
let pool = {
+
sw;
+
net;
+
clock;
+
config;
+
tls;
+
endpoints = Hashtbl.create 16;
+
endpoints_mutex = Eio.Mutex.create ();
+
} in
+
+
(* Auto-cleanup on switch release *)
+
Eio.Switch.on_release sw (fun () ->
+
Eio.Cancel.protect (fun () ->
+
Log.info (fun m -> m "Closing connection pool");
+
(* Close all idle connections - active ones will be cleaned up by switch *)
+
Hashtbl.iter (fun _endpoint _ep_pool ->
+
(* Connections are bound to the switch and will be auto-closed *)
+
()
+
) pool.endpoints;
+
+
Hashtbl.clear pool.endpoints
+
)
+
);
+
+
pool
+
+
(** {1 Public API - Connection Management} *)
+
+
let with_connection (pool : ('clock Eio.Time.clock, 'net Eio.Net.t) t) endpoint f =
+
Log.debug (fun m -> m "Acquiring connection to %a" Endpoint.pp endpoint);
+
let ep_pool = get_or_create_endpoint_pool pool endpoint in
+
+
(* Increment active count *)
+
Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
+
ep_pool.stats.active <- ep_pool.stats.active + 1
+
);
+
+
Fun.protect
+
~finally:(fun () ->
+
(* Decrement active count *)
+
Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
+
ep_pool.stats.active <- ep_pool.stats.active - 1
+
);
+
Log.debug (fun m -> m "Released connection to %a" Endpoint.pp endpoint)
+
)
+
(fun () ->
+
(* Use Eio.Pool for resource management *)
+
Eio.Pool.use ep_pool.pool (fun conn ->
+
Log.debug (fun m -> m "Using connection to %a (uses=%d)"
+
Endpoint.pp endpoint (Connection.use_count conn));
+
+
(* Update last used time and use count *)
+
conn.last_used <- get_time pool;
+
conn.use_count <- conn.use_count + 1;
+
+
(* Update idle stats (connection taken from idle pool) *)
+
Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
+
ep_pool.stats.idle <- max 0 (ep_pool.stats.idle - 1)
+
);
+
+
try
+
let result = f conn.flow in
+
+
(* Success - connection will be returned to pool by Eio.Pool *)
+
(* Update idle stats (connection returned to idle pool) *)
+
Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
+
ep_pool.stats.idle <- ep_pool.stats.idle + 1
+
);
+
+
result
+
with e ->
+
(* Error - close connection so it won't be reused *)
+
Log.warn (fun m -> m "Error using connection to %a: %s"
+
Endpoint.pp endpoint (Printexc.to_string e));
+
close_internal pool conn;
+
+
(* Update error stats *)
+
Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
+
ep_pool.stats.errors <- ep_pool.stats.errors + 1
+
);
+
+
raise e
+
)
+
)
+
+
(** {1 Public API - Statistics} *)
+
+
let stats (pool : ('clock Eio.Time.clock, 'net Eio.Net.t) t) endpoint =
+
match Hashtbl.find_opt pool.endpoints endpoint with
+
| Some ep_pool ->
+
Eio.Mutex.use_ro ep_pool.mutex (fun () ->
+
snapshot_stats ep_pool.stats
+
)
+
| None ->
+
(* No pool for this endpoint yet *)
+
{
+
Stats.active = 0;
+
idle = 0;
+
total_created = 0;
+
total_reused = 0;
+
total_closed = 0;
+
errors = 0;
+
}
+
+
let all_stats (pool : ('clock Eio.Time.clock, 'net Eio.Net.t) t) =
+
Eio.Mutex.use_ro pool.endpoints_mutex (fun () ->
+
Hashtbl.fold (fun endpoint ep_pool acc ->
+
let stats = Eio.Mutex.use_ro ep_pool.mutex (fun () ->
+
snapshot_stats ep_pool.stats
+
) in
+
(endpoint, stats) :: acc
+
) pool.endpoints []
+
)
+
+
(** {1 Public API - Pool Management} *)
+
+
let clear_endpoint (pool : ('clock Eio.Time.clock, 'net Eio.Net.t) t) endpoint =
+
Log.info (fun m -> m "Clearing endpoint %a from pool" Endpoint.pp endpoint);
+
match Hashtbl.find_opt pool.endpoints endpoint with
+
| Some _ep_pool ->
+
Eio.Cancel.protect (fun () ->
+
(* Remove endpoint pool from hashtable *)
+
(* Idle connections will be discarded *)
+
(* Active connections will be closed when returned *)
+
Eio.Mutex.use_rw ~protect:true pool.endpoints_mutex (fun () ->
+
Hashtbl.remove pool.endpoints endpoint
+
)
+
)
+
| None ->
+
Log.debug (fun m -> m "No endpoint pool found for %a" Endpoint.pp endpoint)
+213
lib/conpool.mli
···
+
(** Conpool - Protocol-agnostic TCP/IP connection pooling library for Eio *)
+
+
(** {1 Logging} *)
+
+
val src : Logs.Src.t
+
(** Logs source for conpool. Configure logging with:
+
{[
+
Logs.Src.set_level Conpool.src (Some Logs.Debug);
+
Logs.set_reporter (Logs_fmt.reporter ());
+
]}
+
*)
+
+
(** {1 Core Types} *)
+
+
(** Network endpoint *)
+
module Endpoint : sig
+
type t
+
(** Network endpoint identified by host and port *)
+
+
val make : host:string -> port:int -> t
+
(** Create an endpoint *)
+
+
val host : t -> string
+
(** Get the hostname *)
+
+
val port : t -> int
+
(** Get the port number *)
+
+
val pp : Format.formatter -> t -> unit
+
(** Pretty-print an endpoint *)
+
+
val equal : t -> t -> bool
+
(** Compare two endpoints for equality *)
+
+
val hash : t -> int
+
(** Hash an endpoint *)
+
end
+
+
(** TLS configuration *)
+
module Tls_config : sig
+
type t
+
(** TLS configuration applied to all connections in a pool *)
+
+
val make : config:Tls.Config.client -> ?servername:string -> unit -> t
+
(** Create TLS configuration.
+
@param config TLS client configuration
+
@param servername Optional SNI server name override. If None, uses endpoint host *)
+
+
val config : t -> Tls.Config.client
+
(** Get the TLS client configuration *)
+
+
val servername : t -> string option
+
(** Get the SNI server name override *)
+
+
val pp : Format.formatter -> t -> unit
+
(** Pretty-print TLS configuration *)
+
end
+
+
+
(** Pool configuration *)
+
module Config : sig
+
type t
+
(** Pool configuration *)
+
+
val make :
+
?max_connections_per_endpoint:int ->
+
?max_idle_time:float ->
+
?max_connection_lifetime:float ->
+
?max_connection_uses:int ->
+
?health_check:([ `Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t -> bool) ->
+
?connect_timeout:float ->
+
?connect_retry_count:int ->
+
?connect_retry_delay:float ->
+
?on_connection_created:(Endpoint.t -> unit) ->
+
?on_connection_closed:(Endpoint.t -> unit) ->
+
?on_connection_reused:(Endpoint.t -> unit) ->
+
unit -> t
+
(** Create pool configuration with optional parameters.
+
See field descriptions for defaults. *)
+
+
val default : t
+
(** Sensible defaults for most use cases:
+
- max_connections_per_endpoint: 10
+
- max_idle_time: 60.0s
+
- max_connection_lifetime: 300.0s
+
- max_connection_uses: None (unlimited)
+
- health_check: None
+
- connect_timeout: 10.0s
+
- connect_retry_count: 3
+
- connect_retry_delay: 0.1s
+
- hooks: None *)
+
+
val max_connections_per_endpoint : t -> int
+
val max_idle_time : t -> float
+
val max_connection_lifetime : t -> float
+
val max_connection_uses : t -> int option
+
val health_check : t -> ([ `Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t -> bool) option
+
val connect_timeout : t -> float option
+
val connect_retry_count : t -> int
+
val connect_retry_delay : t -> float
+
+
val pp : Format.formatter -> t -> unit
+
(** Pretty-print configuration *)
+
end
+
+
(** Statistics for an endpoint *)
+
module Stats : sig
+
type t
+
(** Statistics for a specific endpoint *)
+
+
val active : t -> int
+
(** Connections currently in use *)
+
+
val idle : t -> int
+
(** Connections in pool waiting to be reused *)
+
+
val total_created : t -> int
+
(** Total connections created (lifetime) *)
+
+
val total_reused : t -> int
+
(** Total times connections were reused *)
+
+
val total_closed : t -> int
+
(** Total connections closed *)
+
+
val errors : t -> int
+
(** Total connection errors *)
+
+
val pp : Format.formatter -> t -> unit
+
(** Pretty-print endpoint statistics *)
+
end
+
+
(** {1 Connection Pool} *)
+
+
type ('clock, 'net) t
+
(** Connection pool managing multiple endpoints, parameterized by clock and network types *)
+
+
val create :
+
sw:Eio.Switch.t ->
+
net:'net Eio.Net.t ->
+
clock:'clock Eio.Time.clock ->
+
?tls:Tls_config.t ->
+
?config:Config.t ->
+
unit -> ('clock Eio.Time.clock, 'net Eio.Net.t) t
+
(** Create connection pool bound to switch.
+
All connections will be closed when switch is released.
+
+
@param sw Switch for resource management
+
@param net Network interface for creating connections
+
@param clock Clock for timeouts and time-based validation
+
@param tls Optional TLS configuration applied to all connections
+
@param config Optional pool configuration (uses Config.default if not provided) *)
+
+
(** {1 Connection Usage} *)
+
+
val with_connection :
+
('clock Eio.Time.clock, 'net Eio.Net.t) t ->
+
Endpoint.t ->
+
([ `Close | `Flow | `R | `Shutdown | `W ] Eio.Resource.t -> 'a) ->
+
'a
+
(** Acquire connection, use it, automatically release back to pool.
+
+
This is the only way to use connections from the pool. All resource management
+
is handled automatically through Eio's switch mechanism.
+
+
If idle connection available and healthy:
+
- Reuse from pool (validates health first)
+
Else:
+
- Create new connection (may block if endpoint at limit)
+
+
On success: connection returned to pool for reuse
+
On error: connection closed, not returned to pool
+
+
Example:
+
{[
+
let endpoint = Conpool.Endpoint.make ~host:"example.com" ~port:443 in
+
Conpool.with_connection pool endpoint (fun conn ->
+
(* Use conn for HTTP request, Redis command, etc. *)
+
Eio.Flow.copy_string "GET / HTTP/1.1\r\n\r\n" conn;
+
let buf = Eio.Buf_read.of_flow conn ~max_size:4096 in
+
Eio.Buf_read.take_all buf
+
)
+
]}
+
*)
+
+
(** {1 Statistics & Monitoring} *)
+
+
val stats :
+
('clock Eio.Time.clock, 'net Eio.Net.t) t ->
+
Endpoint.t ->
+
Stats.t
+
(** Get statistics for specific endpoint *)
+
+
val all_stats :
+
('clock Eio.Time.clock, 'net Eio.Net.t) t ->
+
(Endpoint.t * Stats.t) list
+
(** Get statistics for all endpoints in pool *)
+
+
(** {1 Pool Management} *)
+
+
val clear_endpoint :
+
('clock Eio.Time.clock, 'net Eio.Net.t) t ->
+
Endpoint.t ->
+
unit
+
(** Clear all cached connections for a specific endpoint.
+
+
This removes the endpoint from the pool, discarding all idle connections.
+
Active connections will continue to work but won't be returned to the pool.
+
+
Use this when you know an endpoint's connections are no longer valid
+
(e.g., server restarted, network reconfigured, credentials changed).
+
+
The pool will be automatically cleaned up when its switch is released. *)
+4
lib/dune
···
+
(library
+
(name conpool)
+
(public_name conpool)
+
(libraries eio eio.unix tls-eio logs))