fix: shutdown behaviour & tests
This commit is contained in:
parent
975ebc65c1
commit
19b8b0616f
@ -587,9 +587,11 @@ shutdownApp :: (MonadIO m, MonadUnliftIO m) => UniWorX -> m ()
|
|||||||
shutdownApp app = do
|
shutdownApp app = do
|
||||||
stopJobCtl app
|
stopJobCtl app
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
for_ (appWidgetMemcached app) Memcached.close
|
|
||||||
for_ (appSmtpPool app) destroyAllResources
|
|
||||||
destroyAllResources $ appConnPool app
|
destroyAllResources $ appConnPool app
|
||||||
|
for_ (appSmtpPool app) destroyAllResources
|
||||||
|
for_ (appLdapPool app) . mapFailover $ views _2 destroyAllResources
|
||||||
|
for_ (appWidgetMemcached app) Memcached.close
|
||||||
|
for_ (appMemcached app) $ views _2 Memcached.close
|
||||||
release . fst $ appLogger app
|
release . fst $ appLogger app
|
||||||
|
|
||||||
liftIO $ threadDelay 1e6
|
liftIO $ threadDelay 1e6
|
||||||
|
|||||||
@ -73,7 +73,7 @@ import Data.Ratio as Import ((%))
|
|||||||
|
|
||||||
import Net.IP as Import (IP)
|
import Net.IP as Import (IP)
|
||||||
|
|
||||||
import Database.Persist.Sql as Import (SqlReadT, SqlWriteT, IsSqlBackend, fromSqlKey, toSqlKey)
|
import Database.Persist.Sql as Import (SqlReadBackend, SqlReadT, SqlWriteT, IsSqlBackend, fromSqlKey, toSqlKey)
|
||||||
|
|
||||||
import Ldap.Client.Pool as Import
|
import Ldap.Client.Pool as Import
|
||||||
|
|
||||||
|
|||||||
113
src/Jobs.hs
113
src/Jobs.hs
@ -44,7 +44,7 @@ import Data.Time.Zones
|
|||||||
import Control.Concurrent.STM (retry)
|
import Control.Concurrent.STM (retry)
|
||||||
import Control.Concurrent.STM.Delay
|
import Control.Concurrent.STM.Delay
|
||||||
|
|
||||||
import UnliftIO.Concurrent (forkIO)
|
import UnliftIO.Concurrent (forkIO, myThreadId)
|
||||||
|
|
||||||
|
|
||||||
import Jobs.Handler.SendNotification
|
import Jobs.Handler.SendNotification
|
||||||
@ -90,9 +90,9 @@ handleJobs :: ( MonadResource m
|
|||||||
handleJobs foundation@UniWorX{..}
|
handleJobs foundation@UniWorX{..}
|
||||||
| foundation ^. _appJobWorkers == 0 = return ()
|
| foundation ^. _appJobWorkers == 0 = return ()
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
jobPoolManager <- allocateLinkedAsyncWithUnmask $ \unmask -> manageJobPool foundation unmask
|
jobPoolManager <- allocateLinkedAsyncWithUnmask $ manageJobPool foundation
|
||||||
|
|
||||||
jobCron <- allocateLinkedAsync $ manageCrontab foundation
|
jobCron <- allocateLinkedAsyncWithUnmask $ manageCrontab foundation
|
||||||
|
|
||||||
let jobWorkers = Map.empty
|
let jobWorkers = Map.empty
|
||||||
jobWorkerName = const $ error "Unknown worker"
|
jobWorkerName = const $ error "Unknown worker"
|
||||||
@ -105,18 +105,29 @@ handleJobs foundation@UniWorX{..}
|
|||||||
}
|
}
|
||||||
|
|
||||||
manageCrontab :: forall m.
|
manageCrontab :: forall m.
|
||||||
MonadResource m
|
( MonadResource m
|
||||||
=> UniWorX -> m ()
|
, MonadUnliftIO m
|
||||||
manageCrontab foundation@UniWorX{..} = do
|
)
|
||||||
context <- atomically . fmap jobContext $ readTMVar appJobState
|
=> UniWorX -> (forall a. m a -> m a) -> m ()
|
||||||
let awaitTermination = atomically $ do
|
manageCrontab foundation@UniWorX{..} unmask = do
|
||||||
shouldTerminate <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
|
ch <- allocateLinkedAsync $ do
|
||||||
guard shouldTerminate
|
context <- atomically . fmap jobContext $ readTMVar appJobState
|
||||||
liftIO . race_ awaitTermination . unsafeHandler foundation . void $ do
|
liftIO . unsafeHandler foundation . void $ do
|
||||||
atomically . assertM_ (not . Map.null . jobWorkers) $ readTMVar appJobState
|
atomically . assertM_ (not . Map.null . jobWorkers) $ readTMVar appJobState
|
||||||
runReaderT ?? foundation $
|
runReaderT ?? foundation $
|
||||||
writeJobCtlBlock JobCtlDetermineCrontab
|
writeJobCtlBlock JobCtlDetermineCrontab
|
||||||
evalRWST (forever execCrontab) context HashMap.empty
|
void $ evalRWST (forever execCrontab) context HashMap.empty
|
||||||
|
|
||||||
|
let awaitTermination = guardM $
|
||||||
|
readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
|
||||||
|
|
||||||
|
handleAny (throwTo $ asyncThreadId ch) . unmask $ do
|
||||||
|
doCancel <- atomically $ asum
|
||||||
|
[ False <$ waitSTM ch
|
||||||
|
, True <$ awaitTermination
|
||||||
|
]
|
||||||
|
when doCancel $
|
||||||
|
cancel ch
|
||||||
|
|
||||||
manageJobPool :: forall m.
|
manageJobPool :: forall m.
|
||||||
( MonadResource m
|
( MonadResource m
|
||||||
@ -125,18 +136,25 @@ manageJobPool :: forall m.
|
|||||||
, MonadMask m
|
, MonadMask m
|
||||||
)
|
)
|
||||||
=> UniWorX -> (forall a. m a -> m a) -> m ()
|
=> UniWorX -> (forall a. m a -> m a) -> m ()
|
||||||
manageJobPool foundation@UniWorX{..} unmask = shutdownOnException $
|
manageJobPool foundation@UniWorX{..} unmask = shutdownOnException $ \routeExc ->
|
||||||
flip runContT return . forever . join . atomically $ asum
|
flip runContT return . callCC $ \terminate' ->
|
||||||
[ spawnMissingWorkers
|
forever . join . lift . routeExc . atomically $ asum
|
||||||
, reapDeadWorkers
|
[ spawnMissingWorkers
|
||||||
, terminateGracefully
|
, reapDeadWorkers
|
||||||
]
|
, terminateGracefully terminate'
|
||||||
|
]
|
||||||
where
|
where
|
||||||
shutdownOnException :: m a -> m a
|
shutdownOnException :: ((forall m'. Monad m' => m (m' ()) -> m (m' ())) -> m a) -> m a
|
||||||
shutdownOnException act = do
|
shutdownOnException act = do
|
||||||
actAsync <- allocateLinkedAsyncMasked act
|
me <- myThreadId
|
||||||
|
let
|
||||||
|
routeExc :: forall m'. Monad m' => (forall b. m b -> m b) -> m (m' ()) -> m (m' ())
|
||||||
|
routeExc unmask' = handleAll (\exc -> return () <$ throwTo me exc) . unmask'
|
||||||
|
|
||||||
|
actAsync <- allocateLinkedAsyncWithUnmask $ \unmask' -> act (routeExc unmask')
|
||||||
|
|
||||||
let handleExc e = do
|
let handleExc e = do
|
||||||
|
$logWarnS "manageJobPool" [st|Received exception: #{displayException e}|]
|
||||||
atomically $ do
|
atomically $ do
|
||||||
jState <- tryReadTMVar appJobState
|
jState <- tryReadTMVar appJobState
|
||||||
for_ jState $ \JobState{jobShutdown} -> tryPutTMVar jobShutdown ()
|
for_ jState $ \JobState{jobShutdown} -> tryPutTMVar jobShutdown ()
|
||||||
@ -149,7 +167,7 @@ manageJobPool foundation@UniWorX{..} unmask = shutdownOnException $
|
|||||||
num :: Int
|
num :: Int
|
||||||
num = fromIntegral $ foundation ^. _appJobWorkers
|
num = fromIntegral $ foundation ^. _appJobWorkers
|
||||||
|
|
||||||
spawnMissingWorkers, reapDeadWorkers, terminateGracefully :: STM (ContT () m ())
|
spawnMissingWorkers, reapDeadWorkers :: STM (ContT () m ())
|
||||||
spawnMissingWorkers = do
|
spawnMissingWorkers = do
|
||||||
shouldTerminate' <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
|
shouldTerminate' <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
|
||||||
guard $ not shouldTerminate'
|
guard $ not shouldTerminate'
|
||||||
@ -178,8 +196,8 @@ manageJobPool foundation@UniWorX{..} unmask = shutdownOnException $
|
|||||||
runWorker = unsafeHandler foundation . flip runReaderT (jobContext oldState) $ do
|
runWorker = unsafeHandler foundation . flip runReaderT (jobContext oldState) $ do
|
||||||
$logInfoS logIdent "Started"
|
$logInfoS logIdent "Started"
|
||||||
runConduit $ streamChan .| handleJobs' workerId
|
runConduit $ streamChan .| handleJobs' workerId
|
||||||
$logInfoS logIdent "Stopped"
|
$logInfoS logIdent "Stopping"
|
||||||
worker <- lift . lift . allocateLinkedAsync $ liftIO runWorker
|
worker <- lift . lift . allocateAsync $ liftIO runWorker
|
||||||
|
|
||||||
tell . Endo $ \cSt -> cSt
|
tell . Endo $ \cSt -> cSt
|
||||||
{ jobWorkers = Map.insert worker chan $ jobWorkers cSt
|
{ jobWorkers = Map.insert worker chan $ jobWorkers cSt
|
||||||
@ -212,14 +230,14 @@ manageJobPool foundation@UniWorX{..} unmask = shutdownOnException $
|
|||||||
go
|
go
|
||||||
in go
|
in go
|
||||||
|
|
||||||
terminateGracefully = do
|
terminateGracefully terminate = do
|
||||||
shouldTerminate <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
|
shouldTerminate <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
|
||||||
guard shouldTerminate
|
guard shouldTerminate
|
||||||
|
|
||||||
oldState <- takeTMVar appJobState
|
oldState <- takeTMVar appJobState
|
||||||
guard $ 0 == Map.size (jobWorkers oldState)
|
guard $ 0 == Map.size (jobWorkers oldState)
|
||||||
|
|
||||||
return . callCC $ \terminate -> do
|
return $ do
|
||||||
$logInfoS "JobPoolManager" "Shutting down"
|
$logInfoS "JobPoolManager" "Shutting down"
|
||||||
terminate ()
|
terminate ()
|
||||||
|
|
||||||
@ -255,7 +273,7 @@ execCrontab = do
|
|||||||
| otherwise = return ()
|
| otherwise = return ()
|
||||||
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ mergeLastExec
|
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ mergeLastExec
|
||||||
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ mergeQueued
|
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ mergeQueued
|
||||||
mapRWST (liftHandler . runDB . setSerializable) $ mergeState
|
mapRWST (liftHandler . runDB . setSerializable) mergeState
|
||||||
|
|
||||||
refT <- liftIO getCurrentTime
|
refT <- liftIO getCurrentTime
|
||||||
settings <- getsYesod appSettings'
|
settings <- getsYesod appSettings'
|
||||||
@ -275,25 +293,25 @@ execCrontab = do
|
|||||||
$logDebugS "Crontab" . intercalate "\n" . map tshow . sortOn fst . flip map (HashMap.toList currentCrontab) $ \(job, cron) -> (,job) $ nextCronMatch appTZ (getMax <$> HashMap.lookup job lastTimes) (debouncingAcc settings job) now cron
|
$logDebugS "Crontab" . intercalate "\n" . map tshow . sortOn fst . flip map (HashMap.toList currentCrontab) $ \(job, cron) -> (,job) $ nextCronMatch appTZ (getMax <$> HashMap.lookup job lastTimes) (debouncingAcc settings job) now cron
|
||||||
|
|
||||||
let doJob = mapRWST (liftHandler . runDBJobs . setSerializable) $ do
|
let doJob = mapRWST (liftHandler . runDBJobs . setSerializable) $ do
|
||||||
newCrontab <- lift . hoist lift $ determineCrontab'
|
newCrontab <- lift $ hoist lift determineCrontab'
|
||||||
when (newCrontab /= currentCrontab) $
|
when (newCrontab /= currentCrontab) $
|
||||||
mapRWST (liftIO . atomically) $
|
mapRWST (liftIO . atomically) $
|
||||||
liftBase . void . flip swapTVar newCrontab =<< asks jobCrontab
|
liftBase . void . flip swapTVar newCrontab =<< asks jobCrontab
|
||||||
|
|
||||||
mergeState
|
mergeState
|
||||||
newState <- State.get
|
newState <- State.get
|
||||||
|
|
||||||
let upToDate = and
|
let upToDate = and
|
||||||
[ ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
|
[ ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
|
||||||
, ((==) `on` HashMap.lookup jobCtl) newState currentState
|
, ((==) `on` HashMap.lookup jobCtl) newState currentState
|
||||||
]
|
]
|
||||||
when upToDate $ do
|
when upToDate $ do
|
||||||
now <- liftIO $ getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
foundation <- getYesod
|
foundation <- getYesod
|
||||||
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl
|
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl
|
||||||
case jobCtl of
|
case jobCtl of
|
||||||
JobCtlQueue job -> lift $ queueDBJobCron job
|
JobCtlQueue job -> lift $ queueDBJobCron job
|
||||||
other -> runReaderT ?? foundation $ writeJobCtl other
|
other -> runReaderT ?? foundation $ writeJobCtl other
|
||||||
|
|
||||||
case nextMatch of
|
case nextMatch of
|
||||||
MatchAsap -> doJob
|
MatchAsap -> doJob
|
||||||
@ -305,6 +323,7 @@ execCrontab = do
|
|||||||
logFunc <- askLoggerIO
|
logFunc <- askLoggerIO
|
||||||
whenM (liftIO . flip runLoggingT logFunc $ waitUntil jobCrontab currentCrontab nextTime')
|
whenM (liftIO . flip runLoggingT logFunc $ waitUntil jobCrontab currentCrontab nextTime')
|
||||||
doJob
|
doJob
|
||||||
|
|
||||||
where
|
where
|
||||||
acc :: NominalDiffTime
|
acc :: NominalDiffTime
|
||||||
acc = 1e-3
|
acc = 1e-3
|
||||||
@ -335,7 +354,7 @@ execCrontab = do
|
|||||||
t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) (debouncingAcc settings jobCtl) now cron
|
t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) (debouncingAcc settings jobCtl) now cron
|
||||||
|
|
||||||
waitUntil :: (Eq a, MonadUnliftIO m, MonadLogger m) => TVar a -> a -> UTCTime -> m Bool
|
waitUntil :: (Eq a, MonadUnliftIO m, MonadLogger m) => TVar a -> a -> UTCTime -> m Bool
|
||||||
waitUntil crontabTV crontab nextTime = runResourceT $ do
|
waitUntil crontabTV crontab nextTime = do
|
||||||
diffT <- diffUTCTime nextTime <$> liftIO getCurrentTime
|
diffT <- diffUTCTime nextTime <$> liftIO getCurrentTime
|
||||||
let waitTime = fromInteger (truncate $ diffT / acc) * toRational acc
|
let waitTime = fromInteger (truncate $ diffT / acc) * toRational acc
|
||||||
waitTime'
|
waitTime'
|
||||||
|
|||||||
@ -5,6 +5,7 @@ module Utils.Failover
|
|||||||
, FailoverException(..)
|
, FailoverException(..)
|
||||||
, withFailover, withFailoverReTest
|
, withFailover, withFailoverReTest
|
||||||
, FailoverMetrics, registerFailoverMetrics
|
, FailoverMetrics, registerFailoverMetrics
|
||||||
|
, mapFailover
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude hiding (try, Vector, finally, onException)
|
import ClassyPrelude hiding (try, Vector, finally, onException)
|
||||||
@ -39,6 +40,8 @@ import qualified Data.Foldable as F
|
|||||||
|
|
||||||
import Data.Unique
|
import Data.Unique
|
||||||
|
|
||||||
|
import Utils (foldMapM)
|
||||||
|
|
||||||
|
|
||||||
data FailoverItem a = FailoverItem
|
data FailoverItem a = FailoverItem
|
||||||
{ failoverValue :: a
|
{ failoverValue :: a
|
||||||
@ -81,6 +84,14 @@ mkFailoverLabeled opts = fmap Failover . liftIO $ newTVarIO opts'
|
|||||||
where opts' = flip map opts $ \(failoverLabel, failoverValue) -> FailoverItem{ failoverLastTest = Nothing, failoverReferences = Set.empty, .. }
|
where opts' = flip map opts $ \(failoverLabel, failoverValue) -> FailoverItem{ failoverLastTest = Nothing, failoverReferences = Set.empty, .. }
|
||||||
|
|
||||||
|
|
||||||
|
mapFailover :: ( MonadIO m, Monoid b)
|
||||||
|
=> (a -> m b)
|
||||||
|
-> Failover a
|
||||||
|
-> m b
|
||||||
|
mapFailover f Failover{..} = do
|
||||||
|
as <- toListOf (folded . _failoverValue) <$> readTVarIO failover
|
||||||
|
foldMapM f as
|
||||||
|
|
||||||
withFailoverReference :: (MonadIO m, MonadMask m)
|
withFailoverReference :: (MonadIO m, MonadMask m)
|
||||||
=> Failover a
|
=> Failover a
|
||||||
-> (Unique -> m b)
|
-> (Unique -> m b)
|
||||||
|
|||||||
@ -6,6 +6,6 @@ import ModelSpec ()
|
|||||||
|
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = withApp $ do
|
spec = withApp $
|
||||||
describe "CorrectionsR" $ do
|
xdescribe "CorrectionsR" $
|
||||||
return ()
|
return ()
|
||||||
|
|||||||
@ -83,15 +83,15 @@ runHandler handler = do
|
|||||||
|
|
||||||
withApp :: YSpec UniWorX -> Spec
|
withApp :: YSpec UniWorX -> Spec
|
||||||
withApp = around $ \act -> runResourceT $ do
|
withApp = around $ \act -> runResourceT $ do
|
||||||
settings <- liftIO $ loadYamlSettings
|
settings <- liftIO $ loadYamlSettings
|
||||||
["config/test-settings.yml", "config/settings.yml"]
|
["config/test-settings.yml", "config/settings.yml"]
|
||||||
[]
|
[]
|
||||||
useEnv
|
useEnv
|
||||||
foundation <- makeFoundation settings
|
foundation <- makeFoundation settings
|
||||||
wipeDB foundation
|
wipeDB foundation
|
||||||
runAppLoggingT foundation $ handleJobs foundation
|
runAppLoggingT foundation $ handleJobs foundation
|
||||||
logWare <- makeMiddleware foundation
|
logWare <- makeMiddleware foundation
|
||||||
lift $ act (foundation, logWare) `finally` shutdownApp foundation
|
lift $ act (foundation, logWare) `finally` shutdownApp foundation
|
||||||
|
|
||||||
-- This function will truncate all of the tables in your database.
|
-- This function will truncate all of the tables in your database.
|
||||||
-- 'withApp' calls it before each test, creating a clean environment for each
|
-- 'withApp' calls it before each test, creating a clean environment for each
|
||||||
|
|||||||
Reference in New Issue
Block a user