packages feed

dbmonitor-0.1.0: src/Monitor/Queue.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ImplicitParams #-}
module Monitor.Queue where

import GHC.Conc

import Control.Concurrent
import qualified Control.Concurrent.Lifted as Lifted
import Control.Concurrent.STM.TVar

import System.Directory
import System.FilePath

import qualified Data.HashMap.Strict as HM
import Data.Maybe
import qualified Data.Text.IO as T
import Data.Time

import Monitor.DataModel
import Monitor.Loader
import Monitor.DB
import Monitor.Telegram

-- This is a hack. On connection error all thread must try to restart.
touchConfig :: (?mutex :: Mutexes) => Monitor ()
touchConfig = do
  dir <- asks databaseDirectory
  logMessage ("Monitor at " <> dir <> "is restarted in order to reestablish db connection.")
  time <- liftIO getCurrentTime
  liftIO $ setModificationTime (dir </> configName) time

processQueryResult :: (?mutex :: Mutexes) => FilePath -> PureJob -> JobFeedback -> Monitor ()
processQueryResult _path _ (ConnectionError err) =
  alertConnectionError err >> touchConfig
processQueryResult path PureJob{..} (QueryError err) =
  alertQueryError path err pureJobSQL
processQueryResult path job (AssertionResult value) =
  if value
    then pure ()
    else alertFailedAssertion path job

purify :: Job -> Assertion -> FilePath -> PureJob
purify Job{..} assertion path = PureJob {
    pureJobDescription = fromMaybe ("Job at " <> path) jobDescription
  , pureJobAssertion = fromMaybe assertion jobAssertion
  , pureJobSQL = jobSQL
  }

periodicEvent :: (?mutex :: Mutexes) => Job -> FilePath -> Monitor ()
periodicEvent job@Job{..} path = forever $ do
  defFreq <- asks defaultFrequency
  defAssert <- asks defaultAssertion
  let pureJob = purify job defAssert path
      delay = 60 * 10^((6)::Int) * (fromMaybe defFreq jobFrequency)
  queryResult <- runSQL pureJob
  processQueryResult path pureJob queryResult
  logMessage ("Job at " <> path <> " is executed.")
  liftIO $ threadDelay delay

forkWaitable :: (?mutex :: Mutexes) => Monitor () -> Monitor (ThreadId, MVar ())
forkWaitable action = do
  handle <- liftIO newEmptyMVar
  thread <- Lifted.forkFinally action (\_ -> liftIO $ putMVar handle ())
  return (thread, handle)

startJob :: (?mutex :: Mutexes) => FilePath -> Monitor ()
startJob path = do
  queue <- asks jobQueue
  job <- liftIO $ parseJob <$> T.readFile path
  queueMap <- liftIO $ readTVarIO queue
  case HM.lookup path queueMap of
    Nothing -> pure ()
    Just accidental_thread -> do
      logMessage ("Job " <> path <> " was probably initiated by different monitor threads. Please report a bug.")
      liftIO $ killThread accidental_thread
  (thread, waitHandle) <- forkWaitable (periodicEvent job path)
  liftIO $ labelThread thread ("jobThread: " <> path)
  liftIO . atomically $ modifyTVar queue (HM.insert path thread)
  logMessage ("Job " <> path <> " is started.")
  void $ liftIO $ takeMVar waitHandle

removeJob :: (?mutex :: Mutexes) => FilePath -> Monitor ()
removeJob path = do
  queueTVar <- asks jobQueue
  queue <- liftIO $ readTVarIO queueTVar
  liftIO . killThread $ queue HM.! path
  liftIO . atomically $ modifyTVar queueTVar (HM.delete path)
  logMessage ("Job " <> path <> " is removed")

restartJob :: (?mutex :: Mutexes) => FilePath -> Monitor ()
restartJob path = do
  queueTVar <- asks jobQueue
  queue <- liftIO $ readTVarIO queueTVar
  liftIO . killThread $ queue HM.! path
  job <- liftIO $ parseJob <$> T.readFile path
  (thread, waitHandle) <- forkWaitable (periodicEvent job path)
  liftIO $ labelThread thread ("jobThread: " <> path)
  liftIO . atomically $ modifyTVar queueTVar (HM.adjust (\_ -> thread) path)
  logMessage ("Job " <> path <> " is restarted due to file modification.")
  void $ liftIO $ takeMVar waitHandle

destroyQueue :: Monitor ()
destroyQueue = do
  queueTVar <- asks jobQueue
  queue <- liftIO $ readTVarIO queueTVar
  mapM_ (liftIO . killThread) $ HM.elems queue
  liftIO . atomically $ modifyTVar queueTVar (\_ -> HM.empty)

destroyMonitor :: (?mutex :: Mutexes) => MVar () -> Monitor ()
destroyMonitor monitorHolder = do
  liftIO $ putMVar monitorHolder ()
  destroyQueue
  alertThreadDeath