ejfinneran / ratelimit Goto Github PK
View Code? Open in Web Editor NEWA Redis-backed rate limiter written in Ruby
License: MIT License
A Redis-backed rate limiter written in Ruby
License: MIT License
I'm using rate limit to limit the requests to onesignal.
When i use the method exec_within_threshold passing the create_notification inside the block.
The unicode emojis sent to onesignal got broken.
I tried to figured out on the gem why this was happening, and didn't find anything that could point me to the problem.
Is there some json parsing during the process?
Instead of multiple buckets, it uses a single integer in redis.
http://stackoverflow.com/a/8857962/76486
Keeps Time.now as an integer minus some buffer into the past. Each time the action is performed it increments the timestamp and if it becomes > Time.now the action is denied. After waiting for some time to pass the action is allowed again.
It seems more efficient, but maybe I'm missing something?
I've been wondering... Why not allow receive some configurations on initialize method?
Today:
def with_ratelimit(&block)
ratelimit = Ratelimit.new("whatsapp", { bucket_interval: BUCKET_INTERVAL })
ratelimit.exec_within_threshold('send_message', threshold: THRESHOLD, interval: INTERVAL) do
ratelimit.add('send_message')
yield
end
end
Desire:
def with_ratelimit(&block)
opts = { bucket_interval: BUCKET_INTERVAL, threshold: THRESHOLD, interval: INTERVAL}
Ratelimit.new("whatsapp::send_message", opts).exec_within_threshold do
# doesnt need to use add method anymore
yield
end
end
I can implement this modification if you agree, what do you think?
Is anyone interesting in taking over maintaining this gem? I'm not writing Ruby these days and don't have much time to invest in this.
Hi, I have read the source code and found out that it deletes the field in bucket + 1, and bucket + 2. Would this cause any issue when we set the interval to the bucket_span?
ratelimit = Ratelimit.new("messages", bucket_span : 60, bucket_interval : 20 )
5.times do
ratelimit.add(phone_number)
end
ratelimit.count(phone_number, 60)
Using the example in the document for example and set the span to 60 interval to 20, if we set add the phone number in the first 20 second and add the phone number in the next 20 sec and so on, the count will always be 5.
Thanks!
I recently found that we had a lot of places where bucket_span
did not span the whole interval we checked when using count
or exceeded?
, like when checking a rate limit for the last 24 hours without modifying bucket_span
in the initualizer.
The failure is silent and I could imagine that a lot of tests would not catch this, if the add
s are not wrapped in timecop etc.
I'd suggest that when the count
method receives a options[:interval]
larger than options[:bucket_span]
an error is raised, so the problem is uncovered.
Does that make any sense?
The last release is over 2 years old, although there are nice things in master such as incrementing counters with other values than 1.
Could you release a new version of the gem as things seem quite stable? I'd say labelling this 1.0.0 wouldn't be out of the question.
I seted my threshold: 1
and interval: 60
and I got a concurrency problem on exec_within_threshold method when I start 5 sidekiq processes:
(INFO) time=2018-04-24 15:08:25 -0300 Msg=Checking contact for {:payload=>{:users=>["+5511958122379"]}}.
(INFO) time=2018-04-24 15:09:26 -0300 Msg=Checking contact for {:payload=>{:users=>["+5511941016555"]}}.
(INFO) time=2018-04-24 15:10:27 -0300 Msg=Checking contact for {:payload=>{:users=>
(INFO) time=2018-04-24 15:11:27 -0300 Msg=Checking contact for {:payload=>{:users=>["+5519983014013"]}}.
(INFO) time=2018-04-24 15:11:27 -0300 Msg=Checking contact for {:payload=>{:users=>["+5511941016555"]}}.
(INFO) time=2018-04-24 15:11:27 -0300 Msg=Checking contact for {:payload=>{:users=>["+5511958122379"]}}.
(INFO) time=2018-04-24 15:12:28 -0300 Msg=Checking contact for {:payload=>{:users=>["+5519983014013"]}}.
(INFO) time=2018-04-24 15:13:28 -0300 Msg=Checking contact for {:payload=>{:users=>["+5511941016555"]}}.
my code is:
ratelimit.exec_within_threshold('check_contacts', threshold: 1, interval: 60) do
ratelimit.add('check_contacts')
logger.info { "Checking contact for #{@body}." }
### My code here
end
And executed:
[1259, 1258, 1257, 1258, 1257].each { |p| MyWorker.perform_async(p) }
It seems that process read my redis at same time and execute ignoring rate limit configuration.
Thanks for the great software. Question:
Does this library support fractions of seconds? I am interested in the following:
r.exceeded?(foo, threshold: 1, interval: 0.1)
Consider following test:
should "be able to add to the count for a given subject" do
@r.add("value1")
@r.add("value1")
assert_equal 2, @r.count("value1", 1)
assert_equal 0, @r.count("value2", 1)
Timecop.travel(600) do
assert_equal 0, @r.count("value1", 1)
end
end
It fails.
After 600 seconds of inactivity bucket wasn't expired yet, but it's index is the same again, so it return wrong result in count.
Although it's rare case, it's possible to produce wrong results. Also, probability of error increases when increasing interval in count.
One of possible solutions may be restricting bucket_expire to be no more than bucket_span minus maximum interval which may be requested in count.
The current implementation of the Ratelimit
class uses a fixed number of buckets (bucket_count
) to store rate-limiting data. When the bucket index wraps around, it may include old values in the count, leading to inaccurate rate limiting.
This issue occurs when there is no consistent adding to the counter. If the add
method is not called for an extended period, the buckets for bucket + 1
and bucket + 2
are not deleted. As a result, when the bucket index wraps around, old values in these buckets are still present and are included in the count, causing unexpected behavior.
Proposed Solutions:
Modify the count
method to check if the queried buckets are expired based on their timestamp. Store a timestamp for each bucket when it is updated and compare it with the current time when fetching the count. This ensures that only unexpired bucket values are included in the count, even if the add
method is not called consistently.
Update the get_bucket
method to use the timestamp directly instead of mapping it to a range of 0 to bucket_count
. By doing so, you can avoid wrapping the bucket index and use a more straightforward approach to manage and remove old timestamp values. Modify the add
and count
methods accordingly to handle the new bucket indexing method and periodically remove expired keys from the Redis hashes.
These solutions aim to address the issue of inaccurate rate limiting due to wrapping around the bucket index and ensure consistent rate limiting even when the add
method is not called regularly.
My suggestion would be to use 2. It would use longer keys in the hash, but would not add more keys. The pruning of old timestamps could by done in a redis lua script if necessary.
When testing code that had ratelimit implemented, it will sometimes hang with on this code as it thinks the subject has exceeded its amount. User testing it works fine, anyone else having issues with this?
ratelimit.exec_within_threshold "shopify_api", threshold: 2, interval: 1 do
I am trying to respect shopify's rate limit.
The use case I'm thinking of is max amount of money a user can send in a day. I want to rate limit the total amount sent, not the number of transactions.
So if I wanted to limit it to $500 per day, they could either send 1 tx for $500, or 500 for $1.
Was thinking something like
ratelimit.add(transaction, 45)
I think the limiter uses expire incorrectly on the Redis hashes. In my observation, the limiter works for some time but when it wraps around the bucket_span, it blocks until the expire takes place on the whole limiter hash.
Here's a similar implementation that operates on invidual keys:
https://gist.github.com/ncr/c2b1b62a8d2b636daca1dff12b457a31
In the add
function, the next 2 buckets are deleted so when there are < 3 buckets, add
does nothing since the bucket that was added to is immediately deleted.
The initialize function should probably check for this condition and raise the appropriate error.
Make ratelimit thread safe. Target VMs are Rubinius and JRuby.
The count
method (changed in 1.1.0) is no longer returning (or sometimes may return) a nil
, while previously an integer was always returned.
I got the following message, also I see you do it the fix, but no release it or for some reason this did not stay in last version
`Redis.current` is deprecated and will be removed in 5.0. (called from: /xxxxx/xxxx/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/graph_attack-2.1.0/lib/graph_attack/rate_limit.rb:53:in `redis_client')
Pipelining commands on a Redis instance is deprecated and will be removed in Redis 5.0.0.
redis.multi do
redis.get("key")
end
should be replaced by
redis.multi do |pipeline|
pipeline.get("key")
end
Also, I see something fails in CI
After updating redis-rb
in our project we received the following deprecation warning:
Pipelining commands on a Redis instance is deprecated and will be removed in Redis 5.0.0.
redis.multi do
redis.get("key")
end
should be replaced by
redis.multi do |pipeline|
pipeline.get("key")
end
I noticed that the call comes from this library.
I would drop a quick PR for updating this code path if that is all right :)
Cheers ๐
Andi
This is not a bug per se, but the behavior is unexpected; exceeded?
returns true AT the threshold in addition to over it, which is unintuitive. This is reinforced by the language in the readme, which states that "the following code checks if the currently rate is over 10 executions in the last 30 seconds or not. ratelimit.exceeded?(phone_number, threshold: 10, interval: 30)
"; in reality, the code checks if the rate is over 9 executions / equal-to-or-over 10 executions in the last 30 seconds.
The consequences of this depend on where .add()
is called, but I personally feel like this line should be > rather than >=:
Line 75 in 177b1bf
RateLimit.js does not implement exceeded, but the example linked from your readme also suggests > over >=: https://gist.github.com/chriso/54dd46b03155fcf555adccea822193da#get-the-code
I can work around this in my implementation, but it thought this was worth mentioning. Otherwise, thanks for the great gem.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.