packages feed

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

{-# LANGUAGE EmptyDataDecls             #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
module Batchd.Daemon.Database where

import Control.Monad
import Control.Monad.Reader
import qualified Control.Monad.State as State

import Data.Time
import Database.Persist
import Data.Maybe
import Data.Int
import Data.Monoid ((<>))
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE

import           Database.Persist.Sql as Sql
import           Database.Persist.Sqlite as Sqlite
import           Database.Persist.Postgresql as Postgres
import qualified Database.Persist.SqlBackend as SqlBackend
import qualified Database.Esqueleto as E
import Database.Esqueleto ((^.))
import System.Log.Heavy

import Batchd.Core.Common.Types
import Batchd.Daemon.Types
import Batchd.Common.Types
import Batchd.Common.Data
import Batchd.Common.Schedule
import Batchd.Daemon.Schedule

getPool :: GlobalConfig -> LoggingTState -> IO Sql.ConnectionPool
getPool cfg lts =
  case dbcDriver cfg of
    Sqlite -> runLoggingT (Sqlite.createSqlitePool (dbcConnectionString cfg) (dbcConnectionPoolSize cfg)) lts
    PostgreSql -> do
        let str = TE.encodeUtf8 (dbcConnectionString cfg)
        runLoggingT (Postgres.createPostgresqlPool str (dbcConnectionPoolSize cfg)) lts

connectPool :: Daemon Sql.ConnectionPool
connectPool = do
  cfg <- askConfig
  lts <- askLoggingStateM
  pool <- liftIO $ getPool cfg lts
  Daemon $ lift $ State.modify $ \st -> st {ciPool = Just pool}
  return pool

buildJobInfo :: Int64 -> Job -> Maybe JobResult -> JobParamInfo -> JobInfo
buildJobInfo jid j mbr params =
   JobInfo {
      jiId = jid,
      jiQueue = jobQueueName j,
      jiType = jobTypeName j,
      jiCreateTime = jobCreateTime j,
      jiStartTime = jobStartTime j,
      jiSeq = jobSeq j,
      jiUserName = jobUserName j,
      jiStatus = jobStatus j,
      jiTryCount = jobTryCount j,
      jiHostName = jobHostName j,
      jiNotes = jobNotes j,
      jiResultTime = fmap jobResultTime mbr,
      jiExitCode = jobResultExitCode =<< mbr,
      jiStdout = fmap jobResultStdout mbr,
      jiStderr = fmap jobResultStderr mbr,
      jiParams = params
     }

loadJob :: Key Job -> DB JobInfo
loadJob jkey@(JobKey (SqlBackendKey jid)) = do
  mbJob <- get jkey
  case mbJob of
    Nothing -> throwR JobNotExists
    Just j -> do
      mbr <- selectFirst [JobResultJobId ==. jkey] [Desc JobResultTime]
      ps <- selectList [JobParamJobId ==. jkey] []
      let params = M.fromList [(jobParamName p, jobParamValue p) | p <- map entityVal ps]
      return $ buildJobInfo jid j (fmap entityVal mbr) params

loadJob' :: Int64 -> DB JobInfo
loadJob' jid = loadJob (JobKey (SqlBackendKey jid))

canLock :: DB Bool
canLock = do
  -- Dirty hack: we can't do "for update" in sqlite
  backend <- ask
  return $ SqlBackend.getRDBMS backend /= "sqlite"

lockJob :: JobInfo -> DB ()
lockJob ji = do
  ok <- canLock
  when ok $ do
    let qname = jiQueue ji
        seq = jiSeq ji
    E.select $
      E.from $ \job -> do
      E.where_ $ (job ^. JobQueueName `equals` E.val qname) `eand` (job ^. JobSeq `equals` E.val seq)
      E.locking E.ForUpdate
    return ()

lockQueue :: String -> DB ()
lockQueue qname = do
  ok <- canLock
  when ok $ do
    E.select $
      E.from $ \queue -> do
      E.where_ (queue ^. QueueName `equals` E.val qname)
      E.locking E.ForUpdate
    return ()

loadJobSeq :: String -> Int -> DB JobInfo
loadJobSeq qname seq = do
  mbJe <- getBy (UniqJobSeq qname seq)
  case mbJe of
    Nothing -> throwR JobNotExists
    Just je -> do
      let jkey = entityKey je
      let JobKey (SqlBackendKey jid) = jkey
      mbr <- selectFirst [JobResultJobId ==. jkey] [Desc JobResultTime]
      ps <- selectList [JobParamJobId ==. jkey] []
      let j = entityVal je
      let params = M.fromList [(jobParamName p, jobParamValue p) | p <- map entityVal ps]
      return $ buildJobInfo jid j (fmap entityVal mbr) params

updateJob :: Int64 -> UpdateList Job -> DB ()
updateJob jid (UpdateList updates) = do
  let jkey = JobKey (SqlBackendKey jid)
  update jkey updates

moveJob :: Int64 -> String -> DB ()
moveJob jid qname = do
  let jkey = JobKey (SqlBackendKey jid)
  oldJob <- loadJob' jid
  lockQueue (jiQueue oldJob)
  lockQueue qname
  seq <- getLastJobSeq qname
  update jkey [JobQueueName =. qname, JobSeq =. (seq+1)]

prioritizeJob :: Int64 -> MoveAction -> DB ()
prioritizeJob jid action = do
  let jkey = JobKey (SqlBackendKey jid)
  oldJob <- loadJob' jid
  let qname = jiQueue oldJob
  let oldSeq = jiSeq oldJob
  lockQueue qname
  case action of
    Last -> do
            lastSeq <- getLastJobSeq qname
            let seq' = lastSeq + 1
            update jkey [JobSeq =. seq']
    First -> do
            lastSeq <- getFirstJobSeq qname
            let seq' = lastSeq - 1
            update jkey [JobSeq =. seq']
    _ -> do
         let next = action == Less
         (mbJid, seq') <- getAdjJob next qname oldSeq
         update jkey [JobSeq =. seq']
         case mbJid of
           Nothing -> return ()
           Just jid' -> do
             let jkey' = JobKey (SqlBackendKey jid')
             update jkey' [JobSeq =. oldSeq]
  

setJobStatus :: JobInfo -> JobStatus -> DB ()
setJobStatus ji status = do
  updateWhere [JobQueueName ==. jiQueue ji, JobSeq ==. jiSeq ji] [JobStatus =. status]

increaseTryCount :: JobInfo -> DB Int
increaseTryCount ji = do
  let qname = jiQueue ji
      seq = jiSeq ji
  mbJe <- getBy (UniqJobSeq qname seq)
  case mbJe of
    Nothing -> throwR JobNotExists
    Just je -> do
      let count = jobTryCount (entityVal je)
          count' = count + 1
      updateWhere [JobQueueName ==. qname, JobSeq ==. seq] [JobTryCount =. count']
      return count'

moveToEnd :: JobInfo -> DB ()
moveToEnd ji = do
  let qname = jiQueue ji
      seq = jiSeq ji
  lockQueue qname
  mbJe <- getBy (UniqJobSeq qname seq)
  case mbJe of
    Nothing -> throwR JobNotExists
    Just je -> do
      lastSeq <- getLastJobSeq qname
      let seq' = lastSeq + 1
      updateWhere [JobQueueName ==. qname, JobSeq ==. seq] [JobSeq =. seq', JobStatus =. New]

getAllQueues :: DB [Entity Queue]
getAllQueues = selectList [] []

getEnabledQueues :: DB [Entity Queue]
getEnabledQueues = selectList [QueueEnabled ==. True] []

getDisabledQueues :: DB [Entity Queue]
getDisabledQueues = selectList [QueueEnabled ==. False] []

getDisabledQueues' :: DB [Queue]
getDisabledQueues' = map entityVal `fmap` getDisabledQueues

getAllQueues' :: DB [Queue]
getAllQueues' = map entityVal `fmap` selectList [] []

getAllowedQueues :: String -> Permission -> DB [Queue]
getAllowedQueues name perm = do
  lst <- E.select $ E.distinct $
         E.from $ \(queue `E.InnerJoin` uperm) -> do
           E.on $ (uperm ^. UserPermissionUserName `equals` E.val name)
                      `eand` (uperm ^. UserPermissionPermission `equals` E.val perm)
                      `eand` ((uperm ^. UserPermissionQueueName `equals` E.just (queue ^. QueueName))
                              `eor` (E.isNothing $ uperm ^. UserPermissionQueueName))
           return queue
  return $ map entityVal lst

enableQueue :: String -> DB ()
enableQueue qname = do
  update (QueueKey qname) [QueueEnabled =. True]

getAllJobs :: String -> DB [Entity Job]
getAllJobs qid = selectList [JobQueueName ==. qid] [Asc JobSeq]

getJobs :: String -> Maybe JobStatus -> DB [Entity Job]
getJobs qname mbStatus = do
  let filt = case mbStatus of
               Nothing -> []
               Just status -> [JobStatus ==. status]
  selectList ([JobQueueName ==. qname] ++ filt) [Asc JobSeq]

loadJobs :: String -> Maybe JobStatus -> DB [JobInfo]
loadJobs qname mbStatus = do
  mbQ <- getQueue qname
  case mbQ of
    Nothing -> throwR (QueueNotExists qname)
    Just _ -> do
        jes <- getJobs qname mbStatus
        forM jes $ \je -> do
          loadJob (entityKey je)

loadJobsByStatus :: Maybe JobStatus -> DB [JobInfo]
loadJobsByStatus mbStatus = do
  let filt = case mbStatus of
               Nothing -> []
               Just status -> [JobStatus ==. status]
  jes <- selectList filt [Asc JobId]
  forM jes $ \je -> do
      loadJob (entityKey je)

getNewJobsCount :: String -> DB Int
getNewJobsCount qname = do
  lst <- E.select $
         E.from $ \job -> do
           E.where_ $ job ^. JobStatus `equals` E.val New
           return E.countRows
  case lst of
    [] -> return 0
    [E.Value cnt] -> return cnt
    _ -> throwR $ UnknownError "impossible: query for new jobs count returned more than one row"

getJobResult :: Int64 -> DB JobResult
getJobResult jid = do
  let jkey = JobKey (SqlBackendKey jid)
  r <- selectFirst [JobResultJobId ==. jkey] [Desc JobResultTime]
  case r of
    Nothing -> throwR JobNotExists
    Just res -> return $ entityVal res

getJobResultE :: Int64 -> DB (Entity JobResult)
getJobResultE jid = do
  let jkey = JobKey (SqlBackendKey jid)
  r <- selectFirst [JobResultJobId ==. jkey] [Desc JobResultTime]
  case r of
    Nothing -> throwR JobNotExists
    Just res -> return res

getJobResults :: Int64 -> DB [JobResult]
getJobResults jid = do
  let jkey = JobKey (SqlBackendKey jid)
  res <- selectList [JobResultJobId ==. jkey] [Asc JobResultTime]
  return $ map entityVal res

equals :: (PersistField typ) =>
                E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value Bool)
equals = (E.==.)
infix 4 `equals`

leq :: (PersistField typ) =>
             E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value Bool)
leq = (E.<=.)
infix 4 `leq`

geq :: (PersistField typ) =>
             E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value Bool)
geq = (E.>=.)
infix 4 `geq`

eand :: E.SqlExpr (E.Value Bool) -> E.SqlExpr (E.Value Bool) -> E.SqlExpr (E.Value Bool)
eand = (E.&&.)
infixr 3 `eand`

eor :: E.SqlExpr (E.Value Bool) -> E.SqlExpr (E.Value Bool) -> E.SqlExpr (E.Value Bool)
eor = (E.||.)
infixr 3 `eor`

getLastJobSeq :: String -> DB Int
getLastJobSeq qid = do
  lst <- E.select $
         E.from $ \job -> do
         E.where_ (job ^. JobQueueName `equals` E.val qid)
         return $ E.max_ (job ^. JobSeq)
  case map E.unValue lst of
    (Just r:_) -> return r
    _ -> return 0

getFirstJobSeq :: String -> DB Int
getFirstJobSeq qid = do
  lst <- E.select $
         E.from $ \job -> do
         E.where_ (job ^. JobQueueName `equals` E.val qid)
         return $ E.min_ (job ^. JobSeq)
  case map E.unValue lst of
    (Just r:_) -> return r
    _ -> return 1

getAdjJob :: Bool -> String -> Int -> DB (Maybe Int64, Int)
getAdjJob next qname seq = do
  let cmp = if next then (E.>.) else (E.<.)
  let aggr = if next then E.min_ else E.max_
  lst <- E.select $
         E.from $ \job -> do
         E.where_ ((job ^. JobQueueName `equals` E.val qname) `eand` (job ^. JobSeq `cmp` E.val seq))
         return $ aggr (job ^. JobSeq)
  seq' <- case map E.unValue lst of
            (Just r:_) -> return r
            _ -> return $  if next then (seq+1) else (seq-1)
  mbJob <- getBy (UniqJobSeq qname seq')
  let mbJid = case entityKey `fmap` mbJob of
                Nothing -> Nothing
                Just (JobKey (SqlBackendKey jid)) -> Just jid
  return (mbJid, seq')

getNextJob :: Key Queue -> DB (Maybe JobInfo)
getNextJob (QueueKey qname) = do
  now <- liftIO $ getCurrentTime
  lst <- E.select $
         E.from $ \job -> do
         E.where_ $ (job ^. JobQueueName `equals` E.val qname)
             `eand` (job ^. JobStatus `equals` E.val New)
             `eand` ((E.isNothing (job ^. JobStartTime)) `eor` ((job ^. JobStartTime) `leq` (E.val (Just now))))
         return $ E.min_ (job ^. JobSeq)
  case map E.unValue lst of
    (Just seq:_) -> do
        jinfo <- loadJobSeq qname seq
        return $ Just jinfo
    _ -> return Nothing

deleteQueue :: String -> Bool -> DB ()
deleteQueue name forced = do
  mbQueue <- get (QueueKey name)
  case mbQueue of
    Nothing -> throwR (QueueNotExists name)
    Just qe -> do
      js <- selectFirst [JobQueueName ==. name] []
      if isNothing js || forced
        then delete (QueueKey name)
        else throwR QueueNotEmpty

addQueue :: Queue -> DB (Key Queue)
addQueue q = do
  r <- getQueue (queueName q)
  case r of
    Nothing -> do
        mbSchedule <- get (queueSchedule q)
        case mbSchedule of
          Nothing -> throwR (ScheduleNotExists (queueScheduleName q))
          otherwise -> insert q
    Just _ -> throwR (QueueExists (queueName q))

addQueue' :: String -> String -> DB (Key Queue)
addQueue' name scheduleId = do
  r <- insertUnique $ Queue name name True scheduleId Nothing Nothing
  case r of
    Just qid -> return qid
    Nothing -> throwR $ QueueExists name

getQueue :: String -> DB (Maybe Queue)
getQueue name = get (QueueKey name)

updateQueue :: String -> (UpdateList Queue) -> DB ()
updateQueue qname (UpdateList updates) = do
  update (QueueKey qname) updates

getQueueStats :: String -> DB (ByStatus Int)
getQueueStats qname = do
  let sql = "select status, count(1) from job where queue_name = ? group by status"
  pvs <- Sql.rawSql sql [toPersistValue qname]
  return $ ByStatus $ M.fromList [(unSingle st, unSingle cnt) | (st,cnt) <- pvs]

getStats :: DB (M.Map String (ByStatus Int))
getStats = do
  queues <- getAllQueues'
  rs <- forM queues $ \q -> do
            st <- getQueueStats (queueName q)
            return (queueName q, st)
  return $ M.fromList rs

enqueue :: String -> String -> JobInfo -> DB (Key Job)
enqueue username qname jinfo = do
  mbQueue <- getQueue qname
  lockQueue qname
  case mbQueue of
    Nothing -> throwR (QueueNotExists qname)
    Just qe -> do
      seq <- getLastJobSeq qname
      now <- liftIO getCurrentTime
      checkStartTime (jiStartTime jinfo) (queueSchedule qe)
      let job = Job (jiType jinfo) qname (seq+1) username now (jiStartTime jinfo) (jiStatus jinfo) (jiTryCount jinfo) (jiHostName jinfo) (jiNotes jinfo)
      jid <- insert job
      forM_ (M.assocs $ jiParams jinfo) $ \(name,value) -> do
        let param = JobParam jid name value
        insert_ param
      return jid

checkStartTime :: Maybe UTCTime -> Key Schedule -> DB ()
checkStartTime Nothing _ = return ()
checkStartTime (Just startTime) scheduleId = do
  schedule <- loadSchedule scheduleId
  when (not (schedule `allowsU` startTime)) $
    throwR InvalidStartTime

removeJob :: String -> Int -> DB ()
removeJob qname jseq = do
    deleteCascadeWhere [JobQueueName ==. qname, JobSeq ==. jseq]

removeJobById :: Int64 -> DB ()
removeJobById jid = do
    let key = JobKey (SqlBackendKey jid)
    deleteCascade key

removeJobs :: String -> JobStatus -> DB ()
removeJobs qname status = do
    deleteCascadeWhere [JobQueueName ==. qname, JobStatus ==. status]

cleanupJobResults :: Int -> DB ()
cleanupJobResults days = do
  now <- liftIO $ getCurrentTime
  let delta = fromIntegral $ days * 24 * 3600
  let edge = addUTCTime (negate delta) now
  -- Delete jobs with obsolete results
  -- let deleteParams = "delete from job_param where job_id in (select job_id from job_result where time < ?) and (select status from job where id = job_param.job_id) in ('Done', 'Failed')"
  -- Sql.rawExecute deleteParams [toPersistValue edge]
  -- liftIO $ putStrLn deleteParams
  -- let deleteJobs = "delete from job where id in (select job_id from job_result where time < ?) and status in ('Done', 'Failed')"
  -- liftIO $ putStrLn deleteJobs
  -- Sql.rawExecute deleteJobs [toPersistValue edge]
  -- Delete obsolete job results
  deleteCascadeWhere [JobResultTime <. edge]
  deleteCascadeWhere [JobCreateTime <. edge, JobStatus <-. [Done, Failed]]

-- cleanupJobs :: Int -> DB ()
-- cleanupJobs days = do
--   now <- liftIO $ getCurrentTime
--   let delta = fromIntegral $ days * 3600
--   let edge = addUTCTime (negate delta) now
  -- let deleteResults = "delete from job_result where job_id in (select job_id from job_result where time < ?)"
  -- Sql.rawExecute deleteResults [toPersistValue edge]
  -- let deleteJobs = "delete from job where time 

getMetrics :: Maybe T.Text -> DB [MetricRecord]
getMetrics mbPrefix = do
  lst <- E.select $
         E.from $ \record -> do
           case mbPrefix of
             Nothing -> return ()
             Just prefix -> E.where_ (record ^. MetricRecordName `E.like` E.val (prefix <> "%"))
           E.orderBy [E.desc (record ^. MetricRecordTime), E.asc (record ^. MetricRecordName)]
           return record
  return $ map entityVal lst

getLastMetric :: T.Text -> DB MetricRecord
getLastMetric name = do
  lst <- E.select $
         E.from $ \record -> do
           E.where_ (record ^. MetricRecordName `equals` E.val name)
           E.orderBy [E.desc (record ^. MetricRecordTime), E.asc (record ^. MetricRecordName)]
           return record
  case lst of
    [] -> throwR $ MetricNotExists (T.unpack name)
    (r:_) -> return $ entityVal r

queryMetric :: T.Text -> UTCTime -> UTCTime -> DB [MetricRecord]
queryMetric nameLike from to = do
  lst <- E.select $
         E.from $ \record -> do
           E.where_ ((record ^. MetricRecordName `E.like` E.val nameLike)
                     `eand` (record ^. MetricRecordTime `geq` E.val from)
                     `eand` (record ^. MetricRecordTime `leq` E.val to))
           E.orderBy [E.asc (record ^. MetricRecordTime)]
           return record
  return $ map entityVal lst