packages feed

replicant-0.1.0.0: src/Replicant/Bot/Supervisor.hs

{-# LANGUAGE FlexibleContexts #-}
module Replicant.Bot.Supervisor
  ( Supervisor
  , WorkerStatus(..)
  , newSupervisor
  , monitor
  , halt
  , status
  ) where

import           Control.Concurrent          (ThreadId, killThread)
import           Control.Concurrent.Lifted   (forkFinally)
import           Control.Concurrent.STM      (STM, atomically)
import           Control.Concurrent.STM.TVar
import           Control.Exception           (SomeException)
import           Control.Monad               (void)
import           Control.Monad.IO.Class      (MonadIO, liftIO)
import           Control.Monad.Trans.Control (MonadBaseControl)
import qualified Data.Map                    as M

data Worker m = Worker
  { workerJob    :: m ()
  , workerStatus :: TVar WorkerStatus
  }

data Supervisor m a = Supervisor
  { supervisorWorkers :: TVar (M.Map a (Worker m))
  }

data WorkerStatus = WorkerBooting | WorkerRunning ThreadId | WorkerCrashed SomeException | WorkerDone deriving Show

newSupervisor :: IO (Supervisor m a)
newSupervisor = Supervisor <$> newTVarIO M.empty

monitor :: (MonadBaseControl IO m, MonadIO m, Ord a)
        => Supervisor m a -> (WorkerStatus -> m c) -> a -> m b -> m ()
monitor s@Supervisor{..} cb key job = do
  halt s key
  w <- mkWorker job
  liftIO . atomically . modifyTVar supervisorWorkers $ M.insert key w
  runWorker w $ \st -> void $ do
    liftIO . atomically $ writeTVar (workerStatus w) st
    cb st

halt :: (MonadIO m, Ord a) => Supervisor m a -> a -> m ()
halt Supervisor{..} key = do
  keys <- liftIO . atomically $ pop key supervisorWorkers
  mapM_ shutdownWorker keys

status :: MonadIO m => Supervisor m a -> m (M.Map a WorkerStatus)
status Supervisor{..} = liftIO . atomically $
  readTVar supervisorWorkers >>= mapM (readTVar . workerStatus)

mkWorker :: MonadIO m => m b -> m (Worker m)
mkWorker job = liftIO $ Worker
  <$> pure (void job)
  <*> newTVarIO WorkerBooting

runWorker :: (MonadBaseControl IO m, MonadIO m) => Worker m -> (WorkerStatus -> m ()) -> m ()
runWorker Worker{..} watcher = do
  watcher WorkerBooting
  thread <- forkFinally workerJob (handleExit watcher)
  watcher $ WorkerRunning thread

-- TODO: can we determine if this was a deliberate shutdown? Should we try to reboot?
handleExit :: MonadIO m => (WorkerStatus -> m c) -> Either SomeException () -> m c
handleExit watcher (Left err) = watcher $ WorkerCrashed err
handleExit watcher _ = watcher WorkerDone

shutdownWorker :: MonadIO m => Worker m -> m ()
shutdownWorker Worker{..} = liftIO $ readTVarIO workerStatus >>= \case
  WorkerRunning threadId -> killThread threadId
  _ -> return ()

pop :: Ord a => a -> TVar (M.Map a b) -> STM (Maybe b)
pop k tmap = do
  val <- M.lookup k <$> readTVar tmap
  modifyTVar' tmap $ M.delete k
  return val