class Stud::Try
A class implementing ‘retry-on-failure’
Example:
Try.new.try(5.times) { your_code }
A failure is indicated by any exception being raised. On success, the return value of the block is the return value of the try call.
On final failure (ran out of things to try), the last exception is raised.
Constants
- BACKOFF_SCHEDULE
- DEFAULT_CATCHABLE_EXCEPTIONS
- FOREVER
Public Instance Methods
This method is called when a try attempt fails.
The default implementation will sleep with exponential backoff up to a maximum of 2 seconds (see BACKOFF_SCHEDULE
)
exception - the exception causing the failure fail_count - how many times we have failed.
# File lib/stud/try.rb, line 44 def failure(exception, fail_count) backoff = BACKOFF_SCHEDULE[fail_count] || BACKOFF_SCHEDULE.last log_failure(exception, fail_count, "Sleeping for #{backoff}") sleep(backoff) end
Log a failure.
You should override this method if you want a better logger.
# File lib/stud/try.rb, line 33 def log_failure(exception, fail_count, message) puts "Failed (#{exception}). #{message}" end
Public: try a block of code until either it succeeds or we give up.
enumerable - an Enumerable or omitted/nil, each is invoked and is tried
that number of times. If this value is omitted or nil, we will try until success with no limit on the number of tries.
exceptions - the type of exceptions to retry, we use ‘StandardError` by default.
Returns the return value of the block once the block succeeds. Raises the last seen exception if we run out of tries.
Examples
# Try 10 times to fetch http://google.com/ response = try(10.times) { Net::HTTP.get_response("google.com", "/") } # Try many times, yielding the value of the enumeration to the block. # This allows you to try different inputs. response = try([0, 2, 4, 6]) { |val| 50 / val } Output: Failed (divided by 0). Retrying in 0.01 seconds... => 25 # Try forever return_value = try { ... }
# File lib/stud/try.rb, line 76 def try(enumerable=FOREVER, exceptions=DEFAULT_CATCHABLE_EXCEPTIONS, &block) if block.arity == 0 # If the block takes no arguments, give none procedure = lambda { |val| return block.call } else # Otherwise, pass the current 'enumerable' value to the block. procedure = lambda { |val| return block.call(val) } end # Track the last exception so we can reraise it on failure. last_exception = nil # When 'enumerable' runs out of things, if we still haven't succeeded, # we'll reraise fail_count = 0 enumerable.each do |val| begin # If the 'procedure' (the block, really) succeeds, we'll break # and return the return value of the block. Win! return procedure.call(val) rescue NoMethodError, NameError # Abort immediately on exceptions that are unlikely to recover. raise rescue *exceptions => exception last_exception = exception fail_count += 1 # Note: Since we can't reliably detect the 'end' of an enumeration, we # will call 'failure' for the final iteration (if it failed) and sleep # even though there's no strong reason to backoff on the last error. failure(exception, fail_count) end end # enumerable.each # generally make the exception appear from the 'try' method itself, not from # any deeply nested enumeration/begin/etc # It is my hope that this makes the backtraces easier to read, not more # difficult. If you find this is not the case, please please please let me # know. last_exception.set_backtrace(StandardError.new.backtrace) raise last_exception end