diff --git a/.travis.yml b/.travis.yml index e00610b..89061d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,6 +36,7 @@ install: - git clone https://github.com/openresty/no-pool-nginx.git ../no-pool-nginx - git clone https://github.com/openresty/lua-resty-lrucache.git ../lua-resty-lrucache - git clone https://github.com/openresty/lua-resty-core.git ../lua-resty-core + - git clone https://github.com/openresty/lua-resty-lock.git ../lua-resty-lock - git clone -b v2.1-agentzh https://github.com/openresty/luajit2.git script: diff --git a/lib/resty/limit/token.lua b/lib/resty/limit/token.lua new file mode 100644 index 0000000..cae6c46 --- /dev/null +++ b/lib/resty/limit/token.lua @@ -0,0 +1,243 @@ +-- limit request rate using the token bucket method: +-- https://en.wikipedia.org/wiki/Token_bucket + + +local ffi = require "ffi" +local math = require "math" +local lock = require "resty.lock" + + +local ffi_cast = ffi.cast +local ffi_str = ffi.string + +local type = type +local assert = assert +local ngx_now = ngx.now +local floor = math.floor +local ngx_shared = ngx.shared +local setmetatable = setmetatable + + +ffi.cdef[[ + struct lua_resty_limit_token_rec { + int64_t avail; + uint64_t last; /* time in milliseconds */ + }; +]] +local const_rec_ptr_type = ffi.typeof("const struct lua_resty_limit_token_rec*") +local rec_size = ffi.sizeof("struct lua_resty_limit_token_rec") + +local rec_cdata = ffi.new("struct lua_resty_limit_token_rec") + + +local _M = { + _VERSION = "0.01", +} + + +local mt = { + __index = _M +} + + +local is_str = function(s) return type(s) == "string" end +local is_num = function(n) return type(n) == "number" end + + +local function acquire_lock(self, key) + local lock, err = lock:new(self.locks_shdict_name) + if not lock then + return nil, err + end + + self.lock = lock + + return lock:lock(key) +end + + +local function release_lock(self) + local lock = self.lock + + return lock:unlock() +end + + +local function update(self, key, avail, last) + local dict = self.dict + + rec_cdata.avail = avail + rec_cdata.last = last + dict:set(key, ffi_str(rec_cdata, rec_size)) + + -- ngx.log(ngx.ERR, "key = ", key, " avail = ", avail, " last = ", last) +end + + +local function adjust(self, key, now) + local dict = self.dict + + local res = { + last = now, + avail = self.capacity + } + + local v = dict:get(key) + if v then + if not is_str(v) or #v ~= rec_size then + return nil, "shdict abused by other users" + end + + local rec = ffi_cast(const_rec_ptr_type, v) + + res.last = tonumber(rec.last) + res.avail = tonumber(rec.avail) + end + + local tick = floor((now - res.last) / self.interval) + res.last = res.last + tick * self.interval + + if res.avail >= self.capacity then + return res + end + + res.avail = res.avail + tick * self.quantum + if res.avail > self.capacity then + res.avail = self.capacity + end + + return res +end + + +function _M.new(dict_name, interval, capacity, quantum, opts) + local dict = ngx_shared[dict_name] + if not dict then + return nil, "shared dict not found" + end + + if not quantum then + quantum = 1 + end + + assert(interval > 0 and capacity > 0 and quantum > 0) + + if not opts then + opts = {} + end + + local locks_shdict_name = opts.locks_shdict_name or "locks" + + local self = { + dict = dict, + interval = interval, + capacity = capacity, + quantum = quantum, + + locks_shdict_name = locks_shdict_name, + } + + return setmetatable(self, mt) +end + + +function _M.rate(self) + return 1000 * self.quantum / self.interval +end + + +function _M.take(self, key, count, max_wait, fake_now) + if not is_str(key) or count <= 0 then + return 0 + end + + local now = ngx_now() * 1000 + + -- just for testing + if is_num(fake_now) then + now = fake_now + end + + local res, err = acquire_lock(self, key) + if not res then + return nil, err + end + + local res, err = adjust(self, key, now) + if not res then + release_lock(self) + return nil, err + end + + local last = res.last + local avail = res.avail + + avail = avail - count + if avail >= 0 then + update(self, key, avail, last) + release_lock(self) + return 0 + end + + local quantum = self.quantum + local tick = floor((-avail + quantum - 1) / quantum) + local wait_time = tick * self.interval - (now - last) + + if is_num(max_wait) and wait_time > max_wait then + update(self, key, avail + count, last) + release_lock(self) + return nil, "rejected" + end + + update(self, key, avail, last) + release_lock(self) + + return wait_time / 1000 +end + + +function _M.take_available(self, key, count, fake_now) + if not is_str(key) or count <= 0 then + return 0 + end + + local now = ngx_now() * 1000 + + -- just for testing + if is_num(fake_now) then + now = fake_now + end + + local res, err = acquire_lock(self, key) + if not res then + return nil, err + end + + local res, err = adjust(self, key, now) + if not res then + release_lock(self) + return nil, err + end + + local last = res.last + local avail = res.avail + + if avail <= 0 then + update(self, key, avail, last) + release_lock(self) + return 0 + end + + if count > avail then + count = avail + end + + avail = avail - count + update(self, key, avail, last) + release_lock(self) + + return count +end + + +return _M diff --git a/lib/resty/limit/token.md b/lib/resty/limit/token.md new file mode 100644 index 0000000..39d1d4a --- /dev/null +++ b/lib/resty/limit/token.md @@ -0,0 +1,273 @@ +Name +==== + +lua-resty-limit-token - Lua module for limiting request rate for OpenResty/ngx_lua, using the token bucket method. + +Table of Contents +================= + +* [Status](#status) +* [Synopsis](#synopsis) +* [Description](#description) +* [Methods](#methods) + * [new](#new) + * [rate](#rate) + * [take](#take) + * [take_available](#take_available) +* [Limiting Granularity](#limiting-granularity) +* [Installation](#installation) +* [Bugs and Patches](#bugs-and-patches) +* [Author](#author) +* [Copyright and License](#copyright-and-license) +* [See Also](#see-also) + +Synopsis +======== + +```nginx +http { + lua_shared_dict my_limit_token_store 100m; + lua_shared_dict my_locks 100k; + + server { + location / { + access_by_lua_block { + local limit_token = require "resty.limit.token" + + local lim, err = limit_token.new("my_limit_token_store", 500, 10, 3, { + locks_shdict_name = "my_locks", -- use lua-resty-lock + }) + + if not lim then + ngx.log(ngx.ERR, + "failed to instantiate a resty.limit.token object: ", err) + return ngx.exit(500) + end + + -- the following call must be per-request. + -- here we use the remote (IP) address as the limiting key + local key = ngx.var.binary_remote_addr + local delay, err = lim:take(key, 2, 200) + if not delay then + if err == "rejected" then + return ngx.exit(503) + end + ngx.log(ngx.ERR, "failed to take token: ", err) + return ngx.exit(500) + end + + if delay >= 0.001 then + ngx.sleep(delay) + end + } + + # content handler goes here. if it is content_by_lua, then you can + # merge the Lua code above in access_by_lua into your content_by_lua's + # Lua handler to save a little bit of CPU time. + } + + location /available { + access_by_lua_block { + local limit_token = require "resty.limit.token" + + -- global 20r/s 6000r/5m + local lim_global, err = limit_token.new("my_limit_token_store", 100, 6000, 2, { + locks_shdict_name = "my_locks", + }) + + if not lim_global then + ngx.log(ngx.ERR, + "failed to instantiate a resty.limit.token object: ", err) + return ngx.exit(500) + end + + -- single 2r/s 600r/5m + local lim_single, err = limit_token.new("my_limit_token_store", 500, 600, 1, { + locks_shdict_name = "my_locks", + }) + + if not lim_single then + ngx.log(ngx.ERR, + "failed to instantiate a resty.limit.token object: ", err) + return ngx.exit(500) + end + + local t0, err = lim_global:take_available("__global__", 1) + if not t0 then + ngx.log(ngx.ERR, "failed to take available: ", err) + return ngx.exit(500) + end + + -- here we use the userid as the limiting key + local key = ngx.var.arg_userid or "__single__" + + local t1, err = lim_single:take_available(key, 1) + if not t1 then + ngx.log(ngx.ERR, "failed to take available: ", err) + return ngx.exit(500) + end + + if t0 == 1 then + return -- global bucket is not hungry + else + if t1 == 1 then + return -- single bucket is not hungry + else + return ngx.exit(503) + end + end + } + } + } +} +``` + +Description +=========== + +This module provides APIs to help the OpenResty/ngx_lua user programmers limit request rate using the "[token bucket](https://en.wikipedia.org/wiki/Token_bucket)" method. + +Methods +======= + +[Back to TOC](#table-of-contents) + +new +=== + +**syntax:** `obj, err = class.new(shdict_name, interval, capacity, quantum?, opts?)` + +Instantiates an object of this class. The `class` value is returned by the call `require "resty.limit.token"`. + +The method returns a new token bucket that fills at the rate of `quantum` number tokens every `interval`, up to the given maximum `capacity`. The bucket is initially full. + +This method takes the following arguments and an optional options table `opts`: + +* `shdict_name` is the name of the [lua_shared_dict](https://github.com/openresty/lua-nginx-module#lua_shared_dict) shm zone. + + It is best practice to use separate shm zones for different kinds of limiters. + +* `interval` is the time passing between adding tokens, in milliseconds. + +* `capacity` is the maximum number of tokens to hold in the bucket. + +* `quantum` is the number of tokens to add to the bucket in one interval, default `1`. + +The options table accepts the following options: + +* `locks_shdict_name` Specifies the shared dictionary name (created by [lua_shared_dict](http://https://github.com/openresty/lua-nginx-module#lua_shared_dict)) for the lock, default `locks`. See [lua-resty-lock](http://github.com/agentzh/lua-resty-lock) for more details. + +On failure, this method returns `nil` and a string describing the error (like a bad `lua_shared_dict` name). + + +[Back to TOC](#table-of-contents) + +rate +==== + +**syntax:** `rate = obj:rate()` + +The method returns the fill rate of the bucket, in tokens per second. + +[Back to TOC](#table-of-contents) + +take +==== + +**syntax:** `delay, err = obj:take(key, count, max_wait?)` + +The method takes count tokens from the bucket without blocking. If the 3rd argument is `nil`, it returns the time that the caller should wait until the tokens are actually available. + +This method accepts the following arguments: + +* `key` is the user specified key to limit the rate. + + Please note that this module does not prefix nor suffix the user key so it is the user's responsibility to ensure the key is unique in the `lua_shared_dict` shm zone. + +* `count` is the number of tokens to remove. + +* `max_wait` is the maximum time that we would wait for enough tokens to be added, in milliseconds. This argument is optional. + + In this way, the method will only take tokens from the bucket if the wait time for the tokens is no greater than `max_wait`, and returns the time that the caller should wait until the tokens are actually available, otherwise it returns `nil` and the error string `"rejected"`. + +If an error occurred (like failures when accessing the lua_shared_dict shm zone backing the current object), then this method returns nil and a string describing the error. + +[Back to TOC](#table-of-contents) + +take_available +============== + +**syntax:** `count, err = obj:take_available(key, count)` + +The method takes up to count immediately available tokens from the bucket. It returns the number of tokens removed, or zero if there are no available tokens. It does not block. + +This method accepts the following arguments: + +* `key` is the user specified key to limit the rate. + + Please note that this module does not prefix nor suffix the user key so it is the user's responsibility to ensure the key is unique in the `lua_shared_dict` shm zone. + +* `count` is the number of tokens to remove. + +If an error occurred (like failures when accessing the lua_shared_dict shm zone backing the current object), then this method returns nil and a string describing the error. + +[Back to TOC](#table-of-contents) + +Limiting Granularity +==================== + +The limiting works on the granularity of an individual NGINX server instance (including all its worker processes). Thanks to the shm mechanism; we can share state cheaply across all the workers in a single NGINX server instance. + +[Back to TOC](#table-of-contents) + +Installation +============ + +Please see [library installation instructions](../../../README.md#installation). + +[Back to TOC](#table-of-contents) + +Bugs and Patches +================ + +Please report bugs or submit patches by + +1. creating a ticket on the [GitHub Issue Tracker](https://github.com/openresty/lua-resty-limit-traffic/issues), +1. or posting to the [OpenResty community](#community). + +[Back to TOC](#table-of-contents) + +Author +====== + +Monkey Zhang , UPYUN Inc. + +[Back to TOC](#table-of-contents) + +# Copyright and License + +This module is licensed under the BSD license. + +Copyright (C) 2016-2017, by Yichun "agentzh" Zhang, OpenResty Inc. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +[Back to TOC](#table-of-contents) + +See Also +======== + +* module [resty.limit.traffic](./traffic.md) +* library [lua-resty-limit-traffic](../../../README.md) +* the ngx_lua module: https://github.com/openresty/lua-nginx-module +* OpenResty: https://openresty.org/ + +[Back to TOC](#table-of-contents) diff --git a/t/token.t b/t/token.t new file mode 100644 index 0000000..87031f2 --- /dev/null +++ b/t/token.t @@ -0,0 +1,502 @@ +# vim:set ft= ts=4 sw=4 et fdm=marker: + +use Test::Nginx::Socket::Lua; +use Cwd qw(cwd); + +repeat_each(2); + +plan tests => repeat_each() * (blocks() * 4); + +#no_diff(); +#no_long_string(); + +my $pwd = cwd(); + +our $HttpConfig = <<_EOC_; + lua_package_path "$pwd/../lua-resty-lock/lib/?.lua;$pwd/lib/?.lua;;"; +_EOC_ + +no_long_string(); +run_tests(); + +__DATA__ + +=== TEST 1: take +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict my_locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 250, 10, 1, { + locks_shdict_name = "my_locks", + }) + + local now = ngx.now() * 1000 + local delay1, _ = lim:take(uri, 10, nil, now) + local delay2, _ = lim:take(uri, 2, nil, now) + local delay3, _ = lim:take(uri, 2, nil, now) + local delay4, _ = lim:take(uri, 1, nil, now) + + ngx.say("delay1: ", delay1) + ngx.say("delay2: ", delay2) + ngx.say("delay3: ", delay3) + ngx.say("delay4: ", delay4) + '; + } +--- request +GET /t +--- response_body +delay1: 0 +delay2: 0.5 +delay3: 1 +delay4: 1.25 +--- no_error_log +[error] +[lua] + + + +=== TEST 2: take - offset time +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 250, 10) + + local now = ngx.now() * 1000 + local delay1, _ = lim:take(uri, 0, nil, now) + local delay2, _ = lim:take(uri, 10, nil, now) + local delay3, _ = lim:take(uri, 1, nil, now) + local delay4, _ = lim:take(uri, 1, nil, now + 250) + + ngx.say("delay1: ", delay1) + ngx.say("delay2: ", delay2) + ngx.say("delay3: ", delay3) + ngx.say("delay4: ", delay4) + '; + } +--- request +GET /t +--- response_body +delay1: 0 +delay2: 0 +delay3: 0.25 +delay4: 0.25 +--- no_error_log +[error] +[lua] + + + +=== TEST 3: take - more than capacity +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 1, 10) + + local now = ngx.now() * 1000 + local delay1, _ = lim:take(uri, 10, nil, now) + local delay2, _ = lim:take(uri, 15, nil, now + 20) + + ngx.say("delay1: ", delay1) + ngx.say("delay2: ", delay2) + '; + } +--- request +GET /t +--- response_body +delay1: 0 +delay2: 0.005 +--- no_error_log +[error] +[lua] + + + +=== TEST 4: take - offset sub-quantum time +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 10, 10) + + local now = ngx.now() * 1000 + local delay1, _ = lim:take(uri, 10, nil, now) + local delay2, _ = lim:take(uri, 1, nil, now + 7) + local delay3, _ = lim:take(uri, 1, nil, now + 8) + local delay4, _ = lim:take(uri, 1, nil, now + 10) + local delay5, _ = lim:take(uri, 1, nil, now + 25) + + ngx.say("delay1: ", delay1) + ngx.say("delay2: ", delay2) + ngx.say("delay3: ", delay3) + ngx.say("delay4: ", delay4) + ngx.say("delay5: ", delay5) + '; + } +--- request +GET /t +--- response_body +delay1: 0 +delay2: 0.003 +delay3: 0.012 +delay4: 0.02 +delay5: 0.015 +--- no_error_log +[error] +[lua] + + + +=== TEST 5: take - within capacity +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 10, 5) + + local now = ngx.now() * 1000 + local delay1, _ = lim:take(uri, 5, nil, now) + local delay2, _ = lim:take(uri, 5, nil, now + 60) + local delay3, _ = lim:take(uri, 1, nil, now + 60) + local delay4, _ = lim:take(uri, 2, nil, now + 80) + + ngx.say("delay1: ", delay1) + ngx.say("delay2: ", delay2) + ngx.say("delay3: ", delay3) + ngx.say("delay4: ", delay4) + '; + } +--- request +GET /t +--- response_body +delay1: 0 +delay2: 0 +delay3: 0.01 +delay4: 0.01 +--- no_error_log +[error] +[lua] + + + +=== TEST 6: take - max wait +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 1, 10) + + local now = ngx.now() * 1000 + local delay1, err1 = lim:take(uri, 10, nil, now) + local delay2, err2 = lim:take(uri, 15, 4, now + 20) + local delay3, err3 = lim:take(uri, 10, 4, now + 25) + + ngx.say("delay1: ", delay1) + ngx.say("err1: ", err1) + ngx.say("delay2: ", delay2) + ngx.say("err2: ", err2) + ngx.say("delay3: ", delay3) + ngx.say("err3: ", err3) + '; + } +--- request +GET /t +--- response_body +delay1: 0 +err1: nil +delay2: nil +err2: rejected +delay3: 0 +err3: nil +--- no_error_log +[error] +[lua] + + + +=== TEST 7: take - count greater than capacity +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 1, 10) + + local now = ngx.now() * 1000 + local delay1, err1 = lim:take(uri, 15, 4, now) + local delay2, err2 = lim:take(uri, 15, nil, now + 20) + + ngx.say("delay1: ", delay1) + ngx.say("err1: ", err1) + ngx.say("delay2: ", delay2) + ngx.say("err2: ", err2) + '; + } +--- request +GET /t +--- response_body +delay1: nil +err1: rejected +delay2: 0.005 +err2: nil +--- no_error_log +[error] +[lua] + + + +=== TEST 8: take_available +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 250, 10) + + local now = ngx.now() * 1000 + local count1, _ = lim:take_available(uri, 5, now) + local count2, _ = lim:take_available(uri, 2, now) + local count3, _ = lim:take_available(uri, 5, now) + local count4, _ = lim:take_available(uri, 1, now) + + ngx.say("count1: ", count1) + ngx.say("count2: ", count2) + ngx.say("count3: ", count3) + ngx.say("count4: ", count4) + '; + } +--- request +GET /t +--- response_body +count1: 5 +count2: 2 +count3: 3 +count4: 0 +--- no_error_log +[error] +[lua] + + + +=== TEST 9: take_available - offset time +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 250, 10) + + local now = ngx.now() * 1000 + local count1, _ = lim:take_available(uri, 0, now) + local count2, _ = lim:take_available(uri, 10, now) + local count3, _ = lim:take_available(uri, 1, now) + local count4, _ = lim:take_available(uri, 1, now + 250) + + ngx.say("count1: ", count1) + ngx.say("count2: ", count2) + ngx.say("count3: ", count3) + ngx.say("count4: ", count4) + '; + } +--- request +GET /t +--- response_body +count1: 0 +count2: 10 +count3: 0 +count4: 1 +--- no_error_log +[error] +[lua] + + + +=== TEST 10: take_available - more than capacity +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 1, 10) + + local now = ngx.now() * 1000 + local count1, _ = lim:take_available(uri, 10, now) + local count2, _ = lim:take_available(uri, 15, now + 20) + + ngx.say("count1: ", count1) + ngx.say("count2: ", count2) + '; + } +--- request +GET /t +--- response_body +count1: 10 +count2: 10 +--- no_error_log +[error] +[lua] + + + +=== TEST 11: take_available - within capacity +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 10, 5) + + local now = ngx.now() * 1000 + local count1, _ = lim:take_available(uri, 5, now) + local count2, _ = lim:take_available(uri, 5, now + 60) + local count3, _ = lim:take_available(uri, 1, now + 70) + + ngx.say("count1: ", count1) + ngx.say("count2: ", count2) + ngx.say("count3: ", count3) + '; + } +--- request +GET /t +--- response_body +count1: 5 +count2: 5 +count3: 1 +--- no_error_log +[error] +[lua] + + + +=== TEST 12: rate +--- http_config eval +" +$::HttpConfig + + lua_shared_dict store 1m; + lua_shared_dict locks 100k; +" +--- config + location /t { + content_by_lua ' + local limit_token = require "resty.limit.token" + ngx.shared.store:flush_all() + + local uri = ngx.var.uri + local lim = limit_token.new("store", 500, 5, 2) + ngx.say("rate: ", lim:rate()) + '; + } +--- request +GET /t +--- response_body +rate: 4 +--- no_error_log +[error] +[lua]