-
Notifications
You must be signed in to change notification settings - Fork 337
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
Add LeakyBucket-based throttling #206
Conversation
Sorry, this doesn't work yet, just tried it out in my app, and it's not blocking. Will investigate & fix. |
bc3a09a
to
bf74dcb
Compare
It works now—turns out not rounding the |
Using the leaky bucket algorithm, you can get a better distrubution that allows a more human behaviour and that should (in theory) decrease false-positives. It does that because it throttles differently. Let's take the old one, and concider you have a limit of 3 for 5 mins. If the user then did 3 requests, it'll have to wait for the next 5-min slot to do more requests. Using the leaky bucket algorithm, doing 3 requests adds 3 drops in the bucket, and the bucket is full. The bucket then leaks consistently, and when leaked enough, it'll allow 1 more request (and then it will be full again). [Shopify uses and explains this too here](https://help.shopify.com/api/guides/api-call-limit). We might want to add a custom throttled_response for the leaky bucket algorithm, so that it has the limits in there too.
bf74dcb
to
2cf442c
Compare
@ktheory I've also added documentation to the README, if you have time, could you check this out? Meanwhile, I'll push this into staging/prod to try out. |
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.
Hi @jeroenvisser101,
Thanks for putting this together. It's highlighted a design constraint that I wasn't particularly clear on...
I want Rack::Attack throttle state to be written atomically.
The current leaky bucket implementation is not atomic: it's possible for two simultaneous requests to read the same value from cache, compute the updated value, and both write the same updated value; effectively not counting one request.
While it's generally unlikely that such a race condition occurs, it becomes more likely the more concurrent requests an application has (when you need throttling the most).
Rack::Attack.throttle
is careful to use the redis/memcache atomic increment features to avoid uncounted requests.
One atomic leaky bucket implementation that I'd like to caution against is using compare-and-swap (cas
) in a loop until it succeeds. The problem with that implementation is that it's non-deterministic, i.e., we don't know how many calls to our cache store we need in order to get a result.
If you can think of an atomic, deterministic leaky-bucket implementation that works in redis and memcached, I'd welcome this feature.
bucket = LeakyBucket.unserialize(cache.read(key), current_capacity, current_leak) | ||
throttled = bucket.full? | ||
bucket.add(1) unless bucket.full? | ||
store_bucket(key, bucket) if bucket.updated? |
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.
@jeroenvisser101 Since the cached value is read in line 34, and written in line 37, this implementation is not atomic, and can lead to uncounted requests.
Hey @ktheory, you're right, I didn't think about being atomic here. I'll have a look tonight to see if I can make it atomic in a deterministic fashion. Thanks for checking this out! |
So I can do the following:
This will mean that for each client, 2 keys will be stored. I'll also change the TTL logic to be the amount of seconds it takes to drain the full capacity (rather than the current value), since it wouldn't be possible to update that in an atomic way. Update: This won't work, since it's not counting down, let me think some more about this |
Gonna close this out as it's stalled. Feel free to re-open with updates. |
@ktheory yeah, I think this idea is a little dead, I've not been able to come up with a solution that's fully atomic, but it's semi-atomic. If I have time I'll set up a new PR, thanks for all the feedback so far! |
Using the leaky bucket algorithm, you can get a better distrubution that allows a more human behaviour and that should (in theory) decrease false-positives.
It does that because it throttles differently. Let's take the old one, and concider you have a limit of 3 for 5 mins. If the user then did 3 requests, it'll have to wait for the next 5-min slot to do more requests.
Using the leaky bucket algorithm, doing 3 requests adds 3 drops in the bucket, and the bucket is full. The bucket then leaks consistently, and when leaked enough, it'll allow 1 more request (and then it will be full again).
Shopify uses and explains this too here.
We might want to add a custom throttled_response for the leaky bucket algorithm, so that it has the limits in there too.
Fixes #205.