Fix supervisor and proper rate limiting

This commit is contained in:
2026-09-16 12:28:48 +03:00
parent 64561718a5
commit 3d7f6cb3c6
12 changed files with 164 additions and 247 deletions
+74
View File
@@ -0,0 +1,74 @@
{-# Language DerivingVia #-}
{-# Language OverloadedStrings #-}
module HomeAssistant.Runtime.RateLimit where
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Time (NominalDiffTime)
import Control.Concurrent.STM (newTVarIO, atomically, readTVar, writeTVar)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Katip.Monadic (KatipContext(..))
import Control.Retry (fullJitterBackoff, limitRetries, retrying, RetryStatus (..))
import Data.Maybe (isNothing)
import Control.Exception.Annotated (Exception, checkpointCallStack, throw)
import Control.Monad.Catch (MonadCatch)
import Katip (Katip, Severity (WarningS), logFM, ls)
import Control.Monad (when)
import System.Metrics.Counter (Counter)
import qualified System.Metrics.Counter as Counter
import System.Metrics (Store, createCounter)
data RateLimitMetrics = RateLimitMetrics
{ rlRetries :: Counter
, rlRejects :: Counter
}
registerRateLimitMetrics :: MonadIO m => Store -> m RateLimitMetrics
registerRateLimitMetrics store = liftIO $ RateLimitMetrics
<$> createCounter "ratelimit.retry" store
<*> createCounter "ratelimit.exhausted" store
newtype RateLimited m a = RateLimited (m a)
deriving (Functor, Applicative, Monad, MonadIO, KatipContext, Katip) via m
data RateLimiter = RateLimiter
{ metrics :: RateLimitMetrics
, limiter :: forall m a. (KatipContext m, MonadIO m) => RateLimited m a -> m (Maybe a)
}
slidingWindowLimiter
:: RateLimitMetrics
-> NominalDiffTime -- ^ Within how many seconds
-> Int -- ^ How many requests we can make
-> IO RateLimiter
slidingWindowLimiter metrics timeWindow size = do
window <- newTVarIO []
return $ RateLimiter metrics $ \(RateLimited action) -> do
now <- liftIO getPOSIXTime
allowed <- liftIO $ atomically $ do
elements <- takeWhile (\x -> x > now - timeWindow) <$> readTVar window
if length elements >= size
then pure False
else True <$ writeTVar window (now : elements)
if allowed
then Just <$> action
else pure Nothing
data RateLimitException = RateLimitExhausted
deriving Show
instance Exception RateLimitException
runRateLimited :: (MonadIO m, KatipContext m, MonadCatch m) => RateLimiter -> RateLimited m a -> m a
runRateLimited (RateLimiter metrics run) action = checkpointCallStack $ do
x <- retrying retryPolicy (const $ pure . isNothing) $ \RetryStatus{rsIterNumber} -> do
when (rsIterNumber > 0) $ do
logFM WarningS $ ls $ "Rate limited, attempt no. " <> show rsIterNumber
incRetry
run action
maybe (incReject >> throw RateLimitExhausted) pure x
where
retryPolicy = fullJitterBackoff 50000 <> limitRetries 5
incRetry = liftIO $ Counter.inc (rlRetries metrics)
incReject = liftIO $ Counter.inc (rlRejects metrics)