Skip to content

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

Closed
wants to merge 9 commits into from
Closed

Conversation

moonming
Copy link

@moonming moonming commented Sep 4, 2017

implemented resty.limit.count as GitHub request rate limiting: https://developer.github.com/v3/#rate-limiting

@agentzh
Copy link
Member

agentzh commented Sep 4, 2017

@doujiang24 @dndx @membphis Will you please review this PR for me? Many thanks!

-- 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)
Copy link
Member

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?

Copy link
Member

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.

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.
Copy link
Member

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

Copy link
Member

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 :)

end

-- return remaining and time to reset
return 0, remaining, reset
Copy link
Member

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.

Copy link
Member

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).

Copy link
Author

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.

return nil, "shdict abused by other users"
end
local rec = ffi_cast(const_rec_ptr_type, v)
reset = tonumber(rec.reset)
Copy link
Member

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?

Copy link
Member

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.

end


-- sees an new incoming event
Copy link
Member

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?

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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.

Copy link
Author

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.

if not delay then
if err == "rejected" then
ngx.header["X-RateLimit-Limit"] = "5000"
gx.header["X-RateLimit-Reset"] = reset

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thx

@moonming
Copy link
Author

@agentzh @dndx can we merge this PR now?
I will create a new PR to fix race condition after incr() support ttl.

Copy link
Member

@agentzh agentzh left a 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.


return 0, remaining, reset
end

Copy link
Member

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.


else
remaining = limit - 1
reset = now + window
Copy link
Member

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?

@moonming
Copy link
Author

@agentzh fixed the race condition by using the shdict:expire(). please have a look, thx.

assert(key)
local dict = self.dict

local remaining, err = dict:incr(key, 1)
Copy link
Member

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"?

Copy link
Member

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agentzh @dndx fixed race condition. please have a look.

if not delay then
if err == "rejected" then
ngx.header["X-RateLimit-Limit"] = "5000"
ngx.header["X-RateLimit-Remaining"] = 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded remaining?

Copy link
Author

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.

@agentzh
Copy link
Member

agentzh commented Sep 20, 2017

@thibaultcha @dndx Will you please review the new version of this PR? Thanks!

if not remaining then
return nil, err
end
if remaining == limit - 1 then
Copy link
Member

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.

Copy link
Member

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'.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

end

else
remaining = (dict:get(key) or 0) - 1
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

Copy link
Member

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.

assert(key)
local dict = self.dict

local remaining, err = dict:incr(key, 1)
Copy link
Member

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.

Copy link
Member

@thibaultcha thibaultcha left a 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!

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: depends

Copy link
Member

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?

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.
Copy link
Member

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?

```nginx
init_by_lua_block {
require "resty.core"
}
Copy link
Member

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


* `count` is the specified number of requests threshold.

* `time_window` is the time window in second before the request count is reset.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"in seconds" (plural)?

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).
Copy link
Member

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)
Copy link
Member

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 '
Copy link
Member

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)
Copy link
Member

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.

uncommit
--------

**syntax:** `remaining = obj:uncommit(key)`
Copy link
Member

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)
Copy link
Member

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.

Copy link
Author

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
Copy link
Member

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.

local remaining, err = dict:incr(key, 1)
if not remaining then
if err == "not found" then
remaining = dict:get(key)
Copy link
Member

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().

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dndx fixed, thx.

end

else
remaining = (dict:get(key) or 0) - 1
Copy link
Member

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.

@agentzh
Copy link
Member

agentzh commented Sep 22, 2017

Merged with minor edits. Thanks!

@agentzh agentzh closed this Sep 22, 2017
@shawnzhu
Copy link
Contributor

@moonming @agentzh Thank you!

monkeyDluffy6017 pushed a commit to monkeyDluffy6017/lua-resty-limit-traffic that referenced this pull request Apr 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants