{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_HADDOCK not-home #-}
module Arbiter.Core.RateLimit.Schema
(
arbiterRateLimitPoliciesTable
, arbiterRateLimitPoliciesTableName
, arbiterRateLimitsTable
, arbiterRateLimitsTableName
, bucketSeedInsert
, createRateLimitPoliciesTableSQL
, createRateLimitsTableSQL
, alterRateLimitsDurabilitySQL
, upsertRateLimitPolicyRowSQL
, addRateLimitColumnsSQL
, addRateLimitCostColumnSQL
, createThrottledIndexSQL
, createRateLimitBucketTriggerFunctionsSQL
, createRateLimitBucketTriggersSQL
) where
import Data.Text (Text)
import Data.Text qualified as T
import NeatInterpolation (text)
import Arbiter.Core.Admission (effectivePolicyCol, policyUpsertSQL)
import Arbiter.Core.Job.Schema
( SchemaName
, TableName
, indexSQL
, jobQueueDLQTable
, jobQueueTable
, maintenanceFunctionNames
, statementTriggerSQL
)
import Arbiter.Core.RateLimit.Spec (Durability (..), RateLimitPolicy (..))
import Arbiter.Core.SqlLiterals (dollarQuote, doubleLiteral, sharedTable, textLiteral)
arbiterRateLimitPoliciesTable :: SchemaName -> Text
arbiterRateLimitPoliciesTable :: Text -> Text
arbiterRateLimitPoliciesTable Text
schemaName = Text -> Text -> Text
sharedTable Text
schemaName Text
arbiterRateLimitPoliciesTableName
arbiterRateLimitPoliciesTableName :: Text
arbiterRateLimitPoliciesTableName :: Text
arbiterRateLimitPoliciesTableName = Text
"arbiter_rate_limit_policies"
arbiterRateLimitsTableName :: Text
arbiterRateLimitsTableName :: Text
arbiterRateLimitsTableName = Text
"arbiter_rate_limits"
arbiterRateLimitsTable :: SchemaName -> Text
arbiterRateLimitsTable :: Text -> Text
arbiterRateLimitsTable Text
schemaName = Text -> Text -> Text
sharedTable Text
schemaName Text
arbiterRateLimitsTableName
createRateLimitPoliciesTableSQL :: SchemaName -> Text
createRateLimitPoliciesTableSQL :: Text -> Text
createRateLimitPoliciesTableSQL Text
schemaName =
[Text] -> Text
T.unlines
[ Text
"CREATE TABLE IF NOT EXISTS " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text
arbiterRateLimitPoliciesTable Text
schemaName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ("
, Text
" prefix_id TEXT PRIMARY KEY,"
, Text
" default_max_tokens DOUBLE PRECISION NOT NULL CHECK (default_max_tokens >= 0),"
, Text
" default_refill_amount DOUBLE PRECISION NOT NULL CHECK (default_refill_amount >= 0),"
, Text
" default_interval DOUBLE PRECISION NOT NULL CHECK (default_interval > 0),"
, Text
" override_max_tokens DOUBLE PRECISION CHECK (override_max_tokens >= 0),"
, Text
" override_refill_amount DOUBLE PRECISION CHECK (override_refill_amount >= 0),"
, Text
" override_interval DOUBLE PRECISION CHECK (override_interval > 0)"
, Text
");"
]
createRateLimitsTableSQL :: SchemaName -> Text
createRateLimitsTableSQL :: Text -> Text
createRateLimitsTableSQL Text
schemaName =
[Text] -> Text
T.unlines
[ Text
"CREATE UNLOGGED TABLE IF NOT EXISTS " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text
arbiterRateLimitsTable Text
schemaName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ("
, Text
" rate_limit_key TEXT PRIMARY KEY,"
, Text
" policy_prefix TEXT NOT NULL,"
, Text
" tokens DOUBLE PRECISION NOT NULL,"
, Text
" last_refill TIMESTAMPTZ NOT NULL"
, Text
") WITH (fillfactor = 80);"
]
alterRateLimitsDurabilitySQL :: SchemaName -> Durability -> Text
alterRateLimitsDurabilitySQL :: Text -> Durability -> Text
alterRateLimitsDurabilitySQL Text
schemaName Durability
dur =
Text
"ALTER TABLE " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text
arbiterRateLimitsTable Text
schemaName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
set Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
";"
where
set :: Text
set = case Durability
dur of
Durability
Durable -> Text
" SET LOGGED"
Durability
Unlogged -> Text
" SET UNLOGGED"
upsertRateLimitPolicyRowSQL :: SchemaName -> RateLimitPolicy -> Text
upsertRateLimitPolicyRowSQL :: Text -> RateLimitPolicy -> Text
upsertRateLimitPolicyRowSQL Text
schemaName RateLimitPolicy
policy =
Text -> Text -> [(Text, Text)] -> Text
policyUpsertSQL
(Text -> Text
arbiterRateLimitPoliciesTable Text
schemaName)
(Text -> Text
textLiteral (RateLimitPolicy -> Text
policyPrefix RateLimitPolicy
policy))
[ (Text
"default_max_tokens", Double -> Text
doubleLiteral (RateLimitPolicy -> Double
policyMax RateLimitPolicy
policy))
, (Text
"default_refill_amount", Double -> Text
doubleLiteral (RateLimitPolicy -> Double
policyRefill RateLimitPolicy
policy))
, (Text
"default_interval", Double -> Text
doubleLiteral (NominalDiffTime -> Double
forall a b. (Real a, Fractional b) => a -> b
realToFrac (RateLimitPolicy -> NominalDiffTime
policyInterval RateLimitPolicy
policy)))
]
addRateLimitColumnsSQL :: SchemaName -> TableName -> Text
addRateLimitColumnsSQL :: Text -> Text -> Text
addRateLimitColumnsSQL Text
schemaName Text
tableName =
[Text] -> Text
T.unlines
[ Text
"ALTER TABLE " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text -> Text
jobQueueTable Text
schemaName Text
tableName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ADD COLUMN IF NOT EXISTS rate_limit_key TEXT;"
, Text
"ALTER TABLE " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text -> Text
jobQueueTable Text
schemaName Text
tableName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ADD COLUMN IF NOT EXISTS rate_limit_prefix TEXT;"
, Text
"ALTER TABLE " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text -> Text
jobQueueTable Text
schemaName Text
tableName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ADD COLUMN IF NOT EXISTS throttled_until TIMESTAMPTZ;"
, Text
"ALTER TABLE " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text -> Text
jobQueueDLQTable Text
schemaName Text
tableName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ADD COLUMN IF NOT EXISTS rate_limit_key TEXT;"
, Text
"ALTER TABLE " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text -> Text
jobQueueDLQTable Text
schemaName Text
tableName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ADD COLUMN IF NOT EXISTS rate_limit_prefix TEXT;"
]
addRateLimitCostColumnSQL :: SchemaName -> TableName -> Text
addRateLimitCostColumnSQL :: Text -> Text -> Text
addRateLimitCostColumnSQL Text
schemaName Text
tableName =
[Text] -> Text
T.unlines
[ Text
"ALTER TABLE "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text -> Text
jobQueueTable Text
schemaName Text
tableName
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ADD COLUMN IF NOT EXISTS rate_limit_cost DOUBLE PRECISION NOT NULL DEFAULT 1;"
, Text
"ALTER TABLE "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> Text -> Text
jobQueueDLQTable Text
schemaName Text
tableName
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ADD COLUMN IF NOT EXISTS rate_limit_cost DOUBLE PRECISION NOT NULL DEFAULT 1;"
]
createRateLimitBucketTriggerFunctionsSQL :: SchemaName -> TableName -> Text
createRateLimitBucketTriggerFunctionsSQL :: Text -> Text -> Text
createRateLimitBucketTriggerFunctionsSQL Text
schemaName Text
tableName =
let buckets :: Text
buckets = Text -> Text
arbiterRateLimitsTable Text
schemaName
policies :: Text
policies = Text -> Text
arbiterRateLimitPoliciesTable Text
schemaName
baseName :: Text
baseName = Text -> Text
bucketTriggerBase Text
tableName
(Text
funcInsert, Text
_funcDelete, Text
funcUpdate) = Text -> Text -> (Text, Text, Text)
maintenanceFunctionNames Text
schemaName Text
baseName
in [Text] -> Text
T.unlines
[ Text -> Text -> Text -> Text
bucketInsertFunction Text
funcInsert Text
buckets Text
policies
, Text -> Text -> Text -> Text
bucketUpdateFunction Text
funcUpdate Text
buckets Text
policies
]
bucketSeedInsert :: Text -> Text -> Text -> Text -> Text
bucketSeedInsert :: Text -> Text -> Text -> Text -> Text
bucketSeedInsert Text
buckets Text
policies Text
source Text
match =
let effMax :: Text
effMax = Text -> Text -> Text
effectivePolicyCol Text
"p" Text
"max_tokens"
in [text|
INSERT INTO ${buckets} (rate_limit_key, policy_prefix, tokens, last_refill)
SELECT n.rate_limit_key, MAX(n.rate_limit_prefix),
MAX(${effMax}), NOW()
FROM ${source}
JOIN ${policies} p ON p.prefix_id = n.rate_limit_prefix
WHERE ${match}
AND NOT EXISTS (SELECT 1 FROM ${buckets} b WHERE b.rate_limit_key = n.rate_limit_key)
GROUP BY n.rate_limit_key
ORDER BY n.rate_limit_key
ON CONFLICT (rate_limit_key) DO NOTHING
|]
bucketInsertFunction :: Text -> Text -> Text -> Text
bucketInsertFunction :: Text -> Text -> Text -> Text
bucketInsertFunction Text
funcName Text
buckets Text
policies =
let seed :: Text
seed = Text -> Text -> Text -> Text -> Text
bucketSeedInsert Text
buckets Text
policies Text
"new_table n" Text
"n.rate_limit_key IS NOT NULL"
in [text|
CREATE OR REPLACE FUNCTION ${funcName}()
RETURNS TRIGGER AS ${dollarQuote}
BEGIN
IF NOT EXISTS (SELECT 1 FROM new_table WHERE rate_limit_key IS NOT NULL LIMIT 1) THEN
RETURN NULL;
END IF;
${seed};
RETURN NULL;
END;
${dollarQuote} LANGUAGE plpgsql;
|]
bucketUpdateFunction :: Text -> Text -> Text -> Text
bucketUpdateFunction :: Text -> Text -> Text -> Text
bucketUpdateFunction Text
funcName Text
buckets Text
policies =
let seed :: Text
seed =
Text -> Text -> Text -> Text -> Text
bucketSeedInsert
Text
buckets
Text
policies
Text
"new_table n JOIN old_table o ON o.id = n.id"
Text
"n.rate_limit_key IS NOT NULL AND n.rate_limit_key IS DISTINCT FROM o.rate_limit_key"
in [text|
CREATE OR REPLACE FUNCTION ${funcName}()
RETURNS TRIGGER AS ${dollarQuote}
BEGIN
IF NOT EXISTS (
SELECT 1 FROM new_table n JOIN old_table o ON o.id = n.id
WHERE n.rate_limit_key IS NOT NULL AND n.rate_limit_key IS DISTINCT FROM o.rate_limit_key
LIMIT 1
) THEN
RETURN NULL;
END IF;
${seed};
RETURN NULL;
END;
${dollarQuote} LANGUAGE plpgsql;
|]
createRateLimitBucketTriggersSQL :: SchemaName -> TableName -> Text
Text
schemaName Text
tableName =
let tbl :: Text
tbl = Text -> Text -> Text
jobQueueTable Text
schemaName Text
tableName
baseName :: Text
baseName = Text -> Text
bucketTriggerBase Text
tableName
in Text -> [Text] -> Text
T.intercalate
Text
"\n\n"
[ Text -> Text -> Text -> Text -> Text -> Text -> Text
statementTriggerSQL Text
schemaName Text
tbl Text
baseName Text
"_insert" Text
"INSERT" Text
"NEW TABLE AS new_table"
, Text -> Text -> Text -> Text -> Text -> Text -> Text
statementTriggerSQL Text
schemaName Text
tbl Text
baseName Text
"_update" Text
"UPDATE" Text
"OLD TABLE AS old_table NEW TABLE AS new_table"
]
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"\n"
createThrottledIndexSQL :: SchemaName -> TableName -> Text
createThrottledIndexSQL :: Text -> Text -> Text
createThrottledIndexSQL Text
schemaName Text
tableName =
Text -> Text -> Text -> Maybe Text -> Text
indexSQL
(Text
"idx_" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
tableName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"_throttled")
(Text -> Text -> Text
jobQueueTable Text
schemaName Text
tableName)
Text
"rate_limit_prefix, rate_limit_key"
(Text -> Maybe Text
forall a. a -> Maybe a
Just Text
"throttled_until IS NOT NULL")
bucketTriggerBase :: TableName -> Text
bucketTriggerBase :: Text -> Text
bucketTriggerBase Text
tableName = Text
"ensure_" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
tableName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"_rate_limit_buckets"