token-limiter-concurrent-0.0.0.0: test/Control/Concurrent/TokenLimiter/ConcurrentSpec.hs
{-# LANGUAGE NumericUnderscores #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Control.Concurrent.TokenLimiter.ConcurrentSpec (spec) where
import Control.Concurrent.Async
import Control.Concurrent.TokenLimiter.Concurrent
import Control.Monad
import Data.Word
import GHC.Clock
import Test.QuickCheck
import Test.Syd
import Test.Syd.Validity
instance Validity TokenLimitConfig
instance GenValid TokenLimitConfig where
genValid = genValidStructurallyWithoutExtraChecking
shrinkValid = shrinkValidStructurallyWithoutExtraFiltering
spec :: Spec
spec = do
describe "computeCurrentCount" $ do
it "works in this super simple case" $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 0,
tokenLimitConfigMaxTokens = 1,
tokenLimitConfigTokensPerSecond = 1
}
-- One nanosecond later we shouldn't have any more tokens
computeCurrentCount config 0 0 1 `shouldBe` 0
it "Does not need to wait whole seconds to work" $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 0,
tokenLimitConfigMaxTokens = 1000,
tokenLimitConfigTokensPerSecond = 1000
}
-- After half a second we should have 500 tokens
computeCurrentCount config 0 0 500_000_000 `shouldBe` 500
it "Does not go over the maximum" $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 0,
tokenLimitConfigMaxTokens = 10,
tokenLimitConfigTokensPerSecond = 10
}
-- One nanosecond later we shouldn't have any more tokens
computeCurrentCount config 0 0 2_000_000_000 `shouldBe` 10
it "Can deal with overflow" $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = maxBound -1,
tokenLimitConfigMaxTokens = maxBound,
tokenLimitConfigTokensPerSecond = 1
}
-- 10 seconds later we should have no more than maxBound, eventhough the computed count would be maxBound + 9
computeCurrentCount config 0 maxBound 10_000_000_000 `shouldBe` maxBound
describe "makeTokenLimiter" $ do
it "always succeeds" $
forAllValid $ \config -> do
tokenLimiter <- makeTokenLimiter config
tokenLimiterConfig tokenLimiter `shouldBe` config
describe "canDebit" $ do
it "is correct for this simple example" $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 10,
tokenLimitConfigMaxTokens = 20,
tokenLimitConfigTokensPerSecond = 1
}
limiter <- makeTokenLimiter config
canDebit limiter 10 `shouldReturn` True
it "says true when tokens are available from the start and false otherwise" $
forAllValid $ \config ->
forAllValid $ \needed -> do
-- We set the tokens per second to 1 because otherwise the generated
-- value can be large enough for the time difference between making
-- the token limiter and running 'canDebit' to matter.
limiter <- makeTokenLimiter config {tokenLimitConfigTokensPerSecond = 1}
couldDebit <- canDebit limiter needed
couldDebit `shouldBe` needed <= min (tokenLimitConfigInitialTokens config) (tokenLimitConfigMaxTokens config)
describe "tryDebit" $ do
it "is correct for this simple example" $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 10,
tokenLimitConfigMaxTokens = 20,
tokenLimitConfigTokensPerSecond = 1
}
limiter <- makeTokenLimiter config
tryDebit limiter 10 `shouldReturn` True
it "says true when tokens are available from the start and false otherwise" $
forAllValid $ \config ->
forAllValid $ \needed -> do
-- We set the tokens per second to 1 because otherwise the generated
-- value can be large enough for the time difference between making
-- the token limiter and running 'tryDebit' to matter.
limiter <- makeTokenLimiter config {tokenLimitConfigTokensPerSecond = 1}
didDebit <- tryDebit limiter needed
didDebit `shouldBe` needed <= min (tokenLimitConfigInitialTokens config) (tokenLimitConfigMaxTokens config)
it "always says true if canDebit said true first and there are no other threads" $
forAllValid $ \config ->
forAllValid $ \needed -> do
limiter <- makeTokenLimiter config
couldDebit <- canDebit limiter needed
when couldDebit $ tryDebit limiter needed `shouldReturn` True
describe "waitDebit" $ do
it "does not need to wait for this simple example" $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 10,
tokenLimitConfigMaxTokens = 20,
tokenLimitConfigTokensPerSecond = 1
}
limiter <- makeTokenLimiter config
nanos <- time_ $ waitDebit limiter 10
nanos `shouldSatisfy` (< 1_000_000_000)
it "Does not need to wait if tokens are available" $
forAllValid $ \initial ->
forAllValid $ \maxTokens -> do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = initial,
tokenLimitConfigMaxTokens = maxTokens,
tokenLimitConfigTokensPerSecond = 1
}
limiter <- makeTokenLimiter config
let needed = min initial maxTokens
nanos <- time_ $ waitDebit limiter needed
nanos `shouldSatisfy` (< 1_000_000_000)
it "Waits appropriately when there is one threads that want tokens in this example." $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 0,
tokenLimitConfigMaxTokens = 1,
tokenLimitConfigTokensPerSecond = 1
}
limiter <- makeTokenLimiter config
nanos <- time_ $ waitDebit limiter 1
nanos `shouldSatisfy` (>= 1_000_000_000)
it "does not need to wait a whole number of seconds" $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 0,
tokenLimitConfigMaxTokens = 10,
tokenLimitConfigTokensPerSecond = 10
}
limiter <- makeTokenLimiter config
nanos <- time_ $ waitDebit limiter 1
nanos `shouldSatisfy` (<= 500_000_000)
it "Waits appropriately when there are multiple threads that want tokens at the same time in this example." $ do
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 0,
tokenLimitConfigMaxTokens = 1,
tokenLimitConfigTokensPerSecond = 1
}
limiter <- makeTokenLimiter config
nanos <- time_ $ concurrently_ (waitDebit limiter 1) (waitDebit limiter 1)
nanos `shouldSatisfy` (>= 2_000_000_000)
modifyMaxSuccess (`div` 50) $
it "Waits appropriately when there are many threads that want tokens at the same time in this example." $ do
forAll (sized pure) $ \numberOfThreads -> do
print numberOfThreads
let config =
TokenLimitConfig
{ tokenLimitConfigInitialTokens = 0,
tokenLimitConfigMaxTokens = 1,
tokenLimitConfigTokensPerSecond = 100
}
limiter <- makeTokenLimiter config
nanos <- time_ $ replicateConcurrently_ numberOfThreads (waitDebit limiter 1)
nanos `shouldSatisfy` (>= fromIntegral numberOfThreads * 10_000_000)
time_ :: IO a -> IO Word64
time_ func = snd <$> time func
time :: IO a -> IO (a, Word64)
time func = do
begin <- getMonotonicTimeNSec
result <- func
end <- getMonotonicTimeNSec
pure (result, end - begin)