-
Notifications
You must be signed in to change notification settings - Fork 170
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
Improve rate limit policy #665
Conversation
@@ -104,7 +83,7 @@ function _M:access() | |||
local keys = {} | |||
|
|||
for _, limiter in ipairs(self.limiters) do | |||
local lim, initerr = init_limiter(limiter) | |||
local lim, initerr = traffic_limiters[limiter.name](limiter) |
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'm not sure about this change.
resty.limit
calls assert
when instantiating some limiters. See this for example:
https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/req.lua#L56
I'd expect that the library returned that kind of errors in the second var of the response, but that's not the case. For that reason, I think that we should catch the error like we did before.
What do you think @mikz ?
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 just define those requirements fro those parameters in the JSON schema.
Catching errors in LuaJIT is going to trash performance and it is really not necessary as we can just ensure we pass the right parameters.
Errors in OpenResty are raised only on super critical unexpected issues and those are not really expected to be recovered.
Errors that are supposed to be handled are returned as second return value and those we should handle as well as nil values etc.
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.
Defining those constraints in the schema sounds better 👍
@@ -20,7 +20,7 @@ | |||
"description": "Limiting request concurrency (or concurrent connections)" | |||
}, | |||
"key": { | |||
"description": "Key of limiter", | |||
"description": "The user key corresponding to the limiter object. This must be unique in the scope.", |
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 is just a detail, but I think we should change the user key
part. The reason is that one of the credentials that 3scale backend accepts is also called user key
, and it's unrelated to this. In order to avoid confusion, I'd just call it key
.
return | ||
end | ||
end | ||
lim.dict = red and red or lim.dict |
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.
One of the red
is redundant. This should be red or lim.dict
.
end | ||
|
||
for i, lim in ipairs(limiters) do | ||
lim.dict = red and red or lim.dict |
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 should be red or lim.dict
.
}, | ||
"status_code": { | ||
"type": "integer", | ||
"description": "The status code when requests over the limit, default 429" |
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 can use the default
field.
local error_settings = default_error_settings | ||
if config_error_settings then | ||
for _, error_setting in pairs(config_error_settings) do | ||
if error_setting.type 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.
Do we need this check? I think the type should be required according to the schema.
if config_error_settings then | ||
for _, error_setting in pairs(config_error_settings) do | ||
if error_setting.type then | ||
if error_setting.status_code 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.
status_code
is not required according to the schema. This means that here the code could skip this if
, and leave the status_code
unassigned. I'm not sure if this could cause problems.
I think we should either we make status_code
required, or should have an else
branch here that assigns the default value defined (429).
Same for error_handling
below.
What do you think?
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.
@davidor At L82, I set default_error_settings
to error_settings
, so the code does not leave the status_code
and the error_handling
unassigned.
When a user defines type
and status_code
, then status_code
is changed.
When a user defines type
and error_handling
, then error_handling
is changed.
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're right 👍
I was confused, sorry.
Good job @y-tabata 👍 This PR introduces some nice improvements. I've added comments about minor issues but in general looks good 👍 The circle Can you please rebase on master so the fix applies in this branch? Thanks. |
Tests are failing because there are errors in the schema. The error is in the
|
@davidor The |
@y-tabata yep, |
@y-tabata This looks good 👍 Could you squash some of your commits before merging, please? We try to keep a clean git history, although we are not strict about it. In your PR, there are many commits that fix issues introduced in previous commits. For example, the Just try to squash those that seem obvious, like the ones I mentioned, to clean the PR a bit. Do not spend too much time on this. Please let me know if you need any help with this. Also, @mikz do you want to do a quick review before merging? |
8588974
to
4f5d327
Compare
4f5d327
to
c31ed90
Compare
c31ed90
to
4ab2ac8
Compare
@davidor I squashed some of my commits. Is there any unnecessary commits? |
Thanks @y-tabata 👍 |
|
||
local key | ||
if limiter.key.scope == "service" then | ||
key = limiter.key.service_name.."_"..limiter.name.."_"..limiter.key.name |
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.
Better to use string.format
. Concatenating strings allocates new one on every concatenation.
And this is done in access
phase which is evaluated on every request.
And to even further help JIT, cache the string.format
in the top of the file to avoid global table lookup by local format = string.format
and then using just local function string
.
end | ||
|
||
limiters[#limiters + 1] = lim | ||
keys[#keys + 1] = limiter.key | ||
table.insert(keys, 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.
table
is global object and global table lookup trashes performance.
Cache any functions on global objects locally on top of the file like: local insert = table.insert
and then just use their local reference insert
.
"description": "Service scope, affecting to one service" | ||
}] | ||
}, | ||
"service_name": { |
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 thought this won't be supplied by users but rather just automatically using the service_id available in the context. So all service scope limiters are automatically scoped and can't be shared between services. This could still be shared by multiple services when they fill the same identifier.
"scope": { | ||
"type": "string", | ||
"description": "Scope of the key", | ||
"default": "global", |
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 default should be service.
"type": "string", | ||
"description": "The name of the key, must be unique in the scope" | ||
}, | ||
"scope": { |
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 could extract this into shared definition in this schema an reuse it so it is not duplicated 3 times.
See an example:
https://github.com/3scale/apicast/blob/4ab2ac80d1bd6f952de4a0068932b5a992619e0b/gateway/src/apicast/policy/manifest-schema.json#L11-L21
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.
Possibly even the whole key
property could be shared between all limiters.
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.
@mikz If I change schema like this:
"definitions": {
"key": {
"$id": "#/definitions/key",
"description": "The key corresponding to the limiter object",
...
}
},
"properties": {
"limiters": {
...
"items": {
"anyOf": [{
"type": "object",
"properties": {
"name": {
...
},
"$ref": "#/definitions/key",
"conn": {
...
},
the following errors appear in ajv:
gateway/src/apicast/policy/rate_limit/apicast-policy.json invalid
[ { keyword: 'type',
dataPath: '.configuration.properties[\'limiters\'].items.anyOf[0].properties[\'$ref\']',
schemaPath: '#/type',
params: { type: 'object,boolean' },
message: 'should be object,boolean' },
{ keyword: 'type',
dataPath: '.configuration.properties[\'limiters\'].items',
schemaPath: '#/type',
params: { type: 'array' },
message: 'should be array' },
{ keyword: 'anyOf',
dataPath: '.configuration.properties[\'limiters\'].items',
schemaPath: '#/properties/items/anyOf',
params: {},
message: 'should match some schema in anyOf' } ]
Do you have any idea to avoid them?
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.
@y-tabata it should be:
{ "properties": {
"key": {
"$ref": "#/definitions/key"
}
}
You have "$ref"
as a property of "properties"
, not "key" property
.
See the example I linked:
https://github.com/3scale/apicast/blob/4ab2ac80d1bd6f952de4a0068932b5a992619e0b/gateway/src/apicast/policy/manifest-schema.json#L44-L62
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.
@mikz Thank you!
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.
👍 good job!
My comments are not critical enough, so we can merge this and continue later.
We are trying to merge PRs that will go into the next release, so better to merge this now and make improvements later.
This PR improves the rate limit policy according to the comments of PR #648.