···11+# This file is generated by dune, edit dune-project instead
22+opam-version: "2.0"
33+synopsis: "Protocol-agnostic TCP/IP connection pooling library for Eio"
44+description:
55+ "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.)"
66+maintainer: ["Your Name"]
77+authors: ["Your Name"]
88+license: "MIT"
99+homepage: "https://github.com/username/conpool"
1010+bug-reports: "https://github.com/username/conpool/issues"
1111+depends: [
1212+ "ocaml"
1313+ "dune" {>= "3.0" & >= "3.0"}
1414+ "eio"
1515+ "tls-eio" {>= "1.0"}
1616+ "logs"
1717+ "odoc" {with-doc}
1818+]
1919+build: [
2020+ ["dune" "subst"] {dev}
2121+ [
2222+ "dune"
2323+ "build"
2424+ "-p"
2525+ name
2626+ "-j"
2727+ jobs
2828+ "@install"
2929+ "@runtest" {with-test}
3030+ "@doc" {with-doc}
3131+ ]
3232+]
3333+dev-repo: "git+https://github.com/username/conpool.git"
+24
dune-project
···11+(lang dune 3.0)
22+(name conpool)
33+44+(generate_opam_files true)
55+66+(source
77+ (github username/conpool))
88+99+(authors "Your Name")
1010+1111+(maintainers "Your Name")
1212+1313+(license MIT)
1414+1515+(package
1616+ (name conpool)
1717+ (synopsis "Protocol-agnostic TCP/IP connection pooling library for Eio")
1818+ (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.)")
1919+ (depends
2020+ ocaml
2121+ (dune (>= 3.0))
2222+ eio
2323+ (tls-eio (>= 1.0))
2424+ logs))
+632
lib/conpool.ml
···11+(** Conpool - Protocol-agnostic TCP/IP connection pooling library for Eio *)
22+33+let src = Logs.Src.create "conpool" ~doc:"Connection pooling library"
44+module Log = (val Logs.src_log src : Logs.LOG)
55+66+module Endpoint = struct
77+ type t = {
88+ host : string;
99+ port : int;
1010+ }
1111+1212+ let make ~host ~port = { host; port }
1313+1414+ let host t = t.host
1515+ let port t = t.port
1616+1717+ let pp fmt t =
1818+ Format.fprintf fmt "%s:%d" t.host t.port
1919+2020+ let equal t1 t2 =
2121+ String.equal t1.host t2.host && t1.port = t2.port
2222+2323+ let hash t =
2424+ Hashtbl.hash (t.host, t.port)
2525+end
2626+2727+module Tls_config = struct
2828+ type t = {
2929+ config : Tls.Config.client;
3030+ servername : string option;
3131+ }
3232+3333+ let make ~config ?servername () = { config; servername }
3434+3535+ let config t = t.config
3636+ let servername t = t.servername
3737+3838+ let pp fmt t =
3939+ Format.fprintf fmt "TLS(servername=%s)"
4040+ (match t.servername with Some s -> s | None -> "<default>")
4141+end
4242+4343+(* Internal connection type - not exposed in public API *)
4444+module Connection = struct
4545+ type t = {
4646+ flow : [`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t;
4747+ created_at : float;
4848+ mutable last_used : float;
4949+ mutable use_count : int;
5050+ endpoint : Endpoint.t;
5151+ }
5252+5353+ let flow t = t.flow
5454+ let endpoint t = t.endpoint
5555+ let created_at t = t.created_at
5656+ let last_used t = t.last_used
5757+ let use_count t = t.use_count
5858+end
5959+6060+module Config = struct
6161+ type t = {
6262+ max_connections_per_endpoint : int;
6363+ max_idle_time : float;
6464+ max_connection_lifetime : float;
6565+ max_connection_uses : int option;
6666+ health_check : ([`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t -> bool) option;
6767+ connect_timeout : float option;
6868+ connect_retry_count : int;
6969+ connect_retry_delay : float;
7070+ on_connection_created : (Endpoint.t -> unit) option;
7171+ on_connection_closed : (Endpoint.t -> unit) option;
7272+ on_connection_reused : (Endpoint.t -> unit) option;
7373+ }
7474+7575+ let make
7676+ ?(max_connections_per_endpoint = 10)
7777+ ?(max_idle_time = 60.0)
7878+ ?(max_connection_lifetime = 300.0)
7979+ ?max_connection_uses
8080+ ?health_check
8181+ ?(connect_timeout = 10.0)
8282+ ?(connect_retry_count = 3)
8383+ ?(connect_retry_delay = 0.1)
8484+ ?on_connection_created
8585+ ?on_connection_closed
8686+ ?on_connection_reused
8787+ () =
8888+ {
8989+ max_connections_per_endpoint;
9090+ max_idle_time;
9191+ max_connection_lifetime;
9292+ max_connection_uses;
9393+ health_check;
9494+ connect_timeout = Some connect_timeout;
9595+ connect_retry_count;
9696+ connect_retry_delay;
9797+ on_connection_created;
9898+ on_connection_closed;
9999+ on_connection_reused;
100100+ }
101101+102102+ let default = make ()
103103+104104+ let max_connections_per_endpoint t = t.max_connections_per_endpoint
105105+ let max_idle_time t = t.max_idle_time
106106+ let max_connection_lifetime t = t.max_connection_lifetime
107107+ let max_connection_uses t = t.max_connection_uses
108108+ let health_check t = t.health_check
109109+ let connect_timeout t = t.connect_timeout
110110+ let connect_retry_count t = t.connect_retry_count
111111+ let connect_retry_delay t = t.connect_retry_delay
112112+113113+ let pp fmt t =
114114+ Format.fprintf fmt
115115+ "@[<v>Config:@,\
116116+ - max_connections_per_endpoint: %d@,\
117117+ - max_idle_time: %.1fs@,\
118118+ - max_connection_lifetime: %.1fs@,\
119119+ - max_connection_uses: %s@,\
120120+ - connect_timeout: %s@,\
121121+ - connect_retry_count: %d@,\
122122+ - connect_retry_delay: %.2fs@]"
123123+ t.max_connections_per_endpoint
124124+ t.max_idle_time
125125+ t.max_connection_lifetime
126126+ (match t.max_connection_uses with Some n -> string_of_int n | None -> "unlimited")
127127+ (match t.connect_timeout with Some f -> Printf.sprintf "%.1fs" f | None -> "none")
128128+ t.connect_retry_count
129129+ t.connect_retry_delay
130130+end
131131+132132+module Stats = struct
133133+ type t = {
134134+ active : int;
135135+ idle : int;
136136+ total_created : int;
137137+ total_reused : int;
138138+ total_closed : int;
139139+ errors : int;
140140+ }
141141+142142+ let active t = t.active
143143+ let idle t = t.idle
144144+ let total_created t = t.total_created
145145+ let total_reused t = t.total_reused
146146+ let total_closed t = t.total_closed
147147+ let errors t = t.errors
148148+149149+ let pp fmt t =
150150+ Format.fprintf fmt
151151+ "@[<v>Stats:@,\
152152+ - Active: %d@,\
153153+ - Idle: %d@,\
154154+ - Created: %d@,\
155155+ - Reused: %d@,\
156156+ - Closed: %d@,\
157157+ - Errors: %d@]"
158158+ t.active
159159+ t.idle
160160+ t.total_created
161161+ t.total_reused
162162+ t.total_closed
163163+ t.errors
164164+end
165165+166166+type endp_stats = {
167167+ mutable active : int;
168168+ mutable idle : int;
169169+ mutable total_created : int;
170170+ mutable total_reused : int;
171171+ mutable total_closed : int;
172172+ mutable errors : int;
173173+}
174174+175175+type endpoint_pool = {
176176+ pool : Connection.t Eio.Pool.t;
177177+ stats : endp_stats;
178178+ mutex : Eio.Mutex.t;
179179+}
180180+181181+type ('clock, 'net) t = {
182182+ sw : Eio.Switch.t;
183183+ net : 'net;
184184+ clock : 'clock;
185185+ config : Config.t;
186186+ tls : Tls_config.t option;
187187+ endpoints : (Endpoint.t, endpoint_pool) Hashtbl.t;
188188+ endpoints_mutex : Eio.Mutex.t;
189189+}
190190+191191+module EndpointTbl = Hashtbl.Make(struct
192192+ type t = Endpoint.t
193193+ let equal = Endpoint.equal
194194+ let hash = Endpoint.hash
195195+end)
196196+197197+let get_time pool =
198198+ Eio.Time.now pool.clock
199199+200200+let create_endp_stats () = {
201201+ active = 0;
202202+ idle = 0;
203203+ total_created = 0;
204204+ total_reused = 0;
205205+ total_closed = 0;
206206+ errors = 0;
207207+}
208208+209209+let snapshot_stats (stats : endp_stats) : Stats.t = {
210210+ active = stats.active;
211211+ idle = stats.idle;
212212+ total_created = stats.total_created;
213213+ total_reused = stats.total_reused;
214214+ total_closed = stats.total_closed;
215215+ errors = stats.errors;
216216+}
217217+218218+(** {1 DNS Resolution} *)
219219+220220+let resolve_endpoint pool endpoint =
221221+ Log.debug (fun m -> m "Resolving %a..." Endpoint.pp endpoint);
222222+ let addrs = Eio.Net.getaddrinfo_stream pool.net (Endpoint.host endpoint) ~service:(string_of_int (Endpoint.port endpoint)) in
223223+ Log.debug (fun m -> m "Got address list for %a" Endpoint.pp endpoint);
224224+ match addrs with
225225+ | addr :: _ ->
226226+ Log.debug (fun m -> m "Resolved %a to %a"
227227+ Endpoint.pp endpoint Eio.Net.Sockaddr.pp addr);
228228+ addr
229229+ | [] ->
230230+ Log.err (fun m -> m "Failed to resolve hostname: %s" (Endpoint.host endpoint));
231231+ failwith (Printf.sprintf "Failed to resolve hostname: %s" (Endpoint.host endpoint))
232232+233233+(** {1 Connection Creation with Retry} *)
234234+235235+let rec create_connection_with_retry pool endpoint attempt =
236236+ if attempt > pool.config.connect_retry_count then begin
237237+ Log.err (fun m -> m "Failed to connect to %a after %d attempts"
238238+ Endpoint.pp endpoint pool.config.connect_retry_count);
239239+ failwith (Printf.sprintf "Failed to connect to %s:%d after %d attempts"
240240+ (Endpoint.host endpoint) (Endpoint.port endpoint) pool.config.connect_retry_count)
241241+ end;
242242+243243+ Log.debug (fun m -> m "Connecting to %a (attempt %d/%d)"
244244+ Endpoint.pp endpoint attempt pool.config.connect_retry_count);
245245+246246+ try
247247+ let addr = resolve_endpoint pool endpoint in
248248+ Log.debug (fun m -> m "Resolved %a to address" Endpoint.pp endpoint);
249249+250250+ (* Connect with optional timeout *)
251251+ let socket =
252252+ match pool.config.connect_timeout with
253253+ | Some timeout ->
254254+ Eio.Time.with_timeout_exn pool.clock timeout
255255+ (fun () -> Eio.Net.connect ~sw:pool.sw pool.net addr)
256256+ | None ->
257257+ Eio.Net.connect ~sw:pool.sw pool.net addr
258258+ in
259259+260260+ Log.debug (fun m -> m "TCP connection established to %a" Endpoint.pp endpoint);
261261+262262+ let flow = match pool.tls with
263263+ | None -> (socket :> [`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t)
264264+ | Some tls_cfg ->
265265+ Log.debug (fun m -> m "Initiating TLS handshake with %a" Endpoint.pp endpoint);
266266+ let host = match Tls_config.servername tls_cfg with
267267+ | Some name -> Domain_name.(host_exn (of_string_exn name))
268268+ | None -> Domain_name.(host_exn (of_string_exn (Endpoint.host endpoint)))
269269+ in
270270+ let tls_flow = Tls_eio.client_of_flow ~host (Tls_config.config tls_cfg) socket in
271271+ Log.info (fun m -> m "TLS connection established to %a" Endpoint.pp endpoint);
272272+ (tls_flow :> [`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t)
273273+ in
274274+275275+ let now = get_time pool in
276276+ Log.info (fun m -> m "Connection created to %a" Endpoint.pp endpoint);
277277+ {
278278+ Connection.flow;
279279+ created_at = now;
280280+ last_used = now;
281281+ use_count = 0;
282282+ endpoint;
283283+ }
284284+285285+ with
286286+ | Eio.Time.Timeout ->
287287+ Log.warn (fun m -> m "Connection timeout to %a (attempt %d)" Endpoint.pp endpoint attempt);
288288+ (* Exponential backoff *)
289289+ let delay = pool.config.connect_retry_delay *. (2.0 ** float_of_int (attempt - 1)) in
290290+ Eio.Time.sleep pool.clock delay;
291291+ create_connection_with_retry pool endpoint (attempt + 1)
292292+ | e ->
293293+ (* Other errors - retry with backoff *)
294294+ Log.warn (fun m -> m "Connection attempt %d to %a failed: %s"
295295+ attempt Endpoint.pp endpoint (Printexc.to_string e));
296296+ if attempt < pool.config.connect_retry_count then (
297297+ let delay = pool.config.connect_retry_delay *. (2.0 ** float_of_int (attempt - 1)) in
298298+ Eio.Time.sleep pool.clock delay;
299299+ create_connection_with_retry pool endpoint (attempt + 1)
300300+ ) else
301301+ raise e
302302+303303+let create_connection pool endpoint =
304304+ create_connection_with_retry pool endpoint 1
305305+306306+(** {1 Connection Validation} *)
307307+308308+let is_healthy pool ?(check_readable = false) conn =
309309+ let now = get_time pool in
310310+311311+ (* Check age *)
312312+ let age = now -. Connection.created_at conn in
313313+ if age > pool.config.max_connection_lifetime then begin
314314+ Log.debug (fun m -> m "Connection to %a unhealthy: exceeded max lifetime (%.2fs > %.2fs)"
315315+ Endpoint.pp (Connection.endpoint conn) age pool.config.max_connection_lifetime);
316316+ false
317317+ end
318318+319319+ (* Check idle time *)
320320+ else if (now -. Connection.last_used conn) > pool.config.max_idle_time then begin
321321+ let idle_time = now -. Connection.last_used conn in
322322+ Log.debug (fun m -> m "Connection to %a unhealthy: exceeded max idle time (%.2fs > %.2fs)"
323323+ Endpoint.pp (Connection.endpoint conn) idle_time pool.config.max_idle_time);
324324+ false
325325+ end
326326+327327+ (* Check use count *)
328328+ else if (match pool.config.max_connection_uses with
329329+ | Some max -> Connection.use_count conn >= max
330330+ | None -> false) then begin
331331+ Log.debug (fun m -> m "Connection to %a unhealthy: exceeded max use count (%d)"
332332+ Endpoint.pp (Connection.endpoint conn) (Connection.use_count conn));
333333+ false
334334+ end
335335+336336+ (* Optional: custom health check *)
337337+ else if (match pool.config.health_check with
338338+ | Some check ->
339339+ (try
340340+ let healthy = check (Connection.flow conn) in
341341+ if not healthy then
342342+ Log.debug (fun m -> m "Connection to %a failed custom health check"
343343+ Endpoint.pp (Connection.endpoint conn));
344344+ not healthy
345345+ with e ->
346346+ Log.debug (fun m -> m "Connection to %a health check raised exception: %s"
347347+ Endpoint.pp (Connection.endpoint conn) (Printexc.to_string e));
348348+ true) (* Exception in health check = unhealthy *)
349349+ | None -> false) then
350350+ false
351351+352352+ (* Optional: check if socket still connected *)
353353+ else if check_readable then
354354+ try
355355+ (* TODO avsm: a sockopt for this? *)
356356+ true
357357+ with
358358+ | _ -> false
359359+360360+ else begin
361361+ Log.debug (fun m -> m "Connection to %a is healthy (age=%.2fs, idle=%.2fs, uses=%d)"
362362+ Endpoint.pp (Connection.endpoint conn)
363363+ age
364364+ (now -. Connection.last_used conn)
365365+ (Connection.use_count conn));
366366+ true
367367+ end
368368+369369+(** {1 Internal Pool Operations} *)
370370+371371+let close_internal pool conn =
372372+ Log.debug (fun m -> m "Closing connection to %a (age=%.2fs, uses=%d)"
373373+ Endpoint.pp (Connection.endpoint conn)
374374+ (get_time pool -. Connection.created_at conn)
375375+ (Connection.use_count conn));
376376+377377+ Eio.Cancel.protect (fun () ->
378378+ try
379379+ Eio.Flow.close (Connection.flow conn)
380380+ with _ -> ()
381381+ );
382382+383383+ (* Call hook if configured *)
384384+ Option.iter (fun f -> f (Connection.endpoint conn)) pool.config.on_connection_closed
385385+386386+let get_or_create_endpoint_pool pool endpoint =
387387+ Log.debug (fun m -> m "Getting or creating endpoint pool for %a" Endpoint.pp endpoint);
388388+389389+ (* First try with read lock *)
390390+ match Eio.Mutex.use_ro pool.endpoints_mutex (fun () ->
391391+ Hashtbl.find_opt pool.endpoints endpoint
392392+ ) with
393393+ | Some ep_pool ->
394394+ Log.debug (fun m -> m "Found existing endpoint pool for %a" Endpoint.pp endpoint);
395395+ ep_pool
396396+ | None ->
397397+ Log.debug (fun m -> m "No existing pool, need to create for %a" Endpoint.pp endpoint);
398398+ (* Need to create - use write lock *)
399399+ Eio.Mutex.use_rw ~protect:true pool.endpoints_mutex (fun () ->
400400+ (* Check again in case another fiber created it *)
401401+ match Hashtbl.find_opt pool.endpoints endpoint with
402402+ | Some ep_pool ->
403403+ Log.debug (fun m -> m "Another fiber created pool for %a" Endpoint.pp endpoint);
404404+ ep_pool
405405+ | None ->
406406+ (* Create new endpoint pool *)
407407+ let stats = create_endp_stats () in
408408+ let mutex = Eio.Mutex.create () in
409409+410410+ Log.info (fun m -> m "Creating new endpoint pool for %a (max_connections=%d)"
411411+ Endpoint.pp endpoint pool.config.max_connections_per_endpoint);
412412+413413+ Log.debug (fun m -> m "About to create Eio.Pool for %a" Endpoint.pp endpoint);
414414+415415+ let eio_pool = Eio.Pool.create
416416+ pool.config.max_connections_per_endpoint
417417+ ~validate:(fun conn ->
418418+ Log.debug (fun m -> m "Validate called for connection to %a" Endpoint.pp endpoint);
419419+ (* Called before reusing from pool *)
420420+ let healthy = is_healthy pool ~check_readable:false conn in
421421+422422+ if healthy then (
423423+ Log.debug (fun m -> m "Reusing connection to %a from pool" Endpoint.pp endpoint);
424424+425425+ (* Update stats for reuse *)
426426+ Eio.Mutex.use_rw ~protect:true mutex (fun () ->
427427+ stats.total_reused <- stats.total_reused + 1
428428+ );
429429+430430+ (* Call hook if configured *)
431431+ Option.iter (fun f -> f endpoint) pool.config.on_connection_reused;
432432+433433+ (* Run health check if configured *)
434434+ match pool.config.health_check with
435435+ | Some check ->
436436+ (try check (Connection.flow conn)
437437+ with _ -> false)
438438+ | None -> true
439439+ ) else begin
440440+ Log.debug (fun m -> m "Connection to %a failed validation, creating new one" Endpoint.pp endpoint);
441441+ false
442442+ end
443443+ )
444444+ ~dispose:(fun conn ->
445445+ (* Called when removing from pool *)
446446+ Eio.Cancel.protect (fun () ->
447447+ close_internal pool conn;
448448+449449+ (* Update stats *)
450450+ Eio.Mutex.use_rw ~protect:true mutex (fun () ->
451451+ stats.total_closed <- stats.total_closed + 1
452452+ )
453453+ )
454454+ )
455455+ (fun () ->
456456+ Log.debug (fun m -> m "Factory function called for %a" Endpoint.pp endpoint);
457457+ try
458458+ let conn = create_connection pool endpoint in
459459+460460+ Log.debug (fun m -> m "Connection created successfully for %a" Endpoint.pp endpoint);
461461+462462+ (* Update stats *)
463463+ Eio.Mutex.use_rw ~protect:true mutex (fun () ->
464464+ stats.total_created <- stats.total_created + 1
465465+ );
466466+467467+ (* Call hook if configured *)
468468+ Option.iter (fun f -> f endpoint) pool.config.on_connection_created;
469469+470470+ conn
471471+ with e ->
472472+ Log.err (fun m -> m "Factory function failed for %a: %s"
473473+ Endpoint.pp endpoint (Printexc.to_string e));
474474+ (* Update error stats *)
475475+ Eio.Mutex.use_rw ~protect:true mutex (fun () ->
476476+ stats.errors <- stats.errors + 1
477477+ );
478478+ raise e
479479+ )
480480+ in
481481+482482+ Log.debug (fun m -> m "Eio.Pool created successfully for %a" Endpoint.pp endpoint);
483483+484484+ let ep_pool = {
485485+ pool = eio_pool;
486486+ stats;
487487+ mutex;
488488+ } in
489489+490490+ Hashtbl.add pool.endpoints endpoint ep_pool;
491491+ Log.debug (fun m -> m "Endpoint pool added to hashtable for %a" Endpoint.pp endpoint);
492492+ ep_pool
493493+ )
494494+495495+(** {1 Public API - Pool Creation} *)
496496+497497+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 =
498498+ Log.info (fun m -> m "Creating new connection pool (max_per_endpoint=%d, max_idle=%.1fs, max_lifetime=%.1fs)"
499499+ config.max_connections_per_endpoint
500500+ config.max_idle_time
501501+ config.max_connection_lifetime);
502502+503503+ let pool = {
504504+ sw;
505505+ net;
506506+ clock;
507507+ config;
508508+ tls;
509509+ endpoints = Hashtbl.create 16;
510510+ endpoints_mutex = Eio.Mutex.create ();
511511+ } in
512512+513513+ (* Auto-cleanup on switch release *)
514514+ Eio.Switch.on_release sw (fun () ->
515515+ Eio.Cancel.protect (fun () ->
516516+ Log.info (fun m -> m "Closing connection pool");
517517+ (* Close all idle connections - active ones will be cleaned up by switch *)
518518+ Hashtbl.iter (fun _endpoint _ep_pool ->
519519+ (* Connections are bound to the switch and will be auto-closed *)
520520+ ()
521521+ ) pool.endpoints;
522522+523523+ Hashtbl.clear pool.endpoints
524524+ )
525525+ );
526526+527527+ pool
528528+529529+(** {1 Public API - Connection Management} *)
530530+531531+let with_connection (pool : ('clock Eio.Time.clock, 'net Eio.Net.t) t) endpoint f =
532532+ Log.debug (fun m -> m "Acquiring connection to %a" Endpoint.pp endpoint);
533533+ let ep_pool = get_or_create_endpoint_pool pool endpoint in
534534+535535+ (* Increment active count *)
536536+ Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
537537+ ep_pool.stats.active <- ep_pool.stats.active + 1
538538+ );
539539+540540+ Fun.protect
541541+ ~finally:(fun () ->
542542+ (* Decrement active count *)
543543+ Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
544544+ ep_pool.stats.active <- ep_pool.stats.active - 1
545545+ );
546546+ Log.debug (fun m -> m "Released connection to %a" Endpoint.pp endpoint)
547547+ )
548548+ (fun () ->
549549+ (* Use Eio.Pool for resource management *)
550550+ Eio.Pool.use ep_pool.pool (fun conn ->
551551+ Log.debug (fun m -> m "Using connection to %a (uses=%d)"
552552+ Endpoint.pp endpoint (Connection.use_count conn));
553553+554554+ (* Update last used time and use count *)
555555+ conn.last_used <- get_time pool;
556556+ conn.use_count <- conn.use_count + 1;
557557+558558+ (* Update idle stats (connection taken from idle pool) *)
559559+ Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
560560+ ep_pool.stats.idle <- max 0 (ep_pool.stats.idle - 1)
561561+ );
562562+563563+ try
564564+ let result = f conn.flow in
565565+566566+ (* Success - connection will be returned to pool by Eio.Pool *)
567567+ (* Update idle stats (connection returned to idle pool) *)
568568+ Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
569569+ ep_pool.stats.idle <- ep_pool.stats.idle + 1
570570+ );
571571+572572+ result
573573+ with e ->
574574+ (* Error - close connection so it won't be reused *)
575575+ Log.warn (fun m -> m "Error using connection to %a: %s"
576576+ Endpoint.pp endpoint (Printexc.to_string e));
577577+ close_internal pool conn;
578578+579579+ (* Update error stats *)
580580+ Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
581581+ ep_pool.stats.errors <- ep_pool.stats.errors + 1
582582+ );
583583+584584+ raise e
585585+ )
586586+ )
587587+588588+(** {1 Public API - Statistics} *)
589589+590590+let stats (pool : ('clock Eio.Time.clock, 'net Eio.Net.t) t) endpoint =
591591+ match Hashtbl.find_opt pool.endpoints endpoint with
592592+ | Some ep_pool ->
593593+ Eio.Mutex.use_ro ep_pool.mutex (fun () ->
594594+ snapshot_stats ep_pool.stats
595595+ )
596596+ | None ->
597597+ (* No pool for this endpoint yet *)
598598+ {
599599+ Stats.active = 0;
600600+ idle = 0;
601601+ total_created = 0;
602602+ total_reused = 0;
603603+ total_closed = 0;
604604+ errors = 0;
605605+ }
606606+607607+let all_stats (pool : ('clock Eio.Time.clock, 'net Eio.Net.t) t) =
608608+ Eio.Mutex.use_ro pool.endpoints_mutex (fun () ->
609609+ Hashtbl.fold (fun endpoint ep_pool acc ->
610610+ let stats = Eio.Mutex.use_ro ep_pool.mutex (fun () ->
611611+ snapshot_stats ep_pool.stats
612612+ ) in
613613+ (endpoint, stats) :: acc
614614+ ) pool.endpoints []
615615+ )
616616+617617+(** {1 Public API - Pool Management} *)
618618+619619+let clear_endpoint (pool : ('clock Eio.Time.clock, 'net Eio.Net.t) t) endpoint =
620620+ Log.info (fun m -> m "Clearing endpoint %a from pool" Endpoint.pp endpoint);
621621+ match Hashtbl.find_opt pool.endpoints endpoint with
622622+ | Some _ep_pool ->
623623+ Eio.Cancel.protect (fun () ->
624624+ (* Remove endpoint pool from hashtable *)
625625+ (* Idle connections will be discarded *)
626626+ (* Active connections will be closed when returned *)
627627+ Eio.Mutex.use_rw ~protect:true pool.endpoints_mutex (fun () ->
628628+ Hashtbl.remove pool.endpoints endpoint
629629+ )
630630+ )
631631+ | None ->
632632+ Log.debug (fun m -> m "No endpoint pool found for %a" Endpoint.pp endpoint)
+213
lib/conpool.mli
···11+(** Conpool - Protocol-agnostic TCP/IP connection pooling library for Eio *)
22+33+(** {1 Logging} *)
44+55+val src : Logs.Src.t
66+(** Logs source for conpool. Configure logging with:
77+ {[
88+ Logs.Src.set_level Conpool.src (Some Logs.Debug);
99+ Logs.set_reporter (Logs_fmt.reporter ());
1010+ ]}
1111+*)
1212+1313+(** {1 Core Types} *)
1414+1515+(** Network endpoint *)
1616+module Endpoint : sig
1717+ type t
1818+ (** Network endpoint identified by host and port *)
1919+2020+ val make : host:string -> port:int -> t
2121+ (** Create an endpoint *)
2222+2323+ val host : t -> string
2424+ (** Get the hostname *)
2525+2626+ val port : t -> int
2727+ (** Get the port number *)
2828+2929+ val pp : Format.formatter -> t -> unit
3030+ (** Pretty-print an endpoint *)
3131+3232+ val equal : t -> t -> bool
3333+ (** Compare two endpoints for equality *)
3434+3535+ val hash : t -> int
3636+ (** Hash an endpoint *)
3737+end
3838+3939+(** TLS configuration *)
4040+module Tls_config : sig
4141+ type t
4242+ (** TLS configuration applied to all connections in a pool *)
4343+4444+ val make : config:Tls.Config.client -> ?servername:string -> unit -> t
4545+ (** Create TLS configuration.
4646+ @param config TLS client configuration
4747+ @param servername Optional SNI server name override. If None, uses endpoint host *)
4848+4949+ val config : t -> Tls.Config.client
5050+ (** Get the TLS client configuration *)
5151+5252+ val servername : t -> string option
5353+ (** Get the SNI server name override *)
5454+5555+ val pp : Format.formatter -> t -> unit
5656+ (** Pretty-print TLS configuration *)
5757+end
5858+5959+6060+(** Pool configuration *)
6161+module Config : sig
6262+ type t
6363+ (** Pool configuration *)
6464+6565+ val make :
6666+ ?max_connections_per_endpoint:int ->
6767+ ?max_idle_time:float ->
6868+ ?max_connection_lifetime:float ->
6969+ ?max_connection_uses:int ->
7070+ ?health_check:([ `Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t -> bool) ->
7171+ ?connect_timeout:float ->
7272+ ?connect_retry_count:int ->
7373+ ?connect_retry_delay:float ->
7474+ ?on_connection_created:(Endpoint.t -> unit) ->
7575+ ?on_connection_closed:(Endpoint.t -> unit) ->
7676+ ?on_connection_reused:(Endpoint.t -> unit) ->
7777+ unit -> t
7878+ (** Create pool configuration with optional parameters.
7979+ See field descriptions for defaults. *)
8080+8181+ val default : t
8282+ (** Sensible defaults for most use cases:
8383+ - max_connections_per_endpoint: 10
8484+ - max_idle_time: 60.0s
8585+ - max_connection_lifetime: 300.0s
8686+ - max_connection_uses: None (unlimited)
8787+ - health_check: None
8888+ - connect_timeout: 10.0s
8989+ - connect_retry_count: 3
9090+ - connect_retry_delay: 0.1s
9191+ - hooks: None *)
9292+9393+ val max_connections_per_endpoint : t -> int
9494+ val max_idle_time : t -> float
9595+ val max_connection_lifetime : t -> float
9696+ val max_connection_uses : t -> int option
9797+ val health_check : t -> ([ `Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t -> bool) option
9898+ val connect_timeout : t -> float option
9999+ val connect_retry_count : t -> int
100100+ val connect_retry_delay : t -> float
101101+102102+ val pp : Format.formatter -> t -> unit
103103+ (** Pretty-print configuration *)
104104+end
105105+106106+(** Statistics for an endpoint *)
107107+module Stats : sig
108108+ type t
109109+ (** Statistics for a specific endpoint *)
110110+111111+ val active : t -> int
112112+ (** Connections currently in use *)
113113+114114+ val idle : t -> int
115115+ (** Connections in pool waiting to be reused *)
116116+117117+ val total_created : t -> int
118118+ (** Total connections created (lifetime) *)
119119+120120+ val total_reused : t -> int
121121+ (** Total times connections were reused *)
122122+123123+ val total_closed : t -> int
124124+ (** Total connections closed *)
125125+126126+ val errors : t -> int
127127+ (** Total connection errors *)
128128+129129+ val pp : Format.formatter -> t -> unit
130130+ (** Pretty-print endpoint statistics *)
131131+end
132132+133133+(** {1 Connection Pool} *)
134134+135135+type ('clock, 'net) t
136136+(** Connection pool managing multiple endpoints, parameterized by clock and network types *)
137137+138138+val create :
139139+ sw:Eio.Switch.t ->
140140+ net:'net Eio.Net.t ->
141141+ clock:'clock Eio.Time.clock ->
142142+ ?tls:Tls_config.t ->
143143+ ?config:Config.t ->
144144+ unit -> ('clock Eio.Time.clock, 'net Eio.Net.t) t
145145+(** Create connection pool bound to switch.
146146+ All connections will be closed when switch is released.
147147+148148+ @param sw Switch for resource management
149149+ @param net Network interface for creating connections
150150+ @param clock Clock for timeouts and time-based validation
151151+ @param tls Optional TLS configuration applied to all connections
152152+ @param config Optional pool configuration (uses Config.default if not provided) *)
153153+154154+(** {1 Connection Usage} *)
155155+156156+val with_connection :
157157+ ('clock Eio.Time.clock, 'net Eio.Net.t) t ->
158158+ Endpoint.t ->
159159+ ([ `Close | `Flow | `R | `Shutdown | `W ] Eio.Resource.t -> 'a) ->
160160+ 'a
161161+(** Acquire connection, use it, automatically release back to pool.
162162+163163+ This is the only way to use connections from the pool. All resource management
164164+ is handled automatically through Eio's switch mechanism.
165165+166166+ If idle connection available and healthy:
167167+ - Reuse from pool (validates health first)
168168+ Else:
169169+ - Create new connection (may block if endpoint at limit)
170170+171171+ On success: connection returned to pool for reuse
172172+ On error: connection closed, not returned to pool
173173+174174+ Example:
175175+ {[
176176+ let endpoint = Conpool.Endpoint.make ~host:"example.com" ~port:443 in
177177+ Conpool.with_connection pool endpoint (fun conn ->
178178+ (* Use conn for HTTP request, Redis command, etc. *)
179179+ Eio.Flow.copy_string "GET / HTTP/1.1\r\n\r\n" conn;
180180+ let buf = Eio.Buf_read.of_flow conn ~max_size:4096 in
181181+ Eio.Buf_read.take_all buf
182182+ )
183183+ ]}
184184+*)
185185+186186+(** {1 Statistics & Monitoring} *)
187187+188188+val stats :
189189+ ('clock Eio.Time.clock, 'net Eio.Net.t) t ->
190190+ Endpoint.t ->
191191+ Stats.t
192192+(** Get statistics for specific endpoint *)
193193+194194+val all_stats :
195195+ ('clock Eio.Time.clock, 'net Eio.Net.t) t ->
196196+ (Endpoint.t * Stats.t) list
197197+(** Get statistics for all endpoints in pool *)
198198+199199+(** {1 Pool Management} *)
200200+201201+val clear_endpoint :
202202+ ('clock Eio.Time.clock, 'net Eio.Net.t) t ->
203203+ Endpoint.t ->
204204+ unit
205205+(** Clear all cached connections for a specific endpoint.
206206+207207+ This removes the endpoint from the pool, discarding all idle connections.
208208+ Active connections will continue to work but won't be returned to the pool.
209209+210210+ Use this when you know an endpoint's connections are no longer valid
211211+ (e.g., server restarted, network reconfigured, credentials changed).
212212+213213+ The pool will be automatically cleaned up when its switch is released. *)