Package: asyncio-connection-pool
asyncio-connection-poolThis is a generic, high-throughput, optionally-burstable pool for asyncio.Some cool features:No locking[^1]; noasyncio.Lockorasyncio.Conditionneeds to be taken in
order to get a connection.Available connections are retrieved without yielding to the event loop.Whenburst_limitis specified,max_sizeacts as a "soft" limit; the pool
can go beyond this limit to handle increased load, and shrinks back down
after.The contents of the pool can be anything; just implement aConnectionStrategy.[^1]: Theoretically, there is an implicit "lock" that is held while an asyncio
task is executing. No other task can execute until the current task
yields (since it's cooperative multitasking), so any operations during
that time are atomic.Why?We were using a different pool for handling our Redis connections, and noticed
that, under heavy load, we would spend a lot of time waiting for the lock, even
when there were available connections in the pool.We also thought it would be nice if we didn't need to keep many connections
open when they weren't needed, but still have the ability to make more when
they are required.APIasyncio_connection_pool.ConnectionPoolThis is the implementation of the pool. It is generic over a type of
connection, and all implementation-specific logic is contained within aConnectionStrategy.A pool is created as follows:fromasyncio_connection_poolimportConnectionPoolpool=ConnectionPool(strategy=my_strategy,max_size=15)The constructor can optionally be passed an integer asburst_limit. This
allows the pool to open more connections thanmax_sizetemporarily.@asynccontextmanager async def get_connection(self) -> AsyncIterator[Conn]This method is the only way to get a connection from the pool. It is expected
to be used as follows:pool=ConnectionPool(...)asyncwithpool.get_connection()asconn:# Use the connectionpassWhen theasync withblock is entered, a connection is retrieved. If a
connection needs to be opened or if the pool is at capacity and no connections
are available, the caller will yield to the event loop.When the block is exited, the connection will be returned to the pool.asyncio_connection_pool.ConnectionStrategyThis is an abstract class that defines the interface of the object passed asstrategy. A subclassmustimplement the following methods:async def create_connection(self) -> Awaitable[Conn]This method is called to create a new connection to the resource. This happens
when a connection is requested and all connections are in use, as long as the
pool is not at capacity.The result of a call to this method is what will be provided to a consumer of
the pool, and in most cases will be stored in the pool to be re-used later.If this method raises an exception, it will bubble up to the frame whereConnectionPool.get_connection()was called.def connection_is_closed(self, conn: Conn) -> boolThis method is called to check if a connection is no longer able to be used.
When the pool is retrieving a connection to give to a client, this method is
called to make sure it is valid.The return value should beTrueif the connection isnotvalid.If this method raises an exception, it is assumed that the connection is
invalid. The passed-in connection is dropped and a new one is retrieved. The
exception is suppressed unless it is not aBaseException, likeasyncio.CancelledError. It is the responsibility of theConnectionStrategyimplementation to avoid leaking a connection in this case.async def close_connection(self, conn: Conn)This method is called to close a connection. This occurs when the pool has
exceededmax_size(i.e. it is bursting) and a connection is returned that is
no longer needed (i.e. there are no more consumers waiting for a connection).If this method raises an exception, the connection is assumed to be closed and
the exception bubbles to the caller ofConnectionPool.get_connection().__aexit__(usually anasync withblock).Integrations  with 3rd-party librariesThis package includes support forddtrace/datadogand
foraioredis(<2.0.0).asyncio_connection_pool.contrib.datadog.ConnectionPoolThis class subclasses theConnectionPoolin the root of the package, and adds
a bunch of tracing, gauges, and events. The constructor, in addition to the
arguments of the base class, supports:Requiredservice_nameargument: A prefix to all of the metricsOptionalextra_tagsargument: Additional tags to provide to all metrics
(strings in a"key:value"format)asyncio_connection_pool.contrib.aioredis.RedisConnectionStrategyThis class implements theConnectionStrategyabstract methods, usingaioredis.Redisobjects as connections. The constructor takes arbitrary
arguments and forwards them toaioredis.create_redis.How is this safe without locks?I encourage you to read thesourceto find out (it is quite well-commented). If you notice any faults in the
logic, please feel free to file an issue.