Skip to content

Commit b7ec0c8

Browse files
fix(limit-conn): implement configurable redis key expiry (#12872)
1 parent 854dede commit b7ec0c8

File tree

8 files changed

+272
-25
lines changed

8 files changed

+272
-25
lines changed

apisix/plugins/limit-conn.lua

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
local core = require("apisix.core")
1818
local limit_conn = require("apisix.plugins.limit-conn.init")
1919
local redis_schema = require("apisix.utils.redis-schema")
20-
local policy_to_additional_properties = redis_schema.schema
2120
local plugin_name = "limit-conn"
2221
local workflow = require("apisix.plugins.workflow")
2322

@@ -55,7 +54,7 @@ local schema = {
5554
},
5655
},
5756
},
58-
["then"] = policy_to_additional_properties.redis,
57+
["then"] = redis_schema.limit_conn_redis_schema,
5958
["else"] = {
6059
["if"] = {
6160
properties = {
@@ -64,7 +63,7 @@ local schema = {
6463
},
6564
},
6665
},
67-
["then"] = policy_to_additional_properties["redis-cluster"],
66+
["then"] = redis_schema.limit_conn_redis_cluster_schema,
6867
}
6968
}
7069

apisix/plugins/limit-conn/limit-conn-redis-cluster.lua

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ local core = require("apisix.core")
1919
local util = require("apisix.plugins.limit-conn.util")
2020
local setmetatable = setmetatable
2121
local ngx_timer_at = ngx.timer.at
22+
local ngx = ngx
2223

2324
local _M = {version = 0.1}
2425

@@ -41,6 +42,7 @@ function _M.new(plugin_name, conf, max, burst, default_conn_delay)
4142
max = max + 0, -- just to ensure the param is good
4243
unit_delay = default_conn_delay,
4344
red_cli = red_cli,
45+
use_evalsha = false,
4446
}
4547
return setmetatable(self, mt)
4648
end
@@ -56,14 +58,19 @@ function _M.is_committed(self)
5658
end
5759

5860

59-
local function leaving_thread(premature, self, key, req_latency)
60-
return util.leaving(self, self.red_cli, key, req_latency)
61+
local function leaving_thread(premature, self, key, req_latency, req_id)
62+
return util.leaving(self, self.red_cli, key, req_latency, req_id)
6163
end
6264

6365

6466
function _M.leaving(self, key, req_latency)
67+
local req_id
68+
if ngx.ctx.limit_conn_req_ids then
69+
req_id = ngx.ctx.limit_conn_req_ids[key]
70+
end
71+
6572
-- log_by_lua can't use cosocket
66-
local ok, err = ngx_timer_at(0, leaving_thread, self, key, req_latency)
73+
local ok, err = ngx_timer_at(0, leaving_thread, self, key, req_latency, req_id)
6774
if not ok then
6875
core.log.error("failed to create timer: ", err)
6976
return nil, err

apisix/plugins/limit-conn/limit-conn-redis.lua

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ local redis = require("apisix.utils.redis")
1818
local core = require("apisix.core")
1919
local util = require("apisix.plugins.limit-conn.util")
2020
local ngx_timer_at = ngx.timer.at
21+
local ngx = ngx
2122

2223
local setmetatable = setmetatable
2324

@@ -37,6 +38,7 @@ function _M.new(plugin_name, conf, max, burst, default_conn_delay)
3738
burst = burst,
3839
max = max + 0, -- just to ensure the param is good
3940
unit_delay = default_conn_delay,
41+
use_evalsha = true,
4042
}
4143
return setmetatable(self, mt)
4244
end
@@ -57,20 +59,25 @@ function _M.is_committed(self)
5759
end
5860

5961

60-
local function leaving_thread(premature, self, key, req_latency)
62+
local function leaving_thread(premature, self, key, req_latency, req_id)
6163

6264
local conf = self.conf
6365
local red, err = redis.new(conf)
6466
if not red then
6567
return red, err
6668
end
67-
return util.leaving(self, red, key, req_latency)
69+
return util.leaving(self, red, key, req_latency, req_id)
6870
end
6971

7072

7173
function _M.leaving(self, key, req_latency)
74+
local req_id
75+
if ngx.ctx.limit_conn_req_ids then
76+
req_id = ngx.ctx.limit_conn_req_ids[key]
77+
end
78+
7279
-- log_by_lua can't use cosocket
73-
local ok, err = ngx_timer_at(0, leaving_thread, self, key, req_latency)
80+
local ok, err = ngx_timer_at(0, leaving_thread, self, key, req_latency, req_id)
7481
if not ok then
7582
core.log.error("failed to create timer: ", err)
7683
return nil, err

apisix/plugins/limit-conn/util.lua

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,100 @@
1818
local assert = assert
1919
local math = require "math"
2020
local floor = math.floor
21+
local ngx = ngx
22+
local ngx_time = ngx.time
23+
local uuid = require("resty.jit-uuid")
24+
local core = require("apisix.core")
25+
2126
local _M = {version = 0.3}
27+
local redis_incoming_script = core.string.compress_script([=[
28+
local key = KEYS[1]
29+
local limit = tonumber(ARGV[1])
30+
local ttl = tonumber(ARGV[2])
31+
local now = tonumber(ARGV[3])
32+
local req_id = ARGV[4]
33+
34+
redis.call('ZREMRANGEBYSCORE', key, 0, now)
35+
36+
local count = redis.call('ZCARD', key)
37+
if count >= limit then
38+
return {0, count}
39+
end
40+
41+
redis.call('ZADD', key, now + ttl, req_id)
42+
redis.call('EXPIRE', key, ttl)
43+
return {1, count + 1}
44+
]=])
45+
local redis_incoming_script_sha
46+
47+
48+
local function generate_redis_sha1(red)
49+
local sha1, err = red:script("LOAD", redis_incoming_script)
50+
if not sha1 then
51+
return nil, err
52+
end
53+
return sha1
54+
end
2255

2356

2457
function _M.incoming(self, red, key, commit)
2558
local max = self.max
2659
self.committed = false
60+
local raw_key = key
2761
key = "limit_conn" .. ":" .. key
2862

29-
local conn, err
63+
local conn
3064
if commit then
31-
conn, err = red:incrby(key, 1)
32-
if not conn then
33-
return nil, err
65+
local req_id = ngx.ctx.request_id or uuid.generate_v4()
66+
if not ngx.ctx.limit_conn_req_ids then
67+
ngx.ctx.limit_conn_req_ids = {}
3468
end
69+
ngx.ctx.limit_conn_req_ids[raw_key] = req_id
70+
71+
local now = ngx_time()
72+
local res, err
73+
74+
if self.use_evalsha then
75+
if not redis_incoming_script_sha then
76+
redis_incoming_script_sha, err = generate_redis_sha1(red)
77+
if not redis_incoming_script_sha then
78+
core.log.error("failed to generate redis sha1: ", err)
79+
return nil, err
80+
end
81+
end
3582

36-
if conn > max + self.burst then
37-
conn, err = red:incrby(key, -1)
38-
if not conn then
39-
return nil, err
83+
res, err = red:evalsha(redis_incoming_script_sha, 1, key,
84+
max + self.burst, self.conf.key_ttl, now, req_id)
85+
86+
if err and core.string.has_prefix(err, "NOSCRIPT") then
87+
core.log.warn("redis evalsha failed: ", err, ". Falling back to eval...")
88+
redis_incoming_script_sha = nil
89+
res, err = red:eval(redis_incoming_script, 1, key,
90+
max + self.burst, self.conf.key_ttl, now, req_id)
4091
end
92+
else
93+
res, err = red:eval(redis_incoming_script, 1, key,
94+
max + self.burst, self.conf.key_ttl, now, req_id)
95+
end
96+
97+
if not res then
98+
return nil, err
99+
end
100+
101+
local allowed = res[1]
102+
conn = res[2]
103+
104+
if allowed == 0 then
41105
return nil, "rejected"
42106
end
107+
43108
self.committed = true
44109

45110
else
46-
local conn_from_red, err = red:get(key)
47-
if err then
48-
return nil, err
49-
end
50-
conn = (conn_from_red or 0) + 1
111+
red:zremrangebyscore(key, 0, ngx_time())
112+
local count, err = red:zcard(key)
113+
if err then return nil, err end
114+
conn = (count or 0) + 1
51115
end
52116

53117
if conn > max then
@@ -60,11 +124,19 @@ function _M.incoming(self, red, key, commit)
60124
end
61125

62126

63-
function _M.leaving(self, red, key, req_latency)
127+
function _M.leaving(self, red, key, req_latency, req_id)
64128
assert(key)
65129
key = "limit_conn" .. ":" .. key
66130

67-
local conn, err = red:incrby(key, -1)
131+
local conn, err
132+
if req_id then
133+
local res, err = red:zrem(key, req_id)
134+
if not res then
135+
return nil, err
136+
end
137+
end
138+
conn, err = red:zcard(key)
139+
68140
if not conn then
69141
return nil, err
70142
end

apisix/utils/redis-schema.lua

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,20 @@ local policy_to_additional_properties = {
7474
},
7575
}
7676

77+
local limit_conn_redis_cluster_schema = policy_to_additional_properties["redis-cluster"]
78+
limit_conn_redis_cluster_schema.properties.key_ttl = {
79+
type = "integer", default = 3600,
80+
}
81+
82+
local limit_conn_redis_schema = policy_to_additional_properties["redis"]
83+
limit_conn_redis_schema.properties.key_ttl = {
84+
type = "integer", default = 3600,
85+
}
86+
7787
local _M = {
78-
schema = policy_to_additional_properties
88+
schema = policy_to_additional_properties,
89+
limit_conn_redis_cluster_schema = limit_conn_redis_cluster_schema,
90+
limit_conn_redis_schema = limit_conn_redis_schema,
7991
}
8092

8193
return _M

docs/en/latest/plugins/limit-conn.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The `limit-conn` Plugin limits the rate of requests by the number of concurrent
4444
| 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 requests are rejected. If true, use `default_conn_delay` to delay all excessive requests within the `burst` range. Requests beyond `conn + burst` are rejected immediately. For instance, with `conn` being `5`, `burst` being `3`, and `default_conn_delay` being `1`, 6, 7, or 8 concurrent requests are all delayed by exactly 1 second each. |
4545
| 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. |
4646
| 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`. |
47+
| key_ttl | integer | False | 3600 | | The TTL of the Redis key in seconds. Used when `policy` is `redis` or `redis-cluster`. |
4748
| rejected_code | integer | False | 503 | [200,...,599] | The HTTP status code returned when a request is rejected for exceeding the threshold. |
4849
| rejected_msg | string | False | | non-empty | The response body returned when a request is rejected for exceeding the threshold. |
4950
| allow_degradation | boolean | False | false | | If true, allow APISIX to continue handling requests without the Plugin when the Plugin or its dependencies become unavailable. |

docs/zh/latest/plugins/limit-conn.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ description: limit-conn 插件通过管理并发连接来限制请求速率。
4444
| 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 秒。|
4545
| key_type | string || var | ["var","var_combination"] | key 的类型。如果`key_type``var`,则 `key` 将被解释为变量。如果 `key_type``var_combination`,则 `key` 将被解释为变量的组合。 |
4646
| 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`|
47+
| key_ttl | integer || 3600 | | Redis 键的 TTL(以秒为单位)。当 `policy``redis``redis-cluster` 时使用。 |
4748
| rejection_code | integer || 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 状态代码。|
4849
| rejection_msg | string || | 非空 | 请求因超出阈值而被拒绝时返回的响应主体。|
4950
| allow_degradation | boolean || false | | 如果为 true,则允许 APISIX 在插件或其依赖项不可用时继续处理没有插件的请求。|

0 commit comments

Comments
 (0)