capataz 0.0.0.2 → 0.1.0.0
raw patch · 21 files changed
+3987/−2787 lines, 21 filesdep +microlensdep ~protoludedep ~safe-exceptionsdep ~tastyPVP ok
version bump matches the API change (PVP)
Dependencies added: microlens
Dependency ranges changed: protolude, safe-exceptions, tasty, tasty-rerun, text
API changes (from Hackage documentation)
- Control.Concurrent.Capataz: Capataz :: !CapatazRuntime -> !CapatazEnv -> !(Async ()) -> !Teardown -> Capataz
- Control.Concurrent.Capataz: CapatazFailed :: !CapatazName -> !CapatazId -> !SomeException -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: CapatazOptions :: Text -> !Int -> !NominalDiffTime -> !CapatazRestartStrategy -> ![WorkerSpec] -> !WorkerTerminationOrder -> !(IO ()) -> !(CapatazEvent -> IO ()) -> CapatazOptions
- Control.Concurrent.Capataz: CapatazShutdownInvoked :: !CapatazName -> !CapatazId -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: CapatazStatusChanged :: !CapatazId -> !CapatazName -> !CapatazStatus -> !CapatazStatus -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: CapatazTerminated :: !CapatazName -> !CapatazId -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: InvalidCapatazStatusReached :: !CapatazId -> !CapatazName -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: WorkerCallbackExecuted :: !CapatazName -> !CapatazId -> !WorkerThreadId -> !WorkerId -> !WorkerName -> !(Maybe SomeException) -> !CallbackType -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: WorkerCallbackFailed :: !WorkerId -> !(Maybe SomeException) -> !CallbackType -> !SomeException -> WorkerError
- Control.Concurrent.Capataz: WorkerCompleted :: !CapatazName -> !CapatazId -> !WorkerThreadId -> !WorkerId -> !WorkerName -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: WorkerFailed :: !CapatazName -> !CapatazId -> !WorkerThreadId -> !WorkerId -> !WorkerName -> !SomeException -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: WorkerOptions :: !WorkerName -> !(SomeException -> IO ()) -> !(IO ()) -> !(IO ()) -> !WorkerTerminationPolicy -> !WorkerRestartStrategy -> WorkerOptions
- Control.Concurrent.Capataz: WorkerRestarted :: !CapatazName -> !CapatazId -> !WorkerThreadId -> !WorkerId -> !WorkerName -> !Int -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: WorkerStarted :: !CapatazName -> !CapatazId -> !WorkerThreadId -> !WorkerId -> !WorkerName -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: WorkerTerminated :: !CapatazName -> !CapatazId -> !WorkerThreadId -> !WorkerId -> !WorkerName -> !Text -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: WorkersTerminationFinished :: !CapatazName -> !CapatazId -> !Text -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: WorkersTerminationStarted :: !CapatazName -> !CapatazId -> !Text -> !UTCTime -> CapatazEvent
- Control.Concurrent.Capataz: [$sel:callbackType:InvalidCapatazStatusReached] :: CapatazEvent -> !CallbackType
- Control.Concurrent.Capataz: [$sel:callbackType:WorkerCallbackFailed] :: WorkerError -> !CallbackType
- Control.Concurrent.Capataz: [$sel:capatazAsync:Capataz] :: Capataz -> !(Async ())
- Control.Concurrent.Capataz: [$sel:capatazEnv:Capataz] :: Capataz -> !CapatazEnv
- Control.Concurrent.Capataz: [$sel:capatazError:InvalidCapatazStatusReached] :: CapatazEvent -> !SomeException
- Control.Concurrent.Capataz: [$sel:capatazId:InvalidCapatazStatusReached] :: CapatazEvent -> !CapatazId
- Control.Concurrent.Capataz: [$sel:capatazIntensity:CapatazOptions] :: CapatazOptions -> !Int
- Control.Concurrent.Capataz: [$sel:capatazName:CapatazOptions] :: CapatazOptions -> Text
- Control.Concurrent.Capataz: [$sel:capatazName:InvalidCapatazStatusReached] :: CapatazEvent -> !CapatazName
- Control.Concurrent.Capataz: [$sel:capatazPeriodSeconds:CapatazOptions] :: CapatazOptions -> !NominalDiffTime
- Control.Concurrent.Capataz: [$sel:capatazRestartStrategy:CapatazOptions] :: CapatazOptions -> !CapatazRestartStrategy
- Control.Concurrent.Capataz: [$sel:capatazRuntime:Capataz] :: Capataz -> !CapatazRuntime
- Control.Concurrent.Capataz: [$sel:capatazTeardown:Capataz] :: Capataz -> !Teardown
- Control.Concurrent.Capataz: [$sel:capatazWorkerSpecList:CapatazOptions] :: CapatazOptions -> ![WorkerSpec]
- Control.Concurrent.Capataz: [$sel:capatazWorkerTerminationOrder:CapatazOptions] :: CapatazOptions -> !WorkerTerminationOrder
- Control.Concurrent.Capataz: [$sel:eventTime:InvalidCapatazStatusReached] :: CapatazEvent -> !UTCTime
- Control.Concurrent.Capataz: [$sel:newCapatazStatus:InvalidCapatazStatusReached] :: CapatazEvent -> !CapatazStatus
- Control.Concurrent.Capataz: [$sel:notifyEvent:CapatazOptions] :: CapatazOptions -> !(CapatazEvent -> IO ())
- Control.Concurrent.Capataz: [$sel:onCapatazIntensityReached:CapatazOptions] :: CapatazOptions -> !(IO ())
- Control.Concurrent.Capataz: [$sel:prevCapatazStatus:InvalidCapatazStatusReached] :: CapatazEvent -> !CapatazStatus
- Control.Concurrent.Capataz: [$sel:terminationReason:InvalidCapatazStatusReached] :: CapatazEvent -> !Text
- Control.Concurrent.Capataz: [$sel:workerAction:WorkerSpec] :: WorkerSpec -> WorkerAction
- Control.Concurrent.Capataz: [$sel:workerActionError:WorkerCallbackFailed] :: WorkerError -> !(Maybe SomeException)
- Control.Concurrent.Capataz: [$sel:workerCallbackError:InvalidCapatazStatusReached] :: CapatazEvent -> !(Maybe SomeException)
- Control.Concurrent.Capataz: [$sel:workerCallbackError:WorkerCallbackFailed] :: WorkerError -> !SomeException
- Control.Concurrent.Capataz: [$sel:workerError:InvalidCapatazStatusReached] :: CapatazEvent -> !SomeException
- Control.Concurrent.Capataz: [$sel:workerId:InvalidCapatazStatusReached] :: CapatazEvent -> !WorkerId
- Control.Concurrent.Capataz: [$sel:workerId:WorkerCallbackFailed] :: WorkerError -> !WorkerId
- Control.Concurrent.Capataz: [$sel:workerName:InvalidCapatazStatusReached] :: CapatazEvent -> !WorkerName
- Control.Concurrent.Capataz: [$sel:workerName:WorkerOptions] :: WorkerOptions -> !WorkerName
- Control.Concurrent.Capataz: [$sel:workerName:WorkerSpec] :: WorkerSpec -> !WorkerName
- Control.Concurrent.Capataz: [$sel:workerOnCompletion:WorkerOptions] :: WorkerOptions -> !(IO ())
- Control.Concurrent.Capataz: [$sel:workerOnCompletion:WorkerSpec] :: WorkerSpec -> !(IO ())
- Control.Concurrent.Capataz: [$sel:workerOnFailure:WorkerOptions] :: WorkerOptions -> !(SomeException -> IO ())
- Control.Concurrent.Capataz: [$sel:workerOnFailure:WorkerSpec] :: WorkerSpec -> !(SomeException -> IO ())
- Control.Concurrent.Capataz: [$sel:workerOnTermination:WorkerOptions] :: WorkerOptions -> !(IO ())
- Control.Concurrent.Capataz: [$sel:workerOnTermination:WorkerSpec] :: WorkerSpec -> !(IO ())
- Control.Concurrent.Capataz: [$sel:workerRestartCount:InvalidCapatazStatusReached] :: CapatazEvent -> !Int
- Control.Concurrent.Capataz: [$sel:workerRestartStrategy:WorkerOptions] :: WorkerOptions -> !WorkerRestartStrategy
- Control.Concurrent.Capataz: [$sel:workerRestartStrategy:WorkerSpec] :: WorkerSpec -> !WorkerRestartStrategy
- Control.Concurrent.Capataz: [$sel:workerTerminationPolicy:WorkerOptions] :: WorkerOptions -> !WorkerTerminationPolicy
- Control.Concurrent.Capataz: [$sel:workerTerminationPolicy:WorkerSpec] :: WorkerSpec -> !WorkerTerminationPolicy
- Control.Concurrent.Capataz: [$sel:workerThreadId:InvalidCapatazStatusReached] :: CapatazEvent -> !WorkerThreadId
- Control.Concurrent.Capataz: capatazToAsync :: Capataz -> Async ()
- Control.Concurrent.Capataz: data CapatazEvent
- Control.Concurrent.Capataz: data CapatazRestartStrategy
- Control.Concurrent.Capataz: data CapatazStatus
- Control.Concurrent.Capataz: data WorkerError
- Control.Concurrent.Capataz: data WorkerSpec
- Control.Concurrent.Capataz: data WorkerTerminationOrder
- Control.Concurrent.Capataz: defCapatazOptions :: CapatazOptions
- Control.Concurrent.Capataz: defWorkerOptions :: WorkerOptions
- Control.Concurrent.Capataz: defWorkerSpec :: WorkerSpec
- Control.Concurrent.Capataz: terminateWorker :: Text -> WorkerId -> Capataz -> IO ()
- Control.Concurrent.Capataz: type WorkerAction = IO ()
+ Control.Concurrent.Capataz: (&) :: () => a -> (a -> b) -> b
+ Control.Concurrent.Capataz: (.~) :: () => ASetter s t a b -> b -> s -> t
+ Control.Concurrent.Capataz: ProcessCallbackFailed :: !WorkerId -> !(Maybe SomeException) -> !SomeException -> !CallbackType -> ProcessError
+ Control.Concurrent.Capataz: SupervisorSpec :: SupervisorOptions -> ProcessSpec
+ Control.Concurrent.Capataz: SupervisorType :: ProcessType
+ Control.Concurrent.Capataz: WorkerType :: ProcessType
+ Control.Concurrent.Capataz: [$sel:processCallbackError:ProcessCallbackFailed] :: ProcessError -> !SomeException
+ Control.Concurrent.Capataz: [$sel:processCallbackType:ProcessCallbackFailed] :: ProcessError -> !CallbackType
+ Control.Concurrent.Capataz: [$sel:processError:ProcessCallbackFailed] :: ProcessError -> !(Maybe SomeException)
+ Control.Concurrent.Capataz: [$sel:processId:ProcessCallbackFailed] :: ProcessError -> !WorkerId
+ Control.Concurrent.Capataz: buildSupervisorOptions :: SupervisorName -> (SupervisorOptions -> SupervisorOptions) -> SupervisorOptions
+ Control.Concurrent.Capataz: buildSupervisorOptionsWithDefaults :: SupervisorName -> SupervisorOptions
+ Control.Concurrent.Capataz: buildWorkerOptions :: WorkerName -> (IO ()) -> (WorkerOptions -> WorkerOptions) -> WorkerOptions
+ Control.Concurrent.Capataz: buildWorkerOptionsWithDefaults :: WorkerName -> (IO ()) -> WorkerOptions
+ Control.Concurrent.Capataz: class HasSupervisor a
+ Control.Concurrent.Capataz: data ProcessError
+ Control.Concurrent.Capataz: data ProcessSpec
+ Control.Concurrent.Capataz: data ProcessTerminationOrder
+ Control.Concurrent.Capataz: data ProcessType
+ Control.Concurrent.Capataz: data Supervisor
+ Control.Concurrent.Capataz: data SupervisorOptions
+ Control.Concurrent.Capataz: data SupervisorRestartStrategy
+ Control.Concurrent.Capataz: data SupervisorStatus
+ Control.Concurrent.Capataz: forkSupervisor :: HasSupervisor parentSupervisor => SupervisorOptions -> parentSupervisor -> IO Supervisor
+ Control.Concurrent.Capataz: getCapatazTeardown :: Capataz -> Teardown
+ Control.Concurrent.Capataz: getSupervisor :: HasSupervisor a => a -> Supervisor
+ Control.Concurrent.Capataz: getSupervisorAsync :: Supervisor -> Async ()
+ Control.Concurrent.Capataz: getSupervisorProcessId :: Supervisor -> ProcessId
+ Control.Concurrent.Capataz: infixl 1 &
+ Control.Concurrent.Capataz: infixr 4 .~
+ Control.Concurrent.Capataz: joinCapatazThread :: Capataz -> IO ()
+ Control.Concurrent.Capataz: onSystemEventL :: Functor f => ((CapatazEvent -> IO ()) -> f (CapatazEvent -> IO ())) -> CapatazOptions -> f CapatazOptions
+ Control.Concurrent.Capataz: set :: () => ASetter s t a b -> b -> s -> t
+ Control.Concurrent.Capataz: supervisorIntensityL :: (HasSupervisorIntensity s, Functor f) => (Int -> f Int) -> s -> f s
+ Control.Concurrent.Capataz: supervisorOnFailureL :: (HasSupervisorFailureCallback s, Functor f) => ((SomeException -> IO ()) -> f (SomeException -> IO ())) -> s -> f s
+ Control.Concurrent.Capataz: supervisorOnIntensityReachedL :: (HasSupervisorIntensityReachedCallback s, Functor f) => (IO () -> f (IO ())) -> s -> f s
+ Control.Concurrent.Capataz: supervisorPeriodSecondsL :: (HasSupervisorPeriodSeconds s, Functor f) => (NominalDiffTime -> f NominalDiffTime) -> s -> f s
+ Control.Concurrent.Capataz: supervisorProcessSpecListL :: (HasSupervisorProcessSpecList s, Functor f) => ([ProcessSpec] -> f [ProcessSpec]) -> s -> f s
+ Control.Concurrent.Capataz: supervisorProcessTerminationOrderL :: (HasSupervisorProcessTerminationOrder s, Functor f) => (ProcessTerminationOrder -> f ProcessTerminationOrder) -> s -> f s
+ Control.Concurrent.Capataz: supervisorRestartStrategyL :: (HasSupervisorRestartStrategy s, Functor f) => (SupervisorRestartStrategy -> f SupervisorRestartStrategy) -> s -> f s
+ Control.Concurrent.Capataz: supervisorSpec :: SupervisorName -> (SupervisorOptions -> SupervisorOptions) -> ProcessSpec
+ Control.Concurrent.Capataz: supervisorSpecWithDefaults :: SupervisorName -> ProcessSpec
+ Control.Concurrent.Capataz: terminateProcess :: HasSupervisor supervisor => Text -> ProcessId -> supervisor -> IO Bool
+ Control.Concurrent.Capataz: type ProcessId = UUID
+ Control.Concurrent.Capataz: type SupervisorId = UUID
+ Control.Concurrent.Capataz: type WorkerId = UUID
+ Control.Concurrent.Capataz: workerOnCompletionL :: Functor f => (IO () -> f (IO ())) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz: workerOnFailureL :: Functor f => ((SomeException -> IO ()) -> f (SomeException -> IO ())) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz: workerOnTerminationL :: Functor f => (IO () -> f (IO ())) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz: workerRestartStrategyL :: Functor f => (WorkerRestartStrategy -> f WorkerRestartStrategy) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz: workerSpec :: WorkerName -> (IO ()) -> (WorkerOptions -> WorkerOptions) -> ProcessSpec
+ Control.Concurrent.Capataz: workerSpecWithDefaults :: WorkerName -> (IO ()) -> ProcessSpec
+ Control.Concurrent.Capataz: workerTerminationPolicyL :: Functor f => (WorkerTerminationPolicy -> f WorkerTerminationPolicy) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz.Event: CapatazFailed :: !SupervisorId -> !SupervisorName -> !SomeException -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: CapatazTerminated :: !SupervisorName -> !SupervisorId -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: InvalidSupervisorStatusReached :: !SupervisorId -> !SupervisorName -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: ProcessCallbackExecuted :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !(Maybe SomeException) -> !CallbackType -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: ProcessCompleted :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: ProcessFailed :: !SupervisorName -> !SupervisorId -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !SomeException -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: ProcessRestarted :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !Int -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: ProcessStarted :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: ProcessTerminated :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !Text -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: ProcessTerminationFinished :: !SupervisorName -> !SupervisorId -> !Text -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: ProcessTerminationStarted :: !SupervisorName -> !SupervisorId -> !Text -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: SupervisorStatusChanged :: !SupervisorId -> !SupervisorName -> !SupervisorStatus -> !SupervisorStatus -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Event: [$sel:eventTime:InvalidSupervisorStatusReached] :: CapatazEvent -> !UTCTime
+ Control.Concurrent.Capataz.Event: [$sel:newSupervisorStatus:InvalidSupervisorStatusReached] :: CapatazEvent -> !SupervisorStatus
+ Control.Concurrent.Capataz.Event: [$sel:prevSupervisorStatus:InvalidSupervisorStatusReached] :: CapatazEvent -> !SupervisorStatus
+ Control.Concurrent.Capataz.Event: [$sel:processCallbackError:InvalidSupervisorStatusReached] :: CapatazEvent -> !(Maybe SomeException)
+ Control.Concurrent.Capataz.Event: [$sel:processCallbackType:InvalidSupervisorStatusReached] :: CapatazEvent -> !CallbackType
+ Control.Concurrent.Capataz.Event: [$sel:processError:InvalidSupervisorStatusReached] :: CapatazEvent -> !SomeException
+ Control.Concurrent.Capataz.Event: [$sel:processId:InvalidSupervisorStatusReached] :: CapatazEvent -> !ProcessId
+ Control.Concurrent.Capataz.Event: [$sel:processName:InvalidSupervisorStatusReached] :: CapatazEvent -> !ProcessName
+ Control.Concurrent.Capataz.Event: [$sel:processRestartCount:InvalidSupervisorStatusReached] :: CapatazEvent -> !Int
+ Control.Concurrent.Capataz.Event: [$sel:processThreadId:InvalidSupervisorStatusReached] :: CapatazEvent -> !ProcessThreadId
+ Control.Concurrent.Capataz.Event: [$sel:processType:InvalidSupervisorStatusReached] :: CapatazEvent -> !ProcessType
+ Control.Concurrent.Capataz.Event: [$sel:supervisorError:InvalidSupervisorStatusReached] :: CapatazEvent -> !SomeException
+ Control.Concurrent.Capataz.Event: [$sel:supervisorId:InvalidSupervisorStatusReached] :: CapatazEvent -> !SupervisorId
+ Control.Concurrent.Capataz.Event: [$sel:supervisorName:InvalidSupervisorStatusReached] :: CapatazEvent -> !SupervisorName
+ Control.Concurrent.Capataz.Event: [$sel:terminationReason:InvalidSupervisorStatusReached] :: CapatazEvent -> !Text
+ Control.Concurrent.Capataz.Event: data CapatazEvent
+ Control.Concurrent.Capataz.Internal.Core: class HasSupervisor a
+ Control.Concurrent.Capataz.Internal.Core: forkCapataz :: (CapatazOptions -> CapatazOptions) -> IO Capataz
+ Control.Concurrent.Capataz.Internal.Core: forkSupervisor :: HasSupervisor parentSupervisor => SupervisorOptions -> parentSupervisor -> IO Supervisor
+ Control.Concurrent.Capataz.Internal.Core: forkWorker :: HasSupervisor supervisor => WorkerOptions -> supervisor -> IO WorkerId
+ Control.Concurrent.Capataz.Internal.Core: getCapatazTeardown :: Capataz -> Teardown
+ Control.Concurrent.Capataz.Internal.Core: getSupervisor :: HasSupervisor a => a -> Supervisor
+ Control.Concurrent.Capataz.Internal.Core: getSupervisorAsync :: Supervisor -> Async ()
+ Control.Concurrent.Capataz.Internal.Core: getSupervisorProcessId :: Supervisor -> ProcessId
+ Control.Concurrent.Capataz.Internal.Core: instance Control.Concurrent.Capataz.Internal.Core.HasSupervisor Control.Concurrent.Capataz.Internal.Types.Capataz
+ Control.Concurrent.Capataz.Internal.Core: instance Control.Concurrent.Capataz.Internal.Core.HasSupervisor Control.Concurrent.Capataz.Internal.Types.Supervisor
+ Control.Concurrent.Capataz.Internal.Core: joinCapatazThread :: Capataz -> IO ()
+ Control.Concurrent.Capataz.Internal.Core: terminateProcess :: HasSupervisor supervisor => Text -> ProcessId -> supervisor -> IO Bool
+ Control.Concurrent.Capataz.Internal.Supervisor: buildSupervisorEnv :: (CapatazEvent -> IO ()) -> (SupervisorMessage -> IO ()) -> STM SupervisorMessage -> SupervisorId -> SupervisorOptions -> IO SupervisorEnv
+ Control.Concurrent.Capataz.Internal.Supervisor: calcDiffSeconds :: UTCTime -> IO NominalDiffTime
+ Control.Concurrent.Capataz.Internal.Supervisor: calcRestartAction :: SupervisorEnv -> Int -> NominalDiffTime -> ProcessRestartAction
+ Control.Concurrent.Capataz.Internal.Supervisor: execCapatazRestartStrategy :: SupervisorEnv -> ProcessId -> ProcessSpec -> Int -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: execRestartAction :: SupervisorEnv -> ProcessId -> ProcessSpec -> Text -> UTCTime -> Int -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: forceRestartProcess :: SupervisorEnv -> Process -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: forkSupervisor :: ParentSupervisorEnv -> SupervisorOptions -> Maybe (ProcessId, RestartCount) -> IO Supervisor
+ Control.Concurrent.Capataz.Internal.Supervisor: haltSupervisor :: Text -> SupervisorEnv -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: handleControlAction :: SupervisorEnv -> ControlAction -> IO Bool
+ Control.Concurrent.Capataz.Internal.Supervisor: handleMonitorEvent :: SupervisorEnv -> MonitorEvent -> IO Bool
+ Control.Concurrent.Capataz.Internal.Supervisor: handleProcessCompleted :: SupervisorEnv -> ProcessId -> UTCTime -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: handleProcessFailed :: SupervisorEnv -> WorkerId -> SomeException -> Int -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: handleProcessTerminated :: SupervisorEnv -> ProcessId -> Text -> Int -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: handleSupervisorFailed :: SupervisorEnv -> Supervisor -> Int -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: handleSupervisorMessage :: SupervisorEnv -> SupervisorMessage -> IO Bool
+ Control.Concurrent.Capataz.Internal.Supervisor: handleSupervisorTerminated :: SupervisorEnv -> Supervisor -> Int -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: handleWorkerCompleted :: SupervisorEnv -> Worker -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: handleWorkerFailed :: SupervisorEnv -> Worker -> Int -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: handleWorkerTerminated :: SupervisorEnv -> Worker -> Int -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: restartProcessList :: SupervisorEnv -> WorkerId -> RestartCount -> IO [Process]
+ Control.Concurrent.Capataz.Internal.Supervisor: restartSupervisor :: ParentSupervisorEnv -> SupervisorOptions -> ProcessId -> RestartCount -> IO Process
+ Control.Concurrent.Capataz.Internal.Supervisor: restartWorker :: SupervisorEnv -> WorkerOptions -> WorkerId -> RestartCount -> IO Process
+ Control.Concurrent.Capataz.Internal.Supervisor: supervisorLoop :: (forall b. IO b -> IO b) -> ParentSupervisorEnv -> SupervisorEnv -> RestartCount -> IO ()
+ Control.Concurrent.Capataz.Internal.Supervisor: supervisorMain :: ParentSupervisorEnv -> SupervisorOptions -> SupervisorId -> RestartCount -> IO Supervisor
+ Control.Concurrent.Capataz.Internal.Types: AllForOne :: SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: BrutalTermination :: WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: BrutallyTerminateProcessException :: !ProcessId -> !Text -> CapatazSignal
+ Control.Concurrent.Capataz.Internal.Types: Capataz :: !Supervisor -> !Teardown -> Capataz
+ Control.Concurrent.Capataz.Internal.Types: CapatazFailed :: !SupervisorId -> !SupervisorName -> !SomeException -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: CapatazFailure :: CapatazSignal
+ Control.Concurrent.Capataz.Internal.Types: CapatazOptions :: !Int -> !NominalDiffTime -> !SupervisorRestartStrategy -> ![ProcessSpec] -> !ProcessTerminationOrder -> !(IO ()) -> !(SomeException -> IO ()) -> !(CapatazEvent -> IO ()) -> CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types: CapatazTerminated :: !SupervisorName -> !SupervisorId -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: ControlAction :: !ControlAction -> SupervisorMessage
+ Control.Concurrent.Capataz.Internal.Types: ForkSupervisor :: !SupervisorOptions -> !(Supervisor -> IO ()) -> ControlAction
+ Control.Concurrent.Capataz.Internal.Types: ForkWorker :: !WorkerOptions -> !(WorkerId -> IO ()) -> ControlAction
+ Control.Concurrent.Capataz.Internal.Types: HaltSupervisor :: ProcessRestartAction
+ Control.Concurrent.Capataz.Internal.Types: Halted :: SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: Halting :: SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: IncreaseRestartCount :: ProcessRestartAction
+ Control.Concurrent.Capataz.Internal.Types: Infinity :: WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: Initializing :: SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: InvalidSupervisorStatusReached :: !SupervisorId -> !SupervisorName -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: MonitorEvent :: !MonitorEvent -> SupervisorMessage
+ Control.Concurrent.Capataz.Internal.Types: NewestFirst :: ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: OldestFirst :: ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: OnCompletion :: CallbackType
+ Control.Concurrent.Capataz.Internal.Types: OnFailure :: CallbackType
+ Control.Concurrent.Capataz.Internal.Types: OnTermination :: CallbackType
+ Control.Concurrent.Capataz.Internal.Types: OneForOne :: SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: ParentSupervisorEnv :: !SupervisorId -> !SupervisorName -> !(SupervisorMessage -> IO ()) -> !(CapatazEvent -> IO ()) -> ParentSupervisorEnv
+ Control.Concurrent.Capataz.Internal.Types: Permanent :: WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: ProcessCallbackExecuted :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !(Maybe SomeException) -> !CallbackType -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessCallbackFailed :: !WorkerId -> !(Maybe SomeException) -> !SomeException -> !CallbackType -> ProcessError
+ Control.Concurrent.Capataz.Internal.Types: ProcessCompleted :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessCompleted' :: !ProcessId -> !ProcessName -> !UTCTime -> MonitorEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessEnv :: !ProcessId -> !ProcessName -> !(Async ()) -> !UTCTime -> !WorkerRestartStrategy -> ProcessEnv
+ Control.Concurrent.Capataz.Internal.Types: ProcessFailed :: !SupervisorName -> !SupervisorId -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !SomeException -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessFailed' :: !WorkerId -> !WorkerName -> !RestartCount -> !SomeException -> !UTCTime -> MonitorEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessForcedRestart :: !ProcessId -> !ProcessName -> !UTCTime -> MonitorEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessRestarted :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !Int -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessStarted :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessTerminated :: !SupervisorId -> !SupervisorName -> !ProcessThreadId -> !ProcessId -> !ProcessName -> !ProcessType -> !Text -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessTerminated' :: !ProcessId -> !ProcessName -> !RestartCount -> !Text -> !UTCTime -> MonitorEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessTerminationFinished :: !SupervisorName -> !SupervisorId -> !Text -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: ProcessTerminationStarted :: !SupervisorName -> !SupervisorId -> !Text -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: ResetRestartCount :: ProcessRestartAction
+ Control.Concurrent.Capataz.Internal.Types: RestartProcessException :: CapatazSignal
+ Control.Concurrent.Capataz.Internal.Types: Running :: SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: Supervisor :: !SupervisorId -> !SupervisorName -> !SupervisorOptions -> !UTCTime -> !(Async ()) -> (SupervisorMessage -> IO ()) -> !SupervisorEnv -> Supervisor
+ Control.Concurrent.Capataz.Internal.Types: SupervisorEnv :: !SupervisorId -> !SupervisorName -> !(SupervisorMessage -> IO ()) -> !(STM SupervisorMessage) -> !(IORef ProcessMap) -> !(TVar SupervisorStatus) -> !SupervisorOptions -> !Int -> !NominalDiffTime -> !SupervisorRestartStrategy -> !ProcessTerminationOrder -> !(IO ()) -> !(SomeException -> IO ()) -> !(CapatazEvent -> IO ()) -> SupervisorEnv
+ Control.Concurrent.Capataz.Internal.Types: SupervisorIntensityReached :: !ProcessId -> !ProcessName -> !Int -> CapatazError
+ Control.Concurrent.Capataz.Internal.Types: SupervisorOptions :: Text -> !Int -> !NominalDiffTime -> !SupervisorRestartStrategy -> ![ProcessSpec] -> !ProcessTerminationOrder -> !(IO ()) -> !(SomeException -> IO ()) -> SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types: SupervisorProcess :: Supervisor -> Process
+ Control.Concurrent.Capataz.Internal.Types: SupervisorSpec :: SupervisorOptions -> ProcessSpec
+ Control.Concurrent.Capataz.Internal.Types: SupervisorStatusChanged :: !SupervisorId -> !SupervisorName -> !SupervisorStatus -> !SupervisorStatus -> !UTCTime -> CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: SupervisorType :: ProcessType
+ Control.Concurrent.Capataz.Internal.Types: Temporary :: WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: TerminateProcess :: !ProcessId -> !Text -> !(Bool -> IO ()) -> ControlAction
+ Control.Concurrent.Capataz.Internal.Types: TerminateProcessException :: !ProcessId -> !Text -> CapatazSignal
+ Control.Concurrent.Capataz.Internal.Types: TimeoutMillis :: !Int -> WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: Transient :: WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: Worker :: !WorkerId -> !(Async ()) -> !UTCTime -> !WorkerName -> !WorkerOptions -> Worker
+ Control.Concurrent.Capataz.Internal.Types: WorkerOptions :: WorkerAction -> !WorkerName -> !(SomeException -> IO ()) -> !(IO ()) -> !(IO ()) -> !WorkerTerminationPolicy -> !WorkerRestartStrategy -> WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types: WorkerProcess :: Worker -> Process
+ Control.Concurrent.Capataz.Internal.Types: WorkerSpec :: WorkerOptions -> ProcessSpec
+ Control.Concurrent.Capataz.Internal.Types: WorkerType :: ProcessType
+ Control.Concurrent.Capataz.Internal.Types: [$sel:capatazSupervisor:Capataz] :: Capataz -> !Supervisor
+ Control.Concurrent.Capataz.Internal.Types: [$sel:capatazTeardown:Capataz] :: Capataz -> !Teardown
+ Control.Concurrent.Capataz.Internal.Types: [$sel:eventTime:InvalidSupervisorStatusReached] :: CapatazEvent -> !UTCTime
+ Control.Concurrent.Capataz.Internal.Types: [$sel:monitorEventTime:ProcessTerminated'] :: MonitorEvent -> !UTCTime
+ Control.Concurrent.Capataz.Internal.Types: [$sel:newSupervisorStatus:InvalidSupervisorStatusReached] :: CapatazEvent -> !SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: [$sel:notifyEvent:CapatazOptions] :: CapatazOptions -> !(CapatazEvent -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:notifyEvent:ParentSupervisorEnv] :: ParentSupervisorEnv -> !(CapatazEvent -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:notifyEvent:SupervisorEnv] :: SupervisorEnv -> !(CapatazEvent -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:notifyProcessTermination:ForkWorker] :: ControlAction -> !(Bool -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:prevSupervisorStatus:InvalidSupervisorStatusReached] :: CapatazEvent -> !SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processAsync:ProcessEnv] :: ProcessEnv -> !(Async ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processCallbackError:InvalidSupervisorStatusReached] :: CapatazEvent -> !(Maybe SomeException)
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processCallbackError:ProcessCallbackFailed] :: ProcessError -> !SomeException
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processCallbackType:InvalidSupervisorStatusReached] :: CapatazEvent -> !CallbackType
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processCallbackType:ProcessCallbackFailed] :: ProcessError -> !CallbackType
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processCreationTime:ProcessEnv] :: ProcessEnv -> !UTCTime
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processError:InvalidSupervisorStatusReached] :: CapatazEvent -> !SomeException
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processError:ProcessCallbackFailed] :: ProcessError -> !(Maybe SomeException)
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processError:ProcessTerminated'] :: MonitorEvent -> !SomeException
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processId:CapatazFailure] :: CapatazSignal -> !ProcessId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processId:ForkWorker] :: ControlAction -> !ProcessId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processId:InvalidSupervisorStatusReached] :: CapatazEvent -> !ProcessId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processId:ProcessCallbackFailed] :: ProcessError -> !WorkerId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processId:ProcessEnv] :: ProcessEnv -> !ProcessId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processId:ProcessTerminated'] :: MonitorEvent -> !ProcessId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processId:SupervisorIntensityReached] :: CapatazError -> !ProcessId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processName:InvalidSupervisorStatusReached] :: CapatazEvent -> !ProcessName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processName:ProcessEnv] :: ProcessEnv -> !ProcessName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processName:ProcessTerminated'] :: MonitorEvent -> !ProcessName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processName:SupervisorIntensityReached] :: CapatazError -> !ProcessName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processRestartCount:InvalidSupervisorStatusReached] :: CapatazEvent -> !Int
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processRestartCount:ProcessTerminated'] :: MonitorEvent -> !RestartCount
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processRestartCount:SupervisorIntensityReached] :: CapatazError -> !Int
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processRestartStrategy:ProcessEnv] :: ProcessEnv -> !WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processTerminationReason:CapatazFailure] :: CapatazSignal -> !Text
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processTerminationReason:ForkWorker] :: ControlAction -> !Text
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processTerminationReason:ProcessTerminated'] :: MonitorEvent -> !Text
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processThreadId:InvalidSupervisorStatusReached] :: CapatazEvent -> !ProcessThreadId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:processType:InvalidSupervisorStatusReached] :: CapatazEvent -> !ProcessType
+ Control.Concurrent.Capataz.Internal.Types: [$sel:returnSupervisor:ForkWorker] :: ControlAction -> !(Supervisor -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:returnWorkerId:ForkWorker] :: ControlAction -> !(WorkerId -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorAsync:Supervisor] :: Supervisor -> !(Async ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorCreationTime:Supervisor] :: Supervisor -> !UTCTime
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorEnv:Supervisor] :: Supervisor -> !SupervisorEnv
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorError:InvalidSupervisorStatusReached] :: CapatazEvent -> !SomeException
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorGetNotification:SupervisorEnv] :: SupervisorEnv -> !(STM SupervisorMessage)
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorId:InvalidSupervisorStatusReached] :: CapatazEvent -> !SupervisorId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorId:ParentSupervisorEnv] :: ParentSupervisorEnv -> !SupervisorId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorId:SupervisorEnv] :: SupervisorEnv -> !SupervisorId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorId:Supervisor] :: Supervisor -> !SupervisorId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorIntensity:CapatazOptions] :: CapatazOptions -> !Int
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorIntensity:SupervisorEnv] :: SupervisorEnv -> !Int
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorIntensity:SupervisorOptions] :: SupervisorOptions -> !Int
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorName:InvalidSupervisorStatusReached] :: CapatazEvent -> !SupervisorName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorName:ParentSupervisorEnv] :: ParentSupervisorEnv -> !SupervisorName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorName:SupervisorEnv] :: SupervisorEnv -> !SupervisorName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorName:SupervisorOptions] :: SupervisorOptions -> Text
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorName:Supervisor] :: Supervisor -> !SupervisorName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorNotify:ParentSupervisorEnv] :: ParentSupervisorEnv -> !(SupervisorMessage -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorNotify:SupervisorEnv] :: SupervisorEnv -> !(SupervisorMessage -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorNotify:Supervisor] :: Supervisor -> SupervisorMessage -> IO ()
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorOnFailure:CapatazOptions] :: CapatazOptions -> !(SomeException -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorOnFailure:SupervisorOptions] :: SupervisorOptions -> !(SomeException -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorOnIntensityReached:CapatazOptions] :: CapatazOptions -> !(IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorOnIntensityReached:SupervisorEnv] :: SupervisorEnv -> !(SomeException -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorOnIntensityReached:SupervisorOptions] :: SupervisorOptions -> !(IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorOptions:ForkWorker] :: ControlAction -> !SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorOptions:SupervisorEnv] :: SupervisorEnv -> !SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorOptions:Supervisor] :: Supervisor -> !SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorPeriodSeconds:CapatazOptions] :: CapatazOptions -> !NominalDiffTime
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorPeriodSeconds:SupervisorEnv] :: SupervisorEnv -> !NominalDiffTime
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorPeriodSeconds:SupervisorOptions] :: SupervisorOptions -> !NominalDiffTime
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorProcessMap:SupervisorEnv] :: SupervisorEnv -> !(IORef ProcessMap)
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorProcessSpecList:CapatazOptions] :: CapatazOptions -> ![ProcessSpec]
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorProcessSpecList:SupervisorOptions] :: SupervisorOptions -> ![ProcessSpec]
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorProcessTerminationOrder:CapatazOptions] :: CapatazOptions -> !ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorProcessTerminationOrder:SupervisorEnv] :: SupervisorEnv -> !ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorProcessTerminationOrder:SupervisorOptions] :: SupervisorOptions -> !ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorRestartStrategy:CapatazOptions] :: CapatazOptions -> !SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorRestartStrategy:SupervisorEnv] :: SupervisorEnv -> !SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorRestartStrategy:SupervisorOptions] :: SupervisorOptions -> !SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: [$sel:supervisorStatusVar:SupervisorEnv] :: SupervisorEnv -> !(TVar SupervisorStatus)
+ Control.Concurrent.Capataz.Internal.Types: [$sel:terminationReason:InvalidSupervisorStatusReached] :: CapatazEvent -> !Text
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerAction:WorkerOptions] :: WorkerOptions -> WorkerAction
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerAsync:Worker] :: Worker -> !(Async ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerCreationTime:Worker] :: Worker -> !UTCTime
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerId:Worker] :: Worker -> !WorkerId
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerName:WorkerOptions] :: WorkerOptions -> !WorkerName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerName:Worker] :: Worker -> !WorkerName
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerOnCompletion:WorkerOptions] :: WorkerOptions -> !(IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerOnFailure:WorkerOptions] :: WorkerOptions -> !(SomeException -> IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerOnTermination:WorkerOptions] :: WorkerOptions -> !(IO ())
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerOptions:ForkWorker] :: ControlAction -> !WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerOptions:Worker] :: Worker -> !WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerRestartStrategy:WorkerOptions] :: WorkerOptions -> !WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: [$sel:workerTerminationPolicy:WorkerOptions] :: WorkerOptions -> !WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: buildSupervisorOptions :: SupervisorName -> (SupervisorOptions -> SupervisorOptions) -> SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types: buildSupervisorOptionsWithDefaults :: SupervisorName -> SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types: buildWorkerOptions :: WorkerName -> (IO ()) -> (WorkerOptions -> WorkerOptions) -> WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types: buildWorkerOptionsWithDefaults :: WorkerName -> (IO ()) -> WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types: data CallbackType
+ Control.Concurrent.Capataz.Internal.Types: data Capataz
+ Control.Concurrent.Capataz.Internal.Types: data CapatazError
+ Control.Concurrent.Capataz.Internal.Types: data CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: data CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types: data CapatazSignal
+ Control.Concurrent.Capataz.Internal.Types: data ControlAction
+ Control.Concurrent.Capataz.Internal.Types: data MonitorEvent
+ Control.Concurrent.Capataz.Internal.Types: data ParentSupervisorEnv
+ Control.Concurrent.Capataz.Internal.Types: data Process
+ Control.Concurrent.Capataz.Internal.Types: data ProcessEnv
+ Control.Concurrent.Capataz.Internal.Types: data ProcessError
+ Control.Concurrent.Capataz.Internal.Types: data ProcessRestartAction
+ Control.Concurrent.Capataz.Internal.Types: data ProcessSpec
+ Control.Concurrent.Capataz.Internal.Types: data ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: data ProcessType
+ Control.Concurrent.Capataz.Internal.Types: data Supervisor
+ Control.Concurrent.Capataz.Internal.Types: data SupervisorEnv
+ Control.Concurrent.Capataz.Internal.Types: data SupervisorMessage
+ Control.Concurrent.Capataz.Internal.Types: data SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types: data SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: data SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: data Worker
+ Control.Concurrent.Capataz.Internal.Types: data WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types: data WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: data WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: defCapatazOptions :: (CapatazOptions -> CapatazOptions) -> CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types: instance Control.DeepSeq.NFData Control.Concurrent.Capataz.Internal.Types.CapatazError
+ Control.Concurrent.Capataz.Internal.Types: instance Control.DeepSeq.NFData Control.Concurrent.Capataz.Internal.Types.CapatazSignal
+ Control.Concurrent.Capataz.Internal.Types: instance Control.DeepSeq.NFData Control.Concurrent.Capataz.Internal.Types.ProcessRestartAction
+ Control.Concurrent.Capataz.Internal.Types: instance Control.DeepSeq.NFData Control.Concurrent.Capataz.Internal.Types.ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: instance Control.DeepSeq.NFData Control.Concurrent.Capataz.Internal.Types.SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance Control.DeepSeq.NFData Control.Concurrent.Capataz.Internal.Types.SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: instance Control.DeepSeq.NFData Control.Concurrent.Capataz.Internal.Types.WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance Control.DeepSeq.NFData Control.Concurrent.Capataz.Internal.Types.WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: instance Control.Teardown.Internal.Types.ITeardown Control.Concurrent.Capataz.Internal.Types.Capataz
+ Control.Concurrent.Capataz.Internal.Types: instance Data.Default.Class.Default Control.Concurrent.Capataz.Internal.Types.ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: instance Data.Default.Class.Default Control.Concurrent.Capataz.Internal.Types.SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance Data.Default.Class.Default Control.Concurrent.Capataz.Internal.Types.WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance Data.Default.Class.Default Control.Concurrent.Capataz.Internal.Types.WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Eq Control.Concurrent.Capataz.Internal.Types.CallbackType
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Eq Control.Concurrent.Capataz.Internal.Types.ProcessRestartAction
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Eq Control.Concurrent.Capataz.Internal.Types.ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Eq Control.Concurrent.Capataz.Internal.Types.ProcessType
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Eq Control.Concurrent.Capataz.Internal.Types.SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Eq Control.Concurrent.Capataz.Internal.Types.SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Eq Control.Concurrent.Capataz.Internal.Types.WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Eq Control.Concurrent.Capataz.Internal.Types.WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Ord Control.Concurrent.Capataz.Internal.Types.ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Ord Control.Concurrent.Capataz.Internal.Types.SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Classes.Ord Control.Concurrent.Capataz.Internal.Types.WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Exception.Exception Control.Concurrent.Capataz.Internal.Types.CapatazError
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Exception.Exception Control.Concurrent.Capataz.Internal.Types.CapatazSignal
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Exception.Exception Control.Concurrent.Capataz.Internal.Types.ProcessError
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.CallbackType
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.CapatazError
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.CapatazSignal
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.ControlAction
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.ProcessError
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.ProcessRestartAction
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.SupervisorMessage
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Generics.Generic Control.Concurrent.Capataz.Internal.Types.WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.CallbackType
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.CapatazError
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.CapatazEvent
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.CapatazSignal
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.MonitorEvent
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.ProcessError
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.ProcessRestartAction
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.ProcessTerminationOrder
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.ProcessType
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.SupervisorRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.WorkerRestartStrategy
+ Control.Concurrent.Capataz.Internal.Types: instance GHC.Show.Show Control.Concurrent.Capataz.Internal.Types.WorkerTerminationPolicy
+ Control.Concurrent.Capataz.Internal.Types: supervisorSpec :: SupervisorName -> (SupervisorOptions -> SupervisorOptions) -> ProcessSpec
+ Control.Concurrent.Capataz.Internal.Types: supervisorSpecWithDefaults :: SupervisorName -> ProcessSpec
+ Control.Concurrent.Capataz.Internal.Types: type CapatazId = UUID
+ Control.Concurrent.Capataz.Internal.Types: type CapatazName = Text
+ Control.Concurrent.Capataz.Internal.Types: type ParentSupervisor = Supervisor
+ Control.Concurrent.Capataz.Internal.Types: type ProcessId = UUID
+ Control.Concurrent.Capataz.Internal.Types: type ProcessMap = HashMap ProcessId Process
+ Control.Concurrent.Capataz.Internal.Types: type ProcessName = Text
+ Control.Concurrent.Capataz.Internal.Types: type ProcessThreadId = ThreadId
+ Control.Concurrent.Capataz.Internal.Types: type RestartCount = Int
+ Control.Concurrent.Capataz.Internal.Types: type SupervisorId = UUID
+ Control.Concurrent.Capataz.Internal.Types: type SupervisorName = Text
+ Control.Concurrent.Capataz.Internal.Types: type WorkerAction = IO ()
+ Control.Concurrent.Capataz.Internal.Types: type WorkerId = UUID
+ Control.Concurrent.Capataz.Internal.Types: type WorkerName = Text
+ Control.Concurrent.Capataz.Internal.Types: workerSpec :: WorkerName -> (IO ()) -> (WorkerOptions -> WorkerOptions) -> ProcessSpec
+ Control.Concurrent.Capataz.Internal.Types: workerSpecWithDefaults :: WorkerName -> (IO ()) -> ProcessSpec
+ Control.Concurrent.Capataz.Internal.Types.Lens: class HasSupervisorFailureCallback s
+ Control.Concurrent.Capataz.Internal.Types.Lens: class HasSupervisorIntensity s
+ Control.Concurrent.Capataz.Internal.Types.Lens: class HasSupervisorIntensityReachedCallback s
+ Control.Concurrent.Capataz.Internal.Types.Lens: class HasSupervisorPeriodSeconds s
+ Control.Concurrent.Capataz.Internal.Types.Lens: class HasSupervisorProcessSpecList s
+ Control.Concurrent.Capataz.Internal.Types.Lens: class HasSupervisorProcessTerminationOrder s
+ Control.Concurrent.Capataz.Internal.Types.Lens: class HasSupervisorRestartStrategy s
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorFailureCallback Control.Concurrent.Capataz.Internal.Types.CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorFailureCallback Control.Concurrent.Capataz.Internal.Types.SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorIntensity Control.Concurrent.Capataz.Internal.Types.CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorIntensity Control.Concurrent.Capataz.Internal.Types.SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorIntensityReachedCallback Control.Concurrent.Capataz.Internal.Types.CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorIntensityReachedCallback Control.Concurrent.Capataz.Internal.Types.SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorPeriodSeconds Control.Concurrent.Capataz.Internal.Types.CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorPeriodSeconds Control.Concurrent.Capataz.Internal.Types.SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorProcessSpecList Control.Concurrent.Capataz.Internal.Types.CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorProcessSpecList Control.Concurrent.Capataz.Internal.Types.SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorProcessTerminationOrder Control.Concurrent.Capataz.Internal.Types.CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorProcessTerminationOrder Control.Concurrent.Capataz.Internal.Types.SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorRestartStrategy Control.Concurrent.Capataz.Internal.Types.CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: instance Control.Concurrent.Capataz.Internal.Types.Lens.HasSupervisorRestartStrategy Control.Concurrent.Capataz.Internal.Types.SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: onSystemEventL :: Functor f => ((CapatazEvent -> IO ()) -> f (CapatazEvent -> IO ())) -> CapatazOptions -> f CapatazOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: supervisorIntensityL :: (HasSupervisorIntensity s, Functor f) => (Int -> f Int) -> s -> f s
+ Control.Concurrent.Capataz.Internal.Types.Lens: supervisorOnFailureL :: (HasSupervisorFailureCallback s, Functor f) => ((SomeException -> IO ()) -> f (SomeException -> IO ())) -> s -> f s
+ Control.Concurrent.Capataz.Internal.Types.Lens: supervisorOnIntensityReachedL :: (HasSupervisorIntensityReachedCallback s, Functor f) => (IO () -> f (IO ())) -> s -> f s
+ Control.Concurrent.Capataz.Internal.Types.Lens: supervisorPeriodSecondsL :: (HasSupervisorPeriodSeconds s, Functor f) => (NominalDiffTime -> f NominalDiffTime) -> s -> f s
+ Control.Concurrent.Capataz.Internal.Types.Lens: supervisorProcessSpecListL :: (HasSupervisorProcessSpecList s, Functor f) => ([ProcessSpec] -> f [ProcessSpec]) -> s -> f s
+ Control.Concurrent.Capataz.Internal.Types.Lens: supervisorProcessTerminationOrderL :: (HasSupervisorProcessTerminationOrder s, Functor f) => (ProcessTerminationOrder -> f ProcessTerminationOrder) -> s -> f s
+ Control.Concurrent.Capataz.Internal.Types.Lens: supervisorRestartStrategyL :: (HasSupervisorRestartStrategy s, Functor f) => (SupervisorRestartStrategy -> f SupervisorRestartStrategy) -> s -> f s
+ Control.Concurrent.Capataz.Internal.Types.Lens: workerOnCompletionL :: Functor f => (IO () -> f (IO ())) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: workerOnFailureL :: Functor f => ((SomeException -> IO ()) -> f (SomeException -> IO ())) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: workerOnTerminationL :: Functor f => (IO () -> f (IO ())) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: workerRestartStrategyL :: Functor f => (WorkerRestartStrategy -> f WorkerRestartStrategy) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz.Internal.Types.Lens: workerTerminationPolicyL :: Functor f => (WorkerTerminationPolicy -> f WorkerTerminationPolicy) -> WorkerOptions -> f WorkerOptions
+ Control.Concurrent.Capataz.Internal.Util: appendProcessToMap :: SupervisorEnv -> Process -> IO ()
+ Control.Concurrent.Capataz.Internal.Util: capatazOptionsToSupervisorOptions :: CapatazOptions -> SupervisorOptions
+ Control.Concurrent.Capataz.Internal.Util: fetchProcess :: SupervisorEnv -> ProcessId -> IO (Maybe Process)
+ Control.Concurrent.Capataz.Internal.Util: getProcessId :: Process -> ProcessId
+ Control.Concurrent.Capataz.Internal.Util: getTidNumber :: ThreadId -> Maybe Text
+ Control.Concurrent.Capataz.Internal.Util: readProcessMap :: SupervisorEnv -> IO ProcessMap
+ Control.Concurrent.Capataz.Internal.Util: readSupervisorStatus :: SupervisorEnv -> IO SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Util: readSupervisorStatusSTM :: TVar SupervisorStatus -> STM SupervisorStatus
+ Control.Concurrent.Capataz.Internal.Util: removeProcessFromMap :: SupervisorEnv -> ProcessId -> IO ()
+ Control.Concurrent.Capataz.Internal.Util: resetProcessMap :: SupervisorEnv -> (ProcessMap -> ProcessMap) -> IO ()
+ Control.Concurrent.Capataz.Internal.Util: sendControlMsg :: SupervisorEnv -> ControlAction -> IO ()
+ Control.Concurrent.Capataz.Internal.Util: sendSyncControlMsg :: SupervisorEnv -> (IO () -> ControlAction) -> IO ()
+ Control.Concurrent.Capataz.Internal.Util: setProcessThreadName :: WorkerId -> WorkerName -> IO ()
+ Control.Concurrent.Capataz.Internal.Util: sortProcessesByTerminationOrder :: ProcessTerminationOrder -> ProcessMap -> [Process]
+ Control.Concurrent.Capataz.Internal.Util: toParentSupervisorEnv :: SupervisorEnv -> ParentSupervisorEnv
+ Control.Concurrent.Capataz.Internal.Util: writeSupervisorStatus :: SupervisorEnv -> SupervisorStatus -> IO ()
+ Control.Concurrent.Capataz.Internal.Worker: forkWorker :: ParentSupervisorEnv -> WorkerOptions -> Maybe (WorkerId, RestartCount) -> IO Worker
+ Control.Concurrent.Capataz.Internal.Worker: workerMain :: ParentSupervisorEnv -> WorkerOptions -> WorkerId -> RestartCount -> IO Worker
- Control.Concurrent.Capataz: AllForOne :: CapatazRestartStrategy
+ Control.Concurrent.Capataz: AllForOne :: SupervisorRestartStrategy
- Control.Concurrent.Capataz: Halted :: CapatazStatus
+ Control.Concurrent.Capataz: Halted :: SupervisorStatus
- Control.Concurrent.Capataz: Halting :: CapatazStatus
+ Control.Concurrent.Capataz: Halting :: SupervisorStatus
- Control.Concurrent.Capataz: Initializing :: CapatazStatus
+ Control.Concurrent.Capataz: Initializing :: SupervisorStatus
- Control.Concurrent.Capataz: NewestFirst :: WorkerTerminationOrder
+ Control.Concurrent.Capataz: NewestFirst :: ProcessTerminationOrder
- Control.Concurrent.Capataz: OldestFirst :: WorkerTerminationOrder
+ Control.Concurrent.Capataz: OldestFirst :: ProcessTerminationOrder
- Control.Concurrent.Capataz: OneForOne :: CapatazRestartStrategy
+ Control.Concurrent.Capataz: OneForOne :: SupervisorRestartStrategy
- Control.Concurrent.Capataz: Running :: CapatazStatus
+ Control.Concurrent.Capataz: Running :: SupervisorStatus
- Control.Concurrent.Capataz: WorkerSpec :: WorkerAction -> !WorkerName -> !(SomeException -> IO ()) -> !(IO ()) -> !(IO ()) -> !WorkerTerminationPolicy -> !WorkerRestartStrategy -> WorkerSpec
+ Control.Concurrent.Capataz: WorkerSpec :: WorkerOptions -> ProcessSpec
- Control.Concurrent.Capataz: forkCapataz :: CapatazOptions -> IO Capataz
+ Control.Concurrent.Capataz: forkCapataz :: (CapatazOptions -> CapatazOptions) -> IO Capataz
- Control.Concurrent.Capataz: forkWorker :: WorkerOptions -> IO () -> Capataz -> IO WorkerId
+ Control.Concurrent.Capataz: forkWorker :: HasSupervisor supervisor => WorkerOptions -> supervisor -> IO WorkerId
Files
- README.md +7/−7
- capataz.cabal +24/−16
- src/Control/Concurrent/Capataz.hs +70/−42
- src/Control/Concurrent/Capataz/Event.hs +5/−0
- src/Control/Concurrent/Capataz/Internal/Core.hs +206/−0
- src/Control/Concurrent/Capataz/Internal/Process.hs +379/−0
- src/Control/Concurrent/Capataz/Internal/Supervisor.hs +563/−0
- src/Control/Concurrent/Capataz/Internal/Types.hs +694/−0
- src/Control/Concurrent/Capataz/Internal/Types/Lens.hs +233/−0
- src/Control/Concurrent/Capataz/Internal/Util.hs +174/−0
- src/Control/Concurrent/Capataz/Internal/Worker.hs +72/−0
- src/Control/Concurrent/Capataz/Lens.hs +21/−0
- src/Control/Concurrent/Internal/Capataz/Core.hs +0/−259
- src/Control/Concurrent/Internal/Capataz/Restart.hs +0/−219
- src/Control/Concurrent/Internal/Capataz/Types.hs +0/−519
- src/Control/Concurrent/Internal/Capataz/Util.hs +0/−165
- src/Control/Concurrent/Internal/Capataz/Worker.hs +0/−312
- test/testsuite/Control/Concurrent/Capataz/SupervisorTest.hs +145/−0
- test/testsuite/Control/Concurrent/CapatazTest.hs +974/−1243
- test/testsuite/Main.hs +8/−5
- test/testsuite/Test/Util.hs +412/−0
README.md view
@@ -29,16 +29,16 @@ This library is intended to be a drop-in replacement to `forkIO` invocations throughout your codebase, the difference being, you'll need to do a bit more of setup specifying supervision rules, and also pass along a reference of a-capataz descriptor to every thread fork.+supervisor for every thread you fork. ### Why not [distributed-process](https://hackage.haskell.org/package/distributed-process)? `distributed-process` is an impressive library, and brings many great utilities-if you need to develop applications that are reliable. However, it is a-heavyweight solution that will enforce serious changes to your application. It-also optimizes its implementation around the *distributed* part of its name.-This library is intended to provide some benefits of `distributed-process` ,-without the baggage.+if you need to develop applications that need to be distributed and reliable.+However, it is a heavyweight solution that will enforce serious changes to your+application. This library is intended to provide the reliability benefits of+`distributed-process`, without the constraints imposed by the *distributed*+part. ### Why not a complete actor system? @@ -91,7 +91,7 @@ ## Development [](https://travis-ci.org/roman/Haskell-capataz)-[](https://img.shields.io/github/commits-since/roman/haskell-capataz/v0.0.0.1.svg)+[](https://img.shields.io/github/commits-since/roman/haskell-capataz/v0.0.0.2.svg) [](http://packdeps.haskellers.com/feed?needle=capataz) Follow the [developer guidelines](https://romanandreg.gitbooks.io/capataz/content/developer-guidelines.html)
capataz.cabal view
@@ -1,11 +1,11 @@ name: capataz-version: 0.0.0.2+version: 0.1.0.0 cabal-version: >=1.10 build-type: Simple license: MIT license-file: LICENSE copyright: © 2018 Roman Gonzalez-maintainer: capataz@roman-gonzalez.info+maintainer: open-source@roman-gonzalez.info stability: alpha (experimental) homepage: https://github.com/roman/Haskell-capataz#readme bug-reports: https://github.com/roman/Haskell-capataz/issues@@ -41,16 +41,25 @@ library exposed-modules: Control.Concurrent.Capataz+ Control.Concurrent.Capataz.Event+ Control.Concurrent.Capataz.Lens+ Control.Concurrent.Capataz.Internal.Core+ Control.Concurrent.Capataz.Internal.Types+ Control.Concurrent.Capataz.Internal.Types.Lens+ Control.Concurrent.Capataz.Internal.Supervisor+ Control.Concurrent.Capataz.Internal.Util+ Control.Concurrent.Capataz.Internal.Worker build-depends: async >=2.1.1.1 && <2.2, base >=4.10.1.0 && <4.11, bytestring >=0.10.8.2 && <0.11, data-default >=0.7.1.1 && <0.8,- protolude ==0.2.*,- safe-exceptions >=0.1.6.0 && <0.2,+ microlens >=0.4.8.3 && <0.5,+ protolude >=0.2.1 && <0.3,+ safe-exceptions >=0.1.7.0 && <0.2, stm >=2.4.4.1 && <2.5, teardown >=0.3.0.0 && <0.4,- text >=1.2.2.2 && <1.3,+ text >=1.2.3.0 && <1.3, time >=1.8.0.2 && <1.9, unordered-containers >=0.2.8.0 && <0.3, uuid >=1.3.13 && <1.4,@@ -58,11 +67,7 @@ default-language: Haskell2010 hs-source-dirs: src other-modules:- Control.Concurrent.Internal.Capataz.Core- Control.Concurrent.Internal.Capataz.Restart- Control.Concurrent.Internal.Capataz.Types- Control.Concurrent.Internal.Capataz.Util- Control.Concurrent.Internal.Capataz.Worker+ Control.Concurrent.Capataz.Internal.Process Paths_capataz ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates @@ -75,16 +80,17 @@ bytestring >=0.10.8.2 && <0.11, capataz -any, data-default >=0.7.1.1 && <0.8,+ microlens >=0.4.8.3 && <0.5, pretty-show >=1.6.16 && <1.7,- protolude ==0.2.*,- safe-exceptions >=0.1.6.0 && <0.2,+ protolude >=0.2.1 && <0.3,+ safe-exceptions >=0.1.7.0 && <0.2, stm >=2.4.4.1 && <2.5,- tasty >=1.0.0.1 && <1.1,+ tasty >=0.12.0.1 && <0.13, tasty-hunit >=0.10.0.1 && <0.11,- tasty-rerun >=1.1.8 && <1.2,+ tasty-rerun >=1.1.9 && <1.2, tasty-smallcheck >=0.8.1 && <0.9, teardown >=0.3.0.0 && <0.4,- text >=1.2.2.2 && <1.3,+ text >=1.2.3.0 && <1.3, time >=1.8.0.2 && <1.9, unordered-containers >=0.2.8.0 && <0.3, uuid >=1.3.13 && <1.4,@@ -92,6 +98,8 @@ default-language: Haskell2010 hs-source-dirs: test/testsuite other-modules:+ Control.Concurrent.Capataz.SupervisorTest Control.Concurrent.CapatazTest+ Test.Util Paths_capataz- ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -threaded -rtsopts -with-rtsopts=-N+ ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -threaded
src/Control/Concurrent/Capataz.hs view
@@ -8,51 +8,79 @@ module Control.Concurrent.Capataz ( -- * Types- CallbackType (..)-, WorkerAction-, WorkerError (..)-, WorkerOptions (..)-, WorkerRestartStrategy (..)-, WorkerSpec (..)-, WorkerTerminationOrder (..)-, WorkerTerminationPolicy (..)-, Capataz (..)-, CapatazEvent (..)-, CapatazOptions (..)-, CapatazRestartStrategy (..)-, CapatazStatus (..)-, defWorkerOptions-, defWorkerSpec-, defCapatazOptions+ Control.Concurrent.Capataz.Internal.Core.HasSupervisor (..)+, Control.Concurrent.Capataz.Internal.Types.CallbackType (..)++, Control.Concurrent.Capataz.Internal.Types.WorkerId+, Control.Concurrent.Capataz.Internal.Types.WorkerRestartStrategy (..)+, Control.Concurrent.Capataz.Internal.Types.WorkerTerminationPolicy (..)+, Control.Concurrent.Capataz.Internal.Types.WorkerOptions++, Control.Concurrent.Capataz.Internal.Types.ProcessId+, Control.Concurrent.Capataz.Internal.Types.ProcessSpec (..)+, Control.Concurrent.Capataz.Internal.Types.ProcessType (..)+, Control.Concurrent.Capataz.Internal.Types.ProcessTerminationOrder (..)+, Control.Concurrent.Capataz.Internal.Types.ProcessError (..)++, Control.Concurrent.Capataz.Internal.Types.SupervisorId+, Control.Concurrent.Capataz.Internal.Types.Supervisor+, Control.Concurrent.Capataz.Internal.Types.SupervisorRestartStrategy (..)+, Control.Concurrent.Capataz.Internal.Types.SupervisorStatus (..)+, Control.Concurrent.Capataz.Internal.Types.SupervisorOptions++, Control.Concurrent.Capataz.Internal.Types.CapatazOptions++, Control.Concurrent.Capataz.Internal.Types.Capataz++-- * Default Options for Capataz Processes+, Control.Concurrent.Capataz.Internal.Types.buildSupervisorOptions+, Control.Concurrent.Capataz.Internal.Types.buildSupervisorOptionsWithDefaults+, Control.Concurrent.Capataz.Internal.Types.buildWorkerOptions+, Control.Concurrent.Capataz.Internal.Types.buildWorkerOptionsWithDefaults+, Control.Concurrent.Capataz.Internal.Types.supervisorSpec+, Control.Concurrent.Capataz.Internal.Types.supervisorSpecWithDefaults+, Control.Concurrent.Capataz.Internal.Types.workerSpec+, Control.Concurrent.Capataz.Internal.Types.workerSpecWithDefaults++-- * Lenses to modify Option Records+, Control.Concurrent.Capataz.Lens.onSystemEventL+, Control.Concurrent.Capataz.Lens.supervisorIntensityL+, Control.Concurrent.Capataz.Lens.supervisorPeriodSecondsL+, Control.Concurrent.Capataz.Lens.supervisorRestartStrategyL+, Control.Concurrent.Capataz.Lens.supervisorProcessSpecListL+, Control.Concurrent.Capataz.Lens.supervisorProcessTerminationOrderL+, Control.Concurrent.Capataz.Lens.supervisorOnIntensityReachedL+, Control.Concurrent.Capataz.Lens.supervisorOnFailureL+, Control.Concurrent.Capataz.Lens.workerOnFailureL+, Control.Concurrent.Capataz.Lens.workerOnCompletionL+, Control.Concurrent.Capataz.Lens.workerOnTerminationL+, Control.Concurrent.Capataz.Lens.workerTerminationPolicyL+, Control.Concurrent.Capataz.Lens.workerRestartStrategyL+ -- * Core functionality-, forkWorker-, forkCapataz-, terminateWorker+, Control.Concurrent.Capataz.Internal.Core.forkWorker+, Control.Concurrent.Capataz.Internal.Core.forkSupervisor+, Control.Concurrent.Capataz.Internal.Core.forkCapataz+, Control.Concurrent.Capataz.Internal.Core.terminateProcess+ -- * Utility functions-, capatazToAsync+, Control.Concurrent.Capataz.Internal.Core.joinCapatazThread+, Control.Concurrent.Capataz.Internal.Core.getSupervisorProcessId+, Control.Concurrent.Capataz.Internal.Core.getSupervisorAsync+, Control.Concurrent.Capataz.Internal.Core.getCapatazTeardown+ -- * Teardown (re-exported)-, teardown+, Control.Teardown.teardown++-- * Lens (re-exported)+, (.~)+, (&)+, Control.Concurrent.Capataz.Lens.set ) where -import Control.Concurrent.Internal.Capataz.Core (forkCapataz, forkWorker, terminateWorker)-import Control.Concurrent.Internal.Capataz.Types- ( CallbackType (..)- , Capataz (..)- , CapatazEvent (..)- , CapatazOptions (..)- , CapatazRestartStrategy (..)- , CapatazStatus (..)- , WorkerAction- , WorkerError (..)- , WorkerOptions (..)- , WorkerRestartStrategy (..)- , WorkerSpec (..)- , WorkerTerminationOrder (..)- , WorkerTerminationPolicy (..)- , defCapatazOptions- , defWorkerOptions- , defWorkerSpec- )-import Control.Concurrent.Internal.Capataz.Util (capatazToAsync)-import Control.Teardown (teardown)+import qualified Control.Concurrent.Capataz.Internal.Core+import qualified Control.Concurrent.Capataz.Internal.Types+import qualified Control.Concurrent.Capataz.Lens+import qualified Control.Teardown+import Control.Concurrent.Capataz.Lens ((.~), (&))
+ src/Control/Concurrent/Capataz/Event.hs view
@@ -0,0 +1,5 @@+module Control.Concurrent.Capataz.Event+ (CapatazEvent(..))+ where++import Control.Concurrent.Capataz.Internal.Types (CapatazEvent (..))
+ src/Control/Concurrent/Capataz/Internal/Core.hs view
@@ -0,0 +1,206 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-| This module contains:++* Functions exported on the public API+* High level message handlers of the supervisor thread loop++-}+module Control.Concurrent.Capataz.Internal.Core+(+ HasSupervisor(..)+, forkWorker+, forkSupervisor+, forkCapataz+, terminateProcess+, joinCapatazThread+, getSupervisorProcessId+, getSupervisorAsync+, getCapatazTeardown+)+where++import Protolude++import Control.Concurrent.MVar (newEmptyMVar, takeMVar)+import Control.Teardown (newTeardown)+import Control.Teardown (Teardown)+import Data.Time.Clock (getCurrentTime)++import qualified Data.UUID.V4 as UUID (nextRandom)++import qualified Control.Concurrent.Capataz.Internal.Supervisor as Supervisor++import Control.Concurrent.Capataz.Internal.Types+import qualified Control.Concurrent.Capataz.Internal.Util as Util++--------------------------------------------------------------------------------++-- | Utility typeclass to call public supervision API with types+-- that contain a supervisor (e.g. Capataz record).+class HasSupervisor a where+ -- | Fetches a supervisor from a record internals.+ getSupervisor :: a -> Supervisor++instance HasSupervisor Capataz where+ getSupervisor (Capataz {capatazSupervisor}) = capatazSupervisor++instance HasSupervisor Supervisor where+ getSupervisor = identity++-- | Creates a Capataz record, which holds both a root supervisor and a+-- "Teardown" to shut down the system. The root supervisor monitors failures on+-- process threads defined with "supervisorProcessSpecList" or created+-- dynamically using "forkWorker" or "forkSupervisor".+forkCapataz :: (CapatazOptions -> CapatazOptions) -> IO Capataz+forkCapataz modOptionsFn = do+ capatazId <- UUID.nextRandom+ supervisorId <- UUID.nextRandom+ let+ capatazOptions@CapatazOptions { notifyEvent } = defCapatazOptions modOptionsFn+ supervisorOptions@SupervisorOptions { supervisorName } =+ Util.capatazOptionsToSupervisorOptions capatazOptions+ parentSupervisorEnv = ParentSupervisorEnv+ { supervisorId = capatazId+ , supervisorName = "capataz-root"+ , supervisorNotify = \supervisorEvent -> do+ eventTime <- getCurrentTime+ case supervisorEvent of+ MonitorEvent ProcessFailed' { processError } -> notifyEvent+ CapatazFailed+ { supervisorId+ , supervisorName+ , eventTime+ , supervisorError = processError+ }++ MonitorEvent ProcessTerminated'{} -> notifyEvent CapatazTerminated+ { supervisorId+ , supervisorName+ , eventTime+ }++ MonitorEvent ProcessCompleted'{} ->+ panic "Capataz completed; this should never happen"++ MonitorEvent ProcessForcedRestart{} ->+ panic+ "Capataz was restarted from a OneForAll strategy; this should never happen"++ ControlAction{} ->+ panic "Capataz received a ControlAction message; bad implementation"++ , notifyEvent+ }++ capatazSupervisor@Supervisor { supervisorEnv } <- Supervisor.supervisorMain+ parentSupervisorEnv+ supervisorOptions+ supervisorId+ 0++ capatazTeardown <- newTeardown+ "capataz"+ ( do+ Supervisor.haltSupervisor "capataz system shutdown" supervisorEnv+ eventTime <- getCurrentTime+ notifyEvent CapatazTerminated {supervisorId , supervisorName , eventTime }+ )++ return Capataz {capatazSupervisor , capatazTeardown }++-- | Creates a green thread from an "IO ()" sub-routine. Depending in options+-- defined in the "WorkerOptions" record, it will automatically restart this+-- sub-routine in case of failures.+--+-- See documentation of related functions:+--+-- * "buildWorkerOptionsWithDefault"+-- * "buildWorkerOptions"+--+forkWorker+ :: HasSupervisor supervisor+ => WorkerOptions -- ^ Worker options (restart, name, callbacks, etc)+ -> supervisor -- ^ "Supervisor" that supervises the worker+ -> IO WorkerId -- ^ An identifier that can be used to terminate the "Worker"+forkWorker workerOptions sup = do+ let Supervisor { supervisorNotify } = getSupervisor sup+ workerIdVar <- newEmptyMVar+ supervisorNotify+ ( ControlAction ForkWorker+ { workerOptions+ , returnWorkerId = putMVar workerIdVar+ }+ )+ takeMVar workerIdVar++-- | Creates a green thread which monitors other green threads for failures and+-- restarts them using settings defined on "SupervisorOptions".+--+-- See documentation of related functions:+--+-- * "buildSupervisorOptionsWithDefault"+-- * "buildSupervisorOptions"+--+forkSupervisor+ :: HasSupervisor parentSupervisor+ => SupervisorOptions -- ^ Supervisor options+ -> parentSupervisor -- ^ Parent supervisor instance that supervises new supervisor+ -> IO Supervisor -- ^ A record used to dynamically create and supervise+ -- other processes+forkSupervisor supervisorOptions parentSup = do+ let Supervisor { supervisorNotify } = getSupervisor parentSup+ supervisorVar <- newEmptyMVar+ supervisorNotify+ ( ControlAction ForkSupervisor+ { supervisorOptions+ , returnSupervisor = putMVar supervisorVar+ }+ )+ takeMVar supervisorVar++-- | Stops the execution of a green thread being supervised by the given+-- supervisor.+--+-- NOTE: If "ProcessId" maps to a worker that is configured with a "Permanent"+-- worker restart strategy, the worker green thread __will be restarted again__.+--+terminateProcess+ :: HasSupervisor supervisor => Text -> ProcessId -> supervisor -> IO Bool+terminateProcess processTerminationReason processId supervisor = do+ let Supervisor { supervisorNotify } = getSupervisor supervisor+ result <- newEmptyMVar+ supervisorNotify+ ( ControlAction TerminateProcess+ { processId+ , processTerminationReason+ , notifyProcessTermination = putMVar result+ }+ )+ takeMVar result++-- | Joins the thread of the root supervisor of the given capataz system to the+-- current thread.+joinCapatazThread :: Capataz -> IO ()+joinCapatazThread Capataz { capatazSupervisor } =+ let Supervisor { supervisorAsync } = capatazSupervisor+ in wait supervisorAsync++-- | Gets "Teardown" record of this capataz system.+getCapatazTeardown :: Capataz -> Teardown+getCapatazTeardown Capataz { capatazTeardown } = capatazTeardown++-- | Gets the "Async" of a Supervisor thread.+--+-- NOTE: There is no way to get the "Async" value of the root supervisor; this+-- is to avoid error scenarios.+getSupervisorAsync :: Supervisor -> Async ()+getSupervisorAsync Supervisor { supervisorAsync } =+ supervisorAsync++-- | Gets the process identifier of a Supervisor; normally used for termination.+getSupervisorProcessId :: Supervisor -> ProcessId+getSupervisorProcessId Supervisor { supervisorId } = supervisorId
+ src/Control/Concurrent/Capataz/Internal/Process.hs view
@@ -0,0 +1,379 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NoImplicitPrelude #-}++-- | This module contains functionality that works both for Supervisor and+-- Worker process types.+module Control.Concurrent.Capataz.Internal.Process where++import Protolude++import Control.Concurrent.Capataz.Internal.Types+import Control.Concurrent.Capataz.Internal.Util+ (readProcessMap, sortProcessesByTerminationOrder)+import Data.Time.Clock (UTCTime, getCurrentTime)++-- | Gets "Async" from a given Process.+getProcessAsync :: Process -> Async ()+getProcessAsync process = case process of+ WorkerProcess Worker { workerAsync } -> workerAsync+ SupervisorProcess Supervisor { supervisorAsync } -> supervisorAsync++-- | Gets "ThreadId" from a given Process.+getProcessThreadId :: Process -> ThreadId+getProcessThreadId = asyncThreadId . getProcessAsync++-- | Gets "ProcessId" from a given Process.+getProcessId :: Process -> ProcessId+getProcessId process = case process of+ WorkerProcess Worker { workerId } -> workerId+ SupervisorProcess Supervisor { supervisorId } -> supervisorId++-- | Gets "ProcessName" from a given "ProcessSpec".+getProcessName :: ProcessSpec -> ProcessName+getProcessName procSpec = case procSpec of+ WorkerSpec WorkerOptions { workerName } -> workerName+ SupervisorSpec SupervisorOptions { supervisorName } -> supervisorName++-- | Gets "ProcessType" from a given "ProcessSpec".+getProcessType :: ProcessSpec -> ProcessType+getProcessType processSpec = case processSpec of+ WorkerSpec{} -> WorkerType+ SupervisorSpec{} -> SupervisorType++-- | Gets "ProcessSpec" of a given "Process".+getProcessSpec :: Process -> ProcessSpec+getProcessSpec process = case process of+ WorkerProcess Worker { workerOptions } -> WorkerSpec workerOptions+ SupervisorProcess Supervisor { supervisorOptions } ->+ SupervisorSpec supervisorOptions++-- | Utility function to send notifications to the capataz system callback when+-- a Process fails.+notifyProcessFailed :: SupervisorEnv -> Process -> SomeException -> IO ()+notifyProcessFailed SupervisorEnv { supervisorId, supervisorName, notifyEvent } process processError+ = do+ eventTime <- getCurrentTime+ notifyEvent ProcessFailed+ { supervisorId+ , supervisorName+ , processId = getProcessId process+ , processName = getProcessName (getProcessSpec process)+ , processType = getProcessType (getProcessSpec process)+ , processThreadId = getProcessThreadId process+ , processError+ , eventTime+ }++-- | Utility function to send notifications to the capataz system callback when+-- a Process is terminated.+notifyProcessTerminated :: SupervisorEnv -> Process -> Text -> IO ()+notifyProcessTerminated SupervisorEnv { supervisorId, supervisorName, notifyEvent } process terminationReason+ = do+ eventTime <- getCurrentTime+ notifyEvent ProcessTerminated+ { supervisorId+ , supervisorName+ , processId = getProcessId process+ , processName = getProcessName (getProcessSpec process)+ , processType = getProcessType (getProcessSpec process)+ , processThreadId = getProcessThreadId process+ , terminationReason+ , eventTime+ }++-- | Utility function to send notifications to the capataz system callback when+-- a Process is started or restarted.+notifyProcessStarted+ :: Maybe (ProcessId, RestartCount) -> ParentSupervisorEnv -> Process -> IO ()+notifyProcessStarted mRestartInfo ParentSupervisorEnv { supervisorId, supervisorName, notifyEvent } process+ = do+ eventTime <- getCurrentTime+ case mRestartInfo of+ Just (_processId, processRestartCount) -> notifyEvent ProcessRestarted+ { supervisorId+ , supervisorName+ , processId = getProcessId process+ , processName = getProcessName (getProcessSpec process)+ , processType = getProcessType (getProcessSpec process)+ , processThreadId = getProcessThreadId process+ , processRestartCount+ , eventTime+ }+ Nothing -> notifyEvent ProcessStarted+ { supervisorId+ , supervisorName+ , processId = getProcessId process+ , processName = getProcessName (getProcessSpec process)+ , processType = getProcessType (getProcessSpec process)+ , processThreadId = getProcessThreadId process+ , eventTime+ }++-- | Utility function to send notifications when a Process sub-routine completes+-- without errors.+notifyProcessCompleted :: SupervisorEnv -> Process -> UTCTime -> IO ()+notifyProcessCompleted SupervisorEnv { supervisorId, supervisorName, notifyEvent } process eventTime+ = notifyEvent ProcessCompleted+ { supervisorId+ , supervisorName+ , processId = getProcessId process+ , processName = getProcessName (getProcessSpec process)+ , processType = getProcessType (getProcessSpec process)+ , processThreadId = getProcessThreadId process+ , eventTime+ }+++-- | Utility function to execute a Process onCompletion sub-routine.+callProcessOnCompletion :: ProcessSpec -> IO ()+callProcessOnCompletion procSpec = case procSpec of+ WorkerSpec WorkerOptions { workerOnCompletion } -> workerOnCompletion+ _ -> return ()++-- | Utility function to execute a Process onFailure sub-routine.+callProcessOnFailure :: ProcessSpec -> SomeException -> IO ()+callProcessOnFailure procSpec err = case procSpec of+ WorkerSpec WorkerOptions { workerOnFailure } -> workerOnFailure err+ SupervisorSpec SupervisorOptions { supervisorOnFailure } ->+ supervisorOnFailure err++-- | Utility function to execute a Process onTermination sub-routine.+callProcessOnTermination :: ProcessSpec -> IO ()+callProcessOnTermination procSpec = case procSpec of+ WorkerSpec WorkerOptions { workerOnTermination } -> workerOnTermination+ _ -> return ()++-- | Handles errors produced - or thrown to - a process thread.+handleProcessException+ :: (IO () -> IO a)+ -> ParentSupervisorEnv+ -> ProcessSpec+ -> ProcessId+ -> RestartCount+ -> SomeException+ -> IO MonitorEvent+handleProcessException unmask ParentSupervisorEnv { supervisorId, supervisorName, notifyEvent } procSpec processId restartCount err+ = do+ let processName = getProcessName procSpec+ processThreadId <- myThreadId+ monitorEventTime <- getCurrentTime+ case fromException err of+ Just RestartProcessException -> return ProcessForcedRestart+ { processId+ , processName+ , monitorEventTime+ }++ Just TerminateProcessException { processTerminationReason } -> do+ eErrResult <- try $ unmask $ callProcessOnTermination procSpec++ notifyEvent ProcessCallbackExecuted+ { supervisorId+ , supervisorName+ , processThreadId+ , processId+ , processName+ , processType = getProcessType procSpec+ , processCallbackError = either Just (const Nothing) eErrResult+ , processCallbackType = OnTermination+ , eventTime = monitorEventTime+ }++ case eErrResult of+ Left processCallbackError -> return ProcessFailed'+ { processId+ , processName+ , processError = toException ProcessCallbackFailed+ { processId+ , processCallbackError+ , processCallbackType = OnTermination+ , processError = Just err+ }+ , processRestartCount = restartCount+ , monitorEventTime+ }+ Right _ -> return ProcessTerminated'+ { processId+ , processName+ , monitorEventTime+ , processTerminationReason+ , processRestartCount = restartCount+ }++ Just BrutallyTerminateProcessException { processTerminationReason } ->+ return ProcessTerminated'+ { processId+ , processName+ , monitorEventTime+ , processTerminationReason+ , processRestartCount = restartCount+ }++ -- This exception was an error from the given sub-routine+ _ -> do+ eErrResult <- try $ unmask $ callProcessOnFailure procSpec err++ notifyEvent ProcessCallbackExecuted+ { supervisorId+ , supervisorName+ , processId+ , processName+ , processType = getProcessType procSpec+ , processThreadId+ , processCallbackError = either Just (const Nothing) eErrResult+ , processCallbackType = OnFailure+ , eventTime = monitorEventTime+ }++ case eErrResult of+ Left processCallbackError -> return ProcessFailed'+ { processId+ , processName+ , monitorEventTime+ , processRestartCount = restartCount+ , processError = toException ProcessCallbackFailed+ { processId+ , processCallbackError+ , processCallbackType = OnFailure+ , processError = Just err+ }+ }+ Right _ -> return ProcessFailed'+ { processId+ , processName+ , processError = err+ , processRestartCount = restartCount+ , monitorEventTime+ }++-- | Handles completion of a Process sub-routine.+handleProcessCompletion+ :: (IO () -> IO a)+ -> ParentSupervisorEnv+ -> ProcessSpec+ -> ProcessId+ -> RestartCount+ -> IO MonitorEvent+handleProcessCompletion unmask ParentSupervisorEnv { supervisorId, supervisorName, notifyEvent } procSpec processId restartCount+ = do+ let processName = getProcessName procSpec+ processThreadId <- myThreadId+ monitorEventTime <- getCurrentTime+ eCompResult <- try $ unmask $ callProcessOnCompletion procSpec++ notifyEvent ProcessCallbackExecuted+ { supervisorId+ , supervisorName+ , processId+ , processName+ , processType = getProcessType procSpec+ , processThreadId+ , processCallbackError = either Just (const Nothing) eCompResult+ , processCallbackType = OnCompletion+ , eventTime = monitorEventTime+ }++ case eCompResult of+ Left err -> return ProcessFailed'+ { processId+ , processName+ , processError = toException ProcessCallbackFailed+ { processId+ , processCallbackError = err+ , processError = Nothing+ , processCallbackType = OnCompletion+ }+ , processRestartCount = restartCount+ , monitorEventTime+ }+ Right _ ->+ return ProcessCompleted' {processName , processId , monitorEventTime }++-- | Internal utility function to trigger termination of a Process.+--+-- NOTE: The difference between public's API function and this, is that this+-- function gets executed on the supervisor's thread.+--+terminateProcess+ :: Text -- ^ Description that indicates _why_ there is a termination+ -> SupervisorEnv+ -> Process+ -> IO ()+terminateProcess processTerminationReason env process = do+ case process of+ WorkerProcess worker -> terminateWorker processTerminationReason worker+ SupervisorProcess supervisor ->+ terminateSupervisor processTerminationReason supervisor++ notifyProcessTerminated env process processTerminationReason++-- | Internal utility function that manages execution of a termination policy+-- for a Worker.+terminateWorker :: Text -> Worker -> IO ()+terminateWorker processTerminationReason Worker { workerId, workerOptions, workerAsync }+ = do+ let processId = workerId+ WorkerOptions { workerTerminationPolicy } = workerOptions+ case workerTerminationPolicy of+ Infinity -> cancelWith+ workerAsync+ TerminateProcessException {processId , processTerminationReason }++ BrutalTermination -> cancelWith+ workerAsync+ BrutallyTerminateProcessException+ { processId+ , processTerminationReason+ }++ TimeoutMillis millis -> race_+ ( do+ threadDelay (millis * 1000)+ cancelWith+ workerAsync+ BrutallyTerminateProcessException+ { processId+ , processTerminationReason+ }+ )+ ( cancelWith+ workerAsync+ TerminateProcessException {processId , processTerminationReason }+ )++-- | Internal utility function that manages execution of a termination policy+-- for a Supervisor.+terminateSupervisor :: Text -> Supervisor -> IO ()+terminateSupervisor processTerminationReason Supervisor { supervisorId = processId, supervisorAsync }+ = cancelWith+ supervisorAsync+ TerminateProcessException {processId , processTerminationReason }++-- | Internal sub-routine that terminates workers of a supervisor, used when a+-- supervisor instance is terminated.+terminateProcessMap :: Text -> SupervisorEnv -> IO ()+terminateProcessMap terminationReason env@SupervisorEnv { supervisorId, supervisorName, supervisorProcessTerminationOrder, notifyEvent }+ = do+ eventTime <- getCurrentTime+ processMap <- readProcessMap env++ let processList = sortProcessesByTerminationOrder+ supervisorProcessTerminationOrder+ processMap++ notifyEvent ProcessTerminationStarted+ { supervisorId+ , supervisorName+ , terminationReason+ , eventTime+ }++ forM_ processList (terminateProcess terminationReason env)++ notifyEvent ProcessTerminationFinished+ { supervisorId+ , supervisorName+ , terminationReason+ , eventTime+ }
+ src/Control/Concurrent/Capataz/Internal/Supervisor.hs view
@@ -0,0 +1,563 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Control.Concurrent.Capataz.Internal.Supervisor where++import Control.Concurrent.Async (asyncWithUnmask)+import Control.Concurrent.STM.TQueue (newTQueueIO, readTQueue, writeTQueue)+import Control.Concurrent.STM.TVar (newTVarIO)+import Data.IORef (newIORef)+import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime)+import Protolude++import qualified Data.HashMap.Strict as HashMap+import qualified Data.UUID.V4 as UUID++import Control.Concurrent.Capataz.Internal.Types++import qualified Control.Concurrent.Capataz.Internal.Process as Process+import qualified Control.Concurrent.Capataz.Internal.Util as Util+import qualified Control.Concurrent.Capataz.Internal.Worker as Worker++-- | Internal function that forks a supervisor thread; note this is different+-- from the public @forkSupervisor@ function which sends a message to the+-- supervisor loop.+forkSupervisor+ :: ParentSupervisorEnv+ -> SupervisorOptions+ -> Maybe (ProcessId, RestartCount)+ -> IO Supervisor+forkSupervisor parentEnv supervisorOptions mRestartInfo = do+ (supervisorId, restartCount) <- case mRestartInfo of+ Just (supervisorId, restartCount) -> pure (supervisorId, restartCount)+ Nothing -> (,) <$> UUID.nextRandom <*> pure 0+++ supervisor <- supervisorMain parentEnv+ supervisorOptions+ supervisorId+ restartCount++ Process.notifyProcessStarted mRestartInfo+ parentEnv+ (SupervisorProcess supervisor)+ return supervisor++-- | Utility function that builds an utility record which is used on all+-- internal APIs of the supervision logic.+buildSupervisorEnv+ :: (CapatazEvent -> IO ())+ -> (SupervisorMessage -> IO ())+ -> STM SupervisorMessage+ -> SupervisorId+ -> SupervisorOptions+ -> IO SupervisorEnv+buildSupervisorEnv notifyEvent supervisorNotify supervisorGetNotification supervisorId supervisorOptions@SupervisorOptions {..}+ = do+ supervisorProcessMap <- newIORef mempty+ supervisorStatusVar <- newTVarIO Initializing+ return SupervisorEnv {..}++-- | Handles an event produced by one of the processes this supervisor monitors.+handleMonitorEvent :: SupervisorEnv -> MonitorEvent -> IO Bool+handleMonitorEvent env monitorEv = do+ case monitorEv of+ ProcessForcedRestart{} ->+ -- We do nothing, as restart is being handled on restartWorkers+ -- sub-routine+ return ()++ ProcessCompleted' { processId, monitorEventTime } ->+ handleProcessCompleted env processId monitorEventTime++ ProcessFailed' { processId, processError, processRestartCount } ->+ handleProcessFailed env processId processError processRestartCount++ ProcessTerminated' { processId, processRestartCount, processTerminationReason }+ -> handleProcessTerminated env+ processId+ processTerminationReason+ processRestartCount+++ return True++-- | Handles an action triggered by the public Capataz API.+handleControlAction :: SupervisorEnv -> ControlAction -> IO Bool+handleControlAction env controlAction = case controlAction of+ ForkWorker { workerOptions, returnWorkerId } -> do+ worker@Worker { workerId } <-+ Worker.forkWorker (Util.toParentSupervisorEnv env) workerOptions Nothing+ Util.appendProcessToMap env (WorkerProcess worker)+ returnWorkerId workerId+ return True++ ForkSupervisor { supervisorOptions, returnSupervisor } -> do+ supervisor <- forkSupervisor (Util.toParentSupervisorEnv env)+ supervisorOptions+ Nothing+ Util.appendProcessToMap env (SupervisorProcess supervisor)+ returnSupervisor supervisor+ return True++ TerminateProcess { processId, processTerminationReason, notifyProcessTermination }+ -> do+ mProcess <- Util.fetchProcess env processId+ case mProcess of+ Just process -> do+ Process.terminateProcess processTerminationReason env process+ notifyProcessTermination True+ return True+ _ -> do+ notifyProcessTermination False+ return True++-- | Executes the shutdown operation of a Supervisor, including the termination+-- of its supervised processes.+haltSupervisor :: Text -> SupervisorEnv -> IO ()+haltSupervisor reason env = do+ Util.writeSupervisorStatus env Halting+ Process.terminateProcessMap reason env+ Util.resetProcessMap env (const HashMap.empty)+ Util.writeSupervisorStatus env Halted+++-- | Handles all messages that a Supervisor can receive from its monitored+-- processes or from the public API.+handleSupervisorMessage :: SupervisorEnv -> SupervisorMessage -> IO Bool+handleSupervisorMessage env message = case message of+ ControlAction controlAction -> handleControlAction env controlAction+ MonitorEvent monitorEvent -> handleMonitorEvent env monitorEvent++-- | This sub-routine executes the main thread loop of a "Supervisor" instance.+supervisorLoop+ :: (forall b . IO b -> IO b)+ -> ParentSupervisorEnv+ -> SupervisorEnv+ -> RestartCount+ -> IO ()+supervisorLoop unmask parentEnv@ParentSupervisorEnv { supervisorId, supervisorName, supervisorNotify = notifyParentSupervisor } env@SupervisorEnv { supervisorId = processId, supervisorName = processName, supervisorOptions, supervisorStatusVar, supervisorGetNotification, notifyEvent } restartCount+ = do+ processThreadId <- myThreadId+ loopResult <-+ unmask+ $ try+ $ atomically+ $ (,)+ <$> Util.readSupervisorStatusSTM supervisorStatusVar+ <*> supervisorGetNotification++ case loopResult of+ Left supervisorError -> do+ haltSupervisor (show supervisorError) env+ result <- Process.handleProcessException+ unmask+ parentEnv+ (SupervisorSpec supervisorOptions)+ processId+ restartCount+ supervisorError+ notifyParentSupervisor (MonitorEvent result)++ Right (status, message) -> case status of+ Initializing -> do+ eventTime <- getCurrentTime+ notifyEvent InvalidSupervisorStatusReached+ { supervisorId+ , supervisorName+ , eventTime+ }+ supervisorLoop unmask parentEnv env restartCount++ Running -> do+ eContinueLoop <- try $ unmask $ handleSupervisorMessage env message+ case eContinueLoop of+ Left supervisorError -> do+ haltSupervisor (show supervisorError) env+ result <- Process.handleProcessException+ unmask+ parentEnv+ (SupervisorSpec supervisorOptions)+ processId+ restartCount+ supervisorError+ notifyParentSupervisor (MonitorEvent result)++ Right continueLoop+ | continueLoop -> supervisorLoop unmask parentEnv env restartCount+ | otherwise -> do+ eventTime <- getCurrentTime+ notifyEvent ProcessTerminated+ { supervisorId+ , supervisorName+ , eventTime+ , processId+ , processName+ , processThreadId+ , processType = SupervisorType+ , terminationReason = "Supervisor normal termination"+ }++ Halting ->+ -- Discard messages when halting+ return ()++ Halted ->+ -- Discard messages when halted+ return ()++-- | This sub-routine starts a Supervisor thread and initializes its+-- processList.+supervisorMain+ :: ParentSupervisorEnv+ -> SupervisorOptions+ -> SupervisorId+ -> RestartCount+ -> IO Supervisor+supervisorMain parentEnv@ParentSupervisorEnv { notifyEvent } supervisorOptions@SupervisorOptions { supervisorName, supervisorProcessSpecList } supervisorId restartCount+ = do+ supervisorCreationTime <- getCurrentTime++ supervisorQueue <- newTQueueIO+ let supervisorNotify = atomically . writeTQueue supervisorQueue+ supervisorGetNotification = readTQueue supervisorQueue++ supervisorEnv@SupervisorEnv{} <- buildSupervisorEnv+ notifyEvent+ supervisorNotify+ supervisorGetNotification+ supervisorId+ supervisorOptions++ supervisorAsync <- asyncWithUnmask $ \unmask -> do+ Util.setProcessThreadName supervisorId supervisorName+ supervisorLoop unmask parentEnv supervisorEnv restartCount++ forM_+ supervisorProcessSpecList+ ( \processSpec -> case processSpec of+ WorkerSpec workerOptions -> do+ worker <- Worker.forkWorker (Util.toParentSupervisorEnv supervisorEnv) workerOptions Nothing+ Util.appendProcessToMap supervisorEnv (WorkerProcess worker)++ SupervisorSpec childSupervisorOptions -> do+ supervisor <- forkSupervisor+ (Util.toParentSupervisorEnv supervisorEnv)+ childSupervisorOptions+ Nothing+ Util.appendProcessToMap supervisorEnv (SupervisorProcess supervisor)+ )++ Util.writeSupervisorStatus supervisorEnv Running++ return Supervisor+ { supervisorId+ , supervisorName+ , supervisorAsync+ , supervisorOptions+ , supervisorEnv+ , supervisorNotify+ , supervisorCreationTime+ }++--------------------------------------------------------------------------------++-- | Tracks difference between two timestamps so that we keep track of a+-- Supervisor error intensity.+calcDiffSeconds :: UTCTime -> IO NominalDiffTime+calcDiffSeconds creationTime = do+ currentTime <- getCurrentTime+ return $ diffUTCTime currentTime creationTime++-- | Checks restart counts and worker start times to assess if the Supervisor+-- error intensity has been breached, see "ProcessRestartAction" for possible+-- outcomes.+calcRestartAction+ :: SupervisorEnv -> Int -> NominalDiffTime -> ProcessRestartAction+calcRestartAction SupervisorEnv { supervisorIntensity, supervisorPeriodSeconds } restartCount diffSeconds+ = case () of+ _+ | diffSeconds+ < supervisorPeriodSeconds+ && restartCount+ >= supervisorIntensity+ -> HaltSupervisor+ | diffSeconds > supervisorPeriodSeconds+ -> ResetRestartCount+ | otherwise+ -> IncreaseRestartCount++-- | Sub-routine responsible of executing a "SupervisorRestartStrategy".+execCapatazRestartStrategy+ :: SupervisorEnv -> ProcessId -> ProcessSpec -> Int -> IO ()+execCapatazRestartStrategy supervisorEnv@SupervisorEnv { supervisorRestartStrategy } processId processSpec processRestartCount+ = case supervisorRestartStrategy of+ AllForOne -> do+ newProcessList <- restartProcessList supervisorEnv+ processId+ processRestartCount+ let newProcessMap =+ newProcessList+ & fmap (\process -> (Util.getProcessId process, process))+ & HashMap.fromList+ Util.resetProcessMap supervisorEnv (const newProcessMap)++ OneForOne -> do+ Util.removeProcessFromMap supervisorEnv processId+ newProcess <- case processSpec of+ WorkerSpec workerOptions -> restartWorker supervisorEnv+ workerOptions+ processId+ processRestartCount++ SupervisorSpec supervisorOptions -> restartSupervisor+ (Util.toParentSupervisorEnv supervisorEnv)+ supervisorOptions+ processId+ processRestartCount++ Util.appendProcessToMap supervisorEnv newProcess++-- | Executes a restart action returned from the invokation of+-- "calcRestartAction".+execRestartAction+ :: SupervisorEnv+ -> ProcessId+ -> ProcessSpec+ -> Text+ -> UTCTime+ -> Int+ -> IO ()+execRestartAction supervisorEnv@SupervisorEnv { supervisorOnIntensityReached } processId processSpec processName processCreationTime processRestartCount+ = do+ restartAction <- calcRestartAction supervisorEnv processRestartCount+ <$> calcDiffSeconds processCreationTime++ case restartAction of+ HaltSupervisor -> do+ -- skip exceptions on callback+ (_ :: Either SomeException ()) <- try supervisorOnIntensityReached+ throwIO SupervisorIntensityReached+ { processId+ , processName+ , processRestartCount+ }++ ResetRestartCount ->+ execCapatazRestartStrategy supervisorEnv processId processSpec 0++ IncreaseRestartCount -> execCapatazRestartStrategy+ supervisorEnv+ processId+ processSpec+ (succ processRestartCount)+++--------------------------------------------------------------------------------++-- | Restarts _all_ processes that are supervised by Supervisor, invoked when+-- one worker green thread fails and causes sibling process threads to get+-- restarted as well (e.g. "AllForOne" supervisor restart strategy).+restartProcessList :: SupervisorEnv -> WorkerId -> RestartCount -> IO [Process]+restartProcessList supervisorEnv@SupervisorEnv { supervisorProcessTerminationOrder } failingProcessId restartCount+ = do+ processMap <- Util.readProcessMap supervisorEnv++ let processList = Util.sortProcessesByTerminationOrder+ supervisorProcessTerminationOrder+ processMap++ newProcessList <- forM processList $ \process -> do+ unless (failingProcessId == Process.getProcessId process)+ $ forceRestartProcess supervisorEnv process++ case process of+ WorkerProcess Worker { workerId, workerOptions } -> do+ let WorkerOptions { workerRestartStrategy } = workerOptions+ case workerRestartStrategy of+ Temporary -> return Nothing+ _ ->+ Just+ <$> restartWorker supervisorEnv+ workerOptions+ workerId+ restartCount++ SupervisorProcess Supervisor { supervisorId, supervisorOptions } ->+ Just+ <$> restartSupervisor (Util.toParentSupervisorEnv supervisorEnv)+ supervisorOptions+ supervisorId+ restartCount+++ return $ catMaybes newProcessList++-- | Sub-routine that is used when there is a restart request sent to a Process+-- caused by an "AllForOne" restart from a failing sibling process.+forceRestartProcess :: SupervisorEnv -> Process -> IO ()+forceRestartProcess env process = do+ Process.notifyProcessTerminated env process "forced restart"+ cancelWith (Process.getProcessAsync process) RestartProcessException++-- | Starts a new worker thread taking into account an existing "WorkerId" and+-- keeping a "RestartCount" to manage the Supervisor error intensity.+restartWorker+ :: SupervisorEnv -> WorkerOptions -> WorkerId -> RestartCount -> IO Process+restartWorker supervisorEnv workerOptions workerId restartCount =+ WorkerProcess <$> Worker.forkWorker (Util.toParentSupervisorEnv supervisorEnv)+ workerOptions+ (Just (workerId, restartCount))++-- | Starts a new Supervisor thread taking into account an existing+-- "SupervisorId" and keeping a "RestartCount" to manage the parent Supervisor+-- error intensity.+restartSupervisor+ :: ParentSupervisorEnv+ -> SupervisorOptions+ -> ProcessId+ -> RestartCount+ -> IO Process+restartSupervisor parentEnv supervisorOptions processId restartCount =+ SupervisorProcess <$> forkSupervisor parentEnv+ supervisorOptions+ (Just (processId, restartCount))+++--------------------------------------------------------------------------------++-- | Executes restart strategy for when a worker finishes it execution because+-- of a completion (e.g. worker sub-routine finished without any errors).+handleWorkerCompleted :: SupervisorEnv -> Worker -> IO ()+handleWorkerCompleted env worker = do+ let Worker { workerId, workerOptions, workerCreationTime } = worker+ WorkerOptions { workerName, workerRestartStrategy } = workerOptions+ case workerRestartStrategy of+ Permanent -> do+ -- NOTE: Completed workers should never account as errors happening on+ -- a supervised thread, ergo, they should be restarted every time.++ -- TODO: Notify a warning around having a workerRestartStrategy different+ -- than Temporary on workers that may complete.+ let restartCount = 0+ execRestartAction env+ workerId+ (WorkerSpec workerOptions)+ workerName+ workerCreationTime+ restartCount++ _ -> Util.removeProcessFromMap env workerId++-- | Executes restart strategy for when a process finishes it execution because+-- of a completion (e.g. worker sub-routine finished without any errors).+handleProcessCompleted :: SupervisorEnv -> ProcessId -> UTCTime -> IO ()+handleProcessCompleted env processId completionTime = do+ mProcess <- Util.fetchProcess env processId+ case mProcess of+ Nothing -> return ()++ Just process -> do+ Process.notifyProcessCompleted env process completionTime+ case process of+ WorkerProcess worker -> handleWorkerCompleted env worker+ _ ->+ panic+ $ "ERROR: Supervisor ("+ <> show (Process.getProcessId process)+ <> ") should never complete"++-- | Executes restart strategy for when a worker finishes it execution because+-- of a failure.+handleWorkerFailed :: SupervisorEnv -> Worker -> Int -> IO ()+handleWorkerFailed env worker restartCount = do+ let Worker { workerId, workerCreationTime, workerOptions } = worker+ WorkerOptions { workerName, workerRestartStrategy } = workerOptions+ case workerRestartStrategy of+ Temporary -> Util.removeProcessFromMap env workerId+ _ -> execRestartAction env+ workerId+ (WorkerSpec workerOptions)+ workerName+ workerCreationTime+ restartCount++-- | Executes restart strategy for when a supervisor finishes it execution because+-- of a failure.+handleSupervisorFailed :: SupervisorEnv -> Supervisor -> Int -> IO ()+handleSupervisorFailed env supervisor restartCount = do+ let Supervisor { supervisorId, supervisorCreationTime, supervisorOptions } =+ supervisor+ SupervisorOptions { supervisorName } = supervisorOptions+ execRestartAction env+ supervisorId+ (SupervisorSpec supervisorOptions)+ supervisorName+ supervisorCreationTime+ restartCount++-- | Executes restart strategy for when a process finishes it execution because+-- of a failure.+handleProcessFailed+ :: SupervisorEnv -> WorkerId -> SomeException -> Int -> IO ()+handleProcessFailed env processId processError restartCount = do+ mProcess <- Util.fetchProcess env processId+ case mProcess of+ Nothing -> return ()+ Just process -> do+ Process.notifyProcessFailed env process processError+ case process of+ WorkerProcess worker -> handleWorkerFailed env worker restartCount++ SupervisorProcess supervisor ->+ handleSupervisorFailed env supervisor restartCount++-- | Executes restart strategy for when a worker finishes it execution because+-- of a termination from its supervisor.+handleWorkerTerminated :: SupervisorEnv -> Worker -> Int -> IO ()+handleWorkerTerminated env worker restartCount = do+ let Worker { workerId, workerCreationTime, workerOptions } = worker+ WorkerOptions { workerName, workerRestartStrategy } = workerOptions++ case workerRestartStrategy of+ Permanent -> execRestartAction env+ workerId+ (WorkerSpec workerOptions)+ workerName+ workerCreationTime+ restartCount++ _ -> Util.removeProcessFromMap env workerId++-- | Executes restart strategy for when a supervisor finishes it execution+-- because of a termination from its parent supervisor.+handleSupervisorTerminated :: SupervisorEnv -> Supervisor -> Int -> IO ()+handleSupervisorTerminated env supervisor restartCount = do+ let Supervisor { supervisorId, supervisorCreationTime, supervisorOptions } =+ supervisor+ SupervisorOptions { supervisorName } = supervisorOptions+ execRestartAction env+ supervisorId+ (SupervisorSpec supervisorOptions)+ supervisorName+ supervisorCreationTime+ restartCount++-- | Executes restart strategy for when a process finishes it execution because+-- of a termination from its supervisor.+handleProcessTerminated :: SupervisorEnv -> ProcessId -> Text -> Int -> IO ()+handleProcessTerminated env processId terminationReason restartCount = do+ mProcess <- Util.fetchProcess env processId+ case mProcess of+ Nothing -> return ()+ Just process -> do+ Process.notifyProcessTerminated env process terminationReason+ case process of+ WorkerProcess worker -> handleWorkerTerminated env worker restartCount++ SupervisorProcess supervisor ->+ handleSupervisorTerminated env supervisor restartCount
+ src/Control/Concurrent/Capataz/Internal/Types.hs view
@@ -0,0 +1,694 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-| This module contains all the types used across all the other modules -}+module Control.Concurrent.Capataz.Internal.Types where++import Protolude++import Control.Concurrent.STM.TVar (TVar)+import Control.Teardown (ITeardown (..), Teardown)+import Data.Default (Default (..))+import Data.HashMap.Strict (HashMap)+import Data.IORef (IORef)+import Data.Time.Clock (NominalDiffTime, UTCTime)+import Data.UUID (UUID)++type CapatazId = UUID+type WorkerId = UUID+type SupervisorId = UUID+type ProcessId = UUID+type WorkerAction = IO ()+type ProcessThreadId = ThreadId+type ProcessName = Text+type CapatazName = Text+type SupervisorName = Text+type WorkerName = Text+type RestartCount = Int+type ProcessMap = HashMap ProcessId Process+type ParentSupervisor = Supervisor++-- | Event delivered to the "notifyEvent" callback sub-routine; these events can+-- be used to monitor the capataz system and track what is doing, providing high+-- levels of telemetry for all supervisors and workers of a capataz system,+-- ergo, should be used for logging, monitoring and testing purposes.+data CapatazEvent+ = InvalidSupervisorStatusReached {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , eventTime :: !UTCTime+ }+ | SupervisorStatusChanged {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , prevSupervisorStatus :: !SupervisorStatus+ , newSupervisorStatus :: !SupervisorStatus+ , eventTime :: !UTCTime+ }+ | ProcessTerminated {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , processThreadId :: !ProcessThreadId+ , processId :: !ProcessId+ , processName :: !ProcessName+ , processType :: !ProcessType+ , terminationReason :: !Text+ , eventTime :: !UTCTime+ }+ | ProcessStarted {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , processThreadId :: !ProcessThreadId+ , processId :: !ProcessId+ , processName :: !ProcessName+ , processType :: !ProcessType+ , eventTime :: !UTCTime+ }+ | ProcessRestarted {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , processThreadId :: !ProcessThreadId+ , processId :: !ProcessId+ , processName :: !ProcessName+ , processType :: !ProcessType+ , processRestartCount :: !Int+ , eventTime :: !UTCTime+ }+ | ProcessCompleted {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , processThreadId :: !ProcessThreadId+ , processId :: !ProcessId+ , processName :: !ProcessName+ , processType :: !ProcessType+ , eventTime :: !UTCTime+ }+ | ProcessFailed {+ supervisorName :: !SupervisorName+ , supervisorId :: !SupervisorId+ , processThreadId :: !ProcessThreadId+ , processId :: !ProcessId+ , processName :: !ProcessName+ , processType :: !ProcessType+ , processError :: !SomeException+ , eventTime :: !UTCTime+ }+ | ProcessCallbackExecuted {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , processThreadId :: !ProcessThreadId+ , processId :: !ProcessId+ , processName :: !ProcessName+ , processType :: !ProcessType+ , processCallbackError :: !(Maybe SomeException)+ , processCallbackType :: !CallbackType+ , eventTime :: !UTCTime+ }+ | ProcessTerminationStarted {+ supervisorName :: !SupervisorName+ , supervisorId :: !SupervisorId+ , terminationReason :: !Text+ , eventTime :: !UTCTime+ }+ | ProcessTerminationFinished {+ supervisorName :: !SupervisorName+ , supervisorId :: !SupervisorId+ , terminationReason :: !Text+ , eventTime :: !UTCTime+ }+ | CapatazFailed {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , supervisorError :: !SomeException+ , eventTime :: !UTCTime+ }+ | CapatazTerminated {+ supervisorName :: !SupervisorName+ , supervisorId :: !SupervisorId+ , eventTime :: !UTCTime+ }+ deriving (Generic, Show)++-- | Defines how a "Worker" process termination should be handled by its+-- supervisor.+data WorkerTerminationPolicy+ -- | Supervisor waits until infinity for the worker termination callback to+ -- finish execution.+ = Infinity++ -- | Supervisor terminates worker process without a chance to call its+ -- termination callback.+ | BrutalTermination++ -- | Supervisor allows a number of milliseconds for worker termination+ -- callback complete, if not completed by specified milliseconds the+ -- termination is cancelled via a "BrutalTermination" signal.+ | TimeoutMillis !Int+ deriving (Generic, Show, Eq, Ord)++instance Default WorkerTerminationPolicy where+ -- | Default worker termination is a timeout of three (3) seconds.+ def = TimeoutMillis 3000++instance NFData WorkerTerminationPolicy++-- | Internal helper record that assesses if a Supervisor error intensity has+-- been breached.+data ProcessRestartAction+ -- | Indicates a Supervisor to restart a failed process _and_ reset the+ -- restart count given this Supervisor's intensity period timeout has passed.+ = ResetRestartCount++ -- | Indicates a Supervisor to restart the failed process _and_ increase the restart+ -- count (normal operation) of the supervised process.+ | IncreaseRestartCount++ -- | Indicates a Supervisor stop executing given the error intensity has been+ -- breached.+ | HaltSupervisor+ deriving (Generic, Show, Eq)++instance NFData ProcessRestartAction++-- | Specifies the order in which supervised process should be terminated by a+-- Supervisor in case of a restart or shutdown.+data ProcessTerminationOrder+ -- | Supervisor terminates supervised process from most recent to oldest.+ = NewestFirst+ -- | Supervisor terminates supervised process from oldest to most recent.+ | OldestFirst+ deriving (Generic, Show, Eq, Ord)++instance Default ProcessTerminationOrder where+ -- | default is "OldestFirst".+ def = OldestFirst++instance NFData ProcessTerminationOrder++-- | Specifies how a Supervisor restarts a failing process.+data SupervisorRestartStrategy+ -- | Supervisor terminates all sibling supervised processes that didn't fail,+ -- and then restarts all of them together. This strategy serves best when all+ -- processes depend upon each other.+ = AllForOne++ -- | Supervisor only restarts the supervised process that failed.+ | OneForOne+ deriving (Generic, Show, Eq, Ord)++instance Default SupervisorRestartStrategy where+ -- | Default restart strategy is "OneForOne".+ def = OneForOne++instance NFData SupervisorRestartStrategy++-- | Allows to:+--+-- * Specify options for The root supervisor of a capataz system.+--+-- * Provie a "notifyEvent" callback to monitor or log a capataz system.+--+data CapatazOptions+ = CapatazOptions {+ supervisorIntensity :: !Int+ , supervisorPeriodSeconds :: !NominalDiffTime+ , supervisorRestartStrategy :: !SupervisorRestartStrategy+ , supervisorProcessSpecList :: ![ProcessSpec]+ , supervisorProcessTerminationOrder :: !ProcessTerminationOrder+ , supervisorOnIntensityReached :: !(IO ())+ -- | Callback sub-routine that gets executed when the root supervisor fails.+ , supervisorOnFailure :: !(SomeException -> IO ())+ -- | Callback used for telemetry purposes.+ , notifyEvent :: !(CapatazEvent -> IO ())+ }+++-- | Specifies how a Supervisor deals with the lifecycle of worker process in+-- case of completion without errors and failure.+data WorkerRestartStrategy+ -- | Supervisor will __always__ restart a worker process, in both completion+ -- and failure scenarios.+ = Permanent++ -- | Supervisor will __only__ restart worker process if it has a failure in+ -- execution.+ | Transient++ -- | Supervisor will __never__ restart a worker, even on failure.+ | Temporary++ deriving (Generic, Show, Eq)++instance NFData WorkerRestartStrategy+instance Default WorkerRestartStrategy where+ -- | A worker default restart strategy is "Transient".+ def = Transient++-- | Specifies all options that can be used to create a Worker Process. You may+-- create a record of this type via the smart constructor "buildWorkerOptions".+data WorkerOptions+ = WorkerOptions {+ -- | An @IO ()@ sub-routine that will be executed when the worker+ -- thread is created, this attribute is lazy given we want to this+ -- value on a worker thread environment.+ workerAction :: WorkerAction+ -- | Name of the Worker (present on "CapatazEvent" records)+ , workerName :: !WorkerName+ -- | Callback used when the worker fails with an error+ , workerOnFailure :: !(SomeException -> IO ())+ -- | Callback used when the worker completes execution without error+ , workerOnCompletion :: !(IO ())+ -- | Callback used when the worker is terminated+ , workerOnTermination :: !(IO ())+ -- | Indicates how a worker should be terminated+ , workerTerminationPolicy :: !WorkerTerminationPolicy+ -- | Indicates how a worker should be restarted+ , workerRestartStrategy :: !WorkerRestartStrategy+ }+ deriving (Generic)++-- | Record that contains the "Async" record (thread reference) of a worker+data Worker+ = Worker {+ -- | Unique identifier for a worker that is executing+ workerId :: !WorkerId+ -- | "Async" thread of a worker, this Async executes the @IO ()@ sub-routine+ , workerAsync :: !(Async ())+ -- | Time where this worker was created (used for error intensity checks)+ , workerCreationTime :: !UTCTime+ -- | Name of the Worker (present on "CapatazEvent" records)+ , workerName :: !WorkerName+ -- | "WorkerOptions" contains all the options around restart and termination+ -- policies+ , workerOptions :: !WorkerOptions+ }++data ProcessEnv+ = ProcessEnv {+ processId :: !ProcessId+ , processName :: !ProcessName+ , processAsync :: !(Async ())+ , processCreationTime :: !UTCTime+ , processRestartStrategy :: !WorkerRestartStrategy+ }++data SupervisorOptions+ = SupervisorOptions {+ -- | Name of the Supervisor (present on "CapatazEvent" records)+ supervisorName :: Text+ -- | How many errors is the Supervisor be able to handle; check:+ -- http://erlang.org/doc/design_principles/sup_princ.html#max_intensity+ , supervisorIntensity :: !Int+ -- | Period of time where the Supervisor can receive "supervisorIntensity" amount+ -- of errors+ , supervisorPeriodSeconds :: !NominalDiffTime+ -- | What is the "SupervisorRestartStrategy" for this Capataz+ , supervisorRestartStrategy :: !SupervisorRestartStrategy+ -- | Static set of workers that start as soon as the "Capataz" is created+ , supervisorProcessSpecList :: ![ProcessSpec]+ -- | In which order the "Supervisor" record is going to terminate it's workers+ , supervisorProcessTerminationOrder :: !ProcessTerminationOrder+ -- | Callback used when the error intensity is reached+ , supervisorOnIntensityReached :: !(IO ())+ , supervisorOnFailure :: !(SomeException -> IO ())+ }++data Supervisor+ = Supervisor {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , supervisorOptions :: !SupervisorOptions+ , supervisorCreationTime :: !UTCTime+ , supervisorAsync :: !(Async ())+ , supervisorNotify :: SupervisorMessage -> IO ()+ , supervisorEnv :: !SupervisorEnv+ }++-- | Internal record that represents an action being sent from threads using+-- the Capataz public API.+data ControlAction+ = ForkWorker {+ workerOptions :: !WorkerOptions+ , returnWorkerId :: !(WorkerId -> IO ())+ }+ | ForkSupervisor {+ supervisorOptions :: !SupervisorOptions+ , returnSupervisor :: !(Supervisor -> IO ())+ }+ | TerminateProcess {+ processId :: !ProcessId+ , processTerminationReason :: !Text+ , notifyProcessTermination :: !(Bool -> IO ())+ }+ deriving (Generic)++-- | Internal exception thrown to the Capataz loop to indicate termination of+-- execution+data CapatazSignal+ = CapatazFailure+ | RestartProcessException+ | TerminateProcessException {+ processId :: !ProcessId+ , processTerminationReason :: !Text+ }+ | BrutallyTerminateProcessException {+ processId :: !ProcessId+ , processTerminationReason :: !Text+ }+ deriving (Generic, Show)++instance Exception CapatazSignal+instance NFData CapatazSignal++-- | Internal exception triggered when a Worker violates error intensity+-- specification+data CapatazError+ = SupervisorIntensityReached {+ processId :: !ProcessId+ , processName :: !ProcessName+ , processRestartCount :: !Int+ }+ deriving (Generic, Show)++instance Exception CapatazError+instance NFData CapatazError++-- | Internal record that indicates what type of callback function is being+-- invoked; this is used for telemetry purposes+data CallbackType+ = OnCompletion+ | OnFailure+ | OnTermination+ deriving (Generic, Show, Eq)++data ProcessType+ = SupervisorType+ | WorkerType+ deriving (Show, Eq)++-- | Internal exception triggered when a callback of a Worker fails+data ProcessError+ = ProcessCallbackFailed {+ processId :: !WorkerId+ , processError :: !(Maybe SomeException)+ , processCallbackError :: !SomeException+ , processCallbackType :: !CallbackType+ }+ deriving (Generic, Show)++instance Exception ProcessError++-- | Internal event delivered from Worker threads to the Capataz thread to+-- indicate completion, failure or termination+data MonitorEvent+ = ProcessTerminated' {+ processId :: !ProcessId+ , processName :: !ProcessName+ , processRestartCount :: !RestartCount+ , processTerminationReason :: !Text+ , monitorEventTime :: !UTCTime+ }+ | ProcessFailed' {+ processId :: !WorkerId+ , processName :: !WorkerName+ , processRestartCount :: !RestartCount+ , processError :: !SomeException+ , monitorEventTime :: !UTCTime+ }+ | ProcessCompleted' {+ processId :: !ProcessId+ , processName :: !ProcessName+ , monitorEventTime :: !UTCTime+ }+ | ProcessForcedRestart {+ processId :: !ProcessId+ , processName :: !ProcessName+ , monitorEventTime :: !UTCTime+ }+ deriving (Show)++-- | Internal record used as a state machine, indicating the state of a+-- supervisor process+data SupervisorStatus+ -- | This state is set when the process is created and it starts spawning its+ -- static process list.+ = Initializing+ -- | This state is set when the supervisor process starts listenting to both+ -- "ControlAction" and "MonitorEvent" messages.+ | Running+ -- | This state is set when the supervisor process is terminating it's+ -- assigned worker+ | Halting+ -- | This state is set when the supervisor process is finished+ | Halted+ deriving (Generic, Show, Eq)++instance NFData SupervisorStatus++-- | Internal message delivered to a supervisor process that can either be a+-- call from public API or an event from its monitored worker process.+data SupervisorMessage+ -- | Represents a request from done to the supervisor thread from another+ -- thread using the public API+ = ControlAction !ControlAction+ -- | Represents an event (failure, completion, etc) from a monitored worker+ -- process to the supervisor+ | MonitorEvent !MonitorEvent+ deriving (Generic)++-- | Internal Type to manage both Worker and Supervisor processes+data Process+ = WorkerProcess Worker+ | SupervisorProcess Supervisor++-- | Record used to specify how to __build__ a runtime "Process" in a static+-- supervision tree; to create values of this type, you must use:+--+-- * "workerSpec" or "workerSpecWithDefaults" to build a worker process+--+-- * "supervisorSpec" or "supervisorSpecWithDefaults" to build a supervisor+-- process+--+data ProcessSpec+ = WorkerSpec WorkerOptions+ | SupervisorSpec SupervisorOptions++-- | Record that contains the environment of a capataz monitor, this is used as+-- the main record to create workers and to stop the supervisor thread.+data Capataz+ = Capataz {+ capatazSupervisor :: !Supervisor+ , capatazTeardown :: !Teardown+ }++instance ITeardown Capataz where+ teardown Capataz {capatazTeardown} =+ teardown capatazTeardown++-- | Internal utility record used to hold part of the runtime information of a+-- supervisor that acts as a parent of another supervisor.+data ParentSupervisorEnv+ = ParentSupervisorEnv {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , supervisorNotify :: !(SupervisorMessage -> IO ())+ , notifyEvent :: !(CapatazEvent -> IO ())+ }++-- | Convenience internal utility record that contains all values related to a+-- supervisor process.+data SupervisorEnv+ = SupervisorEnv {+ supervisorId :: !SupervisorId+ , supervisorName :: !SupervisorName+ , supervisorNotify :: !(SupervisorMessage -> IO ())+ , supervisorGetNotification :: !(STM SupervisorMessage)+ , supervisorProcessMap :: !(IORef ProcessMap)+ , supervisorStatusVar :: !(TVar SupervisorStatus)+ , supervisorOptions :: !SupervisorOptions+ , supervisorIntensity :: !Int+ -- ^ http://erlang.org/doc/design_principles/sup_princ.html#max_intensity+ , supervisorPeriodSeconds :: !NominalDiffTime+ , supervisorRestartStrategy :: !SupervisorRestartStrategy+ , supervisorProcessTerminationOrder :: !ProcessTerminationOrder+ , supervisorOnIntensityReached :: !(IO ())+ , supervisorOnIntensityReached :: !(SomeException -> IO ())+ , notifyEvent :: !(CapatazEvent -> IO ())+ }++-- | Builds a "CapatazOptions" record with defaults on how to create a capataz+-- root supervisor, these defaults are:+--+-- * Intensity error tolerance is set to 1 error every 5 seconds+--+-- * A "SupervisorRestartStrategy" of "OneForOne"+--+-- * A "ProcessTerminationOrder" of "OldestFirst"+--+-- This function is intended to be used in combination with "forkCapataz".+--+defCapatazOptions+ :: (CapatazOptions -> CapatazOptions) -- ^ Function to modify root supervisor+ -- options+ -> CapatazOptions+defCapatazOptions modFn =+ modFn CapatazOptions+ {+ supervisorIntensity = 2+ , supervisorPeriodSeconds = 5+ , supervisorRestartStrategy = def+ , supervisorProcessSpecList = []+ , supervisorProcessTerminationOrder = OldestFirst+ , supervisorOnIntensityReached = return ()+ , supervisorOnFailure = const $ return ()+ , notifyEvent = const $ return ()+ }++-- | Builds a "ProcessSpec" record for a supervisor process with defaults from+-- "supervisorSpecWithDefaults". This function allows overrides of these+-- defaults using lenses.+--+-- This function is used when building a supervisor branch in a static+-- supervision trees.+--+supervisorSpec+ :: SupervisorName -- ^ Name used for telemetry purposes+ -> (SupervisorOptions -> SupervisorOptions) -- ^ Function to modify default+ -- supervisor options+ -> ProcessSpec+supervisorSpec sName modFn =+ SupervisorSpec (buildSupervisorOptions sName modFn)+{-# INLINE supervisorSpec #-}++-- | Builds a "ProcessSpec" record for a supervisor process with defaults from+-- "buildSupervisorOptionsWithDefaults".+--+-- This function is used when building a supervisor branch in a static+-- supervision trees.+--+supervisorSpecWithDefaults+ :: SupervisorName -- ^ Name used for telemetry purposes+ -> ProcessSpec+supervisorSpecWithDefaults sName =+ supervisorSpec sName identity+{-# INLINE supervisorSpecWithDefaults #-}++-- | Builds a "ProcessSpec" record for a worker process with defaults from+-- "workerSpecWithDefaults". This function allows overrides of these+-- defaults using lenses.+--+-- This function is used when building a worker in a static supervision tree.+--+workerSpec+ :: WorkerName -- ^ Name used for telemetry purposes+ -> (IO ()) -- ^ IO sub-routine to be supervised+ -> (WorkerOptions -> WorkerOptions) -- ^ Function to modify default worker+ -- options+ -> ProcessSpec+workerSpec wName wAction modFn =+ WorkerSpec (buildWorkerOptions wName wAction modFn)+{-# INLINE workerSpec #-}++-- | Builds a "ProcessSpec" record for a worker process with defaults from+-- "buildSupervisorOptionsWithDefaults".+--+-- This function is used when building a worker in a static supervision tree.+--+workerSpecWithDefaults+ :: WorkerName -- ^ Name used for telemetry purposes+ -> (IO ()) -- ^ IO sub-routine to be supervised+ -> ProcessSpec+workerSpecWithDefaults wName wAction =+ workerSpec wName wAction identity+{-# INLINE workerSpecWithDefaults #-}++-- | Builds a "SupervisorOptions" record with defaults from+-- "buildSupervisorOptionsWithDefaults". This function allows overrides of these+-- defaults using lenses.+--+-- This function is intended to be used in combination with "forkSupervisor".+--+buildSupervisorOptions+ :: SupervisorName -- ^ Name used for telemetry purposes+ -> (SupervisorOptions -> SupervisorOptions) -- ^ Function to modify default+ -- supervisor options+ -> SupervisorOptions+buildSupervisorOptions supervisorName modFn = modFn SupervisorOptions+ { supervisorName+ , supervisorIntensity = 2+ , supervisorPeriodSeconds = 5+ , supervisorRestartStrategy = def+ , supervisorProcessSpecList = []+ , supervisorProcessTerminationOrder = OldestFirst+ , supervisorOnIntensityReached = return ()+ , supervisorOnFailure = const $ return ()+ }+{-# INLINE buildSupervisorOptions #-}++-- | Builds a "SupervisorOptions" record with defaults to create a supervisor+-- process, these defaults are:+--+-- * Intensity error tolerance is set to 1 error every 5 seconds+--+-- * A "SupervisorRestartStrategy" of "OneForOne"+--+-- * A "ProcessTerminationOrder" of "OldestFirst"+--+-- This function is intended to be used in combination with "forkSupervisor".+--+buildSupervisorOptionsWithDefaults+ :: SupervisorName -- ^ Name used for telemetry purposes+ -> SupervisorOptions+buildSupervisorOptionsWithDefaults =+ flip buildSupervisorOptions identity+{-# INLINE buildSupervisorOptionsWithDefaults #-}++-- | Builds a "WorkerOptions" record, keeps the defaults from+-- "buildWorkerOptionsWithDefaults" but allows overrides using lenses.+--+-- This function is intended to be used in combination with "forkWorker". See+-- the ... example in the examples directory for a demonstration.+--+buildWorkerOptions+ :: WorkerName -- ^ Name used for telemetry purposes+ -> (IO ()) -- ^ IO sub-routine to be supervised+ -> (WorkerOptions -> WorkerOptions) -- ^ Function to modify default worker+ -- options+ -> WorkerOptions+buildWorkerOptions workerName workerAction f = f WorkerOptions+ { workerName+ , workerAction+ , workerOnFailure = const $ return ()+ , workerOnCompletion = return ()+ , workerOnTermination = return ()+ , workerTerminationPolicy = def+ , workerRestartStrategy = def+ }+{-# INLINE buildWorkerOptions #-}++-- | Builds a "WorkerOptions" record with defaults to create a worker process,+-- the defaults are:+--+-- * A "Transient" "WorkerRestartStrategy"+--+-- * A "WorkerTerminationPolicy" of a 3 seconds timeout+--+-- * A _completion_ callback that just returns unit+--+-- * A _termination_ callback that just returns unit+--+-- * A _failure_ callback that just returns unit+--+-- This function is intended to be used in combination with "forkWorker", for+-- creating a worker in an static supervision tree, use "workerSpecWithDefaults"+-- instead. See the ... example for a demonstration.+--+buildWorkerOptionsWithDefaults+ :: WorkerName -- ^ Name used for telemetry purposes+ -> (IO ()) -- ^ IO sub-routine to be supervised+ -> WorkerOptions+buildWorkerOptionsWithDefaults wName wAction =+ buildWorkerOptions wName wAction identity+{-# INLINE buildWorkerOptionsWithDefaults #-}
+ src/Control/Concurrent/Capataz/Internal/Types/Lens.hs view
@@ -0,0 +1,233 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE RecordWildCards #-}+module Control.Concurrent.Capataz.Internal.Types.Lens where++import Control.Concurrent.Capataz.Internal.Types+import Data.Time.Clock (NominalDiffTime)+import Protolude++--------------------------------------------------------------------------------++class HasSupervisorIntensity s where+ -- | Specifies how many errors is a supervisor able to handle; check:+ -- http://erlang.org/doc/design_principles/sup_princ.html#max_intensity.+ supervisorIntensityL+ :: Functor f+ => (Int -> f Int)+ -> s+ -> f s++class HasSupervisorPeriodSeconds s where+ -- | Specifies period of time in which a supervisor can receive a number of+ -- errors specified in "supervisorIntensityL".+ supervisorPeriodSecondsL+ :: Functor f+ => (NominalDiffTime -> f NominalDiffTime)+ -> s+ -> f s++class HasSupervisorRestartStrategy s where+ -- | Specifies the "SupervisorRestartStrategy" for a root supervisor.+ supervisorRestartStrategyL+ :: Functor f+ => (SupervisorRestartStrategy -> f SupervisorRestartStrategy)+ -> s+ -> f s++class HasSupervisorProcessSpecList s where+ -- | Specifies a static list of processes that start automatically with a+ -- supervisor.+ supervisorProcessSpecListL+ :: Functor f+ => ([ProcessSpec] -> f [ProcessSpec])+ -> s+ -> f s++class HasSupervisorProcessTerminationOrder s where+ -- | Specifies order in which a supervisor is going to terminate its+ -- supervised processes.+ supervisorProcessTerminationOrderL+ :: Functor f+ => (ProcessTerminationOrder -> f ProcessTerminationOrder)+ -> s+ -> f s++class HasSupervisorIntensityReachedCallback s where+ -- | Specifies a callback sub-routine that gets executed when there is a+ -- breach in a supervisor's error intensity.+ supervisorOnIntensityReachedL+ :: Functor f+ => (IO () -> f (IO ()))+ -> s+ -> f s++class HasSupervisorFailureCallback s where+ -- | Specifies callback sub-routine that gets executed when a supervisor+ -- fails.+ supervisorOnFailureL+ :: Functor f+ => ((SomeException -> IO ()) -> f (SomeException -> IO ()))+ -> s+ -> f s+++instance HasSupervisorIntensity SupervisorOptions where+ supervisorIntensityL k SupervisorOptions {supervisorIntensity, ..} =+ fmap (\newSupIntensity -> SupervisorOptions { supervisorIntensity = newSupIntensity, .. })+ (k supervisorIntensity)++instance HasSupervisorPeriodSeconds SupervisorOptions where+ supervisorPeriodSecondsL k SupervisorOptions {supervisorPeriodSeconds, ..} =+ fmap (\newSupPeriodSeconds -> SupervisorOptions { supervisorPeriodSeconds = newSupPeriodSeconds, .. })+ (k supervisorPeriodSeconds)++instance HasSupervisorRestartStrategy SupervisorOptions where+ supervisorRestartStrategyL k SupervisorOptions {supervisorRestartStrategy, ..} =+ fmap (\newSupRestartStrategy ->+ SupervisorOptions { supervisorRestartStrategy = newSupRestartStrategy, .. })+ (k supervisorRestartStrategy)++instance HasSupervisorProcessSpecList SupervisorOptions where+ supervisorProcessSpecListL k SupervisorOptions {supervisorProcessSpecList, ..} =+ fmap (\newSupProcessSpecList ->+ SupervisorOptions { supervisorProcessSpecList = newSupProcessSpecList, .. })+ (k supervisorProcessSpecList)++instance HasSupervisorProcessTerminationOrder SupervisorOptions where+ supervisorProcessTerminationOrderL k SupervisorOptions {supervisorProcessTerminationOrder, ..} =+ fmap (\newSupProcessTerminationOrder ->+ SupervisorOptions { supervisorProcessTerminationOrder = newSupProcessTerminationOrder, .. })+ (k supervisorProcessTerminationOrder)++instance HasSupervisorIntensityReachedCallback SupervisorOptions where+ supervisorOnIntensityReachedL k SupervisorOptions {supervisorOnIntensityReached, ..} =+ fmap (\newSupOnIntensityReached -> SupervisorOptions { supervisorOnIntensityReached = newSupOnIntensityReached, .. })+ (k supervisorOnIntensityReached)++instance HasSupervisorFailureCallback SupervisorOptions where+ supervisorOnFailureL k SupervisorOptions {supervisorOnFailure, ..} =+ fmap (\newSupOnFailure -> SupervisorOptions { supervisorOnFailure = newSupOnFailure, .. })+ (k supervisorOnFailure)++--------------------++instance HasSupervisorIntensity CapatazOptions where+ supervisorIntensityL k CapatazOptions {supervisorIntensity, ..} =+ fmap (\newSupIntensity -> CapatazOptions { supervisorIntensity = newSupIntensity, .. })+ (k supervisorIntensity)++instance HasSupervisorPeriodSeconds CapatazOptions where+ supervisorPeriodSecondsL k CapatazOptions {supervisorPeriodSeconds, ..} =+ fmap (\newSupPeriodSeconds -> CapatazOptions { supervisorPeriodSeconds = newSupPeriodSeconds, .. })+ (k supervisorPeriodSeconds)++instance HasSupervisorRestartStrategy CapatazOptions where+ supervisorRestartStrategyL k CapatazOptions {supervisorRestartStrategy, ..} =+ fmap (\newSupRestartStrategy ->+ CapatazOptions { supervisorRestartStrategy = newSupRestartStrategy, .. })+ (k supervisorRestartStrategy)++instance HasSupervisorProcessSpecList CapatazOptions where+ supervisorProcessSpecListL k CapatazOptions {supervisorProcessSpecList, ..} =+ fmap (\newSupProcessSpecList ->+ CapatazOptions { supervisorProcessSpecList = newSupProcessSpecList, .. })+ (k supervisorProcessSpecList)++instance HasSupervisorProcessTerminationOrder CapatazOptions where+ supervisorProcessTerminationOrderL k CapatazOptions {supervisorProcessTerminationOrder, ..} =+ fmap (\newSupProcessTerminationOrder ->+ CapatazOptions { supervisorProcessTerminationOrder = newSupProcessTerminationOrder, .. })+ (k supervisorProcessTerminationOrder)++instance HasSupervisorIntensityReachedCallback CapatazOptions where+ supervisorOnIntensityReachedL k CapatazOptions {supervisorOnIntensityReached, ..} =+ fmap (\newSupOnIntensityReached -> CapatazOptions { supervisorOnIntensityReached = newSupOnIntensityReached, .. })+ (k supervisorOnIntensityReached)++instance HasSupervisorFailureCallback CapatazOptions where+ supervisorOnFailureL k CapatazOptions {supervisorOnFailure, ..} =+ fmap (\newSupOnFailure -> CapatazOptions { supervisorOnFailure = newSupOnFailure, .. })+ (k supervisorOnFailure)++ -- | Specifies a callback sub-routine that gets triggered everytime something+ -- important happens on the capataz system. This callback should be used for+ -- telemetry purposes (e.g. logging, monitoring, etc).+onSystemEventL+ :: Functor f+ => ((CapatazEvent -> IO ()) -> f (CapatazEvent -> IO ()))+ -> CapatazOptions+ -> f CapatazOptions+onSystemEventL k CapatazOptions { notifyEvent, ..} = fmap+ (\newNotifyEvent -> CapatazOptions {notifyEvent = newNotifyEvent, ..})+ (k notifyEvent)++--------------------++-- | Specifies callback that gets executed when worker sub-routine has runtime+-- error.+--+-- NOTE: the given sub-routine execution may be interrupted depending on the+-- worker "WorkerTerminationPolicy".+--+workerOnFailureL+ :: Functor f+ => ((SomeException -> IO ()) -> f (SomeException -> IO ()))+ -> WorkerOptions+ -> f WorkerOptions+workerOnFailureL k WorkerOptions { workerOnFailure, ..} = fmap+ (\newWorkerAction -> WorkerOptions {workerOnFailure = newWorkerAction, ..})+ (k workerOnFailure)++-- | Specifies callback that gets executed when worker sub-routine completes+-- with no errors.+--+-- NOTE: the given sub-routine execution may be interrupted depending on the+-- worker "WorkerTerminationPolicy".+--+workerOnCompletionL+ :: Functor f => (IO () -> f (IO ())) -> WorkerOptions -> f WorkerOptions+workerOnCompletionL k WorkerOptions { workerOnCompletion, ..} = fmap+ (\newWorkerAction -> WorkerOptions {workerOnCompletion = newWorkerAction, ..})+ (k workerOnCompletion)++-- | Specifies callback that gets executed when worker sub-routine is terminated+-- by its supervisor; this may happen in case of a capataz system shutdown or+-- when there is an "AllForOne" restart policy in place.+--+-- NOTE: the given sub-routine execution may be interrupted depending on the+-- worker "WorkerTerminationPolicy".+--+workerOnTerminationL+ :: Functor f => (IO () -> f (IO ())) -> WorkerOptions -> f WorkerOptions+workerOnTerminationL k WorkerOptions { workerOnTermination, ..} = fmap+ ( \newWorkerAction ->+ WorkerOptions {workerOnTermination = newWorkerAction, ..}+ )+ (k workerOnTermination)++-- | Specifies how to handle a worker termination. See "WorkerTerminationPolicy"+-- documentation for more details.+workerTerminationPolicyL+ :: Functor f+ => (WorkerTerminationPolicy -> f WorkerTerminationPolicy)+ -> WorkerOptions+ -> f WorkerOptions+workerTerminationPolicyL k WorkerOptions { workerTerminationPolicy, ..} = fmap+ ( \newWorkerAction ->+ WorkerOptions {workerTerminationPolicy = newWorkerAction, ..}+ )+ (k workerTerminationPolicy)++-- | Specifies how supervisor should deal with an error when worker fails or+-- completes. See "WorkerRestartStrategy" documentation for more details.+workerRestartStrategyL+ :: Functor f+ => (WorkerRestartStrategy -> f WorkerRestartStrategy)+ -> WorkerOptions+ -> f WorkerOptions+workerRestartStrategyL k WorkerOptions { workerRestartStrategy, ..} = fmap+ ( \newWorkerAction ->+ WorkerOptions {workerRestartStrategy = newWorkerAction, ..}+ )+ (k workerRestartStrategy)
+ src/Control/Concurrent/Capataz/Internal/Util.hs view
@@ -0,0 +1,174 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE RecordWildCards #-}+{-| This module contains:++* Functions to manipulate the state of the Supervisor record+* Utility functions used for communication between threads+* Public API utility functions++-}+module Control.Concurrent.Capataz.Internal.Util where++import Protolude++import Control.Concurrent.STM (STM, atomically, retry)+import Control.Concurrent.STM.TVar (TVar, readTVar, writeTVar)+import Data.IORef (atomicModifyIORef', readIORef)+import qualified Data.Text as T+import Data.Time.Clock (getCurrentTime)++import qualified Data.HashMap.Strict as HashMap+import GHC.Conc (labelThread)++import Control.Concurrent.Capataz.Internal.Types++-- | Returns only the number of the ThreadId.+getTidNumber :: ThreadId -> Maybe Text+getTidNumber tid = case T.words $ show tid of+ (_:tidNumber:_) -> Just tidNumber+ _ -> Nothing++--------------------------------------------------------------------------------++-- | Internal functions that overwrites the GHC thread name, for increasing+-- traceability on GHC internals.+setProcessThreadName :: WorkerId -> WorkerName -> IO ()+setProcessThreadName workerId workerName = do+ tid <- myThreadId+ let workerIdentifier =+ T.unpack workerName <> "_" <> show workerId <> "_" <> maybe+ ""+ T.unpack+ (getTidNumber tid)+ labelThread tid workerIdentifier++-- | Gets the "ProcessId" of both a Worker or Supervisor process.+getProcessId :: Process -> ProcessId+getProcessId process = case process of+ WorkerProcess Worker { workerId } -> workerId+ SupervisorProcess Supervisor { supervisorId } -> supervisorId++-- | Gets a supervised "Process" from a "Supervisor" instance.+fetchProcess :: SupervisorEnv -> ProcessId -> IO (Maybe Process)+fetchProcess SupervisorEnv { supervisorProcessMap } processId = do+ processMap <- readIORef supervisorProcessMap+ case HashMap.lookup processId processMap of+ Just process -> return $ Just process+ _ -> return Nothing++-- | Appends a new "Process" to the "Supervisor" existing process map.+appendProcessToMap :: SupervisorEnv -> Process -> IO ()+appendProcessToMap SupervisorEnv { supervisorProcessMap } process =+ atomicModifyIORef' supervisorProcessMap+ (\processMap -> (appendProcess processMap, ()))+ where+ appendProcess = HashMap.alter (const $ Just process) (getProcessId process)++-- | Removes a "Process" from a "Supervisor" existing process map.+removeProcessFromMap :: SupervisorEnv -> ProcessId -> IO ()+removeProcessFromMap SupervisorEnv { supervisorProcessMap } processId =+ atomicModifyIORef'+ supervisorProcessMap+ ( \processMap -> maybe (processMap, ())+ (const (HashMap.delete processId processMap, ()))+ (HashMap.lookup processId processMap)+ )++-- | Function to modify a "Supervisor" process map using a pure function.+resetProcessMap :: SupervisorEnv -> (ProcessMap -> ProcessMap) -> IO ()+resetProcessMap SupervisorEnv { supervisorProcessMap } processMapFn =+ atomicModifyIORef' supervisorProcessMap+ (\processMap -> (processMapFn processMap, ()))++-- | Function to get a snapshot of a "Supervisor" process map.+readProcessMap :: SupervisorEnv -> IO ProcessMap+readProcessMap SupervisorEnv { supervisorProcessMap } =+ readIORef supervisorProcessMap++-- | Returns all processes of a "Supervisor" by "ProcessTerminationOrder". This+-- is used on "AllForOne" restarts and shutdown operations.+sortProcessesByTerminationOrder+ :: ProcessTerminationOrder -> ProcessMap -> [Process]+sortProcessesByTerminationOrder terminationOrder processMap =+ case terminationOrder of+ OldestFirst -> workers+ NewestFirst -> reverse workers+ where+ -- NOTE: dissambiguates workerCreationTime field+ processCreationTime (WorkerProcess Worker { workerCreationTime }) =+ workerCreationTime+ processCreationTime (SupervisorProcess Supervisor { supervisorCreationTime })+ = supervisorCreationTime++ workers = sortBy (comparing processCreationTime) (HashMap.elems processMap)++--------------------------------------------------------------------------------++-- | Returns the "SupervisorStatus", this sub-routine will retry transaction+-- until its associated "Supervisor" has a status different from "Initializing".+readSupervisorStatusSTM :: TVar SupervisorStatus -> STM SupervisorStatus+readSupervisorStatusSTM statusVar = do+ status <- readTVar statusVar+ if status == Initializing then retry else return status++-- | Executes transaction that returns the "SupervisorStatus".+readSupervisorStatus :: SupervisorEnv -> IO SupervisorStatus+readSupervisorStatus SupervisorEnv { supervisorStatusVar } =+ atomically $ readTVar supervisorStatusVar++-- | Modifes the "Supervisor" status.+--+-- IMPORTANT: this is the only function that should be used for this purpose+-- given it has the side-effect of notifying a status change via the+-- "notifyEvent" sub-routine, orginally given in the "CapatazOption" record.+writeSupervisorStatus :: SupervisorEnv -> SupervisorStatus -> IO ()+writeSupervisorStatus SupervisorEnv { supervisorId, supervisorName, supervisorStatusVar, notifyEvent } newSupervisorStatus+ = do++ prevSupervisorStatus <- atomically $ do+ prevStatus <- readTVar supervisorStatusVar+ writeTVar supervisorStatusVar newSupervisorStatus+ return prevStatus++ eventTime <- getCurrentTime+ notifyEvent SupervisorStatusChanged+ { supervisorId = supervisorId+ , supervisorName = supervisorName+ , prevSupervisorStatus+ , newSupervisorStatus+ , eventTime+ }++-- | Used from public API functions to send "ControlAction" messages to a+-- Supervisor thread loop.+sendControlMsg :: SupervisorEnv -> ControlAction -> IO ()+sendControlMsg SupervisorEnv { supervisorNotify } ctrlMsg =+ supervisorNotify (ControlAction ctrlMsg)++-- | Used from public API functions to send ControlAction messages to the a+-- Supervisor thread loop, it receives an IO sub-routine that expects an IO+-- operation that blocks a thread until the message is done.+sendSyncControlMsg+ :: SupervisorEnv+ -> (IO () -> ControlAction) -- ^ Blocking sub-routine used from the caller+ -> IO ()+sendSyncControlMsg SupervisorEnv { supervisorNotify } mkCtrlMsg = do+ result <- newEmptyMVar+ supervisorNotify (ControlAction $ mkCtrlMsg (putMVar result ()))+ takeMVar result++-- | Utility function to transform a "CapatazOptions" record to a+-- "SupervisorOptions" record.+capatazOptionsToSupervisorOptions :: CapatazOptions -> SupervisorOptions+capatazOptionsToSupervisorOptions CapatazOptions {..} =+ SupervisorOptions {supervisorName = "capataz-root-supervisor", ..}++-- | Utility function to transform a "SupervisorEnv" record to a+-- "ParentSupervisorEnv" record; used on functions where supervision of+-- supervisors is managed.+toParentSupervisorEnv :: SupervisorEnv -> ParentSupervisorEnv+toParentSupervisorEnv SupervisorEnv { supervisorId, supervisorName, supervisorNotify, notifyEvent }+ = ParentSupervisorEnv {..}
+ src/Control/Concurrent/Capataz/Internal/Worker.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NoImplicitPrelude #-}++{-| This module contains all logic related to error handling when spawning threads+ to execute Worker sub-routines+-}+module Control.Concurrent.Capataz.Internal.Worker where++import Protolude++import Control.Concurrent.Async (asyncWithUnmask)+import Data.Time.Clock (getCurrentTime)++import qualified Data.UUID.V4 as UUID++import qualified Control.Concurrent.Capataz.Internal.Process as Process+import qualified Control.Concurrent.Capataz.Internal.Util as Util++import Control.Concurrent.Capataz.Internal.Types++-- | Decorates the given @IO ()@ sub-routine with failure handling+workerMain+ :: ParentSupervisorEnv -> WorkerOptions -> WorkerId -> RestartCount -> IO Worker+workerMain env@ParentSupervisorEnv { supervisorNotify } workerOptions@WorkerOptions { workerName, workerAction } workerId restartCount+ = do+ workerCreationTime <- getCurrentTime+ workerAsync <- asyncWithUnmask $ \unmask -> do++ eResult <- try $ do+ Util.setProcessThreadName workerId workerName+ unmask workerAction++ resultEvent <- case eResult of+ Left err -> Process.handleProcessException unmask+ env+ (WorkerSpec workerOptions)+ workerId+ restartCount+ err+ Right _ -> Process.handleProcessCompletion unmask+ env+ (WorkerSpec workerOptions)+ workerId+ restartCount++ supervisorNotify (MonitorEvent resultEvent)++ return Worker+ { workerId+ , workerName+ , workerAsync+ , workerCreationTime+ , workerOptions+ }++-- | Internal function that forks a worker thread on the Capataz thread; note+-- this is different from the public @forkWorker@ function which sends a message+-- to the capataz loop+forkWorker+ :: ParentSupervisorEnv+ -> WorkerOptions+ -> Maybe (WorkerId, RestartCount)+ -> IO Worker+forkWorker env workerOptions mRestartInfo = do+ (workerId, restartCount) <- case mRestartInfo of+ Just (workerId, restartCount) -> pure (workerId, restartCount)+ Nothing -> (,) <$> UUID.nextRandom <*> pure 0++ worker <- workerMain env workerOptions workerId restartCount+ Process.notifyProcessStarted mRestartInfo env (WorkerProcess worker)+ return worker
+ src/Control/Concurrent/Capataz/Lens.hs view
@@ -0,0 +1,21 @@+module Control.Concurrent.Capataz.Lens+ ( module X )+ where++import Control.Concurrent.Capataz.Internal.Types.Lens as X+ ( onSystemEventL+ , supervisorIntensityL+ , supervisorOnFailureL+ , supervisorOnIntensityReachedL+ , supervisorPeriodSecondsL+ , supervisorProcessSpecListL+ , supervisorProcessTerminationOrderL+ , supervisorRestartStrategyL+ , workerOnCompletionL+ , workerOnFailureL+ , workerOnTerminationL+ , workerRestartStrategyL+ , workerTerminationPolicyL+ )+import Lens.Micro as X (set, (&), (.~), (^.))+import Lens.Micro.Extras as X (view)
− src/Control/Concurrent/Internal/Capataz/Core.hs
@@ -1,259 +0,0 @@-{-# 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- }- )
− src/Control/Concurrent/Internal/Capataz/Restart.hs
@@ -1,219 +0,0 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-| This module contains all logic related to the restart of workers -}-module Control.Concurrent.Internal.Capataz.Restart where--import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime)--import Protolude--import Control.Concurrent.Internal.Capataz.Types-import Control.Concurrent.Internal.Capataz.Util- ( appendWorkerToMap- , fetchWorkerEnv- , readWorkerMap- , removeWorkerFromMap- , resetWorkerMap- , sortWorkersByTerminationOrder- )-import qualified Control.Concurrent.Internal.Capataz.Worker as Worker-import qualified Data.HashMap.Strict as HashMap-------------------------------------------------------------------------------------- | Function used to track difference between two timestamps to track capataz--- error intensity-calcDiffSeconds :: UTCTime -> IO NominalDiffTime-calcDiffSeconds creationTime = do- currentTime <- getCurrentTime- return $ diffUTCTime currentTime creationTime---- | Function that checks restart counts and worker start time to assess if the--- capataz error intensity has been breached, see "WorkerRestartAction" for--- possible outcomes.-calcRestartAction :: CapatazEnv -> Int -> NominalDiffTime -> WorkerRestartAction-calcRestartAction CapatazEnv { capatazIntensity, capatazPeriodSeconds } restartCount diffSeconds- = case () of- _- | diffSeconds < capatazPeriodSeconds && restartCount > capatazIntensity- -> HaltCapataz- | diffSeconds > capatazPeriodSeconds- -> ResetRestartCount- | otherwise- -> IncreaseRestartCount---- | Sub-routine responsible of executing a "CapatazRestartStrategy"-execCapatazRestartStrategy :: CapatazEnv -> WorkerEnv -> Int -> IO ()-execCapatazRestartStrategy capatazEnv@CapatazEnv { capatazRestartStrategy } WorkerEnv { workerId, workerSpec } workerRestartCount- = case capatazRestartStrategy of- AllForOne -> do- newWorkers <- restartWorkers capatazEnv workerId workerRestartCount- let newWorkersMap =- newWorkers- & fmap (\worker@Worker { workerId = cid } -> (cid, worker))- & HashMap.fromList- resetWorkerMap capatazEnv (const newWorkersMap)-- OneForOne -> do- removeWorkerFromMap capatazEnv workerId- newWorker <- restartWorker capatazEnv- workerSpec- workerId- workerRestartCount- appendWorkerToMap capatazEnv newWorker---- | Executes a restart action returned from the invokation of "calcRestartAction"-execRestartAction :: CapatazEnv -> WorkerEnv -> Int -> IO ()-execRestartAction capatazEnv@CapatazEnv { onCapatazIntensityReached } workerEnv@WorkerEnv { workerId, workerName, workerCreationTime } workerRestartCount- = do- restartAction <- calcRestartAction capatazEnv workerRestartCount- <$> calcDiffSeconds workerCreationTime-- case restartAction of- HaltCapataz -> do- -- skip exceptions on callback- (_ :: Either SomeException ()) <- try onCapatazIntensityReached- throwIO CapatazIntensityReached- { workerId- , workerName- , workerRestartCount = succ workerRestartCount- }-- ResetRestartCount -> execCapatazRestartStrategy capatazEnv workerEnv 0-- IncreaseRestartCount -> execCapatazRestartStrategy- capatazEnv- workerEnv- (succ workerRestartCount)-------------------------------------------------------------------------------------- | Restarts _all_ the worker green thread of a Capataz, invoked when one--- worker green thread fails and causes sibling worker threads to get restarted--- as well-restartWorkers :: CapatazEnv -> WorkerId -> RestartCount -> IO [Worker]-restartWorkers capatazEnv@CapatazEnv { capatazWorkerTerminationOrder } failingWorkerId restartCount- = do- workerMap <- readWorkerMap capatazEnv-- let workers =- sortWorkersByTerminationOrder capatazWorkerTerminationOrder workerMap-- newWorkers <- forM workers $ \worker@Worker { workerId, workerSpec } -> do- unless (failingWorkerId == workerId)- $ forceRestartWorker capatazEnv worker-- let WorkerSpec { workerRestartStrategy } = workerSpec- case workerRestartStrategy of- Temporary -> return Nothing- _ -> Just <$> restartWorker capatazEnv workerSpec workerId restartCount-- return $ catMaybes newWorkers---- | Sub-routine that is used when there is a restart request to a Worker caused--- by an "AllForOne" restart from a failing sibling worker.-forceRestartWorker :: CapatazEnv -> Worker -> IO ()-forceRestartWorker CapatazEnv { capatazName, capatazId, notifyEvent } Worker { workerId, workerName, workerAsync }- = do- eventTime <- getCurrentTime- notifyEvent WorkerTerminated- { capatazName- , capatazId- , workerId- , workerName- , eventTime- , workerThreadId = asyncThreadId workerAsync- , terminationReason = "forced restart"- }- cancelWith workerAsync RestartWorkerException---- | Starts a new worker thread taking into account an existing "WorkerId" and--- keeping a "RestartCount" to manage Capataz error intensity.-restartWorker- :: CapatazEnv -> WorkerSpec -> WorkerId -> RestartCount -> IO Worker-restartWorker capatazEnv workerSpec workerId restartCount =- Worker.forkWorker capatazEnv workerSpec (Just (workerId, restartCount))-------------------------------------------------------------------------------------- | This sub-routine is responsible of the restart strategies execution when a--- supervised worker finishes it execution because of a completion (e.g. worker--- sub-routine finished without any errors).-handleWorkerCompleted :: CapatazEnv -> WorkerId -> UTCTime -> IO ()-handleWorkerCompleted env@CapatazEnv { capatazName, capatazId, notifyEvent } workerId eventTime- = do- mWorkerEnv <- fetchWorkerEnv env workerId- case mWorkerEnv of- Nothing -> return ()- Just workerEnv@WorkerEnv { workerName, workerAsync, workerRestartStrategy }- -> do- notifyEvent WorkerCompleted- { capatazId- , capatazName- , workerId- , workerName- , eventTime- , workerThreadId = asyncThreadId workerAsync- }- case workerRestartStrategy of- Permanent -> do- -- NOTE: Completed workers should never account as errors happening on- -- a supervised thread, ergo, they should be restarted every time.-- -- TODO: Notify a warning around having a workerRestartStrategy different- -- than Temporary on workers that may complete.- let restartCount = 0- execRestartAction env workerEnv restartCount-- _ -> removeWorkerFromMap env workerId---- | This sub-routine is responsible of the restart strategies execution when a--- supervised worker finishes it execution because of a failure.-handleWorkerFailed :: CapatazEnv -> WorkerId -> SomeException -> Int -> IO ()-handleWorkerFailed env@CapatazEnv { capatazName, capatazId, notifyEvent } workerId workerError restartCount- = do- mWorkerEnv <- fetchWorkerEnv env workerId- case mWorkerEnv of- Nothing -> return ()- Just workerEnv@WorkerEnv { workerName, workerAsync, workerRestartStrategy }- -> do- eventTime <- getCurrentTime- notifyEvent WorkerFailed- { capatazName- , capatazId- , workerId- , workerName- , workerError- , workerThreadId = asyncThreadId workerAsync- , eventTime- }- case workerRestartStrategy of- Temporary -> removeWorkerFromMap env workerId- _ -> execRestartAction env workerEnv restartCount---- | This sub-routine is responsible of the restart strategies execution when a--- supervised worker finishes it execution because of a termination.-handleWorkerTerminated :: CapatazEnv -> WorkerId -> Text -> Int -> IO ()-handleWorkerTerminated env@CapatazEnv { capatazName, capatazId, notifyEvent } workerId terminationReason workerRestartCount- = do- mWorkerEnv <- fetchWorkerEnv env workerId- case mWorkerEnv of- Nothing -> return ()- Just workerEnv@WorkerEnv { workerName, workerAsync, workerRestartStrategy }- -> do- eventTime <- getCurrentTime- notifyEvent WorkerTerminated- { capatazName- , capatazId- , workerId- , workerName- , eventTime- , terminationReason- , workerThreadId = asyncThreadId workerAsync- }- case workerRestartStrategy of- Permanent -> execRestartAction env workerEnv workerRestartCount- _ -> removeWorkerFromMap env workerId
− src/Control/Concurrent/Internal/Capataz/Types.hs
@@ -1,519 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-| This module contains all the types used across all the other modules -}-module Control.Concurrent.Internal.Capataz.Types where--import Protolude--import Control.Concurrent.STM.TQueue (TQueue)-import Control.Concurrent.STM.TVar (TVar)-import Control.Teardown (ITeardown (..), Teardown)-import Data.Default (Default (..))-import Data.HashMap.Strict (HashMap)-import Data.IORef (IORef)-import Data.Time.Clock (NominalDiffTime, UTCTime)-import Data.UUID (UUID)--type CapatazId = UUID-type WorkerId = UUID-type WorkerAction = IO ()-type WorkerThreadId = ThreadId-type CapatazName = Text-type WorkerName = Text-type RestartCount = Int-type WorkerMap = HashMap WorkerId Worker---- | Event passed to the "notifyEvent" callback sub-routine, this events can be--- used to monitor the capataz system and understanding what is doing. This--- provides high levels of telemetry for the Capataz instance, so is mainly used--- for logging, monitoring and testing purposes.-data CapatazEvent- = InvalidCapatazStatusReached {- capatazId :: !CapatazId- , capatazName :: !CapatazName- , eventTime :: !UTCTime- }- | CapatazStatusChanged {- capatazId :: !CapatazId- , capatazName :: !CapatazName- , prevCapatazStatus :: !CapatazStatus- , newCapatazStatus :: !CapatazStatus- , eventTime :: !UTCTime- }- | WorkerTerminated {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , workerThreadId :: !WorkerThreadId- , workerId :: !WorkerId- , workerName :: !WorkerName- , terminationReason :: !Text- , eventTime :: !UTCTime- }- | WorkerStarted {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , workerThreadId :: !WorkerThreadId- , workerId :: !WorkerId- , workerName :: !WorkerName- , eventTime :: !UTCTime- }- | WorkerRestarted {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , workerThreadId :: !WorkerThreadId- , workerId :: !WorkerId- , workerName :: !WorkerName- , workerRestartCount :: !Int- , eventTime :: !UTCTime- }- | WorkerCompleted {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , workerThreadId :: !WorkerThreadId- , workerId :: !WorkerId- , workerName :: !WorkerName- , eventTime :: !UTCTime- }- | WorkerFailed {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , workerThreadId :: !WorkerThreadId- , workerId :: !WorkerId- , workerName :: !WorkerName- , workerError :: !SomeException- , eventTime :: !UTCTime- }- | WorkerCallbackExecuted {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , workerThreadId :: !WorkerThreadId- , workerId :: !WorkerId- , workerName :: !WorkerName- , workerCallbackError :: !(Maybe SomeException)- , callbackType :: !CallbackType- , eventTime :: !UTCTime- }- | WorkersTerminationStarted {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , terminationReason :: !Text- , eventTime :: !UTCTime- }- | WorkersTerminationFinished {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , terminationReason :: !Text- , eventTime :: !UTCTime- }- | CapatazFailed {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , capatazError :: !SomeException- , eventTime :: !UTCTime- }- | CapatazTerminated {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , eventTime :: !UTCTime- }- | CapatazShutdownInvoked {- capatazName :: !CapatazName- , capatazId :: !CapatazId- , eventTime :: !UTCTime- }- deriving (Generic, Show)---- | Defines how a "Worker" termination should be handled, default--- "WorkerTerminationPolicy" is 3 seconds-data WorkerTerminationPolicy- -- | Waits until infinity for the worker to terminate- = Infinity-- -- | Worker is terminated wihtout a chance to call its callback- | BrutalTermination-- -- | Allows n milliseconds for worker termination callback to be- -- executed, otherwise "BrutalTermination occurs"- | TimeoutMillis !Int- deriving (Generic, Show, Eq, Ord)--instance Default WorkerTerminationPolicy where- def = TimeoutMillis 3000--instance NFData WorkerTerminationPolicy---- | Helper record to assess if the capataz error intensity has been breached-data WorkerRestartAction- -- | The capataz will restart the failed worker and reset the restart count- -- given intensity period has passed- = ResetRestartCount-- -- | The capataz will restart the failed worker and increase the restart count- | IncreaseRestartCount-- -- | The error intensity has been reached- | HaltCapataz- deriving (Generic, Show, Eq)--instance NFData WorkerRestartAction---- | Specifies how order in which workers should be terminated by a Capataz in--- case of restart or shutdown; default is "OldestFirst"-data WorkerTerminationOrder- -- | Terminate worker threads from most recent to oldest- = NewestFirst- -- | Terminate worker threads from oldest to most recent- | OldestFirst- deriving (Generic, Show, Eq, Ord)--instance Default WorkerTerminationOrder where- def = OldestFirst--instance NFData WorkerTerminationOrder---- | Specifies how a Capataz should restart a failing worker. Default is--- "OneForOne"-data CapatazRestartStrategy- -- | Terminate all workers threads when one fails and restart them all- = AllForOne-- -- | Only restart worker thread that failed- | OneForOne- deriving (Generic, Show, Eq, Ord)--instance Default CapatazRestartStrategy where- def = OneForOne--instance NFData CapatazRestartStrategy---- | Utility record used to specify options to a "Capataz" instance-data CapatazOptions- = CapatazOptions {- -- | Name of the Capataz (present on "CapatazEvent" records)- capatazName :: Text- -- | How many errors is the Capataz be able to handle; check:- -- http://erlang.org/doc/design_principles/sup_princ.html#max_intensity- , capatazIntensity :: !Int- -- | Period of time where the Capataz can receive "capatazIntensity" amount- -- of errors- , capatazPeriodSeconds :: !NominalDiffTime- -- | What is the "CapatazRestartStrategy" for this Capataz- , capatazRestartStrategy :: !CapatazRestartStrategy- -- | Static set of workers that start as soon as the "Capataz" is created- , capatazWorkerSpecList :: ![WorkerSpec]- -- | In which order the "Capataz" record is going to terminate it's workers- , capatazWorkerTerminationOrder :: !WorkerTerminationOrder- -- | Callback used when the error intensity is reached- , onCapatazIntensityReached :: !(IO ())- -- | Callback used for telemetry purposes- , notifyEvent :: !(CapatazEvent -> IO ())- }----- | Utility record used to specify options to a "Worker" instance-data WorkerOptions- = WorkerOptions {- -- | Name of the Worker (present on "CapatazEvent" records)- workerName :: !WorkerName- -- | Callback used when the worker fails with an error- , workerOnFailure :: !(SomeException -> IO ())- -- | Callback used when the worker completes execution without error- , workerOnCompletion :: !(IO ())- -- | Callback used when the worker is terminated- , workerOnTermination :: !(IO ())- -- | Indicates how a worker should be terminated- , workerTerminationPolicy :: !WorkerTerminationPolicy- -- | Indicates how a worker should be restarted- , workerRestartStrategy :: !WorkerRestartStrategy- }- deriving (Generic)---- | Specifies how a "Worker" should restart on failure. Default is "Transient"-data WorkerRestartStrategy- -- | Worker thread is __always__ restarted- = Permanent-- -- | Worker thread is restarted only if it failed- | Transient-- -- | Worker thread is __never__ restarted- | Temporary-- deriving (Generic, Show, Eq)--instance NFData WorkerRestartStrategy-instance Default WorkerRestartStrategy where- def = Transient---- | WorkerSpec is a representation of the "WorkerOptions" record that embeds--- the @"IO" ()@ sub-routine of the worker thread. This record is used when we--- want to bound worker threads to a "Capataz" instance-data WorkerSpec- = WorkerSpec {- -- | An @IO ()@ sub-routine that will be executed when the worker- -- thread is created, this attribute is lazy given we want to this- -- value on a worker thread environment.- workerAction :: WorkerAction- -- | Name of the Worker (present on "CapatazEvent" records)- , workerName :: !WorkerName- -- | Callback used when the worker fails with an error- , workerOnFailure :: !(SomeException -> IO ())- -- | Callback used when the worker completes execution without error- , workerOnCompletion :: !(IO ())- -- | Callback used when the worker is terminated- , workerOnTermination :: !(IO ())- -- | Indicates how a worker should be terminated- , workerTerminationPolicy :: !WorkerTerminationPolicy- -- | Indicates how a worker should be restarted- , workerRestartStrategy :: !WorkerRestartStrategy- }- deriving (Generic)---- | Record that contains the "Async" record (thread reference) of a worker-data Worker- = Worker {- -- | Unique identifier for a worker that is executing- workerId :: !WorkerId- -- | "Async" thread of a worker, this Async executes the @IO ()@ sub-routine- , workerAsync :: !(Async ())- -- | Time where this worker was created (used for error intensity checks)- , workerCreationTime :: !UTCTime- -- | Name of the Worker (present on "CapatazEvent" records)- , workerName :: !WorkerName- -- | "WorkerSpec" contains all the options around restart and termination- -- policies- , workerSpec :: !WorkerSpec- }---- | Convenience utility record that contains all values related to a "Worker";--- this is used on internal functions of the Capataz library.-data WorkerEnv- = WorkerEnv {- workerAction :: WorkerAction- , workerId :: !WorkerId- , workerAsync :: !(Async ())- , workerCreationTime :: !UTCTime- , workerName :: !WorkerName- , workerSpec :: !WorkerSpec- , workerOnFailure :: !(SomeException -> IO ())- , workerOnCompletion :: !(IO ())- , workerOnTermination :: !(IO ())- , workerRestartStrategy :: !WorkerRestartStrategy- }---- | Internal record that represents an action being sent from threads using--- the Capataz public API.-data ControlAction- = ForkWorker {- workerSpec :: !WorkerSpec- , returnWorkerId :: !(WorkerId -> IO ())- }- | TerminateWorker {- workerId :: !WorkerId- , terminationReason :: !Text- , notifyWorkerTermination :: !(IO ())- }- | TerminateCapataz {- notifyCapatazTermination :: !(IO ())- }- deriving (Generic)---- | Internal exception thrown to the Capataz loop to indicate termination of--- execution-data CapatazSignal- = RestartWorkerException- | TerminateWorkerException {- workerId :: !WorkerId- , workerTerminationReason :: !Text- }- | BrutallyTerminateWorkerException {- workerId :: !WorkerId- , workerTerminationReason :: !Text- }- deriving (Generic, Show)--instance Exception CapatazSignal-instance NFData CapatazSignal---- | Internal exception triggered when a Worker violates error intensity--- specification-data CapatazError- = CapatazIntensityReached {- workerId :: !WorkerId- , workerName :: !WorkerName- , workerRestartCount :: !Int- }- deriving (Generic, Show)--instance Exception CapatazError-instance NFData CapatazError---- | Internal record that indicates what type of callback function is being--- invoked; this is used for telemetry purposes-data CallbackType- = OnCompletion- | OnFailure- | OnTermination- deriving (Generic, Show, Eq)---- | Internal exception triggered when a callback of a Worker fails-data WorkerError- = WorkerCallbackFailed {- workerId :: !WorkerId- , workerActionError :: !(Maybe SomeException)- , callbackType :: !CallbackType- , workerCallbackError :: !SomeException- }- deriving (Generic, Show)--instance Exception WorkerError---- | Internal event delivered from Worker threads to the Capataz thread to--- indicate completion, failure or termination-data MonitorEvent- = WorkerTerminated' {- workerId :: !WorkerId- , workerName :: !WorkerName- , workerRestartCount :: !RestartCount- , workerTerminationReason :: !Text- , monitorEventTime :: !UTCTime- }- | WorkerFailed' {- workerId :: !WorkerId- , workerName :: !WorkerName- , workerRestartCount :: !RestartCount- , workerError :: !SomeException- , monitorEventTime :: !UTCTime- }- | WorkerCompleted' {- workerId :: !WorkerId- , workerName :: !WorkerName- , monitorEventTime :: !UTCTime- }- | WorkerForcedRestart {- workerId :: !WorkerId- , workerName :: !WorkerName- , monitorEventTime :: !UTCTime- }- deriving (Show)---- | Internal state machine record that indicates the state of a Capataz-data CapatazStatus- -- | This state is set when Worker is created and it spawn static worker- -- threads- = Initializing- -- | This state is set when the Capataz thread is listenting to both- -- "ControlAction" and "MonitorEvent" messages- | Running- -- | This state is set when the Capataz thread is terminating it's assigned- -- worker- | Halting- -- | The Capataz thread is done- | Halted- deriving (Generic, Show, Eq)--instance NFData CapatazStatus---- | Internal message delivered to a Capataz thread that can either be a call--- from public API or an event from a monitored Worker-data CapatazMessage- = ControlAction !ControlAction- | MonitorEvent !MonitorEvent- deriving (Generic)---- | Record that contains the environment of a capataz monitor, this is used as--- the main record to create workers and to stop the supervisor thread.-data Capataz- = Capataz {- capatazRuntime :: !CapatazRuntime- , capatazEnv :: !CapatazEnv- , capatazAsync :: !(Async ())- , capatazTeardown :: !Teardown- }--instance ITeardown Capataz where- teardown Capataz {capatazTeardown} =- teardown capatazTeardown---- | Internal record used to hold part of the runtime information of a "Capataz"--- record-data CapatazRuntime- = CapatazRuntime {- capatazId :: !CapatazId- , capatazQueue :: !(TQueue CapatazMessage)- , capatazWorkerMap :: !(IORef (HashMap WorkerId Worker))- , capatazStatusVar :: !(TVar CapatazStatus)- , capatazOptions :: !CapatazOptions- }---- | Convenience utility record that contains all values related to a "Capataz";--- this is used on internal functions of the Capataz library.-data CapatazEnv- = CapatazEnv {- capatazId :: !CapatazId- , capatazName :: !CapatazName- , capatazQueue :: !(TQueue CapatazMessage)- , capatazWorkerMap :: !(IORef (HashMap WorkerId Worker))- , capatazStatusVar :: !(TVar CapatazStatus)- , capatazOptions :: !CapatazOptions- , capatazRuntime :: !CapatazRuntime- , capatazIntensity :: !Int- -- ^ http://erlang.org/doc/design_principles/sup_princ.html#max_intensity- , capatazPeriodSeconds :: !NominalDiffTime- , capatazRestartStrategy :: !CapatazRestartStrategy- , capatazWorkerTerminationOrder :: !WorkerTerminationOrder- , onCapatazIntensityReached :: !(IO ())- , notifyEvent :: !(CapatazEvent -> IO ())- }---- | Default options to easily create capataz instances:--- * name defaults to \"default-capataz\"--- * intensity error tolerance is set to 1 error every 5 seconds--- * has a "OneForOne " capataz restart strategy--- * has a termination order of "OldestFirst"-defCapatazOptions :: CapatazOptions-defCapatazOptions = CapatazOptions- { capatazName = "default-capataz"-- -- One (1) restart every five (5) seconds- , capatazIntensity = 1- , capatazPeriodSeconds = 5- , capatazRestartStrategy = def- , capatazWorkerSpecList = []- , capatazWorkerTerminationOrder = OldestFirst- , onCapatazIntensityReached = return ()- , notifyEvent = const $ return ()- }---- | Default options to easily create worker instances:--- * name defaults to \"default-worker\"--- * has a "Transient" worker restart strategy--- * has a termination policy of three (3) seconds-defWorkerOptions :: WorkerOptions-defWorkerOptions = WorkerOptions- { workerName = "default-worker"- , workerOnFailure = const $ return ()- , workerOnCompletion = return ()- , workerOnTermination = return ()- , workerTerminationPolicy = def- , workerRestartStrategy = def- }---- | Default spec to easily create worker instances:--- * @IO ()@ sub-routine simply returns unit--- * name defaults to \"default-worker\"--- * has a "Transient" worker restart strategy--- * has a termination policy of three (3) seconds-defWorkerSpec :: WorkerSpec-defWorkerSpec = WorkerSpec- { workerName = "default-worker"- , workerAction = return ()- , workerOnFailure = const $ return ()- , workerOnCompletion = return ()- , workerOnTermination = return ()- , workerTerminationPolicy = def- , workerRestartStrategy = def- }
− src/Control/Concurrent/Internal/Capataz/Util.hs
@@ -1,165 +0,0 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE RecordWildCards #-}-{-| This module contains:--* Functions to manipulate the state of the Capataz record-* Utility functions used for communication between threads-* Public API utility functions---}-module Control.Concurrent.Internal.Capataz.Util where--import Protolude--import Control.Concurrent.STM (STM, atomically, retry)-import Control.Concurrent.STM.TQueue (writeTQueue)-import Control.Concurrent.STM.TVar (TVar, readTVar, writeTVar)-import Data.IORef (atomicModifyIORef', readIORef)-import qualified Data.Text as T-import Data.Time.Clock (getCurrentTime)--import qualified Data.HashMap.Strict as HashMap--import Control.Concurrent.Internal.Capataz.Types---- | Returns only the number of the ThreadId-getTidNumber :: ThreadId -> Maybe Text-getTidNumber tid = case T.words $ show tid of- (_:tidNumber:_) -> Just tidNumber- _ -> Nothing-------------------------------------------------------------------------------------- | Fetches a "Worker" from the "Capataz" instance environment-fetchWorker :: CapatazEnv -> WorkerId -> IO (Maybe Worker)-fetchWorker CapatazEnv { capatazWorkerMap } workerId =- HashMap.lookup workerId <$> readIORef capatazWorkerMap---- | Fetches a "WorkerEnv" from the "Capataz" instance environment-fetchWorkerEnv :: CapatazEnv -> WorkerId -> IO (Maybe WorkerEnv)-fetchWorkerEnv CapatazEnv { capatazWorkerMap } workerId =- ((workerToEnv <$>) . HashMap.lookup workerId) <$> readIORef capatazWorkerMap---- | Appends a new "Worker" to the "Capataz" existing worker map.-appendWorkerToMap :: CapatazEnv -> Worker -> IO ()-appendWorkerToMap CapatazEnv { capatazWorkerMap } worker@Worker { workerId } =- atomicModifyIORef' capatazWorkerMap- (\workerMap -> (appendWorker workerMap, ()))- where appendWorker = HashMap.alter (const $ Just worker) workerId---- | Removes a "Worker" from the "Capataz" existing worker map.-removeWorkerFromMap :: CapatazEnv -> WorkerId -> IO ()-removeWorkerFromMap CapatazEnv { capatazWorkerMap } workerId =- atomicModifyIORef'- capatazWorkerMap- ( \workerMap -> maybe (workerMap, ())- (const (HashMap.delete workerId workerMap, ()))- (HashMap.lookup workerId workerMap)- )---- | Function to modify a "Capataz" worker map using a pure function.-resetWorkerMap :: CapatazEnv -> (WorkerMap -> WorkerMap) -> IO ()-resetWorkerMap CapatazEnv { capatazWorkerMap } workerMapFn = atomicModifyIORef'- capatazWorkerMap- (\workerMap -> (workerMapFn workerMap, ()))---- | Function to get a snapshot of the "Capataz"' worker map-readWorkerMap :: CapatazEnv -> IO WorkerMap-readWorkerMap CapatazEnv { capatazWorkerMap } = readIORef capatazWorkerMap---- | Returns all worker's of a "Capataz" by "WorkerTerminationOrder". This is--- used "AllForOne" restarts and shutdown operations.-sortWorkersByTerminationOrder :: WorkerTerminationOrder -> WorkerMap -> [Worker]-sortWorkersByTerminationOrder terminationOrder workerMap =- case terminationOrder of- OldestFirst -> workers- NewestFirst -> reverse workers- where- -- NOTE: dissambiguates workerCreationTime field- workerCreationTime' Worker { workerCreationTime } = workerCreationTime-- workers = sortBy (comparing workerCreationTime') (HashMap.elems workerMap)-------------------------------------------------------------------------------------- | Sub-routine that returns the "CapatazStatus", this sub-routine will block--- until the "Capataz" has a status different from "Initializing".-readCapatazStatusSTM :: TVar CapatazStatus -> STM CapatazStatus-readCapatazStatusSTM statusVar = do- status <- readTVar statusVar- if status == Initializing then retry else return status---- | Sub-routine that returns the "CapatazStatus" on the IO monad-readCapatazStatus :: CapatazEnv -> IO CapatazStatus-readCapatazStatus CapatazEnv { capatazStatusVar } =- atomically $ readTVar capatazStatusVar---- | Modifes the "Capataz" status, this is the only function that should be used--- to this end given it has the side-effect of notifying a status change via the--- "notifyEvent" sub-routine, given via an attribute of the "CapatazOption"--- record.-writeCapatazStatus :: CapatazEnv -> CapatazStatus -> IO ()-writeCapatazStatus CapatazEnv { capatazId, capatazName, capatazStatusVar, notifyEvent } newCapatazStatus- = do-- prevCapatazStatus <- atomically $ do- prevStatus <- readTVar capatazStatusVar- writeTVar capatazStatusVar newCapatazStatus- return prevStatus-- eventTime <- getCurrentTime- notifyEvent CapatazStatusChanged- { capatazId- , capatazName- , prevCapatazStatus- , newCapatazStatus- , eventTime- }----- | Used from public API functions to send a ControlAction to the Capataz--- supervisor thread loop-sendControlMsg :: CapatazEnv -> ControlAction -> IO ()-sendControlMsg CapatazEnv { capatazQueue } ctrlMsg =- atomically $ writeTQueue capatazQueue (ControlAction ctrlMsg)---- | Used from public API functions to send a ControlAction to the Capataz--- supervisor thread loop, it receives an IO sub-routine that expects an IO--- operation that blocks a thread until the message is done.-sendSyncControlMsg- :: CapatazEnv- -> (IO () -> ControlAction) -- ^ Blocking sub-routine used from the caller- -> IO ()-sendSyncControlMsg CapatazEnv { capatazQueue } mkCtrlMsg = do- result <- newEmptyMVar- atomically- $ writeTQueue capatazQueue (ControlAction $ mkCtrlMsg (putMVar result ()))- takeMVar result---- | Utility function to transform a "CapatazRuntime" into a "CapatazEnv"-capatazToEnv :: CapatazRuntime -> CapatazEnv-capatazToEnv capatazRuntime@CapatazRuntime {..} =- let CapatazOptions {..} = capatazOptions in CapatazEnv {..}---- | Utility function to transform a "Worker" into a "WorkerEnv"-workerToEnv :: Worker -> WorkerEnv-workerToEnv Worker {..} =- let- WorkerSpec { workerAction, workerOnFailure, workerOnCompletion, workerOnTermination, workerRestartStrategy }- = workerSpec- in- WorkerEnv {..}---- | Utility function to transform a "WorkerEnv" into a "Worker"-envToWorker :: WorkerEnv -> Worker-envToWorker WorkerEnv {..} = Worker {..}---- | Utility function to transform a "WorkerOptions" into a "WorkerSpec"-workerOptionsToSpec :: WorkerOptions -> IO () -> WorkerSpec-workerOptionsToSpec WorkerOptions {..} workerAction = WorkerSpec {..}---- | Utility function to transform a "Capataz" into an @"Async" ()@-capatazToAsync :: Capataz -> Async ()-capatazToAsync = capatazAsync
− src/Control/Concurrent/Internal/Capataz/Worker.hs
@@ -1,312 +0,0 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-| This module contains all logic related to error handling when spawning threads- to execute Worker sub-routines--}-module Control.Concurrent.Internal.Capataz.Worker where--import Protolude--import Control.Concurrent.Async (asyncWithUnmask)-import Control.Concurrent.STM.TQueue (writeTQueue)-import Data.Time.Clock (getCurrentTime)-import GHC.Conc (labelThread)--import qualified Data.Text as T-import qualified Data.UUID.V4 as UUID--import Control.Concurrent.Internal.Capataz.Types-import Control.Concurrent.Internal.Capataz.Util- (getTidNumber, readWorkerMap, sortWorkersByTerminationOrder)---- | Internal functions that overwrites the GHC thread name, for increasing--- traceability on GHC internals-setWorkerThreadName :: WorkerId -> WorkerName -> IO ()-setWorkerThreadName workerId workerName = do- tid <- myThreadId- let workerIdentifier =- T.unpack workerName <> "_" <> show workerId <> "_" <> maybe- ""- T.unpack- (getTidNumber tid)- labelThread tid workerIdentifier---- | Handles errors caused by the execution of the "workerMain" sub-routine-handleWorkerException- :: (IO () -> IO a)- -> CapatazEnv- -> WorkerSpec- -> WorkerId- -> RestartCount- -> SomeException- -> IO MonitorEvent-handleWorkerException unmask CapatazEnv { capatazId, capatazName, notifyEvent } WorkerSpec { workerName, workerOnFailure, workerOnTermination } workerId restartCount err- = do- workerThreadId <- myThreadId- monitorEventTime <- getCurrentTime- case fromException err of- Just RestartWorkerException ->- return WorkerForcedRestart {workerId , workerName , monitorEventTime }-- Just TerminateWorkerException { workerTerminationReason } -> do- eErrResult <- try $ unmask workerOnTermination-- notifyEvent WorkerCallbackExecuted- { capatazId- , capatazName- , workerId- , workerName- , workerThreadId- , workerCallbackError = either Just (const Nothing) eErrResult- , callbackType = OnTermination- , eventTime = monitorEventTime- }-- case eErrResult of- Left workerCallbackError -> return WorkerFailed'- { workerName- , workerId- , monitorEventTime- , workerError = toException WorkerCallbackFailed- { workerId- , workerCallbackError- , callbackType = OnTermination- , workerActionError = Just err- }- , workerRestartCount = restartCount- }- Right _ -> return WorkerTerminated'- { workerId- , workerName- , monitorEventTime- , workerTerminationReason- , workerRestartCount = restartCount- }-- Just BrutallyTerminateWorkerException { workerTerminationReason } ->- return WorkerTerminated'- { workerId- , workerName- , monitorEventTime- , workerTerminationReason- , workerRestartCount = restartCount- }-- -- This exception was an error from the given sub-routine- Nothing -> do- eErrResult <- try $ unmask $ workerOnFailure err-- notifyEvent WorkerCallbackExecuted- { capatazId- , capatazName- , workerId- , workerName- , workerThreadId- , workerCallbackError = either Just (const Nothing) eErrResult- , callbackType = OnFailure- , eventTime = monitorEventTime- }-- case eErrResult of- Left workerCallbackError -> return WorkerFailed'- { workerName- , workerId- , monitorEventTime- , workerRestartCount = restartCount- , workerError = toException WorkerCallbackFailed- { workerId- , workerCallbackError- , callbackType = OnFailure- , workerActionError = Just err- }- }- Right _ -> return WorkerFailed'- { workerName- , workerId- , monitorEventTime- , workerError = err- , workerRestartCount = restartCount- }---- | Handles completion of the "workerMain" sub-routine-handleWorkerCompletion- :: (IO () -> IO a)- -> CapatazEnv- -> WorkerSpec- -> WorkerId- -> RestartCount- -> IO MonitorEvent-handleWorkerCompletion unmask CapatazEnv { capatazId, capatazName, notifyEvent } WorkerSpec { workerName, workerOnCompletion } workerId restartCount- = do- workerThreadId <- myThreadId- monitorEventTime <- getCurrentTime- eCompResult <- try $ unmask workerOnCompletion-- notifyEvent WorkerCallbackExecuted- { capatazId- , capatazName- , workerId- , workerName- , workerThreadId- , workerCallbackError = either Just (const Nothing) eCompResult- , callbackType = OnCompletion- , eventTime = monitorEventTime- }-- case eCompResult of- Left err -> return WorkerFailed'- { workerName- , workerId- , monitorEventTime- , workerError = toException WorkerCallbackFailed- { workerId- , workerCallbackError = err- , callbackType = OnCompletion- , workerActionError = Nothing- }- , workerRestartCount = restartCount- }- Right _ ->- return WorkerCompleted' {workerName , workerId , monitorEventTime }---- | Decorates the given @IO ()@ sub-routine with failure handling-workerMain :: CapatazEnv -> WorkerSpec -> WorkerId -> RestartCount -> IO Worker-workerMain env@CapatazEnv { capatazQueue } workerSpec@WorkerSpec { workerName, workerAction } workerId restartCount- = do- workerCreationTime <- getCurrentTime- workerAsync <- asyncWithUnmask $ \unmask -> do-- eResult <- try $ do- setWorkerThreadName workerId workerName- unmask workerAction-- resultEvent <- case eResult of- Left err ->- handleWorkerException unmask env workerSpec workerId restartCount err- Right _ ->- handleWorkerCompletion unmask env workerSpec workerId restartCount-- atomically $ writeTQueue capatazQueue (MonitorEvent resultEvent)-- return Worker- { workerId- , workerName- , workerAsync- , workerCreationTime- , workerSpec- }---- | Internal function used to send a proper "CapatazEvent" to the "notifyEvent"--- callback, this event can either be a @WorkerStarted@ or a @WorkerRestarted@-notifyWorkerStarted :: Maybe (WorkerId, Int) -> CapatazEnv -> Worker -> IO ()-notifyWorkerStarted mRestartInfo CapatazEnv { capatazId, capatazName, notifyEvent } Worker { workerId, workerName, workerAsync }- = do- eventTime <- getCurrentTime- case mRestartInfo of- Just (_workerId, workerRestartCount) -> notifyEvent WorkerRestarted- { capatazId- , capatazName- , workerId- , workerName- , workerRestartCount- , workerThreadId = asyncThreadId workerAsync- , eventTime- }- Nothing -> notifyEvent WorkerStarted- { capatazId- , capatazName- , workerId- , workerName- , eventTime- , workerThreadId = asyncThreadId workerAsync- }---- | Internal function that forks a worker thread on the Capataz thread; note--- this is different from the public @forkWorker@ function which sends a message--- to the capataz loop-forkWorker- :: CapatazEnv -> WorkerSpec -> Maybe (WorkerId, RestartCount) -> IO Worker-forkWorker env workerSpec mRestartInfo = do- (workerId, restartCount) <- case mRestartInfo of- Just (workerId, restartCount) -> pure (workerId, restartCount)- Nothing -> (,) <$> UUID.nextRandom <*> pure 0-- worker <- workerMain env workerSpec workerId restartCount- notifyWorkerStarted mRestartInfo env worker- return worker---- | Internal function that forks a worker thread on the Capataz thread; note--- this is different from the public @forkWorker@ function which sends a message--- to the capataz loop-terminateWorker- :: Text -- ^ Text that indicates why there is a termination- -> CapatazEnv- -> Worker- -> IO ()-terminateWorker workerTerminationReason CapatazEnv { capatazId, capatazName, notifyEvent } Worker { workerId, workerName, workerSpec, workerAsync }- = do- let WorkerSpec { workerTerminationPolicy } = workerSpec- case workerTerminationPolicy of- Infinity -> cancelWith- workerAsync- TerminateWorkerException {workerId , workerTerminationReason }-- BrutalTermination -> cancelWith- workerAsync- BrutallyTerminateWorkerException {workerId , workerTerminationReason }-- TimeoutMillis millis -> race_- ( do- threadDelay (millis * 1000)- cancelWith- workerAsync- BrutallyTerminateWorkerException- { workerId- , workerTerminationReason- }- )- ( cancelWith- workerAsync- TerminateWorkerException {workerId , workerTerminationReason }- )-- eventTime <- getCurrentTime- notifyEvent WorkerTerminated- { capatazId- , capatazName- , eventTime- , workerId- , workerName- , workerThreadId = asyncThreadId workerAsync- , terminationReason = workerTerminationReason- }----- | Internal sub-routine that terminates workers of a Capataz, used when a--- Capataz instance is terminated-terminateWorkers :: Text -> CapatazEnv -> IO ()-terminateWorkers terminationReason env@CapatazEnv { capatazName, capatazId, capatazWorkerTerminationOrder, notifyEvent }- = do- eventTime <- getCurrentTime- workerMap <- readWorkerMap env-- let workers =- sortWorkersByTerminationOrder capatazWorkerTerminationOrder workerMap-- notifyEvent WorkersTerminationStarted- { capatazName- , capatazId- , terminationReason- , eventTime- }-- forM_ workers (terminateWorker terminationReason env)-- notifyEvent WorkersTerminationFinished- { capatazName- , capatazId- , terminationReason- , eventTime- }
+ test/testsuite/Control/Concurrent/Capataz/SupervisorTest.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+module Control.Concurrent.Capataz.SupervisorTest where++import Protolude++import Control.Concurrent.Capataz (set)+import qualified Control.Concurrent.Capataz as SUT+import Test.Util++import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (testCase)++tests :: TestTree+tests = testGroup+ "supervision trees"+ [ testCase "initialize and teardown of supervision tree works as expected"+ $ testCapatazStreamWithOptions+ ( & set+ SUT.supervisorProcessSpecListL+ [ SUT.supervisorSpec+ "tree-1"+ ( set SUT.supervisorProcessSpecListL+ [ SUT.workerSpecWithDefaults "1-A" (forever $ threadDelay 10001000)+ , SUT.workerSpecWithDefaults "1-B" (forever $ threadDelay 10001000)+ ]+ )+ , SUT.supervisorSpec+ "tree-2"+ ( set+ SUT.supervisorProcessSpecListL+ [ SUT.workerSpecWithDefaults "2-A" (forever $ threadDelay 10001000)+ , SUT.workerSpecWithDefaults "2-B" (forever $ threadDelay 10001000)+ ]+ )+ ]+ )+ [ andP [assertSupervisorName "tree-1", assertWorkerStarted "1-A"]+ , andP [assertSupervisorName "tree-1", assertWorkerStarted "1-B"]+ , andP+ [ assertSupervisorName "tree-1"+ , assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Initializing SUT.Running+ ]+ , andP [assertSupervisorName "tree-2", assertWorkerStarted "2-A"]+ , andP [assertSupervisorName "tree-2", assertWorkerStarted "2-B"]+ , andP+ [ assertSupervisorName "tree-2"+ , assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Initializing SUT.Running+ ]+ , andP+ [ assertSupervisorName "capataz-root-supervisor"+ , assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Initializing SUT.Running+ ]+ ]+ (const $ return ())+ []+ [ andP+ [ assertSupervisorName "capataz-root-supervisor"+ , assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Running SUT.Halting+ ]+ , andP+ [ assertSupervisorName "capataz-root-supervisor"+ , assertEventType ProcessTerminationStarted+ ]+ , andP [assertSupervisorName "tree-1", assertWorkerTerminated "1-A"]+ , andP [assertSupervisorName "tree-1", assertWorkerTerminated "1-B"]+ , andP [assertSupervisorName "tree-2", assertWorkerTerminated "2-A"]+ , andP [assertSupervisorName "tree-2", assertWorkerTerminated "2-B"]+ , andP+ [ assertSupervisorName "capataz-root-supervisor"+ , assertEventType ProcessTerminationFinished+ ]+ , andP+ [ assertSupervisorName "capataz-root-supervisor"+ , assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Halting SUT.Halted+ ]+ ]+ Nothing+ , testCase "supervision sub-tree gets restarted on failure" $ do+ failingAction <- mkFailingSubRoutine 2+ testCapatazStreamWithOptions+ ( set SUT.supervisorIntensityL 3 . set+ SUT.supervisorProcessSpecListL+ [ SUT.supervisorSpec+ "tree-1"+ ( set SUT.supervisorIntensityL 1+ . set SUT.supervisorPeriodSecondsL 10+ . set SUT.supervisorProcessSpecListL+ [SUT.workerSpecWithDefaults "failing-worker" failingAction]+ )+ ]+ )+ []+ (const $ threadDelay 1000)+ [ assertWorkerFailed "failing-worker"+ , assertSupervisorFailed "tree-1"+ , assertWorkerStarted "failing-worker"+ , assertSupervisorRestarted "tree-1"+ ]+ []+ Nothing+ , testCase "AllForOne strategy restarts sibling supervision tree" $ do+ failingAction <- mkFailingSubRoutine 2+ testCapatazStreamWithOptions+ ( set SUT.supervisorIntensityL 3+ . set SUT.supervisorRestartStrategyL SUT.AllForOne+ . set+ SUT.supervisorProcessSpecListL+ [ SUT.supervisorSpec+ "tree-1"+ ( set SUT.supervisorIntensityL 1+ . set SUT.supervisorPeriodSecondsL 10+ . set SUT.supervisorProcessSpecListL+ [SUT.workerSpecWithDefaults "failing-worker" failingAction]+ )+ , SUT.supervisorSpec+ "tree-2"+ ( set+ SUT.supervisorProcessSpecListL+ [ SUT.workerSpec "stable-worker"+ (forever $ threadDelay 1000100)+ identity+ ]+ )+ ]+ )+ []+ (const $ threadDelay 9000)+ [ assertWorkerFailed "failing-worker"+ , assertWorkerStarted "stable-worker"+ , assertSupervisorFailed "tree-1"+ , assertSupervisorRestarted "tree-1"+ , assertSupervisorTerminated "tree-2"+ , assertWorkerStarted "stable-worker"+ , assertSupervisorRestarted "tree-2"+ ]+ []+ Nothing+ ]
test/testsuite/Control/Concurrent/CapatazTest.hs view
@@ -22,1246 +22,977 @@ import Protolude -import qualified Data.Text as T--import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)-import Text.Show.Pretty (ppShow)--import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit (assertBool, assertFailure, testCase)--import Control.Concurrent.STM.TQueue (newTQueueIO, readTQueue, writeTQueue)-import Control.Concurrent.STM.TVar (modifyTVar', newTVarIO, readTVar)--import qualified Control.Concurrent.Capataz as SUT------------------------------------------------------------------------------------- Util---- | Utility function that gets the type name of a Record through it's Show--- output.-fetchRecordName :: Show a => a -> Text-fetchRecordName = T.takeWhile (/= ' ') . show---- | Composes two predicate functions together with a boolean AND-andP :: [a -> Bool] -> a -> Bool-andP predList a = all ($ a) predList------------------------------------------------------------------------------------- Assertions and Testers---- | This record duplicate the same event names as the ones found in the--- "CapatazEvent" type, we use this to avoid using Text comparisons on assertion--- helper functions. The "CapatazEvent" record is imported qualified, so there--- is no conflict happening.-data EventType- = InvalidCapatazStatusReached- | CapatazStatusChanged- | WorkerTerminated- | WorkerStarted- | WorkerRestarted- | WorkerCompleted- | WorkerFailed- | WorkerCallbackExecuted- | WorkersTerminationStarted- | WorkersTerminationFinished- | CapatazFailed- | CapatazTerminated- deriving (Show)---- | Predicate function to assert "CapatazEvent" types-assertEventType :: EventType -> SUT.CapatazEvent -> Bool-assertEventType evType ev = fetchRecordName ev == show evType---- | Predicate function to assert "CapatazEvent" worker name-assertWorkerName :: Text -> SUT.CapatazEvent -> Bool-assertWorkerName workerName' ev = case ev of- SUT.WorkerRestarted { workerName } -> workerName' == workerName- SUT.WorkerFailed { workerName } -> workerName' == workerName- SUT.WorkerTerminated { workerName } -> workerName' == workerName- SUT.WorkerStarted { workerName } -> workerName' == workerName- _ -> False---- | Predicate function to assert type of an error inside a "CapatazEvent"-assertErrorType :: Text -> SUT.CapatazEvent -> Bool-assertErrorType errType ev = case ev of- SUT.WorkerFailed { workerError } -> fetchRecordName workerError == errType- SUT.CapatazFailed { capatazError } -> fetchRecordName capatazError == errType- SUT.WorkerCallbackExecuted { workerCallbackError } ->- case workerCallbackError of- Nothing -> False- Just originalError -> fetchRecordName originalError == errType- _ -> False---- | Predicate function to assert type of callback executed inside a--- "CapatazEvent"-assertCallbackType :: SUT.CallbackType -> SUT.CapatazEvent -> Bool-assertCallbackType cbType ev = case ev of- SUT.WorkerFailed { workerError } -> case fromException workerError of- Just SUT.WorkerCallbackFailed { callbackType } -> cbType == callbackType- _ -> False- SUT.WorkerCallbackExecuted { callbackType } -> cbType == callbackType- _ -> False---- | Predicate function to assert restart count inside a "CapatazEvent"-assertRestartCount :: (Int -> Bool) -> SUT.CapatazEvent -> Bool-assertRestartCount predFn ev = case ev of- SUT.WorkerRestarted { workerRestartCount } -> predFn workerRestartCount- _ -> False---- | Predicate function to assert a Capataz status change-assertCapatazStatusChanged- :: SUT.CapatazStatus -> SUT.CapatazStatus -> SUT.CapatazEvent -> Bool-assertCapatazStatusChanged fromEv toEv ev = case ev of- SUT.CapatazStatusChanged { prevCapatazStatus, newCapatazStatus } ->- fromEv == prevCapatazStatus && toEv == newCapatazStatus- _ -> False---- | Predicate function to assert a worker was started-assertWorkerStarted :: Text -> SUT.CapatazEvent -> Bool-assertWorkerStarted workerName =- andP [assertEventType WorkerStarted, assertWorkerName workerName]---- | Predicate function to assert a worker was terminated-assertWorkerTerminated :: Text -> SUT.CapatazEvent -> Bool-assertWorkerTerminated workerName =- andP [assertEventType WorkerTerminated, assertWorkerName workerName]---- | Predicate function to assert a capataz thread failed with error type-assertCapatazFailedWith :: Text -> SUT.CapatazEvent -> Bool-assertCapatazFailedWith errorName =- andP [assertEventType CapatazFailed, assertErrorType errorName]-------------------------------------------------------------------------------------- | Exception used to test failures inside Worker sub-routines-data RestartingWorkerError- = RestartingWorkerError- deriving (Show)--instance Exception RestartingWorkerError---- | Exception used to test failures inside Worker callback sub-routines-data TimeoutError- = TimeoutError- deriving (Show)--instance Exception TimeoutError---- | Utility function to create a Worker sub-routine that fails at least a--- number of times-mkFailingSubRoutine- :: Int -- ^ Number of times the Worker sub-routine will fail- -> IO (IO ()) -- ^ Sub-routine used on worker creation-mkFailingSubRoutine failCount = do- countRef <- newIORef failCount- let subRoutine = do- shouldFail <- atomicModifyIORef' countRef- (\count -> (pred count, count > 0))- when shouldFail (throwIO RestartingWorkerError)-- return subRoutine---- | A sub-routine that will complete for `initCount` amount of times. This--- function works great when testing `Permanent` strategies, as you would like--- to assert restart events once (if it keeps completing it will fill up the log--- with restart events)-mkCompletingBeforeNRestartsSubRoutine :: Int -> IO (IO ())-mkCompletingBeforeNRestartsSubRoutine initCount = do- countRef <- newIORef initCount- let subRoutine = do- shouldStop <- atomicModifyIORef' countRef- (\count -> (pred count, count > 0))- if shouldStop then return () else forever $ threadDelay 1000100- return subRoutine---- | A sub-routine that will complete once. This function works great when--- testing `Permanent` strategies, as you would like to assert restart events--- once (if it keeps completing it will fill up the log with restart events)-mkCompletingOnceSubRoutine :: IO (IO ())-mkCompletingOnceSubRoutine = mkCompletingBeforeNRestartsSubRoutine 1---- | Utility function to build a test environment for a Capataz execution.--- It is composed by:------ * List of assertions that represent events that should be triggered by the--- capataz instance in order------ * A function to modify the default "CapatazOptions", this utility function injects--- a special @notifyEvent@ callback to execute given assertions.-testCapatazStreamWithOptions- :: [SUT.CapatazEvent -> Bool] -- ^ Assertions happening before setup function- -- is called- -> (SUT.CapatazOptions -> SUT.CapatazOptions) -- ^ Function to modify default- -- @CapatazOptions@- -> (SUT.Capataz -> IO ()) -- ^ Function used to test public the supervisor- -- public API (a.k.a setup function)- -> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the setup- -- function- -> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the capataz- -- record is terminated- -> Maybe (SUT.CapatazEvent -> Bool) -- ^ An assertion checked across all- -- @CapatazEvents@ that happened in a- -- test, great when testing that an event- -- __did not__ happen- -> IO ()-testCapatazStreamWithOptions preSetupAssertion optionModFn setupFn postSetupAssertions postTeardownAssertions mAllEventsAssertion- = do-- eventStream <- newTQueueIO- accRef <- newIORef []- pendingCountVar <- newIORef- ( sum $ fmap- length- [preSetupAssertion, postSetupAssertions, postTeardownAssertions]- )-- capataz <- SUT.forkCapataz $ (optionModFn SUT.defCapatazOptions)- { SUT.notifyEvent = trackEvent accRef eventStream- }-- -- We check preSetup assertions are met before we execute the setup- -- function. This serves to test initialization of capataz instance- runAssertions "PRE-SETUP"- (eventStream, accRef)- pendingCountVar- preSetupAssertion- capataz-- -- We execute the setup sub-routine, which is going to use the Capataz public- -- API to assert events- setupResult <- try (setupFn capataz)-- case setupResult of- -- If the sub-routine fails, show exception- Left err -> assertFailure (show (err :: SomeException))- Right _ -> do- -- We now run post-setup assertions- runAssertions "POST-SETUP"- (eventStream, accRef)- pendingCountVar- postSetupAssertions- capataz-- -- We now shutdown the capataz instance- void $ SUT.teardown capataz-- -- We run assertions for after the capataz has been shut down- runAssertions "POST-TEARDOWN"- (eventStream, accRef)- pendingCountVar- postTeardownAssertions- capataz-- -- Lastly, we check if there is a function that we want to execute- -- across all events that happened in the test, this is to assert the- -- absence of an event- case mAllEventsAssertion of- Nothing -> return ()- Just allEventsAssertion -> do- events <- reverse <$> readIORef accRef- assertBool- ( "On AFTER-TEST, expected all events to match predicate, but didn't ("- <> show (length events)- <> " events tried)\n"- <> ppShow (zip ([0 ..] :: [Int]) events)- )- (all allEventsAssertion events)- where- -- Utility functions that runs the readEventLoop function with a timeout- -- of a second, this way we can guarantee assertions are met without having- -- to add @threadDelays@ to the test execution- runAssertions stageName (eventStream, accRef) pendingCountVar assertions capataz- = do- raceResult <- race- (threadDelay 1000100)- (readEventLoop eventStream pendingCountVar assertions)- case raceResult of- Left _ -> do- events <- reverse <$> readIORef accRef- pendingCount <- readIORef pendingCountVar- void $ SUT.teardown capataz- assertFailure- ( "On "- <> stageName- <> " stage, expected all assertions to match, but didn't ("- <> show pendingCount- <> " assertions remaining, "- <> show (length events)- <> " events tried)\n"- <> ppShow (zip ([0 ..] :: [Int]) events)- )- Right _ -> return ()--- -- Sub-routine that accumulates all events that have happened in the Capataz- -- instance so far- trackEvent accRef eventStream event = do- atomicModifyIORef' accRef (\old -> (event : old, ()))- atomically $ writeTQueue eventStream event-- -- Sub-routine that reads the event stream, and ensures that all assertions- -- are executed, this loop won't stop until all assertions are met- readEventLoop eventStream pendingCount assertions = do- writeIORef pendingCount (length assertions)- case assertions of- [] -> return ()- (assertionFn:assertions1) -> do- event <- atomically $ readTQueue eventStream- if assertionFn event- then readEventLoop eventStream pendingCount assertions1- else readEventLoop eventStream pendingCount assertions----- | A version of "testCapatazStreamWithOptions" that does not receive the--- function that modifies a "CapatazOptions" record.-testCapatazStream- :: [SUT.CapatazEvent -> Bool] -- ^ Assertions happening before setup function- -- is called- -> (SUT.Capataz -> IO ()) -- ^ Function used to test public the supervisor- -- public API (a.k.a setup function)- -> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the setup- -- function- -> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the capataz- -- record is terminated- -> Maybe (SUT.CapatazEvent -> Bool) -- ^ An assertion checked across all- -- @CapatazEvents@ that happened in a- -- test, great when testing that an event- -- __did not__ happen- -> IO ()-testCapatazStream preSetupAssertions =- testCapatazStreamWithOptions preSetupAssertions identity------------------------------------------------------------------------------------- Actual Tests--tests :: [TestTree]-tests- = [ testGroup- "capataz without workerSpecList"- [ testCase "initialize and teardown works as expected" $ testCapatazStream- [ andP- [ assertEventType CapatazStatusChanged- , assertCapatazStatusChanged SUT.Initializing SUT.Running- ]- ]- (const $ return ())- []- [ andP- [ assertEventType CapatazStatusChanged- , assertCapatazStatusChanged SUT.Running SUT.Halting- ]- , andP- [ assertEventType CapatazStatusChanged- , assertCapatazStatusChanged SUT.Halting SUT.Halted- ]- ]- Nothing- ]- , testGroup- "capataz with workerSpecList"- [ testCase "initialize and teardown works as expected"- $ testCapatazStreamWithOptions- [ assertWorkerStarted "A"- , assertWorkerStarted "B"- , andP- [ assertEventType CapatazStatusChanged- , assertCapatazStatusChanged SUT.Initializing SUT.Running- ]- ]- ( \supOptions -> supOptions- { SUT.capatazWorkerSpecList = [ SUT.defWorkerSpec- { SUT.workerName = "A"- , SUT.workerAction = forever- (threadDelay 10001000)- }- , SUT.defWorkerSpec- { SUT.workerName = "B"- , SUT.workerAction = forever- (threadDelay 10001000)- }- ]- }- )- (const $ return ())- []- [ andP- [ assertEventType CapatazStatusChanged- , assertCapatazStatusChanged SUT.Running SUT.Halting- ]- , assertEventType WorkersTerminationStarted- , assertWorkerTerminated "A"- , assertWorkerTerminated "B"- , assertEventType WorkersTerminationFinished- , andP- [ assertEventType CapatazStatusChanged- , assertCapatazStatusChanged SUT.Halting SUT.Halted- ]- ]- Nothing- ]- , testCase "reports error when capataz thread receives async exception"- $ testCapatazStream- [ andP- [ assertEventType CapatazStatusChanged- , assertCapatazStatusChanged SUT.Initializing SUT.Running- ]- ]- ( \SUT.Capataz { capatazAsync } -> do- threadDelay 100 -- leave enough room for capataz to start- cancelWith capatazAsync (ErrorCall "async exception")- )- [assertEventType CapatazFailed]- []- Nothing- , testCase "reports error when worker retries violate restart intensity"- $ do- lockVar <- newEmptyMVar- let (signalIntensityReached, waitTillIntensityReached) =- (putMVar lockVar (), takeMVar lockVar)- testCapatazStreamWithOptions- []- ( \supOptions -> supOptions- { SUT.onCapatazIntensityReached = signalIntensityReached- }- )- ( \capataz -> do- _workerId <- SUT.forkWorker SUT.defWorkerOptions- (throwIO RestartingWorkerError)- capataz- waitTillIntensityReached- )- [ assertEventType WorkerFailed- , assertEventType WorkerFailed- , assertEventType WorkerFailed- , assertCapatazFailedWith "CapatazIntensityReached"- ]- []- Nothing- , testGroup- "single supervised IO sub-routine"- [ testGroup- "callbacks"- [ testGroup- "workerOnCompletion"- [ testCase "does execute callback when sub-routine is completed"- $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- }- )- (return ())- capataz- return ()- )- [ andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnCompletion- ]- , assertEventType WorkerCompleted- ]- []- Nothing- , testCase "does not execute callback when sub-routine fails"- $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- }- )- (throwIO RestartingWorkerError)- capataz- return ()- )- [ andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnFailure- ]- , assertEventType WorkerFailed- ]- [assertEventType CapatazTerminated]- ( Just $ not . andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnCompletion- ]- )- , testCase "does not execute callback when sub-routine is terminated"- $ testCapatazStream- []- ( \capataz -> do- workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- }- )- (forever $ threadDelay 1000100)- capataz-- _workerId <- SUT.terminateWorker- "testing onCompletion callback"- workerId- capataz- return ()- )- [assertEventType WorkerTerminated]- [assertEventType CapatazTerminated]- ( Just $ not . andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnCompletion- ]- )- , testCase "treats as sub-routine failed if callback fails"- $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- , SUT.workerOnCompletion = throwIO TimeoutError- }- )- (return ())- capataz-- return ()- )- [ andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnCompletion- , assertErrorType "TimeoutError"- ]- , andP- [ assertEventType WorkerFailed- , assertErrorType "WorkerCallbackFailed"- ]- ]- []- Nothing- ]- , testGroup- "workerOnFailure"- [ testCase "does execute callback when sub-routine fails"- $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- }- )- (throwIO RestartingWorkerError)- capataz- return ()- )- [ andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnFailure- ]- , assertEventType WorkerFailed- ]- [assertEventType CapatazTerminated]- Nothing- , testCase "does not execute callback when sub-routine is completed"- $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- }- )- (return ())- capataz- return ()- )- [ andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnCompletion- ]- , assertEventType WorkerCompleted- ]- []- ( Just- $ not- . andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnFailure- ]- )- , testCase "does not execute callback when sub-routine is terminated"- $ testCapatazStream- []- ( \capataz -> do- workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- }- )- (forever $ threadDelay 1000100)- capataz-- SUT.terminateWorker "testing onFailure callback"- workerId- capataz- )- [assertEventType WorkerTerminated]- []- ( Just- $ not- . andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnFailure- ]- )- , testCase "treats as sub-routine failed if callback fails"- $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- , SUT.workerOnFailure = const $ throwIO TimeoutError- }- )- (throwIO RestartingWorkerError)- capataz-- return ()- )- [ andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnFailure- , assertErrorType "TimeoutError"- ]- , andP- [ assertEventType WorkerFailed- , assertErrorType "WorkerCallbackFailed"- ]- ]- []- Nothing- ]- , testGroup- "workerOnTermination"- [ testCase- "gets brutally killed when TimeoutSeconds termination policy is not met"- $ testCapatazStream- []- ( \capataz -> do- workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- , SUT.workerTerminationPolicy = SUT.TimeoutMillis 1- , SUT.workerOnTermination = forever $ threadDelay 100100- }- )- (forever $ threadDelay 10001000)- capataz-- SUT.terminateWorker "testing workerOnTermination callback"- workerId- capataz- )- [ andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnTermination- , assertErrorType "BrutallyTerminateWorkerException"- ]- , andP- [ assertEventType WorkerFailed- , assertErrorType "WorkerCallbackFailed"- , assertCallbackType SUT.OnTermination- ]- ]- []- Nothing- , testCase "does execute callback when sub-routine is terminated"- $ testCapatazStream- []- ( \capataz -> do- workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- }- )- (forever $ threadDelay 1000100)- capataz-- SUT.terminateWorker "testing workerOnTermination callback"- workerId- capataz- )- [ andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnTermination- ]- , assertEventType WorkerTerminated- ]- [assertEventType CapatazTerminated]- Nothing- , testCase "does not execute callback when sub-routine is completed"- $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- }- )- (return ())- capataz- return ()- )- [assertEventType WorkerCompleted]- []- ( Just $ not . andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnTermination- ]- )- , testCase "does not execute callback when sub-routine fails"- $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- }- )- (throwIO (ErrorCall "surprise!"))- capataz- return ()- )- [assertEventType WorkerFailed]- []- ( Just $ not . andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnTermination- ]- )- , testCase "treats as sub-routine failed if callback fails"- $ testCapatazStream- []- ( \capataz -> do- workerId <- SUT.forkWorker- ( SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Temporary- , SUT.workerOnTermination = throwIO TimeoutError- }- )- (forever $ threadDelay 10001000)- capataz-- SUT.terminateWorker "testing workerOnTermination callback"- workerId- capataz- )- [ andP- [ assertEventType WorkerCallbackExecuted- , assertCallbackType SUT.OnTermination- , assertErrorType "TimeoutError"- ]- , andP- [ assertEventType WorkerFailed- , assertErrorType "WorkerCallbackFailed"- ]- ]- []- Nothing- ]- ]- , testGroup- "with transient strategy"- [ testCase "does not restart on completion" $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerRestartStrategy = SUT.Transient }- (return ())- capataz- return ()- )- [assertEventType WorkerStarted, assertEventType WorkerCompleted]- [assertEventType CapatazTerminated]- (Just $ not . assertEventType WorkerRestarted)- , testCase "does not restart on termination" $ testCapatazStream- []- ( \capataz -> do- workerId <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerRestartStrategy = SUT.Transient }- (forever $ threadDelay 1000100)- capataz- SUT.terminateWorker "termination test (1)" workerId capataz- )- [assertEventType WorkerTerminated]- [assertEventType CapatazTerminated]- (Just $ not . assertEventType WorkerRestarted)- , testCase "does restart on failure" $ testCapatazStream- []- ( \capataz -> do- subRoutineAction <- mkFailingSubRoutine 1- _workerId <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerRestartStrategy = SUT.Transient }- subRoutineAction- capataz- return ()- )- [ assertEventType WorkerStarted- , assertEventType WorkerFailed- , andP [assertEventType WorkerRestarted, assertRestartCount (== 1)]- ]- []- Nothing- , testCase "does increase restart count on multiple failures"- $ testCapatazStream- []- ( \capataz -> do- subRoutineAction <- mkFailingSubRoutine 2- _workerId <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Transient- }- subRoutineAction- capataz- return ()- )- [ andP- [assertEventType WorkerRestarted, assertRestartCount (== 1)]- , andP- [assertEventType WorkerRestarted, assertRestartCount (== 2)]- ]- []- Nothing- ]- , testGroup- "with permanent strategy"- [ testCase "does restart on completion" $ testCapatazStream- []- ( \capataz -> do- subRoutineAction <- mkCompletingOnceSubRoutine- _workerId <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerRestartStrategy = SUT.Permanent }- subRoutineAction- capataz- return ()- )- [ assertEventType WorkerStarted- , assertEventType WorkerCompleted- , assertEventType WorkerRestarted- ]- [assertEventType CapatazTerminated]- Nothing- , testCase "does not increase restart count on multiple completions"- $ testCapatazStream- []- ( \capataz -> do- -- Note the number is two (2) given the assertion list has two `WorkerRestarted` assertions- let expectedRestartCount = 2- subRoutineAction <- mkCompletingBeforeNRestartsSubRoutine- expectedRestartCount- _workerId <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Permanent- }- subRoutineAction- capataz- return ()- )- [ andP- [assertEventType WorkerRestarted, assertRestartCount (== 1)]- , andP- [assertEventType WorkerRestarted, assertRestartCount (== 1)]- ]- []- Nothing- , testCase "does restart on termination" $ testCapatazStream- []- ( \capataz -> do- workerId <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerRestartStrategy = SUT.Permanent }- (forever $ threadDelay 10001000)- capataz- SUT.terminateWorker "testing termination (1)" workerId capataz- )- [assertEventType WorkerTerminated, assertEventType WorkerRestarted]- []- Nothing- , testCase "does increase restart count on multiple terminations" $ do- terminationCountVar <- newTVarIO (0 :: Int)- let signalWorkerTermination =- atomically (modifyTVar' terminationCountVar (+ 1))- waitWorkerTermination i = atomically $ do- n <- readTVar terminationCountVar- when (n /= i) retry- testCapatazStream- []- ( \capataz -> do- workerId <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Permanent- , SUT.workerOnTermination = signalWorkerTermination- }- (forever $ threadDelay 10001000)- capataz-- SUT.terminateWorker "testing termination (1)" workerId capataz- waitWorkerTermination 1- SUT.terminateWorker "testing termination (2)" workerId capataz- waitWorkerTermination 2- )- [ assertEventType WorkerTerminated- , andP [assertEventType WorkerRestarted, assertRestartCount (== 1)]- , assertEventType WorkerTerminated- , andP [assertEventType WorkerRestarted, assertRestartCount (== 2)]- ]- []- Nothing- , testCase "does restart on failure" $ testCapatazStream- []- ( \capataz -> do- subRoutineAction <- mkFailingSubRoutine 1- _workerId <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerRestartStrategy = SUT.Permanent }- subRoutineAction- capataz- return ()- )- [ assertEventType WorkerStarted- , assertEventType WorkerFailed- , andP [assertEventType WorkerRestarted, assertRestartCount (== 1)]- ]- []- Nothing- , testCase "does increase restart count on multiple failures"- $ testCapatazStream- []- ( \capataz -> do- subRoutineAction <- mkFailingSubRoutine 2- _workerId <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerRestartStrategy = SUT.Permanent- }- subRoutineAction- capataz- return ()- )- [ andP- [assertEventType WorkerRestarted, assertRestartCount (== 1)]- , andP- [assertEventType WorkerRestarted, assertRestartCount (== 2)]- ]- []- Nothing- ]- , testGroup- "with temporary strategy"- [ testCase "does not restart on completion" $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerRestartStrategy = SUT.Temporary }- (return ())- capataz- return ()- )- [assertEventType WorkerStarted, assertEventType WorkerCompleted]- [assertEventType CapatazTerminated]- (Just $ not . assertEventType WorkerRestarted)- , testCase "does not restart on termination" $ testCapatazStream- []- ( \capataz -> do- workerId <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerRestartStrategy = SUT.Temporary }- (forever $ threadDelay 1000100)- capataz- SUT.terminateWorker "termination test (1)" workerId capataz- threadDelay 100- )- [assertEventType WorkerStarted, assertEventType WorkerTerminated]- [assertEventType CapatazTerminated]- (Just $ not . assertEventType WorkerRestarted)- , testCase "does not restart on failure" $ testCapatazStream- []- ( \capataz -> do- _workerId <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerRestartStrategy = SUT.Temporary }- (panic "worker failed!")- capataz- threadDelay 100- )- [assertEventType WorkerStarted, assertEventType WorkerFailed]- [assertEventType CapatazTerminated]- (Just $ not . assertEventType WorkerRestarted)- ]- ]- , testGroup- "multiple supervised IO sub-routines"- [ testCase "terminates all supervised sub-routines on teardown"- $ testCapatazStream- []- ( \capataz -> do- _workerA <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerName = "A"- , SUT.workerRestartStrategy = SUT.Permanent- }- (forever $ threadDelay 1000100)- capataz--- _workerB <- SUT.forkWorker- SUT.defWorkerOptions { SUT.workerName = "B"- , SUT.workerRestartStrategy = SUT.Permanent- }- (forever $ threadDelay 1000100)- capataz-- return ()- )- [ andP [assertEventType WorkerStarted, assertWorkerName "A"]- , andP [assertEventType WorkerStarted, assertWorkerName "B"]- ]- [ andP [assertEventType WorkerTerminated, assertWorkerName "A"]- , andP [assertEventType WorkerTerminated, assertWorkerName "B"]- , assertEventType CapatazTerminated- ]- Nothing- , testGroup- "with one for one capataz restart strategy"- [ testCase "restarts failing sub-routine only"- $ testCapatazStreamWithOptions- []- ( \supOptions ->- supOptions { SUT.capatazRestartStrategy = SUT.OneForOne }- )- ( \capataz -> do- _workerA <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "A"- , SUT.workerRestartStrategy = SUT.Temporary- }- (forever $ threadDelay 1000100)- capataz-- ioB <- mkFailingSubRoutine 1-- _workerB <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "B"- , SUT.workerRestartStrategy = SUT.Permanent- }- (forever $ ioB >> threadDelay 1000100)- capataz-- return ()- )- [andP [assertEventType WorkerRestarted, assertWorkerName "B"]]- []- ( Just $ not . andP- [assertEventType WorkerRestarted, assertWorkerName "A"]- )- ]- , testGroup- "with all for one capataz restart strategy with newest first order"- [ testCase "does terminate all other workers that did not fail"- $ testCapatazStreamWithOptions- []- ( \supOptions -> supOptions- { SUT.capatazRestartStrategy = SUT.AllForOne- , SUT.capatazWorkerTerminationOrder = SUT.OldestFirst- }- )- ( \capataz -> do- -- This lockVar guarantees that workerB executes before workerA- lockVar <- newEmptyMVar-- ioA <- mkFailingSubRoutine 1-- _workerA <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "A"- , SUT.workerRestartStrategy = SUT.Permanent- }- (forever $ readMVar lockVar >> ioA)- capataz-- _workerB <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "B"- , SUT.workerRestartStrategy = SUT.Permanent- }- (putMVar lockVar () >> forever (threadDelay 10))- capataz-- return ()- )- [ andP [assertEventType WorkerStarted, assertWorkerName "A"]- , andP [assertEventType WorkerStarted, assertWorkerName "B"]- , andP [assertEventType WorkerFailed, assertWorkerName "A"]- , andP [assertEventType WorkerRestarted, assertWorkerName "A"]- , andP [assertEventType WorkerTerminated, assertWorkerName "B"]- , andP [assertEventType WorkerRestarted, assertWorkerName "B"]- ]- []- Nothing- , testCase "does not restart sub-routines that are temporary"- $ testCapatazStreamWithOptions- []- ( \supOptions -> supOptions- { SUT.capatazRestartStrategy = SUT.AllForOne- , SUT.capatazWorkerTerminationOrder = SUT.OldestFirst- }- )- ( \capataz -> do- lockVar <- newEmptyMVar-- ioA <- mkFailingSubRoutine 1-- _workerA <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "A"- , SUT.workerRestartStrategy = SUT.Permanent- }- (forever $ readMVar lockVar >> ioA)- capataz-- _workerB <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "B"- , SUT.workerRestartStrategy = SUT.Temporary- }- (putMVar lockVar () >> forever (threadDelay 10))- capataz-- return ()- )- [ andP [assertEventType WorkerStarted, assertWorkerName "A"]- , andP [assertEventType WorkerStarted, assertWorkerName "B"]- , andP [assertEventType WorkerFailed, assertWorkerName "A"]- , andP [assertEventType WorkerRestarted, assertWorkerName "A"]- , andP [assertEventType WorkerTerminated, assertWorkerName "B"]- ]- []- ( Just $ not . andP- [assertEventType WorkerRestarted, assertWorkerName "B"]- )- , testCase "restarts sub-routines that are not temporary"- $ testCapatazStreamWithOptions- []- ( \supOptions -> supOptions- { SUT.capatazRestartStrategy = SUT.AllForOne- , SUT.capatazWorkerTerminationOrder = SUT.NewestFirst- }- )- ( \capataz -> do- ioA <- mkFailingSubRoutine 1-- lockVar <- newEmptyMVar-- _workerA <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "A"- , SUT.workerRestartStrategy = SUT.Permanent- }- (forever $ readMVar lockVar >> ioA)- capataz-- _workerB <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "B"- , SUT.workerRestartStrategy = SUT.Transient- }- (putMVar lockVar () >> forever (threadDelay 10))- capataz-- return ()- )- [ andP [assertEventType WorkerRestarted, assertWorkerName "B"]- , andP [assertEventType WorkerRestarted, assertWorkerName "A"]- ]- []- Nothing- ]- , testGroup- "with all for one capataz restart strategy with oldest first order"- [ testCase "does not restart sub-routines that are temporary"- $ testCapatazStreamWithOptions- []- ( \supOptions -> supOptions- { SUT.capatazRestartStrategy = SUT.AllForOne- , SUT.capatazWorkerTerminationOrder = SUT.OldestFirst- }- )- ( \capataz -> do- ioA <- mkFailingSubRoutine 1-- -- This lockVar guarantees that workerB executes before workerA- lockVar <- newEmptyMVar-- _workerA <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "A"- , SUT.workerRestartStrategy = SUT.Permanent- }- (forever $ readMVar lockVar >> ioA)- capataz-- _workerB <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "B"- , SUT.workerRestartStrategy = SUT.Temporary- }- (putMVar lockVar () >> forever (threadDelay 10))- capataz-- return ()- )- [andP [assertEventType WorkerRestarted, assertWorkerName "A"]]- []- ( Just $ not . andP- [assertEventType WorkerRestarted, assertWorkerName "B"]- )- , testCase "restarts sub-routines that are not temporary"- $ testCapatazStreamWithOptions- []- ( \supOptions -> supOptions- { SUT.capatazRestartStrategy = SUT.AllForOne- , SUT.capatazWorkerTerminationOrder = SUT.OldestFirst- }- )- ( \capataz -> do- ioA <- mkFailingSubRoutine 1-- -- This lockVar guarantees that workerB executes before workerA- lockVar <- newEmptyMVar-- _workerA <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "A"- , SUT.workerRestartStrategy = SUT.Permanent- }- (forever $ readMVar lockVar >> ioA)- capataz-- _workerB <- SUT.forkWorker- SUT.defWorkerOptions- { SUT.workerName = "B"- , SUT.workerRestartStrategy = SUT.Transient- }- (putMVar lockVar () >> forever (threadDelay 10))- capataz-- return ()- )- [ andP [assertEventType WorkerRestarted, assertWorkerName "A"]- , andP [assertEventType WorkerRestarted, assertWorkerName "B"]- ]- []- Nothing- ]- ]- ]+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (testCase)++import Control.Concurrent.STM.TVar (modifyTVar', newTVarIO, readTVar)++import Control.Concurrent.Capataz (set)+import qualified Control.Concurrent.Capataz as SUT++import Test.Util++--------------------------------------------------------------------------------+-- Actual Tests++tests :: TestTree+tests = testGroup+ "capataz core"+ [ testGroup+ "capataz without workerOptionsList"+ [ testCase "initialize and teardown works as expected" $ testCapatazStream+ [ andP+ [ assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Initializing SUT.Running+ ]+ ]+ (const $ return ())+ []+ [ andP+ [ assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Running SUT.Halting+ ]+ , andP+ [ assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Halting SUT.Halted+ ]+ ]+ Nothing+ ]+ , testGroup+ "capataz with processSpecList"+ [ testCase "initialize and teardown of workers works as expected"+ $ testCapatazStreamWithOptions+ ( set+ SUT.supervisorProcessSpecListL+ [ SUT.workerSpec "A" (forever $ threadDelay 10001000) identity+ , SUT.workerSpec "B" (forever $ threadDelay 10001000) identity+ ]+ )+ [ assertWorkerStarted "A"+ , assertWorkerStarted "B"+ , andP+ [ assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Initializing SUT.Running+ ]+ ]+ (const $ return ())+ []+ [ andP+ [ assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Running SUT.Halting+ ]+ , assertEventType ProcessTerminationStarted+ , assertWorkerTerminated "A"+ , assertWorkerTerminated "B"+ , assertEventType ProcessTerminationFinished+ , andP+ [ assertEventType SupervisorStatusChanged+ , assertSupervisorStatusChanged SUT.Halting SUT.Halted+ ]+ ]+ Nothing+ , testCase "reports error when worker retries violate restart intensity"+ $ do+ lockVar <- newEmptyMVar+ let (signalIntensityReached, waitTillIntensityReached) =+ (putMVar lockVar (), takeMVar lockVar)+ testCapatazStreamWithOptions+ (set SUT.supervisorOnIntensityReachedL signalIntensityReached)+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions "test-worker"+ (throwIO RestartingWorkerError)+ identity+ )+ capataz+ waitTillIntensityReached+ )+ [ assertEventType ProcessFailed+ , assertEventType ProcessFailed+ , assertEventType ProcessFailed+ , assertCapatazFailedWith "SupervisorIntensityReached"+ ]+ []+ Nothing+ , testGroup+ "single supervised worker"+ [ testGroup+ "callbacks"+ [ testGroup+ "workerOnCompletion"+ [ testCase+ "does execute callback when worker sub-routine is completed"+ $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (return ())+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz+ return ()+ )+ [ andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnCompletion+ ]+ , assertEventType ProcessCompleted+ ]+ []+ Nothing+ , testCase "does not execute callback when worker sub-routine fails"+ $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions "test-worker"+ (throwIO RestartingWorkerError)+ identity+ )+ capataz+ return ()+ )+ [ andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnFailure+ ]+ , assertEventType ProcessFailed+ ]+ [assertEventType CapatazTerminated]+ ( Just $ not . andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnCompletion+ ]+ )+ , testCase+ "does not execute callback when worker sub-routine is terminated"+ $ testCapatazStream+ []+ ( \capataz -> do+ workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (forever $ threadDelay 1000100)+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz++ _workerId <- void $ SUT.terminateProcess+ "testing onCompletion callback"+ workerId+ capataz+ return ()+ )+ [assertEventType ProcessTerminated]+ [assertEventType CapatazTerminated]+ ( Just $ not . andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnCompletion+ ]+ )+ , testCase "treats as worker sub-routine failed if callback fails"+ $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (return ())+ ( set SUT.workerRestartStrategyL SUT.Temporary+ . set SUT.workerOnCompletionL (throwIO TimeoutError)+ )+ )+ capataz++ return ()+ )+ [ andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnCompletion+ , assertErrorType "TimeoutError"+ ]+ , andP+ [ assertEventType ProcessFailed+ , assertErrorType "ProcessCallbackFailed"+ ]+ ]+ []+ Nothing+ ]+ , testGroup+ "workerOnFailure"+ [ testCase "does execute callback when worker sub-routine fails"+ $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (throwIO RestartingWorkerError)+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz+ return ()+ )+ [ andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnFailure+ ]+ , assertEventType ProcessFailed+ ]+ [assertEventType CapatazTerminated]+ Nothing+ , testCase+ "does not execute callback when worker sub-routine is completed"+ $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (return ())+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz+ return ()+ )+ [ andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnCompletion+ ]+ , assertEventType ProcessCompleted+ ]+ []+ ( Just+ $ not+ . andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnFailure+ ]+ )+ , testCase+ "does not execute callback when worker sub-routine is terminated"+ $ testCapatazStream+ []+ ( \capataz -> do+ workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (forever $ threadDelay 1000100)+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz++ void $ SUT.terminateProcess "testing onFailure callback"+ workerId+ capataz+ )+ [assertEventType ProcessTerminated]+ []+ ( Just+ $ not+ . andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnFailure+ ]+ )+ , testCase "treats as worker sub-routine failed if callback fails"+ $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (throwIO RestartingWorkerError)+ ( set SUT.workerRestartStrategyL SUT.Temporary+ . set+ SUT.workerOnFailureL+ (const $ throwIO TimeoutError)+ )+ )+ capataz++ return ()+ )+ [ andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnFailure+ , assertErrorType "TimeoutError"+ ]+ , andP+ [ assertEventType ProcessFailed+ , assertErrorType "ProcessCallbackFailed"+ ]+ ]+ []+ Nothing+ ]+ , testGroup+ "workerOnTermination"+ [ testCase+ "gets brutally killed when TimeoutSeconds termination policy is not met"+ $ testCapatazStream+ []+ ( \capataz -> do+ workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (forever $ threadDelay 10001000)+ ( set SUT.workerRestartStrategyL SUT.Temporary+ . set SUT.workerTerminationPolicyL (SUT.TimeoutMillis 1)+ . set SUT.workerOnTerminationL+ (forever $ threadDelay 100100)+ )+ )+ capataz++ void $ SUT.terminateProcess "testing workerOnTermination callback"+ workerId+ capataz+ )+ [ andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnTermination+ , assertErrorType "BrutallyTerminateProcessException"+ ]+ , andP+ [ assertEventType ProcessFailed+ , assertCallbackType SUT.OnTermination+ , assertErrorType "ProcessCallbackFailed"+ ]+ ]+ []+ Nothing+ , testCase+ "does execute callback when worker sub-routine is terminated"+ $ testCapatazStream+ []+ ( \capataz -> do+ workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (forever $ threadDelay 1000100)+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz++ void $ SUT.terminateProcess "testing workerOnTermination callback"+ workerId+ capataz+ )+ [ andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnTermination+ ]+ , assertEventType ProcessTerminated+ ]+ [assertEventType CapatazTerminated]+ Nothing+ , testCase+ "does not execute callback when worker sub-routine is completed"+ $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (return ())+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz+ return ()+ )+ [assertEventType ProcessCompleted]+ []+ ( Just $ not . andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnTermination+ ]+ )+ , testCase "does not execute callback when worker sub-routine fails"+ $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (throwIO (ErrorCall "surprise!"))+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz+ return ()+ )+ [assertEventType ProcessFailed]+ []+ ( Just $ not . andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnTermination+ ]+ )+ , testCase "treats as worker sub-routine failed if callback fails"+ $ testCapatazStream+ []+ ( \capataz -> do+ workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (forever $ threadDelay 10001000)+ ( set SUT.workerRestartStrategyL SUT.Temporary+ . set SUT.workerOnTerminationL (throwIO TimeoutError)+ )+ )+ capataz++ void $ SUT.terminateProcess "testing workerOnTermination callback"+ workerId+ capataz+ )+ [ andP+ [ assertEventType ProcessCallbackExecuted+ , assertCallbackType SUT.OnTermination+ , assertErrorType "TimeoutError"+ ]+ , andP+ [ assertEventType ProcessFailed+ , assertErrorType "ProcessCallbackFailed"+ ]+ ]+ []+ Nothing+ ]+ ]+ , testGroup+ "with transient strategy"+ [ testCase "does not restart worker on completion" $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (return ())+ (set SUT.workerRestartStrategyL SUT.Transient)+ )+ capataz+ return ()+ )+ [assertEventType ProcessStarted, assertEventType ProcessCompleted]+ [assertEventType CapatazTerminated]+ (Just $ not . assertEventType ProcessRestarted)+ , testCase "does not restart worker on termination" $ testCapatazStream+ []+ ( \capataz -> do+ workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (forever $ threadDelay 1000100)+ (set SUT.workerRestartStrategyL SUT.Transient)+ )+ capataz+ void $ SUT.terminateProcess "termination test (1)" workerId capataz+ )+ [assertEventType ProcessTerminated]+ [assertEventType CapatazTerminated]+ (Just $ not . assertEventType ProcessRestarted)+ , testCase "does restart on failure" $ testCapatazStream+ []+ ( \capataz -> do+ subRoutineAction <- mkFailingSubRoutine 1+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ subRoutineAction+ (set SUT.workerRestartStrategyL SUT.Transient)+ )+ capataz+ return ()+ )+ [ assertEventType ProcessStarted+ , assertEventType ProcessFailed+ , andP [assertEventType ProcessRestarted, assertRestartCount (== 1)]+ ]+ []+ Nothing+ , testCase "does increase restart count on multiple worker failures"+ $ testCapatazStream+ []+ ( \capataz -> do+ subRoutineAction <- mkFailingSubRoutine 2+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ subRoutineAction+ (set SUT.workerRestartStrategyL SUT.Transient)+ )+ capataz+ return ()+ )+ [ andP+ [assertEventType ProcessRestarted, assertRestartCount (== 1)]+ , andP+ [assertEventType ProcessRestarted, assertRestartCount (== 2)]+ ]+ []+ Nothing+ ]+ , testGroup+ "with permanent strategy"+ [ testCase "does restart worker on completion" $ testCapatazStream+ []+ ( \capataz -> do+ subRoutineAction <- mkCompletingOnceSubRoutine+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ subRoutineAction+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz+ return ()+ )+ [ assertEventType ProcessStarted+ , assertEventType ProcessCompleted+ , assertEventType ProcessRestarted+ ]+ [assertEventType CapatazTerminated]+ Nothing+ , testCase+ "does not increase restart count on multiple worker completions"+ $ testCapatazStream+ []+ ( \capataz -> do+ -- Note the number is two (2) given the assertion list has two `ProcessRestarted` assertions+ let expectedRestartCount = 2+ subRoutineAction <- mkCompletingBeforeNRestartsSubRoutine+ expectedRestartCount+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ subRoutineAction+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz+ return ()+ )+ [ andP+ [assertEventType ProcessRestarted, assertRestartCount (== 1)]+ , andP+ [assertEventType ProcessRestarted, assertRestartCount (== 1)]+ ]+ []+ Nothing+ , testCase "does restart on worker termination" $ testCapatazStream+ []+ ( \capataz -> do+ workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (forever $ threadDelay 10001000)+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz+ void $ SUT.terminateProcess "testing termination (1)" workerId capataz+ )+ [assertEventType ProcessTerminated, assertEventType ProcessRestarted]+ []+ Nothing+ , testCase "does increase restart count on multiple worker terminations"+ $ do+ terminationCountVar <- newTVarIO (0 :: Int)+ let signalWorkerTermination =+ atomically (modifyTVar' terminationCountVar (+ 1))+ waitWorkerTermination i = atomically $ do+ n <- readTVar terminationCountVar+ when (n /= i) retry+ testCapatazStream+ []+ ( \capataz -> do+ workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (forever $ threadDelay 10001000)+ ( set SUT.workerRestartStrategyL SUT.Permanent+ . set SUT.workerOnTerminationL signalWorkerTermination+ )+ )+ capataz++ void $ SUT.terminateProcess "testing termination (1)"+ workerId+ capataz+ waitWorkerTermination 1+ void $ SUT.terminateProcess "testing termination (2)"+ workerId+ capataz+ waitWorkerTermination 2+ )+ [ assertEventType ProcessTerminated+ , andP+ [assertEventType ProcessRestarted, assertRestartCount (== 1)]+ , assertEventType ProcessTerminated+ , andP+ [assertEventType ProcessRestarted, assertRestartCount (== 2)]+ ]+ []+ Nothing+ , testCase "does restart on worker failure" $ testCapatazStream+ []+ ( \capataz -> do+ subRoutineAction <- mkFailingSubRoutine 1+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ subRoutineAction+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz+ return ()+ )+ [ assertEventType ProcessStarted+ , assertEventType ProcessFailed+ , andP [assertEventType ProcessRestarted, assertRestartCount (== 1)]+ ]+ []+ Nothing+ , testCase "does increase restart count on multiple worker failures"+ $ testCapatazStream+ []+ ( \capataz -> do+ subRoutineAction <- mkFailingSubRoutine 2+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ subRoutineAction+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz+ return ()+ )+ [ andP+ [assertEventType ProcessRestarted, assertRestartCount (== 1)]+ , andP+ [assertEventType ProcessRestarted, assertRestartCount (== 2)]+ ]+ []+ Nothing+ ]+ , testGroup+ "with temporary strategy"+ [ testCase "does not restart on worker completion" $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (return ())+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz+ return ()+ )+ [assertEventType ProcessStarted, assertEventType ProcessCompleted]+ [assertEventType CapatazTerminated]+ (Just $ not . assertEventType ProcessRestarted)+ , testCase "does not restart on worker termination" $ testCapatazStream+ []+ ( \capataz -> do+ workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "test-worker"+ (forever $ threadDelay 1000100)+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz+ void $ SUT.terminateProcess "termination test (1)" workerId capataz+ threadDelay 100+ )+ [assertEventType ProcessStarted, assertEventType ProcessTerminated]+ [assertEventType CapatazTerminated]+ (Just $ not . assertEventType ProcessRestarted)+ , testCase "does not restart on worker failure" $ testCapatazStream+ []+ ( \capataz -> do+ _workerId <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "failing-worker"+ (panic "worker failed!")+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz+ threadDelay 100+ )+ [assertEventType ProcessStarted, assertEventType ProcessFailed]+ [assertEventType CapatazTerminated]+ (Just $ not . assertEventType ProcessRestarted)+ ]+ ]+ , testGroup+ "multiple supervised IO sub-routines"+ [ testCase "terminates all supervised worker sub-routines on teardown"+ $ testCapatazStream+ []+ ( \capataz -> do+ _workerA <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "A"+ (forever $ threadDelay 1000100)+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz+++ _workerB <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "B"+ (forever $ threadDelay 1000100)+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz++ return ()+ )+ [ andP [assertEventType ProcessStarted, assertProcessName "A"]+ , andP [assertEventType ProcessStarted, assertProcessName "B"]+ ]+ [ andP [assertEventType ProcessTerminated, assertProcessName "A"]+ , andP [assertEventType ProcessTerminated, assertProcessName "B"]+ , assertEventType CapatazTerminated+ ]+ Nothing+ , testGroup+ "with one for one supervisor restart strategy"+ [ testCase "restarts failing worker sub-routine only"+ $ testCapatazStreamWithOptions+ (set SUT.supervisorRestartStrategyL SUT.OneForOne)+ []+ ( \capataz -> do+ _workerA <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "A"+ (forever $ threadDelay 1000100)+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz++ ioB <- mkFailingSubRoutine 1++ _workerB <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "B"+ (forever $ ioB >> threadDelay 1000100)+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz++ return ()+ )+ [andP [assertEventType ProcessRestarted, assertProcessName "B"]]+ []+ ( Just $ not . andP+ [assertEventType ProcessRestarted, assertProcessName "A"]+ )+ ]+ , testGroup+ "with all for one supervisor restart strategy with newest first order"+ [ testCase "does terminate all other workers that did not fail"+ $ testCapatazStreamWithOptions+ ( \supOptions ->+ supOptions+ & set SUT.supervisorRestartStrategyL SUT.AllForOne+ & set SUT.supervisorProcessTerminationOrderL SUT.OldestFirst+ )+ []+ ( \capataz -> do+ -- This lockVar guarantees that workerB executes before workerA+ lockVar <- newEmptyMVar++ ioA <- mkFailingSubRoutine 1++ _workerA <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "A"+ (forever $ readMVar lockVar >> ioA)+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz++ _workerB <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "B"+ (putMVar lockVar () >> forever (threadDelay 10))+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz++ return ()+ )+ [ andP [assertEventType ProcessStarted, assertProcessName "A"]+ , andP [assertEventType ProcessStarted, assertProcessName "B"]+ , andP [assertEventType ProcessFailed, assertProcessName "A"]+ , andP [assertEventType ProcessRestarted, assertProcessName "A"]+ , andP [assertEventType ProcessTerminated, assertProcessName "B"]+ , andP [assertEventType ProcessRestarted, assertProcessName "B"]+ ]+ []+ Nothing+ , testCase "does not restart workers that are temporary"+ $ testCapatazStreamWithOptions+ ( \supOptions ->+ supOptions+ & set SUT.supervisorRestartStrategyL SUT.AllForOne+ & set SUT.supervisorProcessTerminationOrderL SUT.OldestFirst+ )+ []+ ( \capataz -> do+ lockVar <- newEmptyMVar++ ioA <- mkFailingSubRoutine 1++ _workerA <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "A"+ (forever $ readMVar lockVar >> ioA)+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz++ _workerB <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "B"+ (putMVar lockVar () >> forever (threadDelay 10))+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz++ return ()+ )+ [ andP [assertEventType ProcessStarted, assertProcessName "A"]+ , andP [assertEventType ProcessStarted, assertProcessName "B"]+ , andP [assertEventType ProcessFailed, assertProcessName "A"]+ , andP [assertEventType ProcessRestarted, assertProcessName "A"]+ , andP [assertEventType ProcessTerminated, assertProcessName "B"]+ ]+ []+ ( Just $ not . andP+ [assertEventType ProcessRestarted, assertProcessName "B"]+ )+ , testCase "restarts workers that are not temporary"+ $ testCapatazStreamWithOptions+ ( \supOptions ->+ supOptions+ & set SUT.supervisorRestartStrategyL SUT.AllForOne+ & set SUT.supervisorProcessTerminationOrderL SUT.NewestFirst+ )+ []+ ( \capataz -> do+ ioA <- mkFailingSubRoutine 1++ lockVar <- newEmptyMVar++ _workerA <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "A"+ (forever $ readMVar lockVar >> ioA)+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz++ _workerB <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "B"+ (putMVar lockVar () >> forever (threadDelay 10))+ (set SUT.workerRestartStrategyL SUT.Transient)+ )+ capataz++ return ()+ )+ [ andP [assertEventType ProcessRestarted, assertProcessName "B"]+ , andP [assertEventType ProcessRestarted, assertProcessName "A"]+ ]+ []+ Nothing+ ]+ , testGroup+ "with all for one capataz restart strategy with oldest first order"+ [ testCase "does not restart workers that are temporary"+ $ testCapatazStreamWithOptions+ ( \supOptions ->+ supOptions+ & set SUT.supervisorRestartStrategyL SUT.AllForOne+ & set SUT.supervisorProcessTerminationOrderL SUT.OldestFirst+ )+ []+ ( \capataz -> do+ ioA <- mkFailingSubRoutine 1++ -- This lockVar guarantees that workerB executes before workerA+ lockVar <- newEmptyMVar++ _workerA <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "A"+ (forever $ readMVar lockVar >> ioA)+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz++ _workerB <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "B"+ (putMVar lockVar () >> forever (threadDelay 10))+ (set SUT.workerRestartStrategyL SUT.Temporary)+ )+ capataz++ return ()+ )+ [andP [assertEventType ProcessRestarted, assertProcessName "A"]]+ []+ ( Just $ not . andP+ [assertEventType ProcessRestarted, assertProcessName "B"]+ )+ , testCase "restarts workers that are not temporary"+ $ testCapatazStreamWithOptions+ ( \supOptions ->+ supOptions+ & set SUT.supervisorRestartStrategyL SUT.AllForOne+ & set SUT.supervisorProcessTerminationOrderL SUT.OldestFirst+ )+ []+ ( \capataz -> do+ ioA <- mkFailingSubRoutine 1++ -- This lockVar guarantees that workerB executes before workerA+ lockVar <- newEmptyMVar++ _workerA <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "A"+ (forever $ readMVar lockVar >> ioA)+ (set SUT.workerRestartStrategyL SUT.Permanent)+ )+ capataz++ _workerB <- SUT.forkWorker+ ( SUT.buildWorkerOptions+ "B"+ (putMVar lockVar () >> forever (threadDelay 10))+ (set SUT.workerRestartStrategyL SUT.Transient)+ )+ capataz++ return ()+ )+ [ andP [assertEventType ProcessRestarted, assertProcessName "A"]+ , andP [assertEventType ProcessRestarted, assertProcessName "B"]+ ]+ []+ Nothing+ ]+ ]+ ]+ ]
test/testsuite/Main.hs view
@@ -7,12 +7,15 @@ import Protolude -import Control.Concurrent.CapatazTest-import Test.Tasty (defaultMainWithIngredients, testGroup)-import Test.Tasty.Ingredients.Rerun (rerunningTests)-import Test.Tasty.Runners (consoleTestReporter, listingTests)+import qualified Control.Concurrent.Capataz.SupervisorTest as Supervisor+import qualified Control.Concurrent.CapatazTest as Capataz+import Test.Tasty+ (defaultMainWithIngredients, testGroup)+import Test.Tasty.Ingredients.Rerun (rerunningTests)+import Test.Tasty.Runners+ (consoleTestReporter, listingTests) main :: IO () main = defaultMainWithIngredients [rerunningTests [listingTests, consoleTestReporter]]- (testGroup "capataz" tests)+ (testGroup "capataz" [Capataz.tests, Supervisor.tests])
+ test/testsuite/Test/Util.hs view
@@ -0,0 +1,412 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+module Test.Util where++import Protolude++import Control.Concurrent.STM.TQueue (newTQueueIO, readTQueue, writeTQueue)+import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)+import Test.Tasty.HUnit (assertBool, assertFailure)+import Text.Show.Pretty (ppShow)++import qualified Data.Text as T++import qualified Control.Concurrent.Capataz as SUT+import qualified Control.Concurrent.Capataz.Event as SUT++--------------------------------------------------------------------------------+-- Util++-- | Utility function that gets the type name of a Record through it's Show+-- output.+fetchRecordName :: Show a => a -> Text+fetchRecordName = T.takeWhile (/= ' ') . show++-- | Composes two predicate functions together with a boolean AND+andP :: [a -> Bool] -> a -> Bool+andP predList a = all ($ a) predList++--------------------------------------------------------------------------------+-- Assertions and Testers++-- | This record duplicate the same event names as the ones found in the+-- "CapatazEvent" type, we use this to avoid using Text comparisons on assertion+-- helper functions. The "CapatazEvent" record is imported qualified, so there+-- is no conflict happening.+data EventType+ = InvalidCapatazStatusReached+ | SupervisorStatusChanged+ | ProcessTerminated+ | ProcessStarted+ | ProcessRestarted+ | ProcessCompleted+ | ProcessFailed+ | ProcessCallbackExecuted+ | ProcessTerminationStarted+ | ProcessTerminationFinished+ | CapatazFailed+ | CapatazTerminated+ deriving (Show, Eq)++toEventType :: SUT.CapatazEvent -> EventType+toEventType ev = case ev of+ SUT.InvalidSupervisorStatusReached{} -> InvalidCapatazStatusReached+ SUT.SupervisorStatusChanged{} -> SupervisorStatusChanged+ SUT.ProcessTerminated{} -> ProcessTerminated+ SUT.ProcessStarted{} -> ProcessStarted+ SUT.ProcessRestarted{} -> ProcessRestarted+ SUT.ProcessCompleted{} -> ProcessCompleted+ SUT.ProcessFailed{} -> ProcessFailed+ SUT.ProcessCallbackExecuted{} -> ProcessCallbackExecuted+ SUT.ProcessTerminationStarted{} -> ProcessTerminationStarted+ SUT.ProcessTerminationFinished{} -> ProcessTerminationFinished+ SUT.CapatazFailed{} -> CapatazFailed+ SUT.CapatazTerminated{} -> CapatazTerminated++-- | Predicate function to assert "CapatazEvent" types+assertEventType :: EventType -> SUT.CapatazEvent -> Bool+assertEventType evType ev = toEventType ev == evType++-- | Predicate function to assert "CapatazEvent" process type+assertProcessType :: SUT.ProcessType -> SUT.CapatazEvent -> Bool+assertProcessType processTy ev = case ev of+ SUT.ProcessFailed { processType } -> processTy == processType+ SUT.ProcessTerminated { processType } -> processTy == processType+ SUT.ProcessStarted { processType } -> processTy == processType+ SUT.ProcessCallbackExecuted { processType } -> processTy == processType+ SUT.ProcessRestarted { processType } -> processTy == processType+ _ -> False++-- | Predicate function to assert "CapatazEvent" worker name+assertProcessName :: Text -> SUT.CapatazEvent -> Bool+assertProcessName processName' ev = case ev of+ SUT.ProcessRestarted { processName } -> processName' == processName+ SUT.ProcessFailed { processName } -> processName' == processName+ SUT.ProcessTerminated { processName } -> processName' == processName+ SUT.ProcessStarted { processName } -> processName' == processName+ _ -> False++-- | Predicate function to assert type of an error inside a "CapatazEvent"+assertErrorType :: Text -> SUT.CapatazEvent -> Bool+assertErrorType errType ev = case ev of+ SUT.CapatazFailed { supervisorError } ->+ fetchRecordName supervisorError == errType+ SUT.ProcessFailed { processError } -> fetchRecordName processError == errType+ SUT.ProcessCallbackExecuted { processCallbackError } ->+ case processCallbackError of+ Nothing -> False+ Just originalError -> fetchRecordName originalError == errType+ _ -> False++-- | Predicate function to assert type of callback executed inside a+-- "CapatazEvent"+assertCallbackType :: SUT.CallbackType -> SUT.CapatazEvent -> Bool+assertCallbackType cbType ev = case ev of+ SUT.ProcessFailed { processError } -> case fromException processError of+ Just SUT.ProcessCallbackFailed { processCallbackType } ->+ cbType == processCallbackType+ _ -> False+ SUT.ProcessCallbackExecuted { processCallbackType } ->+ cbType == processCallbackType+ _ -> False++-- | Predicate function to assert restart count inside a "CapatazEvent"+assertRestartCount :: (Int -> Bool) -> SUT.CapatazEvent -> Bool+assertRestartCount predFn ev = case ev of+ SUT.ProcessRestarted { processRestartCount } -> predFn processRestartCount+ _ -> False++-- | Predicate function to assert a Capataz status change+assertSupervisorStatusChanged+ :: SUT.SupervisorStatus -> SUT.SupervisorStatus -> SUT.CapatazEvent -> Bool+assertSupervisorStatusChanged fromEv toEv ev = case ev of+ SUT.SupervisorStatusChanged { prevSupervisorStatus, newSupervisorStatus } ->+ fromEv == prevSupervisorStatus && toEv == newSupervisorStatus+ _ -> False++-- | Predicate function to assert process was supervised by a a supervisor+-- indicated by name+assertSupervisorName :: Text -> SUT.CapatazEvent -> Bool+assertSupervisorName supervisorName' ev = case ev of+ SUT.SupervisorStatusChanged { supervisorName } ->+ supervisorName' == supervisorName+ SUT.ProcessStarted { supervisorName } -> supervisorName' == supervisorName+ SUT.ProcessTerminated { supervisorName } -> supervisorName' == supervisorName+ SUT.ProcessRestarted { supervisorName } -> supervisorName' == supervisorName+ SUT.ProcessCompleted { supervisorName } -> supervisorName' == supervisorName+ SUT.ProcessCallbackExecuted { supervisorName } ->+ supervisorName' == supervisorName+ SUT.ProcessTerminationStarted { supervisorName } ->+ supervisorName' == supervisorName+ SUT.ProcessTerminationFinished { supervisorName } ->+ supervisorName' == supervisorName+ _ -> False++-- | Predicate function to assert a supervisor was started+assertSupervisorStarted :: Text -> SUT.CapatazEvent -> Bool+assertSupervisorStarted supervisorName = andP+ [ assertEventType ProcessStarted+ , assertProcessType SUT.SupervisorType+ , assertProcessName supervisorName+ ]++-- | Predicate function to assert a supervisor was terminated+assertSupervisorTerminated :: Text -> SUT.CapatazEvent -> Bool+assertSupervisorTerminated supervisorName = andP+ [ assertEventType ProcessTerminated+ , assertProcessType SUT.SupervisorType+ , assertProcessName supervisorName+ ]++assertSupervisorFailed :: Text -> SUT.CapatazEvent -> Bool+assertSupervisorFailed supervisorName = andP+ [ assertEventType ProcessFailed+ , assertProcessType SUT.SupervisorType+ , assertProcessName supervisorName+ ]++assertSupervisorRestarted :: Text -> SUT.CapatazEvent -> Bool+assertSupervisorRestarted supervisorName = andP+ [ assertEventType ProcessRestarted+ , assertProcessType SUT.SupervisorType+ , assertProcessName supervisorName+ ]++-- | Predicate function to assert a worker was started+assertWorkerStarted :: Text -> SUT.CapatazEvent -> Bool+assertWorkerStarted workerName = andP+ [ assertEventType ProcessStarted+ , assertProcessType SUT.WorkerType+ , assertProcessName workerName+ ]++-- | Predicate function to assert a worker was terminated+assertWorkerTerminated :: Text -> SUT.CapatazEvent -> Bool+assertWorkerTerminated workerName = andP+ [ assertEventType ProcessTerminated+ , assertProcessType SUT.WorkerType+ , assertProcessName workerName+ ]++assertWorkerFailed :: Text -> SUT.CapatazEvent -> Bool+assertWorkerFailed workerName = andP+ [ assertEventType ProcessFailed+ , assertProcessType SUT.WorkerType+ , assertProcessName workerName+ ]++assertWorkerRestarted :: Text -> SUT.CapatazEvent -> Bool+assertWorkerRestarted workerName = andP+ [ assertEventType ProcessRestarted+ , assertProcessType SUT.WorkerType+ , assertProcessName workerName+ ]++-- | Predicate function to assert a capataz thread failed with error type+assertCapatazFailedWith :: Text -> SUT.CapatazEvent -> Bool+assertCapatazFailedWith errorName =+ andP [assertEventType CapatazFailed, assertErrorType errorName]++--------------------------------------------------------------------------------++-- | Exception used to test failures inside Worker sub-routines+data RestartingWorkerError+ = RestartingWorkerError+ deriving (Show)++instance Exception RestartingWorkerError++-- | Exception used to test failures inside Worker callback sub-routines+data TimeoutError+ = TimeoutError+ deriving (Show)++instance Exception TimeoutError++-- | Utility function to create a Worker sub-routine that fails at least a+-- number of times+mkFailingSubRoutine+ :: Int -- ^ Number of times the Worker sub-routine will fail+ -> IO (IO ()) -- ^ Sub-routine used on worker creation+mkFailingSubRoutine failCount = do+ countRef <- newIORef failCount+ let subRoutine = do+ shouldFail <- atomicModifyIORef' countRef+ (\count -> (pred count, count > 0))+ when shouldFail (throwIO RestartingWorkerError)++ return subRoutine++-- | A sub-routine that will complete for `initCount` amount of times. This+-- function works great when testing `Permanent` strategies, as you would like+-- to assert restart events once (if it keeps completing it will fill up the log+-- with restart events)+mkCompletingBeforeNRestartsSubRoutine :: Int -> IO (IO ())+mkCompletingBeforeNRestartsSubRoutine initCount = do+ countRef <- newIORef initCount+ let subRoutine = do+ shouldStop <- atomicModifyIORef' countRef+ (\count -> (pred count, count > 0))+ if shouldStop then return () else forever $ threadDelay 1000100+ return subRoutine++-- | A sub-routine that will complete once. This function works great when+-- testing `Permanent` strategies, as you would like to assert restart events+-- once (if it keeps completing it will fill up the log with restart events)+mkCompletingOnceSubRoutine :: IO (IO ())+mkCompletingOnceSubRoutine = mkCompletingBeforeNRestartsSubRoutine 1++-- | Utility function to build a test environment for a Capataz execution.+-- It is composed by:+--+-- * List of assertions that represent events that should be triggered by the+-- capataz instance in order+--+-- * A function to modify the default "CapatazOptions", this utility function injects+-- a special @notifyEvent@ callback to execute given assertions.+testCapatazStreamWithOptions+ :: (SUT.CapatazOptions -> SUT.CapatazOptions) -- ^ Function to modify default+ -- @CapatazOptions@+ -> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening before setup function+ -- is called+ -> (SUT.Capataz -> IO ()) -- ^ Function used to test public the supervisor+ -- public API (a.k.a setup function)+ -> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the setup+ -- function+ -> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the capataz+ -- record is terminated+ -> Maybe (SUT.CapatazEvent -> Bool) -- ^ An assertion checked across all+ -- @CapatazEvents@ that happened in a+ -- test, great when testing that an event+ -- __did not__ happen+ -> IO ()+testCapatazStreamWithOptions optionModFn preSetupAssertion setupFn postSetupAssertions postTeardownAssertions mAllEventsAssertion+ = do++ eventStream <- newTQueueIO+ accRef <- newIORef []+ pendingCountVar <- newIORef+ ( sum $ fmap+ length+ [preSetupAssertion, postSetupAssertions, postTeardownAssertions]+ )++ capataz <-+ SUT.forkCapataz+ ( SUT.set SUT.onSystemEventL (trackEvent accRef eventStream)+ . optionModFn+ )++ -- We check preSetup assertions are met before we execute the setup+ -- function. This serves to test initialization of capataz instance+ runAssertions "PRE-SETUP"+ (eventStream, accRef)+ pendingCountVar+ preSetupAssertion+ capataz++ -- We execute the setup sub-routine, which is going to use the Capataz public+ -- API to assert events+ setupResult <- try (setupFn capataz)++ case setupResult of+ -- If the sub-routine fails, show exception+ Left err -> assertFailure (show (err :: SomeException))+ Right _ -> do+ -- We now run post-setup assertions+ runAssertions "POST-SETUP"+ (eventStream, accRef)+ pendingCountVar+ postSetupAssertions+ capataz++ -- We now shutdown the capataz instance+ void $ SUT.teardown capataz++ -- We run assertions for after the capataz has been shut down+ runAssertions "POST-TEARDOWN"+ (eventStream, accRef)+ pendingCountVar+ postTeardownAssertions+ capataz++ -- Lastly, we check if there is a function that we want to execute+ -- across all events that happened in the test, this is to assert the+ -- absence of an event+ case mAllEventsAssertion of+ Nothing -> return ()+ Just allEventsAssertion -> do+ events <- reverse <$> readIORef accRef+ assertBool+ ( "On AFTER-TEST, expected all events to match predicate, but didn't ("+ <> show (length events)+ <> " events tried)\n"+ <> ppShow (zip ([0 ..] :: [Int]) events)+ )+ (all allEventsAssertion events)+ where+ -- Utility functions that runs the readEventLoop function with a timeout+ -- of a second, this way we can guarantee assertions are met without having+ -- to add @threadDelays@ to the test execution+ runAssertions stageName (eventStream, accRef) pendingCountVar assertions capataz+ = do+ raceResult <- race+ (threadDelay 1000100)+ (readEventLoop eventStream pendingCountVar assertions)+ case raceResult of+ Left _ -> do+ events <- reverse <$> readIORef accRef+ pendingCount <- readIORef pendingCountVar+ void $ SUT.teardown capataz+ assertFailure+ ( "On "+ <> stageName+ <> " stage, expected all assertions to match, but didn't ("+ <> show pendingCount+ <> " assertions remaining, "+ <> show (length events)+ <> " events tried)\n"+ <> ppShow (zip ([0 ..] :: [Int]) events)+ )+ Right _ -> return ()+++ -- Sub-routine that accumulates all events that have happened in the Capataz+ -- instance so far+ trackEvent accRef eventStream event = do+ atomicModifyIORef' accRef (\old -> (event : old, ()))+ atomically $ writeTQueue eventStream event++ -- Sub-routine that reads the event stream, and ensures that all assertions+ -- are executed, this loop won't stop until all assertions are met+ readEventLoop eventStream pendingCount assertions = do+ writeIORef pendingCount (length assertions)+ case assertions of+ [] -> return ()+ (assertionFn:assertions1) -> do+ event <- atomically $ readTQueue eventStream+ if assertionFn event+ then readEventLoop eventStream pendingCount assertions1+ else readEventLoop eventStream pendingCount assertions+++-- | A version of "testCapatazStreamWithOptions" that does not receive the+-- function that modifies a "CapatazOptions" record.+testCapatazStream+ :: [SUT.CapatazEvent -> Bool] -- ^ Assertions happening before setup function+ -- is called+ -> (SUT.Capataz -> IO ()) -- ^ Function used to test public the supervisor+ -- public API (a.k.a setup function)+ -> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the setup+ -- function+ -> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the capataz+ -- record is terminated+ -> Maybe (SUT.CapatazEvent -> Bool) -- ^ An assertion checked across all+ -- @CapatazEvents@ that happened in a+ -- test, great when testing that an event+ -- __did not__ happen+ -> IO ()+testCapatazStream preSetupAssertions =+ testCapatazStreamWithOptions identity preSetupAssertions