This is an automated email from the ASF dual-hosted git repository.
shreemaanabhishek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 21c7b9152 feat: support rules in limit-conn and ai-rate-limiting
(#13000)
21c7b9152 is described below
commit 21c7b915240bb6812c665167374cebd2b488c99a
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Wed Feb 11 14:13:41 2026 +0545
feat: support rules in limit-conn and ai-rate-limiting (#13000)
---
apisix/plugins/ai-rate-limiting.lua | 101 +++++++++++----
apisix/plugins/limit-conn.lua | 33 ++++-
apisix/plugins/limit-conn/init.lua | 116 +++++++++++++----
docs/en/latest/plugins/ai-rate-limiting.md | 8 +-
docs/en/latest/plugins/limit-conn.md | 10 +-
docs/zh/latest/plugins/ai-rate-limiting.md | 8 +-
docs/zh/latest/plugins/limit-conn.md | 12 +-
t/plugin/ai-rate-limiting.t | 199 +++++++++++++++++++++++++++++
t/plugin/limit-conn-variable.t | 185 ++++++++++++++++++++++++++-
t/plugin/limit-conn.t | 8 +-
t/plugin/limit-req.t | 5 +-
11 files changed, 608 insertions(+), 77 deletions(-)
diff --git a/apisix/plugins/ai-rate-limiting.lua
b/apisix/plugins/ai-rate-limiting.lua
index 6f4ce14b1..b24c392a8 100644
--- a/apisix/plugins/ai-rate-limiting.lua
+++ b/apisix/plugins/ai-rate-limiting.lua
@@ -76,17 +76,46 @@ local schema = {
rejected_msg = {
type = "string", minLength = 1
},
+ 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"},
+ },
+ },
},
dependencies = {
limit = {"time_window"},
time_window = {"limit"}
},
- anyOf = {
+ oneOf = {
{
- required = {"limit", "time_window"}
+ anyOf = {
+ {
+ required = {"limit", "time_window"}
+ },
+ {
+ required = {"instances"}
+ }
+ }
},
{
- required = {"instances"}
+ required = {"rules"},
}
}
}
@@ -109,6 +138,26 @@ end
local function transform_limit_conf(plugin_conf, instance_conf, instance_name)
+ local limit_conf = {
+ rejected_code = plugin_conf.rejected_code,
+ rejected_msg = plugin_conf.rejected_msg,
+ show_limit_quota_header = plugin_conf.show_limit_quota_header,
+
+ -- we may expose those fields to ai-rate-limiting later
+ policy = "local",
+ key_type = "constant",
+ allow_degradation = false,
+ sync_interval = -1,
+ limit_header = "X-AI-RateLimit-Limit",
+ remaining_header = "X-AI-RateLimit-Remaining",
+ reset_header = "X-AI-RateLimit-Reset",
+ }
+ if plugin_conf.rules and #plugin_conf.rules > 0 then
+ limit_conf.rules = plugin_conf.rules
+ limit_conf._meta = plugin_conf._meta
+ return limit_conf
+ end
+
local key = plugin_name .. "#global"
local limit = plugin_conf.limit
local time_window = plugin_conf.time_window
@@ -119,25 +168,15 @@ local function transform_limit_conf(plugin_conf,
instance_conf, instance_name)
limit = instance_conf.limit
time_window = instance_conf.time_window
end
- return {
- _vid = key,
-
- key = key,
- _meta = plugin_conf._meta,
- count = limit,
- time_window = time_window,
- rejected_code = plugin_conf.rejected_code,
- rejected_msg = plugin_conf.rejected_msg,
- show_limit_quota_header = plugin_conf.show_limit_quota_header,
- -- limit-count need these fields
- policy = "local",
- key_type = "constant",
- allow_degradation = false,
-
- limit_header = "X-AI-RateLimit-Limit-" .. name,
- remaining_header = "X-AI-RateLimit-Remaining-" .. name,
- reset_header = "X-AI-RateLimit-Reset-" .. name,
- }
+ limit_conf._vid = key
+ limit_conf.key = key
+ limit_conf._meta = plugin_conf._meta
+ limit_conf.count = limit
+ limit_conf.time_window = time_window
+ limit_conf.limit_header = "X-AI-RateLimit-Limit-" .. name
+ limit_conf.remaining_header = "X-AI-RateLimit-Remaining-" .. name
+ limit_conf.reset_header = "X-AI-RateLimit-Reset-" .. name
+ return limit_conf
end
@@ -168,8 +207,13 @@ function _M.access(conf, ctx)
return
end
- local limit_conf_kvs = limit_conf_cache(conf, nil, fetch_limit_conf_kvs,
conf)
- local limit_conf = limit_conf_kvs[ai_instance_name]
+ local limit_conf
+ if conf.rules and #conf.rules > 0 then
+ limit_conf = transform_limit_conf(conf)
+ else
+ local limit_conf_kvs = limit_conf_cache(conf, nil,
fetch_limit_conf_kvs, conf)
+ limit_conf = limit_conf_kvs[ai_instance_name]
+ end
if not limit_conf then
return
end
@@ -243,8 +287,13 @@ function _M.log(conf, ctx)
core.log.info("instance name: ", instance_name, " used tokens: ",
used_tokens)
- local limit_conf_kvs = limit_conf_cache(conf, nil, fetch_limit_conf_kvs,
conf)
- local limit_conf = limit_conf_kvs[instance_name]
+ local limit_conf
+ if conf.rules and #conf.rules > 0 then
+ limit_conf = transform_limit_conf(conf)
+ else
+ local limit_conf_kvs = limit_conf_cache(conf, nil,
fetch_limit_conf_kvs, conf)
+ limit_conf = limit_conf_kvs[instance_name]
+ end
if limit_conf then
limit_count.rate_limit(limit_conf, ctx, plugin_name, used_tokens)
end
diff --git a/apisix/plugins/limit-conn.lua b/apisix/plugins/limit-conn.lua
index dd88162af..7b79ac20a 100644
--- a/apisix/plugins/limit-conn.lua
+++ b/apisix/plugins/limit-conn.lua
@@ -54,9 +54,38 @@ local schema = {
rejected_msg = {
type = "string", minLength = 1
},
- allow_degradation = {type = "boolean", default = false}
+ allow_degradation = {type = "boolean", default = false},
+ rules = {
+ type = "array",
+ items = {
+ type = "object",
+ properties = {
+ conn = {
+ oneOf = {
+ {type = "integer", exclusiveMinimum = 0},
+ {type = "string"},
+ },
+ },
+ burst = {
+ oneOf = {
+ {type = "integer", minimum = 0},
+ {type = "string"},
+ },
+ },
+ key = {type = "string"},
+ },
+ required = {"conn", "burst", "key"},
+ },
+ },
+ },
+ oneOf = {
+ {
+ required = {"conn", "burst", "default_conn_delay", "key"},
+ },
+ {
+ required = {"default_conn_delay", "rules"},
+ }
},
- required = {"conn", "burst", "default_conn_delay", "key"},
["if"] = {
properties = {
policy = {
diff --git a/apisix/plugins/limit-conn/init.lua
b/apisix/plugins/limit-conn/init.lua
index 6c5c823e1..b45ff567c 100644
--- a/apisix/plugins/limit-conn/init.lua
+++ b/apisix/plugins/limit-conn/init.lua
@@ -21,6 +21,7 @@ local sleep = core.sleep
local tonumber = tonumber
local type = type
local tostring = tostring
+local ipairs = ipairs
local shdict_name = "plugin-limit-conn"
if ngx.config.subsystem == "stream" then
shdict_name = shdict_name .. "-stream"
@@ -40,34 +41,75 @@ end
local _M = {}
-local function create_limit_obj(ctx, conf)
- core.log.info("create new limit-conn plugin instance")
-
- local conn = conf.conn
- if type(conn) == "string" then
+local function resolve_var(ctx, value)
+ if type(value) == "string" then
local err, _
- conn, err, _ = core.utils.resolve_var(conn, ctx.var)
+ value, err, _ = core.utils.resolve_var(value, ctx.var)
if err then
- return nil, "could not resolve vars in conn: " .. err
+ return nil, "could not resolve var for value: " .. value .. ",
err: " .. err
end
- conn = tonumber(conn)
- if not conn then
- return nil, "resolved conn is not a number: " .. tostring(conn)
+ value = tonumber(value)
+ if not value then
+ return nil, "resolved value is not a number: " .. tostring(value)
end
end
+ return value
+end
- local burst = conf.burst
- if type(burst) == "string" then
- local err, _
- burst, err, _ = core.utils.resolve_var(burst, ctx.var)
+
+local function get_rules(ctx, conf)
+ if not conf.rules then
+ local conn, err = resolve_var(ctx, conf.conn)
+ if err then
+ return nil, err
+ end
+ local burst, err2 = resolve_var(ctx, conf.burst)
+ if err2 then
+ return nil, err2
+ end
+ return {
+ {
+ conn = conn,
+ burst = burst,
+ key = conf.key,
+ key_type = conf.key_type,
+ }
+ }
+ end
+
+ local rules = {}
+ for _, rule in ipairs(conf.rules) do
+ local conn, err = resolve_var(ctx, rule.conn)
if err then
- return nil, "could not resolve vars in burst: " .. err
+ goto CONTINUE
+ end
+ local burst, err2 = resolve_var(ctx, rule.burst)
+ if err2 then
+ goto CONTINUE
end
- burst = tonumber(burst)
- if not burst then
- return nil, "resolved burst is not a number: " .. tostring(burst)
+
+ local key, _, n_resolved = core.utils.resolve_var(rule.key, ctx.var)
+ if n_resolved == 0 then
+ goto CONTINUE
end
+ core.table.insert(rules, {
+ conn = conn,
+ burst = burst,
+ key_type = "constant",
+ key = key,
+ })
+
+ ::CONTINUE::
end
+ return rules
+end
+
+
+local function create_limit_obj(conf, rule, default_conn_delay)
+ core.log.info("create new limit-conn plugin instance")
+
+ local conn = rule.conn
+ local burst = rule.burst
core.log.info("limit conn: ", conn, ", burst: ", burst)
@@ -75,25 +117,24 @@ local function create_limit_obj(ctx, conf)
core.log.info("create new limit-conn redis plugin instance")
return redis_single_new("plugin-limit-conn", conf, conn, burst,
- conf.default_conn_delay)
+ default_conn_delay)
elseif conf.policy == "redis-cluster" then
core.log.info("create new limit-conn redis-cluster plugin instance")
return redis_cluster_new("plugin-limit-conn", conf, conn, burst,
- conf.default_conn_delay)
+ default_conn_delay)
else
core.log.info("create new limit-conn plugin instance")
return limit_conn_new(shdict_name, conn, burst,
- conf.default_conn_delay)
+ default_conn_delay)
end
end
-function _M.increase(conf, ctx)
- core.log.info("ver: ", ctx.conf_version)
- local lim, err = create_limit_obj(ctx, conf)
+local function run_limit_conn(conf, rule, ctx)
+ local lim, err = create_limit_obj(conf, rule, conf.default_conn_delay)
if not lim then
core.log.error("failed to instantiate a resty.limit.conn object: ",
err)
if conf.allow_degradation then
@@ -102,9 +143,9 @@ function _M.increase(conf, ctx)
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
@@ -114,6 +155,8 @@ function _M.increase(conf, ctx)
if n_resolved == 0 then
key = nil
end
+ elseif rule.key_type == "constant" then
+ key = conf_key
else
key = ctx.var[conf_key]
end
@@ -157,6 +200,27 @@ function _M.increase(conf, ctx)
end
+function _M.increase(conf, ctx)
+ 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 limit conn rules: ", err)
+ if conf.allow_degradation then
+ return
+ end
+ return 500
+ end
+
+ for _, rule in ipairs(rules) do
+ local code, msg = run_limit_conn(conf, rule, ctx)
+ if code then
+ return code, msg
+ end
+ end
+end
+
+
function _M.decrease(conf, ctx)
local limit_conn = ctx.limit_conn
if not limit_conn then
diff --git a/docs/en/latest/plugins/ai-rate-limiting.md
b/docs/en/latest/plugins/ai-rate-limiting.md
index c05006335..fd2f68730 100644
--- a/docs/en/latest/plugins/ai-rate-limiting.md
+++ b/docs/en/latest/plugins/ai-rate-limiting.md
@@ -41,8 +41,12 @@ The `ai-rate-limiting` Plugin enforces token-based rate
limiting for requests se
| Name | Type | Required | Default | Valid
values | Description |
|------------------------------|----------------|----------|----------|---------------------------------------------------------|-------------|
-| limit | integer | False | | >0
| The maximum number of tokens allowed within a given
time interval. At least one of `limit` and `instances.limit` should be
configured. |
-| time_window | integer | False | | >0
| The time interval corresponding to the rate limiting
`limit` in seconds. At least one of `time_window` and `instances.time_window`
should be configured. |
+| limit | integer | False | | >0
| The maximum number of tokens allowed within a given
time interval. At least one of `limit` and `instances.limit` should be
configured. Required if `rules` is not configured. |
+| time_window | integer | False | | >0
| The time interval corresponding to the rate limiting
`limit` in seconds. At least one of `time_window` and `instances.time_window`
should be configured. 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`. If
configured, this takes precedence over `limit` and `time_window`. |
+| rules.count | integer or string | True | | >0 or
variable expression | The maximum number of tokens
allowed within a given time interval. Can be a static integer or a variable
expression like `$http_custom_limit`. |
+| rules.time_window | integer or string | True | | >0 or
variable expression | The time interval
corresponding to the rate limiting `count` in seconds. Can be a static integer
or a variable expression. |
+| 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`. |
| show_limit_quota_header | boolean | False | true |
| If true, includes
`X-AI-RateLimit-Limit-*`, `X-AI-RateLimit-Remaining-*`, and
`X-AI-RateLimit-Reset-*` headers in the response, where `*` is the instance
name. |
| limit_strategy | string | False | total_tokens |
[total_tokens, prompt_tokens, completion_tokens] | Type of token to apply rate
limiting. `total_tokens` is the sum of `prompt_tokens` and `completion_tokens`.
|
| instances | array[object] | False | |
| LLM instance rate limiting
configurations. |
diff --git a/docs/en/latest/plugins/limit-conn.md
b/docs/en/latest/plugins/limit-conn.md
index 2d7b07884..1cba1976e 100644
--- a/docs/en/latest/plugins/limit-conn.md
+++ b/docs/en/latest/plugins/limit-conn.md
@@ -38,12 +38,16 @@ The `limit-conn` Plugin limits the rate of requests by the
number of concurrent
| Name | Type | Required | Default | Valid values |
Description |
|------------|---------|----------|-------------|-------------------|-----------------|
-| conn | integer | True | | > 0 | The maximum number of
concurrent requests allowed. Requests exceeding the configured limit and below
`conn + burst` will be delayed. |
-| burst | integer | True | | >= 0 | The number of
excessive concurrent requests allowed to be delayed per second. Requests
exceeding the limit will be rejected immediately. |
+| conn | integer | False | | > 0 | The maximum number of
concurrent requests allowed. Requests exceeding the configured limit and below
`conn + burst` will be delayed. Required if `rules` is not configured. |
+| burst | integer | False | | >= 0 | The number of
excessive concurrent requests allowed to be delayed per second. Requests
exceeding the limit will be rejected immediately. Required if `rules` is not
configured. |
| default_conn_delay | number | True | | > 0 | Processing
latency allowed in seconds for concurrent requests exceeding `conn + burst`,
which can be dynamically adjusted based on `only_use_default_delay` setting.
|
| only_use_default_delay | boolean | False | false | | If
false, delay requests proportionally based on how much they exceed the `conn`
limit. The delay grows larger as congestion increases. For instance, with
`conn` being `5`, `burst` being `3`, and `default_conn_delay` being `1`, 6
concurrent requests would result in a 1-second delay, 7 requests a 2-second
delay, 8 requests a 3-second delay, and so on, until the total limit of `conn +
burst` is reached, beyond which req [...]
+| rules | array[object] | False | |
| A list of connection limiting rules. Each rule is an object containing
`conn`, `burst`, and `key`. If configured, this takes precedence over `conn`,
`burst`, and `key`. |
+| rules.conn | integer or string | True | | > 0 or
variable expression | The maximum number of concurrent requests allowed. Can be
a static integer or a variable expression like `$http_custom_conn`. |
+| rules.burst | integer or string | True | | >= 0 or
variable expression | The number of excessive concurrent requests allowed to be
delayed. Can be a static integer or a variable expression. |
+| 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"] |
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. |
-| 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 [...]
+| 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 [...]
| key_ttl | integer | False | 3600 | | The TTL of the Redis
key in seconds. Used when `policy` is `redis` or `redis-cluster`. |
| rejected_code | integer | False | 503 | [200,...,599] | The HTTP
status code returned when a request is rejected for exceeding the threshold.
|
| rejected_msg | string | False | | non-empty | The
response body returned when a request is rejected for exceeding the threshold.
|
diff --git a/docs/zh/latest/plugins/ai-rate-limiting.md
b/docs/zh/latest/plugins/ai-rate-limiting.md
index 642077681..c6a719c2a 100644
--- a/docs/zh/latest/plugins/ai-rate-limiting.md
+++ b/docs/zh/latest/plugins/ai-rate-limiting.md
@@ -41,8 +41,12 @@ description: ai-rate-limiting 插件对发送到 LLM 服务的请求实施基于
| 名称 | 类型 | 必选项 | 默认值 | 有效值
| 描述 |
|------------------------------|----------------|----------|----------|---------------------------------------------------------|-------------|
-| limit | integer | 否 | | >0
| 在给定时间间隔内允许的最大令牌数。`limit` 和 `instances.limit` 中至少应配置一个。 |
-| time_window | integer | 否 | | >0
| 与速率限制 `limit` 对应的时间间隔(秒)。`time_window` 和
`instances.time_window` 中至少应配置一个。 |
+| limit | integer | 否 | | >0
| 在给定时间间隔内允许的最大令牌数。`limit` 和 `instances.limit`
中至少应配置一个。如果未配置 `rules`,则为必填项。 |
+| time_window | integer | 否 | | >0
| 与速率限制 `limit` 对应的时间间隔(秒)。`time_window` 和
`instances.time_window` 中至少应配置一个。如果未配置 `rules`,则为必填项。 |
+| rules | array[object] | 否 | |
| 速率限制规则列表。每个规则是一个包含
`count`、`time_window` 和 `key` 的对象。如果配置了此项,则优先于 `limit` 和 `time_window`。 |
+| rules.count | integer 或 string | 是 | | >0 或变量表达式
| 在给定时间间隔内允许的最大令牌数。可以是静态整数或变量表达式,如
`$http_custom_limit`。 |
+| rules.time_window | integer 或 string | 是 | | >0 或变量表达式
| 与速率限制 `count` 对应的时间间隔(秒)。可以是静态整数或变量表达式。 |
+| rules.key | string | 是 | |
|
用于计数请求的键。如果配置的键不存在,则不会执行该规则。`key` 被解释为变量组合,例如:`$http_custom_a $http_custom_b`。 |
| show_limit_quota_header | boolean | 否 | true |
| 如果为 true,则在响应中包含
`X-AI-RateLimit-Limit-*`、`X-AI-RateLimit-Remaining-*` 和
`X-AI-RateLimit-Reset-*` 头部,其中 `*` 是实例名称。 |
| limit_strategy | string | 否 | total_tokens |
[total_tokens, prompt_tokens, completion_tokens] | 应用速率限制的令牌类型。`total_tokens` 是
`prompt_tokens` 和 `completion_tokens` 的总和。 |
| instances | array[object] | 否 | |
| LLM 实例速率限制配置。 |
diff --git a/docs/zh/latest/plugins/limit-conn.md
b/docs/zh/latest/plugins/limit-conn.md
index 66836a23c..3e796632f 100644
--- a/docs/zh/latest/plugins/limit-conn.md
+++ b/docs/zh/latest/plugins/limit-conn.md
@@ -38,12 +38,16 @@ description: limit-conn 插件通过管理并发连接来限制请求速率。
| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述
|
|------------|---------|----------|-------|----------------------------|------------------|
-| conn | integer | 是 | | > 0 | 允许的最大并发请求数。超过配置的限制且低于`conn + burst`的请求将被延迟。|
-| burst | integer | 是 | | >= 0 | 每秒允许延迟的过多并发请求数。超过限制的请求将被立即拒绝。|
+| conn | integer | 否 | | > 0 | 允许的最大并发请求数。超过配置的限制且低于`conn +
burst`的请求将被延迟。如果未配置 `rules`,则为必填项。|
+| burst | integer | 否 | | >= 0 | 每秒允许延迟的过多并发请求数。超过限制的请求将被立即拒绝。如果未配置
`rules`,则为必填项。|
| default_conn_delay | number | 是 | | > 0 | 允许超过`conn +
burst`的并发请求的处理延迟(秒),可根据`only_use_default_delay`设置动态调整。|
| only_use_default_delay | boolean | 否 | false | | 如果为
false,则根据请求超出`conn`限制的程度按比例延迟请求。拥塞越严重,延迟就越大。例如,当 `conn` 为 `5`、`burst` 为 `3` 且
`default_conn_delay` 为 `1` 时,6 个并发请求将导致 1 秒的延迟,7 个请求将导致 2 秒的延迟,8 个请求将导致 3
秒的延迟,依此类推,直到达到 `conn + burst` 的总限制,超过此限制的请求将被拒绝。如果为 true,则使用
`default_conn_delay` 延迟 `burst` 范围内的所有超额请求。超出 `conn + burst` 的请求将被立即拒绝。例如,当
`conn` 为 `5`、`burst` 为 `3` 且 `default_conn_delay` 为 `1` 时,6、7 或 8 个并发请求都将延迟 1
秒。|
+| rules | array[object] | 否 | |
| 连接限制规则列表。每个规则是一个包含 `conn`、`burst` 和 `key` 的对象。如果配置了此项,则优先于 `conn`、`burst` 和
`key`。 |
+| rules.conn | integer 或 string | 是 | | > 0 或变量表达式 |
允许的最大并发请求数。可以是静态整数或变量表达式,如 `$http_custom_conn`。 |
+| rules.burst | integer 或 string | 是 | | >= 0 或变量表达式 |
允许延迟的过多并发请求数。可以是静态整数或变量表达式。 |
+| rules.key | string | 是 | | |
用于计数请求的键。如果配置的键不存在,则不会执行该规则。`key` 被解释为变量组合,例如:`$http_custom_a $http_custom_b`。 |
| key_type | string | 否 | var | ["var","var_combination"] | key
的类型。如果`key_type` 为 `var`,则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`,则
`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 | 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`。如果未配置 `rules`,则为必填项。|
| key_ttl | integer | 否 | 3600 | | Redis 键的 TTL(以秒为单位)。当 `policy` 为 `redis` 或
`redis-cluster` 时使用。 |
| rejection_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP
状态代码。|
| rejection_msg | string | 否 | | 非空 | 请求因超出阈值而被拒绝时返回的响应主体。|
@@ -60,7 +64,7 @@ description: limit-conn 插件通过管理并发连接来限制请求速率。
| redis_keepalive_timeout | integer | 否 | 10000 | ≥ 1000 | 当 `policy` 为
`redis` 或 `redis-cluster` 时,与 `redis` 或 `redis-cluster` 的空闲连接超时时间,单位为毫秒。|
| redis_keepalive_pool | integer | 否 | 100 | ≥ 1 | 当 `policy` 为 `redis` 或
`redis-cluster` 时,与 `redis` 或 `redis-cluster` 的连接池最大连接数。|
| redis_cluster_nodes | array[string] | 否 | | | 具有至少两个地址的 Redis 群集节点列表。当
policy 为 redis-cluster 时必填。 |
-redis_cluster_name | string | 否 | | | | Redis 集群的名称。当 `policy` 为
`redis-cluster` 时必须使用。|
+| redis_cluster_name | string | 否 | | | Redis 集群的名称。当 `policy` 为
`redis-cluster` 时必须使用。|
| redis_cluster_ssl | boolean | 否 | false | | 如果为 `true`,当 `policy` 为
`redis-cluster`时,使用 SSL 连接 Redis 集群。|
| redis_cluster_ssl_verify | boolean | 否 | false | | 如果为 `true`,当 `policy` 为
`redis-cluster` 时,验证服务器 SSL 证书。 |
diff --git a/t/plugin/ai-rate-limiting.t b/t/plugin/ai-rate-limiting.t
index 48c71d04f..f0ae0462c 100644
--- a/t/plugin/ai-rate-limiting.t
+++ b/t/plugin/ai-rate-limiting.t
@@ -1174,3 +1174,202 @@ picked instance: openai
picked instance: openai
picked instance: openai
picked instance: nil
+
+
+
+=== TEST 24: configure instances and rules at the same time
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/ai",
+ "plugins": {
+ "ai-rate-limiting": {
+ "limit": "${http_count ?? 10}",
+ "time_window": "${http_time_window ?? 60}",
+ "instances": [
+ {
+ "name": "openai",
+ "limit": "${http_openai_count ?? 20}",
+ "time_window": "${http_time_window ?? 60}"
+ }
+ ],
+ "rules": [
+ {
+ "count": 1,
+ "time_window": 10,
+ "key": "${http_company}"
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "canbeanything.com": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin ai-rate-limiting
err: value should match only one schema, but matches both schemas 1 and 2"}
+
+
+
+=== TEST 25: setup route with rules
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/ai",
+ "plugins": {
+ "ai-proxy-multi": {
+ "instances": [
+ {
+ "name": "deepseek",
+ "provider": "openai",
+ "weight": 1,
+ "auth": {
+ "header": {
+ "Authorization": "Bearer token"
+ }
+ },
+ "override": {
+ "endpoint": "http://127.0.0.1:16724"
+ }
+ }
+ ],
+ "ssl_verify": false
+ },
+ "ai-rate-limiting": {
+ "rejected_code": 429,
+ "rules": [
+ {
+ "count": 20,
+ "time_window": 10,
+ "key": "${http_user}"
+ },
+ {
+ "count": "${http_count ?? 30}",
+ "time_window": "${http_window ?? 10}",
+ "key": "${http_project}"
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "canbeanything.com": 1
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 26: request to confirm rules work
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require("resty.http")
+
+ local run_tests = function(name, test_cases)
+ local httpc = http.new()
+ for i, case in ipairs(test_cases) do
+ case.headers["Content-Type"] = "application/json"
+ local res = httpc:request_uri(
+ "http://127.0.0.1:" .. ngx.var.server_port .. "/ai",
+ {
+ method = "POST",
+ body = [[{
+ "messages": [
+ { "role": "system", "content": "You are a
mathematician" },
+ { "role": "user", "content": "What is
1+1?" }
+ ]
+ }]],
+ headers = case.headers
+ }
+ )
+ if res.status ~= case.code then
+ ngx.say(name .. ": " .. i .. "th request should
return " .. case.code .. ", but got " .. res.status)
+ ngx.exit(500)
+ end
+ -- Add delay to ensure rate limit counters are updated
properly
+ ngx.sleep(0.01)
+ end
+ end
+
+ -- for user rule
+ run_tests("user_rule", {
+ { headers = { ["user"] = "jack" }, code = 200 },
+ { headers = { ["user"] = "jack" }, code = 200 },
+ { headers = { ["user"] = "jack" }, code = 429 },
+ { headers = { ["user"] = "rose" }, code = 200 },
+ { headers = { ["user"] = "rose" }, code = 200 },
+ { headers = { ["user"] = "rose" }, code = 429 },
+ })
+
+ -- for project rule with default variable value
+ run_tests("project_rule_default_value", {
+ { headers = { ["project"] = "apisix" }, code = 200 },
+ { headers = { ["project"] = "apisix" }, code = 200 },
+ { headers = { ["project"] = "apisix" }, code = 200 },
+ { headers = { ["project"] = "apisix" }, code = 429 },
+ })
+
+ -- for project rule with custom variable value
+ run_tests("project_rule_custom_variables", {
+ { headers = { ["project"] = "linux", ["count"] = "20",
["window"] = "2" }, code = 200 },
+ { headers = { ["project"] = "linux", ["count"] = "20",
["window"] = "2" }, code = 200 },
+ { headers = { ["project"] = "linux", ["count"] = "20",
["window"] = "2" }, code = 429 },
+ })
+ ngx.sleep(2.1)
+ run_tests("project_rule_custom_variables2", {
+ { headers = { ["project"] = "linux", ["count"] = "20",
["window"] = "2" }, code = 200 },
+ { headers = { ["project"] = "linux", ["count"] = "20",
["window"] = "2" }, code = 200 },
+ { headers = { ["project"] = "linux", ["count"] = "20",
["window"] = "2" }, code = 429 },
+ })
+
+ -- no rule hit
+ run_tests("no_rules", {
+ { headers = {}, code = 500 },
+ })
+
+ ngx.say("passed")
+ }
+ }
+--- request
+GET /t
+--- timeout: 10
+--- response_body
+passed
+--- error_log
+failed to get rate limit rules
diff --git a/t/plugin/limit-conn-variable.t b/t/plugin/limit-conn-variable.t
index 8c82100e4..e1db32c1e 100644
--- a/t/plugin/limit-conn-variable.t
+++ b/t/plugin/limit-conn-variable.t
@@ -129,8 +129,6 @@ GET /test_concurrency
503
503
503
---- error_log
-limit conn: 5, burst: 2
@@ -151,8 +149,6 @@ conn: 3
503
503
503
---- error_log
-limit conn: 3, burst: 2
@@ -174,5 +170,184 @@ burst: 4
503
503
503
+
+
+
+=== TEST 5: configure conn/burst and rules at same time
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "methods": ["GET"],
+ "plugins": {
+ "limit-conn": {
+ "conn": 2,
+ "burst": 1,
+ "default_conn_delay": 0.01,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "rules": [
+ {
+ "conn": 1,
+ "burst": 0,
+ "key": "${http_company}"
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin limit-conn err:
value should match only one schema, but matches both schemas 1 and 2"}
+
+
+
+=== TEST 6: setup route with rules
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "methods": ["GET"],
+ "plugins": {
+ "limit-conn": {
+ "default_conn_delay": 0.01,
+ "rejected_code": 503,
+ "rules": [
+ {
+ "conn": 4,
+ "burst": 3,
+ "key": "${http_user}"
+ },
+ {
+ "conn": "${http_project_conn ?? 3}",
+ "burst": "${http_project_burst ?? 2}",
+ "key": "${http_project}"
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/limit_conn"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 7: request matching user rule
+--- request
+GET /test_concurrency
+--- more_headers
+user: jack
+--- timeout: 10s
+--- response_body
+200
+200
+200
+200
+200
+200
+200
+503
+503
+503
+
+
+
+=== TEST 8: request matching project rule with default conn/burst
+--- request
+GET /test_concurrency
+--- more_headers
+project: apisix
+--- timeout: 10s
+--- response_body
+200
+200
+200
+200
+200
+503
+503
+503
+503
+503
+
+
+
+=== TEST 9: request matching project rule with custom conn/burst
+--- request
+GET /test_concurrency
+--- more_headers
+project: apisix
+project-conn: 2
+project-burst: 1
+--- timeout: 10s
+--- response_body
+200
+200
+200
+503
+503
+503
+503
+503
+503
+503
+
+
+
+=== TEST 10: request not matching any rule
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+500
+500
+500
+500
+500
+500
+500
+500
+500
+500
--- error_log
-limit conn: 3, burst: 4
+failed to get limit conn rules
diff --git a/t/plugin/limit-conn.t b/t/plugin/limit-conn.t
index 364282f49..93c69730f 100644
--- a/t/plugin/limit-conn.t
+++ b/t/plugin/limit-conn.t
@@ -106,7 +106,7 @@ done
--- request
GET /t
--- response_body
-property "burst" is required
+value should match only one schema, but matches none
done
@@ -321,7 +321,7 @@ GET /test_concurrency
GET /t
--- error_code: 400
--- response_body
-{"error_msg":"failed to check the configuration of plugin limit-conn err:
property \"conn\" is required"}
+{"error_msg":"failed to check the configuration of plugin limit-conn err:
value should match only one schema, but matches none"}
@@ -401,7 +401,7 @@ GET /t
GET /t
--- error_code: 400
--- response_body
-{"error_msg":"failed to check the configuration of plugin limit-conn err:
property \"conn\" is required"}
+{"error_msg":"failed to check the configuration of plugin limit-conn err:
value should match only one schema, but matches none"}
@@ -1101,7 +1101,7 @@ qr/limit key: consumer_jackroute&consumer\d+/
--- request
GET /t
--- response_body
-property "burst" is required
+value should match only one schema, but matches none
done
diff --git a/t/plugin/limit-req.t b/t/plugin/limit-req.t
index 0f46374d0..6cfc7d306 100644
--- a/t/plugin/limit-req.t
+++ b/t/plugin/limit-req.t
@@ -70,10 +70,9 @@ done
}
--- request
GET /t
---- response_body_like eval
-qr/property "(conn|default_conn_delay)" is required
+--- response_body
+value should match only one schema, but matches none
done
-/