Skip to content

Commit

Permalink
feat: add redis and redis-cluster in limit-req (#10874)
Browse files Browse the repository at this point in the history
  • Loading branch information
theweakgod authored Feb 22, 2024
1 parent 72f6364 commit 1439b13
Show file tree
Hide file tree
Showing 11 changed files with 1,524 additions and 6 deletions.
1 change: 1 addition & 0 deletions apisix/cli/ngx_tpl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ http {
{% end %}
{% if enabled_plugins["limit-req"] then %}
lua_shared_dict plugin-limit-req-redis-cluster-slot-lock {* http.lua_shared_dict["plugin-limit-req-redis-cluster-slot-lock"] *};
lua_shared_dict plugin-limit-req {* http.lua_shared_dict["plugin-limit-req"] *};
{% end %}
Expand Down
61 changes: 55 additions & 6 deletions apisix/plugins/limit-req.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,29 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local limit_req_new = require("resty.limit.req").new
local core = require("apisix.core")
local plugin_name = "limit-req"
local limit_req_new = require("resty.limit.req").new
local core = require("apisix.core")
local redis_schema = require("apisix.utils.redis-schema")
local policy_to_additional_properties = redis_schema.schema
local plugin_name = "limit-req"
local sleep = core.sleep

local redis_single_new
local redis_cluster_new
do
local redis_src = "apisix.plugins.limit-req.limit-req-redis"
redis_single_new = require(redis_src).new

local cluster_src = "apisix.plugins.limit-req.limit-req-redis-cluster"
redis_cluster_new = require(cluster_src).new
end


local lrucache = core.lrucache.new({
type = "plugin",
})


local schema = {
type = "object",
properties = {
Expand All @@ -34,6 +47,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
},
Expand All @@ -45,7 +63,25 @@ local schema = {
},
allow_degradation = {type = "boolean", default = false}
},
required = {"rate", "burst", "key"}
required = {"rate", "burst", "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"],
}
}


Expand All @@ -68,8 +104,21 @@ end


local function create_limit_obj(conf)
core.log.info("create new limit-req plugin instance")
return limit_req_new("plugin-limit-req", conf.rate, conf.burst)
if conf.policy == "local" then
core.log.info("create new limit-req plugin instance")
return limit_req_new("plugin-limit-req", conf.rate, conf.burst)

elseif conf.policy == "redis" then
core.log.info("create new limit-req redis plugin instance")
return redis_single_new("plugin-limit-req", conf, conf.rate, conf.burst)

elseif conf.policy == "redis-cluster" then
core.log.info("create new limit-req redis-cluster plugin instance")
return redis_cluster_new("plugin-limit-req", conf, conf.rate, conf.burst)

else
return nil, "policy enum not match"
end
end


Expand Down
50 changes: 50 additions & 0 deletions apisix/plugins/limit-req/limit-req-redis-cluster.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--
-- 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 setmetatable = setmetatable
local util = require("apisix.plugins.limit-req.util")

local _M = {version = 0.1}


local mt = {
__index = _M
}


function _M.new(plugin_name, conf, rate, burst)
local red_cli, err = redis_cluster.new(conf, "plugin-limit-req-redis-cluster-slot-lock")
if not red_cli then
return nil, err
end
local self = {
conf = conf,
plugin_name = plugin_name,
burst = burst * 1000,
rate = rate * 1000,
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


return _M
54 changes: 54 additions & 0 deletions apisix/plugins/limit-req/limit-req-redis.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
--
-- 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 setmetatable = setmetatable
local util = require("apisix.plugins.limit-req.util")

local setmetatable = setmetatable


local _M = {version = 0.1}


local mt = {
__index = _M
}


function _M.new(plugin_name, conf, rate, burst)
local self = {
conf = conf,
plugin_name = plugin_name,
burst = burst * 1000,
rate = rate * 1000,
}
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


return _M
78 changes: 78 additions & 0 deletions apisix/plugins/limit-req/util.lua
Original file line number Diff line number Diff line change
@@ -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 math = require "math"
local abs = math.abs
local max = math.max
local ngx_now = ngx.now
local ngx_null = ngx.null
local tonumber = tonumber


local _M = {version = 0.1}


-- the "commit" argument controls whether should we record the event in shm.
function _M.incoming(self, red, key, commit)
local rate = self.rate
local now = ngx_now() * 1000

key = "limit_req" .. ":" .. key
local excess_key = key .. "excess"
local last_key = key .. "last"

local excess, err = red:get(excess_key)
if err then
return nil, err
end
local last, err = red:get(last_key)
if err then
return nil, err
end

if excess ~= ngx_null and last ~= ngx_null then
excess = tonumber(excess)
last = tonumber(last)
local elapsed = now - last
excess = max(excess - rate * abs(elapsed) / 1000 + 1000, 0)

if excess > self.burst then
return nil, "rejected"
end
else
excess = 0
end

if commit then
local ok
local err
ok, err = red:set(excess_key, excess)
if not ok then
return nil, err
end

ok, err = red:set(last_key, now)
if not ok then
return nil, err
end
end

-- return the delay in seconds, as well as excess
return excess / rate, excess / 1000
end


return _M
1 change: 1 addition & 0 deletions conf/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ nginx_config: # Config for render the template to generate n
balancer-ewma: 10m
balancer-ewma-locks: 10m
balancer-ewma-last-touched-at: 10m
plugin-limit-req-redis-cluster-slot-lock: 1m
plugin-limit-count-redis-cluster-slot-lock: 1m
plugin-limit-conn-redis-cluster-slot-lock: 1m
tracing_buffer: 10m
Expand Down
13 changes: 13 additions & 0 deletions docs/en/latest/plugins/limit-req.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ The `limit-req` Plugin limits the number of requests to your service using the [
| rejected_msg | string | False | | non-empty | Body of the response returned when the requests exceeding the threshold are rejected. |
| nodelay | boolean | False | false | | If set to `true`, requests within the burst threshold would not be delayed. |
| 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 instead of a single instance. |
| 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

Expand Down
Loading

0 comments on commit 1439b13

Please sign in to comment.