Project

restrainer

0.01
Low commit activity in last 3 years
A long-lived project that still receives updates
Code for throttling workloads so as not to overwhelm external services
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

>= 0
 Project Readme

Continuous Integration Regression Test Ruby Style Guide Gem Version

This gem provides a method of throttling calls across processes that can be very useful if you have to call an external service with limited resources.

A redis server is required to use this gem.

Usage

This code will throttle all calls to the mythical MyService so that no more than 100 calls are ever being made at a single time across all application processes.

restrainer = Restrainer.new(:my_service, limit: 100)
restrainer.throttle do
  MyServiceClient.call
end

If the throttle is already full, the block will not be run and a Restrainer::ThrottledError will be raised.

You can also override the limit in the throttle method. Setting a limit of zero will disable processing entirely. Setting a limit less than zero will remove the limit. Note that the limit set in the throttle is not shared with other processes, but the count of the number of processes is shared. Thus it is possible to have the throttle allow one process but reject another if the limits are different.

You can also manually lock and release processes using the lock and release methods if your logic needs to break out of a block.

  process_id = restrainer.lock!
  begin
    # Do something
  ensure
    restrainer.release!(process_id)
  end

If you already hava a unique identifier, you can pass it in to the lock! method. This can be useful if the calls to lock! and release! are in different parts of the code but have access to the same common identifier. Identifiers are unique per throttle name, so you can use something as simple as database row id.

Instances of Restrainer do not use any internal state to keep track of the number of running processes. All of that information is maintained in redis. Therefore you don't need to worry about maintaining references to Restrainer instances and you can create them as needed as long as they are named consistently. You can create multiple Restrainers for different uses in your application by simply giving them different names.

Configuration

To set the redis connection used by for the gem you can either specify a block that yields a Redis object (from the redis gem) or you can explicitly set the attribute. The block form is generally preferred since it can work with connection pools, etc.

Restrainer.redis{ connection_pool.redis }

Restrainer.redis = redis_client

You can also pass in a Redis instance in the constructor.

restrainer = Restrainer.new(limit: 5, redis: my_redis)

Internals

To protect against situations where a process is killed without a chance to cleanup after itself (i.e. kill -9), each process is only tracked for a limited amount of time (one minute by default). After this time, the Restrainer will assume that the process has been orphaned and removes it from the list.

The timeout can be set by the timeout option on the constructor. If you have any timeouts set on the services being called in the block, you should set the Restrainer timeout to a slightly higher value.

restrainer = Restrainer.new(:my_service, 100, timeout: 10)

This gem does clean up after itself nicely, so that it won't ever leave unused data lying around in redis.

Installation

Add this line to your application's Gemfile:

gem 'restrainer'

And then execute:

$ bundle

Or install it yourself as:

$ gem install restrainer

Contributing

Open a pull request on GitHub.

Please use the standardrb syntax and lint your code with standardrb --fix before submitting.

License

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