packages feed

batchd-0.1.1.0: src/Batchd/Daemon/Types.hs

{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, TemplateHaskell, GeneralizedNewtypeDeriving, DeriveGeneric, StandaloneDeriving, OverloadedStrings, TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses #-}
-- | This module contains type declarations and utilities that are used only by batchd daemon.
module Batchd.Daemon.Types where

import Control.Exception (catch)
import Control.Monad.Reader
import Control.Monad.State
import qualified Control.Monad.State.Strict as SSt
import Control.Monad.Except
import qualified Control.Monad.Catch as MC
import Control.Monad.Trans.Resource
import qualified Control.Monad.Metrics as Metrics
import Control.Concurrent
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Format.Heavy as F
import qualified Database.Persist.Sql as Sql
import Network.Wai
import Network.Wai.Metrics
import qualified Web.Scotty.Trans as Scotty
import qualified Web.Scotty.Internal.Types as SI
import System.Exit (ExitCode (..))
import System.Log.Heavy
import System.Log.Heavy.Instances.UnliftIO () -- import instances only
import Text.Localize
import Text.Localize.IO

import Batchd.Core.Common.Types
import Batchd.Common.Types

instance Scotty.ScottyError Error where
  stringError e = UnknownError e
  showError e = TL.pack (show e)

-- deriving instance MonadUnliftIO (LoggingT (ResourceT IO))

-- | Standard monad for database actions
type DB a = ReaderT Sql.SqlBackend (LoggingT (ExceptT Error (ResourceT IO))) a

-- | Temporary monad type for usage of DB from IO directly
type DBIO a = ReaderT Sql.SqlBackend (LoggingT (ResourceT IO)) a

-- | Throw an error in DB monad
throwR :: Error -> DB a
throwR ex = lift $ lift $ throwError ex

-- | Convert DB monad into DBIO
dbio :: DB a -> DBIO (Either Error a)
dbio action = do
  backend <- ask
  lts <- lift $ ask
  x <- liftIO $ runResourceT $ runExceptT $ runLoggingT (runReaderT action backend) lts
  case x of
    Left err -> do
        return $ Left err
    Right r -> return $ Right r

-- | Database connection information and configuration
data ConnectionInfo = ConnectionInfo {
    ciGlobalConfig :: GlobalConfig     -- ^ Global configuration
  , ciPool :: Maybe Sql.ConnectionPool -- ^ DB connection pool
  , ciTranslations :: Maybe Translations
  , ciMetrics :: Maybe Metrics.Metrics
  , ciWaiMetrics :: Maybe WaiMetrics
  }

data JobResultUpdate =
    StartExecution
  | StdoutLine T.Text
  | StderrLine T.Text
  | Exited ExitCode OnFailAction
  | ExecError T.Text OnFailAction
  deriving (Eq, Show)

type ResultsChan = Chan (JobInfo, JobResultUpdate)

deriving instance MonadThrow m => MC.MonadThrow (LoggingT m)
deriving instance MC.MonadCatch m => MC.MonadCatch (LoggingT m)
deriving instance MC.MonadMask m => MC.MonadMask (LoggingT m)

deriving instance MonadFail m => MonadFail (LoggingT m)

-- | Main monad for daemon actions (both Manager and Dispatcher). This handles logging
-- and DB connection.
newtype Daemon a = Daemon {
    runDaemonT :: LoggingT (StateT ConnectionInfo IO) a
  }
  deriving (Applicative,Functor,Monad,MonadIO, MonadFail, MonadReader LoggingTState,
            MC.MonadThrow, MC.MonadCatch, MC.MonadMask,
            HasLogContext, HasLogger)

instance Localized Daemon where
  getLanguage = liftIO $ languageFromLocale

  getTranslations = do
    ci <- askConnectionInfo
    case ciTranslations ci of
      Just tr -> return tr
      Nothing -> fail $ "Translations are not loaded yet"

  getContext = return Nothing

instance Localized (ReaderT Sql.SqlBackend (LoggingT (ExceptT Error (ResourceT IO)))) where
  getLanguage = liftIO $ languageFromLocale

  getTranslations = liftIO getTranslations

  getContext = return Nothing

-- | Run daemon actions within IO monad
runDaemonIO :: ConnectionInfo -> LoggingTState -> Daemon a -> IO a
runDaemonIO connInfo lts actions =
  evalStateT (runLoggingT (runDaemonT actions) lts) connInfo

-- | REST handler monad type
type Action a = Scotty.ActionT Error Daemon a

-- TODO: this should respect Accept-Language HTTP header
instance Localized (Scotty.ActionT Error Daemon) where
  getLanguage = lift getLanguage
  getTranslations = lift getTranslations
  getContext = lift getContext

instance HasLogContext (Scotty.ActionT Error Daemon) where
  getLogContext = lift getLogContext

  withLogContext frame actions =
      SI.ActionT $ ExceptT $ ReaderT $ \env -> SSt.StateT $ \rs ->
        let actions' = SI.runAM actions
            r = (runReaderT (runExcept actions') env)
        in withLogContext frame $ SSt.runStateT r rs
    where
      runExcept (ExceptT m) = m

instance HasLogger (Scotty.ActionT Error Daemon) where
  getLogger = lift getLogger

  localLogger logger actions = 
      SI.ActionT $ ExceptT $ ReaderT $ \env -> SSt.StateT $ \rs ->
        localLogger logger $ SSt.runStateT (runReaderT (runExcept $ SI.runAM actions) env) rs
    where
      runExcept (ExceptT m) = m

instance HasLogContext (ReaderT Sql.SqlBackend (LoggingT (ExceptT Error (ResourceT IO)))) where
  getLogContext = lift getLogContext

  withLogContext frame actions =
      ReaderT $ \backend -> withLogContext frame $ runReaderT actions backend

instance HasLogger (ReaderT Sql.SqlBackend (LoggingT (ExceptT Error (ResourceT IO)))) where 
  getLogger = lift getLogger

  localLogger logger actions = 
      ReaderT $ \backend -> localLogger logger $ runReaderT actions backend

instance F.VarContainer Request where
  lookupVar "method" rq = Just $ F.Variable $ show $ requestMethod rq
  lookupVar "path" rq = Just $ F.Variable $ rawPathInfo rq
  lookupVar "secure" rq = Just $ F.Variable $ isSecure rq
  lookupVar "referer" rq = Just $ F.Variable $ requestHeaderReferer rq
  lookupVar "useragent" rq = Just $ F.Variable $ requestHeaderUserAgent rq
  lookupVar _ rq = Nothing

instance F.ClosedVarContainer Request where
  allVarNames _ = ["method", "path", "secure", "referer", "useragent"]

-- | Set up translations
setupTranslations :: LocatePolicy -> Daemon ()
setupTranslations p = do
  liftIO $ Text.Localize.IO.setupTranslations p
  translations <- liftIO $ locateTranslations p
  Daemon $ lift $ modify $ \st -> st {ciTranslations = Just translations}

-- | Obtain DB connection
askConnectionInfo :: Daemon ConnectionInfo
askConnectionInfo = Daemon $ lift get

-- | Obtain field of DB connection within Action monad
asksConnectionInfo :: (ConnectionInfo -> a) -> Action a
asksConnectionInfo fn = do
  ci <- lift askConnectionInfo
  return $ fn ci

-- | Obtain logger function.
-- Note: if logger is called directly, it does not perform
-- context-level filter check.
askLoggerM :: Daemon SpecializedLogger
askLoggerM = asks ltsLogger

-- | Obtain logging state within Daemon monad
askLoggingStateM :: Daemon LoggingTState
askLoggingStateM = ask

-- | Obtain logger function.
-- Note: if logger is called directly, it does not perform
-- context-level filter check.
askLogger :: Action SpecializedLogger
askLogger = lift $ askLoggerM

-- | Obtain logging state within Action monad
askLoggingState :: Action LoggingTState
askLoggingState = lift $ askLoggingStateM

-- | Obtain DB connections pool
askPool :: Daemon Sql.ConnectionPool
askPool = do
  mbPool <- Daemon $ lift $ gets ciPool
  case mbPool of
    Nothing -> fail =<< TL.unpack `fmap` (__ "Database connection is not established yet")
    Just pool -> return pool

-- | Obtain global daemon configuration
askConfig :: Daemon GlobalConfig
askConfig = Daemon $ lift $ gets ciGlobalConfig

-- | Obtain DB connections pool within Action monad
askPoolA :: Action Sql.ConnectionPool
askPoolA = do
  mbPool <- asksConnectionInfo ciPool
  case mbPool of
    Nothing -> fail =<< TL.unpack `fmap` (__ "Database connection is not established yet")
    Just pool -> return pool

-- | Obtain global daemon configuration within Action monad
askConfigA :: Action GlobalConfig
askConfigA = asksConnectionInfo ciGlobalConfig

-- | Run DB action within Action monad. Raise HTTP-level error if DB action fails.
runDBA :: DB a -> Action a
runDBA qry = do
  pool <- askPoolA
  cfg <- askConfigA
  lts <- askLoggingState
  r <- liftIO $ do
         (runResourceT $ runLoggingT (Sql.runSqlPool (dbio qry) pool) lts)
          `catch` (\e -> return $ Left $ SqlError e)
  case r of
    Left err -> Scotty.raise err
    Right x -> return x

-- | Run DB action within Action monad.
-- This version does not raise HTTP-level error on DB action fail.
runDBA' :: DB a -> Action (Either Error a)
runDBA' qry = do
  pool <- askPoolA
  cfg <- asksConnectionInfo ciGlobalConfig
  lts <- askLoggingState
  r <- liftIO $ do
         (runResourceT $ runLoggingT (Sql.runSqlPool (dbio qry) pool) lts)
          `catch` (\e -> return $ Left $ SqlError e)
  return r

-- | Run DB action within Daemon monad.
runDB :: DB a -> Daemon (Either Error a)
runDB qry = do
  pool <- askPool
  cfg <- askConfig
  lts <- askLoggingStateM
  liftIO $ do
    (runResourceT $ runLoggingT (Sql.runSqlPool (dbio qry) pool) lts)
      `catch` (\e -> return $ Left $ SqlError e)

-- | Run DB action within IO monad.
runDBIO :: GlobalConfig -> Sql.ConnectionPool -> LoggingTState -> DB a -> IO (Either Error a)
runDBIO cfg pool lts qry = do
  (runResourceT $ runLoggingT (Sql.runSqlPool (dbio qry) pool) lts)
    `catch` (\e -> return $ Left $ SqlError e)

-- | Run Daemon action within IO monad.
-- This is generally used to spawn main daemon threads.
runDaemon :: GlobalConfig -> Maybe Sql.ConnectionPool -> LoggingSettings -> Daemon a -> IO a
runDaemon cfg mbPool backend daemon =
    runner $ withLoggingT backend $ 
      withLogContext (LogContextFrame [] noChange) $ runDaemonT daemon
  where
    runner r = evalStateT r initState
    initState = ConnectionInfo cfg mbPool Nothing Nothing Nothing
    -- undefinedLogger = error "Internal error: logger is not defined yet"

-- | Wrap Daemon action with some @with@-style function in IO monad.
-- Example: @wrapDaemon (withResource x) $ \resource -> do ...@.
wrapDaemon :: ((c -> IO a) -> IO a) -> (c -> Daemon a) -> Daemon a
wrapDaemon wrapper daemon = do
    lts <- askLoggingStateM
    connInfo <- askConnectionInfo
    result <- liftIO $ wrapper $ \c -> runDaemonIO connInfo lts (daemon c)
    return result

wrapDaemon_ :: ((c -> IO a) -> IO ()) -> (c -> Daemon a) -> Daemon ()
wrapDaemon_ wrapper daemon = do
    lts <- askLoggingStateM
    connInfo <- askConnectionInfo
    liftIO $ wrapper $ \c -> runDaemonIO connInfo lts (daemon c)

-- | forkIO for Daemon monad.
forkDaemon :: String -> Daemon a -> Daemon ()
forkDaemon name daemon = do
    lts <- askLoggingStateM
    connInfo <- askConnectionInfo
    withLogVariable "thread" name $
        liftIO $ forkIO $ do
                   runDaemonIO connInfo lts daemon
                   return ()
    return ()

-- | Special constant for default host of the queue
defaultHostOfQueue :: T.Text
defaultHostOfQueue = "__default__"

formatScript :: [T.Text] -> T.Text
formatScript lines =
  let lines' = if "#!" `T.isPrefixOf` head lines
                 then lines
                 else "#!/bin/sh" : lines
  in  T.unlines lines'