packages feed

polysemy-hasql-0.0.1.0: lib/Polysemy/Hasql/Interpreter/DbConnectionPool.hs

module Polysemy.Hasql.Interpreter.DbConnectionPool where

import Conc (interpretAtomic)
import Control.Concurrent (ThreadId, myThreadId, throwTo)
import qualified Data.Map.Strict as Map
import Data.Map.Strict ((!?))
import qualified Data.Sequence as Seq
import Data.Sequence (Seq ((:<|)), (<|))
import Exon (exon)
import qualified Hasql.Connection as Connection
import Hasql.Connection (Connection)
import Lens.Micro.Extras (view)
import qualified Log
import Polysemy.Db.Data.DbConfig (DbConfig (DbConfig))
import qualified Polysemy.Db.Data.DbConnectionError as DbConnectionError
import Polysemy.Db.Data.DbConnectionError (DbConnectionError)
import Polysemy.Db.Data.DbHost (DbHost (DbHost))
import Polysemy.Db.Data.DbName (DbName (DbName))
import Polysemy.Db.Data.DbPassword (DbPassword (DbPassword))
import Polysemy.Db.Data.DbUser (DbUser (DbUser))
import qualified Text.Show as Show

import Polysemy.Hasql.Data.ConnectionTag (ConnectionTag)
import Polysemy.Hasql.Effect.DbConnectionPool (DbConnectionPool (Acquire, Config, Free, Kill, Release, UnsafeGet, Use))

data KillCommand =
  KillCommand
  deriving stock (Show)
  deriving anyclass (Exception)

newtype PoolConn =
  PoolConn { unPoolConn :: Connection }
  deriving stock (Generic)

instance Show PoolConn where
  show _ = "PoolConn"

data ConnectionClients =
  ConnectionClients {
    connection :: PoolConn,
    clients :: Map ThreadId Int
  }
  deriving stock (Show, Generic)

data Pools =
  Pools {
    maxActive :: Maybe Int,
    maxAvailable :: Maybe Int,
    active :: Map ConnectionTag ConnectionClients,
    available :: Seq PoolConn
  }
  deriving stock (Show, Generic)

connectionSettings ::
  DbConfig ->
  Connection.Settings
connectionSettings (DbConfig (DbHost host) port (DbName dbName) (DbUser user) (DbPassword password)) =
  Connection.settings (encodeUtf8 host) (fromIntegral port) (encodeUtf8 user) (encodeUtf8 password) (encodeUtf8 dbName)

dbError :: Maybe ByteString -> DbConnectionError
dbError err =
  DbConnectionError.Acquire (maybe "unspecified error" decodeUtf8 err)

withActive ::
  Member (AtomicState Pools) r =>
  (Int -> Map ConnectionTag ConnectionClients -> Sem r a) ->
  Sem r (Maybe a)
withActive f =
  atomicGet >>= \case
    Pools {maxActive = Just ma, ..} ->
      Just <$> f ma active
    _ ->
      pure Nothing

acquireNative ::
  Members [Stop DbConnectionError, Embed IO] r =>
  DbConfig ->
  Sem r Connection
acquireNative dbConfig = do
  conn <- stopTryIOError DbConnectionError.Acquire (Connection.acquire (connectionSettings dbConfig))
  stopEither (first dbError conn)

acquire ::
  Members [AtomicState Pools, Stop DbConnectionError, Embed IO] r =>
  DbConfig ->
  ConnectionTag ->
  Sem r Connection
acquire dbConfig ctag = do
  void $ withActive \ m act ->
    when (Map.size act >= m) (stop (DbConnectionError.Limit [exon|Too many active connections: #{show m}|]))
  conn <- acquireNative dbConfig
  conn <$ atomicModify' (#active . at ctag ?~ ConnectionClients (PoolConn conn) mempty)

reuseOrAcquire ::
  Members [AtomicState Pools, Stop DbConnectionError, Log, Embed IO] r =>
  DbConfig ->
  ConnectionTag ->
  Sem r Connection
reuseOrAcquire dbConfig ctag = do
  reuse <- atomicState' \ pools@Pools {..} ->
    case active !? ctag of
      Just (ConnectionClients (PoolConn conn) _) ->
        (pools, Just conn)
      Nothing ->
        case available of
          PoolConn conn :<| rest ->
            (pools {available = rest}, Just conn)
          _ ->
            (pools, Nothing)
  when (isJust reuse) do
    Log.trace [exon|Reusing connection for '##{ctag}'|]
  fromMaybeA (acquire dbConfig ctag) reuse

releaseNative ::
  Members [Stop DbConnectionError, Embed IO] r =>
  Connection ->
  Sem r ()
releaseNative connection =
  stopTryIOError DbConnectionError.Release (Connection.release connection)

release ::
  Members [AtomicState Pools, Stop DbConnectionError, Embed IO] r =>
  ConnectionTag ->
  Sem r ()
release ctag = do
  conn <- atomicState' \ Pools {..} -> (Pools {active = Map.delete ctag active, ..}, active !? ctag)
  traverse_ (releaseNative . coerce . connection) conn

-- | Remove the connection used by @ctag@ from the active pool if it exists.
-- Store it for reuse if @maxAvailable@ is @Nothing@ or larger than the currently stored number, otherwise return the
-- connection for release.
removeActive :: ConnectionTag -> Pools -> (Pools, Maybe Connection)
removeActive ctag Pools {..} =
  (Pools {active = newActive, available = newAvailable, ..}, coerce toRelease)
  where
    -- Chooses the functor @(Maybe Connection, -)@ for @alterF@, thereby returning the potentially existing element.
    -- The @Nothing@ causes the element to be deleted if it exists.
    (conn, newActive) = Map.alterF (,Nothing) ctag active

    (toRelease, newAvailable) = case conn of
      Just (ConnectionClients c _)
        | keep -> (Nothing, c <| available)
        | otherwise -> (Just c, available)
      Nothing -> (Nothing, available)

    keep = case maxAvailable of
      Nothing -> True
      Just m -> Seq.length available < m

catchingKill ::
  Members [Stop DbConnectionError, Final IO] r =>
  Sem r a ->
  Sem r a
catchingKill =
  stopOnError . mapError exception . fromExceptionSem . raiseUnder . raise
  where
    exception KillCommand =
      DbConnectionError.Query "command was interrupted by DbConnectionPool.Kill"

-- Incrementing the tid must not be masked or it might not be cleaned up.
withRegisteredClient ::
  Members [AtomicState Pools, Stop DbConnectionError, Resource, Embed IO, Final IO] r =>
  ConnectionTag ->
  Sem r a ->
  Sem r a
withRegisteredClient ctag main = do
  tid <- embed myThreadId
  finally
    do
      catchingKill do
        change tid increment
        main
    do
      change tid decrement
  where
    change tid f =
      atomicModify' (#active . at ctag %~ fmap (#clients %~ Map.alter f tid))
    increment = \case
      Just n -> Just (n + 1)
      Nothing -> Just 1
    decrement = \case
      Just 1 -> Nothing
      Just n -> Just (n - 1)
      Nothing -> Nothing

releaseAll ::
  Members [AtomicState Pools, Log, Resource, Embed IO, Final IO] r =>
  Sem r ()
releaseAll =
  atomicGet >>= \ Pools {active, available} -> do
    for_ (Map.elems active) \ (ConnectionClients conn _) ->
      releaseOrLog conn
    traverse_ releaseOrLog available
  where
    releaseOrLog (PoolConn conn) =
      runStop (releaseNative conn) >>= leftA \ e ->
        Log.error [exon|Releasing connection failed: #{show e}|]

handleDbConnectionPool ::
  Members [AtomicState Pools, Stop DbConnectionError, Log, Resource, Embed IO, Final IO] r =>
  DbConfig ->
  DbConnectionPool m a ->
  Tactical e m r a
handleDbConnectionPool dbConfig = \case
  Acquire ctag -> do
    Log.trace [exon|Acquiring connection '##{ctag}'|]
    pureT =<< reuseOrAcquire dbConfig ctag
  Free ctag -> do
    traverse_ releaseNative =<< atomicState' (removeActive ctag)
    unitT
  Release ctag -> do
    Log.trace [exon|Releasing connection '##{ctag}'|]
    pureT =<< release ctag
  Use ctag ma ->
    withRegisteredClient ctag (runTSimple ma)
  Kill ctag -> do
    cur <- embed myThreadId
    atomicGets (view (#active . at ctag)) >>= traverse_ \ (ConnectionClients _ clients) -> do
      for_ (Map.keys clients) \ c ->
        unless (cur == c) (embed (throwTo c KillCommand))
    pureT =<< release ctag
  UnsafeGet ctag ->
    pureT . fmap (coerce . connection) =<< atomicGets (view (#active . at ctag))
  Config ->
    pureT dbConfig

interpretDbConnectionPool ::
  Members [Log, Resource, Embed IO, Final IO] r =>
  DbConfig ->
  Maybe Int ->
  Maybe Int ->
  InterpreterFor (DbConnectionPool !! DbConnectionError) r
interpretDbConnectionPool dbConfig maxActive maxAvailable =
  interpretAtomic (Pools maxActive maxAvailable mempty mempty) .
  flip finally releaseAll .
  interpretResumableH (handleDbConnectionPool dbConfig) .
  raiseUnder

handleDbConnectionPoolSingle ::
  Members [AtomicState (Maybe Connection), Stop DbConnectionError, Embed IO] r =>
  DbConfig ->
  DbConnectionPool m a ->
  Tactical e m r a
handleDbConnectionPoolSingle dbConfig = \case
  Acquire _ -> do
    let
      acquireSingle = do
        c <- acquireNative dbConfig
        c <$ atomicPut (Just c)
    pureT =<< fromMaybeA acquireSingle =<< atomicGet
  Free _ ->
    unitT
  -- TODO this should be called from outside of the DbConnection scope interpreter only
  Release _ -> do
    traverse_ releaseNative =<< atomicGet
    unitT
  -- TODO maybe not very useful but possible
  Use _ ma ->
    runTSimple ma
  Kill _ ->
    unitT
  UnsafeGet _ ->
    pureT Nothing
  Config ->
    pureT dbConfig

interpretDbConnectionPoolSingle ::
  Member (Embed IO) r =>
  DbConfig ->
  InterpreterFor (DbConnectionPool !! DbConnectionError) r
interpretDbConnectionPoolSingle dbConfig =
  interpretAtomic Nothing .
  interpretResumableH (handleDbConnectionPoolSingle dbConfig) .
  raiseUnder