A Ruby code block wrapper to help prevent race conditions across multiple threads and even separate servers.
Disclaimer This code has been used in production for several years now without incident, but I make no guarantee about the thread-safeness it has. Use at your own risk.
Multiple servers all have the same cron jobs, but we only want one of them to actually execute the job.
Since all the cron jobs should fire at roughly the same time, we can have each one of them generate a unique identifier and "claim" the cron job. We wait 0.5 seconds (or sleep_delay
) for all of the servers to generate a unique identifier and claim the job by writing their unique identifier to a Redis cache. After the 0.5 seconds, each server checks the Redis cache to see if the stored value matches the one they generated. Only one server will still have a match and successfully claim the job to execute.
Add this line to your application's Gemfile:
gem 'race_block'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install race_block
- Ruby >= 2.5
- A Redis server
The original inspiration behind this gem was running cron jobs across multiple servers to send emails. Since all of the servers would trigger the cron job simultaneously, they would all attempt to send the same email at the same time leading to multiple copies of a single email being sent out. RaceBlock
solved this problem by ensuring only one instance of RaceBlock
across all the servers was allowed to work on a single email.
RaceBlock
could also apply to any cron jobs or block of code that really should only be run once, regardless of which servers tried to initiate it.
Before calling RaceBlock.start
you'll need to configure your own redis client in RaceBlock.config
RaceBlock.config do |config|
config.redis = Redis.new
end
# or...
RaceBlock.config.redis = Redis.new
Any code that you want to be "thread-safe" and ensure is only executing in one location should be placed in a RaceBlock
with a unique identifying key that will be checked across all instances.
RaceBlock.start('unique_key', **kwargs) do
# Insert code that should only be executed once at a time here...
end
Option | Default | Description |
---|---|---|
sleep_delay | 0.5 | How many seconds the RaceBlock should wait after generating its unique token before it checks if it can execute the RaceBlock. Important This value should be longer than the amount of time it takes your server to write to the Redis database. 0.5 seconds has worked for us, but longer sleep_delay values should technically be safer. |
expire | 60 | How many seconds the key should be stored for while running. This number should be longer than the length of time the RaceBlock will take to execute once it starts. The key will be deleted 3 seconds after the block completes, regardless of the time left from expire . |
expiration_delay | 3 | How long after block completion before the key is deleted. Setting to 0 will delete it instanttly upon block completion. |
desync_tokens | 0 | TESTING ONLY This is purely for testing purposes to simulate inconsistent request times. It should never be used in a production environment. |
You can change the default values using RaceBlock.config
, otherwise they can be overridden on a per call basis.
RaceBlock.config do |config|
config.sleep_delay = 1.5
config.expire = 14
config.expiration_delay = 4
end
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/joeyparis/race_block. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
I am also interested in porting this project to multiple languages and would love help in the languages I am less familiar with.
- Ruby
- JavaScript
- Python
- Go
- LolCode
- ..?
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the RaceBlock project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.