···
1
+
# Conpool - TCP/IP Connection Pool Library for Eio
5
+
**Conpool** is a protocol-agnostic TCP/IP connection pooling library built on Eio.Pool. It manages connection lifecycles, validates connection health, and provides per-endpoint resource limiting for any TCP-based protocol (HTTP, Redis, PostgreSQL, etc.).
9
+
### Separation of Concerns
11
+
- **Conpool**: Manages TCP sockets and their lifecycle (connect, validate, close)
12
+
- **Protocol Libraries** (requests, redis-eio, etc.): Handle protocol-level logic (HTTP requests, Redis commands)
16
+
1. **Protocol Agnostic**: Works with any TCP-based protocol
17
+
2. **Eio.Pool Foundation**: Leverages Eio.Pool for resource management and limits
18
+
3. **Per-Endpoint Pooling**: Separate pool per (host, port, tls) tuple
19
+
4. **Connection Validation**: Health checks, age limits, idle timeouts
20
+
5. **Structured Concurrency**: All resources bound to switches
21
+
6. **Cancel-Safe**: Critical operations protected with `Cancel.protect`
22
+
7. **Observable**: Rich statistics and monitoring hooks
34
+
tls : tls_config option;
37
+
config : Tls.Config.client;
38
+
servername : string option;
42
+
(** Opaque connection handle with metadata *)
45
+
(** Connection pool managing multiple endpoints *)
48
+
max_connections_per_endpoint : int;
49
+
(** Maximum connections per (host, port, tls) endpoint. Default: 10 *)
51
+
max_idle_time : float;
52
+
(** Maximum time (seconds) a connection can be idle before closure. Default: 60.0 *)
54
+
max_connection_lifetime : float;
55
+
(** Maximum lifetime (seconds) of any connection. Default: 300.0 *)
57
+
max_connection_uses : int option;
58
+
(** Maximum number of times a connection can be reused. None = unlimited. Default: None *)
60
+
health_check : (Eio.Flow.two_way -> bool) option;
61
+
(** Optional health check function. Called before reusing idle connection. Default: None *)
63
+
connect_timeout : float option;
64
+
(** Timeout for establishing new connections. Default: Some 10.0 *)
66
+
on_connection_created : (endpoint -> unit) option;
67
+
(** Hook called when new connection created. Default: None *)
69
+
on_connection_closed : (endpoint -> unit) option;
70
+
(** Hook called when connection closed. Default: None *)
72
+
on_connection_reused : (endpoint -> unit) option;
73
+
(** Hook called when connection reused from pool. Default: None *)
76
+
val default_config : config
77
+
(** Sensible defaults for most use cases *)
88
+
(** Create connection pool bound to switch.
89
+
All connections will be closed when switch is released. *)
92
+
### Connection Acquisition & Release
95
+
val with_connection :
98
+
(Eio.Flow.two_way -> 'a) ->
100
+
(** Acquire connection, use it, automatically release.
102
+
If idle connection available and healthy:
105
+
- Create new connection (may block if endpoint at limit)
107
+
On success: connection returned to pool
108
+
On error: connection closed, not returned to pool
112
+
Conpool.with_connection pool endpoint (fun conn ->
113
+
(* Use conn for HTTP request, Redis command, etc. *)
114
+
Eio.Flow.write conn request_bytes;
115
+
Eio.Flow.read conn response_buf
124
+
(** Manually acquire connection. Must call [release] or [close] later.
125
+
Use [with_connection] instead unless you need explicit control. *)
131
+
(** Return connection to pool. Connection must be in clean state.
132
+
If connection is unhealthy, call [close] instead. *)
138
+
(** Close connection immediately, remove from pool. *)
143
+
(** Extract underlying Eio flow from connection. *)
146
+
### Connection Validation
150
+
?check_readable:bool ->
153
+
(** Check if connection is healthy.
156
+
- Not past max_connection_lifetime
157
+
- Not idle past max_idle_time
158
+
- Not exceeded max_connection_uses
159
+
- Optional: health_check function (from config)
160
+
- Optional: check_readable=true tests if socket still connected via 0-byte read
163
+
val validate_and_release :
167
+
(** Validate connection health, then release to pool if healthy or close if not.
168
+
Equivalent to: if is_healthy conn then release pool conn else close pool conn *)
171
+
### Statistics & Monitoring
174
+
type endpoint_stats = {
175
+
active : int; (** Connections currently in use *)
176
+
idle : int; (** Connections in pool waiting to be reused *)
177
+
total_created : int; (** Total connections created (lifetime) *)
178
+
total_reused : int; (** Total times connections were reused *)
179
+
total_closed : int; (** Total connections closed *)
180
+
errors : int; (** Total connection errors *)
187
+
(** Get statistics for specific endpoint *)
191
+
(endpoint * endpoint_stats) list
192
+
(** Get statistics for all endpoints in pool *)
195
+
Format.formatter ->
198
+
(** Pretty-print endpoint statistics *)
201
+
### Pool Management
204
+
val close_idle_connections :
208
+
(** Close all idle connections for endpoint (keeps active ones) *)
210
+
val close_all_connections :
214
+
(** Close all connections for endpoint (blocks until active ones released) *)
219
+
(** Close entire pool. Blocks until all active connections released.
220
+
Automatically called when switch releases. *)
223
+
## Implementation Details
225
+
### Per-Endpoint Pool Structure
227
+
Each endpoint gets its own `Eio.Pool.t` managing connections to that destination:
230
+
(* Internal implementation *)
232
+
type connection_metadata = {
233
+
flow : Eio.Flow.two_way;
234
+
created_at : float;
235
+
mutable last_used : float;
236
+
mutable use_count : int;
237
+
endpoint : endpoint;
240
+
type connection = connection_metadata
242
+
type endpoint_pool = {
243
+
pool : connection Eio.Pool.t;
244
+
stats : endpoint_stats_mutable;
245
+
mutex : Eio.Mutex.t;
252
+
endpoints : (endpoint, endpoint_pool) Hashtbl.t;
253
+
endpoints_mutex : Eio.Mutex.t;
257
+
### Connection Lifecycle with Eio.Pool
260
+
let with_connection pool endpoint f =
261
+
(* Get or create endpoint pool *)
262
+
let ep_pool = get_or_create_endpoint_pool pool endpoint in
264
+
(* Use Eio.Pool.use for resource management *)
265
+
Eio.Pool.use ep_pool.pool (fun conn ->
266
+
(* Connection acquired from pool or newly created *)
268
+
(* Validate before use *)
269
+
if not (is_healthy ~check_readable:true conn) then (
270
+
(* Connection unhealthy, close and create new one *)
271
+
close_internal pool conn;
272
+
let new_conn = create_connection pool endpoint in
274
+
(* Use new connection with failure boundary *)
275
+
match f new_conn.flow with
277
+
(* Success - update metadata and return to pool *)
278
+
conn.last_used <- Unix.gettimeofday ();
279
+
conn.use_count <- conn.use_count + 1;
282
+
(* Error - close connection, don't return to pool *)
283
+
close_internal pool new_conn;
286
+
(* Connection healthy, use it *)
287
+
match f conn.flow with
289
+
(* Success - update metadata and return to pool *)
290
+
conn.last_used <- Unix.gettimeofday ();
291
+
conn.use_count <- conn.use_count + 1;
294
+
(* Error - close connection, don't return to pool *)
295
+
close_internal pool conn;
301
+
### Eio.Pool Integration
304
+
let create_endpoint_pool pool endpoint =
305
+
(* Eio.Pool.create manages resource limits *)
306
+
let eio_pool = Eio.Pool.create
307
+
pool.config.max_connections_per_endpoint
308
+
~validate:(fun conn ->
309
+
(* Called before reusing from pool *)
310
+
is_healthy ~check_readable:false conn
312
+
~dispose:(fun conn ->
313
+
(* Called when removing from pool *)
314
+
Eio.Cancel.protect (fun () ->
315
+
close_internal pool conn
319
+
(* Factory: create new connection *)
320
+
create_connection pool endpoint
326
+
stats = create_stats ();
327
+
mutex = Eio.Mutex.create ();
331
+
### Connection Creation with Timeout & TLS
334
+
let create_connection pool endpoint =
336
+
Eio.Mutex.use_rw pool.endpoints_mutex (fun () ->
337
+
let ep_pool = Hashtbl.find pool.endpoints endpoint in
338
+
ep_pool.stats.total_created <- ep_pool.stats.total_created + 1
341
+
(* Call hook if configured *)
342
+
Option.iter (fun f -> f endpoint) pool.config.on_connection_created;
344
+
(* Connect with optional timeout *)
345
+
let connect_with_timeout () =
347
+
Eio.Net.Ipaddr.V4.loopback, (* TODO: resolve hostname *)
351
+
match pool.config.connect_timeout with
353
+
Eio.Time.with_timeout_exn pool.clock timeout (fun () ->
354
+
Eio.Net.connect ~sw:pool.sw pool.net addr
357
+
Eio.Net.connect ~sw:pool.sw pool.net addr
360
+
let flow = connect_with_timeout () in
362
+
(* Wrap with TLS if configured *)
363
+
let flow = match endpoint.tls with
366
+
let host = match tls_cfg.servername with
367
+
| Some name -> Domain_name.(host_exn (of_string_exn name))
368
+
| None -> Domain_name.(host_exn (of_string_exn endpoint.host))
370
+
Tls_eio.client_of_flow ~host tls_cfg.config flow
375
+
created_at = Unix.gettimeofday ();
376
+
last_used = Unix.gettimeofday ();
382
+
### Connection Validation
385
+
let is_healthy ?(check_readable = false) conn =
386
+
let now = Unix.gettimeofday () in
387
+
let config = (* get config from pool *) in
390
+
let age = now -. conn.created_at in
391
+
if age > config.max_connection_lifetime then
394
+
(* Check idle time *)
395
+
else if (now -. conn.last_used) > config.max_idle_time then
398
+
(* Check use count *)
399
+
else if (match config.max_connection_uses with
400
+
| Some max -> conn.use_count >= max
401
+
| None -> false) then
404
+
(* Optional: custom health check *)
405
+
else if (match config.health_check with
406
+
| Some check -> not (check conn.flow)
407
+
| None -> false) then
410
+
(* Optional: check if socket still connected *)
411
+
else if check_readable then
413
+
(* Try zero-byte read - if socket closed, will raise *)
414
+
let buf = Cstruct.create 0 in
415
+
let _ = Eio.Flow.single_read conn.flow buf in
418
+
| End_of_file -> false
425
+
### Graceful Shutdown with Cancel.protect
428
+
let close_pool pool =
429
+
Eio.Cancel.protect (fun () ->
430
+
(* Close all endpoint pools *)
431
+
Hashtbl.iter (fun endpoint ep_pool ->
432
+
(* Eio.Pool.dispose will call our dispose function for each connection *)
433
+
Eio.Pool.dispose ep_pool.pool
436
+
Hashtbl.clear pool.endpoints
439
+
(* Register cleanup with switch *)
440
+
let create ~sw ~net ?(config = default_config) () =
445
+
endpoints = Hashtbl.create 16;
446
+
endpoints_mutex = Eio.Mutex.create ();
449
+
(* Auto-cleanup on switch release *)
450
+
Eio.Switch.on_release sw (fun () ->
459
+
### Example 1: HTTP Client Connection Pooling
465
+
Eio_main.run @@ fun env ->
466
+
Switch.run @@ fun sw ->
468
+
(* Create connection pool *)
469
+
let pool = Conpool.create ~sw ~net:env#net () in
471
+
(* Define endpoint *)
472
+
let endpoint = Conpool.{
473
+
host = "example.com";
476
+
config = my_tls_config;
477
+
servername = Some "example.com";
481
+
(* Make 100 requests - connections will be reused *)
482
+
for i = 1 to 100 do
483
+
Conpool.with_connection pool endpoint (fun conn ->
484
+
(* Send HTTP request *)
485
+
let request = Printf.sprintf
486
+
"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" in
487
+
Eio.Flow.write conn [Cstruct.of_string request];
489
+
(* Read response *)
490
+
let buf = Cstruct.create 4096 in
491
+
let n = Eio.Flow.single_read conn buf in
492
+
Printf.printf "Response %d: %d bytes\n" i n
496
+
(* Print statistics *)
497
+
let stats = Conpool.stats pool endpoint in
498
+
Printf.printf "Created: %d, Reused: %d\n"
499
+
stats.total_created stats.total_reused
502
+
### Example 2: Redis Client with Health Checks
505
+
let redis_health_check flow =
506
+
(* Send PING command *)
508
+
Eio.Flow.write flow [Cstruct.of_string "PING\r\n"];
509
+
let buf = Cstruct.create 7 in
510
+
let n = Eio.Flow.single_read flow buf in
511
+
(* Check for "+PONG\r\n" response *)
512
+
n = 7 && Cstruct.to_string buf = "+PONG\r\n"
517
+
Eio_main.run @@ fun env ->
518
+
Switch.run @@ fun sw ->
520
+
let config = Conpool.{
521
+
default_config with
522
+
health_check = Some redis_health_check;
523
+
max_idle_time = 30.0; (* Redis connections timeout quickly *)
524
+
max_connections_per_endpoint = 50;
527
+
let pool = Conpool.create ~sw ~net:env#net ~config () in
529
+
let redis_endpoint = Conpool.{
530
+
host = "localhost";
535
+
(* Connection automatically validated with PING before reuse *)
536
+
Conpool.with_connection pool redis_endpoint (fun conn ->
537
+
Eio.Flow.write conn [Cstruct.of_string "GET mykey\r\n"];
538
+
(* ... read response ... *)
542
+
### Example 3: PostgreSQL with Connection Limits
546
+
Eio_main.run @@ fun env ->
547
+
Switch.run @@ fun sw ->
549
+
let config = Conpool.{
550
+
default_config with
551
+
max_connections_per_endpoint = 20; (* PostgreSQL default max_connections *)
552
+
max_connection_lifetime = 3600.0; (* 1 hour max lifetime *)
553
+
max_connection_uses = Some 1000; (* Recycle after 1000 queries *)
556
+
let pool = Conpool.create ~sw ~net:env#net ~config () in
558
+
let db_endpoint = Conpool.{
559
+
host = "db.example.com";
561
+
tls = Some { config = tls_config; servername = None };
564
+
(* 100 concurrent queries - limited to 20 connections *)
565
+
Eio.Fiber.all (List.init 100 (fun i ->
567
+
Conpool.with_connection pool db_endpoint (fun conn ->
568
+
(* Execute query on conn *)
569
+
Printf.printf "Query %d\n" i
575
+
### Example 4: Manual Connection Management
578
+
(* Advanced: manual acquire/release for transactions *)
579
+
let with_transaction pool endpoint f =
580
+
let conn = Conpool.acquire pool endpoint in
583
+
(* Begin transaction *)
584
+
Eio.Flow.write (Conpool.get_flow conn)
585
+
[Cstruct.of_string "BEGIN\r\n"];
587
+
(* Execute user code *)
588
+
let result = f (Conpool.get_flow conn) in
591
+
Eio.Flow.write (Conpool.get_flow conn)
592
+
[Cstruct.of_string "COMMIT\r\n"];
594
+
(* Return connection to pool *)
595
+
Conpool.release pool conn;
599
+
(* Rollback on error *)
600
+
Eio.Flow.write (Conpool.get_flow conn)
601
+
[Cstruct.of_string "ROLLBACK\r\n"];
603
+
(* Connection still usable, return to pool *)
604
+
Conpool.release pool conn;
609
+
## Integration with Requests Library
611
+
The requests library will use Conpool for TCP connection management:
614
+
(* requests/lib/one.ml *)
619
+
default_headers : Headers.t;
620
+
timeout : Timeout.t;
621
+
(* ... other fields ... *)
622
+
connection_pool : Conpool.t; (* NEW *)
625
+
let request ~sw ?client ~method_ url =
626
+
let client = get_client client in
628
+
(* Parse URL to endpoint *)
629
+
let uri = Uri.of_string url in
630
+
let endpoint = uri_to_conpool_endpoint uri client.tls_config in
632
+
(* Acquire connection from pool *)
633
+
Conpool.with_connection client.connection_pool endpoint (fun tcp_conn ->
634
+
(* Create cohttp client wrapping this connection *)
635
+
let cohttp_client = make_cohttp_client_from_flow tcp_conn in
637
+
(* Make HTTP request over pooled connection *)
638
+
let resp, resp_body =
639
+
Cohttp_eio.Client.call ~sw cohttp_client method_ uri
643
+
(* Must fully drain body before with_connection returns *)
644
+
(* Otherwise connection in inconsistent state *)
645
+
Eio.Cancel.protect (fun () ->
646
+
let body_data = Eio.Buf_read.parse_exn take_all resp_body
647
+
~max_size:max_int in
649
+
(* Connection auto-returned to pool when with_connection exits *)
650
+
Response.make ~sw ~status ~headers
651
+
~body:(string_source body_data) ~url ~elapsed
656
+
## Advanced Features
658
+
### DNS Caching & Resolution
660
+
Conpool will cache DNS lookups per hostname:
664
+
(* ... existing fields ... *)
665
+
dns_cache : (string, Eio.Net.Sockaddr.t list) Hashtbl.t;
666
+
dns_cache_mutex : Eio.Mutex.t;
667
+
dns_cache_ttl : float; (* Default: 300.0 seconds *)
670
+
let resolve_hostname pool host port =
671
+
(* Check cache first *)
672
+
(* Fall back to Eio.Net.getaddrinfo_stream *)
673
+
(* Cache result with TTL *)
676
+
### Connection Warming
678
+
Pre-establish connections to reduce first-request latency:
681
+
val warm_endpoint :
686
+
(** Pre-create [count] connections to endpoint and add to pool *)
689
+
### Circuit Breaker Integration
692
+
type circuit_breaker_state =
693
+
| Closed (* Normal operation *)
694
+
| Open (* Failing, reject requests *)
695
+
| HalfOpen (* Testing if recovered *)
697
+
val with_circuit_breaker :
700
+
failure_threshold:int ->
702
+
(Eio.Flow.two_way -> 'a) ->
704
+
(** Wrap with_connection with circuit breaker pattern *)
707
+
## Testing Strategy
711
+
1. **Connection lifecycle:**
712
+
- Create → Use → Release → Reuse
713
+
- Create → Use → Error → Close (not reused)
719
+
- Health check failure
721
+
3. **Concurrency:**
722
+
- Multiple fibers acquiring from same endpoint
723
+
- Limit enforcement (blocks at max_connections_per_endpoint)
724
+
- Thread-safety of pool operations
726
+
4. **Cancel safety:**
727
+
- Cancel during connection creation
728
+
- Cancel during use
729
+
- Cancel during release
730
+
- Pool cleanup on switch release
732
+
### Integration Tests
734
+
1. **Real TCP servers:**
735
+
- HTTP server with keep-alive
736
+
- Echo server for connection reuse
737
+
- Server that closes connections to test validation
739
+
2. **Performance:**
740
+
- Connection reuse speedup (10x for 100 requests)
741
+
- Concurrent request handling
742
+
- Pool contention under load
748
+
├── CLAUDE.md (this file)
755
+
│ └── conpool_stats.ml (statistics tracking)
758
+
│ ├── test_lifecycle.ml
759
+
│ ├── test_validation.ml
760
+
│ ├── test_concurrency.ml
761
+
│ └── test_integration.ml
764
+
├── redis_client.ml
765
+
└── postgres_client.ml
775
+
## Comparison with Other Approaches
777
+
### vs. cohttp-lwt Connection_cache
779
+
cohttp-lwt has `Connection_cache` module for HTTP connection pooling:
780
+
- **Limited:** HTTP-specific, tied to cohttp
781
+
- **Conpool:** Protocol-agnostic, reusable across libraries
783
+
### vs. Per-Request Connections (current requests impl)
785
+
Current requests library creates new connection per request:
786
+
- **Overhead:** 3-way handshake + TLS handshake every request
787
+
- **Conpool:** Reuse connections, 5-10x speedup
789
+
### vs. Global Connection Pool
791
+
Some libraries use global singleton pools:
792
+
- **Inflexible:** Can't configure per-client
793
+
- **Conpool:** Pool per Conpool.t instance, fine-grained control
795
+
## Future Enhancements
797
+
1. **HTTP/2 & HTTP/3 Support**
798
+
- Track streams per connection
799
+
- Multiplexing-aware pooling
801
+
2. **Metrics Export**
802
+
- Prometheus exporter
803
+
- OpenTelemetry integration
805
+
3. **Advanced Health Checks**
806
+
- Periodic background health checks
807
+
- Adaptive health check frequency
809
+
4. **Connection Affinity**
810
+
- Sticky connections for stateful protocols
811
+
- Session-aware pooling
813
+
5. **Proxy Support**
814
+
- HTTP CONNECT tunneling
817
+
6. **Unix Domain Sockets**
818
+
- Pool UDS connections (e.g., local Redis, PostgreSQL)
823
+
- ✅ **Protocol-agnostic** TCP/IP connection pooling
824
+
- ✅ **Eio.Pool foundation** for resource management
825
+
- ✅ **Per-endpoint** isolation and limits
826
+
- ✅ **Connection validation** (age, idle, health checks)
827
+
- ✅ **Cancel-safe** operations with `Cancel.protect`
828
+
- ✅ **Rich statistics** and monitoring
829
+
- ✅ **Drop-in integration** with requests, redis-eio, etc.
831
+
This design separates TCP pooling from protocol logic, making it a reusable foundation for any TCP-based Eio library.