Skip to content
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

Merged
merged 7 commits into from
Apr 23, 2018

Conversation

y-tabata
Copy link
Contributor

This PR improves the rate limit policy according to the comments of PR #648.

@@ -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)
Copy link
Contributor

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 ?

Copy link
Contributor

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.

Copy link
Contributor

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.",
Copy link
Contributor

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

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

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"
Copy link
Contributor

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

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

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@davidor
Copy link
Contributor

davidor commented Apr 13, 2018

Good job @y-tabata 👍

This PR introduces some nice improvements. I've added comments about minor issues but in general looks good 👍

The circle s2i-builder check is failing, but it has nothing to do with your PR. I fixed the issue in PR #666

Can you please rebase on master so the fix applies in this branch? Thanks.

@davidor
Copy link
Contributor

davidor commented Apr 16, 2018

Tests are failing because there are errors in the schema. The error is in the s2i-builder check:

gateway/src/apicast/policy/rate_limit/apicast-policy.json invalid
[ { keyword: 'type',
    dataPath: '.configuration.properties[\'limiters\'].items.oneOf[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' } ]

@y-tabata
Copy link
Contributor Author

@davidor The deploy error has nothing to do with this PR, isn't it?

@mikz
Copy link
Contributor

mikz commented Apr 18, 2018

@y-tabata yep, ci/circleci: deploy will always fail for external contributors.

@davidor
Copy link
Contributor

davidor commented Apr 19, 2018

@y-tabata This looks good 👍
Great work 👍

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 remove redundant commit fixes a problem introduced in change to define redis just once commit. So would be nice to squash those 2 before merging.

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?

@y-tabata y-tabata force-pushed the rate_limiting_to_service_policy branch 5 times, most recently from 8588974 to 4f5d327 Compare April 20, 2018 03:09
@y-tabata y-tabata force-pushed the rate_limiting_to_service_policy branch from 4f5d327 to c31ed90 Compare April 20, 2018 03:11
@y-tabata y-tabata force-pushed the rate_limiting_to_service_policy branch from c31ed90 to 4ab2ac8 Compare April 20, 2018 03:51
@ytabata
Copy link

ytabata commented Apr 20, 2018

@davidor I squashed some of my commits. Is there any unnecessary commits?

@davidor
Copy link
Contributor

davidor commented Apr 20, 2018

Thanks @y-tabata 👍


local key
if limiter.key.scope == "service" then
key = limiter.key.service_name.."_"..limiter.name.."_"..limiter.key.name
Copy link
Contributor

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

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": {
Copy link
Contributor

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",
Copy link
Contributor

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": {
Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

@mikz mikz Apr 24, 2018

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mikz Thank you!

Copy link
Contributor

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

@mikz mikz merged commit 21fa6e9 into 3scale:master Apr 23, 2018
@y-tabata y-tabata deleted the rate_limiting_to_service_policy branch April 23, 2018 23:24
@y-tabata
Copy link
Contributor Author

@mikz @davidor Thank you for your review! I continue to improve the policy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants