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

[count-inc] Add decrement logic to the incoming function #843

Merged
merged 4 commits into from
Sep 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- The `scope` of the Rate Limit policy is `service` by default [PR #704](https://github.com/3scale/apicast/pull/704)
- Decoded JWTs are now exposed in the policies context by the APIcast policy [PR #718](https://github.com/3scale/apicast/pull/718)
- Upgraded OpenResty to 1.13.6.2, uses OpenSSL 1.1 [PR #733](https://github.com/3scale/apicast/pull/733)
- Use forked `resty.limit.count` that uses increments instead of decrements [PR #758](https://github.com/3scale/apicast/pull/758)
- Use forked `resty.limit.count` that uses increments instead of decrements [PR #758](https://github.com/3scale/apicast/pull/758), [PR 843](https://github.com/3scale/apicast/pull/843)
- Rate Limit policy to take into account changes in the config [PR #703](https://github.com/3scale/apicast/pull/703)
- The regular expression for mapping rules has been changed, so that special characters are accepted in the wildcard values for path [PR #714](https://github.com/3scale/apicast/pull/714)
- Call `init` and `init_worker` on all available policies regardless they are used or not [PR #770](https://github.com/3scale/apicast/pull/770)
Expand Down
11 changes: 11 additions & 0 deletions gateway/src/resty/limit/count-inc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ function _M.incoming(self, key, commit)
if not count then
return nil, err
end

if count > limit then
count, err = dict:incr(key, -1)
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this should be done by the .uncommit that is being called by resty.limit.traffic.

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 you mean .uncommit needs to be called here with the condition of i == n, right?
https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/traffic.lua#L27-L29

However resty.limit.conn includes this.

So we should choose which file we edit, resty.limit.traffic and resty.limit.conn, or resty.limit.count-inc.

Copy link
Contributor

Choose a reason for hiding this comment

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

@y-tabata I think we don't need to change any resty.limit library. We have to use it correctly.

resty.limit.traffic calls :incoming(key, false) until the last limiter which is called as :incoming(key, true). If any of those return an error, it is fine to just exit. Nothing was committed to the database.

When all of them succeed, it tries to call again :incoming(key, true), but in this case, if any of them fails then call :uncommit(key) for all limiters that it already called :incoming.

So I'd suspect our limiters are not rolled back because they are not wrapped in one resty.limit.traffic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resty.limit.traffic calls :incoming(key, false) until the last limiter which is called as :incoming(key, true). If any of those return an error, it is fine to just exit. Nothing was committed to the database.

Using the current count-inc, I think that when the last limiter, which is called as :incoming(key, true), only returns nil and "rejected", the last one is committed to the database.

Copy link
Contributor

@mikz mikz Sep 7, 2018

Choose a reason for hiding this comment

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

Ok. I got it now. Implemented a fix to the upstream library: openresty/lua-resty-limit-traffic@c53f224 (openresty/lua-resty-limit-traffic#34)

I think we can just copy that here.


if not count then
return nil, err
end

return nil, "rejected"
end

else
count = (dict:get(key) or 0) + 1
end
Expand Down
6 changes: 3 additions & 3 deletions spec/policy/rate_limit/rate_limit_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describe('Rate limit policy', function()
assert(rate_limit_policy:access(context))
assert.returns_error('limits exceeded', rate_limit_policy:access(context))

assert.equal('2', redis:get('11110_fixed_window_test3'))
assert.equal('1', redis:get('11110_fixed_window_test3'))
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't fully understand the purpose of this change.
Before, we increased the number of hits even when going over limits. Now we force the value to be at most equal to the limit. Why do we need to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before, we increased the number of hits even when going over limits.

First, we should not increase the number of hits when going over limits, because we should count the number the client calls the API backend.

The previous behavior is the below:

Case 1:

fixed_window_limiters : [
  { "key" : { "name" : "a"}, "count" : 1, "window" : 10 },
  { "key" : { "name" : "b"}, "count" : 2, "window" : 10 }
]

After 2 API requests,
the number of hits 'a' is '1',
the number of hits 'b' is '1'.
-> correct

Case 2:

fixed_window_limiters : [
  { "key" : { "name" : "a"}, "count" : 2, "window" : 10 },
  { "key" : { "name" : "b"}, "count" : 1, "window" : 10 }
]

After 2 API requests,
the number of hits 'a' is '1',
the number of hits 'b' is '2'.
-> wrong

assert.spy(ngx.exit).was_called_with(429)
end)

Expand All @@ -211,7 +211,7 @@ describe('Rate limit policy', function()
assert(rate_limit_policy:access(ctx))
assert.returns_error('limits exceeded', rate_limit_policy:access(ctx))

assert.equal('2', redis:get('11110_fixed_window_test3'))
assert.equal('1', redis:get('11110_fixed_window_test3'))
assert.spy(ngx.exit).was_called_with(429)
end)

Expand All @@ -232,7 +232,7 @@ describe('Rate limit policy', function()
assert(rate_limit_policy:access(context))
assert.returns_error('limits exceeded', rate_limit_policy:access(context))

assert.equal('2', redis:get('11110_fixed_window_' .. test_host))
assert.equal('1', redis:get('11110_fixed_window_' .. test_host))
assert.spy(ngx.exit).was_called_with(429)
end)

Expand Down
152 changes: 152 additions & 0 deletions spec/resty/limit/count-inc_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
local _M = require 'resty.limit.count-inc'

local shdict_mt = {
__index = {
get = function(t, k) return rawget(t, k) end,
set = function(t, k, v) rawset(t, k , v); return true end,
incr = function(t, key, inc, init, _)
local value = t:get(key) or init
if not value then return nil, 'not found' end

t:set(key, value + inc)
return t:get(key)
end,
}
}
local function shdict()
return setmetatable({ }, shdict_mt)
end

describe('resty.limit.count-inc', function()

before_each(function()
ngx.shared.limiter = mock(shdict(), true)
end)

describe('.new', function()
describe('using correct shdict', function()
it('returns limiter', function()
local lim = _M.new('limiter', 1, 1)
assert.same(1, lim.limit)
assert.same(1, lim.window)
end)
end)

describe('using incorrect shdict', function()
it('returns error', function()
local _, err = _M.new('incorrect', 1, 1)
assert.same('shared dict not found', err)
end)
end)
end)

describe('.incoming', function()
local lim

before_each(function()
lim = _M.new('limiter', 1, 1)
end)

describe('when commit is true', function()
describe('if count is less than limit', function()
it('returns zero and remaining', function()
local delay, err = lim:incoming('tmp', true)
assert.same(0, delay)
assert.same(0, err)
end)

describe('and incr method fails', function()
it('returns error', function()
ngx.shared.limiter.incr = function()
return nil, 'something'
end
local delay, err = lim:incoming('tmp', true)
assert.is_nil(delay)
assert.same('something', err)
end)
end)
end)

describe('if count is greater than limit', function()
it('return rejected', function()
lim:incoming('tmp', true)
local delay, err = lim:incoming('tmp', true)
assert.is_nil(delay)
assert.same('rejected', err)
end)

describe('and incr method fails', function()
it('returns error', function()
ngx.shared.limiter.incr = function(t, key, inc, init, _)
local value = t:get(key) or init
if not value then return nil, 'not found' end
if inc == -1 then return nil, 'something' end
t:set(key, value + inc)
return t:get(key)
end
lim:incoming('tmp', true)
local delay, err = lim:incoming('tmp', true)
assert.is_nil(delay)
assert.same('something', err)
end)
end)
end)
end)

describe('when commit is false', function()
describe('if count is less than limit', function()
it('returns zero and remaining', function()
local delay, err = lim:incoming('tmp', false)
assert.same(0, delay)
assert.same(0, err)
end)
end)

describe('if count is greater than limit', function()
it('return rejected', function()
lim:incoming('tmp', true)
local delay, err = lim:incoming('tmp', false)
assert.is_nil(delay)
assert.same('rejected', err)
end)
end)
end)

end)

describe('.uncommit', function()
local lim

before_each(function()
lim = _M.new('limiter', 1, 1)
end)

describe('when incr method succeeds', function()
it('returns remaining', function()
lim:incoming('tmp', true)
local delay = lim:uncommit('tmp')
assert.same(1, delay)
end)
end)

describe('when incr method fails', function()
describe('if key is not found', function()
it('returns remaining', function()
local delay = lim:uncommit('tmp')
assert.same(1, delay)
end)
end)

it('returns error', function()
lim:incoming('tmp', true)
ngx.shared.limiter.incr = function()
return nil, 'something'
end
local delay, err = lim:uncommit('tmp')
assert.is_nil(delay)
assert.same('something', err)
end)
end)
end)

end)