···11+# v1.0.0 (dev)
22+33+- Initial release of Conpool
+15
LICENSE.md
···11+ISC License
22+33+Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
44+55+Permission to use, copy, modify, and distribute this software for any
66+purpose with or without fee is hereby granted, provided that the above
77+copyright notice and this permission notice appear in all copies.
88+99+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
1010+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1111+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1212+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1313+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1414+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1515+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+113
README.md
···11+# Conpool - Protocol-agnostic Connection Pooling for Eio
22+33+Conpool is a connection pooling library built on Eio that manages TCP connection lifecycles, validates connection health, and provides per-endpoint resource limiting for any TCP-based protocol.
44+55+## Key Features
66+77+- **Protocol-agnostic**: Works with HTTP, Redis, PostgreSQL, or any TCP-based protocol
88+- **Health validation**: Automatically validates connections before reuse
99+- **Per-endpoint limits**: Independent connection limits and pooling for each endpoint
1010+- **TLS support**: Optional TLS configuration for secure connections
1111+- **Statistics & monitoring**: Track connection usage, hits/misses, and health status
1212+- **Built on Eio**: Leverages Eio's structured concurrency and resource management
1313+1414+## Usage
1515+1616+Basic example establishing a connection pool:
1717+1818+```ocaml
1919+open Eio.Std
2020+2121+let run env =
2222+ Switch.run (fun sw ->
2323+ (* Create a connection pool *)
2424+ let pool = Conpool.create
2525+ ~sw
2626+ ~net:(Eio.Stdenv.net env)
2727+ ~clock:(Eio.Stdenv.clock env)
2828+ ()
2929+ in
3030+3131+ (* Define an endpoint *)
3232+ let endpoint = Conpool.Endpoint.make ~host:"example.com" ~port:80 in
3333+3434+ (* Use a connection from the pool *)
3535+ Conpool.with_connection pool endpoint (fun conn ->
3636+ Eio.Flow.copy_string "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" conn;
3737+ let buf = Eio.Buf_read.of_flow conn ~max_size:4096 in
3838+ Eio.Buf_read.take_all buf
3939+ )
4040+ )
4141+```
4242+4343+With TLS configuration:
4444+4545+```ocaml
4646+let run env =
4747+ Switch.run (fun sw ->
4848+ (* Create TLS configuration *)
4949+ let tls = Conpool.Tls_config.make
5050+ ~authenticator:(Ca_certs.authenticator ())
5151+ ()
5252+ in
5353+5454+ (* Create pool with TLS *)
5555+ let pool = Conpool.create
5656+ ~sw
5757+ ~net:(Eio.Stdenv.net env)
5858+ ~clock:(Eio.Stdenv.clock env)
5959+ ~tls
6060+ ()
6161+ in
6262+6363+ let endpoint = Conpool.Endpoint.make ~host:"example.com" ~port:443 in
6464+ Conpool.with_connection pool endpoint (fun conn ->
6565+ (* Use TLS-encrypted connection *)
6666+ ...
6767+ )
6868+ )
6969+```
7070+7171+Custom pool configuration:
7272+7373+```ocaml
7474+let config = Conpool.Config.make
7575+ ~max_connections_per_endpoint:20
7676+ ~max_idle_per_endpoint:5
7777+ ~connection_timeout:10.0
7878+ ~validation_interval:300.0
7979+ ()
8080+in
8181+8282+let pool = Conpool.create ~sw ~net ~clock ~config ()
8383+```
8484+8585+Monitor pool statistics:
8686+8787+```ocaml
8888+let stats = Conpool.stats pool endpoint in
8989+Printf.printf "Active: %d, Idle: %d, Hits: %d, Misses: %d\n"
9090+ (Conpool.Stats.active_connections stats)
9191+ (Conpool.Stats.idle_connections stats)
9292+ (Conpool.Stats.cache_hits stats)
9393+ (Conpool.Stats.cache_misses stats)
9494+```
9595+9696+## Installation
9797+9898+```
9999+opam install conpool
100100+```
101101+102102+## Documentation
103103+104104+API documentation is available at https://tangled.org/@anil.recoil.org/ocaml-conpool or via:
105105+106106+```
107107+opam install conpool
108108+odig doc conpool
109109+```
110110+111111+## License
112112+113113+ISC
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Cmdliner terms for connection pool configuration *)
2738open Cmdliner
49510let max_connections_per_endpoint =
611 let doc = "Maximum concurrent connections per endpoint." in
77- Arg.(value & opt int 10 & info ["max-connections-per-endpoint"] ~doc ~docv:"NUM")
1212+ Arg.(
1313+ value & opt int 10
1414+ & info [ "max-connections-per-endpoint" ] ~doc ~docv:"NUM")
815916let max_idle_time =
1017 let doc = "Maximum time a connection can sit idle in seconds." in
1111- Arg.(value & opt float 60.0 & info ["max-idle-time"] ~doc ~docv:"SECONDS")
1818+ Arg.(value & opt float 60.0 & info [ "max-idle-time" ] ~doc ~docv:"SECONDS")
12191320let max_connection_lifetime =
1421 let doc = "Maximum connection age in seconds." in
1515- Arg.(value & opt float 300.0 & info ["max-connection-lifetime"] ~doc ~docv:"SECONDS")
2222+ Arg.(
2323+ value & opt float 300.0
2424+ & info [ "max-connection-lifetime" ] ~doc ~docv:"SECONDS")
16251726let max_connection_uses =
1827 let doc = "Maximum times a connection can be reused (omit for unlimited)." in
1919- Arg.(value & opt (some int) None & info ["max-connection-uses"] ~doc ~docv:"NUM")
2828+ Arg.(
2929+ value
3030+ & opt (some int) None
3131+ & info [ "max-connection-uses" ] ~doc ~docv:"NUM")
20322133let connect_timeout =
2234 let doc = "Connection timeout in seconds." in
2323- Arg.(value & opt float 10.0 & info ["connect-timeout"] ~doc ~docv:"SECONDS")
3535+ Arg.(value & opt float 10.0 & info [ "connect-timeout" ] ~doc ~docv:"SECONDS")
24362537let connect_retry_count =
2638 let doc = "Number of connection retry attempts." in
2727- Arg.(value & opt int 3 & info ["connect-retry-count"] ~doc ~docv:"NUM")
3939+ Arg.(value & opt int 3 & info [ "connect-retry-count" ] ~doc ~docv:"NUM")
28402941let connect_retry_delay =
3042 let doc = "Initial retry delay in seconds (with exponential backoff)." in
3131- Arg.(value & opt float 0.1 & info ["connect-retry-delay"] ~doc ~docv:"SECONDS")
4343+ Arg.(
4444+ value & opt float 0.1 & info [ "connect-retry-delay" ] ~doc ~docv:"SECONDS")
32453346let config =
3434- let make max_conn max_idle max_lifetime max_uses timeout retry_count retry_delay =
3535- Config.make
3636- ~max_connections_per_endpoint:max_conn
3737- ~max_idle_time:max_idle
3838- ~max_connection_lifetime:max_lifetime
3939- ?max_connection_uses:max_uses
4040- ~connect_timeout:timeout
4141- ~connect_retry_count:retry_count
4242- ~connect_retry_delay:retry_delay
4343- ()
4747+ let make max_conn max_idle max_lifetime max_uses timeout retry_count
4848+ retry_delay =
4949+ Config.make ~max_connections_per_endpoint:max_conn ~max_idle_time:max_idle
5050+ ~max_connection_lifetime:max_lifetime ?max_connection_uses:max_uses
5151+ ~connect_timeout:timeout ~connect_retry_count:retry_count
5252+ ~connect_retry_delay:retry_delay ()
4453 in
4545- Term.(const make
4646- $ max_connections_per_endpoint
4747- $ max_idle_time
4848- $ max_connection_lifetime
4949- $ max_connection_uses
5050- $ connect_timeout
5151- $ connect_retry_count
5252- $ connect_retry_delay)
5454+ Term.(
5555+ const make $ max_connections_per_endpoint $ max_idle_time
5656+ $ max_connection_lifetime $ max_connection_uses $ connect_timeout
5757+ $ connect_retry_count $ connect_retry_delay)
+20-22
lib/cmd.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Cmdliner terms for connection pool configuration *)
2738(** {1 Configuration Terms} *)
49510val max_connections_per_endpoint : int Cmdliner.Term.t
66-(** Cmdliner term for maximum connections per endpoint.
77- Default: 10
88- Flag: [--max-connections-per-endpoint] *)
1111+(** Cmdliner term for maximum connections per endpoint. Default: 10 Flag:
1212+ [--max-connections-per-endpoint] *)
9131014val max_idle_time : float Cmdliner.Term.t
1111-(** Cmdliner term for maximum idle time in seconds.
1212- Default: 60.0
1313- Flag: [--max-idle-time] *)
1515+(** Cmdliner term for maximum idle time in seconds. Default: 60.0 Flag:
1616+ [--max-idle-time] *)
14171518val max_connection_lifetime : float Cmdliner.Term.t
1616-(** Cmdliner term for maximum connection lifetime in seconds.
1717- Default: 300.0
1919+(** Cmdliner term for maximum connection lifetime in seconds. Default: 300.0
1820 Flag: [--max-connection-lifetime] *)
19212022val max_connection_uses : int option Cmdliner.Term.t
2121-(** Cmdliner term for maximum connection uses.
2222- Default: None (unlimited)
2323- Flag: [--max-connection-uses] *)
2323+(** Cmdliner term for maximum connection uses. Default: None (unlimited) Flag:
2424+ [--max-connection-uses] *)
24252526val connect_timeout : float Cmdliner.Term.t
2626-(** Cmdliner term for connection timeout in seconds.
2727- Default: 10.0
2828- Flag: [--connect-timeout] *)
2727+(** Cmdliner term for connection timeout in seconds. Default: 10.0 Flag:
2828+ [--connect-timeout] *)
29293030val connect_retry_count : int Cmdliner.Term.t
3131-(** Cmdliner term for number of connection retry attempts.
3232- Default: 3
3333- Flag: [--connect-retry-count] *)
3131+(** Cmdliner term for number of connection retry attempts. Default: 3 Flag:
3232+ [--connect-retry-count] *)
34333534val connect_retry_delay : float Cmdliner.Term.t
3636-(** Cmdliner term for initial retry delay in seconds.
3737- Default: 0.1
3838- Flag: [--connect-retry-delay] *)
3535+(** Cmdliner term for initial retry delay in seconds. Default: 0.1 Flag:
3636+ [--connect-retry-delay] *)
39374038(** {1 Combined Terms} *)
41394240val config : Config.t Cmdliner.Term.t
4341(** Cmdliner term that combines all configuration options into a {!Config.t}.
4444- This term can be used in your application's main command to accept
4545- all connection pool configuration options from the command line. *)
4242+ This term can be used in your application's main command to accept all
4343+ connection pool configuration options from the command line. *)
+46-37
lib/config.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Configuration for connection pools *)
2738let src = Logs.Src.create "conpool.config" ~doc:"Connection pool configuration"
99+410module Log = (val Logs.src_log src : Logs.LOG)
511612type t = {
···814 max_idle_time : float;
915 max_connection_lifetime : float;
1016 max_connection_uses : int option;
1111- health_check : ([`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t -> bool) option;
1717+ health_check :
1818+ ([ `Close | `Flow | `R | `Shutdown | `W ] Eio.Resource.t -> bool) option;
1219 connect_timeout : float option;
1320 connect_retry_count : int;
1421 connect_retry_delay : float;
···1724 on_connection_reused : (Endpoint.t -> unit) option;
1825}
19262020-let make
2121- ?(max_connections_per_endpoint = 10)
2222- ?(max_idle_time = 60.0)
2323- ?(max_connection_lifetime = 300.0)
2424- ?max_connection_uses
2525- ?health_check
2626- ?(connect_timeout = 10.0)
2727- ?(connect_retry_count = 3)
2828- ?(connect_retry_delay = 0.1)
2929- ?on_connection_created
3030- ?on_connection_closed
3131- ?on_connection_reused
3232- () =
2727+let make ?(max_connections_per_endpoint = 10) ?(max_idle_time = 60.0)
2828+ ?(max_connection_lifetime = 300.0) ?max_connection_uses ?health_check
2929+ ?(connect_timeout = 10.0) ?(connect_retry_count = 3)
3030+ ?(connect_retry_delay = 0.1) ?on_connection_created ?on_connection_closed
3131+ ?on_connection_reused () =
3332 (* Validate parameters *)
3433 if max_connections_per_endpoint <= 0 then
3535- invalid_arg (Printf.sprintf "max_connections_per_endpoint must be positive, got %d"
3636- max_connections_per_endpoint);
3434+ invalid_arg
3535+ (Printf.sprintf "max_connections_per_endpoint must be positive, got %d"
3636+ max_connections_per_endpoint);
37373838 if max_idle_time <= 0.0 then
3939- invalid_arg (Printf.sprintf "max_idle_time must be positive, got %.2f" max_idle_time);
3939+ invalid_arg
4040+ (Printf.sprintf "max_idle_time must be positive, got %.2f" max_idle_time);
40414142 if max_connection_lifetime <= 0.0 then
4242- invalid_arg (Printf.sprintf "max_connection_lifetime must be positive, got %.2f"
4343- max_connection_lifetime);
4343+ invalid_arg
4444+ (Printf.sprintf "max_connection_lifetime must be positive, got %.2f"
4545+ max_connection_lifetime);
44464547 (match max_connection_uses with
4646- | Some n when n <= 0 ->
4747- invalid_arg (Printf.sprintf "max_connection_uses must be positive, got %d" n)
4848- | _ -> ());
4848+ | Some n when n <= 0 ->
4949+ invalid_arg
5050+ (Printf.sprintf "max_connection_uses must be positive, got %d" n)
5151+ | _ -> ());
49525053 if connect_timeout <= 0.0 then
5151- invalid_arg (Printf.sprintf "connect_timeout must be positive, got %.2f" connect_timeout);
5454+ invalid_arg
5555+ (Printf.sprintf "connect_timeout must be positive, got %.2f"
5656+ connect_timeout);
52575358 if connect_retry_count < 0 then
5454- invalid_arg (Printf.sprintf "connect_retry_count must be non-negative, got %d"
5555- connect_retry_count);
5959+ invalid_arg
6060+ (Printf.sprintf "connect_retry_count must be non-negative, got %d"
6161+ connect_retry_count);
56625763 if connect_retry_delay <= 0.0 then
5858- invalid_arg (Printf.sprintf "connect_retry_delay must be positive, got %.2f"
5959- connect_retry_delay);
6464+ invalid_arg
6565+ (Printf.sprintf "connect_retry_delay must be positive, got %.2f"
6666+ connect_retry_delay);
60676168 Log.debug (fun m ->
6262- m "Creating config: max_connections=%d, max_idle=%.1fs, max_lifetime=%.1fs"
6363- max_connections_per_endpoint max_idle_time max_connection_lifetime);
6969+ m
7070+ "Creating config: max_connections=%d, max_idle=%.1fs, \
7171+ max_lifetime=%.1fs"
7272+ max_connections_per_endpoint max_idle_time max_connection_lifetime);
6473 {
6574 max_connections_per_endpoint;
6675 max_idle_time;
···7685 }
77867887let default = make ()
7979-8088let max_connections_per_endpoint t = t.max_connections_per_endpoint
8189let max_idle_time t = t.max_idle_time
8290let max_connection_lifetime t = t.max_connection_lifetime
···99107 - connect_timeout: %s@,\
100108 - connect_retry_count: %d@,\
101109 - connect_retry_delay: %.2fs@]"
102102- t.max_connections_per_endpoint
103103- t.max_idle_time
104104- t.max_connection_lifetime
105105- (match t.max_connection_uses with Some n -> string_of_int n | None -> "unlimited")
106106- (match t.connect_timeout with Some f -> Fmt.str "%.1fs" f | None -> "none")
107107- t.connect_retry_count
108108- t.connect_retry_delay
110110+ t.max_connections_per_endpoint t.max_idle_time t.max_connection_lifetime
111111+ (match t.max_connection_uses with
112112+ | Some n -> string_of_int n
113113+ | None -> "unlimited")
114114+ (match t.connect_timeout with
115115+ | Some f -> Fmt.str "%.1fs" f
116116+ | None -> "none")
117117+ t.connect_retry_count t.connect_retry_delay
+25-15
lib/config.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Configuration for connection pools *)
2738(** {1 Logging} *)
···510val src : Logs.Src.t
611(** Logs source for configuration operations. Configure logging with:
712 {[
88- Logs.Src.set_level Conpool.Config.src (Some Logs.Debug);
99- ]}
1010-*)
1313+ Logs.Src.set_level Conpool.Config.src (Some Logs.Debug)
1414+ ]} *)
11151216(** {1 Type} *)
1317···2125 ?max_idle_time:float ->
2226 ?max_connection_lifetime:float ->
2327 ?max_connection_uses:int ->
2424- ?health_check:([ `Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t -> bool) ->
2828+ ?health_check:
2929+ ([ `Close | `Flow | `R | `Shutdown | `W ] Eio.Resource.t -> bool) ->
2530 ?connect_timeout:float ->
2631 ?connect_retry_count:int ->
2732 ?connect_retry_delay:float ->
2833 ?on_connection_created:(Endpoint.t -> unit) ->
2934 ?on_connection_closed:(Endpoint.t -> unit) ->
3035 ?on_connection_reused:(Endpoint.t -> unit) ->
3131- unit -> t
3636+ unit ->
3737+ t
3238(** Create pool configuration with optional parameters.
33393434- @param max_connections_per_endpoint Maximum concurrent connections per endpoint (default: 10)
3535- @param max_idle_time Maximum time a connection can sit idle in seconds (default: 60.0)
3636- @param max_connection_lifetime Maximum connection age in seconds (default: 300.0)
3737- @param max_connection_uses Maximum times a connection can be reused (default: unlimited)
4040+ @param max_connections_per_endpoint
4141+ Maximum concurrent connections per endpoint (default: 10)
4242+ @param max_idle_time
4343+ Maximum time a connection can sit idle in seconds (default: 60.0)
4444+ @param max_connection_lifetime
4545+ Maximum connection age in seconds (default: 300.0)
4646+ @param max_connection_uses
4747+ Maximum times a connection can be reused (default: unlimited)
3848 @param health_check Custom health check function (default: none)
3949 @param connect_timeout Connection timeout in seconds (default: 10.0)
4050 @param connect_retry_count Number of connection retry attempts (default: 3)
4141- @param connect_retry_delay Initial retry delay in seconds, with exponential backoff (default: 0.1)
5151+ @param connect_retry_delay
5252+ Initial retry delay in seconds, with exponential backoff (default: 0.1)
4253 @param on_connection_created Hook called when a connection is created
4354 @param on_connection_closed Hook called when a connection is closed
4444- @param on_connection_reused Hook called when a connection is reused
4545-*)
5555+ @param on_connection_reused Hook called when a connection is reused *)
46564757val default : t
4858(** Sensible defaults for most use cases:
···5464 - connect_timeout: 10.0s
5565 - connect_retry_count: 3
5666 - connect_retry_delay: 0.1s
5757- - hooks: none
5858-*)
6767+ - hooks: none *)
59686069(** {1 Accessors} *)
6170···7180val max_connection_uses : t -> int option
7281(** Get maximum connection uses, if any. *)
73827474-val health_check : t -> ([ `Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t -> bool) option
8383+val health_check :
8484+ t -> ([ `Close | `Flow | `R | `Shutdown | `W ] Eio.Resource.t -> bool) option
7585(** Get custom health check function, if any. *)
76867787val connect_timeout : t -> float option
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Conpool - Protocol-agnostic TCP/IP connection pooling library for Eio *)
2738let src = Logs.Src.create "conpool" ~doc:"Connection pooling library"
99+410module Log = (val Logs.src_log src : Logs.LOG)
511612(* Re-export submodules *)
···14201521type error =
1622 | Dns_resolution_failed of { hostname : string }
1717- | Connection_failed of { endpoint : Endpoint.t; attempts : int; last_error : string }
2323+ | Connection_failed of {
2424+ endpoint : Endpoint.t;
2525+ attempts : int;
2626+ last_error : string;
2727+ }
1828 | Connection_timeout of { endpoint : Endpoint.t; timeout : float }
1929 | Invalid_config of string
2030 | Invalid_endpoint of string
···2535 | Dns_resolution_failed { hostname } ->
2636 Fmt.pf ppf "DNS resolution failed for hostname: %s" hostname
2737 | Connection_failed { endpoint; attempts; last_error } ->
2828- Fmt.pf ppf "Failed to connect to %a after %d attempts: %s"
2929- Endpoint.pp endpoint attempts last_error
3838+ Fmt.pf ppf "Failed to connect to %a after %d attempts: %s" Endpoint.pp
3939+ endpoint attempts last_error
3040 | Connection_timeout { endpoint; timeout } ->
3131- Fmt.pf ppf "Connection timeout to %a after %.2fs"
3232- Endpoint.pp endpoint timeout
3333- | Invalid_config msg ->
3434- Fmt.pf ppf "Invalid configuration: %s" msg
3535- | Invalid_endpoint msg ->
3636- Fmt.pf ppf "Invalid endpoint: %s" msg
4141+ Fmt.pf ppf "Connection timeout to %a after %.2fs" Endpoint.pp endpoint
4242+ timeout
4343+ | Invalid_config msg -> Fmt.pf ppf "Invalid configuration: %s" msg
4444+ | Invalid_endpoint msg -> Fmt.pf ppf "Invalid endpoint: %s" msg
37453846type endp_stats = {
3947 mutable active : int;
···62706371type t = T : ('clock Eio.Time.clock, 'net Eio.Net.t) internal -> t
64726565-module EndpointTbl = Hashtbl.Make(struct
7373+module EndpointTbl = Hashtbl.Make (struct
6674 type t = Endpoint.t
7575+6776 let equal = Endpoint.equal
6877 let hash = Endpoint.hash
6978end)
70797171-let get_time (pool : ('clock, 'net) internal) =
7272- Eio.Time.now pool.clock
8080+let get_time (pool : ('clock, 'net) internal) = Eio.Time.now pool.clock
73817474-let create_endp_stats () = {
7575- active = 0;
7676- idle = 0;
7777- total_created = 0;
7878- total_reused = 0;
7979- total_closed = 0;
8080- errors = 0;
8181-}
8282+let create_endp_stats () =
8383+ {
8484+ active = 0;
8585+ idle = 0;
8686+ total_created = 0;
8787+ total_reused = 0;
8888+ total_closed = 0;
8989+ errors = 0;
9090+ }
82918392let snapshot_stats (stats : endp_stats) : Stats.t =
8484- Stats.make
8585- ~active:stats.active
8686- ~idle:stats.idle
8787- ~total_created:stats.total_created
8888- ~total_reused:stats.total_reused
8989- ~total_closed:stats.total_closed
9090- ~errors:stats.errors
9393+ Stats.make ~active:stats.active ~idle:stats.idle
9494+ ~total_created:stats.total_created ~total_reused:stats.total_reused
9595+ ~total_closed:stats.total_closed ~errors:stats.errors
91969297(** {1 DNS Resolution} *)
93989499let resolve_endpoint (pool : ('clock, 'net) internal) endpoint =
95100 Log.debug (fun m -> m "Resolving %a..." Endpoint.pp endpoint);
9696- let addrs = Eio.Net.getaddrinfo_stream pool.net (Endpoint.host endpoint) ~service:(string_of_int (Endpoint.port endpoint)) in
101101+ let addrs =
102102+ Eio.Net.getaddrinfo_stream pool.net (Endpoint.host endpoint)
103103+ ~service:(string_of_int (Endpoint.port endpoint))
104104+ in
97105 Log.debug (fun m -> m "Got address list for %a" Endpoint.pp endpoint);
98106 match addrs with
99107 | addr :: _ ->
100100- Log.debug (fun m -> m "Resolved %a to %a"
101101- Endpoint.pp endpoint Eio.Net.Sockaddr.pp addr);
108108+ Log.debug (fun m ->
109109+ m "Resolved %a to %a" Endpoint.pp endpoint Eio.Net.Sockaddr.pp addr);
102110 addr
103111 | [] ->
104104- Log.err (fun m -> m "Failed to resolve hostname: %s" (Endpoint.host endpoint));
105105- raise (Pool_error (Dns_resolution_failed { hostname = Endpoint.host endpoint }))
112112+ Log.err (fun m ->
113113+ m "Failed to resolve hostname: %s" (Endpoint.host endpoint));
114114+ raise
115115+ (Pool_error
116116+ (Dns_resolution_failed { hostname = Endpoint.host endpoint }))
106117107118(** {1 Connection Creation with Retry} *)
108119109109-let rec create_connection_with_retry (pool : ('clock, 'net) internal) endpoint attempt last_error =
120120+let rec create_connection_with_retry (pool : ('clock, 'net) internal) endpoint
121121+ attempt last_error =
110122 let retry_count = Config.connect_retry_count pool.config in
111123 if attempt > retry_count then begin
112112- Log.err (fun m -> m "Failed to connect to %a after %d attempts"
113113- Endpoint.pp endpoint retry_count);
114114- raise (Pool_error (Connection_failed { endpoint; attempts = retry_count; last_error }))
124124+ Log.err (fun m ->
125125+ m "Failed to connect to %a after %d attempts" Endpoint.pp endpoint
126126+ retry_count);
127127+ raise
128128+ (Pool_error
129129+ (Connection_failed { endpoint; attempts = retry_count; last_error }))
115130 end;
116131117117- Log.debug (fun m -> m "Connecting to %a (attempt %d/%d)"
118118- Endpoint.pp endpoint attempt retry_count);
132132+ Log.debug (fun m ->
133133+ m "Connecting to %a (attempt %d/%d)" Endpoint.pp endpoint attempt
134134+ retry_count);
119135120136 try
121137 let addr = resolve_endpoint pool endpoint in
···125141 let socket =
126142 match Config.connect_timeout pool.config with
127143 | Some timeout ->
128128- Eio.Time.with_timeout_exn pool.clock timeout
129129- (fun () -> Eio.Net.connect ~sw:pool.sw pool.net addr)
130130- | None ->
131131- Eio.Net.connect ~sw:pool.sw pool.net addr
144144+ Eio.Time.with_timeout_exn pool.clock timeout (fun () ->
145145+ Eio.Net.connect ~sw:pool.sw pool.net addr)
146146+ | None -> Eio.Net.connect ~sw:pool.sw pool.net addr
132147 in
133148134134- Log.debug (fun m -> m "TCP connection established to %a" Endpoint.pp endpoint);
149149+ Log.debug (fun m ->
150150+ m "TCP connection established to %a" Endpoint.pp endpoint);
135151136136- let flow = match pool.tls with
137137- | None -> (socket :> [`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t)
152152+ let flow =
153153+ match pool.tls with
154154+ | None ->
155155+ (socket :> [ `Close | `Flow | `R | `Shutdown | `W ] Eio.Resource.t)
138156 | Some tls_cfg ->
139139- Log.debug (fun m -> m "Initiating TLS handshake with %a" Endpoint.pp endpoint);
140140- let host = match Tls_config.servername tls_cfg with
157157+ Log.debug (fun m ->
158158+ m "Initiating TLS handshake with %a" Endpoint.pp endpoint);
159159+ let host =
160160+ match Tls_config.servername tls_cfg with
141161 | Some name -> Domain_name.(host_exn (of_string_exn name))
142142- | None -> Domain_name.(host_exn (of_string_exn (Endpoint.host endpoint)))
162162+ | None ->
163163+ Domain_name.(host_exn (of_string_exn (Endpoint.host endpoint)))
143164 in
144144- let tls_flow = Tls_eio.client_of_flow ~host (Tls_config.config tls_cfg) socket in
145145- Log.info (fun m -> m "TLS connection established to %a" Endpoint.pp endpoint);
146146- (tls_flow :> [`Close | `Flow | `R | `Shutdown | `W] Eio.Resource.t)
165165+ let tls_flow =
166166+ Tls_eio.client_of_flow ~host (Tls_config.config tls_cfg) socket
167167+ in
168168+ Log.info (fun m ->
169169+ m "TLS connection established to %a" Endpoint.pp endpoint);
170170+ (tls_flow :> [ `Close | `Flow | `R | `Shutdown | `W ] Eio.Resource.t)
147171 in
148172149173 let now = get_time pool in
···156180 endpoint;
157181 mutex = Eio.Mutex.create ();
158182 }
159159-160183 with
161184 | Eio.Time.Timeout as e ->
162162- Log.warn (fun m -> m "Connection timeout to %a (attempt %d)" Endpoint.pp endpoint attempt);
185185+ Log.warn (fun m ->
186186+ m "Connection timeout to %a (attempt %d)" Endpoint.pp endpoint attempt);
163187 let error_msg = Printexc.to_string e in
164188 if attempt >= Config.connect_retry_count pool.config then
165189 (* Last attempt - convert to our error type *)
···167191 | Some timeout ->
168192 raise (Pool_error (Connection_timeout { endpoint; timeout }))
169193 | None ->
170170- raise (Pool_error (Connection_failed { endpoint; attempts = attempt; last_error = error_msg }))
194194+ raise
195195+ (Pool_error
196196+ (Connection_failed
197197+ { endpoint; attempts = attempt; last_error = error_msg }))
171198 else begin
172199 (* Retry with exponential backoff *)
173173- let delay = Config.connect_retry_delay pool.config *. (2.0 ** float_of_int (attempt - 1)) in
200200+ let delay =
201201+ Config.connect_retry_delay pool.config
202202+ *. (2.0 ** float_of_int (attempt - 1))
203203+ in
174204 Eio.Time.sleep pool.clock delay;
175205 create_connection_with_retry pool endpoint (attempt + 1) error_msg
176206 end
177207 | e ->
178208 (* Other errors - retry with backoff *)
179209 let error_msg = Printexc.to_string e in
180180- Log.warn (fun m -> m "Connection attempt %d to %a failed: %s"
181181- attempt Endpoint.pp endpoint error_msg);
210210+ Log.warn (fun m ->
211211+ m "Connection attempt %d to %a failed: %s" attempt Endpoint.pp
212212+ endpoint error_msg);
182213 if attempt < Config.connect_retry_count pool.config then (
183183- let delay = Config.connect_retry_delay pool.config *. (2.0 ** float_of_int (attempt - 1)) in
214214+ let delay =
215215+ Config.connect_retry_delay pool.config
216216+ *. (2.0 ** float_of_int (attempt - 1))
217217+ in
184218 Eio.Time.sleep pool.clock delay;
185185- create_connection_with_retry pool endpoint (attempt + 1) error_msg
186186- ) else
187187- raise (Pool_error (Connection_failed { endpoint; attempts = attempt; last_error = error_msg }))
219219+ create_connection_with_retry pool endpoint (attempt + 1) error_msg)
220220+ else
221221+ raise
222222+ (Pool_error
223223+ (Connection_failed
224224+ { endpoint; attempts = attempt; last_error = error_msg }))
188225189226let create_connection (pool : ('clock, 'net) internal) endpoint =
190227 create_connection_with_retry pool endpoint 1 "No attempts made"
···198235 let age = now -. Connection.created_at conn in
199236 let max_lifetime = Config.max_connection_lifetime pool.config in
200237 if age > max_lifetime then begin
201201- Log.debug (fun m -> m "Connection to %a unhealthy: exceeded max lifetime (%.2fs > %.2fs)"
202202- Endpoint.pp (Connection.endpoint conn) age max_lifetime);
238238+ Log.debug (fun m ->
239239+ m "Connection to %a unhealthy: exceeded max lifetime (%.2fs > %.2fs)"
240240+ Endpoint.pp (Connection.endpoint conn) age max_lifetime);
203241 false
204242 end
205205-206243 (* Check idle time *)
207207- else begin
244244+ else begin
208245 let max_idle = Config.max_idle_time pool.config in
209209- if (now -. Connection.last_used conn) > max_idle then begin
246246+ if now -. Connection.last_used conn > max_idle then begin
210247 let idle_time = now -. Connection.last_used conn in
211211- Log.debug (fun m -> m "Connection to %a unhealthy: exceeded max idle time (%.2fs > %.2fs)"
212212- Endpoint.pp (Connection.endpoint conn) idle_time max_idle);
213213- false
214214- end
215215-216216- (* Check use count *)
217217- else if (match Config.max_connection_uses pool.config with
218218- | Some max -> Connection.use_count conn >= max
219219- | None -> false) then begin
220220- Log.debug (fun m -> m "Connection to %a unhealthy: exceeded max use count (%d)"
221221- Endpoint.pp (Connection.endpoint conn) (Connection.use_count conn));
248248+ Log.debug (fun m ->
249249+ m "Connection to %a unhealthy: exceeded max idle time (%.2fs > %.2fs)"
250250+ Endpoint.pp (Connection.endpoint conn) idle_time max_idle);
222251 false
223223- end
224224-225225- (* Optional: custom health check *)
226226- else if (match Config.health_check pool.config with
227227- | Some check ->
228228- (try
229229- let healthy = check (Connection.flow conn) in
230230- if not healthy then
231231- Log.debug (fun m -> m "Connection to %a failed custom health check"
232232- Endpoint.pp (Connection.endpoint conn));
233233- not healthy
234234- with e ->
235235- Log.debug (fun m -> m "Connection to %a health check raised exception: %s"
236236- Endpoint.pp (Connection.endpoint conn) (Printexc.to_string e));
237237- true) (* Exception in health check = unhealthy *)
238238- | None -> false) then
252252+ end (* Check use count *)
253253+ else if
254254+ match Config.max_connection_uses pool.config with
255255+ | Some max -> Connection.use_count conn >= max
256256+ | None -> false
257257+ then begin
258258+ Log.debug (fun m ->
259259+ m "Connection to %a unhealthy: exceeded max use count (%d)"
260260+ Endpoint.pp (Connection.endpoint conn)
261261+ (Connection.use_count conn));
239262 false
240240-241241- (* Optional: check if socket still connected *)
263263+ end (* Optional: custom health check *)
264264+ else if
265265+ match Config.health_check pool.config with
266266+ | Some check -> (
267267+ try
268268+ let healthy = check (Connection.flow conn) in
269269+ if not healthy then
270270+ Log.debug (fun m ->
271271+ m "Connection to %a failed custom health check" Endpoint.pp
272272+ (Connection.endpoint conn));
273273+ not healthy
274274+ with e ->
275275+ Log.debug (fun m ->
276276+ m "Connection to %a health check raised exception: %s"
277277+ Endpoint.pp (Connection.endpoint conn) (Printexc.to_string e));
278278+ true (* Exception in health check = unhealthy *))
279279+ | None -> false
280280+ then false (* Optional: check if socket still connected *)
242281 else if check_readable then
243282 try
244283 (* TODO avsm: a sockopt for this? *)
245284 true
246246- with
247247- | _ -> false
248248-285285+ with _ -> false
249286 else begin
250250- Log.debug (fun m -> m "Connection to %a is healthy (age=%.2fs, idle=%.2fs, uses=%d)"
251251- Endpoint.pp (Connection.endpoint conn)
252252- age
253253- (now -. Connection.last_used conn)
254254- (Connection.use_count conn));
287287+ Log.debug (fun m ->
288288+ m "Connection to %a is healthy (age=%.2fs, idle=%.2fs, uses=%d)"
289289+ Endpoint.pp (Connection.endpoint conn) age
290290+ (now -. Connection.last_used conn)
291291+ (Connection.use_count conn));
255292 true
256293 end
257294 end
···259296(** {1 Internal Pool Operations} *)
260297261298let close_internal (pool : ('clock, 'net) internal) conn =
262262- Log.debug (fun m -> m "Closing connection to %a (age=%.2fs, uses=%d)"
263263- Endpoint.pp (Connection.endpoint conn)
264264- (get_time pool -. Connection.created_at conn)
265265- (Connection.use_count conn));
299299+ Log.debug (fun m ->
300300+ m "Closing connection to %a (age=%.2fs, uses=%d)" Endpoint.pp
301301+ (Connection.endpoint conn)
302302+ (get_time pool -. Connection.created_at conn)
303303+ (Connection.use_count conn));
266304267305 Eio.Cancel.protect (fun () ->
268268- try
269269- Eio.Flow.close (Connection.flow conn)
270270- with _ -> ()
271271- );
306306+ try Eio.Flow.close (Connection.flow conn) with _ -> ());
272307273308 (* Call hook if configured *)
274274- Option.iter (fun f -> f (Connection.endpoint conn)) (Config.on_connection_closed pool.config)
309309+ Option.iter
310310+ (fun f -> f (Connection.endpoint conn))
311311+ (Config.on_connection_closed pool.config)
275312276313let get_or_create_endpoint_pool (pool : ('clock, 'net) internal) endpoint =
277277- Log.debug (fun m -> m "Getting or creating endpoint pool for %a" Endpoint.pp endpoint);
314314+ Log.debug (fun m ->
315315+ m "Getting or creating endpoint pool for %a" Endpoint.pp endpoint);
278316279317 (* First try with read lock *)
280280- match Eio.Mutex.use_ro pool.endpoints_mutex (fun () ->
281281- Hashtbl.find_opt pool.endpoints endpoint
282282- ) with
318318+ match
319319+ Eio.Mutex.use_ro pool.endpoints_mutex (fun () ->
320320+ Hashtbl.find_opt pool.endpoints endpoint)
321321+ with
283322 | Some ep_pool ->
284284- Log.debug (fun m -> m "Found existing endpoint pool for %a" Endpoint.pp endpoint);
323323+ Log.debug (fun m ->
324324+ m "Found existing endpoint pool for %a" Endpoint.pp endpoint);
285325 ep_pool
286326 | None ->
287287- Log.debug (fun m -> m "No existing pool, need to create for %a" Endpoint.pp endpoint);
327327+ Log.debug (fun m ->
328328+ m "No existing pool, need to create for %a" Endpoint.pp endpoint);
288329 (* Need to create - use write lock *)
289330 Eio.Mutex.use_rw ~protect:true pool.endpoints_mutex (fun () ->
290290- (* Check again in case another fiber created it *)
291291- match Hashtbl.find_opt pool.endpoints endpoint with
292292- | Some ep_pool ->
293293- Log.debug (fun m -> m "Another fiber created pool for %a" Endpoint.pp endpoint);
294294- ep_pool
295295- | None ->
331331+ (* Check again in case another fiber created it *)
332332+ match Hashtbl.find_opt pool.endpoints endpoint with
333333+ | Some ep_pool ->
334334+ Log.debug (fun m ->
335335+ m "Another fiber created pool for %a" Endpoint.pp endpoint);
336336+ ep_pool
337337+ | None ->
296338 (* Create new endpoint pool *)
297339 let stats = create_endp_stats () in
298340 let mutex = Eio.Mutex.create () in
299341300300- Log.info (fun m -> m "Creating new endpoint pool for %a (max_connections=%d)"
301301- Endpoint.pp endpoint (Config.max_connections_per_endpoint pool.config));
342342+ Log.info (fun m ->
343343+ m "Creating new endpoint pool for %a (max_connections=%d)"
344344+ Endpoint.pp endpoint
345345+ (Config.max_connections_per_endpoint pool.config));
302346303303- Log.debug (fun m -> m "About to create Eio.Pool for %a" Endpoint.pp endpoint);
347347+ Log.debug (fun m ->
348348+ m "About to create Eio.Pool for %a" Endpoint.pp endpoint);
304349305305- let eio_pool = Eio.Pool.create
306306- (Config.max_connections_per_endpoint pool.config)
307307- ~validate:(fun conn ->
308308- Log.debug (fun m -> m "Validate called for connection to %a" Endpoint.pp endpoint);
309309- (* Called before reusing from pool *)
310310- let healthy = is_healthy pool ~check_readable:false conn in
350350+ let eio_pool =
351351+ Eio.Pool.create
352352+ (Config.max_connections_per_endpoint pool.config)
353353+ ~validate:(fun conn ->
354354+ Log.debug (fun m ->
355355+ m "Validate called for connection to %a" Endpoint.pp
356356+ endpoint);
357357+ (* Called before reusing from pool *)
358358+ let healthy = is_healthy pool ~check_readable:false conn in
311359312312- if healthy then (
313313- Log.debug (fun m -> m "Reusing connection to %a from pool" Endpoint.pp endpoint);
360360+ if healthy then (
361361+ Log.debug (fun m ->
362362+ m "Reusing connection to %a from pool" Endpoint.pp
363363+ endpoint);
314364315315- (* Update stats for reuse *)
316316- Eio.Mutex.use_rw ~protect:true mutex (fun () ->
317317- stats.total_reused <- stats.total_reused + 1
318318- );
365365+ (* Update stats for reuse *)
366366+ Eio.Mutex.use_rw ~protect:true mutex (fun () ->
367367+ stats.total_reused <- stats.total_reused + 1);
319368320320- (* Call hook if configured *)
321321- Option.iter (fun f -> f endpoint) (Config.on_connection_reused pool.config);
369369+ (* Call hook if configured *)
370370+ Option.iter
371371+ (fun f -> f endpoint)
372372+ (Config.on_connection_reused pool.config);
322373323323- (* Run health check if configured *)
324324- match Config.health_check pool.config with
325325- | Some check ->
326326- (try check (Connection.flow conn)
327327- with _ -> false)
328328- | None -> true
329329- ) else begin
330330- Log.debug (fun m -> m "Connection to %a failed validation, creating new one" Endpoint.pp endpoint);
331331- false
332332- end
333333- )
334334- ~dispose:(fun conn ->
335335- (* Called when removing from pool *)
336336- Eio.Cancel.protect (fun () ->
337337- close_internal pool conn;
374374+ (* Run health check if configured *)
375375+ match Config.health_check pool.config with
376376+ | Some check -> (
377377+ try check (Connection.flow conn) with _ -> false)
378378+ | None -> true)
379379+ else begin
380380+ Log.debug (fun m ->
381381+ m
382382+ "Connection to %a failed validation, creating new \
383383+ one"
384384+ Endpoint.pp endpoint);
385385+ false
386386+ end)
387387+ ~dispose:(fun conn ->
388388+ (* Called when removing from pool *)
389389+ Eio.Cancel.protect (fun () ->
390390+ close_internal pool conn;
338391339339- (* Update stats *)
340340- Eio.Mutex.use_rw ~protect:true mutex (fun () ->
341341- stats.total_closed <- stats.total_closed + 1
342342- )
343343- )
344344- )
345345- (fun () ->
346346- Log.debug (fun m -> m "Factory function called for %a" Endpoint.pp endpoint);
347347- try
348348- let conn = create_connection pool endpoint in
392392+ (* Update stats *)
393393+ Eio.Mutex.use_rw ~protect:true mutex (fun () ->
394394+ stats.total_closed <- stats.total_closed + 1)))
395395+ (fun () ->
396396+ Log.debug (fun m ->
397397+ m "Factory function called for %a" Endpoint.pp endpoint);
398398+ try
399399+ let conn = create_connection pool endpoint in
349400350350- Log.debug (fun m -> m "Connection created successfully for %a" Endpoint.pp endpoint);
401401+ Log.debug (fun m ->
402402+ m "Connection created successfully for %a" Endpoint.pp
403403+ endpoint);
351404352352- (* Update stats *)
353353- Eio.Mutex.use_rw ~protect:true mutex (fun () ->
354354- stats.total_created <- stats.total_created + 1
355355- );
405405+ (* Update stats *)
406406+ Eio.Mutex.use_rw ~protect:true mutex (fun () ->
407407+ stats.total_created <- stats.total_created + 1);
356408357357- (* Call hook if configured *)
358358- Option.iter (fun f -> f endpoint) (Config.on_connection_created pool.config);
409409+ (* Call hook if configured *)
410410+ Option.iter
411411+ (fun f -> f endpoint)
412412+ (Config.on_connection_created pool.config);
359413360360- conn
361361- with e ->
362362- Log.err (fun m -> m "Factory function failed for %a: %s"
363363- Endpoint.pp endpoint (Printexc.to_string e));
364364- (* Update error stats *)
365365- Eio.Mutex.use_rw ~protect:true mutex (fun () ->
366366- stats.errors <- stats.errors + 1
367367- );
368368- raise e
369369- )
414414+ conn
415415+ with e ->
416416+ Log.err (fun m ->
417417+ m "Factory function failed for %a: %s" Endpoint.pp
418418+ endpoint (Printexc.to_string e));
419419+ (* Update error stats *)
420420+ Eio.Mutex.use_rw ~protect:true mutex (fun () ->
421421+ stats.errors <- stats.errors + 1);
422422+ raise e)
370423 in
371424372372- Log.debug (fun m -> m "Eio.Pool created successfully for %a" Endpoint.pp endpoint);
425425+ Log.debug (fun m ->
426426+ m "Eio.Pool created successfully for %a" Endpoint.pp endpoint);
373427374374- let ep_pool = {
375375- pool = eio_pool;
376376- stats;
377377- mutex;
378378- } in
428428+ let ep_pool = { pool = eio_pool; stats; mutex } in
379429380430 Hashtbl.add pool.endpoints endpoint ep_pool;
381381- Log.debug (fun m -> m "Endpoint pool added to hashtable for %a" Endpoint.pp endpoint);
382382- ep_pool
383383- )
431431+ Log.debug (fun m ->
432432+ m "Endpoint pool added to hashtable for %a" Endpoint.pp
433433+ endpoint);
434434+ ep_pool)
384435385436(** {1 Public API - Pool Creation} *)
386437387387-let create ~sw ~(net : 'net Eio.Net.t) ~(clock : 'clock Eio.Time.clock) ?tls ?(config = Config.default) () : t =
388388- Log.info (fun m -> m "Creating new connection pool (max_per_endpoint=%d, max_idle=%.1fs, max_lifetime=%.1fs)"
389389- (Config.max_connections_per_endpoint config)
390390- (Config.max_idle_time config)
391391- (Config.max_connection_lifetime config));
438438+let create ~sw ~(net : 'net Eio.Net.t) ~(clock : 'clock Eio.Time.clock) ?tls
439439+ ?(config = Config.default) () : t =
440440+ Log.info (fun m ->
441441+ m
442442+ "Creating new connection pool (max_per_endpoint=%d, max_idle=%.1fs, \
443443+ max_lifetime=%.1fs)"
444444+ (Config.max_connections_per_endpoint config)
445445+ (Config.max_idle_time config)
446446+ (Config.max_connection_lifetime config));
392447393393- let pool = {
394394- sw;
395395- net;
396396- clock;
397397- config;
398398- tls;
399399- endpoints = Hashtbl.create 16;
400400- endpoints_mutex = Eio.Mutex.create ();
401401- } in
448448+ let pool =
449449+ {
450450+ sw;
451451+ net;
452452+ clock;
453453+ config;
454454+ tls;
455455+ endpoints = Hashtbl.create 16;
456456+ endpoints_mutex = Eio.Mutex.create ();
457457+ }
458458+ in
402459403460 (* Auto-cleanup on switch release *)
404461 Eio.Switch.on_release sw (fun () ->
405405- Eio.Cancel.protect (fun () ->
406406- Log.info (fun m -> m "Closing connection pool");
407407- (* Close all idle connections - active ones will be cleaned up by switch *)
408408- Hashtbl.iter (fun _endpoint _ep_pool ->
409409- (* Connections are bound to the switch and will be auto-closed *)
410410- ()
411411- ) pool.endpoints;
462462+ Eio.Cancel.protect (fun () ->
463463+ Log.info (fun m -> m "Closing connection pool");
464464+ (* Close all idle connections - active ones will be cleaned up by switch *)
465465+ Hashtbl.iter
466466+ (fun _endpoint _ep_pool ->
467467+ (* Connections are bound to the switch and will be auto-closed *)
468468+ ())
469469+ pool.endpoints;
412470413413- Hashtbl.clear pool.endpoints
414414- )
415415- );
471471+ Hashtbl.clear pool.endpoints));
416472417473 T pool
418474···424480425481 (* Increment active count *)
426482 Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
427427- ep_pool.stats.active <- ep_pool.stats.active + 1
428428- );
483483+ ep_pool.stats.active <- ep_pool.stats.active + 1);
429484430485 Fun.protect
431486 ~finally:(fun () ->
432487 (* Decrement active count *)
433488 Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
434434- ep_pool.stats.active <- ep_pool.stats.active - 1
435435- );
436436- Log.debug (fun m -> m "Released connection to %a" Endpoint.pp endpoint)
437437- )
489489+ ep_pool.stats.active <- ep_pool.stats.active - 1);
490490+ Log.debug (fun m -> m "Released connection to %a" Endpoint.pp endpoint))
438491 (fun () ->
439492 (* Use Eio.Pool for resource management *)
440493 Eio.Pool.use ep_pool.pool (fun conn ->
441441- Log.debug (fun m -> m "Using connection to %a (uses=%d)"
442442- Endpoint.pp endpoint (Connection.use_count conn));
494494+ Log.debug (fun m ->
495495+ m "Using connection to %a (uses=%d)" Endpoint.pp endpoint
496496+ (Connection.use_count conn));
443497444444- (* Update last used time and use count *)
445445- Connection.update_usage conn ~now:(get_time pool);
498498+ (* Update last used time and use count *)
499499+ Connection.update_usage conn ~now:(get_time pool);
446500447447- (* Update idle stats (connection taken from idle pool) *)
448448- Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
449449- ep_pool.stats.idle <- max 0 (ep_pool.stats.idle - 1)
450450- );
501501+ (* Update idle stats (connection taken from idle pool) *)
502502+ Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
503503+ ep_pool.stats.idle <- max 0 (ep_pool.stats.idle - 1));
451504452452- try
453453- let result = f conn.flow in
505505+ try
506506+ let result = f conn.flow in
454507455455- (* Success - connection will be returned to pool by Eio.Pool *)
456456- (* Update idle stats (connection returned to idle pool) *)
457457- Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
458458- ep_pool.stats.idle <- ep_pool.stats.idle + 1
459459- );
508508+ (* Success - connection will be returned to pool by Eio.Pool *)
509509+ (* Update idle stats (connection returned to idle pool) *)
510510+ Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
511511+ ep_pool.stats.idle <- ep_pool.stats.idle + 1);
460512461461- result
462462- with e ->
463463- (* Error - close connection so it won't be reused *)
464464- Log.warn (fun m -> m "Error using connection to %a: %s"
465465- Endpoint.pp endpoint (Printexc.to_string e));
466466- close_internal pool conn;
513513+ result
514514+ with e ->
515515+ (* Error - close connection so it won't be reused *)
516516+ Log.warn (fun m ->
517517+ m "Error using connection to %a: %s" Endpoint.pp endpoint
518518+ (Printexc.to_string e));
519519+ close_internal pool conn;
467520468468- (* Update error stats *)
469469- Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
470470- ep_pool.stats.errors <- ep_pool.stats.errors + 1
471471- );
521521+ (* Update error stats *)
522522+ Eio.Mutex.use_rw ~protect:true ep_pool.mutex (fun () ->
523523+ ep_pool.stats.errors <- ep_pool.stats.errors + 1);
472524473473- raise e
474474- )
475475- )
525525+ raise e))
476526477527(** {1 Public API - Statistics} *)
478528479529let stats (T pool) endpoint =
480530 match Hashtbl.find_opt pool.endpoints endpoint with
481531 | Some ep_pool ->
482482- Eio.Mutex.use_ro ep_pool.mutex (fun () ->
483483- snapshot_stats ep_pool.stats
484484- )
532532+ Eio.Mutex.use_ro ep_pool.mutex (fun () -> snapshot_stats ep_pool.stats)
485533 | None ->
486534 (* No pool for this endpoint yet *)
487487- Stats.make
488488- ~active:0
489489- ~idle:0
490490- ~total_created:0
491491- ~total_reused:0
492492- ~total_closed:0
493493- ~errors:0
535535+ Stats.make ~active:0 ~idle:0 ~total_created:0 ~total_reused:0
536536+ ~total_closed:0 ~errors:0
494537495538let all_stats (T pool) =
496539 Eio.Mutex.use_ro pool.endpoints_mutex (fun () ->
497497- Hashtbl.fold (fun endpoint ep_pool acc ->
498498- let stats = Eio.Mutex.use_ro ep_pool.mutex (fun () ->
499499- snapshot_stats ep_pool.stats
500500- ) in
501501- (endpoint, stats) :: acc
502502- ) pool.endpoints []
503503- )
540540+ Hashtbl.fold
541541+ (fun endpoint ep_pool acc ->
542542+ let stats =
543543+ Eio.Mutex.use_ro ep_pool.mutex (fun () ->
544544+ snapshot_stats ep_pool.stats)
545545+ in
546546+ (endpoint, stats) :: acc)
547547+ pool.endpoints [])
504548505549(** {1 Public API - Pool Management} *)
506550···509553 match Hashtbl.find_opt pool.endpoints endpoint with
510554 | Some _ep_pool ->
511555 Eio.Cancel.protect (fun () ->
512512- (* Remove endpoint pool from hashtable *)
513513- (* Idle connections will be discarded *)
514514- (* Active connections will be closed when returned *)
515515- Eio.Mutex.use_rw ~protect:true pool.endpoints_mutex (fun () ->
516516- Hashtbl.remove pool.endpoints endpoint
517517- )
518518- )
556556+ (* Remove endpoint pool from hashtable *)
557557+ (* Idle connections will be discarded *)
558558+ (* Active connections will be closed when returned *)
559559+ Eio.Mutex.use_rw ~protect:true pool.endpoints_mutex (fun () ->
560560+ Hashtbl.remove pool.endpoints endpoint))
519561 | None ->
520520- Log.debug (fun m -> m "No endpoint pool found for %a" Endpoint.pp endpoint)
562562+ Log.debug (fun m ->
563563+ m "No endpoint pool found for %a" Endpoint.pp endpoint)
+41-49
lib/conpool.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Conpool - Protocol-agnostic TCP/IP connection pooling library for Eio *)
2738(** {1 Logging} *)
···611(** Logs source for the main connection pool. Configure logging with:
712 {[
813 Logs.Src.set_level Conpool.src (Some Logs.Debug);
99- Logs.set_reporter (Logs_fmt.reporter ());
1414+ Logs.set_reporter (Logs_fmt.reporter ())
1015 ]}
11161217 Each submodule also exposes its own log source for fine-grained control:
1318 - {!Endpoint.src} - endpoint operations
1419 - {!Tls_config.src} - TLS configuration
1515- - {!Config.src} - pool configuration
1616-*)
2020+ - {!Config.src} - pool configuration *)
17211822(** {1 Core Types} *)
19232020-(** Network endpoint representation *)
2124module Endpoint : module type of Endpoint
2525+(** Network endpoint representation *)
22262323-(** TLS configuration for connection pools *)
2427module Tls_config : module type of Tls_config
2828+(** TLS configuration for connection pools *)
25292626-(** Configuration for connection pools *)
2730module Config : module type of Config
3131+(** Configuration for connection pools *)
28323333+module Stats : module type of Stats
2934(** Statistics for connection pool endpoints *)
3030-module Stats : module type of Stats
31353232-(** Cmdliner terms for connection pool configuration *)
3336module Cmd : module type of Cmd
3737+(** Cmdliner terms for connection pool configuration *)
34383539(** {1 Errors} *)
36403741type error =
3842 | Dns_resolution_failed of { hostname : string }
3939- (** DNS resolution failed for the given hostname *)
4040-4141- | Connection_failed of { endpoint : Endpoint.t; attempts : int; last_error : string }
4242- (** Failed to establish connection after all retry attempts *)
4343-4343+ (** DNS resolution failed for the given hostname *)
4444+ | Connection_failed of {
4545+ endpoint : Endpoint.t;
4646+ attempts : int;
4747+ last_error : string;
4848+ } (** Failed to establish connection after all retry attempts *)
4449 | Connection_timeout of { endpoint : Endpoint.t; timeout : float }
4545- (** Connection attempt timed out *)
4646-4747- | Invalid_config of string
4848- (** Invalid configuration parameter *)
4949-5050- | Invalid_endpoint of string
5151- (** Invalid endpoint specification *)
5050+ (** Connection attempt timed out *)
5151+ | Invalid_config of string (** Invalid configuration parameter *)
5252+ | Invalid_endpoint of string (** Invalid endpoint specification *)
52535354exception Pool_error of error
5455(** Exception raised by pool operations.
···7071 clock:'clock Eio.Time.clock ->
7172 ?tls:Tls_config.t ->
7273 ?config:Config.t ->
7373- unit -> t
7474-(** Create connection pool bound to switch.
7575- All connections will be closed when switch is released.
7474+ unit ->
7575+ t
7676+(** Create connection pool bound to switch. All connections will be closed when
7777+ switch is released.
76787779 @param sw Switch for resource management
7880 @param net Network interface for creating connections
7981 @param clock Clock for timeouts and time-based validation
8082 @param tls Optional TLS configuration applied to all connections
8181- @param config Optional pool configuration (uses Config.default if not provided) *)
8383+ @param config
8484+ Optional pool configuration (uses Config.default if not provided) *)
82858386(** {1 Connection Usage} *)
8487···9093(** Acquire connection, use it, automatically release back to pool.
91949295 If idle connection available and healthy:
9393- - Reuse from pool (validates health first)
9494- Else:
9595- - Create new connection (may block if endpoint at limit)
9696+ - Reuse from pool (validates health first) Else:
9797+ - Create new connection (may block if endpoint at limit)
96989797- On success: connection returned to pool for reuse
9898- On error: connection closed, not returned to pool
9999+ On success: connection returned to pool for reuse On error: connection
100100+ closed, not returned to pool
99101100102 Example:
101103 {[
102104 let endpoint = Conpool.Endpoint.make ~host:"example.com" ~port:443 in
103105 Conpool.with_connection pool endpoint (fun conn ->
104104- (* Use conn for HTTP request, Redis command, etc. *)
105105- Eio.Flow.copy_string "GET / HTTP/1.1\r\n\r\n" conn;
106106- let buf = Eio.Buf_read.of_flow conn ~max_size:4096 in
107107- Eio.Buf_read.take_all buf
108108- )
109109- ]}
110110-*)
106106+ (* Use conn for HTTP request, Redis command, etc. *)
107107+ Eio.Flow.copy_string "GET / HTTP/1.1\r\n\r\n" conn;
108108+ let buf = Eio.Buf_read.of_flow conn ~max_size:4096 in
109109+ Eio.Buf_read.take_all buf)
110110+ ]} *)
111111112112(** {1 Statistics & Monitoring} *)
113113114114-val stats :
115115- t ->
116116- Endpoint.t ->
117117- Stats.t
114114+val stats : t -> Endpoint.t -> Stats.t
118115(** Get statistics for specific endpoint *)
119116120120-val all_stats :
121121- t ->
122122- (Endpoint.t * Stats.t) list
117117+val all_stats : t -> (Endpoint.t * Stats.t) list
123118(** Get statistics for all endpoints in pool *)
124119125120(** {1 Pool Management} *)
126121127127-val clear_endpoint :
128128- t ->
129129- Endpoint.t ->
130130- unit
122122+val clear_endpoint : t -> Endpoint.t -> unit
131123(** Clear all cached connections for a specific endpoint.
132124133125 This removes the endpoint from the pool, discarding all idle connections.
134126 Active connections will continue to work but won't be returned to the pool.
135127136136- Use this when you know an endpoint's connections are no longer valid
137137- (e.g., server restarted, network reconfigured, credentials changed).
128128+ Use this when you know an endpoint's connections are no longer valid (e.g.,
129129+ server restarted, network reconfigured, credentials changed).
138130139131 The pool will be automatically cleaned up when its switch is released. *)
+14-15
lib/endpoint.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+16(** Network endpoint representation *)
2733-let src = Logs.Src.create "conpool.endpoint" ~doc:"Connection pool endpoint operations"
88+let src =
99+ Logs.Src.create "conpool.endpoint" ~doc:"Connection pool endpoint operations"
1010+411module Log = (val Logs.src_log src : Logs.LOG)
51266-type t = {
77- host : string;
88- port : int;
99-}
1313+type t = { host : string; port : int }
10141115let make ~host ~port =
1216 (* Validate port range *)
1317 if port < 1 || port > 65535 then
1414- invalid_arg (Printf.sprintf "Invalid port number: %d (must be 1-65535)" port);
1818+ invalid_arg
1919+ (Printf.sprintf "Invalid port number: %d (must be 1-65535)" port);
15201621 (* Validate hostname is not empty *)
1717- if String.trim host = "" then
1818- invalid_arg "Hostname cannot be empty";
2222+ if String.trim host = "" then invalid_arg "Hostname cannot be empty";
19232024 Log.debug (fun m -> m "Creating endpoint: %s:%d" host port);
2125 { host; port }
22262327let host t = t.host
2428let port t = t.port
2525-2626-let equal t1 t2 =
2727- String.equal t1.host t2.host && t1.port = t2.port
2828-2929-let hash t =
3030- Hashtbl.hash (t.host, t.port)
3131-2929+let equal t1 t2 = String.equal t1.host t2.host && t1.port = t2.port
3030+let hash t = Hashtbl.hash (t.host, t.port)
3231let pp = Fmt.of_to_string (fun t -> Printf.sprintf "%s:%d" t.host t.port)