cql-io-2.0.0: src/lib/Database/CQL/IO/Pool.hs
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
{-# LANGUAGE CPP #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Database.CQL.IO.Pool
( Pool
, create
, destroy
, purge
, with
, PoolSettings
, defSettings
, idleTimeout
, maxConnections
, maxTimeouts
, poolStripes
) where
import Control.AutoUpdate
import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception
import Control.Lens ((^.), makeLenses, view)
import Control.Monad.IO.Class
import Control.Monad
#if !MIN_VERSION_base(4,8,0)
import Data.Foldable (forM_, mapM_, find)
#else
import Data.Foldable (find)
#endif
import Data.Function (on)
import Data.Hashable
import Data.IORef
import Data.Sequence (Seq, ViewL (..), (|>), (><))
#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup ((<>))
#endif
import Data.Time.Clock (UTCTime, NominalDiffTime, getCurrentTime, diffUTCTime)
import Data.Vector (Vector, (!))
import Database.CQL.IO.Connection (Connection)
import Database.CQL.IO.Exception (ConnectionError (..), ignore)
import Database.CQL.IO.Log
import qualified Data.Sequence as Seq
import qualified Data.Vector as Vec
-----------------------------------------------------------------------------
-- API
data PoolSettings = PoolSettings
{ _idleTimeout :: !NominalDiffTime
, _maxConnections :: !Int
, _maxTimeouts :: !Int
, _poolStripes :: !Int
}
data Pool = Pool
{ _createFn :: !(IO Connection)
, _destroyFn :: !(Connection -> IO ())
, _logger :: !Logger
, _settings :: !PoolSettings
, _maxRefs :: !Int
, _currentTime :: !(IO UTCTime)
, _stripes :: !(Vector Stripe)
, _finaliser :: !(IORef ())
}
data Resource = Resource
{ tstamp :: !UTCTime
, refcnt :: !Int
, timeouts :: !Int
, value :: !Connection
} deriving Show
data Box
= New !(IO Resource)
| Used !Resource
| Empty
data Stripe = Stripe
{ conns :: !(TVar (Seq Resource))
, inUse :: !(TVar Int)
}
makeLenses ''PoolSettings
makeLenses ''Pool
defSettings :: PoolSettings
defSettings = PoolSettings
60 -- idle timeout
2 -- max connections per stripe
16 -- max timeouts per connection
4 -- max stripes
create :: IO Connection -> (Connection -> IO ()) -> Logger -> PoolSettings -> Int -> IO Pool
create mk del g s k = do
p <- Pool mk del g s k
<$> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
<*> Vec.replicateM (s^.poolStripes) (Stripe <$> newTVarIO Seq.empty <*> newTVarIO 0)
<*> newIORef ()
r <- async $ reaper p
void $ mkWeakIORef (p^.finaliser) (cancel r >> destroy p)
return p
destroy :: Pool -> IO ()
destroy = purge
with :: MonadIO m => Pool -> (Connection -> IO a) -> m (Maybe a)
with p f = liftIO $ do
s <- stripe p
mask $ \restore -> do
r <- take1 p s
case r of
Just v -> do
x <- restore (f (value v)) `catch` cleanup p s v
put p s v id
return (Just x)
Nothing -> return Nothing
purge :: Pool -> IO ()
purge p = Vec.forM_ (p^.stripes) $ \s -> do
cs <- atomically (swapTVar (conns s) Seq.empty)
mapM_ (ignore . view destroyFn p . value) cs
-----------------------------------------------------------------------------
-- Internal
cleanup :: Pool -> Stripe -> Resource -> SomeException -> IO a
cleanup p s r x = do
case fromException x of
Just (ResponseTimeout {}) -> onTimeout
_ -> destroyR p s r
throwIO x
where
onTimeout =
if timeouts r > p^.settings.maxTimeouts
then do
logInfo (p^.logger) $ string8 (show (value r)) <> ": Too many timeouts."
destroyR p s r
else put p s r incrTimeouts
take1 :: Pool -> Stripe -> IO (Maybe Resource)
take1 p s = do
r <- atomically $ do
c <- readTVar (conns s)
u <- readTVar (inUse s)
let n = Seq.length c
check (u == n)
let r :< rr = Seq.viewl $ Seq.unstableSortBy (compare `on` refcnt) c
if | u < p^.settings.maxConnections -> do
writeTVar (inUse s) $! u + 1
mkNew p
| n > 0 && refcnt r < p^.maxRefs -> use s r rr
| otherwise -> return Empty
case r of
New io -> do
x <- io `onException` atomically (modifyTVar' (inUse s) (subtract 1))
atomically (modifyTVar' (conns s) (|> x))
return (Just x)
Used x -> return (Just x)
Empty -> return Nothing
use :: Stripe -> Resource -> Seq Resource -> STM Box
use s r rr = do
writeTVar (conns s) $! rr |> r { refcnt = refcnt r + 1 }
return (Used r)
{-# INLINE use #-}
mkNew :: Pool -> STM Box
mkNew p = return (New $ Resource <$> p^.currentTime <*> pure 1 <*> pure 0 <*> p^.createFn)
{-# INLINE mkNew #-}
put :: Pool -> Stripe -> Resource -> (Resource -> Resource) -> IO ()
put p s r f = do
now <- p^.currentTime
let updated x = f x { tstamp = now, refcnt = refcnt x - 1 }
atomically $ do
rs <- readTVar (conns s)
let (xs, rr) = Seq.breakl ((value r ==) . value) rs
case Seq.viewl rr of
EmptyL -> writeTVar (conns s) $! xs |> updated r
y :< ys -> writeTVar (conns s) $! (xs >< ys) |> updated y
destroyR :: Pool -> Stripe -> Resource -> IO ()
destroyR p s r = do
atomically $ do
rs <- readTVar (conns s)
case find ((value r ==) . value) rs of
Nothing -> return ()
Just _ -> do
modifyTVar' (inUse s) (subtract 1)
writeTVar (conns s) $! Seq.filter ((value r /=) . value) rs
ignore $ p^.destroyFn $ value r
reaper :: Pool -> IO ()
reaper p = forever $ do
threadDelay 1000000
now <- p^.currentTime
let isStale r = refcnt r == 0 && now `diffUTCTime` tstamp r > p^.settings.idleTimeout
Vec.forM_ (p^.stripes) $ \s -> do
x <- atomically $ do
(stale, okay) <- Seq.partition isStale <$> readTVar (conns s)
unless (Seq.null stale) $ do
writeTVar (conns s) okay
modifyTVar' (inUse s) (subtract (Seq.length stale))
return stale
forM_ x $ \v -> ignore $ do
logDebug (p^.logger) $ "Reaping idle connection: " <> string8 (show (value v))
p^.destroyFn $ value v
stripe :: Pool -> IO Stripe
stripe p = ((p^.stripes) !) . ((`mod` (p^.settings.poolStripes)) . hash) <$> myThreadId
{-# INLINE stripe #-}
incrTimeouts :: Resource -> Resource
incrTimeouts r = r { timeouts = timeouts r + 1 }
{-# INLINE incrTimeouts #-}