Coder Social home page Coder Social logo

sidekiq-throttler's Introduction

Sidekiq::Throttler

Build Status Dependency Status Gem Version

Sidekiq::Throttler is a middleware for Sidekiq that adds the ability to rate limit job execution on a per-worker basis.

Compatibility

Sidekiq::Throttler supports Sidekiq versions 2 and 3 and is actively tested against Ruby versions 2.0.0, 2.1, and 2.2.

Installation

Add this line to your application's Gemfile:

gem 'sidekiq-throttler'

And then execute:

$ bundle

Or install it yourself as:

$ gem install sidekiq-throttler

Configuration

In a Rails initializer or wherever you've configured Sidekiq, add Sidekiq::Throttler to your server middleware:

Sidekiq.configure_server do |config|
  config.server_middleware do |chain|
    chain.add Sidekiq::Throttler
  end
end

Sidekiq::Throttler defaults to in-memory storage of job execution times. If you have multiple worker processes, or frequently restart your processes, this will be unreliable. Instead, specify the :redis storage option:

Sidekiq.configure_server do |config|
  config.server_middleware do |chain|
    chain.add Sidekiq::Throttler, storage: :redis
  end
end

Basic Usage

In a worker, specify a threshold (maximum jobs) and period for throttling:

class MyWorker
  include Sidekiq::Worker

  sidekiq_options throttle: { threshold: 50, period: 1.hour }

  def perform(user_id)
    # Do some heavy API interactions.
  end
end

In the above example, when the number of executed jobs for the worker exceeds 50 in an hour, remaining jobs will be delayed.

Advanced Usage

Custom Keys

By default, each worker has its own key for throttling. For example:

class FooWorker
  include Sidekiq::Worker

  sidekiq_options throttle: { threshold: 50, period: 1.hour }

  # ...
end

class BarWorker
  include Sidekiq::Worker

  sidekiq_options throttle: { threshold: 50, period: 1.hour }

  # ...
end

Even though FooWorker and BarWorker use the same throttle options, they are treated as different groups. To have multiple workers with shared throttling, the :key options can be used:

sidekiq_options throttle: { threshold: 50, period: 1.hour, key: 'foobar' }

Any jobs using the same key, regardless of the worker will be tracked under the same conditions.

Dynamic Throttling

Each option (:threshold, :period, and :key) accepts a static value but can also accept a Proc that's called each time a job is processed.

Dynamic Keys

If throttling is per-user, for example, you can specify a Proc for key which accepts the arguments passed to your worker's perform method:

sidekiq_options throttle: { threshold: 20, period: 1.day, key: ->(user_id){ user_id } }

In the above example, jobs are throttled for each user when they exceed 20 in a day.

Dynamic Thresholds

Thresholds can be configured based on the arguments passed to your worker's perform method, similar to how the key option works:

sidekiq_options throttle: { threshold: ->(user_id, rate_limit) { rate_limit }, period: 1.hour, key: ->(user_id, rate_limit){ user_id } }

In the above example, jobs are throttled for each user when they exceed the rate limit provided in the message. This is useful in cases where each user may have a different rate limit (ex: interacting with external APIs)

Dynamic Periods

In this contrived example, our worker is limited to 9 thousand jobs every 10 minutes. However, on Tuesdays limit jobs to 9 thousand every 15 minutes:

sidekiq_options throttle: { threshold: 9000, period: ->{ Date.today.tuesday? ? 15.minutes : 10.minutes } }

Contributing

  1. Fork it
  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 new Pull Request

License

MIT Licensed. See LICENSE.txt for details.

sidekiq-throttler's People

Contributors

bpinto avatar gevans avatar jwoertink avatar lsimoneau avatar mtparet avatar sferik avatar worst 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

sidekiq-throttler's Issues

Using the throttler outside of sidekiq

How can I use the throttling mechanism outside sidekiq?

for instance if I have a worker which has throttling key "facebook:#{user.facebook_id}", and the user sometimes has sporadic facebook api access that are not in a worker, I want to add these calls effect to the workers throttling.

So If I know that the sidekiq-throttler key is "throttled:facebook:some_id" which points to a list of times (I think), What do I need to do? (not in the redis way, like do I need to maintain it somehow myself? or do I just add a member to the list with the current time? do I need to expire members?)

License missing from gemspec

RubyGems.org doesn't report a license for your gem. This is because it is not specified in the gemspec of your last release.

via e.g.

spec.license = 'MIT'
# or
spec.licenses = ['MIT', 'GPL-2']

Including a license in your gemspec is an easy way for rubygems.org and other tools to check how your gem is licensed. As you can imagine, scanning your repository for a LICENSE file or parsing the README, and then attempting to identify the license or licenses is much more difficult and more error prone. So, even for projects that already specify a license, including a license in your gemspec is a good practice. See, for example, how rubygems.org uses the gemspec to display the rails gem license.

There is even a License Finder gem to help companies/individuals ensure all gems they use meet their licensing needs. This tool depends on license information being available in the gemspec. This is an important enough issue that even Bundler now generates gems with a default 'MIT' license.

I hope you'll consider specifying a license in your gemspec. If not, please just close the issue with a nice message. In either case, I'll follow up. Thanks for your time!

Appendix:

If you need help choosing a license (sorry, I haven't checked your readme or looked for a license file), GitHub has created a license picker tool. Code without a license specified defaults to 'All rights reserved'-- denying others all rights to use of the code.
Here's a list of the license names I've found and their frequencies

p.s. In case you're wondering how I found you and why I made this issue, it's because I'm collecting stats on gems (I was originally looking for download data) and decided to collect license metadata,too, and make issues for gemspecs not specifying a license as a public service :). See the previous link or my blog post about this project for more information.

Throttling queues

It would be great if users could throttle an entire queue, regardless of which worker was running on it. In my case, I have ~15 job types that all need to respect the same rate limit.

Version 0.3 not working

➜ app ✗ rails s
/opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sidekiq-throttler-0.3.0/lib/sidekiq/throttler/storage/memory.rb:7:in `<class:Memory>': uninitialized constant Sidekiq::Throttler::Storage::Memory::Singleton (NameError)
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sidekiq-throttler-0.3.0/lib/sidekiq/throttler/storage/memory.rb:6:in `<module:Storage>'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sidekiq-throttler-0.3.0/lib/sidekiq/throttler/storage/memory.rb:3:in `<class:Throttler>'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sidekiq-throttler-0.3.0/lib/sidekiq/throttler/storage/memory.rb:2:in `<module:Sidekiq>'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sidekiq-throttler-0.3.0/lib/sidekiq/throttler/storage/memory.rb:1:in `<top (required)>'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sidekiq-throttler-0.3.0/lib/sidekiq/throttler.rb:7:in `require'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sidekiq-throttler-0.3.0/lib/sidekiq/throttler.rb:7:in `<top (required)>'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sidekiq-throttler-0.3.0/lib/sidekiq-throttler.rb:1:in `require'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sidekiq-throttler-0.3.0/lib/sidekiq-throttler.rb:1:in `<top (required)>'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/bundler-1.3.5/lib/bundler/runtime.rb:72:in `require'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/bundler-1.3.5/lib/bundler/runtime.rb:72:in `block (2 levels) in require'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/bundler-1.3.5/lib/bundler/runtime.rb:70:in `each'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/bundler-1.3.5/lib/bundler/runtime.rb:70:in `block in require'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/bundler-1.3.5/lib/bundler/runtime.rb:59:in `each'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/bundler-1.3.5/lib/bundler/runtime.rb:59:in `require'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/bundler-1.3.5/lib/bundler.rb:132:in `require'
    from /Users/bpinto/src/app/config/application.rb:7:in `<top (required)>'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/railties-3.2.13/lib/rails/commands.rb:53:in `require'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/railties-3.2.13/lib/rails/commands.rb:53:in `block in <top (required)>'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/railties-3.2.13/lib/rails/commands.rb:50:in `tap'
    from /opt/boxen/rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/railties-3.2.13/lib/rails/commands.rb:50:in `<top (required)>'
    from script/rails:6:in `require'
    from script/rails:6:in `<main>'

Why `Thread.exclusive` ?

Hi,

I looked through the code and I wonder why do you use Thread.exclusive when dealing with the counter?

My first concern is that Redis itself has awesome atomic operations like INCR, SETNX and others. Calls to Redis does not require any critical section.

My second concern is that if Thread.exclusive was added for the sake of memory storage, then how do you share limit counter between processes?

Thanks,
Daniel.

Logical scheduling so massive queues are possible.

As I understand, the way this is working right now: when the threshold is reached, it schedules the jobs for the period from now. When the period cycles, the scheduled jobs drop in, and it all repeats. If I'm wrong about this, please correct me.

The issue I'm having with this is when I want to queue up a massive number of jobs (say 50,000) with the threshold of 50 and a period of 1 minute.

So what happens is every 1 minute just under 50,000 jobs have to get processed. This doesn't scale well.

How I would love to see it work is to smartly delay the items. For example, the first 50 can process now, the next 50 in 1 minute from now. The next 50 in 2 minutes, etc.

This will require some tracking of the current queue so that after all ~50,000 items are scheduled (for as far out as ~1000 minutes from now) it can logically add future items. So in a few hours from now, if I add more items, it should automatically figure out where the end of the queue is time-wise and schedule the new items to be completed at the end of the queue.

Is there any plans to change the functionality to work this way? If not, I might spend some time and try to hack something out.

Persist job execution counters in Redis.

Currently, the tracking of job executions is stored in memory. It works fine with a single worker and many threads but won't scale well if you're running more than that. Also, restarting Sidekiq will reset the counters.

Multiple limits

Is it possible to have a per minute limit and a per day limit on the same job?

Sidekiq 3.0 compatibility?

Hi, Mike is working on releasing Sidekiq 3.0 soon-- he's asking people to test out the current sidekiq master branch but hasn't cut a release yet.

I tried out the master branch with sidekiq-throttler and the sidekiq-throttler tests passed! I also took a quick look through the code and the APIs that you're using, get_sidekiq_options and Sidekiq.redis, don't appear to have changed. Is there anything else that I might have missed that I could research or try in order to be helpful?

If there's nothing I've missed, the only thing that might need to be changed to be compatible is loosening the dependency!

Wrong queues

Jobs that has been throttled/enqueued seems to get enqueued back into the "default" queue no matter what queue they came from originally

key isn't listed in sidekiq

I have this line in one of the workers

    sidekiq_options throttle: { threshold: 500,
                                 period: 600.seconds, 
                                 key: ->(user_id){ "fb:#{user_id}"}  }

but when I schedule a job, in side the extra info is written as

{"throttle"=>{"threshold"=>500, "period"=>600, "key"=>"#"}}

Can I use multiple thresholds in the same worker?

For example, Amazon SES initially limits sends to 5 per second, 10000 per day. Can I add multiple thresholds like so:

sidekiq_options throttle: { threshold: 5, period: 1.second }
sidekiq_options throttle: { threshold: 10000, period: 24.hours }

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.