{-| Module : Irc.RateLimit Description : Rate limit operations for IRC Copyright : (c) Eric Mertens, 2016 License : ISC Maintainer : emertens@gmail.com This module implements a simple rate limiter based on the IRC RFC to be used to keep an IRC client from getting disconnected for flooding. It allows one event per duration with a given threshold. This algorithm keeps track of the time at which the client may start sending messages. Each message sent advances that time into the future by the @penalty@. The client is allowed to transmit up to @threshold@ seconds ahead of this time. -} module Irc.RateLimit ( RateLimit , newRateLimit , tickRateLimit ) where import Control.Concurrent import Control.Monad import Data.Time -- | The 'RateLimit' keeps track of rate limit settings as well -- as the current state of the limit. data RateLimit = RateLimit { rateStamp :: !(MVar UTCTime) -- ^ Time that client can send , rateThreshold :: !Int -- ^ seconds , ratePenalty :: !Int -- ^ seconds } -- | Construct a new rate limit with the given penalty and threshold. newRateLimit :: Int {- ^ penalty -} -> Int {- ^ threshold -} -> IO RateLimit newRateLimit penalty threshold = do unless (penalty > 0) (fail "newRateLimit: Penalty too small") unless (threshold > 0) (fail "newRateLimit: Threshold too small") now <- getCurrentTime ref <- newMVar now return RateLimit { rateStamp = ref , rateThreshold = threshold , ratePenalty = penalty } -- | Account for an event in the context of a 'RateLimit'. This command -- will block and delay as required to satisfy the current rate. Once -- it returns it is safe to proceed with the rate limited action. tickRateLimit :: RateLimit -> IO () tickRateLimit r = modifyMVar_ (rateStamp r) $ \stamp -> do now <- getCurrentTime let stamp' = fromIntegral (ratePenalty r) `addUTCTime` max stamp now diff = diffUTCTime stamp' now excess = diff - fromIntegral (rateThreshold r) when (excess > 0) (threadDelay (ceiling (1000000 * realToFrac excess :: Rational))) return stamp'