Cronjobs & notifications for sheet active/inactive

This commit is contained in:
Gregor Kleen 2018-10-13 15:41:02 +02:00
parent 7bdf015560
commit 99c53fee73
12 changed files with 266 additions and 138 deletions

View File

@ -322,6 +322,12 @@ GermanGermany: Deutsch (Deutschland)
MailSubjectSubmissionRated csh@CourseShorthand: Ihre #{csh}-Abgabe wurde bewertet MailSubjectSubmissionRated csh@CourseShorthand: Ihre #{csh}-Abgabe wurde bewertet
MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{courseName} (#{termDesc}) wurde bewertet. MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{courseName} (#{termDesc}) wurde bewertet.
MailSubjectSheetActive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} wurde herausgegeben
MailSheetActiveIntro courseName@Text termDesc@Text sheetName@SheetName: Sie können nun #{sheetName} im Kurs #{courseName} (#{termDesc}) herunterladen.
MailSubjectSheetInactive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} kann nur noch kurze Zeit abgegeben werden
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Sie können #{sheetName} im Kurs #{courseName} (#{termDesc}) nur noch kurze Zeit abgeben.
SheetTypeBonus: Bonus SheetTypeBonus: Bonus
SheetTypeNormal: Normal SheetTypeNormal: Normal
SheetTypePass: Bestehen SheetTypePass: Bestehen

5
models
View File

@ -230,3 +230,8 @@ QueuedJob
lockInstance InstanceId Maybe lockInstance InstanceId Maybe
lockTime UTCTime Maybe lockTime UTCTime Maybe
deriving Eq Read Show Generic Typeable deriving Eq Read Show Generic Typeable
CronLastExec
job Value
time UTCTime
instance InstanceId
UniqueCronLastExec job

View File

@ -102,6 +102,7 @@ dependencies:
- mime-mail - mime-mail
- hashable - hashable
- aeson-pretty - aeson-pretty
- resourcet
# The library contains all of our application code. The executable # The library contains all of our application code. The executable
# defined below is just a thin wrapper. # defined below is just a thin wrapper.

View File

@ -5,6 +5,7 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleContexts #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Application module Application
( getApplicationDev, getAppDevSettings ( getApplicationDev, getAppDevSettings
@ -12,10 +13,10 @@ module Application
, develMain , develMain
, makeFoundation , makeFoundation
, makeLogWare , makeLogWare
-- * for DevelMain -- -- * for DevelMain
, foundationStoreNum -- , foundationStoreNum
, getApplicationRepl -- , getApplicationRepl
, shutdownApp -- , shutdownApp
-- * for GHCI -- * for GHCI
, handler , handler
, db , db
@ -58,6 +59,8 @@ import Network.HaskellNet.SSL hiding (Settings)
import Network.HaskellNet.SMTP.SSL as SMTP hiding (Settings) import Network.HaskellNet.SMTP.SSL as SMTP hiding (Settings)
import Data.Pool import Data.Pool
import Control.Monad.Trans.Resource
-- Import all relevant handler modules here. -- Import all relevant handler modules here.
-- (HPack takes care to add new modules to our cabal file nowadays.) -- (HPack takes care to add new modules to our cabal file nowadays.)
import Handler.Common import Handler.Common
@ -83,18 +86,16 @@ mkYesodDispatch "UniWorX" resourcesUniWorX
-- performs initialization and returns a foundation datatype value. This is also -- performs initialization and returns a foundation datatype value. This is also
-- the place to put your migrate statements to have automatic database -- the place to put your migrate statements to have automatic database
-- migrations handled by Yesod. -- migrations handled by Yesod.
makeFoundation :: AppSettings -> IO UniWorX makeFoundation :: (MonadResource m, MonadBaseControl IO m) => AppSettings -> m UniWorX
makeFoundation appSettings@(AppSettings{..}) = do makeFoundation appSettings@(AppSettings{..}) = do
-- Some basic initializations: HTTP connection manager, logger, and static -- Some basic initializations: HTTP connection manager, logger, and static
-- subsite. -- subsite.
appHttpManager <- newManager appHttpManager <- newManager
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger appLogger <- liftIO $ newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
appStatic <- appStatic <- liftIO $ bool static staticDevel appMutableStatic appStaticDir
(if appMutableStatic then staticDevel else static)
appStaticDir
appCryptoIDKey <- readKeyFile appCryptoIDKeyFile appCryptoIDKey <- readKeyFile appCryptoIDKeyFile
appInstanceID <- maybe UUID.nextRandom readInstanceIDFile appInstanceIDFile appInstanceID <- liftIO $ maybe UUID.nextRandom readInstanceIDFile appInstanceIDFile
(appJobCtl, recvChans) <- fmap unzip . atomically . replicateM appJobWorkers $ do (appJobCtl, recvChans) <- fmap unzip . atomically . replicateM appJobWorkers $ do
chan <- newBroadcastTMChan chan <- newBroadcastTMChan
@ -132,8 +133,8 @@ makeFoundation appSettings@(AppSettings{..}) = do
-- Return the foundation -- Return the foundation
return $ mkFoundation sqlPool smtpPool return $ mkFoundation sqlPool smtpPool
readInstanceIDFile :: FilePath -> IO UUID readInstanceIDFile :: MonadIO m => FilePath -> m UUID
readInstanceIDFile idFile = handle generateInstead $ LBS.readFile idFile >>= parseBS readInstanceIDFile idFile = liftIO . handle generateInstead $ LBS.readFile idFile >>= parseBS
where where
parseBS :: LBS.ByteString -> IO UUID parseBS :: LBS.ByteString -> IO UUID
parseBS = maybe (throwString "appInstanceIDFile does not contain an UUID encoded in network byte order") return . UUID.fromByteString parseBS = maybe (throwString "appInstanceIDFile does not contain an UUID encoded in network byte order") return . UUID.fromByteString
@ -174,15 +175,15 @@ createSmtpPool SmtpConf{ smtpPool = ResourcePoolConf{..}, .. } = do
-- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and -- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and
-- applying some additional middlewares. -- applying some additional middlewares.
makeApplication :: UniWorX -> IO Application makeApplication :: MonadIO m => UniWorX -> m Application
makeApplication foundation = do makeApplication foundation = liftIO $ do
logWare <- makeLogWare foundation logWare <- makeLogWare foundation
-- Create the WAI application and apply middlewares -- Create the WAI application and apply middlewares
appPlain <- toWaiAppPlain foundation appPlain <- toWaiAppPlain foundation
return $ logWare $ defaultMiddlewaresNoLogging appPlain return $ logWare $ defaultMiddlewaresNoLogging appPlain
makeLogWare :: UniWorX -> IO Middleware makeLogWare :: MonadIO m => UniWorX -> m Middleware
makeLogWare foundation = makeLogWare foundation = liftIO $
mkRequestLogger def mkRequestLogger def
{ outputFormat = { outputFormat =
if appDetailedRequestLogging $ appSettings foundation if appDetailedRequestLogging $ appSettings foundation
@ -211,26 +212,29 @@ warpSettings foundation =
defaultSettings defaultSettings
-- | For yesod devel, return the Warp settings and WAI Application. -- | For yesod devel, return the Warp settings and WAI Application.
getApplicationDev :: IO (Settings, Application) getApplicationDev :: (MonadResource m, MonadBaseControl IO m) => m (Settings, Application)
getApplicationDev = do getApplicationDev = do
settings <- getAppDevSettings settings <- getAppDevSettings
foundation <- makeFoundation settings foundation <- makeFoundation settings
wsettings <- getDevSettings $ warpSettings foundation wsettings <- liftIO . getDevSettings $ warpSettings foundation
app <- makeApplication foundation app <- makeApplication foundation
return (wsettings, app) return (wsettings, app)
getAppDevSettings :: IO AppSettings getAppDevSettings :: MonadIO m => m AppSettings
getAppDevSettings = loadYamlSettings [configSettingsYml] [configSettingsYmlValue] useEnv getAppDevSettings = liftIO $ loadYamlSettings [configSettingsYml] [configSettingsYmlValue] useEnv
-- | main function for use by yesod devel -- | main function for use by yesod devel
develMain :: IO () develMain :: IO ()
develMain = develMainHelper getApplicationDev develMain = runResourceT $ do
app <- getApplicationDev
liftIO . develMainHelper $ return app
-- | The @main@ function for an executable running this site. -- | The @main@ function for an executable running this site.
appMain :: IO () appMain :: MonadResourceBase m => m ()
appMain = do appMain = runResourceT $ do
-- Get the settings from all relevant sources -- Get the settings from all relevant sources
settings <- loadYamlSettingsArgs settings <- liftIO $
loadYamlSettingsArgs
-- fall back to compile-time values, set to [] to require values at runtime -- fall back to compile-time values, set to [] to require values at runtime
[configSettingsYmlValue] [configSettingsYmlValue]
@ -244,31 +248,31 @@ appMain = do
app <- makeApplication foundation app <- makeApplication foundation
-- Run the application with Warp -- Run the application with Warp
runSettings (warpSettings foundation) app liftIO $ runSettings (warpSettings foundation) app
-------------------------------------------------------------- -- --------------------------------------------------------------
-- Functions for DevelMain.hs (a way to run the app from GHCi) -- -- Functions for DevelMain.hs (a way to run the app from GHCi)
-------------------------------------------------------------- -- --------------------------------------------------------------
foundationStoreNum :: Word32 -- foundationStoreNum :: Word32
foundationStoreNum = 2 -- foundationStoreNum = 2
getApplicationRepl :: IO (Int, UniWorX, Application) -- getApplicationRepl :: IO (Int, UniWorX, Application)
getApplicationRepl = do -- getApplicationRepl = do
settings <- getAppDevSettings -- settings <- getAppDevSettings
foundation <- makeFoundation settings -- foundation <- makeFoundation settings
wsettings <- getDevSettings $ warpSettings foundation -- wsettings <- getDevSettings $ warpSettings foundation
app1 <- makeApplication foundation -- app1 <- makeApplication foundation
let foundationStore = Store foundationStoreNum -- let foundationStore = Store foundationStoreNum
deleteStore foundationStore -- deleteStore foundationStore
writeStore foundationStore foundation -- writeStore foundationStore foundation
return (getPort wsettings, foundation, app1) -- return (getPort wsettings, foundation, app1)
shutdownApp :: UniWorX -> IO () -- shutdownApp :: UniWorX -> IO ()
shutdownApp UniWorX{..} = do -- shutdownApp UniWorX{..} = do
atomically $ mapM_ closeTMChan appJobCtl -- atomically $ mapM_ closeTMChan appJobCtl
--------------------------------------------- ---------------------------------------------
@ -277,7 +281,7 @@ shutdownApp UniWorX{..} = do
-- | Run a handler -- | Run a handler
handler :: Handler a -> IO a handler :: Handler a -> IO a
handler h = getAppDevSettings >>= makeFoundation >>= flip unsafeHandler h handler h = runResourceT $ liftIO getAppDevSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h
-- | Run DB queries -- | Run DB queries
db :: ReaderT SqlBackend (HandlerT UniWorX IO) a -> IO a db :: ReaderT SqlBackend (HandlerT UniWorX IO) a -> IO a

View File

@ -8,8 +8,7 @@
#-} #-}
module Cron module Cron
( matchesCron ( CronNextMatch(..)
, CronNextMatch(..)
, nextCronMatch , nextCronMatch
, module Cron.Types , module Cron.Types
) where ) where
@ -204,21 +203,3 @@ nextCronMatch tz mPrev now c@Cron{..}
localDay <- maybeToList $ fromGregorianValid (fromIntegral cronYear) (fromIntegral cronMonth) (fromIntegral cronDayOfMonth) localDay <- maybeToList $ fromGregorianValid (fromIntegral cronYear) (fromIntegral cronMonth) (fromIntegral cronDayOfMonth)
let localTimeOfDay = TimeOfDay (fromIntegral cronHour) (fromIntegral cronMinute) (fromIntegral cronSecond) let localTimeOfDay = TimeOfDay (fromIntegral cronHour) (fromIntegral cronMinute) (fromIntegral cronSecond)
return $ localTimeToUTCTZ tz LocalTime{..} return $ localTimeToUTCTZ tz LocalTime{..}
matchesCron :: TZ -- ^ Timezone of the `Cron`-Entry
-> Maybe UTCTime -- ^ Previous execution of the job
-> NominalDiffTime -- ^ Approximate time until next check
-> UTCTime -- ^ "Current" time
-> Cron
-> Bool
-- ^ @matchesCron tz prev prec now c@ determines whether the given `Cron`
-- specification @c@ should match @now@, under the assumption that the next
-- check will occur no earlier than @now + prec@.
matchesCron tz mPrev prec now cron@Cron{cronOffset} = case nextCronMatch tz mPrev now cron of
MatchAsap -> True
MatchNone -> False
MatchAt ts -> ts < toT
where
toT = case cronOffset of
CronScheduleBefore -> addUTCTime prec now
CronScheduleAfter -> now

View File

@ -5,7 +5,6 @@
module Cron.Types module Cron.Types
( Cron(..), Crontab ( Cron(..), Crontab
, CronScheduleOffset(..)
, CronMatch(..) , CronMatch(..)
, CronAbsolute(..) , CronAbsolute(..)
, CronPeriod(..) , CronPeriod(..)
@ -23,14 +22,6 @@ import Numeric.Natural
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
-- | When the scheduled time for a job falls between two wakeups of the timing
-- thread, execute the job on the wakeup before or after the scheduled time
data CronScheduleOffset
= CronScheduleBefore | CronScheduleAfter
deriving (Eq, Ord, Show, Read, Enum, Bounded)
makePrisms ''CronScheduleOffset
data CronMatch data CronMatch
= CronMatchAny = CronMatchAny
| CronMatchNone | CronMatchNone
@ -67,7 +58,6 @@ makeLenses_ ''CronPeriod
data Cron = Cron data Cron = Cron
{ cronInitial :: CronAbsolute { cronInitial :: CronAbsolute
, cronRepeat :: Maybe CronPeriod , cronRepeat :: Maybe CronPeriod
, cronOffset :: CronScheduleOffset
} }
deriving (Eq, Show) deriving (Eq, Show)

View File

@ -13,7 +13,7 @@
#-} #-}
module Jobs module Jobs
( module Jobs.Types ( module Types
, writeJobCtl , writeJobCtl
, queueJob, queueJob' , queueJob, queueJob'
, handleJobs , handleJobs
@ -23,7 +23,8 @@ import Import hiding ((.=), Proxy)
import Handler.Utils.Mail import Handler.Utils.Mail
import Handler.Utils.DateTime import Handler.Utils.DateTime
import Jobs.Types import Jobs.Types as Types hiding (JobCtl(JobCtlQueue))
import Jobs.Types (JobCtl(JobCtlQueue))
import Data.Conduit.TMChan import Data.Conduit.TMChan
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
@ -32,6 +33,7 @@ import qualified Data.Text.Lazy as LT
import Data.Aeson (fromJSON, toJSON) import Data.Aeson (fromJSON, toJSON)
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
import Database.Persist.Sql (executeQQ, fromSqlKey, transactionSave) import Database.Persist.Sql (executeQQ, fromSqlKey, transactionSave)
import Data.Monoid (Last(..)) import Data.Monoid (Last(..))
@ -63,6 +65,7 @@ import Control.Monad.Trans.State (StateT, evalStateT, mapStateT)
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Control.Monad.Writer.Class (MonadWriter(..)) import Control.Monad.Writer.Class (MonadWriter(..))
import Control.Monad.Reader.Class (MonadReader(..)) import Control.Monad.Reader.Class (MonadReader(..))
import Control.Monad.Trans.Resource (MonadResourceBase, ResourceT, runResourceT, allocate)
import Control.Monad.Random (MonadRandom(..), evalRand) import Control.Monad.Random (MonadRandom(..), evalRand)
@ -80,24 +83,25 @@ data JobQueueException = JInvalid QueuedJobId QueuedJob
instance Exception JobQueueException instance Exception JobQueueException
handleJobs :: MonadIO m => [TMChan JobCtl] -> UniWorX -> m () handleJobs :: (MonadResource m, MonadIO m) => [TMChan JobCtl] -> UniWorX -> m ()
-- | Read control commands from `appJobCtl` and address them as they come in -- | Read control commands from `appJobCtl` and address them as they come in
-- --
-- Uses `unsafeHandler`, as per documentation all HTTP-related fields of state/environment are meaningless placeholders. -- Uses `unsafeHandler`, as per documentation all HTTP-related fields of state/environment are meaningless placeholders.
-- Handling commands in `HandlerT` provides us with the facilities to render urls, unifies logging, provides a value of the foundation type, ... -- Handling commands in `HandlerT` provides us with the facilities to render urls, unifies logging, provides a value of the foundation type, ...
handleJobs recvChans foundation@UniWorX{..} = liftIO $ do handleJobs recvChans foundation@UniWorX{..} = do
jobCrontab <- newTVarIO HashMap.empty jobCrontab <- liftIO $ newTVarIO HashMap.empty
jobConfirm <- newTVarIO HashMap.empty jobConfirm <- liftIO $ newTVarIO HashMap.empty
forM_ (zip [1..] recvChans) $ \(n, chan) -> forM_ (zip [1..] recvChans) $ \(n, chan) ->
let let
logStart = $logDebugS ("Jobs #" <> tshow n) "Starting" logStart = $logDebugS ("Jobs #" <> tshow n) "Starting"
logStop = $logDebugS ("Jobs #" <> tshow n) "Stopping" logStop = $logDebugS ("Jobs #" <> tshow n) "Stopping"
in void . fork . unsafeHandler foundation . bracket_ logStart logStop . flip runReaderT JobContext{..} . runConduit $ sourceTMChan chan .| handleJobs' n doFork = fork . unsafeHandler foundation . bracket_ logStart logStop . flip runReaderT JobContext{..} . runConduit $ sourceTMChan chan .| handleJobs' n
in void $ allocate (liftIO doFork) (liftIO . killThread)
-- Start cron operation -- Start cron operation
void . fork . unsafeHandler foundation $ runReaderT execCrontab JobContext{..} void $ allocate (liftIO . fork . unsafeHandler foundation $ runReaderT execCrontab JobContext{..}) (liftIO . killThread)
unsafeHandler foundation . flip runReaderT JobContext{..} $ liftIO . unsafeHandler foundation . flip runReaderT JobContext{..} $
writeJobCtlBlock JobCtlDetermineCrontab writeJobCtlBlock JobCtlDetermineCrontab
@ -105,9 +109,18 @@ execCrontab :: ReaderT JobContext (HandlerT UniWorX IO) ()
-- ^ Keeping a `HashMap` of the latest execution times of `JobCtl`s we have -- ^ Keeping a `HashMap` of the latest execution times of `JobCtl`s we have
-- seen, wait for the time of the next job and fire it -- seen, wait for the time of the next job and fire it
execCrontab = flip evalStateT HashMap.empty . forever $ do execCrontab = flip evalStateT HashMap.empty . forever $ do
mapStateT (liftHandlerT . runDB . setSerializable) $ do
let
merge (Entity leId CronLastExec{..})
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
= State.modify $ HashMap.insertWith (<>) (JobCtlQueue job) (Max cronLastExecTime)
| otherwise = lift $ delete leId
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ merge
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
(currentCrontab, (jobCtl, nextMatch)) <- mapStateT (mapReaderT $ liftIO . atomically) $ do (currentCrontab, (jobCtl, nextMatch)) <- mapStateT (mapReaderT $ liftIO . atomically) $ do
crontab <- liftBase . readTVar =<< asks jobCrontab crontab <- liftBase . readTVar =<< asks jobCrontab
State.modify . HashMap.filterWithKey $ \k _ -> HashMap.member k crontab
prevExec <- State.get prevExec <- State.get
case earliestJob prevExec crontab now of case earliestJob prevExec crontab now of
Nothing -> liftBase retry Nothing -> liftBase retry
@ -115,9 +128,30 @@ execCrontab = flip evalStateT HashMap.empty . forever $ do
Just x -> return (crontab, x) Just x -> return (crontab, x)
let doJob = do let doJob = do
now <- liftIO $ getCurrentTime mJid <- mapStateT (mapReaderT $ liftHandlerT . runDB . setSerializable) $ do
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl newCrontab <- lift . lift $ determineCrontab
writeJobCtl jobCtl if
| ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
-> do
now <- liftIO $ getCurrentTime
instanceID <- getsYesod appInstanceID
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl
case jobCtl of
JobCtlQueue job -> do
lift . lift $ upsertBy
(UniqueCronLastExec $ toJSON job)
CronLastExec
{ cronLastExecJob = toJSON job
, cronLastExecTime = now
, cronLastExecInstance = instanceID
}
[ CronLastExecTime =. now ]
Just <$> lift (lift $ queueJobUnsafe job)
other -> Nothing <$ writeJobCtl other
| otherwise
-> lift . fmap (const Nothing) . mapReaderT (liftIO . atomically) $
lift . flip writeTVar newCrontab =<< asks jobCrontab
maybe (return ()) (writeJobCtl . JobCtlPerform) mJid
case nextMatch of case nextMatch of
MatchAsap -> doJob MatchAsap -> doJob
@ -151,48 +185,21 @@ execCrontab = flip evalStateT HashMap.empty . forever $ do
where where
t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) now cron t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) now cron
waitUntil :: (Eq a, MonadIO m) => TVar a -> a -> UTCTime -> m Bool waitUntil :: (Eq a, MonadResourceBase m) => TVar a -> a -> UTCTime -> m Bool
waitUntil crontabTV crontab nextTime = liftIO $ do waitUntil crontabTV crontab nextTime = runResourceT $ do
diffT <- diffUTCTime nextTime <$> getCurrentTime diffT <- diffUTCTime nextTime <$> liftIO getCurrentTime
if if
| diffT < acc -> return True | diffT < acc -> return True
| otherwise -> do | otherwise -> do
retVar <- newEmptyTMVarIO retVar <- liftIO newEmptyTMVarIO
delayThread <- forkFinally (threadDelay . floor $ toRational acc * 1e6) (atomically . putTMVar retVar) void $ allocate (liftIO $ forkFinally (threadDelay . floor $ toRational acc * 1e6) $ atomically . putTMVar retVar) (liftIO . killThread)
let let
awaitDelayThread = False <$ takeTMVar retVar awaitDelayThread = False <$ takeTMVar retVar
awaitCrontabChange = do awaitCrontabChange = do
crontab' <- readTVar crontabTV crontab' <- readTVar crontabTV
True <$ guard (crontab /= crontab') True <$ guard (crontab /= crontab')
crontabChanged <- atomically $ awaitCrontabChange <|> awaitDelayThread crontabChanged <- liftIO . atomically $ awaitCrontabChange <|> awaitDelayThread
bool (waitUntil crontabTV crontab nextTime) (False <$ killThread delayThread) crontabChanged bool (waitUntil crontabTV crontab nextTime) (return False) crontabChanged
determineCrontab :: Handler (Crontab JobCtl)
determineCrontab = execWriterT $ do
AppSettings{..} <- getsYesod appSettings
case appJobFlushInterval of
Just interval -> tell $ HashMap.singleton
JobCtlFlush
Cron
{ cronInitial = CronAsap
, cronRepeat = Just CronPeriod
{ cronMinInterval = interval
, cronNext = CronAsap
}
, cronOffset = CronScheduleBefore
}
Nothing -> return ()
now <- liftIO getCurrentTime
tell $ HashMap.singleton
JobCtlDetermineCrontab
Cron
{ cronInitial = CronTimestamp . utcToLocalTimeTZ appTZ $ addUTCTime appJobCronInterval now
, cronRepeat = Nothing
, cronOffset = CronScheduleBefore
}
handleJobs' :: Int -> Sink JobCtl (ReaderT JobContext Handler) () handleJobs' :: Int -> Sink JobCtl (ReaderT JobContext Handler) ()
@ -227,10 +234,10 @@ handleJobs' wNum = C.mapM_ $ \jctl -> do
performJob content performJob content
-- `performJob` is expected to throw a notification if it detects that the job was not done -- `performJob` is expected to throw an exception if it detects that the job was not done
runDB $ delete jId runDB $ delete jId
handleCmd JobCtlDetermineCrontab = do handleCmd JobCtlDetermineCrontab = do
newCTab <- lift determineCrontab newCTab <- liftHandlerT . runDB $ setSerializable determineCrontab
$logDebugS logIdent $ tshow newCTab $logDebugS logIdent $ tshow newCTab
mapReaderT (liftIO . atomically) $ mapReaderT (liftIO . atomically) $
lift . flip writeTVar newCTab =<< asks jobCrontab lift . flip writeTVar newCTab =<< asks jobCrontab
@ -303,20 +310,88 @@ queueJob' job = queueJob job >>= writeJobCtl . JobCtlPerform
setSerializable :: DB a -> DB a setSerializable :: DB a -> DB a
setSerializable = ([executeQQ|SET TRANSACTION ISOLATION LEVEL SERIALIZABLE|] *>) setSerializable = ([executeQQ|SET TRANSACTION ISOLATION LEVEL SERIALIZABLE|] *>)
determineCrontab :: DB (Crontab JobCtl)
-- ^ Extract all future jobs from the database (sheet deadlines, ...)
determineCrontab = execWriterT $ do
AppSettings{..} <- getsYesod appSettings
case appJobFlushInterval of
Just interval -> tell $ HashMap.singleton
JobCtlFlush
Cron
{ cronInitial = CronAsap
, cronRepeat = Just CronPeriod
{ cronMinInterval = interval
, cronNext = CronAsap
}
}
Nothing -> return ()
now <- liftIO getCurrentTime
tell $ HashMap.singleton
JobCtlDetermineCrontab
Cron
{ cronInitial = CronAsap
, cronRepeat = Just CronPeriod
{ cronMinInterval = appJobCronInterval
, cronNext = CronAsap
}
}
let
sheetJobs (Entity nSheet Sheet{..}) = do
tell $ HashMap.singleton
(JobCtlQueue $ JobQueueNotification NotificationSheetActive{..})
Cron
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ sheetActiveFrom
, cronRepeat = Nothing
}
tell $ HashMap.singleton
(JobCtlQueue $ JobQueueNotification NotificationSheetInactive{..})
Cron
{ cronInitial = CronTimestamp . utcToLocalTimeTZ appTZ . max sheetActiveFrom $ addUTCTime (-nominalDay) sheetActiveTo
, cronRepeat = Nothing
}
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ sheetJobs
determineNotificationCandidates :: Notification -> DB [Entity User]
determineNotificationCandidates NotificationSubmissionRated{..} = E.select . E.from $ \(user `E.InnerJoin` submissionUser) -> do
E.on $ user E.^. UserId E.==. submissionUser E.^. SubmissionUserUser
E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val nSubmission
return user
determineNotificationCandidates NotificationSheetActive{..} = E.select . E.from $ \(user `E.InnerJoin` courseParticipant `E.InnerJoin` sheet) -> do
E.on $ sheet E.^. SheetCourse E.==. courseParticipant E.^. CourseParticipantCourse
E.on $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
return user
determineNotificationCandidates NotificationSheetInactive{..} = E.select . E.from $ \(user `E.InnerJoin` courseParticipant `E.InnerJoin` sheet) -> do
E.on $ sheet E.^. SheetCourse E.==. courseParticipant E.^. CourseParticipantCourse
E.on $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
return user
classifyNotification :: Notification -> DB NotificationTrigger
classifyNotification NotificationSubmissionRated{..} = do
Sheet{sheetType} <- belongsToJust submissionSheet =<< getJust nSubmission
return $ case sheetType of
NotGraded -> NTSubmissionRated
_other -> NTSubmissionRatedGraded
classifyNotification NotificationSheetActive{} = return NTSheetActive
classifyNotification NotificationSheetInactive{} = return NTSheetInactive
performJob :: Job -> HandlerT UniWorX IO () performJob :: Job -> HandlerT UniWorX IO ()
performJob JobQueueNotification{ jNotification = n@NotificationSubmissionRated{..} } = do performJob JobQueueNotification{jNotification} = do
jIds <- runDB . setSerializable $ do jIds <- runDB. setSerializable $ do
Submission{submissionSheet} <- getJust nSubmission candidates <- determineNotificationCandidates jNotification
isGraded <- (/= NotGraded) . sheetType <$> getJust submissionSheet nClass <- classifyNotification jNotification
res <- E.select . E.from $ \(user `E.InnerJoin` submissionUser) -> do mapM (queueJobUnsafe . flip JobSendNotification jNotification) $ do
E.on $ user E.^. UserId E.==. submissionUser E.^. SubmissionUserUser Entity uid User{userNotificationSettings} <- candidates
E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val nSubmission guard $ notificationAllowed userNotificationSettings nClass
return (user E.^. UserId, user E.^. UserNotificationSettings) return uid
let recipients = do
(E.Value uid, E.Value nSettings) <- res
guard . notificationAllowed nSettings $ bool NTSubmissionRated NTSubmissionRatedGraded isGraded
return uid
forM recipients $ queueJobUnsafe . flip JobSendNotification n
forM_ jIds $ writeJobCtl . JobCtlPerform forM_ jIds $ writeJobCtl . JobCtlPerform
performJob JobSendNotification{ jNotification = NotificationSubmissionRated{..}, jRecipient } = userMailT jRecipient $ do performJob JobSendNotification{ jNotification = NotificationSubmissionRated{..}, jRecipient } = userMailT jRecipient $ do
(Course{..}, Sheet{..}, Submission{..}, corrector) <- liftHandlerT . runDB $ do (Course{..}, Sheet{..}, Submission{..}, corrector) <- liftHandlerT . runDB $ do
@ -353,7 +428,39 @@ performJob JobSendNotification{ jNotification = NotificationSubmissionRated{..},
, "course-school" Aeson..= courseSchool , "course-school" Aeson..= courseSchool
] ]
-- provideAlternative $ \(MsgRenderer mr) -> ($(textFile "templates/mail/submissionRated.txt") :: TextUrl (Route UniWorX)) -- textFile does not support control statements -- provideAlternative $ \(MsgRenderer mr) -> ($(textFile "templates/mail/submissionRated.txt") :: TextUrl (Route UniWorX)) -- textFile does not support control statements
providePreferredAlternative $ \(MsgRenderer mr) -> ($(ihamletFile "templates/mail/submissionRated.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX)) providePreferredAlternative ($(ihamletFile "templates/mail/submissionRated.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
performJob JobSendNotification{ jNotification = NotificationSheetActive{..}, jRecipient } = userMailT jRecipient $ do
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do
sheet <- getJust nSheet
course <- belongsToJust sheetCourse sheet
return (course, sheet)
setSubjectI $ MsgMailSubjectSheetActive courseShorthand sheetName
MsgRenderer mr <- getMailMsgRenderer
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
tid = courseTerm
ssh = courseSchool
csh = courseShorthand
shn = sheetName
addAlternatives $ do
providePreferredAlternative ($(ihamletFile "templates/mail/sheetActive.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
performJob JobSendNotification{ jNotification = NotificationSheetInactive{..}, jRecipient } = userMailT jRecipient $ do
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do
sheet <- getJust nSheet
course <- belongsToJust sheetCourse sheet
return (course, sheet)
setSubjectI $ MsgMailSubjectSheetInactive courseShorthand sheetName
MsgRenderer mr <- getMailMsgRenderer
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
tid = courseTerm
ssh = courseSchool
csh = courseShorthand
shn = sheetName
addAlternatives $ do
providePreferredAlternative ($(ihamletFile "templates/mail/sheetInactive.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
performJob JobSendTestEmail{..} = mailT jMailContext $ do performJob JobSendTestEmail{..} = mailT jMailContext $ do
_mailTo .= [Address Nothing jEmail] _mailTo .= [Address Nothing jEmail]
setSubjectI MsgMailTestSubject setSubjectI MsgMailTestSubject

View File

@ -23,6 +23,8 @@ data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notifica
| JobQueueNotification { jNotification :: Notification } | JobQueueNotification { jNotification :: Notification }
deriving (Eq, Ord, Show, Read, Generic, Typeable) deriving (Eq, Ord, Show, Read, Generic, Typeable)
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId } data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
| NotificationSheetActive { nSheet :: SheetId }
| NotificationSheetInactive { nSheet :: SheetId }
deriving (Eq, Ord, Show, Read, Generic, Typeable) deriving (Eq, Ord, Show, Read, Generic, Typeable)
instance Hashable Job instance Hashable Job

View File

@ -467,6 +467,8 @@ derivePersistFieldJSON ''Value
-- Could maybe be replaced with `Structure Notification` in the long term -- Could maybe be replaced with `Structure Notification` in the long term
data NotificationTrigger = NTSubmissionRatedGraded data NotificationTrigger = NTSubmissionRatedGraded
| NTSubmissionRated | NTSubmissionRated
| NTSheetActive
| NTSheetInactive
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe NotificationTrigger instance Universe NotificationTrigger
@ -493,6 +495,8 @@ instance Default NotificationSettings where
def = NotificationSettings $ \case def = NotificationSettings $ \case
NTSubmissionRatedGraded -> True NTSubmissionRatedGraded -> True
NTSubmissionRated -> False NTSubmissionRated -> False
NTSheetActive -> True
NTSheetInactive -> True
instance ToJSON NotificationSettings where instance ToJSON NotificationSettings where
toJSON v = toJSON . HashMap.fromList $ map (id &&& notificationAllowed v) universeF toJSON v = toJSON . HashMap.fromList $ map (id &&& notificationAllowed v) universeF

View File

@ -50,7 +50,7 @@ derivePersistFieldJSON n = do
] ]
, instanceD sqlCxt ([t|PersistFieldSql|] `appT` t) , instanceD sqlCxt ([t|PersistFieldSql|] `appT` t)
[ funD (mkName "sqlType") [ funD (mkName "sqlType")
[ clause [wildP] (normalB [e|SqlOther "json"|]) [] [ clause [wildP] (normalB [e|SqlOther "jsonb"|]) []
] ]
] ]
] ]

View File

@ -0,0 +1,14 @@
<html>
<head>
<style>
h1 {
font-size: 1.25em;
font-variant: small-caps;
font-weight: normal;
}
<body>
<h1>
_{MsgMailSheetActiveIntro (CI.original courseName) termDesc sheetName}
<p>
<a href=@{CSheetR tid ssh csh shn SShowR}>
#{sheetName}

View File

@ -0,0 +1,14 @@
<html>
<head>
<style>
h1 {
font-size: 1.25em;
font-variant: small-caps;
font-weight: normal;
}
<body>
<h1>
_{MsgMailSheetInactiveIntro (CI.original courseName) termDesc sheetName}
<p>
<a href=@{CSheetR tid ssh csh shn SShowR}>
#{sheetName}