module GovDelivery::Proctor class CheckTimeExceeded < StandardError; end def self.backoff_check(limit, desc = 'Backoff check', inc = 20) 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 = 'Accelerating check', inc = 20) 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 = 10) 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 accelerate, halve the wait time each time based on limit # and use increment only for minimum wait time # if steady, just keep it steady # to_i used to make ActiveSupport::Durations safe def self.sleep_time(check_type, limit, increment, iteration) limit = limit.to_i increment = increment.to_i sleep_time = case check_type when 'steady' increment when 'backoff' (2**iteration) * increment when 'accelerate' [(limit / 2) / (iteration + 1), increment].max else raise "Check type #{check_type} invalid: choose accelerate, backoff, or steady." end [sleep_time, limit].min 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) getStandardLogger.info("sleeping for #{sleep_time} seconds") 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 x += 1 # 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