Class: Stud::Try

Inherits:
Object
  • Object
show all
Defined in:
lib/stud/try.rb

Overview

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.

Defined Under Namespace

Classes: Forever

Constant Summary collapse

FOREVER =

class Forever

Forever.new
BACKOFF_SCHEDULE =
[0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.0]
DEFAULT_CATCHABLE_EXCEPTIONS =
[StandardError]

Instance Method Summary collapse

Instance Method Details

#failure(exception, fail_count) ⇒ Object

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.



44
45
46
47
48
# 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_failure(exception, fail_count, message) ⇒ Object

Log a failure.

You should override this method if you want a better logger.



33
34
35
# File 'lib/stud/try.rb', line 33

def log_failure(exception, fail_count, message)
  puts "Failed (#{exception}). #{message}"
end

#try(enumerable = FOREVER, exceptions = DEFAULT_CATCHABLE_EXCEPTIONS, &block) ⇒ Object

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 { ... }


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# 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