seibii / speed_limiter

Limit the frequency of execution across multiple threads and processes.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

lint test Gem Version MIT License

SpeedLimiter

SpeedLimiter is a gem that limits the number of executions per unit of time. By default, it achieves throttling through sleep.

You can also use the on_throttled event to raise an exception instead of sleeping, or to re-enqueue the task.

Installation

Add this line to your application's Gemfile:

gem 'speed_limiter'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install speed_limiter

Usage

Limit the number of executions to 10 times per second.

SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1) do |state|
  puts state #=> <SpeedLimiter::State key=server_name/method_name count=1 ttl=0>
  http.get(path)
end

# or

throttle_limit_10_par_sec = SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1)

throttle_limit_10_par_sec.call do |state|
  puts state #=> <SpeedLimiter::State key=server_name/method_name count=1 ttl=0>
  http.get(path)
end

It returns the result of the block execution.

result = SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1) do
  http.get(path)
end
puts result.code #=> 200

on_throttled option

Specify the process when the limit is exceeded.

on_throttled = proc { |state| logger.info("limit exceeded #{state.key} #{state.ttl}") }
SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1, on_throttled: on_throttled) do
  http.get(path)
end

raise_on_throttled option

It raises an exception when the limit is exceeded.

begin
  SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1, raise_on_throttled: true) do
    http.get(path)
  end
rescue SpeedLimiter::ThrottledError => e
  logger.info(e.message) #=> "server_name/method_name rate limit exceeded. Retry after 0.9 seconds. limit=10, count=11, period=1"
  e.state #=> <SpeedLimiter::State key=server_name/method_name count=11 ttl=0.9>
end

Reinitialize the queue instead of sleeping when the limit is reached in ActiveJob.

class CreateSlackChannelJob < ApplicationJob
  rescue_from(SpeedLimiter::ThrottledError) do |e|
    Rails.logger.warn("[#{e.class}] #{self.class} retry job. #{e.message}")
    retry_job(wait: e.ttl, queue: 'low')
  end

  def perform(*args)
    SpeedLimiter.throttle("slack", limit: 20, period: 1.minute, raise_on_throttled: true) do
      create_slack_channel(*args)
    end
  end
end

retry option

To use the retry: option, you need to introduce the Retryable gem. By specifying the options of the Retryable gem, you can retry when an exception occurs.

# Gemfile
gem 'retryable'
SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1, retry: { tries: 3, on: OpenURI::HTTPError }) do
  http.get(path)
end

# equivalent to
SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1) do
  Retryable.retryable(tries: 3, on: OpenURI::HTTPError) do
    http.get(path)
  end
end

retry: true or retry: {} is default use of Retryable.configure.

SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1, retry: true) do
  http.get(path)
end

Configuration

Redis configuration

Redis can be specified as follows

  1. default url redis://localhost:6379/0
  2. SPEED_LIMITER_REDIS_URL environment variable
  3. Configure SpeedLimiter.configure
# config/initializers/speed_limiter.rb
SpeedLimiter.configure do |config|
  config.redis_url = ENV.fetch('SPEED_LIMITER_REDIS_URL', 'redis://localhost:6379/2')
end

or Use Redis instance

# config/initializers/speed_limiter.rb
SpeedLimiter.configure do |config|
  config.redis = Redis.new(host: 'localhost', port: 6379, db: 2)
end

Other configuration defaults

SpeedLimiter.configure do |config|
   config.redis_url = ENV.fetch("SPEED_LIMITER_REDIS_URL", "redis://localhost:6379/0")
   config.redis = nil
   config.no_limit = false # If true, it will not be throttled
   config.prefix = "speed_limiter" # Redis key prefix
   config.on_throttled = nil # Proc to be executed when the limit is exceeded
end

Example

If you do not want to impose a limit in the test environment, please set it as follows.

# spec/support/speed_limiter.rb
RSpec.configure do |config|
  config.before(:suite) do
    SpeedLimiter.configure do |config|
      config.no_limit = true
    end
  end
end

If you want to detect the limit in the test environment, please set it as follows.

Rspec.describe do
  around do |example|
    SpeedLimiter.config.on_throttled = proc { |state| raise "limit exceeded #{state.key} #{state.ttl}" }

    example.run

    SpeedLimiter.config.on_throttled = nil
  end

  it do
    expect { over_limit_method }.to raise_error('limit exceeded key_name [\d.]+')
  end
end

Compatibility

SpeedLimiter officially supports the following Ruby implementations and Redis :

  • Ruby MRI 3.0, 3.1, 3.2
  • Redis 5.0, 6.0, 6.2, 7.0, 7.2

Development

After checking out the repo, run bin/setup to install dependencies. Please run rspec referring to the following.

Before committing, run bundle exec rubocop to perform a style check. You can also run bin/console for an interactive prompt that will allow you to experiment.

rspec

Start a test web server and Redis for testing with the following command.

$ rake throttle_server:start_daemon # or rake throttle_server:start
$ docker compose up -d

After that, please run the test with the following command.

$ bundle exec rspec -fd

Contribution

  1. Fork it ( https://github.com/seibii/speed_limiter/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 new Pull Request

License

The gem is available as open source under the terms of the MIT License.

About

Limit the frequency of execution across multiple threads and processes.

License:MIT License


Languages

Language:Ruby 99.5%Language:Shell 0.5%