Skip to content

Commit

Permalink
Adding rescuer and counter strategy objects.
Browse files Browse the repository at this point in the history
  • Loading branch information
Todd Mazierski committed Jan 6, 2013
1 parent f644b0a commit b82d73c
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 138 deletions.
55 changes: 33 additions & 22 deletions README.md
Expand Up @@ -5,15 +5,15 @@
The wait gem executes a block until there's a valid (by default, truthy) result. Useful for blocking script execution until:
* an HTTP request was successful
* a port has opened
* an external process has started
* a process has started
* etc.

## Installation

Add to your `Gemfile`:

```ruby
gem "wait", "~> 0.2.2"
gem "wait", "~> 0.3"
```

## Examples
Expand All @@ -22,8 +22,11 @@ gem "wait", "~> 0.2.2"
wait = Wait.new
# => #<Wait>
wait.until { Time.now.sec.even? }
# Rescued exception while waiting: Wait::TruthyTester::ResultNotTruthy: false
# Attempt 1/5 failed, delaying for 1s
# [Tester] result: false
# [Rescuer] rescued: Wait::TruthyTester::ResultNotTruthy: false
# [Counter] attempt 1/5 failed
# [Delayer] delaying for 1s
# [Tester] result: true
# => true
```

Expand All @@ -39,30 +42,38 @@ wait.until do |attempt|
when 3 then "foo"
end
end
# Rescued exception while waiting: Wait::TruthyTester::ResultNotTruthy: nil
# Attempt 1/5 failed, delaying for 1s
# Rescued exception while waiting: RuntimeError: RuntimeError
# Attempt 2/5 failed, delaying for 2s
# [Tester] result: nil
# [Rescuer] rescued: Wait::TruthyTester::ResultNotTruthy: nil
# [Counter] attempt 1/5 failed
# [Delayer] delaying for 1s
# [Rescuer] rescued: RuntimeError: RuntimeError
# [Counter] attempt 2/5 failed
# [Delayer] delaying for 1s
# [Tester] result: "foo"
# => "foo"
```

## Options

<dl>
<dt>:attempts</dt>
<dd>Number of times to attempt the block. Default is <code>5</code>.</dd>
<dt>:timeout</dt>
<dd>Seconds until the block times out. Default is <code>15</code>.</dd>
<dt>:delay</dt>
<dd>Seconds to delay in between attempts. Passed to <code>delayer</code>. Default is <code>1</code>.
<dt>:delayer</dt>
<dd>Delay strategy used to sleep in between attempts. Default is <code>Wait::RegularDelayer</code>.</dd>
<dt>:rescue</dt>
<dd>One or an array of exceptions to rescue. Default is <code>nil</code>.</dd>
<dt>:tester</dt>
<dd>Strategy used to test the result. Default is <code>Wait::TruthyTester</code>.</dd>
<dt>:logger</dt>
<dd>Ruby logger used. Default is <code>Wait::Logger</code>.</dd>
<dt>attempts</dt>
<dd>Number of times to attempt the block (passed to <code>counter</code>). Default is <code>5</code>.</dd>
<dt>counter</dt>
<dd>Strategy used to count attempts. Default is <code>Wait::BaseCounter</code>.</dd>
<dt>timeout</dt>
<dd>Seconds until the block times out. Default is <code>15</code>.</dd>
<dt>delay</dt>
<dd>Seconds to delay in between attempts (passed to <code>delayer</code>). Default is <code>1</code>.</dd>
<dt>delayer</dt>
<dd>Strategy used to delay in between attempts. Default is <code>Wait::RegularDelayer</code>.</dd>
<dt>rescue</dt>
<dd>One or an array of exceptions to rescue (passed to <code>rescuer</code>). Default is <code>nil</code>.</dd>
<dt>rescuer</dt>
<dd>Strategy used to handle exceptions. Default is <code>Wait::PassiveRescuer</code>.</dd>
<dt>tester</dt>
<dd>Strategy used to test the result. Default is <code>Wait::TruthyTester</code>.</dd>
<dt>logger</dt>
<dd>Ruby logger used. Default is <code>Wait::BaseLogger</code>.</dd>
</dl>

## Documentation
Expand Down
31 changes: 0 additions & 31 deletions lib/attempt_counter.rb

This file was deleted.

49 changes: 49 additions & 0 deletions lib/counters/base.rb
@@ -0,0 +1,49 @@
class Wait
class BaseCounter
attr_reader :attempt

def initialize(logger, total)
@logger = logger
# Attempt to prevent causing an infinite loop by being very strict about
# the value passed.
unless total.is_a?(Fixnum) and total > 0
raise(ArgumentError, "invalid number of attempts: #{total.inspect}")
end

@total = total
reset
end

# Called in between attempts to reset the counter.
def reset
@attempt = 0
end

# Called before an attempt has started to increment the counter.
def increment
@attempt += 1
end

# When called, the exception given ought to be raised if this is the last
# attempt.
def raise_if_last_attempt(exception)
log_count
raise(exception) if last_attempt?
end

# Returns +true+ if this is the last attempt.
def last_attempt?
@attempt == @total
end

# Logs the current attempt count.
def log_count
@logger.debug "[Counter] attempt #{self} failed"
end

# Returns a string representation of the current count.
def to_s
[@attempt, @total].join("/")
end
end # BaseCounter
end # Wait
24 changes: 24 additions & 0 deletions lib/delayers/base.rb
@@ -0,0 +1,24 @@
class Wait
class BaseDelayer
def initialize(logger, initial_delay)
@logger = logger
@delay = initial_delay
end

# Called before a reattempt to sleep a certain about of time.
def sleep
log_delay
Kernel.sleep(@delay)
end

# Logs how long the delay is.
def log_delay
@logger.debug "[Delayer] delaying for #{self}"
end

# Returns a string representation of the delay.
def to_s
"#{@delay}s"
end
end # BaseDelayer
end # Wait
14 changes: 1 addition & 13 deletions lib/delayers/regular.rb
@@ -1,15 +1,3 @@
class Wait
class RegularDelayer
def initialize(initial_delay)
@delay = initial_delay
end

def sleep
Kernel.sleep(@delay)
end

def to_s
"#{@delay}s"
end
end # RegularDelayer
class RegularDelayer < BaseDelayer; end
end # Wait
15 changes: 15 additions & 0 deletions lib/initialize.rb
@@ -0,0 +1,15 @@
require "timeout"
require "logger"
require "forwardable"

require_relative "loggers/base"
require_relative "loggers/debug"
require_relative "counters/base"
require_relative "delayers/base"
require_relative "delayers/regular"
require_relative "delayers/exponential"
require_relative "testers/base"
require_relative "testers/passive"
require_relative "testers/truthy"
require_relative "rescuers/base"
require_relative "rescuers/passive"
21 changes: 21 additions & 0 deletions lib/loggers/base.rb
@@ -0,0 +1,21 @@
class Wait
class BaseLogger
extend Forwardable

attr_reader :logger
def_delegators :logger, :fatal,
:error,
:warn,
:info,
:debug

def initialize
@logger = ::Logger.new(STDOUT)
@logger.level = level
end

def level
::Logger::WARN
end
end # Logger
end # Wait
7 changes: 7 additions & 0 deletions lib/loggers/debug.rb
@@ -0,0 +1,7 @@
class Wait
class DebugLogger < BaseLogger
def level
::Logger::DEBUG
end
end # Logger
end # Wait
36 changes: 36 additions & 0 deletions lib/rescuers/base.rb
@@ -0,0 +1,36 @@
class Wait
class BaseRescuer
def initialize(logger, *exceptions)
@logger = logger
@exceptions = Array(exceptions).flatten
end

# Returns an array of the exceptions that ought to be rescued.
def exceptions
internal_exceptions + @exceptions
end

# Returns exceptions that are essential to internal operation of the Wait
# gem.
def internal_exceptions
[TimeoutError]
end

# Returns +true+ if an exception can be ignored.
def ignore?(exception)
true
end

# Raises the exception given unless it can be ignored.
def raise_unless_ignore(exception)
log_exception(exception)
raise(exception) unless ignore?(exception)
end

# Logs an exception.
def log_exception(exception)
@logger.debug "[Rescuer] rescued: #{exception.class.name}: #{exception.message}"
@logger.debug exception.backtrace.join("\n")
end
end # BaseRescuer
end # Wait
3 changes: 3 additions & 0 deletions lib/rescuers/passive.rb
@@ -0,0 +1,3 @@
class Wait
class PassiveRescuer < BaseRescuer; end
end # Wait
28 changes: 28 additions & 0 deletions lib/testers/base.rb
@@ -0,0 +1,28 @@
class Wait
class BaseTester
# Returns an array of exceptions that ought to be rescued by the rescuer.
def exceptions
[]
end

def initialize(logger)
@logger = logger
end

# Raises an exception unless the result is valid.
def raise_unless_valid(result)
log_result
valid?(result)
end

# Returns +true+ if a result if valid.
def valid?(result)
true
end

# Logs a result.
def log_result(result)
@logger.debug "[Tester] result: #{result.inspect}"
end
end # BaseTester
end # Wait
3 changes: 3 additions & 0 deletions lib/testers/passive.rb
@@ -0,0 +1,3 @@
class Wait
class PassiveTester < BaseTester; end
end # Wait
20 changes: 10 additions & 10 deletions lib/testers/truthy.rb
@@ -1,21 +1,21 @@
class Wait
class TruthyTester
class TruthyTester < BaseTester
class ResultNotTruthy < RuntimeError; end

def self.exceptions
# Returns an array of exceptions that ought to be rescued by the rescuer.
def exceptions
[ResultNotTruthy]
end

def initialize(result = nil)
@result = result
# Raises an exception unless the result is valid.
def raise_unless_valid(result)
log_result(result)
valid?(result) ? result : raise(ResultNotTruthy, result.inspect)
end

def raise_unless_valid
valid? ? @result : raise(ResultNotTruthy, @result.inspect)
end

def valid?
not (@result.nil? or @result == false)
# Returns +true+ if a result if valid.
def valid?(result)
not (result.nil? or result == false)
end
end # TruthyTester
end # Wait

0 comments on commit b82d73c

Please sign in to comment.