module GovDelivery::Proctor class CheckTimeExceeded < StandardError; end def self.backoff_check(limit, desc = 'Backoff check', inc = 1) raise 'Check requires a block' unless block_given? check(limit, 'backoff', desc, inc) do yield end end # Sometimes you want to reverse backoff because as time # passes it is more likely your condition is true def self.accelerating_check(limit, desc = 'Backon check', inc = 1) raise 'Check requires a block' unless block_given? check(limit, 'accelerate', desc, inc) do yield end end def self.steady_check(limit, desc = 'Steady check', inc = 1) raise 'Check requires a block' unless block_given? check(limit, 'steady', desc, inc) do yield end end # if backoff, double the wait time each time # if accelerating, ramp up after long pause # if steady, just keep it steady def self.sleep_time(check_type, limit, increment, iteration) case check_type when 'steady' increment when 'backoff' (2**iteration) * increment when 'accelerate' val = Math.log(limit, iteration + 1) val = val.to_f.infinite? ? limit / increment : val val = val.to_f.nan? ? 1 : val.round [increment, val * increment].max else raise "Check type #{check_type} invalid: choose backon, accelerate or steady." end end def self.check(limit, check_type, desc, inc) setup slept_time = 0 x = 0 response = nil Kernel.loop do sleep_time = sleep_time(check_type, limit, inc, ++x) sleep(sleep_time) slept_time += sleep_time response = yield break if response if slept_time >= limit fail_message = "#{desc} has taken too long. Have waited #{slept_time} seconds\nlog: #{@backBuffer.string}" raise CheckTimeExceeded, fail_message end # We're doing this twice, once for standard out so you can see it's still # working, and then again inside the logged output if it fails. getStandardLogger.info("Still waiting while #{desc}, current time=#{slept_time}") log.info("Still waiting while #{desc}, current time=#{slept_time}") end teardown response end def self.log getStandardLogger end def self.suppress(*exception_classes) yield rescue *exception_classes end def self.getStandardLogger @log ||= begin logger = Logger.new(ENV['TEST_LOG_FILE'] || STDOUT) logger.level = Logger.const_get(ENV['TEST_LOG_LEVEL'] || 'INFO') logger end end def self.setup @backBuffer = StringIO.new @backLog = Logger.new @backBuffer # We're re-defining log in here so that we can log meaningful information # out during the backoff check, but only for failed runs. def self.log @backLog end end def self.teardown # We have to re-define log back to it's original implementation # so that we get normal logging after we're out of the backoff check. def self.log getStandardLogger end end private_class_method :getStandardLogger, :setup, :teardown end