lib/race_block.rb in race_block-0.1.0 vs lib/race_block.rb in race_block-0.2.0
- old
+ new
@@ -1,29 +1,45 @@
# frozen_string_literal: true
require "securerandom"
require "logger"
-require "redis"
+
require_relative "race_block/version"
# Block for preventing race conditions across multiple threads and instances
module RaceBlock
class Error < StandardError; end
- def self.client(reload: false)
- if @redis.nil? || reload
- @redis = Redis.new(host: ENV["REDIS_HOST"], port: ENV["REDIS_PORT"])
+ # For managing RaceBlock current configuration settings
+ class Configuration
+ attr_accessor :redis, :expire, :expiration_delay, :sleep_delay
- begin
- @redis.ping
- rescue Redis::CannotConnectError => e
- RaceBlock.logger.error e
- end
+ def initialize
+ reset
end
- @redis
+
+ def reset
+ @expire = 60
+ @expiration_delay = 3
+ @sleep_delay = 0.5
+ end
end
+ class << self
+ attr_accessor :configuration
+ end
+
+ def self.config
+ self.configuration ||= Configuration.new
+ yield(configuration) if block_given?
+ configuration
+ end
+
+ def self.client
+ config.redis
+ end
+
def self.logger
@logger ||= Logger.new($stdout)
end
def self.key(key)
@@ -32,56 +48,58 @@
def self.reset(key)
RaceBlock.client.del(RaceBlock.key(key))
end
- def self.start(key, sleep_delay: 0.5, expire: 60, expire_immediately: false, debug: false, desync_tokens: false)
- raise("A key must be provided to start a RaceBlock") if key.nil? || key.empty?
+ def self.start(key, expire: config.expire, expiration_delay: config.expiration_delay, **args)
+ raise("A key must be provided to start a RaceBlock") if key.empty?
@key = RaceBlock.key(key)
# Set an expiration for the token if the key is defined but doesn't
# have an expiration set (happens sometimes if a thread dies early).
# `-1` means the key is set but does not expire, `-2` means the key is
# not set
RaceBlock.client.expire(@key, 10) if RaceBlock.client.ttl(@key) == -1
- if !RaceBlock.client.get(@key)
- sleep rand(0.0..sleep_delay) if desync_tokens && ENV["RACK_ENV"] == "test"
- token = SecureRandom.hex
- RaceBlock.client.set(@key, token)
- RaceBlock.client.expire(@key, [15, sleep_delay].max)
- sleep sleep_delay
- # Okay, so I feel like this is pseudo science, but whatever. Our
- # race condition comes from when the same cron job is called by
- # several different server instances at the same time
- # (theoretically) all within the same second (much less really).
- # By waiting a second we can let all the same cron jobs that were
- # called at roughly the exact same time finish their write to the
- # redis cache so that by the time the sleep is over, only one
- # token is still accurate. I'm hesitant to believe this actually
- # works, but I can't find any flaws in the logic at the current
- # moment, and I also believe this is what is keep the EmailQueue
- # stable which seems to have no duplicate sending problems.
- if RaceBlock.client.get(@key) == token
- RaceBlock.client.expire(@key, expire.is_a?(Integer) ? expire : 60)
- logger.info("Running block") if debug
+ # Token already exists
+ return logger.debug("Token already exists") if RaceBlock.client.get(@key)
- r = yield
+ return unless set_token_and_wait(@key, **args)
- # I have lots of internal debates on whether I should full
- # delete the key here or still let it sit for a few seconds
- RaceBlock.client.expire(@key, desync_tokens && ENV["RACK_ENV"] == "test" ? 10 : 3)
+ RaceBlock.client.expire(@key, expire)
+ logger.debug("Running block")
- RaceBlock.client.del(@key) if expire_immediately
+ r = yield
- r
- elsif debug
- logger.info("Token out of sync")
- end
+ # I have lots of internal debates on whether I should full
+ # delete the key here or still let it sit for a few seconds
+ RaceBlock.client.expire(@key, expiration_delay)
+
+ r
+ end
+
+ def self.set_token_and_wait(key, sleep_delay: config.sleep_delay, desync_tokens: 0)
+ sleep desync_tokens # Used for testing only
+ token = SecureRandom.hex
+ RaceBlock.client.set(key, token)
+ RaceBlock.client.expire(key, (sleep_delay + 15).round)
+ sleep sleep_delay
+ # Okay, so I feel like this is pseudo science, but whatever. Our
+ # race condition comes from when the same cron job is called by
+ # several different server instances at the same time
+ # (theoretically) all within the same second (much less really).
+ # By waiting a second we can let all the same cron jobs that were
+ # called at roughly the exact same time finish their write to the
+ # redis cache so that by the time the sleep is over, only one
+ # token is still accurate. I'm hesitant to believe this actually
+ # works, but I can't find any flaws in the logic at the current
+ # moment, and I also believe this is what is keep the EmailQueue
+ # stable which seems to have no duplicate sending problems.
+
+ return true if RaceBlock.client.get(@key) == token
+
# Token out of sync
- elsif debug
- logger.info("Token already exists")
- end
- # Token already exists
+ logger.debug("Token out of sync")
+ false
end
end