Code Monkey home page Code Monkey logo

ratelimit's Introduction

Ratelimit: Slow your roll

Code Climate Coverage Status

Ratelimit provides a way to rate limit actions across multiple servers using Redis. This is a port of RateLimit.js found here and inspired by this post.

Installation

Add this line to your application's Gemfile:

gem 'ratelimit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install ratelimit

Usage

My example use case is bulk processing data against an external API. This will allow you to limit multiple processes across multiple servers as long as they all use the same Redis database.

Add to the count for a given subject via add with a unique key. I've used the example of a phone number below but anything unique would work (URL, email address, etc.)

You can then fetch the number of executions for given interval in seconds via the count method.

ratelimit = Ratelimit.new("messages")
5.times do
  ratelimit.add(phone_number)
end
ratelimit.count(phone_number, 30)
# => 5

You can check if a given threshold has been exceeded or not. 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)
# => false
ratelimit.within_bounds?(phone_number, threshold: 10, interval: 30)
# => true

You can also pass a block that will only get executed if the given threshold is within bounds. Beware, this code blocks until the block can be run.

ratelimit.exec_within_threshold phone_number, threshold: 10, interval: 30 do
  some_rate_limited_code
  ratelimit.add(phone_number)
end

Documentation

Full documentation can be found here.

Contributing

  1. Fork it ( https://github.com/ejfinneran/ratelimit/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

ratelimit's People

Contributors

alno avatar andreasknoepfle avatar cowlibob avatar dvandersluis avatar ejfinneran avatar jdly avatar jeremywadsack avatar lautis avatar matiasanaya-ffx avatar niels avatar petergoldstein avatar phillipp avatar stephencelis avatar thomas-mcdonald avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ratelimit's Issues

Release new gem version

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.

Interface improvement

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?

Would be cool if `add` could take a number

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)

The count won't be correct when the interval is set close to span

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!

ratelimit.exceeded? checks >= rather than >

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 >=:

return count(subject, options[:interval]) >= options[:threshold]

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.

Working with fractions of seconds

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)

Unicode emojis broken

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?

What do you think about this approach....

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?

Silently fails when the bucket_count < 3

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.

Concurrency problem on exec_within_threshold method

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.

Redis deprected message

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

Screen Shot 2022-12-06 at 4 51 18 PM

Rake Tests hanging

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.

Deprecation warning with new versions of redis

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

Inaccurate rate limit count when wrapping around bucket index

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:

  1. 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.

  2. 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.

Raise error if interval is larger than bucket_span

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 adds 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?

Thread safety

Make ratelimit thread safe. Target VMs are Rubinius and JRuby.

New Mainaintainer?

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.

Wrong count result after long inactivity

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.