hasql-pool-0.5.2.1: library/Hasql/Pool.hs
module Hasql.Pool
(
Pool,
Settings(..),
acquire,
release,
UsageError(..),
use,
)
where
import Hasql.Pool.Prelude
import Hasql.Connection (Connection)
import Hasql.Session (Session)
import qualified Hasql.Connection as Connection
import qualified Hasql.Session as Session
-- |
-- A pool of connections to DB.
data Pool =
Pool
Connection.Settings
{-^ Connection settings. -}
(TQueue ActiveConnection)
{-^ Queue of established connections. -}
(TVar Int)
{-^ Slots available for establishing new connections. -}
(TVar Bool)
{-^ Flag signaling whether pool's alive. -}
data ActiveConnection =
ActiveConnection {
activeConnectionLastUseTimestamp :: Int,
activeConnectionConnection :: Connection
}
loopCollectingGarbage :: Int -> TQueue ActiveConnection -> TVar Int -> TVar Bool -> IO ()
loopCollectingGarbage timeout establishedQueue slotsAvailVar aliveVar =
decide
where
decide =
do
ts <- getMillisecondsSinceEpoch
join $ atomically $ do
alive <- readTVar aliveVar
if alive
then let
tryToRelease =
tryReadTQueue establishedQueue >>= \ case
-- The queue is empty. Just wait for changes in the state.
Nothing ->
retry
Just entry@(ActiveConnection lastUseTs connection) ->
let
outdatingTs =
lastUseTs + timeout
in
-- Check whether it's outdated.
if outdatingTs < ts
-- Fetch the current value of available slots and
-- release this one and other connections.
then do
slotsAvail <- readTVar slotsAvailVar
collectAndRelease slotsAvail [connection] outdatingTs
-- Return it to the front of the queue and
-- wait until it's outdating time.
else do
unGetTQueue establishedQueue entry
return (sleep outdatingTs *> decide)
collectAndRelease !slotsAvail !outdatedList outdatingTs =
tryReadTQueue establishedQueue >>= \ case
Nothing ->
finalizeAndRelease slotsAvail outdatedList outdatingTs
Just entry@(ActiveConnection lastUseTs connection) ->
let
outdatingTs =
lastUseTs + timeout
in
if outdatingTs < ts
then do
unGetTQueue establishedQueue entry
finalizeAndRelease slotsAvail outdatedList outdatingTs
else
collectAndRelease (succ slotsAvail) (connection : outdatedList) outdatingTs
finalizeAndRelease slotsAvail outdatedList outdatingTs =
do
writeTVar slotsAvailVar slotsAvail
return (release outdatedList *> sleep outdatingTs *> decide)
in tryToRelease
else do
list <- flushTQueue establishedQueue
return (release (fmap activeConnectionConnection list))
sleep untilTs =
do
ts <- getMillisecondsSinceEpoch
let
diff =
untilTs - ts
in if diff > 0
then threadDelay (diff * 1000)
else return ()
release =
traverse_ Connection.release
-- |
-- Settings of the connection pool. Consist of:
--
-- * Pool-size.
--
-- * Timeout.
-- An amount of time in milliseconds for which the unused connections are kept open.
--
-- * Connection settings.
--
type Settings =
(Int, Int, Connection.Settings)
-- |
-- Given the pool-size, timeout and connection settings
-- create a connection-pool.
acquire :: Settings -> IO Pool
acquire (size, timeout, connectionSettings) =
do
establishedQueue <- newTQueueIO
slotsAvailVar <- newTVarIO size
aliveVar <- newTVarIO (size > 0)
forkIO $ loopCollectingGarbage timeout establishedQueue slotsAvailVar aliveVar
return (Pool connectionSettings establishedQueue slotsAvailVar aliveVar)
-- |
-- Release the connection-pool.
release :: Pool -> IO ()
release (Pool _ _ _ aliveVar) =
atomically (writeTVar aliveVar False)
-- |
-- A union over the connection establishment error and the session error.
data UsageError =
ConnectionUsageError Connection.ConnectionError |
SessionUsageError Session.QueryError |
PoolIsReleasedUsageError
deriving (Show, Eq)
-- |
-- Use a connection from the pool to run a session and
-- return the connection to the pool, when finished.
use :: Pool -> Session.Session a -> IO (Either UsageError a)
use (Pool connectionSettings establishedQueue slotsAvailVar aliveVar) session =
join $ atomically $ do
alive <- readTVar aliveVar
if alive
then
tryReadTQueue establishedQueue >>= \ case
-- No established connection avail at the moment.
Nothing -> do
slotsAvail <- readTVar slotsAvailVar
-- Do we have any slots left for establishing new connections?
if slotsAvail > 0
-- Reduce the available slots var and instruct to
-- establish and use a new connection.
then do
writeTVar slotsAvailVar $! pred slotsAvail
return acquireConnectionThenUseThenPutItToQueue
-- Wait until the state changes and retry.
else
retry
Just (ActiveConnection _ connection) ->
return (useConnectionThenPutItToQueue connection)
else
return (return (Left PoolIsReleasedUsageError))
where
acquireConnectionThenUseThenPutItToQueue =
do
res <- Connection.acquire connectionSettings
case res of
-- Failed to acquire, so release an availability slot,
-- returning the error details.
Left acquisitionError -> do
atomically $ modifyTVar' slotsAvailVar succ
return (Left (ConnectionUsageError acquisitionError))
Right connection ->
useConnectionThenPutItToQueue connection
useConnectionThenPutItToQueue connection =
do
res <- Session.run session connection
case res of
Left queryError -> do
-- Check whether the error is on client-side,
-- and in that case release the connection.
case queryError of
Session.QueryError _ _ (Session.ClientError _) ->
releaseConnection connection
_ ->
putConnectionToPool connection
return (Left (SessionUsageError queryError))
Right res -> do
putConnectionToPool connection
return (Right res)
putConnectionToPool connection =
do
ts <- getMillisecondsSinceEpoch
atomically $ writeTQueue establishedQueue (ActiveConnection ts connection)
releaseConnection connection =
do
atomically $ modifyTVar' slotsAvailVar succ
Connection.release connection