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