Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 135 additions & 41 deletions apisix/plugins/limit-count/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local get_phase = ngx.get_phase
local tonumber = tonumber
local type = type
local tostring = tostring
local str_format = string.format

local limit_redis_cluster_new
local limit_redis_new
Expand Down Expand Up @@ -82,6 +83,28 @@ local schema = {
{type = "string"},
},
},
rules = {
type = "array",
items = {
type = "object",
properties = {
count = {
oneOf = {
{type = "integer", exclusiveMinimum = 0},
{type = "string"},
},
},
time_window = {
oneOf = {
{type = "integer", exclusiveMinimum = 0},
{type = "string"},
},
},
key = {type = "string"},
},
required = {"count", "time_window", "key"},
},
},
group = {type = "string"},
key = {type = "string", default = "remote_addr"},
key_type = {type = "string",
Expand All @@ -102,7 +125,14 @@ local schema = {
allow_degradation = {type = "boolean", default = false},
show_limit_quota_header = {type = "boolean", default = true}
},
required = {"count", "time_window"},
oneOf = {
{
required = {"count", "time_window"},
},
{
required = {"rules"},
}
},
["if"] = {
properties = {
policy = {
Expand Down Expand Up @@ -180,51 +210,34 @@ function _M.check_schema(conf, schema_type)
end
end

return true
end


local function create_limit_obj(conf, ctx, plugin_name)
core.log.info("create new " .. plugin_name .. " plugin instance")

local count = conf.count
if type(count) == "string" then
local err, _
count, err, _ = core.utils.resolve_var(count, ctx.var)
if err then
return nil, "could not resolve vars in count: " .. err
end
count = tonumber(count)
if not count then
return nil, "resolved count is not a number: " .. tostring(count)
local keys = {}
for _, rule in ipairs(conf.rules or {}) do
if keys[rule.key] then
return false, str_format("duplicate key '%s' in rules", rule.key)
end
keys[rule.key] = true
end

local time_window = conf.time_window
if type(time_window) == "string" then
local err, _
time_window, err, _ = core.utils.resolve_var(time_window, ctx.var)
if err then
return nil, "could not resolve vars in time_window: " .. err
end
time_window = tonumber(time_window)
if not time_window then
return nil, "resolved time_window is not a number: " .. tostring(time_window)
end
end
return true
end


core.log.info("limit count: ", count, ", time_window: ", time_window)
local function create_limit_obj(conf, rule, plugin_name)
core.log.info("create new ", plugin_name, " plugin instance",
", rule: ", core.json.delay_encode(rule, true))

if not conf.policy or conf.policy == "local" then
return limit_local_new("plugin-" .. plugin_name, count, time_window)
return limit_local_new("plugin-" .. plugin_name, rule.count,
rule.time_window)
end

if conf.policy == "redis" then
return limit_redis_new("plugin-" .. plugin_name, count, time_window, conf)
return limit_redis_new("plugin-" .. plugin_name, rule.count, rule.time_window, conf)
end

if conf.policy == "redis-cluster" then
return limit_redis_cluster_new("plugin-" .. plugin_name, count, time_window, conf)
return limit_redis_cluster_new("plugin-" .. plugin_name, rule.count,
rule.time_window, conf)
end

return nil
Expand Down Expand Up @@ -258,11 +271,71 @@ local function gen_limit_key(conf, ctx, key)
end


function _M.rate_limit(conf, ctx, name, cost, dry_run)
core.log.info("ver: ", ctx.conf_version)
core.log.info("conf: ", core.json.delay_encode(conf, true))
local function resolve_var(ctx, value)
if type(value) == "string" then
local err, _
value, err, _ = core.utils.resolve_var(value, ctx.var)
if err then
return nil, "could not resolve var for value: " .. value .. ", err: " .. err
end
value = tonumber(value)
if not value then
return nil, "resolved value is not a number: " .. tostring(value)
end
end
return value
end

local lim, err = create_limit_obj(conf, ctx, name)

local function get_rules(ctx, conf)
if not conf.rules then
local count, err = resolve_var(ctx, conf.count)
if err then
return nil, err
end
local time_window, err2 = resolve_var(ctx, conf.time_window)
if err2 then
return nil, err2
end
return {
{
count = count,
time_window = time_window,
key = conf.key,
key_type = conf.key_type,
}
}
end

local rules = {}
for _, rule in ipairs(conf.rules) do
local count, err = resolve_var(ctx, rule.count)
if err then
goto CONTINUE
end
local time_window, err2 = resolve_var(ctx, rule.time_window)
if err2 then
goto CONTINUE
end
local key, _, n_resolved = core.utils.resolve_var(rule.key, ctx.var)
if n_resolved == 0 then
goto CONTINUE
end
core.table.insert(rules, {
count = count,
time_window = time_window,
key_type = "constant",
key = key,
})

::CONTINUE::
end
return rules
end


local function run_rate_limit(conf, rule, ctx, name, cost, dry_run)
local lim, err = create_limit_obj(conf, rule, name)

if not lim then
core.log.error("failed to fetch limit.count object: ", err)
Expand All @@ -272,9 +345,9 @@ function _M.rate_limit(conf, ctx, name, cost, dry_run)
return 500
end

local conf_key = conf.key
local conf_key = rule.key
local key
if conf.key_type == "var_combination" then
if rule.key_type == "var_combination" then
local err, n_resolved
key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var)
if err then
Expand All @@ -284,7 +357,7 @@ function _M.rate_limit(conf, ctx, name, cost, dry_run)
if n_resolved == 0 then
key = nil
end
elseif conf.key_type == "constant" then
elseif rule.key_type == "constant" then
key = conf_key
else
key = ctx.var[conf_key]
Expand Down Expand Up @@ -353,4 +426,25 @@ function _M.rate_limit(conf, ctx, name, cost, dry_run)
end


function _M.rate_limit(conf, ctx, name, cost, dry_run)
core.log.info("ver: ", ctx.conf_version)

local rules, err = get_rules(ctx, conf)
if not rules or #rules == 0 then
core.log.error("failed to get rate limit rules: ", err)
if conf.allow_degradation then
return
end
return 500
end

for _, rule in ipairs(rules) do
local code, msg = run_rate_limit(conf, rule, ctx, name, cost, dry_run)
if code then
return code, msg
end
end
end


return _M
8 changes: 6 additions & 2 deletions docs/en/latest/plugins/limit-count.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ You may see the following rate limiting headers in the response:

| Name | Type | Required | Default | Valid values | Description |
| ----------------------- | ------- | ----------------------------------------- | ------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| count | integer | True | | > 0 | The maximum number of requests allowed within a given time interval. |
| time_window | integer | True | | > 0 | The time interval corresponding to the rate limiting `count` in seconds. |
| count | integer | False | | > 0 | The maximum number of requests allowed within a given time interval. Required if `rules` is not configured. |
| time_window | integer | False | | > 0 | The time interval corresponding to the rate limiting `count` in seconds. Required if `rules` is not configured. |
| rules | array[object] | False | | | A list of rate limiting rules. Each rule is an object containing `count`, `time_window`, and `key`. |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add rows for rules.count, rules.time_winddow, and rules.key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

| rules.count | integer | True | | > 0 | The maximum number of requests allowed within a given time interval. |
| rules.time_window | integer | True | | > 0 | The time interval corresponding to the rate limiting `count` in seconds. |
| rules.key | string | True | | | The key to count requests by. If the configured key does not exist, the rule will not be executed. The `key` is interpreted as a combination of variables, for example: `$http_custom_a $http_custom_b`. |
| key_type | string | False | var | ["var","var_combination","constant"] | The type of key. If the `key_type` is `var`, the `key` is interpreted a variable. If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. If the `key_type` is `constant`, the `key` is interpreted as a constant. |
| key | string | False | remote_addr | | The key to count requests by. If the `key_type` is `var`, the `key` is interpreted a variable. The variable does not need to be prefixed by a dollar sign (`$`). If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. All variables should be prefixed by dollar signs (`$`). For example, to configure the `key` to use a combination of two request headers `custom-a` and `custom-b`, the `key` should be configured as `$http_custom_a $http_custom_b`. If the `key_type` is `constant`, the `key` is interpreted as a constant value. |
| rejected_code | integer | False | 503 | [200,...,599] | The HTTP status code returned when a request is rejected for exceeding the threshold. |
Expand Down
8 changes: 6 additions & 2 deletions docs/zh/latest/plugins/limit-count.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ description: limit-count 插件使用固定窗口算法,通过给定时间间

| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
| ------------------- | ------- | ---------- | ------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| count | integer | 是 | | > 0 | 给定时间间隔内允许的最大请求数。 |
| time_window | integer | 是 | | > 0 | 速率限制 `count` 对应的时间间隔(以秒为单位)。 |
| count | integer | 否 | | > 0 | 给定时间间隔内允许的最大请求数。如果未配置 `rules`,则此项必填。 |
| time_window | integer | 否 | | > 0 | 速率限制 `count` 对应的时间间隔(以秒为单位)。如果未配置 `rules`,则此项必填。 |
| rules | array[object] | 否 | | | 速率限制规则列表。每个规则是一个包含 `count`、`time_window` 和 `key` 的对象。如果配置了 `rules`,则顶层的 `count` 和 `time_window` 将被忽略。 |
| rules.count | integer | 是 | | > 0 | 给定时间间隔内允许的最大请求数。 |
| rules.time_window | integer | 是 | | > 0 | 速率限制 `count` 对应的时间间隔(以秒为单位)。 |
| rules.key | string | 是 | | | 用于统计请求的键。如果配置的键不存在,则不会执行该规则。`key` 被解释为变量的组合,例如:`$http_custom_a $http_custom_b`。|
| key_type | string | 否 | var | ["var","var_combination","constant"] | key 的类型。如果`key_type` 为 `var`,则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`,则 `key` 将被解释为变量的组合。如果 `key_type` 为 `constant`,则 `key` 将被解释为常量。 |
| key | string | 否 | remote_addr | | 用于计数请求的 key。如果 `key_type` 为 `var`,则 `key` 将被解释为变量。变量不需要以美元符号(`$`)为前缀。如果 `key_type` 为 `var_combination`,则 `key` 会被解释为变量的组合。所有变量都应该以美元符号 (`$`) 为前缀。例如,要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 的组合,则 `key` 应该配置为 `$http_custom_a $http_custom_b`。如果 `key_type` 为 `constant`,则 `key` 会被解释为常量值。|
| rejection_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 状态代码。|
Expand Down
2 changes: 1 addition & 1 deletion t/admin/consumer-group.t
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ passed
}
}
--- response_body
{"error_msg":"failed to check the configuration of plugin limit-count err: property \"count\" is required"}
{"error_msg":"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none"}
--- error_code: 400
Expand Down
2 changes: 1 addition & 1 deletion t/admin/plugin-configs.t
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ passed
}
}
--- response_body
{"error_msg":"failed to check the configuration of plugin limit-count err: property \"count\" is required"}
{"error_msg":"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none"}
--- error_code: 400
Expand Down
Loading
Loading