hurl-workbench-core-0.1.0.0: src/HurlWorkbench/Service/Group.hs
-- | Dynamic group membership is the monitor generation. Removing an entry and
-- observing its exit happen in one STM transaction; no stale monitor can target
-- a replacement acquired by Restart.
module HurlWorkbench.Service.Group
( ServiceGroup,
LifecycleOutcome (..),
newServiceGroup,
startNamed,
startNamedObserved,
stopNamed,
applyServiceStep,
applyServiceStepObserved,
watchServices,
cleanupServices,
)
where
import Control.Concurrent.Async (race)
import Control.Concurrent.STM
import Control.Exception (mask)
import Data.Generics.Labels ()
import Data.Time.Clock (NominalDiffTime, diffUTCTime, getCurrentTime)
import HurlWorkbench.Prelude
import HurlWorkbench.Process.Run
import HurlWorkbench.Service.Resolve
import HurlWorkbench.Service.Run (awaitService)
import HurlWorkbench.Workspace.Types (ServiceName (..), ServiceStep (..))
-- Newest acquisition first, including services still in readiness.
data Entry = Entry !ResolvedService !OwnedProcess !Bool
newtype ServiceGroup = ServiceGroup (TVar [Entry])
data LifecycleOutcome = LifecycleOutcome
{ name :: !ServiceName,
action :: !Text,
status :: !Text,
elapsed :: !NominalDiffTime,
failure :: !(Maybe ServiceError)
}
deriving stock (Generic, Eq, Show)
newServiceGroup :: IO ServiceGroup
newServiceGroup = ServiceGroup <$> newTVarIO []
unexpected :: ServiceGroup -> STM ServiceError
unexpected (ServiceGroup entries) = do
active <- readTVar entries
foldr
orElse
retry
[ ServiceExitedDuringCallback (service ^. #name) <$> waitExitCodeSTM owned
| Entry service owned True <- active
]
watchServices :: ServiceGroup -> IO (Either ServiceError a) -> IO (Either ServiceError a)
watchServices group action = race (atomically (unexpected group)) action <&> either Left id
startNamed :: ServiceGroup -> ResolvedService -> IO (Either ServiceError ())
startNamed group service = startNamedObserved group service (const (pure ()))
startNamedObserved :: ServiceGroup -> ResolvedService -> (LifecycleOutcome -> IO ()) -> IO (Either ServiceError ())
startNamedObserved group@(ServiceGroup entries) service observe = mask $ \restore -> do
began <- getCurrentTime
let emit action began_ result = do
ended <- getCurrentTime
observe (LifecycleOutcome (service ^. #name) action (either (const "failed") (const "passed") result) (diffUTCTime ended began_) (either Just (const Nothing) result))
spawnGroup (service ^. #name) (commandConfig (service ^. #processConfig)) >>= \case
Left err -> emit "start" began (Left err) >> pure (Left err)
Right owned -> do
-- Ownership is registered before readiness becomes interruptible.
atomically (modifyTVar' entries (Entry service owned False :))
emit "start" began (Right ())
readyBegan <- getCurrentTime
result <- restore (watchServices group (awaitService service owned))
emit "readiness" readyBegan result
case result of
Left err -> pure (Left err)
Right () -> do
atomically (modifyTVar' entries (map (markReady (owned ^. #processId))))
pure (Right ())
where
markReady pid entry@(Entry definition process _) =
if process ^. #processId == pid then Entry definition process True else entry
stopNamed :: ServiceGroup -> ServiceName -> IO (Either ServiceError ())
stopNamed (ServiceGroup entries) name = mask $ \_ -> do
retired <- atomically $ do
active <- readTVar entries
-- Check every current leader before retiring the intentional generation.
exits <- for active $ \(Entry service owned ready) -> do
exitCode <- getExitCodeSTM owned
pure [ServiceExitedDuringCallback (service ^. #name) code | ready, Just code <- [exitCode]]
case concat exits of
err : _ -> pure (Left err)
[] -> case [entry | entry@(Entry service _ _) <- active, service ^. #name == name] of
[] -> pure (Left (ServiceShutdownFailed name Nothing "service is not running"))
entry : _ -> do
-- Keep ownership until termination finishes, but retire the monitor.
modifyTVar' entries (map (\entry_@(Entry service owned _) -> if service ^. #name == name then Entry service owned False else entry_))
pure (Right entry)
case retired of
Left err -> pure (Left err)
Right (Entry service owned _) -> do
result <- terminateGroup name (service ^. #shutdownTimeout) owned
case result of
Left err -> pure (Left err)
Right () -> do
atomically (modifyTVar' entries (filter (\(Entry _ process _) -> process ^. #processId /= owned ^. #processId)))
pure (Right ())
applyServiceStep :: ServiceGroup -> [ResolvedService] -> ServiceStep -> IO (Either ServiceError ())
applyServiceStep group definitions operation = applyServiceStepObserved group definitions operation (const (pure ()))
applyServiceStepObserved :: ServiceGroup -> [ResolvedService] -> ServiceStep -> (LifecycleOutcome -> IO ()) -> IO (Either ServiceError ())
applyServiceStepObserved group definitions operation observe = case operation of
Stop name -> watchServices group (stop name)
Start name -> watchServices group (start name)
Restart name ->
watchServices group $
stop name >>= \case
Left err -> pure (Left err)
Right () -> start name
where
stop name = do
began <- getCurrentTime
result <- stopNamed group name
ended <- getCurrentTime
observe (LifecycleOutcome name "stop" (either (const "failed") (const "passed") result) (diffUTCTime ended began) (either Just (const Nothing) result))
pure result
start name = case filter ((== name) . view #name) definitions of
service : _ -> startNamedObserved group service observe
[] -> pure (Left (ServiceProcessIdUnavailable name))
cleanupServices :: ServiceGroup -> IO [LifecycleOutcome]
cleanupServices (ServiceGroup entries) = do
active <- readTVarIO entries
results <- for active $ \(Entry service owned _) -> do
began <- getCurrentTime
result <- terminateGroup (service ^. #name) (service ^. #shutdownTimeout) owned
ended <- getCurrentTime
pure (LifecycleOutcome (service ^. #name) "cleanup" (either (const "failed") (const "passed") result) (diffUTCTime ended began) (either Just (const Nothing) result))
atomically (writeTVar entries [])
pure results