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 7e907a565 feat: add redis and redis-cluster in limit-conn (#10866)
7e907a565 is described below
commit 7e907a565b9302e70daf294398722377fd64c482
Author: theweakgod <[email protected]>
AuthorDate: Wed Feb 7 17:48:03 2024 +0800
feat: add redis and redis-cluster in limit-conn (#10866)
---
apisix/cli/ngx_tpl.lua | 1 +
apisix/plugins/limit-conn.lua | 36 +-
apisix/plugins/limit-conn/init.lua | 33 +-
.../limit-conn/limit-conn-redis-cluster.lua | 78 ++
apisix/plugins/limit-conn/limit-conn-redis.lua | 85 +++
apisix/plugins/limit-conn/util.lua | 81 +++
apisix/plugins/limit-count/init.lua | 60 +-
.../limit-count/limit-count-redis-cluster.lua | 38 +-
apisix/plugins/limit-count/limit-count-redis.lua | 48 +-
apisix/utils/redis-schema.lua | 81 +++
.../limit-count-redis.lua => utils/redis.lua} | 73 +-
.../rediscluster.lua} | 73 +-
conf/config-default.yaml | 1 +
docs/en/latest/plugins/limit-conn.md | 35 +-
docs/zh/latest/plugins/limit-conn.md | 35 +-
t/APISIX.pm | 1 +
t/plugin/limit-conn-redis-cluster.t | 339 +++++++++
t/plugin/limit-conn-redis.t | 810 +++++++++++++++++++++
18 files changed, 1607 insertions(+), 301 deletions(-)
diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua
index 532debe0a..f1b10499c 100644
--- a/apisix/cli/ngx_tpl.lua
+++ b/apisix/cli/ngx_tpl.lua
@@ -289,6 +289,7 @@ http {
{% if enabled_plugins["limit-conn"] then %}
lua_shared_dict plugin-limit-conn {*
http.lua_shared_dict["plugin-limit-conn"] *};
+ lua_shared_dict plugin-limit-conn-redis-cluster-slot-lock {*
http.lua_shared_dict["plugin-limit-conn-redis-cluster-slot-lock"] *};
{% end %}
{% if enabled_plugins["limit-req"] then %}
diff --git a/apisix/plugins/limit-conn.lua b/apisix/plugins/limit-conn.lua
index d8389b701..31a29199b 100644
--- a/apisix/plugins/limit-conn.lua
+++ b/apisix/plugins/limit-conn.lua
@@ -14,15 +14,18 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-local core = require("apisix.core")
-local limit_conn = require("apisix.plugins.limit-conn.init")
+local core = require("apisix.core")
+local limit_conn =
require("apisix.plugins.limit-conn.init")
+local redis_schema = require("apisix.utils.redis-schema")
+local policy_to_additional_properties = redis_schema.schema
+local plugin_name = "limit-conn"
+
-local plugin_name = "limit-conn"
local schema = {
type = "object",
properties = {
- conn = {type = "integer", exclusiveMinimum = 0},
+ conn = {type = "integer", exclusiveMinimum = 0}, --
limit.conn max
burst = {type = "integer", minimum = 0},
default_conn_delay = {type = "number", exclusiveMinimum = 0},
only_use_default_delay = {type = "boolean", default = false},
@@ -31,6 +34,11 @@ local schema = {
enum = {"var", "var_combination"},
default = "var",
},
+ policy = {
+ type = "string",
+ enum = {"redis", "redis-cluster", "local"},
+ default = "local",
+ },
rejected_code = {
type = "integer", minimum = 200, maximum = 599, default = 503
},
@@ -39,7 +47,25 @@ local schema = {
},
allow_degradation = {type = "boolean", default = false}
},
- required = {"conn", "burst", "default_conn_delay", "key"}
+ required = {"conn", "burst", "default_conn_delay", "key"},
+ ["if"] = {
+ properties = {
+ policy = {
+ enum = {"redis"},
+ },
+ },
+ },
+ ["then"] = policy_to_additional_properties.redis,
+ ["else"] = {
+ ["if"] = {
+ properties = {
+ policy = {
+ enum = {"redis-cluster"},
+ },
+ },
+ },
+ ["then"] = policy_to_additional_properties["redis-cluster"],
+ }
}
local _M = {
diff --git a/apisix/plugins/limit-conn/init.lua
b/apisix/plugins/limit-conn/init.lua
index c6ce55f24..d7401df9c 100644
--- a/apisix/plugins/limit-conn/init.lua
+++ b/apisix/plugins/limit-conn/init.lua
@@ -23,6 +23,16 @@ if ngx.config.subsystem == "stream" then
shdict_name = shdict_name .. "-stream"
end
+local redis_single_new
+local redis_cluster_new
+do
+ local redis_src = "apisix.plugins.limit-conn.limit-conn-redis"
+ redis_single_new = require(redis_src).new
+
+ local cluster_src = "apisix.plugins.limit-conn.limit-conn-redis-cluster"
+ redis_cluster_new = require(cluster_src).new
+end
+
local lrucache = core.lrucache.new({
type = "plugin",
@@ -31,9 +41,26 @@ local _M = {}
local function create_limit_obj(conf)
- core.log.info("create new limit-conn plugin instance")
- return limit_conn_new(shdict_name, conf.conn, conf.burst,
- conf.default_conn_delay)
+ if conf.policy == "local" then
+ core.log.info("create new limit-conn plugin instance")
+ return limit_conn_new(shdict_name, conf.conn, conf.burst,
+ conf.default_conn_delay)
+ elseif conf.policy == "redis" then
+
+ core.log.info("create new limit-conn redis plugin instance")
+
+ return redis_single_new("plugin-limit-conn", conf, conf.conn,
conf.burst,
+ conf.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, conf.conn,
conf.burst,
+ conf.default_conn_delay)
+ else
+ return nil, "policy enum not match"
+ end
end
diff --git a/apisix/plugins/limit-conn/limit-conn-redis-cluster.lua
b/apisix/plugins/limit-conn/limit-conn-redis-cluster.lua
new file mode 100644
index 000000000..9e46a04b2
--- /dev/null
+++ b/apisix/plugins/limit-conn/limit-conn-redis-cluster.lua
@@ -0,0 +1,78 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local redis_cluster = require("apisix.utils.rediscluster")
+local core = require("apisix.core")
+local util = require("apisix.plugins.limit-conn.util")
+local setmetatable = setmetatable
+local ngx_timer_at = ngx.timer.at
+
+local _M = {version = 0.1}
+
+
+local mt = {
+ __index = _M
+}
+
+
+function _M.new(plugin_name, conf, max, burst, default_conn_delay)
+
+ local red_cli, err = redis_cluster.new(conf,
"plugin-limit-conn-redis-cluster-slot-lock")
+ if not red_cli then
+ return nil, err
+ end
+ local self = {
+ conf = conf,
+ plugin_name = plugin_name,
+ burst = burst,
+ max = max + 0, -- just to ensure the param is good
+ unit_delay = default_conn_delay,
+ red_cli = red_cli,
+ }
+ return setmetatable(self, mt)
+end
+
+
+function _M.incoming(self, key, commit)
+ return util.incoming(self, self.red_cli, key, commit)
+end
+
+
+function _M.is_committed(self)
+ return self.committed
+end
+
+
+local function leaving_thread(premature, self, key, req_latency)
+ return util.leaving(self, self.red_cli, key, req_latency)
+end
+
+
+function _M.leaving(self, key, req_latency)
+ -- log_by_lua can't use cosocket
+ local ok, err = ngx_timer_at(0, leaving_thread, self, key, req_latency)
+ if not ok then
+ core.log.error("failed to create timer: ", err)
+ return nil, err
+ end
+
+ return ok
+
+end
+
+
+
+return _M
diff --git a/apisix/plugins/limit-conn/limit-conn-redis.lua
b/apisix/plugins/limit-conn/limit-conn-redis.lua
new file mode 100644
index 000000000..4de7a27fd
--- /dev/null
+++ b/apisix/plugins/limit-conn/limit-conn-redis.lua
@@ -0,0 +1,85 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local redis = require("apisix.utils.redis")
+local core = require("apisix.core")
+local util = require("apisix.plugins.limit-conn.util")
+local ngx_timer_at = ngx.timer.at
+
+local setmetatable = setmetatable
+
+
+local _M = {version = 0.1}
+
+
+local mt = {
+ __index = _M
+}
+
+function _M.new(plugin_name, conf, max, burst, default_conn_delay)
+
+ local self = {
+ conf = conf,
+ plugin_name = plugin_name,
+ burst = burst,
+ max = max + 0, -- just to ensure the param is good
+ unit_delay = default_conn_delay,
+ }
+ return setmetatable(self, mt)
+end
+
+
+function _M.incoming(self, key, commit)
+ local conf = self.conf
+ local red, err = redis.new(conf)
+ if not red then
+ return red, err
+ end
+ return util.incoming(self, red, key, commit)
+end
+
+
+function _M.is_committed(self)
+ return self.committed
+end
+
+
+local function leaving_thread(premature, self, key, req_latency)
+
+ local conf = self.conf
+ local red, err = redis.new(conf)
+ if not red then
+ return red, err
+ end
+ return util.leaving(self, red, key, req_latency)
+end
+
+
+function _M.leaving(self, key, req_latency)
+ -- log_by_lua can't use cosocket
+ local ok, err = ngx_timer_at(0, leaving_thread, self, key, req_latency)
+ if not ok then
+ core.log.error("failed to create timer: ", err)
+ return nil, err
+ end
+
+ return ok
+
+end
+
+
+
+return _M
diff --git a/apisix/plugins/limit-conn/util.lua
b/apisix/plugins/limit-conn/util.lua
new file mode 100644
index 000000000..f3ba5bd76
--- /dev/null
+++ b/apisix/plugins/limit-conn/util.lua
@@ -0,0 +1,81 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+local assert = assert
+local math = require "math"
+local floor = math.floor
+local _M = {version = 0.3}
+
+
+function _M.incoming(self, red, key, commit)
+ local max = self.max
+ self.committed = false
+ key = "limit_conn" .. ":" .. key
+
+ local conn, err
+ if commit then
+ conn, err = red:incrby(key, 1)
+ if not conn then
+ return nil, err
+ end
+
+ if conn > max + self.burst then
+ conn, err = red:incrby(key, -1)
+ if not conn then
+ return nil, err
+ end
+ return nil, "rejected"
+ end
+ self.committed = true
+
+ else
+ local conn_from_red, err = red:get(key)
+ if err then
+ return nil, err
+ end
+ conn = (conn_from_red or 0) + 1
+ end
+
+ if conn > max then
+ -- make the excessive connections wait
+ return self.unit_delay * floor((conn - 1) / max), conn
+ end
+
+ -- we return a 0 delay by default
+ return 0, conn
+end
+
+
+function _M.leaving(self, red, key, req_latency)
+ assert(key)
+ key = "limit_conn" .. ":" .. key
+
+ local conn, err = red:incrby(key, -1)
+ if not conn then
+ return nil, err
+ end
+
+ if req_latency then
+ local unit_delay = self.unit_delay
+ self.unit_delay = (req_latency + unit_delay) / 2
+ end
+
+ return conn
+end
+
+
+return _M
diff --git a/apisix/plugins/limit-count/init.lua
b/apisix/plugins/limit-count/init.lua
index 049e238e9..e7d03028e 100644
--- a/apisix/plugins/limit-count/init.lua
+++ b/apisix/plugins/limit-count/init.lua
@@ -19,6 +19,8 @@ local apisix_plugin = require("apisix.plugin")
local tab_insert = table.insert
local ipairs = ipairs
local pairs = pairs
+local redis_schema = require("apisix.utils.redis-schema")
+local policy_to_additional_properties = redis_schema.schema
local limit_redis_cluster_new
local limit_redis_new
@@ -40,64 +42,6 @@ local group_conf_lru = core.lrucache.new({
type = 'plugin',
})
-local policy_to_additional_properties = {
- redis = {
- properties = {
- redis_host = {
- type = "string", minLength = 2
- },
- redis_port = {
- type = "integer", minimum = 1, default = 6379,
- },
- redis_username = {
- type = "string", minLength = 1,
- },
- redis_password = {
- type = "string", minLength = 0,
- },
- redis_database = {
- type = "integer", minimum = 0, default = 0,
- },
- redis_timeout = {
- type = "integer", minimum = 1, default = 1000,
- },
- redis_ssl = {
- type = "boolean", default = false,
- },
- redis_ssl_verify = {
- type = "boolean", default = false,
- },
- },
- required = {"redis_host"},
- },
- ["redis-cluster"] = {
- properties = {
- redis_cluster_nodes = {
- type = "array",
- minItems = 1,
- items = {
- type = "string", minLength = 2, maxLength = 100
- },
- },
- redis_password = {
- type = "string", minLength = 0,
- },
- redis_timeout = {
- type = "integer", minimum = 1, default = 1000,
- },
- redis_cluster_name = {
- type = "string",
- },
- redis_cluster_ssl = {
- type = "boolean", default = false,
- },
- redis_cluster_ssl_verify = {
- type = "boolean", default = false,
- },
- },
- required = {"redis_cluster_nodes", "redis_cluster_name"},
- },
-}
local schema = {
type = "object",
properties = {
diff --git a/apisix/plugins/limit-count/limit-count-redis-cluster.lua
b/apisix/plugins/limit-count/limit-count-redis-cluster.lua
index 7800f1c88..be7029b66 100644
--- a/apisix/plugins/limit-count/limit-count-redis-cluster.lua
+++ b/apisix/plugins/limit-count/limit-count-redis-cluster.lua
@@ -15,11 +15,10 @@
-- limitations under the License.
--
-local rediscluster = require("resty.rediscluster")
+local redis_cluster = require("apisix.utils.rediscluster")
local core = require("apisix.core")
local setmetatable = setmetatable
local tostring = tostring
-local ipairs = ipairs
local _M = {}
@@ -40,41 +39,8 @@ local script = core.string.compress_script([=[
]=])
-local function new_redis_cluster(conf)
- local config = {
- -- can set different name for different redis cluster
- name = conf.redis_cluster_name,
- serv_list = {},
- read_timeout = conf.redis_timeout,
- auth = conf.redis_password,
- dict_name = "plugin-limit-count-redis-cluster-slot-lock",
- connect_opts = {
- ssl = conf.redis_cluster_ssl,
- ssl_verify = conf.redis_cluster_ssl_verify,
- }
- }
-
- for i, conf_item in ipairs(conf.redis_cluster_nodes) do
- local host, port, err = core.utils.parse_addr(conf_item)
- if err then
- return nil, "failed to parse address: " .. conf_item
- .. " err: " .. err
- end
-
- config.serv_list[i] = {ip = host, port = port}
- end
-
- local red_cli, err = rediscluster:new(config)
- if not red_cli then
- return nil, "failed to new redis cluster: " .. err
- end
-
- return red_cli
-end
-
-
function _M.new(plugin_name, limit, window, conf)
- local red_cli, err = new_redis_cluster(conf)
+ local red_cli, err = redis_cluster.new(conf,
"plugin-limit-count-redis-cluster-slot-lock")
if not red_cli then
return nil, err
end
diff --git a/apisix/plugins/limit-count/limit-count-redis.lua
b/apisix/plugins/limit-count/limit-count-redis.lua
index 85f769cbd..c40ed437f 100644
--- a/apisix/plugins/limit-count/limit-count-redis.lua
+++ b/apisix/plugins/limit-count/limit-count-redis.lua
@@ -14,7 +14,7 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-local redis_new = require("resty.redis").new
+local redis = require("apisix.utils.redis")
local core = require("apisix.core")
local assert = assert
local setmetatable = setmetatable
@@ -39,50 +39,6 @@ local script = core.string.compress_script([=[
return {redis.call('incrby', KEYS[1], 0 - ARGV[3]), ttl}
]=])
-local function redis_cli(conf)
- local red = redis_new()
- local timeout = conf.redis_timeout or 1000 -- 1sec
-
- red:set_timeouts(timeout, timeout, timeout)
-
- local sock_opts = {
- ssl = conf.redis_ssl,
- ssl_verify = conf.redis_ssl_verify
- }
-
- local ok, err = red:connect(conf.redis_host, conf.redis_port or 6379,
sock_opts)
- if not ok then
- return false, err
- end
-
- local count
- count, err = red:get_reused_times()
- if 0 == count then
- if conf.redis_password and conf.redis_password ~= '' then
- local ok, err
- if conf.redis_username then
- ok, err = red:auth(conf.redis_username, conf.redis_password)
- else
- ok, err = red:auth(conf.redis_password)
- end
- if not ok then
- return nil, err
- end
- end
-
- -- select db
- if conf.redis_database ~= 0 then
- local ok, err = red:select(conf.redis_database)
- if not ok then
- return false, "failed to change redis db, err: " .. err
- end
- end
- elseif err then
- -- core.log.info(" err: ", err)
- return nil, err
- end
- return red, nil
-end
function _M.new(plugin_name, limit, window, conf)
assert(limit > 0 and window > 0)
@@ -98,7 +54,7 @@ end
function _M.incoming(self, key, cost)
local conf = self.conf
- local red, err = redis_cli(conf)
+ local red, err = redis.new(conf)
if not red then
return red, err, 0
end
diff --git a/apisix/utils/redis-schema.lua b/apisix/utils/redis-schema.lua
new file mode 100644
index 000000000..c9fdec41d
--- /dev/null
+++ b/apisix/utils/redis-schema.lua
@@ -0,0 +1,81 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+local policy_to_additional_properties = {
+ redis = {
+ properties = {
+ redis_host = {
+ type = "string", minLength = 2
+ },
+ redis_port = {
+ type = "integer", minimum = 1, default = 6379,
+ },
+ redis_username = {
+ type = "string", minLength = 1,
+ },
+ redis_password = {
+ type = "string", minLength = 0,
+ },
+ redis_database = {
+ type = "integer", minimum = 0, default = 0,
+ },
+ redis_timeout = {
+ type = "integer", minimum = 1, default = 1000,
+ },
+ redis_ssl = {
+ type = "boolean", default = false,
+ },
+ redis_ssl_verify = {
+ type = "boolean", default = false,
+ },
+ },
+ required = {"redis_host"},
+ },
+ ["redis-cluster"] = {
+ properties = {
+ redis_cluster_nodes = {
+ type = "array",
+ minItems = 1,
+ items = {
+ type = "string", minLength = 2, maxLength = 100
+ },
+ },
+ redis_password = {
+ type = "string", minLength = 0,
+ },
+ redis_timeout = {
+ type = "integer", minimum = 1, default = 1000,
+ },
+ redis_cluster_name = {
+ type = "string",
+ },
+ redis_cluster_ssl = {
+ type = "boolean", default = false,
+ },
+ redis_cluster_ssl_verify = {
+ type = "boolean", default = false,
+ },
+ },
+ required = {"redis_cluster_nodes", "redis_cluster_name"},
+ },
+}
+
+local _M = {
+ schema = policy_to_additional_properties
+}
+
+return _M
diff --git a/apisix/plugins/limit-count/limit-count-redis.lua
b/apisix/utils/redis.lua
similarity index 56%
copy from apisix/plugins/limit-count/limit-count-redis.lua
copy to apisix/utils/redis.lua
index 85f769cbd..423ad6d24 100644
--- a/apisix/plugins/limit-count/limit-count-redis.lua
+++ b/apisix/utils/redis.lua
@@ -14,34 +14,15 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-local redis_new = require("resty.redis").new
-local core = require("apisix.core")
-local assert = assert
-local setmetatable = setmetatable
-local tostring = tostring
+local redis_new = require("resty.redis").new
+local core = require("apisix.core")
-local _M = {version = 0.3}
-
-
-local mt = {
- __index = _M
-}
-
-
-local script = core.string.compress_script([=[
- assert(tonumber(ARGV[3]) >= 1, "cost must be at least 1")
- local ttl = redis.call('ttl', KEYS[1])
- if ttl < 0 then
- redis.call('set', KEYS[1], ARGV[1] - ARGV[3], 'EX', ARGV[2])
- return {ARGV[1] - ARGV[3], ARGV[2]}
- end
- return {redis.call('incrby', KEYS[1], 0 - ARGV[3]), ttl}
-]=])
+local _M = {version = 0.1}
local function redis_cli(conf)
local red = redis_new()
- local timeout = conf.redis_timeout or 1000 -- 1sec
+ local timeout = conf.redis_timeout or 1000 -- default 1sec
red:set_timeouts(timeout, timeout, timeout)
@@ -52,6 +33,7 @@ local function redis_cli(conf)
local ok, err = red:connect(conf.redis_host, conf.redis_port or 6379,
sock_opts)
if not ok then
+ core.log.error(" redis connect error, error: ", err)
return false, err
end
@@ -84,50 +66,9 @@ local function redis_cli(conf)
return red, nil
end
-function _M.new(plugin_name, limit, window, conf)
- assert(limit > 0 and window > 0)
-
- local self = {
- limit = limit,
- window = window,
- conf = conf,
- plugin_name = plugin_name,
- }
- return setmetatable(self, mt)
-end
-
-function _M.incoming(self, key, cost)
- local conf = self.conf
- local red, err = redis_cli(conf)
- if not red then
- return red, err, 0
- end
-
- local limit = self.limit
- local window = self.window
- local res
- key = self.plugin_name .. tostring(key)
-
- local ttl = 0
- res, err = red:eval(script, 1, key, limit, window, cost or 1)
-
- if err then
- return nil, err, ttl
- end
-
- local remaining = res[1]
- ttl = res[2]
- local ok, err = red:set_keepalive(10000, 100)
- if not ok then
- return nil, err, ttl
- end
-
- if remaining < 0 then
- return nil, "rejected", ttl
- end
- return 0, remaining, ttl
+function _M.new(conf)
+ return redis_cli(conf)
end
-
return _M
diff --git a/apisix/plugins/limit-count/limit-count-redis-cluster.lua
b/apisix/utils/rediscluster.lua
similarity index 50%
copy from apisix/plugins/limit-count/limit-count-redis-cluster.lua
copy to apisix/utils/rediscluster.lua
index 7800f1c88..e3bda4aa3 100644
--- a/apisix/plugins/limit-count/limit-count-redis-cluster.lua
+++ b/apisix/utils/rediscluster.lua
@@ -14,40 +14,19 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
+local rediscluster = require("resty.rediscluster")
+local core = require("apisix.core")
+local ipairs = ipairs
-local rediscluster = require("resty.rediscluster")
-local core = require("apisix.core")
-local setmetatable = setmetatable
-local tostring = tostring
-local ipairs = ipairs
+local _M = {version = 0.1}
-local _M = {}
-
-
-local mt = {
- __index = _M
-}
-
-
-local script = core.string.compress_script([=[
- assert(tonumber(ARGV[3]) >= 1, "cost must be at least 1")
- local ttl = redis.call('ttl', KEYS[1])
- if ttl < 0 then
- redis.call('set', KEYS[1], ARGV[1] - ARGV[3], 'EX', ARGV[2])
- return {ARGV[1] - ARGV[3], ARGV[2]}
- end
- return {redis.call('incrby', KEYS[1], 0 - ARGV[3]), ttl}
-]=])
-
-
-local function new_redis_cluster(conf)
+local function new_redis_cluster(conf, dict_name)
local config = {
- -- can set different name for different redis cluster
name = conf.redis_cluster_name,
serv_list = {},
read_timeout = conf.redis_timeout,
auth = conf.redis_password,
- dict_name = "plugin-limit-count-redis-cluster-slot-lock",
+ dict_name = dict_name,
connect_opts = {
ssl = conf.redis_cluster_ssl,
ssl_verify = conf.redis_cluster_ssl_verify,
@@ -73,44 +52,8 @@ local function new_redis_cluster(conf)
end
-function _M.new(plugin_name, limit, window, conf)
- local red_cli, err = new_redis_cluster(conf)
- if not red_cli then
- return nil, err
- end
-
- local self = {
- limit = limit,
- window = window,
- conf = conf,
- plugin_name = plugin_name,
- red_cli = red_cli,
- }
-
- return setmetatable(self, mt)
-end
-
-
-function _M.incoming(self, key, cost)
- local red = self.red_cli
- local limit = self.limit
- local window = self.window
- key = self.plugin_name .. tostring(key)
-
- local ttl = 0
- local res, err = red:eval(script, 1, key, limit, window, cost or 1)
-
- if err then
- return nil, err, ttl
- end
-
- local remaining = res[1]
- ttl = res[2]
-
- if remaining < 0 then
- return nil, "rejected", ttl
- end
- return 0, remaining, ttl
+function _M.new(conf, dict_name)
+ return new_redis_cluster(conf, dict_name)
end
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index fcee30871..3ba8a4e46 100755
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -260,6 +260,7 @@ nginx_config: # Config for render the
template to generate n
balancer-ewma-locks: 10m
balancer-ewma-last-touched-at: 10m
plugin-limit-count-redis-cluster-slot-lock: 1m
+ plugin-limit-conn-redis-cluster-slot-lock: 1m
tracing_buffer: 10m
plugin-api-breaker: 10m
etcd-cluster-health-check: 10m
diff --git a/docs/en/latest/plugins/limit-conn.md
b/docs/en/latest/plugins/limit-conn.md
index af0e59bc0..f95995530 100644
--- a/docs/en/latest/plugins/limit-conn.md
+++ b/docs/en/latest/plugins/limit-conn.md
@@ -32,17 +32,30 @@ The `limit-conn` Plugin limits the number of concurrent
requests to your service
## Attributes
-| Name | Type | Required | Default | Valid values
| Description
|
-| ---------------------- | ------- | -------- | ------- |
-------------------------- |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
-| conn | integer | True | | conn > 0
| Maximum number of concurrent requests allowed. Requests exceeding this
ratio (and below `conn` + `burst`) will be delayed (configured by
`default_conn_delay`).
|
-| burst | integer | True | | burst >= 0
| Number of additional concurrent requests allowed to be delayed per
second. If the number exceeds this hard limit, they will get rejected
immediately.
|
-| default_conn_delay | number | True | | default_conn_delay >
0 | Delay in seconds to process the concurrent requests exceeding `conn`
(and `conn` + `burst`).
|
-| only_use_default_delay | boolean | False | false | [true,false]
| When set to `true`, the Plugin will always set a delay of
`default_conn_delay` and would not use any other calculations.
|
-| key_type | string | False | "var" | ["var",
"var_combination"] | Type of user specified key to use.
|
-| key | string | True | |
| User specified key to base the request limiting on. If the `key_type`
attribute is set to `"var"`, the key will be treated as a name of variable,
like `remote_addr` or `consumer_name`. If the `key_type` is set to
`"var_combination"`, the key will be a combination of variables, like
`$remote_addr $consumer_name`. If the value of the key is empty, `remote_addr`
will be set as the default key. |
-| rejected_code | string | False | 503 | [200,...,599]
| HTTP status code returned when the requests exceeding the threshold are
rejected.
|
-| rejected_msg | string | False | | non-empty
| Body of the response returned when the requests exceeding the threshold
are rejected.
|
-| allow_degradation | boolean | False | false |
| When set to `true` enables Plugin degradation when the Plugin is
temporarily unavailable and allows requests to continue.
|
+| Name | Type | Required | Default | Valid values
| Description
|
+|--------------------------|---------| --------
|-------------|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| conn | integer | True | | conn > 0
| Maximum number of concurrent requests allowed. Requests
exceeding this ratio (and below `conn` + `burst`) will be delayed (configured
by `default_conn_delay`).
|
+| burst | integer | True | | burst >= 0
| Number of additional concurrent requests allowed to be
delayed per second. If the number exceeds this hard limit, they will get
rejected immediately.
|
+| default_conn_delay | number | True | |
default_conn_delay > 0 | Delay in seconds to process the concurrent
requests exceeding `conn` (and `conn` + `burst`).
|
+| only_use_default_delay | boolean | False | false | [true,false]
| When set to `true`, the Plugin will always set a delay of
`default_conn_delay` and would not use any other calculations.
|
+| key_type | string | False | "var" | ["var",
"var_combination"] | Type of user specified key to use.
|
+| key | string | True | |
| User specified key to base the request limiting on. If the
`key_type` attribute is set to `"var"`, the key will be treated as a name of
variable, like `remote_addr` or `consumer_name`. If the `key_type` is set to
`"var_combination"`, the key will be a combination of variables, like
`$remote_addr $consumer_name`. If the value of the key is empty, `remote_addr`
will be set as the default key. |
+| rejected_code | string | False | 503 | [200,...,599]
| HTTP status code returned when the requests exceeding the
threshold are rejected.
|
+| rejected_msg | string | False | | non-empty
| Body of the response returned when the requests exceeding
the threshold are rejected.
|
+| allow_degradation | boolean | False | false |
| When set to `true` enables Plugin degradation when the
Plugin is temporarily unavailable and allows requests to continue.
|
+| policy | string | False
| "local" | ["local", "redis", "redis-cluster"] | Rate-limiting
policies to use for retrieving and increment the limit count. When set to
`local` the counters will be locally stored in memory on the node. When set to
`redis` counters are stored on a Redis server and will be shared across the
nodes. It is done usually for global speed limiting, and setting to
`redis-cluster` uses a Redis cluster inste [...]
+| redis_host | string | required when `policy` is `redis`
| | | Address of the Redis
server. Used when the `policy` attribute is set to `redis`.
[...]
+| redis_port | integer | False
| 6379 | [1,...] | Port of the Redis server.
Used when the `policy` attribute is set to `redis`.
[...]
+| redis_username | string | False
| | | Username for Redis
authentication if Redis ACL is used (for Redis version >= 6.0). If you use the
legacy authentication method `requirepass` to configure Redis password,
configure only the `redis_password`. Used when the `policy` is set to `redis`.
[...]
+| redis_password | string | False
| | | Password for Redis
authentication. Used when the `policy` is set to `redis` or `redis-cluster`.
[...]
+| redis_ssl | boolean | False
| false | | If set to `true`, then
uses SSL to connect to redis instance. Used when the `policy` attribute is set
to `redis`.
[...]
+| redis_ssl_verify | boolean | False
| false | | If set to `true`, then
verifies the validity of the server SSL certificate. Used when the `policy`
attribute is set to `redis`. See
[tcpsock:sslhandshake](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake).
[...]
+| redis_database | integer | False
| 0 | redis_database >= 0 | Selected database of the
Redis server (for single instance operation or when using Redis cloud with a
single entrypoint). Used when the `policy` attribute is set to `redis`.
[...]
+| redis_timeout | integer | False
| 1000 | [1,...] | Timeout in milliseconds
for any command submitted to the Redis server. Used when the `policy` attribute
is set to `redis` or `redis-cluster`.
[...]
+| redis_cluster_nodes | array | required when `policy` is
`redis-cluster` | | | Addresses
of Redis cluster nodes. Used when the `policy` attribute is set to
`redis-cluster`.
[...]
+| redis_cluster_name | string | required when `policy` is
`redis-cluster` | | | Name of the
Redis cluster service nodes. Used when the `policy` attribute is set to
`redis-cluster`.
[...]
+| redis_cluster_ssl | boolean | False | false |
| If set to `true`, then uses SSL to connect to redis-cluster.
Used when the `policy` attribute is set to `redis-cluster`.
|
+| redis_cluster_ssl_verify | boolean | False | false |
| If set to `true`, then verifies the validity of the server
SSL certificate. Used when the `policy` attribute is set to `redis-cluster`.
|
## Enable Plugin
diff --git a/docs/zh/latest/plugins/limit-conn.md
b/docs/zh/latest/plugins/limit-conn.md
index 794835e42..f91df1f5a 100644
--- a/docs/zh/latest/plugins/limit-conn.md
+++ b/docs/zh/latest/plugins/limit-conn.md
@@ -32,17 +32,30 @@ description: 本文介绍了 Apache APISIX limit-conn 插件的相关操作,
## 属性
-| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述
|
-| ------------------ | ------- | ----- | ------ | --------------------------
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| conn | integer | 是 | | conn > 0 |
允许的最大并发请求数。超过 `conn` 的限制、但是低于 `conn` + `burst` 的请求,将被延迟处理。
|
-| burst | integer | 是 | | burst >= 0 |
每秒允许被延迟处理的额外并发请求数。
|
-| default_conn_delay | number | 是 | | default_conn_delay > 0 |
默认的典型连接(或请求)的处理延迟时间。
|
-| only_use_default_delay | boolean | 否 | false | [true,false] |
延迟时间的严格模式。当设置为 `true` 时,将会严格按照设置的 `default_conn_delay` 时间来进行延迟处理。
|
-| key_type | string | 否 | "var" | ["var", "var_combination"] |
`key` 的类型。
|
-| key | string | 是 | | |
用来做请求计数的依据。如果 `key_type` 为 `"var"`,那么 `key` 会被当作变量名称,如 `remote_addr` 和
`consumer_name`;如果 `key_type` 为 `"var_combination"`,那么 `key` 会当作变量组合,如
`$remote_addr $consumer_name`;如果 `key` 的值为空,`$remote_addr` 会被作为默认 `key`。 |
-| rejected_code | string | 否 | 503 | [200,...,599] |
当请求数超过 `conn` + `burst` 阈值时,返回的 HTTP 状态码。
|
-| rejected_msg | string | 否 | | 非空 |
当请求数超过 `conn` + `burst` 阈值时,返回的信息。
|
-| allow_degradation | boolean | 否 | false | |
当设置为 `true` 时,启用插件降级并自动允许请求继续。
|
+| 名称 | 类型 | 必选项 | 默认值 | 有效值
| 描述
|
+| ------------------ | ------- |----------------------------------| ------ |
--------------------------
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| conn | integer | 是 | |
conn > 0 | 允许的最大并发请求数。超过 `conn` 的限制、但是低于 `conn` + `burst`
的请求,将被延迟处理。
|
+| burst | integer | 是 | |
burst >= 0 | 每秒允许被延迟处理的额外并发请求数。
|
+| default_conn_delay | number | 是 | |
default_conn_delay > 0 | 默认的典型连接(或请求)的处理延迟时间。
|
+| only_use_default_delay | boolean | 否 | false
| [true,false] | 延迟时间的严格模式。当设置为 `true` 时,将会严格按照设置的
`default_conn_delay` 时间来进行延迟处理。
|
+| key_type | string | 否 | "var" |
["var", "var_combination"] | `key` 的类型。
|
+| key | string | 是 | |
| 用来做请求计数的依据。如果 `key_type` 为 `"var"`,那么 `key`
会被当作变量名称,如 `remote_addr` 和 `consumer_name`;如果 `key_type` 为
`"var_combination"`,那么 `key` 会当作变量组合,如 `$remote_addr $consumer_name`;如果 `key`
的值为空,`$remote_addr` 会被作为默认 `key`。 |
+| rejected_code | string | 否 | 503 |
[200,...,599] | 当请求数超过 `conn` + `burst` 阈值时,返回的 HTTP 状态码。
|
+| rejected_msg | string | 否 | | 非空
| 当请求数超过 `conn` + `burst` 阈值时,返回的信息。
|
+| allow_degradation | boolean | 否 | false |
| 当设置为 `true` 时,启用插件降级并自动允许请求继续。
|
+| policy | string | 否 | "local" | ["local",
"redis", "redis-cluster"] | 用于检索和增加限制计数的策略。当设置为 `local`
时,计数器被以内存方式保存在节点本地;当设置为 `redis` 时,计数器保存在 Redis
服务节点上,从而可以跨节点共享结果,通常用它来完成全局限速;当设置为 `redis-cluster` 时,使用 Redis 集群而不是单个实例。|
+| redis_host | string | 否 | |
| 当使用 `redis` 限速策略时,Redis 服务节点的地址。**当 `policy` 属性设置为
`redis` 时必选。**
|
+| redis_port | integer | 否 | 6379 | [1,...]
| 当使用 `redis` 限速策略时,Redis 服务节点的端口。
|
+| redis_username | string | 否 | |
| 若使用 Redis ACL 进行身份验证(适用于 Redis 版本 >=6.0),则需要提供 Redis
用户名。若使用 Redis legacy 方式 `requirepass` 进行身份验证,则只需将密码配置在 `redis_password`。当
`policy` 设置为 `redis` 时使用。
|
+| redis_password | string | 否 | |
| 当使用 `redis` 或者 `redis-cluster` 限速策略时,Redis 服务节点的密码。
|
+| redis_ssl | boolean | 否 | false |
| 当使用 `redis` 限速策略时,如果设置为 true,则使用 SSL 连接到 `redis`
|
+| redis_ssl_verify | boolean | 否 | false |
| 当使用 `redis` 限速策略时,如果设置为 true,则验证服务器 SSL 证书的有效性,具体请参考
[tcpsock:sslhandshake](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake).
|
+| redis_database | integer | 否 | 0 | redis_database >=
0 | 当使用 `redis` 限速策略时,Redis 服务节点中使用的 `database`,并且只针对非
Redis 集群模式(单实例模式或者提供单入口的 Redis 公有云服务)生效。
|
+| redis_timeout | integer | 否 | 1000 | [1,...]
| 当 `policy` 设置为 `redis` 或 `redis-cluster` 时,Redis
服务节点的超时时间(以毫秒为单位)。
|
+| redis_cluster_nodes | array | 否 | |
| 当使用 `redis-cluster` 限速策略时,Redis
集群服务节点的地址列表(至少需要两个地址)。**当 `policy` 属性设置为 `redis-cluster` 时必选。**
|
+| redis_cluster_name | string | 否 | |
| 当使用 `redis-cluster` 限速策略时,Redis 集群服务节点的名称。**当 `policy`
设置为 `redis-cluster` 时必选。**
|
+| redis_cluster_ssl | boolean | 否 | false |
| 当使用 `redis-cluster` 限速策略时,如果设置为 true,则使用 SSL 连接到
`redis-cluster`
|
+| redis_cluster_ssl_verify | boolean | 否 | false |
| 当使用 `redis-cluster` 限速策略时,如果设置为 true,则验证服务器
SSL 证书的有效性
|
## 启用插件
diff --git a/t/APISIX.pm b/t/APISIX.pm
index 6428de1c5..73de93f34 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -560,6 +560,7 @@ _EOC_
lua_shared_dict balancer-ewma-locks 1m;
lua_shared_dict balancer-ewma-last-touched-at 1m;
lua_shared_dict plugin-limit-count-redis-cluster-slot-lock 1m;
+ lua_shared_dict plugin-limit-conn-redis-cluster-slot-lock 1m;
lua_shared_dict tracing_buffer 10m; # plugin skywalking
lua_shared_dict access-tokens 1m; # plugin authz-keycloak
lua_shared_dict discovery 1m; # plugin authz-keycloak
diff --git a/t/plugin/limit-conn-redis-cluster.t
b/t/plugin/limit-conn-redis-cluster.t
new file mode 100644
index 000000000..997a4a2a7
--- /dev/null
+++ b/t/plugin/limit-conn-redis-cluster.t
@@ -0,0 +1,339 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+BEGIN {
+ if ($ENV{TEST_NGINX_CHECK_LEAK}) {
+ $SkipReason = "unavailable for the hup tests";
+
+ } else {
+ $ENV{TEST_NGINX_USE_HUP} = 1;
+ undef $ENV{TEST_NGINX_USE_STAP};
+ }
+}
+
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+ my $port = $ENV{TEST_NGINX_SERVER_PORT};
+
+ my $config = $block->config // <<_EOC_;
+ location /access_root_dir {
+ content_by_lua_block {
+ local httpc = require "resty.http"
+ local hc = httpc:new()
+
+ local res, err =
hc:request_uri('http://127.0.0.1:$port/limit_conn')
+ if res then
+ ngx.exit(res.status)
+ end
+ }
+ }
+
+ location /test_concurrency {
+ content_by_lua_block {
+ local reqs = {}
+ local status_map = {}
+ for i = 1, 10 do
+ reqs[i] = { "/access_root_dir" }
+ end
+ local resps = { ngx.location.capture_multi(reqs) }
+ for i, resp in ipairs(resps) do
+ local status_key = resp.status
+ if status_map[status_key] then
+ status_map[status_key] = status_map[status_key] + 1
+ else
+ status_map[status_key] = 1
+ end
+ end
+ for key, value in pairs(status_map) do
+ ngx.say("status:" .. key .. ", " .. "count:" .. value)
+ end
+ }
+ }
+_EOC_
+
+ $block->set_value("config", $config);
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: sanity
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.limit-conn")
+ local ok, err = plugin.check_schema({
+ conn = 1,
+ burst = 0,
+ default_conn_delay = 0.1,
+ rejected_code = 503,
+ key = 'remote_addr',
+ policy = "redis-cluster",
+ redis_cluster_nodes = {
+ "127.0.0.1:5000",
+ "127.0.0.1:5003",
+ "127.0.0.1:5002"
+ },
+ dict_name = "test",
+ redis_cluster_name = "test"
+ })
+ if not ok then
+ ngx.say(err)
+ end
+
+ ngx.say("done")
+ }
+ }
+--- request
+GET /t
+--- response_body
+done
+
+
+
+=== TEST 2: add plugin
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 100,
+ "burst": 50,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "policy": "redis-cluster",
+ "redis_cluster_nodes": [
+ "127.0.0.1:5000",
+ "127.0.0.1:5003",
+ "127.0.0.1:5002"
+ ],
+ "redis_cluster_name": "test"
+ }
+ },
+ "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 3: not exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:10
+
+
+
+=== TEST 4: update plugin
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 2,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "policy": "redis-cluster",
+ "redis_cluster_nodes": [
+ "127.0.0.1:5000",
+ "127.0.0.1:5002"
+ ],
+ "redis_cluster_name": "redis-cluster-1"
+ }
+ },
+ "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 5: exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:3
+status:503, count:7
+
+
+
+=== TEST 6: update plugin
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 5,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "policy": "redis-cluster",
+ "redis_cluster_nodes": [
+ "127.0.0.1:5000",
+ "127.0.0.1:5002"
+ ],
+ "redis_cluster_name": "redis-cluster-1"
+ }
+ },
+ "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: exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:6
+status:503, count:4
+
+
+
+=== TEST 8: set route, with redis_cluster_nodes and redis_cluster_name
redis_cluster_ssl and redis_cluster_ssl_verify
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 5,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "policy": "redis-cluster",
+ "redis_cluster_nodes": [
+ "127.0.0.1:7001",
+ "127.0.0.1:7002",
+ "127.0.0.1:7000"
+ ],
+ "redis_cluster_name": "redis-cluster-2",
+ "redis_cluster_ssl": true,
+ "redis_cluster_ssl_verify": false
+ }
+ },
+ "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 9: exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:6
+status:503, count:4
diff --git a/t/plugin/limit-conn-redis.t b/t/plugin/limit-conn-redis.t
new file mode 100644
index 000000000..a12145370
--- /dev/null
+++ b/t/plugin/limit-conn-redis.t
@@ -0,0 +1,810 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+BEGIN {
+ if ($ENV{TEST_NGINX_CHECK_LEAK}) {
+ $SkipReason = "unavailable for the hup tests";
+
+ } else {
+ $ENV{TEST_NGINX_USE_HUP} = 1;
+ undef $ENV{TEST_NGINX_USE_STAP};
+ }
+}
+
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+ my $port = $ENV{TEST_NGINX_SERVER_PORT};
+
+ my $config = $block->config // <<_EOC_;
+ location /access_root_dir {
+ content_by_lua_block {
+ local httpc = require "resty.http"
+ local hc = httpc:new()
+
+ local res, err =
hc:request_uri('http://127.0.0.1:$port/limit_conn')
+ if res then
+ ngx.exit(res.status)
+ end
+ }
+ }
+
+ location /test_concurrency {
+ content_by_lua_block {
+ local reqs = {}
+ local status_map = {}
+ for i = 1, 10 do
+ reqs[i] = { "/access_root_dir" }
+ end
+ local resps = { ngx.location.capture_multi(reqs) }
+ for i, resp in ipairs(resps) do
+ local status_key = resp.status
+ if status_map[status_key] then
+ status_map[status_key] = status_map[status_key] + 1
+ else
+ status_map[status_key] = 1
+ end
+ end
+ for key, value in pairs(status_map) do
+ ngx.say("status:" .. key .. ", " .. "count:" .. value)
+ end
+ }
+ }
+_EOC_
+
+ $block->set_value("config", $config);
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: sanity
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.limit-conn")
+ local ok, err = plugin.check_schema({
+ conn = 1,
+ burst = 0,
+ default_conn_delay = 0.1,
+ rejected_code = 503,
+ key = 'remote_addr',
+ policy = "redis",
+ redis_host = 'localhost',
+ })
+ if not ok then
+ ngx.say(err)
+ end
+
+ ngx.say("done")
+ }
+ }
+--- request
+GET /t
+--- response_body
+done
+
+
+
+=== TEST 2: add plugin
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 100,
+ "burst": 50,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "policy": "redis",
+ "redis_host": "127.0.0.1",
+ "redis_port": 6379
+ }
+ },
+ "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 3: not exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:10
+
+
+
+=== TEST 4: update plugin
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 2,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "policy": "redis",
+ "redis_host": "127.0.0.1",
+ "redis_port": 6379
+ }
+ },
+ "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 5: exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:3
+status:503, count:7
+
+
+
+=== TEST 6: update plugin
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 5,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "policy": "redis",
+ "redis_host": "127.0.0.1",
+ "redis_port": 6379
+ }
+ },
+ "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: exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:6
+status:503, count:4
+
+
+
+=== TEST 8: update plugin with username, password
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 5,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "policy": "redis",
+ "redis_host": "127.0.0.1",
+ "redis_port": 6379,
+ "redis_username": "alice",
+ "redis_password": "somepassword"
+ }
+ },
+ "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 9: exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:6
+status:503, count:4
+
+
+
+=== TEST 10: update plugin with username, wrong password
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 5,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "policy": "redis",
+ "redis_host": "127.0.0.1",
+ "redis_port": 6379,
+ "redis_username": "alice",
+ "redis_password": "someerrorpassword"
+ }
+ },
+ "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 11: catch wrong pass
+--- request
+GET /access_root_dir
+--- error_code: 500
+--- error_log
+failed to limit conn: WRONGPASS invalid username-password pair or user is
disabled.
+
+
+
+=== TEST 12: invalid route: missing redis_host
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr",
+ "conn": 1,
+ "policy": "redis"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/limit_conn"
+ }]]
+ )
+
+ 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: then
clause did not match"}
+
+
+
+=== TEST 13: disable plugin
+--- 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,
+ [[{
+ "plugins": {
+ },
+ "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 14: exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:10
+
+
+
+=== TEST 15: set route(key: server_addr)
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 100,
+ "burst": 50,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "server_addr",
+ "policy": "redis",
+ "redis_host": "127.0.0.1"
+ }
+ },
+ "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 16: key: http_x_real_ip
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 5,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "http_x_real_ip",
+ "policy": "redis",
+ "redis_host": "127.0.0.1"
+ }
+ },
+ "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 17: exceeding the burst (X-Real-IP)
+--- config
+location /access_root_dir {
+ content_by_lua_block {
+ local port = ngx.var.server_port
+ local httpc = require "resty.http"
+ local hc = httpc:new()
+
+ local res, err = hc:request_uri('http://127.0.0.1:' .. port ..
'/limit_conn', {
+ keepalive = false,
+ headers = {["X-Real-IP"] = "10.10.10.1"}
+ })
+ if res then
+ ngx.exit(res.status)
+ end
+ }
+}
+
+location /test_concurrency {
+ content_by_lua_block {
+ local reqs = {}
+ local status_map = {}
+ for i = 1, 10 do
+ reqs[i] = { "/access_root_dir" }
+ end
+ local resps = { ngx.location.capture_multi(reqs) }
+ for i, resp in ipairs(resps) do
+ local status_key = resp.status
+ if status_map[status_key] then
+ status_map[status_key] = status_map[status_key] + 1
+ else
+ status_map[status_key] = 1
+ end
+ end
+ for key, value in pairs(status_map) do
+ ngx.say("status:" .. key .. ", " .. "count:" .. value)
+ end
+ }
+}
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:6
+status:503, count:4
+--- error_log
+limit key: 10.10.10.1route
+
+
+
+=== TEST 18: key: http_x_forwarded_for
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 5,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "http_x_forwarded_for",
+ "policy": "redis",
+ "redis_host": "127.0.0.1"
+ }
+ },
+ "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 19: exceeding the burst(X-Forwarded-For)
+--- config
+location /access_root_dir {
+ content_by_lua_block {
+ local port = ngx.var.server_port
+ local httpc = require "resty.http"
+ local hc = httpc:new()
+
+ local res, err = hc:request_uri('http://127.0.0.1:' .. port ..
'/limit_conn', {
+ keepalive = false,
+ headers = {["X-Forwarded-For"] = "10.10.10.2"}
+ })
+ if res then
+ ngx.exit(res.status)
+ end
+ }
+}
+
+location /test_concurrency {
+ content_by_lua_block {
+ local reqs = {}
+ local status_map = {}
+ for i = 1, 10 do
+ reqs[i] = { "/access_root_dir" }
+ end
+ local resps = { ngx.location.capture_multi(reqs) }
+ for i, resp in ipairs(resps) do
+ local status_key = resp.status
+ if status_map[status_key] then
+ status_map[status_key] = status_map[status_key] + 1
+ else
+ status_map[status_key] = 1
+ end
+ end
+ for key, value in pairs(status_map) do
+ ngx.say("status:" .. key .. ", " .. "count:" .. value)
+ end
+ }
+}
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:6
+status:503, count:4
+--- error_log
+limit key: 10.10.10.2route
+
+
+
+=== TEST 20: default rejected_code
+--- 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,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 4,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "key": "remote_addr",
+ "policy": "redis",
+ "redis_host": "127.0.0.1"
+ }
+ },
+ "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 21: exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:5
+status:503, count:5
+
+
+
+=== TEST 22: set global rule with conn = 2
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 2,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "key": "remote_addr",
+ "policy": "redis",
+ "redis_host": "127.0.0.1"
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 23: exceeding the burst of global rule
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:3
+status:503, count:7
+
+
+
+=== TEST 24: delete global rule
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
+ ngx.HTTP_DELETE
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 25: not exceeding the burst
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+status:200, count:5
+status:503, count:5