This commit is contained in:
Gregor Kleen 2018-10-01 19:21:45 +02:00
parent a63e59d5a3
commit fb52db33a1
10 changed files with 114 additions and 64 deletions

5
models
View File

@ -223,7 +223,8 @@ Exam
-- By default this file is used in Model.hs (which is imported by Foundation.hs)
QueuedJob
content Value
created UTCTime
lockInstance UUID Maybe
creationInstance InstanceId
creationTime UTCTime
lockInstance InstanceId Maybe
lockTime UTCTime Maybe
deriving Eq Read Show Generic Typeable

View File

@ -77,6 +77,8 @@ dependencies:
- parsec
- uuid
- exceptions
- stm
- stm-chans
- stm-conduit
- lens
- MonadRandom

View File

@ -20,12 +20,12 @@ import Database.Persist.Sql (ConnectionPool, runSqlPool)
import Text.Hamlet (hamletFile)
import Text.Jasmine (minifym)
-- Used only when in "auth-dummy-login" setting is enabled.
import Yesod.Auth.Message
import Yesod.Auth.Dummy
import Auth.LDAP
import Auth.PWHash
import Auth.Dummy
import Jobs.Types
import qualified Network.Wai as W (requestMethod, pathInfo)
@ -115,14 +115,10 @@ data UniWorX = UniWorX
, appHttpManager :: Manager
, appLogger :: Logger
, appCryptoIDKey :: CryptoIDKey
, appInstanceID :: UUID
, appJobCtl :: TMChan JobCtl
, appInstanceID :: InstanceId
, appJobCtl :: TMChan JobCtl
}
data JobCtl = NCtlFlush
| NCtlPerform QueuedJobId
deriving (Eq, Ord, Read, Show)
-- This is where we define all of the routes in our application. For a full
-- explanation of the syntax, please see:
-- http://www.yesodweb.com/book/routing-and-handlers

View File

@ -4,4 +4,3 @@ module Import
import Foundation as Import
import Import.NoFoundation as Import

View File

@ -7,50 +7,41 @@
, TypeFamilies
, DeriveGeneric
, DeriveDataTypeable
, QuasiQuotes
#-}
module Jobs
( handleJobs
, Job(..), Notification(..)
( module Jobs.Types
, writeJobCtl
, queueJob
, handleJobs
) where
import Import
import Jobs.Types
import Data.Conduit.TMChan
import qualified Data.Conduit.List as C
import Data.Aeson (fromJSON, Result(..), defaultOptions, Options(..))
import Data.Aeson (fromJSON, toJSON)
import qualified Data.Aeson as Aeson
import Data.Aeson.TH (deriveJSON)
import Database.Persist.Sql (rawExecute, fromSqlKey)
import Database.Persist.Sql (executeQQ, fromSqlKey)
data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notification }
deriving (Eq, Ord, Show, Read)
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId, nTimestamp :: UTCTime }
deriving (Eq, Ord, Show, Read)
deriveJSON defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, tagSingleConstructors = True
} ''Job
deriveJSON defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, tagSingleConstructors = True
} ''Notification
data JobQueueException = JInvalid QueuedJob
| JLocked QueuedJobId UUID UTCTime
data JobQueueException = JInvalid QueuedJobId QueuedJob
| JLocked QueuedJobId InstanceId UTCTime
| JNonexistant QueuedJobId
deriving (Read, Show, Eq, Generic, Typeable)
instance Exception JobQueueException
handleJobs :: UniWorX -> IO ()
-- | 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.
-- Handling commands in `HandlerT` provides us with the facilities to render urls, unifies logging, provides a value of the foundation type, ...
handleJobs foundation@UniWorX{..} = unsafeHandler foundation . bracket_ logStart logStop . runConduit $ sourceTMChan appJobCtl .| handleJobs'
where
logStart = $(logDebugS) "Jobs" "Started"
@ -60,14 +51,12 @@ handleJobs' :: Sink JobCtl Handler ()
handleJobs' = C.mapM_ $ void . handleAny ($(logErrorS) "Jobs" . tshow) . handleCmd
where
handleQueueException :: MonadLogger m => JobQueueException -> m ()
handleQueueException (JInvalid j) = $(logWarnS) "Jobs" $ "Invalid QueuedJob: " ++ tshow j
handleQueueException (JInvalid jId j) = $(logWarnS) "Jobs" $ "Invalid QueuedJob (#" ++ tshow (fromSqlKey jId) ++ "): " ++ tshow j
handleQueueException (JNonexistant jId) = $(logInfoS) "Jobs" $ "Saw nonexistant queue id: " ++ tshow (fromSqlKey jId)
handleQueueException (JLocked jId lInstance lTime) = $(logDebugS) "Jobs" $ "Saw locked QueuedJob: " ++ tshow (jId, lInstance, lTime)
handleQueueException (JLocked jId lInstance lTime) = $(logDebugS) "Jobs" $ "Saw locked QueuedJob: " ++ tshow (fromSqlKey jId, lInstance, lTime)
handleCmd NCtlFlush = void . fork . runDB . runConduit $ selectKeys [] [ Asc QueuedJobCreated ] .| C.mapM_ cmdPerform
handleCmd (NCtlPerform jId) = handle handleQueueException . (`finally` jUnlock jId) $ do
j@QueuedJob{..} <- jLock jId
handleCmd JobCtlFlush = void . fork . runDB . runConduit $ selectKeys [] [ Asc QueuedJobCreationTime ] .| C.mapM_ (writeJobCtl . JobCtlPerform)
handleCmd (JobCtlPerform jId) = handle handleQueueException . jLocked jId $ \QueuedJob{..} -> do
let
content :: Job
Aeson.Success content = fromJSON queuedJobContent
@ -76,29 +65,51 @@ handleJobs' = C.mapM_ $ void . handleAny ($(logErrorS) "Jobs" . tshow) . handleC
runDB $ delete jId
jLock :: QueuedJobId -> Handler QueuedJob
jLock jId = runDB $ do
rawExecute "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE" []
j@QueuedJob{..} <- maybe (throwM $ JNonexistant jId) return =<< get jId
maybe (return ()) throwM $ JLocked <$> pure jId <*> queuedJobLockInstance <*> queuedJobLockTime
let isSuccess (Aeson.Success _) = True
isSuccess _ = False
unless (isSuccess (fromJSON queuedJobContent :: Result Job)) . throwM $ JInvalid j
instanceID <- getsYesod appInstanceID
now <- liftIO getCurrentTime
updateGet jId [ QueuedJobLockInstance =. Just instanceID
, QueuedJobLockTime =. Just now
]
jUnlock :: QueuedJobId -> Handler ()
jUnlock jId = runDB $ update jId [ QueuedJobLockInstance =. Nothing
jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a
jLocked jId act = do
hasLock <- liftIO $ newTVarIO False
val <- runDB $ do
[executeQQ|
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
|]
j@QueuedJob{..} <- maybe (throwM $ JNonexistant jId) return =<< get jId
maybe (return ()) throwM $ JLocked <$> pure jId <*> queuedJobLockInstance <*> queuedJobLockTime
case fromJSON queuedJobContent :: Aeson.Result Job of
Aeson.Success _ -> return ()
Aeson.Error t -> do
$logErrorS "Jobs" $ "Aeson decoding error: " <> pack t
throwM $ JInvalid jId j
instanceID <- getsYesod appInstanceID
now <- liftIO getCurrentTime
val <- updateGet jId [ QueuedJobLockInstance =. Just instanceID
, QueuedJobLockTime =. Just now
]
liftIO . atomically $ writeTVar hasLock True
return val
act val `finally` whenM (liftIO . atomically $ readTVar hasLock) jUnlock
where
jUnlock :: Handler ()
jUnlock = runDB $ update jId [ QueuedJobLockInstance =. Nothing
, QueuedJobLockTime =. Nothing
]
cmdPerform :: ( MonadHandler m
, HandlerSite m ~ UniWorX
) => QueuedJobId -> m ()
cmdPerform (NCtlPerform -> cmd) = do
chan <- getsYesod appJobCtl
writeJobCtl :: (MonadHandler m, HandlerSite m ~ UniWorX) => JobCtl -> m ()
writeJobCtl cmd = do
chan <- getsYesod appJobCtl
liftIO . atomically $ writeTMChan chan cmd
queueJob :: Job -> YesodDB UniWorX QueuedJobId
queueJob job = do
now <- liftIO getCurrentTime
self <- getsYesod appInstanceID
jId <- insert QueuedJob
{ queuedJobContent = toJSON job
, queuedJobCreationInstance = self
, queuedJobCreationTime = now
, queuedJobLockInstance = Nothing
, queuedJobLockTime = Nothing
}
writeJobCtl $ JobCtlPerform jId -- FIXME: Should do fancy load balancing across instances (or something)
return jId

38
src/Jobs/Types.hs Normal file
View File

@ -0,0 +1,38 @@
{-# LANGUAGE TemplateHaskell
, NoImplicitPrelude
#-}
module Jobs.Types
( Job(..), Notification(..)
, JobCtl(..)
) where
import Import.NoFoundation
import Data.Aeson (defaultOptions, Options(..), SumEncoding(..))
import Data.Aeson.TH (deriveJSON)
data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notification }
deriving (Eq, Ord, Show, Read)
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId, nTimestamp :: UTCTime }
deriving (Eq, Ord, Show, Read)
deriveJSON defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, tagSingleConstructors = True
, sumEncoding = TaggedObject "job" "data"
} ''Job
deriveJSON defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, tagSingleConstructors = True
, sumEncoding = TaggedObject "notification" "data"
} ''Notification
data JobCtl = JobCtlFlush
| JobCtlPerform QueuedJobId
deriving (Eq, Ord, Read, Show)

View File

@ -24,7 +24,6 @@ import Database.Persist.Quasi
-- import Data.ByteString
import Model.Types
import Data.UUID
import Data.Aeson (Value)
import Data.Aeson.TH (deriveJSON, defaultOptions)

View File

@ -453,3 +453,4 @@ type SheetName = CI Text
type UserEmail = CI Text
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
type InstanceId = UUID

View File

@ -1,5 +1,6 @@
{-# LANGUAGE FlexibleInstances, FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Utils.Message
@ -13,7 +14,7 @@ import Data.Universe
import Utils.PathPiece (finiteFromPathPiece, nullaryToPathPiece)
import qualified ClassyPrelude.Yesod (addMessage, addMessageI)
import ClassyPrelude.Yesod (PathPiece(..),MonadHandler,HandlerSite,RenderMessage,Html)
import ClassyPrelude.Yesod hiding (addMessage, addMessageI)
data MessageClass = Error | Warning | Info | Success

View File

@ -1,11 +1,13 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Utils.TH where
-- Common Utility Functions that require TemplateHaskell
-- import Data.Char
import Prelude
import Language.Haskell.TH
-- import Control.Monad
-- import Control.Monad.Trans.Class