{-# OPTIONS_GHC -fno-warn-orphans #-} module Ldap.Client.Pool ( LdapPool , LdapExecutor, Ldap, LdapError , LdapPoolError(..) , withLdap, withLdapFailover, withLdapFailoverReTest , createLdapPool ) where import ClassyPrelude hiding (Handler, catches, try) import Utils.Failover import Control.Lens import Ldap.Client (Ldap, LdapError) import qualified Ldap.Client as Ldap import UnliftIO.Pool import UnliftIO.Async.Utils import Control.Monad.Logger import Data.Time.Clock (NominalDiffTime) import Data.Dynamic import Control.Monad.Trans.Resource (MonadResource) import Control.Monad.Catch import Control.Monad.Trans.Except (throwE) import Data.Fixed (Nano) import Network.Connection type LdapPool = Pool LdapExecutor data LdapExecutor = LdapExecutor { ldapExec :: forall a m. (Typeable a, MonadUnliftIO m) => (Ldap -> m a) -> m (Either LdapPoolError a) , ldapDestroy :: TMVar () , ldapAsync :: Async () } data LdapPoolError = LdapPoolTimeout | LdapError LdapError | LdapLineTooLong | LdapHostNotResolved String | LdapHostCannotConnect String [IOException] deriving (Eq, Show, Generic, Typeable) deriving anyclass (Exception) withLdap :: (MonadUnliftIO m, MonadCatch m, Typeable a) => LdapPool -> (Ldap -> m a) -> m (Either LdapPoolError a) withLdap pool act = fmap join . try . withResource pool $ \LdapExecutor{..} -> ldapExec act withLdapFailover :: (MonadUnliftIO m, MonadMask m, Typeable a, MonadLogger m) => Lens p p' LdapPool Ldap -> Failover p -> FailoverMode -> (p' -> m a) -> m (Either LdapPoolError a) withLdapFailover l@(flip withLens const -> proj) pool' mode act = try . withFailover pool' mode (either throwE return) $ \x -> withLdap (proj x) (\c -> act $ x & l .~ c) withLdapFailoverReTest :: (MonadUnliftIO m, MonadMask m, Typeable a, MonadLogger m) => Lens p p' LdapPool Ldap -> Failover p -> (Nano -> Bool) -> FailoverMode -> (p' -> m a) -> m (Either LdapPoolError a) withLdapFailoverReTest l@(flip withLens const -> proj) pool' doTest mode act = try . withFailoverReTest pool' doTest mode (either throwE return) $ \x -> withLdap (proj x) (\c -> act $ x & l .~ c) createLdapPool :: forall m. ( MonadLogger m , MonadResource m, MonadUnliftIO m , MonadCatch m ) => Ldap.Host -> Ldap.PortNumber -> Int -- ^ Stripes -> NominalDiffTime -- ^ Connection Timeout -> NominalDiffTime -- ^ Action Timeout -> Int -- ^ Limit -> m LdapPool createLdapPool host port stripes timeoutConn (round . (* 1e6) -> timeoutAct) limit = do let mkExecutor :: m LdapExecutor mkExecutor = handleAny (\e -> $logErrorS "LdapExecuter" (tshow e) >> throwM e) . (`catches` convertErrors) $ do ldapDestroy <- liftIO newEmptyTMVarIO ldapAct <- liftIO newEmptyTMVarIO let ldapExec :: forall a m'. (Typeable a, MonadUnliftIO m') => (Ldap -> m' a) -> m' (Either LdapPoolError a) ldapExec act = withRunInIO $ \runInIO -> convertErrors' . withTimeout $ do ldapAnswer <- newEmptyTMVarIO :: IO (TMVar (Either SomeException Dynamic)) atomically $ putTMVar ldapAct (runInIO . fmap toDyn . act, ldapAnswer) either throwIO (return . Right . flip fromDyn (error "Could not cast dynamic")) =<< atomically (takeTMVar ldapAnswer) where convertErrors' = flip catches [ Handler $ return . Left . LdapError . Ldap.ParseError , Handler $ return . Left . LdapError . Ldap.ResponseError , Handler $ return . Left . LdapError . Ldap.IOError , Handler $ return . Left . LdapError . Ldap.DisconnectError , Handler $ return . Left . (id :: LdapPoolError -> LdapPoolError) ] go :: Maybe (TMVar (Maybe a)) -> Ldap -> m () go waiting ldap = do $logDebugS "LdapExecutor" "Waiting" for_ waiting $ atomically . flip putTMVar Nothing instruction <- atomically $ (Nothing <$ takeTMVar ldapDestroy) <|> (Just <$> takeTMVar ldapAct) case instruction of Nothing -> $logDebugS "LdapExecutor" "Terminating" Just (act, returnRes) -> do $logDebugS "LdapExecutor" $ "Executing " <> tshow (host, port) res <- try . withTimeout . liftIO $ act ldap didReturn <- atomically $ tryPutTMVar returnRes res unless didReturn $ $logErrorS "LdapExecutor" "Could not return result" either throwM (const $ return ()) res `catches` [ Handler $ \case Ldap.ResponseError Ldap.ResponseErrorCode{} -> return () other -> throwM other ] go Nothing ldap ldapAsync <- withTimeout $ do setup <- liftIO newEmptyTMVarIO ldapAsync <- allocateLinkedAsync . handleAny (atomically . void . tryPutTMVar setup . Just) $ do $logDebugS "LdapExecutor" $ "Starting " <> tshow (host, port) res <- withRunInIO $ \runInIO -> Ldap.with host port $ runInIO . go (Just setup) case res of Left exc -> do $logWarnS "LdapExecutor" $ tshow exc atomically . void . tryPutTMVar setup . Just $ toException exc Right res' -> return res' maybe (return ()) throwM =<< atomically (takeTMVar setup) return ldapAsync return LdapExecutor{..} delExecutor :: LdapExecutor -> IO () delExecutor LdapExecutor{..} = do atomically . void $ tryPutTMVar ldapDestroy () wait ldapAsync withRunInIO $ \runInIO -> createPool (runInIO mkExecutor) delExecutor stripes timeoutConn limit where withTimeout :: forall m' a. (MonadUnliftIO m', MonadThrow m') => m' a -> m' a withTimeout = maybe (throwM LdapPoolTimeout) return <=< timeout timeoutAct convertErrors = [ Handler $ \LineTooLong -> throwM LdapLineTooLong , Handler $ \(HostNotResolved h) -> throwM $ LdapHostNotResolved h , Handler $ \(HostCannotConnect h es) -> throwM $ LdapHostCannotConnect h es ]