11{-# LANGUAGE LambdaCase #-}
2- {-# LANGUAGE NamedFieldPuns #-}
2+ {-# LANGUAGE NumericUnderscores #-}
33{-# LANGUAGE OverloadedRecordDot #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE RecordWildCards #-}
@@ -15,27 +15,79 @@ import Cardano.Tracer.Handlers.Metrics.Utils (contentHdrUtf8Text)
1515import Cardano.Tracer.MetaTrace
1616import Cardano.Tracer.Timeseries
1717
18+ import Control.Concurrent (threadDelay )
19+ import Control.Concurrent.Extra (forkIO )
20+ import Control.Concurrent.STM (atomically )
21+ import Control.Concurrent.STM.TVar (modifyTVar' , newTVarIO , readTVar , readTVarIO ,
22+ writeTVar )
1823import Control.Monad (guard )
1924import qualified Data.ByteString.Lazy as BL
25+ import Data.Functor (void )
2026import Data.Maybe (fromMaybe )
21- import Data.Text ( Text )
27+ import Data.Text.Encoding ( decodeUtf8Lenient )
2228import qualified Data.Text.Encoding as T
2329import Network.HTTP.Types
2430import Network.Wai
2531import Network.Wai.Handler.Warp hiding (run )
2632import Network.Wai.Handler.WarpTLS
33+ import Network.Wai.Middleware.RequestSizeLimit (defaultRequestSizeLimitSettings ,
34+ requestSizeLimitMiddleware , setMaxLengthForRequest )
35+ import Network.Wai.Middleware.Timeout (timeout )
36+ import Network.Wai.RateLimit
37+ import Network.Wai.RateLimit.Backend (Backend (.. ))
38+ import Network.Wai.RateLimit.Strategy
2739import System.Time.Extra (sleep )
2840
29- parseTimeseriesQuery :: Request -> Maybe Text
41+ -- COMMENT: (@russoul) make the options below configurable?
42+ -- COMMENT: (@russoul) move the rate limiter backend to its own module?
43+ -- COMMENT: (@russoul) make the limiters applicable to every server we have?
44+
45+ -- | Maximum request body size (KB).
46+ requestBodySizeLimiterKB :: Word
47+ requestBodySizeLimiterKB = 2 * 1024
48+
49+ -- | Fixed time window of the request limiter (sec).
50+ requestRateLimiterWindowSec :: Word
51+ requestRateLimiterWindowSec = 60
52+
53+ -- | Maximum number of requests in every window.
54+ requestRateLimiterLimitSec :: Word
55+ requestRateLimiterLimitSec = 30
56+
57+ -- | Maximum duration of response generation per every request (sec).
58+ responseTimeoutSec :: Word
59+ responseTimeoutSec = 5
60+
61+ -- | Simple request rate limiter backend that limits the rate of
62+ -- requests based on the total number of requests.
63+ totalRequestRateLimiterBackend :: IO (Backend () )
64+ totalRequestRateLimiterBackend = do
65+ usage <- newTVarIO (0 :: Integer )
66+
67+ let
68+ backendGetUsage :: () -> IO Integer
69+ backendGetUsage _ = readTVarIO usage
70+
71+ backendIncAndGetUsage :: () -> Integer -> IO Integer
72+ backendIncAndGetUsage _ k = atomically $ modifyTVar' usage (+ k) >> readTVar usage
73+
74+ backendExpireIn :: () -> Integer -> IO ()
75+ backendExpireIn _ s = void $ forkIO $ do
76+ threadDelay (fromIntegral (s * 1_000_000 ))
77+ atomically $ writeTVar usage 0
78+
79+ pure $ MkBackend backendGetUsage backendIncAndGetUsage backendExpireIn
80+
81+ -- | GET timeseries/query
82+ parseTimeseriesQuery :: Request -> Maybe ()
3083parseTimeseriesQuery request = do
3184 guard (request. pathInfo == [" timeseries" , " query" ])
32- case queryToQueryText request. queryString of
33- [(" query" , Just str)] -> pure str
34- _ -> Nothing
85+ guard (request. requestMethod == methodPost)
3586
36- -- | timeseries/query?query=...
3787timeseriesApp :: TimeseriesHandle -> Application
38- timeseriesApp handle (parseTimeseriesQuery -> Just query) send = do
88+ timeseriesApp handle request@ (parseTimeseriesQuery -> Just () ) send = do
89+ bs <- consumeRequestBodyStrict request
90+ let query = decodeUtf8Lenient (BL. toStrict bs)
3991 now <- getTimeMs
4092 execute handle (fromIntegral now) query >>= \ case
4193 Left err -> send $
@@ -58,6 +110,8 @@ runTimeseriesServer tr tracerConfig endpoint handle = do
58110 { ttTimeseriesEndpoint = endpoint
59111 }
60112
113+ requestRateLimiterBackend <- totalRequestRateLimiterBackend
114+
61115 let
62116 settings :: Settings
63117 settings = setEndpoint endpoint defaultSettings
@@ -67,7 +121,24 @@ runTimeseriesServer tr tracerConfig endpoint handle = do
67121 tlsSettingsChain certificateFile (fromMaybe [] certificateChain) certificateKeyFile
68122
69123 application :: Application
70- application = timeseriesApp handle
124+ application =
125+ -- request body size limiter
126+ requestSizeLimitMiddleware
127+ (setMaxLengthForRequest (const (pure (Just (fromIntegral requestBodySizeLimiterKB * 1024 ))))
128+ defaultRequestSizeLimitSettings)
129+ .
130+ -- request rate limiter (fixed window)
131+ rateLimiting (fixedWindow requestRateLimiterBackend
132+ (fromIntegral requestRateLimiterWindowSec)
133+ (fromIntegral requestRateLimiterLimitSec)
134+ (const (pure () ))
135+ )
136+ .
137+ -- response time limiter
138+ timeout (fromIntegral responseTimeoutSec)
139+ $
140+ timeseriesApp handle
141+
71142
72143 run :: IO ()
73144 run | Just True <- epForceSSL endpoint , Just cert <- tlsCertificate tracerConfig
0 commit comments