-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replicate counter identity rules #51
Changes from all commits
f136c12
b61cb08
bdd1bad
7c62315
77b1723
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,30 +4,40 @@ | |
|
||
-- | Tests for "Fencer.Rules". | ||
module Fencer.Rules.Test | ||
( test_loadRulesYaml | ||
, test_loadRulesNonYaml | ||
, test_loadRulesRecursively | ||
( test_rulesLoadRulesYaml | ||
, test_rulesLoadRulesNonYaml | ||
, test_rulesLoadRulesRecursively | ||
, test_rulesLimitUnitChange | ||
) | ||
where | ||
|
||
import BasePrelude | ||
|
||
import qualified Data.HashMap.Strict as HM | ||
import Data.List (sortOn) | ||
import qualified Data.List.NonEmpty as NE | ||
import Data.Maybe (fromMaybe) | ||
import Data.Text (Text) | ||
import qualified Data.Text.IO as TIO | ||
import Test.Tasty (TestTree) | ||
import Test.Tasty.HUnit (assertEqual, testCase) | ||
import qualified System.IO.Temp as Temp | ||
import NeatInterpolation (text) | ||
import qualified StmContainers.Map as StmMap | ||
import qualified System.IO.Temp as Temp | ||
import System.FilePath ((</>)) | ||
import System.Directory (createDirectoryIfMissing) | ||
import Data.List (sortOn) | ||
import Test.Tasty (TestTree, withResource) | ||
import Test.Tasty.HUnit (assertBool, assertEqual, testCase) | ||
|
||
import Fencer.Types | ||
import Fencer.AppState (appStateCounters, appStateRules, recordHits, setRules) | ||
import Fencer.Counter (CounterKey(..), counterHits) | ||
import Fencer.Rules | ||
import Fencer.Types | ||
|
||
import Fencer.Server.Test (createServerAppState, destroyServerAppState) | ||
|
||
|
||
-- | Test that 'loadRulesFromDirectory' loads rules from YAML files. | ||
test_loadRulesYaml :: TestTree | ||
test_loadRulesYaml = | ||
test_rulesLoadRulesYaml :: TestTree | ||
test_rulesLoadRulesYaml = | ||
testCase "Rules are loaded from YAML files" $ do | ||
Temp.withSystemTempDirectory "fencer-config" $ \tempDir -> do | ||
TIO.writeFile (tempDir </> "config1.yml") domain1Text | ||
|
@@ -42,8 +52,8 @@ test_loadRulesYaml = | |
-- YAML files. | ||
-- | ||
-- This counterintuitive behavior matches the behavior of @lyft/ratelimit@. | ||
test_loadRulesNonYaml :: TestTree | ||
test_loadRulesNonYaml = | ||
test_rulesLoadRulesNonYaml :: TestTree | ||
test_rulesLoadRulesNonYaml = | ||
testCase "Rules are loaded from non-YAML files" $ do | ||
Temp.withSystemTempDirectory "fencer-config" $ \tempDir -> do | ||
TIO.writeFile (tempDir </> "config1.bin") domain1Text | ||
|
@@ -57,8 +67,8 @@ test_loadRulesNonYaml = | |
-- | Test that 'loadRulesFromDirectory' loads rules recursively. | ||
-- | ||
-- This matches the behavior of @lyft/ratelimit@. | ||
test_loadRulesRecursively :: TestTree | ||
test_loadRulesRecursively = | ||
test_rulesLoadRulesRecursively :: TestTree | ||
test_rulesLoadRulesRecursively = | ||
testCase "Rules are loaded recursively" $ do | ||
Temp.withSystemTempDirectory "fencer-config" $ \tempDir -> do | ||
createDirectoryIfMissing True (tempDir </> "domain1") | ||
|
@@ -71,6 +81,78 @@ test_loadRulesRecursively = | |
(sortOn domainDefinitionId [domain1, domain2]) | ||
(sortOn domainDefinitionId definitions) | ||
|
||
-- | Test that a rule limit unit change adds a new counter and leaves | ||
-- the old one intact. | ||
test_rulesLimitUnitChange :: TestTree | ||
test_rulesLimitUnitChange = | ||
withResource createServerAppState destroyServerAppState $ \ioLogIdState -> | ||
testCase "A rule limit unit change on rule reloading" $ do | ||
Temp.withSystemTempDirectory "fencer-config-unit" $ \tempDir -> do | ||
createDirectoryIfMissing True (tempDir </> dir) | ||
|
||
definitions1 <- writeLoad tempDir merchantLimitsText1 | ||
(_, _, state) <- ioLogIdState | ||
|
||
atomically $ setRules state (mapRuleDefs definitions1) | ||
|
||
ruleTree :: RuleTree <- atomically $ | ||
fromMaybe' <$> StmMap.lookup domainId (appStateRules state) | ||
let ruleBranch = fromMaybe' $ HM.lookup (ruleKey, Just ruleValue) ruleTree | ||
let rateLimit = fromMaybe' $ ruleBranchRateLimit ruleBranch | ||
|
||
-- Record a hit | ||
void $ atomically $ recordHits state (#hits 1) (#limit rateLimit) counterKey1 | ||
|
||
mV1 <- atomically $ StmMap.lookup counterKey1 $ appStateCounters state | ||
|
||
-- Change rules in the configuration | ||
definitions2 <- writeLoad tempDir merchantLimitsText2 | ||
|
||
-- Set the new rules and the rules reloaded flag | ||
atomically $ setRules state (mapRuleDefs definitions2) | ||
|
||
mV1' <- atomically $ StmMap.lookup counterKey1 $ appStateCounters state | ||
mV2 <- atomically $ StmMap.lookup counterKey2 $ appStateCounters state | ||
|
||
assertBool | ||
"The original counter was not updated after recording a hit!" | ||
((counterHits <$> mV1) == Just 1) | ||
assertBool | ||
"The original counter was mistakenly updated in the meantime!" | ||
(mV1 == mV1') | ||
assertBool "The secondary counter was set!" (mV2 == Nothing) | ||
where | ||
mapRuleDefs :: [DomainDefinition] -> [(DomainId, RuleTree)] | ||
mapRuleDefs defs = | ||
[ ( domainDefinitionId rule | ||
, definitionsToRuleTree (NE.toList . domainDefinitionDescriptors $ rule)) | ||
| rule <- defs | ||
] | ||
|
||
dir = "d11-ratelimits" | ||
cfgFile = "d11-ratelimits1.yaml" | ||
|
||
writeLoad :: FilePath -> Text -> IO [DomainDefinition] | ||
mdimjasevic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
writeLoad tempDir txt = do | ||
TIO.writeFile (tempDir </> dir </> cfgFile) txt | ||
loadRulesFromDirectory (#directory tempDir) (#ignoreDotFiles True) | ||
|
||
ruleKey = RuleKey "generic_key" | ||
ruleValue = RuleValue "dream11_order_create" | ||
domainId = DomainId "merchant_rate_limits" | ||
|
||
counterKey1 = CounterKey | ||
{ counterKeyDomain = domainId | ||
, counterKeyDescriptor = [ (ruleKey, ruleValue) ] | ||
, counterKeyUnit = Minute } | ||
|
||
counterKey2 :: CounterKey | ||
counterKey2 = counterKey1 { counterKeyUnit = Hour } | ||
|
||
fromMaybe' :: Maybe a -> a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this different from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It prints the stack trace when something goes wrong, so that it's at least clear where the error happened.
and no indication whatsoever where things went wrong. |
||
fromMaybe' = fromMaybe (error "") | ||
|
||
|
||
---------------------------------------------------------------------------- | ||
-- Sample definitions | ||
---------------------------------------------------------------------------- | ||
|
@@ -117,3 +199,25 @@ domain2Text = [text| | |
descriptors: | ||
- key: some key 2 | ||
|] | ||
|
||
merchantLimitsText1 :: Text | ||
merchantLimitsText1 = [text| | ||
domain: merchant_rate_limits | ||
descriptors: | ||
- key: generic_key | ||
value: dream11_order_create | ||
rate_limit: | ||
unit: minute | ||
requests_per_unit: 400000 | ||
|] | ||
|
||
merchantLimitsText2 :: Text | ||
merchantLimitsText2 = [text| | ||
domain: merchant_rate_limits | ||
descriptors: | ||
- key: generic_key | ||
value: dream11_order_create | ||
rate_limit: | ||
unit: hour | ||
requests_per_unit: 400000 | ||
|] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have several suggestions regarding this test.
First of all, it does not test the actual logic used by the server. It does the lookup on its own instead of using
getLimit
. To make it more faithful, I would propose:shouldRateLimitDescriptor
toFencer.AppState
and renaming itFencer.AppState
toFencer.Logic
or somethingrecordHits
and not usingrecordHits
in the testThen the test would also get moved to
Fencer.Logic.Test
since it's not related to theFencer.Rules
module.Secondly, here's what this test does:
This does not match the description of the test ("Test that a rule limit unit change adds a new counter [...]"). Also, there are bad implementations it does not catch – for instance, what if counters are removed during
recordHits
and not duringloadRules
?Here's a scenario I would propose instead: