sidekiq-throttled

repository·main·Indexed 18 days ago

https://github.com/ixti/sidekiq-throttled

Sidekiq::Throttled provides concurrency and threshold throttling for Sidekiq, allowing developers to limit how many jobs run at once or how many jobs are processed within a specific time window. It includes features such as cooldown mechanisms to prevent constant queue polling, configurable requeue strategies (:enqueue or :schedule), and the ability to share throttling strategies across multiple workers using Sidekiq::Throttled::Registry.

Tokens
5.5K
Snippets
21
Records
26
Agent score
73%

What's inside sidekiq-throttled

  1. Configure Requeue Strategies

    main

    When a job is throttled, it must be requeued. You can choose between two strategies:

    1. :enqueue (Default): Puts jobs immediately back on the queue. This can cause high Redis CPU usage if many jobs are constantly being dequeued and re-enqueued.
    2. :schedule: Schedules the work for the future. This reduces Redis load by delaying the check until the job is likely to be runnable.

    Global Configuration

    You can set a default strategy globally:

    Sidekiq::Throttled.configure do |config|
      config.default_requeue_options = { with: :schedule }
    end

    Per-job Configuration

    You can specify the strategy and even a target queue for a specific job:

    # Using :schedule strategy
    sidekiq_throttle(
      threshold: { limit: 5, period: 1.minute, requeue: { with: :schedule } }
    )
    
    # Using :schedule strategy and moving to a different queue
    sidekiq_throttle(
      threshold: { limit: 5, period: 1.minute, requeue: { to: :other_queue, with: :schedule } }
    )
    sidekiq_throttle(
      threshold: { limit: 5, period: 1.minute, requeue: { with: :schedule } }
    )
  2. Run the Sidekiq::Throttled demo

    main

    To explore the functionality of sidekiq-throttled, you can run the provided demo environment using the following steps:

    1. Install dependencies: Run the setup script to prepare the environment.
    2. Start services: Run the script to start both the Puma web server and the Sidekiq worker process.
    3. Open a console: Start an IRB session to interact with the application.
    4. Trigger demo jobs: Once in the IRB console, execute the perform_bulk commands to enqueue a large number of jobs and observe the throttling behavior.
    bin/setup
    bin/run
    bin/console
  3. Apply throttling to Sidekiq jobs

    main

    To enable throttling, include Sidekiq::Throttled::Job (or the alias Sidekiq::Throttled::Worker) in your job class and use the sidekiq_throttle method to define limits.

    Throttling can be applied via:

    • concurrency: Limits the number of jobs of this class running simultaneously.
    • threshold: Limits the number of jobs processed within a specific time window (period).
    class MyJob
      include Sidekiq::Job
      include Sidekiq::Throttled::Job
    
      sidekiq_options :queue => :my_queue
    
      sidekiq_throttle(
        # Allow maximum 10 concurrent jobs of this class at a time.
        concurrency: { limit: 10 },
        # Allow maximum 1K jobs being processed within one hour window.
        threshold: { limit: 1_000, period: 1.hour }
      )
    
      def perform
        # ...
      end
    end
  4. Implement dynamic throttling with :key_suffix

    main

    You can apply different throttle limits based on job arguments using the :key_suffix option. This allows for per-user or per-resource throttling.

    Per-argument key suffix

    Pass a proc to :key_suffix that receives the job arguments and returns a unique identifier for the throttle bucket.

    sidekiq_throttle(
      concurrency: { limit: 10, key_suffix: -> (user_id) { user_id } }
    )

    Dynamic limits and periods

    You can also pass procs to limit and period to change the throttling constraints dynamically based on job arguments.

    sidekiq_throttle(
      concurrency: {
        limit:      ->(user_id) { User.vip?(user_id) ? 1_000 : 10 },
        key_suffix: ->(user_id) { User.vip?(user_id) ? "vip" : "std" }
      }
    )

    Multiple throttling keys

    You can pass an array of hashes to concurrency or threshold to apply multiple independent throttling rules to a single worker.

    sidekiq_throttle(
      concurrency: [
        { limit: 10, key_suffix: -> (project_id, user_id) { "project_id:#{project_id}" } },
        { limit: 2, key_suffix: -> (project_id, user_id) { "user_id:#{user_id}" } }
      ]
    )

    IMPORTANT: If you use dynamic limit or period values, you must also specify a :key_suffix that returns different values for different groups (e.g., 'vip' vs 'std'), otherwise they will all share the same throttle bucket.

    sidekiq_throttle(
      concurrency: {
        limit:      ->(user_id) { User.vip?(user_id) ? 1_000 : 10 },
        key_suffix: ->(user_id) { User.vip?(user_id) ? "vip" : "std" }
      }
    )
  5. Configure Sidekiq::Throttled in your application

    main

    To use the library, you must require it during your application's bootstrap process (for example, in config/initializers/sidekiq.rb for Rails applications).

    require "sidekiq/throttled"
    require "sidekiq/throttled"
  6. Configure global cooldown settings

    main

    Cooldown settings prevent the system from repeatedly polling a queue that is heavily throttled, reducing Redis load.

    • config.cooldown_period: The period (in seconds) to exclude a queue from polling after it returns a certain amount of throttled jobs in a row. Default is 1.0.
    • config.cooldown_threshold: The number of throttled jobs in a row that triggers the cooldown. Default is 100.
    Sidekiq::Throttled.configure do |config|
      config.cooldown_period = 1.0
      config.cooldown_threshold = 100
    end
    Sidekiq::Throttled.configure do |config|
      config.cooldown_period = 1.0
      config.cooldown_threshold = 100
    end
  7. Share throttling strategies across multiple workers with sidekiq_throttle_as

    main

    If you want multiple different worker classes to share the same throttling pool (e.g., to respect a single global API rate limit), use sidekiq_throttle_as.

    First, register a named strategy using Sidekiq::Throttled::Registry.add. Then, in your worker classes, call sidekiq_throttle_as with that name. This ensures that the sum of all jobs using that strategy does not exceed the defined limits.

    Example: If a strategy allows 10 concurrent jobs and you have two workers using it, the total number of concurrent jobs across both workers will not exceed 10.

    # 1. Create a shared strategy in the registry
    Sidekiq::Throttled::Registry.add(:google_api, {
      :threshold => { :limit => 123, :period => 1.hour },
      :concurrency => { :limit => 10 }
    })
    
    # 2. Assign workers to that shared strategy
    class FetchProfileJob
      include Sidekiq::Job
      include Sidekiq::Throttled::Job
      sidekiq_throttle_as :google_api
    end
    
    class FetchCommentsJob
      include Sidekiq::Job
      include Sidekiq::Throttled::Job
      sidekiq_throttle_as :google_api
    end
  8. Configure dynamic Threshold limits and periods

    main

    The Threshold strategy supports dynamic configuration. If you pass a Proc for the limit or period parameters, the strategy will call that Proc with the job arguments to determine the specific threshold for that execution.

    If any of the following are provided, the strategy is considered dynamic?:

    • A key_suffix Proc
    • A limit that responds to #call
    • A period that responds to #call
  9. How throttling strategies handle requeueing

    main

    A Strategy in sidekiq-throttled is a meta-strategy that can combine Concurrency (limiting simultaneous jobs) and Threshold (limiting jobs within a time window) constraints.

    When a job is identified as throttled?, the requeue_throttled method is invoked. The behavior depends on the :with option:

    1. :enqueue: Uses re_enqueue_throttled. For standard Sidekiq, this performs an LPUSH to the target queue. If using Sidekiq::Pro::SuperFetch, it uses the UnitOfWork to requeue the job to the head of the target queue.
    2. :schedule: Uses reschedule_throttled. This treats the job as a new unit of work by calling Sidekiq::Client.enqueue_to_in. It calculates a retry_in interval by taking the maximum of the intervals suggested by the active concurrency and threshold strategies, then adding a random jitter (up to 20% of the interval if the interval is > 10 seconds).
  10. Use the Threshold throttling strategy

    main

    The Threshold strategy limits the number of jobs allowed within a specific time period (e.g., 'maximum 1K jobs per hour'). It uses a sliding window approach implemented via a Lua script in Redis to ensure atomicity.

    When configuring this strategy, you can provide:

    • limit: The maximum number of allowed jobs.
    • period: The time window in seconds.
    • key_suffix: An optional Proc to generate dynamic keys, allowing different limits for different job arguments.

    Both limit and period can be passed as a Proc to enable dynamic configuration based on the job arguments.

    # Example conceptual usage (implementation details vary by how you register the strategy)
    Sidekiq::Throttled::Strategy::Threshold.new(
      "my_api_limit",
      limit: 1000,
      period: 3600,
      key_suffix: ->(arg1, arg2) { "#{arg1}" }
    )