-
Notifications
You must be signed in to change notification settings - Fork 157
feature: implemented resty.limit.count
.
#21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@doujiang24 @dndx @membphis Will you please review this PR for me? Many thanks! |
lib/resty/limit/count.lua
Outdated
-- FIXME we have a (small) race-condition window between dict:get() and | ||
-- dict:set() across multiple nginx worker processes. The size of the | ||
-- window is proportional to the number of workers. | ||
function _M.incoming(self, key, commit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we consider "commit" by default since that seems to be a more meaningful default behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dndx The commit thing is for combining multiple limiters together in the resty.limit.traffic
module.
lib/resty/limit/count.md
Outdated
this method returns `nil` and the error string `"rejected"`. | ||
|
||
3. In addition, like the previous two cases, this method | ||
also returns a third return value indicating the next time to reset the `count` value. This 3nd return value can be used to monitor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be used to monitor -> can be used for monitoring
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think both forms are fine? I'm not a native speaker though :)
lib/resty/limit/count.lua
Outdated
end | ||
|
||
-- return remaining and time to reset | ||
return 0, remaining, reset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know 0
evaluates to true
in Lua, but it just seems weird that we are using 0
as the return value for indicating the limit has not been exceeded.
Why don't we just return true
and false
for the first return value instead? That way we can just call the value "whether the access should be allowed" and is much easier to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we just need to be consistent with other limiter modules here (not sure about other modules' code though).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the first return value is treated as delay
in other limiter modules: https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/traffic.lua#L25.
So we should return 0
instead of true
here.
lib/resty/limit/count.lua
Outdated
return nil, "shdict abused by other users" | ||
end | ||
local rec = ffi_cast(const_rec_ptr_type, v) | ||
reset = tonumber(rec.reset) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the tonumber
here necessary since rec.reset
was already declared as an integer type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dndx It's still needed I guess since an integer typed cdata is still quite different from a Lua number object. Usually we do not want to return cdata objects to the caller when a number is sufficient.
lib/resty/limit/count.lua
Outdated
end | ||
|
||
|
||
-- sees an new incoming event |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the race condition here is a big concern as we could be seeing a lot of requests slipping through if the number of worker is high and there are a sudden spike of traffic.
Maybe use https://github.com/openresty/lua-resty-lock to guard the critical section?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thought. This whole algorithm could be implemented much more efficiently and elegantly if ngx.shdict.incr()
supports preservation of expiration timers and provides API to get the remaining TTL for a key.
Currently, calling incr()
on any value forces it's exptime
to 0
which makes it never expire. If we support an option to allow incr()
to not reset exptime
and provides ngx.shared.get_ttl
then this algorithm can be implemented lock-free (in Lua land) with no need for FFI hacking.
Although in the short term using explicit locking should works fine.
@agentzh what's your thought on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dndx I gave a stab at shm:ttl()
here: openresty/lua-resty-core#140 and am willing to work on incr()
as well if we decide to go that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, better enhance the incr()
method wrt ttl handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dndx I used incr
instead of FFI. please have a look.
lib/resty/limit/count.md
Outdated
if not delay then | ||
if err == "rejected" then | ||
ngx.header["X-RateLimit-Limit"] = "5000" | ||
gx.header["X-RateLimit-Reset"] = reset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed, thx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The race condition here is just too big to ignore. We should merge the shdict:expire()
PR first.
lib/resty/limit/count.lua
Outdated
|
||
return 0, remaining, reset | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should use 2 blank lines to separate Lua function definitions consistently in this Lua library. Please fix other similar places as well.
lib/resty/limit/count.lua
Outdated
|
||
else | ||
remaining = limit - 1 | ||
reset = now + window |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reset
is confusing, maybe new_ttl
is a better name here?
e2bdcef
to
1779e72
Compare
@agentzh fixed the race condition by using the |
lib/resty/limit/count.lua
Outdated
assert(key) | ||
local dict = self.dict | ||
|
||
local remaining, err = dict:incr(key, 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a small race condition here between commit()
and uncommit
in which time window the key is expired. Maybe we should not return an error here when the err is "not found"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I think we should just get
again and return whatever the new value is. The old window has expired and no need to incr
it anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if not delay then | ||
if err == "rejected" then | ||
ngx.header["X-RateLimit-Limit"] = "5000" | ||
ngx.header["X-RateLimit-Remaining"] = 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard-coded remaining?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
local delay, err = lim:incoming(key, true)
returns nil, "rejected"
here, and remaining
must be 0 in this situation.
@thibaultcha @dndx Will you please review the new version of this PR? Thanks! |
lib/resty/limit/count.lua
Outdated
if not remaining then | ||
return nil, err | ||
end | ||
if remaining == limit - 1 then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style: need a newline here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a race potential here when key expires between incr
and expire
. Should run this part again when incr
succeeds but expire
errors with 'not found'
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
lib/resty/limit/count.lua
Outdated
end | ||
|
||
else | ||
remaining = (dict:get(key) or 0) - 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, should it be remaining = (dict:get(key) or limit) - 1
instead when key
does not exist? Otherwise a new window will be counted as -1
and be rejected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still need to fix this.
lib/resty/limit/count.lua
Outdated
assert(key) | ||
local dict = self.dict | ||
|
||
local remaining, err = dict:incr(key, 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I think we should just get
again and return whatever the new value is. The old window has expired and no need to incr
it anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing much to say on the logic anymore, good job! I did find a few typos or mistakes in the docs and tests though, hope it helps!
lib/resty/limit/count.md
Outdated
This Lua module's implementation is similar to [GitHub API Rate Limiting](https://developer.github.com/v3/#rate-limiting) But this Lua | ||
module is flexible in that it can be configured with different rate. | ||
|
||
This module depands on [lua-resty-core](https://github.com/openresty/lua-resty-core), so you should enable it like the following codes: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: depends
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
" you should enable it like so:" would probably read better?
lib/resty/limit/count.md
Outdated
rate by a fixed number of requests in given time window. | ||
|
||
This Lua module's implementation is similar to [GitHub API Rate Limiting](https://developer.github.com/v3/#rate-limiting) But this Lua | ||
module is flexible in that it can be configured with different rate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
different rates (plural) maybe? Or window sizes?
lib/resty/limit/count.md
Outdated
```nginx | ||
init_by_lua_block { | ||
require "resty.core" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The indentation seems to be off here
lib/resty/limit/count.md
Outdated
|
||
* `count` is the specified number of requests threshold. | ||
|
||
* `time_window` is the time window in second before the request count is reset. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"in seconds" (plural)?
lib/resty/limit/count.md
Outdated
The return values depend on the following cases: | ||
|
||
1. If the request does not exceed the `count` value specified in the [new](#new) method, then | ||
this method returns `0` as the delay as well as remaining count of allowed requests at the current time (as the 2nd return value). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and the remaining count of allowed requests reads better maybe?
t/count.t
Outdated
local begin = ngx.time() | ||
|
||
for i = 1, 4 do | ||
local delay, err = lim:incoming("foo", i < 3 and true or false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lim:incoming("foo", i < 3)
should be sufficient, no?
t/count.t
Outdated
--- http_config eval: $::HttpConfig | ||
--- config | ||
location = /t { | ||
content_by_lua ' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test suite uses the old content_by_lua
directive instead of the modern content_by_lua_block
alternative. I believe we have a rule for all new tests to be written with the later, more modern form across the OpenResty codebase.
* [Description](#description) | ||
* [Methods](#methods) | ||
* [new](#new) | ||
* [incoming](#incoming) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are missing the anchor to the uncommit
method here.
lib/resty/limit/count.md
Outdated
uncommit | ||
-------- | ||
|
||
**syntax:** `remaining = obj:uncommit(key)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the signature is remaining, err
here (according to the code) - granted, err
is ignored in the traffic
module, but other limiter modules document it nonetheless.
t/traffic.t
Outdated
", states: ", table.concat(states, ", ")) | ||
end | ||
if i == 4 then | ||
ngx.sleep(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This extraneous delay doesn't seem necessary to make this test succeed, is it? Considering we are working in a 100s window.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thibaultcha thanks for your review, fixed all of them.
remaining, err = dict:incr(key, -1, limit) | ||
if not remaining then | ||
return nil, err | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style: need a newline here.
lib/resty/limit/count.lua
Outdated
local remaining, err = dict:incr(key, 1) | ||
if not remaining then | ||
if err == "not found" then | ||
remaining = dict:get(key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think remaining
should be limit
here since we just got "not found"
from incr()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dndx fixed, thx.
lib/resty/limit/count.lua
Outdated
end | ||
|
||
else | ||
remaining = (dict:get(key) or 0) - 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still need to fix this.
Merged with minor edits. Thanks! |
related apisix/apisix#3469
implemented
resty.limit.count
as GitHub request rate limiting: https://developer.github.com/v3/#rate-limiting