packages feed

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

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TemplateHaskell #-}

module Batchd.Daemon.Dispatcher (runDispatcher) where

import Control.Concurrent
import Control.Monad
import Control.Monad.Reader
import qualified Data.Text as T
import qualified Data.Map as M
import Data.Time
import Data.Dates
import Database.Persist
import qualified Database.Persist.Sql as Sql hiding (Single)
import System.Exit
import Data.Text.Format.Heavy
import System.Log.Heavy

import Batchd.Daemon.Types
import Batchd.Common.Types
import Batchd.Common.Data
import Batchd.Common.Config as Config
import Batchd.Daemon.Database
import Batchd.Common.Schedule
import Batchd.Daemon.Schedule
import Batchd.Daemon.Executor
import Batchd.Daemon.Hosts (hostCleaner, hostsMetricDumper)
import Batchd.Core.Daemon.Logging
import Batchd.Core.Daemon.Hosts

-- | Set up workers, callback listener and dispatcher itself.
runDispatcher :: Daemon ()
runDispatcher = do
  cfg <- askConfig
  jobsChan <- liftIO newChan
  resChan <- liftIO newChan
  counters <- liftIO $ newMVar M.empty
  $info "Starting {} workers..." (Single $ dcWorkers $ dbcDispatcher cfg)
  -- Each worker will run in separate thread.
  -- All workers read jobs to be executed from single Chan.
  -- So job put into Chan will be executed by first worker who sees it.
  forM_ [1.. dcWorkers (dbcDispatcher cfg)] $ \idx -> do
    $debug "  Starting worker #{}" (Single idx)
    forkDaemon "worker" $ worker idx counters jobsChan resChan

  -- Listen for job results
  forkDaemon "job results listener" $ callbackListener resChan

  -- Stop hosts when they are not needed anymore
  withLogVariable "thread" ("host cleaner" :: String) $ do
      lts <- askLoggingStateM
      liftIO $ forkIO $ hostCleaner lts counters

  forkDaemon "hosts metrics dumper" $ hostsMetricDumper counters

  dispatcher jobsChan

-- | Dispatcher main loop itself.
dispatcher :: Chan (Queue, JobInfo) -> Daemon ()
dispatcher jobsChan = withLogVariable "thread" ("dispatcher" :: String) $ do
  forever $ do
    qesr <- runDB getEnabledQueues
    cfg <- askConfig
    case qesr of
      Left err -> $reportError "Can't get list of enabled queues: {}" (Single $ Shown err)
      Right qes -> do
        forM_ qes $ \qe -> runDB $ do
          schedule <- loadSchedule (queueSchedule $ entityVal qe)
          now <- liftIO $ getCurrentDateTime
          when (schedule `allows` now) $ do
              -- will work only with queues, shedules of which allow current time
              let QueueKey qname = entityKey qe
              -- pick next job from the queue
              mbJob <- getNextJob (entityKey qe)
              case mbJob of
                Nothing -> $debug "Queue `{}' exhaused." (Single qname)
                Just job -> do
                    -- Waiting means that Dispatcher saw job and put it to Chan to be
                    -- picked by some of workers.
                    setJobStatus job Waiting
                    liftIO $ writeChan jobsChan (entityVal qe, job)
        -- Sleep for poll timeout
        liftIO $ threadDelay $ (dcPollTimeout $ dbcDispatcher cfg) * 1000*1000

-- | This listens for job results Chan and writes results to DB.
-- It also reschedules failed jobs if needed.
callbackListener :: ResultsChan -> Daemon ()
callbackListener resChan = forever $ do
    (job, command) <- liftIO $ readChan resChan
    let jid = jiId job
    let jkey = JobKey (Sql.SqlBackendKey jid)
    now <- liftIO getCurrentTime
      
    withJobContext job $ do
      case command of
        StartExecution -> do
          -- let result = JobResult jkey now Nothing T.empty T.empty
          -- catchDbError $ runDB $ insert_ result
          return ()

        StdoutLine line -> do
          let result = JobResult jkey now Nothing line T.empty
          catchDbError $ runDB $ do
            insert_ result

        StderrLine line -> do
          let result = JobResult jkey now Nothing T.empty line
          catchDbError $ runDB $ do
            insert_ result

        ExecError msg onFail -> do
          let result = JobResult jkey now Nothing T.empty msg
          catchDbError $ runDB $ do
            insert_ result
            processFail job onFail

        Exited ec onFail -> do
          let result = JobResult jkey now (Just ec) T.empty T.empty
          catchDbError $ runDB $ do
            insert_ result
            if ec == ExitSuccess
              then setJobStatus job Done
              else processFail job onFail
  where
    catchDbError db = do
      res <- db
      case res of
        Right _ -> return ()
        Left err -> do
          $reportError "Can't update job result: {}" (Single $ show err)
          return ()

    processFail job Continue = setJobStatus job Failed -- just mark job as Failed
    processFail job (RetryNow m) = do
        count <- increaseTryCount job
        if count <= m
          then do
            $info "Retry now" ()
            setJobStatus job New -- job will be picked up by dispatcher at nearest iteration.
          else setJobStatus job Failed
    processFail job (RetryLater m) = do
        count <- increaseTryCount job
        if count <= m
          then do
            $info "Retry later" ()
            moveToEnd job -- put the job to the end of queue.
          else setJobStatus job Failed

withJobContext :: HasLogContext m => JobInfo -> m a -> m a
withJobContext job =
    withLogContext (LogContextFrame vars noChange)
  where
    vars = [("job", Variable (jiId job)),
            ("user", Variable (jiUserName job))]

-- | Worker loop executes jobs themeselves
worker :: Int -> HostsPool -> Chan (Queue, JobInfo) -> ResultsChan -> Daemon ()
worker idx hosts jobsChan resChan = forever $ withLogVariable "worker" idx $ do
  (queue, job) <- liftIO $ readChan jobsChan
  withJobContext job $ do
    $info "got job #{}" (Single $ jiId job)
    -- now job is picked up by worker, mark it as Processing
    runDB $ setJobStatus job Processing
    jtypeR <- liftIO $  Config.loadTemplate (T.pack $ jiType job)
    case jtypeR of
            Left err -> do -- we could not load job type description
                $reportError "Invalid job type {}: {}" (jiType job, show err)
                liftIO $ writeChan resChan (job, ExecError (T.pack $ show err) Continue)

            Right jtype -> do
                executeJob hosts queue jtype job resChan

    $info "done job #{}." (Single $ jiId job)