feat: load shedding

This commit is contained in:
Gregor Kleen 2021-07-21 12:37:54 +02:00
parent 30641a0d71
commit 9df0686086
15 changed files with 262 additions and 12 deletions

View File

@ -288,3 +288,5 @@ file-source-prewarm:
bot-mitigations: bot-mitigations:
- only-logged-in-table-sorting - only-logged-in-table-sorting
volatile-cluster-settings-cache-time: 10

View File

@ -153,4 +153,6 @@ WorkflowInstanceUpdateNoActions: Keine Updates verfügbar
WorkflowInstanceUpdateUpdatedGraph: Definitions-Update erfolgreich angewandt WorkflowInstanceUpdateUpdatedGraph: Definitions-Update erfolgreich angewandt
WorkflowInstanceUpdateUpdatedCategory: Kategorie-Update erfolgreich angewandt WorkflowInstanceUpdateUpdatedCategory: Kategorie-Update erfolgreich angewandt
WorkflowInstanceUpdateDeletedDescriptionLanguage lang@Lang: Beschreibung/Titel in Sprache „#{lang}“ gelöscht WorkflowInstanceUpdateDeletedDescriptionLanguage lang@Lang: Beschreibung/Titel in Sprache „#{lang}“ gelöscht
WorkflowInstanceUpdateUpdatedDescriptionLanguage lang@Lang: Beschreibung/Titel-Update für Sprache „#{lang}“ angewandt WorkflowInstanceUpdateUpdatedDescriptionLanguage lang@Lang: Beschreibung/Titel-Update für Sprache „#{lang}“ angewandt
WorkflowsDisabled: Workflows sind temporär deaktiviert.

View File

@ -154,3 +154,5 @@ WorkflowInstanceUpdateUpdatedGraph: Successfully applied updated definition
WorkflowInstanceUpdateUpdatedCategory: Successfully applied updated category WorkflowInstanceUpdateUpdatedCategory: Successfully applied updated category
WorkflowInstanceUpdateDeletedDescriptionLanguage lang: Successfully deleted description/title for language “#{lang}” WorkflowInstanceUpdateDeletedDescriptionLanguage lang: Successfully deleted description/title for language “#{lang}”
WorkflowInstanceUpdateUpdatedDescriptionLanguage lang: Successfully applied updated description/title for language “#{lang}” WorkflowInstanceUpdateUpdatedDescriptionLanguage lang: Successfully applied updated description/title for language “#{lang}”
WorkflowsDisabled: Workflows are temporarily disabled.

View File

@ -4,4 +4,10 @@ ClusterConfig
setting ClusterSettingsKey -- I.e. Symmetric key for encrypting database-ids for use in URLs, Symmetric key for encrypting user-sessions so they can be saved directly as a browser-cookie, Symmetric key for encrypting error messages which might contain secret information, ... setting ClusterSettingsKey -- I.e. Symmetric key for encrypting database-ids for use in URLs, Symmetric key for encrypting user-sessions so they can be saved directly as a browser-cookie, Symmetric key for encrypting error messages which might contain secret information, ...
value Value -- JSON-encoded value value Value -- JSON-encoded value
Primary setting Primary setting
deriving Generic
VolatileClusterConfig
setting VolatileClusterSettingsKey
value Value
Primary setting
deriving Generic deriving Generic

View File

@ -116,6 +116,8 @@ import qualified Utils.Pool as Custom
import Utils.Postgresql import Utils.Postgresql
import Handler.Utils.Memcached (manageMemcachedLocalInvalidations) import Handler.Utils.Memcached (manageMemcachedLocalInvalidations)
import qualified System.Clock as Clock
-- 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.News import Handler.News
@ -213,7 +215,7 @@ makeFoundation appSettings''@AppSettings{..} = do
-- from there, and then create the real foundation. -- from there, and then create the real foundation.
let let
mkFoundation :: _ -> (forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend) -> _ mkFoundation :: _ -> (forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend) -> _
mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey = UniWorX {..} mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache = UniWorX {..}
-- The UniWorX {..} syntax is an example of record wild cards. For more -- The UniWorX {..} syntax is an example of record wild cards. For more
-- information, see: -- information, see:
-- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
@ -234,6 +236,7 @@ makeFoundation appSettings''@AppSettings{..} = do
(error "VerpSecret forced in tempFoundation") (error "VerpSecret forced in tempFoundation")
(error "AuthKey forced in tempFoundation") (error "AuthKey forced in tempFoundation")
(error "PersonalisedSheetFilesSeedKey forced in tempFoundation") (error "PersonalisedSheetFilesSeedKey forced in tempFoundation")
(error "VolatileClusterSettingsCache forced in tempFoundation")
runAppLoggingT tempFoundation $ do runAppLoggingT tempFoundation $ do
$logInfoS "InstanceID" $ UUID.toText appInstanceID $logInfoS "InstanceID" $ UUID.toText appInstanceID
@ -296,6 +299,10 @@ makeFoundation appSettings''@AppSettings{..} = do
appAuthKey <- clusterSetting (Proxy :: Proxy 'ClusterAuthKey) `customRunSqlPool` sqlPool appAuthKey <- clusterSetting (Proxy :: Proxy 'ClusterAuthKey) `customRunSqlPool` sqlPool
appPersonalisedSheetFilesSeedKey <- clusterSetting (Proxy :: Proxy 'ClusterPersonalisedSheetFilesSeedKey) `customRunSqlPool` sqlPool appPersonalisedSheetFilesSeedKey <- clusterSetting (Proxy :: Proxy 'ClusterPersonalisedSheetFilesSeedKey) `customRunSqlPool` sqlPool
let appVolatileClusterSettingsCacheTime' = Clock.fromNanoSecs ns
where (MkFixed ns :: Nano) = realToFrac appVolatileClusterSettingsCacheTime
appVolatileClusterSettingsCache <- newTVarIO $ mkVolatileClusterSettingsCache appVolatileClusterSettingsCacheTime'
needsRechunk <- exists [FileContentChunkContentBased !=. True] `customRunSqlPool` sqlPool needsRechunk <- exists [FileContentChunkContentBased !=. True] `customRunSqlPool` sqlPool
let appSettings' = appSettings'' let appSettings' = appSettings''
& _appRechunkFiles %~ guardOnM needsRechunk & _appRechunkFiles %~ guardOnM needsRechunk
@ -328,7 +335,7 @@ makeFoundation appSettings''@AppSettings{..} = do
$logDebugS "Runtime configuration" $ tshow appSettings' $logDebugS "Runtime configuration" $ tshow appSettings'
let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache
-- Return the foundation -- Return the foundation
$logDebugS "setup" "Done" $logDebugS "setup" "Done"

View File

@ -63,6 +63,8 @@ import qualified Data.Binary as Binary
import GHC.TypeLits (TypeError) import GHC.TypeLits (TypeError)
import qualified GHC.TypeLits as TypeError (ErrorMessage(..)) import qualified GHC.TypeLits as TypeError (ErrorMessage(..))
import Utils.VolatileClusterSettings
type BearerAuthSite site type BearerAuthSite site
= ( MonadCrypto (HandlerFor site) = ( MonadCrypto (HandlerFor site)
@ -464,6 +466,11 @@ maybeCurrentBearerRestrictions = runMaybeT $ do
route <- MaybeT getCurrentRoute route <- MaybeT getCurrentRoute
hoistMaybe $ bearer ^? _bearerRestrictionIx route hoistMaybe $ bearer ^? _bearerRestrictionIx route
workflowsEnabledAuth :: (MonadHandler m, HandlerSite m ~ UniWorX)
=> m AuthResult
-> m AuthResult
workflowsEnabledAuth = volatileBool clusterVolatileWorkflowsEnabled (unauthorizedI MsgWorkflowsDisabled)
data AuthorizationCacheKey data AuthorizationCacheKey
= AuthCacheWorkflowWorkflowEdgeActors CryptoFileNameWorkflowWorkflow = AuthCacheWorkflowWorkflowEdgeActors CryptoFileNameWorkflowWorkflow
| AuthCacheWorkflowWorkflowViewers CryptoFileNameWorkflowWorkflow | AuthCacheWorkflowWorkflowViewers CryptoFileNameWorkflowWorkflow
@ -1543,7 +1550,7 @@ tagAccessPredicate AuthEmpty = APDB $ \evalCtx eval' mAuthId route _ -> do
orAR' = shortCircuitM (is _Authorized) (orAR mr) orAR' = shortCircuitM (is _Authorized) (orAR mr)
_andAR' = shortCircuitM (is _Unauthorized) (andAR mr) _andAR' = shortCircuitM (is _Unauthorized) (andAR mr)
workflowInstanceWorkflowsEmpty rScope win = selectLanguageI18n <=< $cachedHereBinary (evalCtx, mAuthId, route) . maybeT (unauthorizedI18n MsgUnauthorizedWorkflowWorkflowsNotEmpty) $ do workflowInstanceWorkflowsEmpty rScope win = workflowsEnabledAuth $ selectLanguageI18n <=< $cachedHereBinary (evalCtx, mAuthId, route) . maybeT (unauthorizedI18n MsgUnauthorizedWorkflowWorkflowsNotEmpty) $ do
roles <- memcacheAuth' (Right diffDay) (AuthCacheWorkflowInstanceWorkflowViewers win rScope) $ do roles <- memcacheAuth' (Right diffDay) (AuthCacheWorkflowInstanceWorkflowViewers win rScope) $ do
scope <- fromRouteWorkflowScope rScope scope <- fromRouteWorkflowScope rScope
let dbScope = scope ^. _DBWorkflowScope let dbScope = scope ^. _DBWorkflowScope
@ -1712,7 +1719,7 @@ tagAccessPredicate AuthAuthentication = APDB $ \_ _ mAuthId route _ -> case rout
guard $ not systemMessageAuthenticatedOnly || isAuthenticated guard $ not systemMessageAuthenticatedOnly || isAuthenticated
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthAuthentication r r -> $unsupportedAuthPredicate AuthAuthentication r
tagAccessPredicate AuthWorkflow = APDB $ \evalCtx eval' mAuthId route isWrite -> do tagAccessPredicate AuthWorkflow = APDB $ \evalCtx eval' mAuthId route isWrite -> workflowsEnabledAuth $ do
mr <- getMsgRenderer mr <- getMsgRenderer
let orAR', _andAR' :: forall m'. Monad m' => m' AuthResult -> m' AuthResult -> m' AuthResult let orAR', _andAR' :: forall m'. Monad m' => m' AuthResult -> m' AuthResult -> m' AuthResult
orAR' = shortCircuitM (is _Authorized) (orAR mr) orAR' = shortCircuitM (is _Authorized) (orAR mr)
@ -1979,7 +1986,7 @@ evalWorkflowRoleFor' :: forall m backend.
-> Route UniWorX -> Route UniWorX
-> Bool -> Bool
-> WriterT (Set AuthTag) (ReaderT backend m) AuthResult -> WriterT (Set AuthTag) (ReaderT backend m) AuthResult
evalWorkflowRoleFor' eval mAuthId mwwId wRole route isWrite = do evalWorkflowRoleFor' eval mAuthId mwwId wRole route isWrite = workflowsEnabledAuth $ do
mr <- getMsgRenderer mr <- getMsgRenderer
let let
@ -2028,7 +2035,7 @@ evalWorkflowRoleFor :: ( HasCallStack
-> Route UniWorX -> Route UniWorX
-> Bool -> Bool
-> ReaderT backend m AuthResult -> ReaderT backend m AuthResult
evalWorkflowRoleFor mAuthId mwwId wRole route isWrite = do evalWorkflowRoleFor mAuthId mwwId wRole route isWrite = workflowsEnabledAuth $ do
isSelf <- (== mAuthId) <$> liftHandler defaultMaybeAuthId isSelf <- (== mAuthId) <$> liftHandler defaultMaybeAuthId
tagActive <- if tagActive <- if
| isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags | isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
@ -2052,7 +2059,7 @@ hasWorkflowRole :: ( HasCallStack
-> Route UniWorX -> Route UniWorX
-> Bool -> Bool
-> ReaderT backend m AuthResult -> ReaderT backend m AuthResult
hasWorkflowRole mwwId wRole route isWrite = do hasWorkflowRole mwwId wRole route isWrite = workflowsEnabledAuth $ do
mAuthId <- maybeAuthId mAuthId <- maybeAuthId
evalWorkflowRoleFor mAuthId mwwId wRole route isWrite evalWorkflowRoleFor mAuthId mwwId wRole route isWrite
@ -2070,7 +2077,7 @@ mayViewWorkflowAction' :: forall backend m fileid.
-> WorkflowWorkflowId -> WorkflowWorkflowId
-> WorkflowAction fileid UserId -> WorkflowAction fileid UserId
-> WriterT (Set AuthTag) (ReaderT backend m) Bool -> WriterT (Set AuthTag) (ReaderT backend m) Bool
mayViewWorkflowAction' eval mAuthId wwId WorkflowAction{..} = hoist (withReaderT $ projectBackend @SqlReadBackend) . maybeT (return False) $ do mayViewWorkflowAction' eval mAuthId wwId WorkflowAction{..} = volatileBool clusterVolatileWorkflowsEnabled (return False) . hoist (withReaderT $ projectBackend @SqlReadBackend) . maybeT (return False) $ do
Entity _ WorkflowWorkflow{..} <- MaybeT . lift $ getWorkflowWorkflowState wwId Entity _ WorkflowWorkflow{..} <- MaybeT . lift $ getWorkflowWorkflowState wwId
rScope <- hoist lift . toRouteWorkflowScope $ _DBWorkflowScope # workflowWorkflowScope rScope <- hoist lift . toRouteWorkflowScope $ _DBWorkflowScope # workflowWorkflowScope
cID <- catchMaybeT (Proxy @CryptoIDError) . lift . lift $ encrypt wwId cID <- catchMaybeT (Proxy @CryptoIDError) . lift . lift $ encrypt wwId
@ -2100,7 +2107,7 @@ mayViewWorkflowAction :: forall backend m fileid.
-> WorkflowWorkflowId -> WorkflowWorkflowId
-> WorkflowAction fileid UserId -> WorkflowAction fileid UserId
-> ReaderT backend m Bool -> ReaderT backend m Bool
mayViewWorkflowAction mAuthId wwId act = do mayViewWorkflowAction mAuthId wwId act = volatileBool clusterVolatileWorkflowsEnabled (return False) $ do
isSelf <- (== mAuthId) <$> liftHandler defaultMaybeAuthId isSelf <- (== mAuthId) <$> liftHandler defaultMaybeAuthId
tagActive <- if tagActive <- if
| isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags | isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags

View File

@ -2,6 +2,7 @@ module Foundation.DB
( runDBRead, runDBRead' ( runDBRead, runDBRead'
, runSqlPoolRetry, runSqlPoolRetry' , runSqlPoolRetry, runSqlPoolRetry'
, dbPoolPressured , dbPoolPressured
, runDBInternal, runDBInternal'
) where ) where
import Import.NoFoundation hiding (runDB, getDBRunner) import Import.NoFoundation hiding (runDB, getDBRunner)
@ -62,6 +63,15 @@ runDBRead' :: CallStack -> ReaderT SqlReadBackend (HandlerFor UniWorX) a -> (Han
runDBRead' lbl action = do runDBRead' lbl action = do
$logDebugS "YesodPersist" "runDBRead" $logDebugS "YesodPersist" "runDBRead"
flip (runSqlPoolRetry' . withReaderT SqlReadBackend $ [executeQQ|SET TRANSACTION READ ONLY|] *> action) lbl . appConnPool =<< getYesod flip (runSqlPoolRetry' . withReaderT SqlReadBackend $ [executeQQ|SET TRANSACTION READ ONLY|] *> action) lbl . appConnPool =<< getYesod
runDBInternal :: HasCallStack
=> ReaderT SqlBackend (HandlerFor UniWorX) a -> HandlerFor UniWorX a
runDBInternal = runDBInternal' callStack
runDBInternal' :: CallStack -> ReaderT SqlBackend (HandlerFor UniWorX) a -> HandlerFor UniWorX a
runDBInternal' lbl action = do
$logDebugS "YesodPersist" "runDBInternal"
flip (runSqlPoolRetry' action) lbl . appConnPool =<< getYesod
dbPoolPressured :: ( MonadHandler m dbPoolPressured :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX

View File

@ -47,6 +47,8 @@ import qualified Data.Set as Set
import Data.List (inits) import Data.List (inits)
import Utils.VolatileClusterSettings
type Breadcrumb = (Text, Maybe (Route UniWorX)) type Breadcrumb = (Text, Maybe (Route UniWorX))
@ -758,6 +760,8 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
} }
} }
, do , do
guardVolatile clusterVolatileWorkflowsEnabled
authCtx <- getAuthContext authCtx <- getAuthContext
(haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,) (haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,)
<$> haveTopWorkflowInstances <$> haveTopWorkflowInstances

View File

@ -39,6 +39,9 @@ import Text.Cassius (cassiusFile)
import Text.Hamlet (hamletFile) import Text.Hamlet (hamletFile)
import Data.FileEmbed (embedFile) import Data.FileEmbed (embedFile)
import Utils.VolatileClusterSettings
data CourseFavouriteToggleButton data CourseFavouriteToggleButton
= BtnCourseFavouriteToggleManual = BtnCourseFavouriteToggleManual
| BtnCourseFavouriteToggleAutomatic | BtnCourseFavouriteToggleAutomatic
@ -303,7 +306,7 @@ siteLayout' overrideHeading widget = do
let cK = MemcachedKeyFavouriteQuickActions (tid, ssh, csh) ctx langs let cK = MemcachedKeyFavouriteQuickActions (tid, ssh, csh) ctx langs
$logDebugS "FavouriteQuickActions" $ tshow cK <> " Checking..." $logDebugS "FavouriteQuickActions" $ tshow cK <> " Checking..."
poolIsPressured <- dbPoolPressured poolIsPressured <- dbPoolPressured
items <- if items <- volatileBool clusterVolatileQuickActionsEnabled (return Nothing) $ if
| poolIsPressured -> Nothing <$ observeFavouritesSkippedDueToDBLoad | poolIsPressured -> Nothing <$ observeFavouritesSkippedDueToDBLoad
| otherwise -> memcachedLimitedKeyTimeoutBy | otherwise -> memcachedLimitedKeyTimeoutBy
MemcachedLimitKeyFavourites appFavouritesQuickActionsBurstsize appFavouritesQuickActionsAvgInverseRate 1 MemcachedLimitKeyFavourites appFavouritesQuickActionsBurstsize appFavouritesQuickActionsAvgInverseRate 1

View File

@ -10,7 +10,7 @@ module Foundation.Type
, AppMemcachedLocal(..) , AppMemcachedLocal(..)
, _memcachedLocalARC , _memcachedLocalARC
, SMTPPool , SMTPPool
, _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionStore, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport, _appMemcached, _appUploadCache, _appVerpSecret, _appAuthKey, _appPersonalisedSheetFilesSeedKey , _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionStore, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport, _appMemcached, _appUploadCache, _appVerpSecret, _appAuthKey, _appPersonalisedSheetFilesSeedKey, _appVolatileClusterSettingsCache
, DB, Form, MsgRenderer, MailM, DBFile , DB, Form, MsgRenderer, MailM, DBFile
) where ) where
@ -95,6 +95,7 @@ data UniWorX = UniWorX
, appFileSourcePrewarm :: Maybe (LRUHandle (FileContentChunkReference, (Int, Int)) UTCTime Int ByteString) , appFileSourcePrewarm :: Maybe (LRUHandle (FileContentChunkReference, (Int, Int)) UTCTime Int ByteString)
, appFileInjectInhibit :: TVar (IntervalMap UTCTime (Set FileContentReference)) , appFileInjectInhibit :: TVar (IntervalMap UTCTime (Set FileContentReference))
, appPersonalisedSheetFilesSeedKey :: PersonalisedSheetFilesSeedKey , appPersonalisedSheetFilesSeedKey :: PersonalisedSheetFilesSeedKey
, appVolatileClusterSettingsCache :: TVar VolatileClusterSettingsCache
} deriving (Typeable) } deriving (Typeable)
makeLenses_ ''UniWorX makeLenses_ ''UniWorX

View File

@ -12,5 +12,6 @@ import Utils.Metrics as Import
import Utils.Files as Import import Utils.Files as Import
import Utils.PersistentTokenBucket as Import import Utils.PersistentTokenBucket as Import
import Utils.Csv.Mail as Import import Utils.Csv.Mail as Import
import Utils.VolatileClusterSettings as Import
import Jobs.Types as Import (JobHandler(..)) import Jobs.Types as Import (JobHandler(..))

View File

@ -46,6 +46,8 @@ import Utils.Sql as Import
import Utils.Widgets as Import import Utils.Widgets as Import
import Utils.Auth as Import import Utils.Auth as Import
import Settings.Cluster.Volatile as Import
import Data.Fixed as Import import Data.Fixed as Import
import Data.UUID as Import (UUID) import Data.UUID as Import (UUID)

View File

@ -222,6 +222,8 @@ data AppSettings = AppSettings
, appFileSourcePrewarmConf :: Maybe PrewarmCacheConf , appFileSourcePrewarmConf :: Maybe PrewarmCacheConf
, appBotMitigations :: Set SettingBotMitigation , appBotMitigations :: Set SettingBotMitigation
, appVolatileClusterSettingsCacheTime :: DiffTime
} deriving Show } deriving Show
data JobMode = JobsLocal { jobsAcceptOffload :: Bool } data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
@ -678,6 +680,8 @@ instance FromJSON AppSettings where
appBotMitigations <- o .:? "bot-mitigations" .!= Set.empty appBotMitigations <- o .:? "bot-mitigations" .!= Set.empty
appVolatileClusterSettingsCacheTime <- o .: "volatile-cluster-settings-cache-time"
return AppSettings{..} return AppSettings{..}
where isValidARCConf ARCConf{..} = arccMaximumWeight > 0 where isValidARCConf ARCConf{..} = arccMaximumWeight > 0

View File

@ -0,0 +1,127 @@
module Settings.Cluster.Volatile
( VolatileClusterSettingsKey(..)
, clusterVolatileWorkflowsEnabled, clusterVolatileQuickActionsEnabled
, VolatileClusterSetting(..)
, VolatileClusterSettingsCache
, mkVolatileClusterSettingsCache
, alterVolatileClusterSettingsCacheF, insertVolatileClusterSettingsCache, lookupVolatileClusterSettingsCache
) where
import ClassyPrelude.Yesod hiding (Proxy)
import Data.HashPSQ (HashPSQ)
import qualified Data.HashPSQ as HashPSQ
import Data.Universe
import Utils.PathPiece
import Model.Types.TH.PathPiece
import Data.Kind (Type)
import Data.Dynamic
import System.Clock (TimeSpec)
import Data.Functor.Const
import Data.Proxy
-- import Control.Lens
data VolatileClusterSettingsKey
= ClusterVolatileWorkflowsEnabled
| ClusterVolatileQuickActionsEnabled
deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic, Typeable)
deriving anyclass (Hashable, Universe, Finite, NFData)
nullaryPathPiece ''VolatileClusterSettingsKey $ camelToPathPiece' 1
pathPieceJSON ''VolatileClusterSettingsKey
pathPieceJSONKey ''VolatileClusterSettingsKey
pathPieceHttpApiData ''VolatileClusterSettingsKey
derivePersistFieldPathPiece ''VolatileClusterSettingsKey
clusterVolatileWorkflowsEnabled :: Proxy 'ClusterVolatileWorkflowsEnabled
clusterVolatileWorkflowsEnabled = Proxy
clusterVolatileQuickActionsEnabled :: Proxy 'ClusterVolatileQuickActionsEnabled
clusterVolatileQuickActionsEnabled = Proxy
class ( ToJSON (VolatileClusterSettingValue key)
, FromJSON (VolatileClusterSettingValue key)
, Typeable (VolatileClusterSettingValue key)
, NFData (VolatileClusterSettingValue key)
) => VolatileClusterSetting (key :: VolatileClusterSettingsKey) where
type VolatileClusterSettingValue key :: Type
initVolatileClusterSetting :: forall m p. MonadIO m => p key -> m (VolatileClusterSettingValue key)
knownVolatileClusterSetting :: forall p. p key -> VolatileClusterSettingsKey
instance VolatileClusterSetting 'ClusterVolatileWorkflowsEnabled where
type VolatileClusterSettingValue 'ClusterVolatileWorkflowsEnabled = Bool
initVolatileClusterSetting _ = return True
knownVolatileClusterSetting _ = ClusterVolatileWorkflowsEnabled
instance VolatileClusterSetting 'ClusterVolatileQuickActionsEnabled where
type VolatileClusterSettingValue 'ClusterVolatileQuickActionsEnabled = Bool
initVolatileClusterSetting _ = return True
knownVolatileClusterSetting _ = ClusterVolatileQuickActionsEnabled
data SomeVolatileClusterSettingsKey = forall key p. VolatileClusterSetting key => SomeVolatileClusterSettingsKey (p key)
instance Eq SomeVolatileClusterSettingsKey where
(SomeVolatileClusterSettingsKey p1) == (SomeVolatileClusterSettingsKey p2) = knownVolatileClusterSetting p1 == knownVolatileClusterSetting p2
instance Ord SomeVolatileClusterSettingsKey where
(SomeVolatileClusterSettingsKey p1) `compare` (SomeVolatileClusterSettingsKey p2) = knownVolatileClusterSetting p1 `compare` knownVolatileClusterSetting p2
instance Hashable SomeVolatileClusterSettingsKey where
hashWithSalt s (SomeVolatileClusterSettingsKey p) = s `hashWithSalt` knownVolatileClusterSetting p
data VolatileClusterSettingsCache = VolatileClusterSettingsCache
{ volatileClusterSettingsCacheExpiry :: TimeSpec
, volatileClusterSettingsCacheCache :: HashPSQ SomeVolatileClusterSettingsKey TimeSpec Dynamic
}
-- makePrisms ''VolatileClusterSettingsCache
mkVolatileClusterSettingsCache :: TimeSpec -> VolatileClusterSettingsCache
mkVolatileClusterSettingsCache volatileClusterSettingsCacheExpiry = VolatileClusterSettingsCache{..}
where volatileClusterSettingsCacheCache = HashPSQ.empty
alterVolatileClusterSettingsCacheF :: forall key f p.
( VolatileClusterSetting key
, Functor f
)
=> p key
-> (Maybe (VolatileClusterSettingValue key) -> f (Maybe (VolatileClusterSettingValue key)))
-> VolatileClusterSettingsCache
-> TimeSpec -- ^ @now@
-> f VolatileClusterSettingsCache
alterVolatileClusterSettingsCacheF p f c now
= f current <&> \new -> c { volatileClusterSettingsCacheCache = maybe (HashPSQ.delete k current') (\new' -> HashPSQ.insert k now (toDyn $!! new') current') new }
where
k = SomeVolatileClusterSettingsKey p
cutoff = now - volatileClusterSettingsCacheExpiry c
current' = volatileClusterSettingsCacheCache c
current = HashPSQ.lookup k current' >>= \(t, v) -> if
| t > cutoff -> fromDynamic v
| otherwise -> Nothing
insertVolatileClusterSettingsCache :: forall key p.
VolatileClusterSetting key
=> p key
-> Maybe (VolatileClusterSettingValue key)
-> VolatileClusterSettingsCache
-> TimeSpec
-> VolatileClusterSettingsCache
insertVolatileClusterSettingsCache k newVal = (runIdentity .) . alterVolatileClusterSettingsCacheF k (const $ pure newVal)
lookupVolatileClusterSettingsCache :: forall key p.
VolatileClusterSetting key
=> p key
-> VolatileClusterSettingsCache
-> TimeSpec
-> Maybe (VolatileClusterSettingValue key)
lookupVolatileClusterSettingsCache k = (getConst .) . alterVolatileClusterSettingsCacheF k Const

View File

@ -0,0 +1,72 @@
module Utils.VolatileClusterSettings
( getVolatileClusterSetting
, VolatileClusterSettingException(..)
, whenVolatile, volatileBool, guardVolatile
) where
import Import.NoModel
import Model
import Foundation.Type
import Foundation.DB
import System.Clock
import qualified Data.Aeson.Types as Aeson
data VolatileClusterSettingException = VolatileClusterSettingExceptionNoParse
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving anyclass (Exception)
getVolatileClusterSetting :: forall key m p.
( VolatileClusterSetting key
, MonadHandler m, HandlerSite m ~ UniWorX
)
=> p key
-> m (VolatileClusterSettingValue key)
getVolatileClusterSetting p = exceptT return return $ do
cacheTVar <- getsYesod appVolatileClusterSettingsCache
now <- liftIO $ getTime Monotonic
oldVal <- flip (lookupVolatileClusterSettingsCache p) now <$> readTVarIO cacheTVar
traverse_ throwE oldVal
dbVal <- liftHandler . runDBInternal $ do
dbVal <- fmap (fmap volatileClusterConfigValue) . get . VolatileClusterConfigKey $ knownVolatileClusterSetting p
case dbVal of
Just v -> maybe (throwM VolatileClusterSettingExceptionNoParse) return $ Aeson.parseMaybe parseJSON v
Nothing -> do
newVal <- initVolatileClusterSetting p
insert_ $ VolatileClusterConfig (knownVolatileClusterSetting p) (toJSON newVal)
return newVal
atomically . modifyTVar' cacheTVar $ \c -> insertVolatileClusterSettingsCache p (Just dbVal) c now
return dbVal
volatileBool :: forall key m a p.
( VolatileClusterSetting key, VolatileClusterSettingValue key ~ Bool
, MonadHandler m, HandlerSite m ~ UniWorX
)
=> p key
-> m a
-> m a
-> m a
volatileBool p ifFalse ifTrue = do
r <- getVolatileClusterSetting p
bool ifFalse ifTrue r
whenVolatile :: forall key m p.
( VolatileClusterSetting key, VolatileClusterSettingValue key ~ Bool
, MonadHandler m, HandlerSite m ~ UniWorX
)
=> p key
-> m ()
-> m ()
whenVolatile p = volatileBool p (return ())
guardVolatile :: forall key m p.
( VolatileClusterSetting key, VolatileClusterSettingValue key ~ Bool
, MonadHandler m, HandlerSite m ~ UniWorX
, MonadPlus m
)
=> p key
-> m ()
guardVolatile p = volatileBool p mzero (return ())