{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
-- |
-- Module : Advent.Throttle
-- Copyright : (c) Justin Le 2018
-- License : BSD3
--
-- Maintainer : justin@jle.im
-- Stability : experimental
-- Portability : non-portable
--
-- (Internal) Implement throttling of API requests.
module Advent.Throttle (
Throttler
, newThrottler
, throttling
, setLimit
, getLimit
) where
import Control.Concurrent
import Control.Exception
import Data.IORef
data Throttler = Throt { _throtSem :: QSem
, _throtWaiting :: IORef Int
, _throtLim :: IORef Int
}
acquireThrottler :: Throttler -> IO Bool
acquireThrottler Throt{..} = do
currWait <- readIORef _throtWaiting
throtLim <- readIORef _throtLim
if currWait >= throtLim
then pure False
else do
atomicModifyIORef' _throtWaiting ((,()) . (+1))
waitQSem _throtSem `finally` atomicModifyIORef' _throtWaiting ((,()) . subtract 1)
pure True
releaseThrottler :: Throttler -> IO ()
releaseThrottler Throt{..} = signalQSem _throtSem
-- | Set the maximum capacity of a 'Throttler'
setLimit :: Throttler -> Int -> IO ()
setLimit Throt{..} = atomicWriteIORef _throtLim
-- | Get the current maximum capacity of a 'Throttler'
getLimit :: Throttler -> IO Int
getLimit Throt{..} = readIORef _throtLim
-- | Create a new 'Throttler' with a given maximum capacity.
newThrottler :: Int -> IO Throttler
newThrottler n = do
s <- newQSem 1
w <- newIORef 0
l <- newIORef n
pure Throt
{ _throtSem = s
, _throtWaiting = w
, _throtLim = l
}
-- | Perform an IO action with the given 'Throttler' and delay. The IO
-- action will "wait in line" and be performed when the line is clear. The
-- IO action will delay the next incoming IO action by the delay amount
-- given.
throttling
:: Throttler
-> Int -- ^ delay (in milliseconds)
-> IO a
-> IO (Maybe a)
throttling throt delay act = bracketOnError (acquireThrottler throt)
(const (releaseThrottler throt)) $ \case
False -> pure Nothing
True -> Just <$> do
res <- act
_ <- forkIO $ do
threadDelay delay
releaseThrottler throt
pure res