Description
I’m seeking for design advice on how to build a connection pool, I would like to spend some time implementing it and hopefully contribute them if I get good results.
I don't know exactly where to start so I thought it might be good to expose what I understand on how the connection pool should work, and show design ideas I found by exploring already existing implementations.
First, the general concept of the connection pool: on each new request, the client asks its connection pool for a connection to the server. When it receives a new connection request, the pool has essentially three choices:
- Use an already opened, unused connection
- Open a new connection if no other connection is available
- If there are already too many concurrent connections to the server, wait until an already opened connection becomes available
In practice the connection is a NIOChannel
that is either directly a TCP connection in the case of HTTP/1.1, or a stream for an HTTP/2 multiplexed connection.
After some research, I stumbled upon the Apache Foundation HttpClient
. It uses an HttpClientConnectionManager
interface that lets the user provide the manager they wish. A PoolingHttpClientConnectionManager
class implements this interface.
I wonder wether the principle of a user provided connection manager makes sense for the Swift NIO HTTP client? In the case of the Apache, I see the other class that implements HttpClientConnectionManager
is BasicHttpClientConnectionManager
, a connection manager that maintains only a single active connection at a time, but if I understand it correctly, this manager purpose is to enable to have one client per thread, which doesn’t seem appliable/useful with NIO? We can also imagine concurrent yet unpooled connection management, which is how the client currently works, but I don’t see the advantage of this over a pooled connection management?
Speaking more specifically about the connection pool, I have a few questions:
- How to give back a connection to the pool? Should we pass it as an argument to a pool method (such as this) , or could we have a design where the pool passes an object (maybe a promise?) to the client on which it calls the appropriate method when it has finished leasing the connection?
- How should we implement the thread safety of the pool? The already existing APIs based on Future/Promises should make this easy for many parts, but the data structure storing the connections must still be protected. Regarding the threading model of NIO, is this a good practice to hold a reference to a specific event loop and to use .execute to ensure what we want is executed on the right thread?
- Speaking of the data structure storing the connections, could this be as simple as a dictionary that keys the connections per host/scheme?
- I saw that some pools have a global cleanup delay where the manager will loop over all opened connections to see which ones have been idle for too long in order to evict them. How does this « global cleanup interval » strategy compare to scheduling timeouts per connection using NIO tasks? From what I see, for instance with the
IdleStateHandler
, NIO doesn’t hesitate to schedule tasks frequently, so I guess the performance impact isn’t too high? - Some pool designs enable specifying a delay after which a connection request is failed if the pool didn’t provide one in time, is this something we would also want for a pool?
Thank you!