Cleanup
This commit is contained in:
parent
a63e59d5a3
commit
fb52db33a1
5
models
5
models
@ -223,7 +223,8 @@ Exam
|
|||||||
-- By default this file is used in Model.hs (which is imported by Foundation.hs)
|
-- By default this file is used in Model.hs (which is imported by Foundation.hs)
|
||||||
QueuedJob
|
QueuedJob
|
||||||
content Value
|
content Value
|
||||||
created UTCTime
|
creationInstance InstanceId
|
||||||
lockInstance UUID Maybe
|
creationTime UTCTime
|
||||||
|
lockInstance InstanceId Maybe
|
||||||
lockTime UTCTime Maybe
|
lockTime UTCTime Maybe
|
||||||
deriving Eq Read Show Generic Typeable
|
deriving Eq Read Show Generic Typeable
|
||||||
|
|||||||
@ -77,6 +77,8 @@ dependencies:
|
|||||||
- parsec
|
- parsec
|
||||||
- uuid
|
- uuid
|
||||||
- exceptions
|
- exceptions
|
||||||
|
- stm
|
||||||
|
- stm-chans
|
||||||
- stm-conduit
|
- stm-conduit
|
||||||
- lens
|
- lens
|
||||||
- MonadRandom
|
- MonadRandom
|
||||||
|
|||||||
@ -20,12 +20,12 @@ import Database.Persist.Sql (ConnectionPool, runSqlPool)
|
|||||||
import Text.Hamlet (hamletFile)
|
import Text.Hamlet (hamletFile)
|
||||||
import Text.Jasmine (minifym)
|
import Text.Jasmine (minifym)
|
||||||
|
|
||||||
-- Used only when in "auth-dummy-login" setting is enabled.
|
|
||||||
import Yesod.Auth.Message
|
import Yesod.Auth.Message
|
||||||
import Yesod.Auth.Dummy
|
import Yesod.Auth.Dummy
|
||||||
import Auth.LDAP
|
import Auth.LDAP
|
||||||
import Auth.PWHash
|
import Auth.PWHash
|
||||||
import Auth.Dummy
|
import Auth.Dummy
|
||||||
|
import Jobs.Types
|
||||||
|
|
||||||
import qualified Network.Wai as W (requestMethod, pathInfo)
|
import qualified Network.Wai as W (requestMethod, pathInfo)
|
||||||
|
|
||||||
@ -115,14 +115,10 @@ data UniWorX = UniWorX
|
|||||||
, appHttpManager :: Manager
|
, appHttpManager :: Manager
|
||||||
, appLogger :: Logger
|
, appLogger :: Logger
|
||||||
, appCryptoIDKey :: CryptoIDKey
|
, appCryptoIDKey :: CryptoIDKey
|
||||||
, appInstanceID :: UUID
|
, appInstanceID :: InstanceId
|
||||||
, appJobCtl :: TMChan JobCtl
|
, 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
|
-- This is where we define all of the routes in our application. For a full
|
||||||
-- explanation of the syntax, please see:
|
-- explanation of the syntax, please see:
|
||||||
-- http://www.yesodweb.com/book/routing-and-handlers
|
-- http://www.yesodweb.com/book/routing-and-handlers
|
||||||
|
|||||||
@ -4,4 +4,3 @@ module Import
|
|||||||
|
|
||||||
import Foundation as Import
|
import Foundation as Import
|
||||||
import Import.NoFoundation as Import
|
import Import.NoFoundation as Import
|
||||||
|
|
||||||
|
|||||||
111
src/Jobs.hs
111
src/Jobs.hs
@ -7,43 +7,30 @@
|
|||||||
, TypeFamilies
|
, TypeFamilies
|
||||||
, DeriveGeneric
|
, DeriveGeneric
|
||||||
, DeriveDataTypeable
|
, DeriveDataTypeable
|
||||||
|
, QuasiQuotes
|
||||||
#-}
|
#-}
|
||||||
|
|
||||||
module Jobs
|
module Jobs
|
||||||
( handleJobs
|
( module Jobs.Types
|
||||||
, Job(..), Notification(..)
|
, writeJobCtl
|
||||||
|
, queueJob
|
||||||
|
, handleJobs
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
|
import Jobs.Types
|
||||||
|
|
||||||
import Data.Conduit.TMChan
|
import Data.Conduit.TMChan
|
||||||
import qualified Data.Conduit.List as C
|
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 qualified Data.Aeson as Aeson
|
||||||
import Data.Aeson.TH (deriveJSON)
|
import Database.Persist.Sql (executeQQ, fromSqlKey)
|
||||||
import Database.Persist.Sql (rawExecute, fromSqlKey)
|
|
||||||
|
|
||||||
|
|
||||||
data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notification }
|
data JobQueueException = JInvalid QueuedJobId QueuedJob
|
||||||
deriving (Eq, Ord, Show, Read)
|
| JLocked QueuedJobId InstanceId UTCTime
|
||||||
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
|
|
||||||
| JNonexistant QueuedJobId
|
| JNonexistant QueuedJobId
|
||||||
deriving (Read, Show, Eq, Generic, Typeable)
|
deriving (Read, Show, Eq, Generic, Typeable)
|
||||||
|
|
||||||
@ -51,6 +38,10 @@ instance Exception JobQueueException
|
|||||||
|
|
||||||
|
|
||||||
handleJobs :: UniWorX -> IO ()
|
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'
|
handleJobs foundation@UniWorX{..} = unsafeHandler foundation . bracket_ logStart logStop . runConduit $ sourceTMChan appJobCtl .| handleJobs'
|
||||||
where
|
where
|
||||||
logStart = $(logDebugS) "Jobs" "Started"
|
logStart = $(logDebugS) "Jobs" "Started"
|
||||||
@ -60,14 +51,12 @@ handleJobs' :: Sink JobCtl Handler ()
|
|||||||
handleJobs' = C.mapM_ $ void . handleAny ($(logErrorS) "Jobs" . tshow) . handleCmd
|
handleJobs' = C.mapM_ $ void . handleAny ($(logErrorS) "Jobs" . tshow) . handleCmd
|
||||||
where
|
where
|
||||||
handleQueueException :: MonadLogger m => JobQueueException -> m ()
|
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 (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
|
let
|
||||||
content :: Job
|
content :: Job
|
||||||
Aeson.Success content = fromJSON queuedJobContent
|
Aeson.Success content = fromJSON queuedJobContent
|
||||||
@ -76,29 +65,51 @@ handleJobs' = C.mapM_ $ void . handleAny ($(logErrorS) "Jobs" . tshow) . handleC
|
|||||||
|
|
||||||
runDB $ delete jId
|
runDB $ delete jId
|
||||||
|
|
||||||
jLock :: QueuedJobId -> Handler QueuedJob
|
jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a
|
||||||
jLock jId = runDB $ do
|
jLocked jId act = do
|
||||||
rawExecute "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE" []
|
hasLock <- liftIO $ newTVarIO False
|
||||||
j@QueuedJob{..} <- maybe (throwM $ JNonexistant jId) return =<< get jId
|
val <- runDB $ do
|
||||||
maybe (return ()) throwM $ JLocked <$> pure jId <*> queuedJobLockInstance <*> queuedJobLockTime
|
[executeQQ|
|
||||||
let isSuccess (Aeson.Success _) = True
|
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
|
||||||
isSuccess _ = False
|
|]
|
||||||
unless (isSuccess (fromJSON queuedJobContent :: Result Job)) . throwM $ JInvalid j
|
j@QueuedJob{..} <- maybe (throwM $ JNonexistant jId) return =<< get jId
|
||||||
instanceID <- getsYesod appInstanceID
|
maybe (return ()) throwM $ JLocked <$> pure jId <*> queuedJobLockInstance <*> queuedJobLockTime
|
||||||
now <- liftIO getCurrentTime
|
case fromJSON queuedJobContent :: Aeson.Result Job of
|
||||||
updateGet jId [ QueuedJobLockInstance =. Just instanceID
|
Aeson.Success _ -> return ()
|
||||||
, QueuedJobLockTime =. Just now
|
Aeson.Error t -> do
|
||||||
]
|
$logErrorS "Jobs" $ "Aeson decoding error: " <> pack t
|
||||||
|
throwM $ JInvalid jId j
|
||||||
jUnlock :: QueuedJobId -> Handler ()
|
instanceID <- getsYesod appInstanceID
|
||||||
jUnlock jId = runDB $ update jId [ QueuedJobLockInstance =. Nothing
|
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
|
, QueuedJobLockTime =. Nothing
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
cmdPerform :: ( MonadHandler m
|
writeJobCtl :: (MonadHandler m, HandlerSite m ~ UniWorX) => JobCtl -> m ()
|
||||||
, HandlerSite m ~ UniWorX
|
writeJobCtl cmd = do
|
||||||
) => QueuedJobId -> m ()
|
|
||||||
cmdPerform (NCtlPerform -> cmd) = do
|
|
||||||
chan <- getsYesod appJobCtl
|
chan <- getsYesod appJobCtl
|
||||||
liftIO . atomically $ writeTMChan chan cmd
|
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
38
src/Jobs/Types.hs
Normal 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)
|
||||||
@ -24,7 +24,6 @@ import Database.Persist.Quasi
|
|||||||
-- import Data.ByteString
|
-- import Data.ByteString
|
||||||
import Model.Types
|
import Model.Types
|
||||||
|
|
||||||
import Data.UUID
|
|
||||||
import Data.Aeson (Value)
|
import Data.Aeson (Value)
|
||||||
import Data.Aeson.TH (deriveJSON, defaultOptions)
|
import Data.Aeson.TH (deriveJSON, defaultOptions)
|
||||||
|
|
||||||
|
|||||||
@ -453,3 +453,4 @@ type SheetName = CI Text
|
|||||||
type UserEmail = CI Text
|
type UserEmail = CI Text
|
||||||
|
|
||||||
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
|
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
|
||||||
|
type InstanceId = UUID
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{-# LANGUAGE FlexibleInstances, FlexibleContexts #-}
|
{-# LANGUAGE FlexibleInstances, FlexibleContexts #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
|
|
||||||
|
|
||||||
module Utils.Message
|
module Utils.Message
|
||||||
@ -13,7 +14,7 @@ import Data.Universe
|
|||||||
import Utils.PathPiece (finiteFromPathPiece, nullaryToPathPiece)
|
import Utils.PathPiece (finiteFromPathPiece, nullaryToPathPiece)
|
||||||
|
|
||||||
import qualified ClassyPrelude.Yesod (addMessage, addMessageI)
|
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
|
data MessageClass = Error | Warning | Info | Success
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
|
|
||||||
module Utils.TH where
|
module Utils.TH where
|
||||||
-- Common Utility Functions that require TemplateHaskell
|
-- Common Utility Functions that require TemplateHaskell
|
||||||
|
|
||||||
-- import Data.Char
|
-- import Data.Char
|
||||||
|
|
||||||
|
import Prelude
|
||||||
import Language.Haskell.TH
|
import Language.Haskell.TH
|
||||||
-- import Control.Monad
|
-- import Control.Monad
|
||||||
-- import Control.Monad.Trans.Class
|
-- import Control.Monad.Trans.Class
|
||||||
|
|||||||
Reference in New Issue
Block a user