capataz-0.0.0.0: src/Control/Concurrent/Internal/Capataz/Core.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-| This module contains:
* Functions exported on the public API
* The supervisor thread loop
* High level message handlers of the supervisor thread loop
-}
module Control.Concurrent.Internal.Capataz.Core where
import Protolude
import Control.Concurrent.Async (asyncWithUnmask)
import Control.Concurrent.MVar (newEmptyMVar, takeMVar)
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TQueue (newTQueueIO, readTQueue, writeTQueue)
import Control.Concurrent.STM.TVar (newTVarIO)
import Control.Teardown (newTeardown)
import Data.IORef (newIORef)
import Data.Time.Clock (getCurrentTime)
import qualified Data.HashMap.Strict as HashMap
import qualified Data.UUID.V4 as UUID (nextRandom)
import qualified Control.Concurrent.Internal.Capataz.Restart as Restart
import qualified Control.Concurrent.Internal.Capataz.Worker as Worker
import Control.Concurrent.Internal.Capataz.Types
import Control.Concurrent.Internal.Capataz.Util
( appendWorkerToMap
, capatazToEnv
, fetchWorker
, readCapatazStatus
, readCapatazStatusSTM
, resetWorkerMap
, sendSyncControlMsg
, workerOptionsToSpec
, writeCapatazStatus
)
--------------------------------------------------------------------------------
-- | Executes the shutdown operation of a Capataz, including the termination of
-- Workers being supervised by it.
haltCapataz :: CapatazEnv -> IO ()
haltCapataz env = do
writeCapatazStatus env Halting
Worker.terminateWorkers "capataz shutdown" env
resetWorkerMap env (const HashMap.empty)
writeCapatazStatus env Halted
-- | Handles an event produced by one of the workers this capataz monitors
handleMonitorEvent :: CapatazEnv -> MonitorEvent -> IO Bool
handleMonitorEvent env monitorEv = do
case monitorEv of
WorkerForcedRestart{} ->
-- We do nothing, as restart is being handled on restartWorkers
-- sub-routine
return ()
WorkerCompleted' { workerId, monitorEventTime } ->
Restart.handleWorkerCompleted env workerId monitorEventTime
WorkerFailed' { workerId, workerError, workerRestartCount } ->
Restart.handleWorkerFailed env workerId workerError workerRestartCount
WorkerTerminated' { workerId, workerRestartCount, workerTerminationReason }
-> Restart.handleWorkerTerminated env
workerId
workerTerminationReason
workerRestartCount
return True
-- | Handles an action triggered by the public API
handleControlAction :: CapatazEnv -> ControlAction -> IO Bool
handleControlAction env controlAction = case controlAction of
ForkWorker { workerSpec, returnWorkerId } -> do
worker@Worker { workerId } <- Worker.forkWorker env workerSpec Nothing
appendWorkerToMap env worker
returnWorkerId workerId
return True
TerminateWorker { terminationReason, workerId, notifyWorkerTermination } ->
do
mWorker <- fetchWorker env workerId
case mWorker of
Nothing -> return True
Just worker -> do
Worker.terminateWorker terminationReason env worker
-- removeWorkerFromMap env workerId
notifyWorkerTermination
return True
TerminateCapataz { notifyCapatazTermination } -> do
haltCapataz env
notifyCapatazTermination
return False
-- | Handles all messages that a capataz instance can receive
handleCapatazMessage :: CapatazEnv -> CapatazMessage -> IO Bool
handleCapatazMessage env message = case message of
ControlAction controlAction -> handleControlAction env controlAction
MonitorEvent monitorEvent -> handleMonitorEvent env monitorEvent
-- | Handles errors caused by the execution of the "runCapatazLoop" sub-routine
handleCapatazException :: CapatazEnv -> SomeException -> IO ()
handleCapatazException env@CapatazEnv { capatazId, capatazName, notifyEvent } capatazError
= do
eventTime <- getCurrentTime
notifyEvent CapatazFailed
{ capatazId
, capatazName
, capatazError
, eventTime
}
haltCapataz env
throwIO capatazError
-- | This is the main thread loop of a "Capataz" instance
runCapatazLoop :: (forall b . IO b -> IO b) -> CapatazEnv -> IO ()
runCapatazLoop unmask env@CapatazEnv { capatazId, capatazName, capatazStatusVar, capatazQueue, notifyEvent }
= do
loopResult <-
unmask
$ try
$ atomically
$ (,)
<$> readCapatazStatusSTM capatazStatusVar
<*> readTQueue capatazQueue
case loopResult of
Left capatazError -> handleCapatazException env capatazError
Right (status, message) -> case status of
Initializing -> do
eventTime <- getCurrentTime
notifyEvent InvalidCapatazStatusReached
{ capatazId
, capatazName
, eventTime
}
runCapatazLoop unmask env
Running -> do
eContinueLoop <- try $ unmask $ handleCapatazMessage env message
case eContinueLoop of
Left capatazError -> handleCapatazException env capatazError
Right continueLoop
| continueLoop -> runCapatazLoop unmask env
| otherwise -> do
eventTime <- getCurrentTime
notifyEvent CapatazTerminated
{ capatazId
, capatazName
, eventTime
}
Halting ->
-- Discard messages when halting
return ()
Halted -> panic "TODO: Pending halted state"
-- | Builds a record that contains runtime values of a "Capataz" (id, queue, status, etc.)
buildCapatazRuntime :: CapatazOptions -> IO CapatazRuntime
buildCapatazRuntime capatazOptions = do
capatazId <- UUID.nextRandom
capatazQueue <- newTQueueIO
capatazStatusVar <- newTVarIO Initializing
capatazWorkerMap <- newIORef HashMap.empty
return CapatazRuntime {..}
-- | Creates a Capataz record, which represents a supervision thread which
-- monitors failure on worker threads defined in the "CapatazOptions" or worker
-- threads that are created dynamically using "forkWorker".
forkCapataz :: CapatazOptions -> IO Capataz
forkCapataz capatazOptions@CapatazOptions { capatazName, capatazWorkerSpecList, notifyEvent }
= do
capatazRuntime <- buildCapatazRuntime capatazOptions
let capatazEnv@CapatazEnv { capatazId } = capatazToEnv capatazRuntime
capatazAsync <- asyncWithUnmask
$ \unmask -> runCapatazLoop unmask capatazEnv
forM_
capatazWorkerSpecList
( \workerSpec -> do
worker <- Worker.forkWorker capatazEnv workerSpec Nothing
appendWorkerToMap capatazEnv worker
)
writeCapatazStatus capatazEnv Running
capatazTeardown <- newTeardown
("capataz[" <> capatazName <> "]")
( do
status <- readCapatazStatus capatazEnv
case status of
Halted -> return ()
Halting -> return ()
_ -> do
eventTime <- getCurrentTime
notifyEvent CapatazShutdownInvoked
{ capatazId
, capatazName
, eventTime
}
sendSyncControlMsg capatazEnv TerminateCapataz
)
return Capataz {..}
-- | Creates a worker green thread "IO ()" sub-routine, and depending in options
-- defined in the "WorkerOptions" record, it will restart the Worker sub-routine
-- in case of failures
forkWorker
:: WorkerOptions -- ^ Worker options (restart, name, callbacks, etc)
-> IO () -- ^ IO sub-routine that will be executed on worker thread
-> Capataz -- ^ "Capataz" instance that supervises the worker
-> IO WorkerId -- ^ An identifier that can be used to terminate the "Worker"
forkWorker workerOptions workerAction Capataz { capatazEnv } = do
let workerSpec = workerOptionsToSpec workerOptions workerAction
CapatazEnv { capatazQueue } = capatazEnv
workerIdVar <- newEmptyMVar
atomically $ writeTQueue
capatazQueue
( ControlAction ForkWorker
{ workerSpec
, returnWorkerId = putMVar workerIdVar
}
)
takeMVar workerIdVar
-- | Stops the execution of a worker green thread being supervised by the given
-- "Capataz" instance, if the WorkerId does not belong to the Capataz, the
-- operation does not perform any side-effect.
--
-- Note: If your worker has a "Permanent" worker restart strategy, the worker
-- thread __will be restarted again__; so use a "Transient" restart strategy
-- instead.
terminateWorker :: Text -> WorkerId -> Capataz -> IO ()
terminateWorker terminationReason workerId Capataz { capatazEnv } =
sendSyncControlMsg
capatazEnv
( \notifyWorkerTermination -> TerminateWorker
{ terminationReason
, workerId
, notifyWorkerTermination
}
)