wait 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/wait.rb +215 -0
- metadata +45 -0
data/lib/wait.rb
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
require "timeout"
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
class Wait
|
5
|
+
# Creates a new Wait instance.
|
6
|
+
#
|
7
|
+
# == Options
|
8
|
+
#
|
9
|
+
# [:attempts]
|
10
|
+
# Number of times to attempt the block. Default is +5+.
|
11
|
+
# [:timeout]
|
12
|
+
# Seconds until the block times out. Default is +15+.
|
13
|
+
# [:delayer]
|
14
|
+
# Delay strategy to use to sleep in between attempts. Default is
|
15
|
+
# +Wait::RegularDelayer.new+.
|
16
|
+
# [:rescue]
|
17
|
+
# One or an array of exceptions to rescue. Default is +nil+.
|
18
|
+
# [:tester]
|
19
|
+
# Strategy to use to test the result. Default is +Wait::TruthyTester+.
|
20
|
+
# [:logger]
|
21
|
+
# Ruby logger to use. Default is +Wait#logger+.
|
22
|
+
#
|
23
|
+
def initialize(options = {})
|
24
|
+
@attempts = options[:attempts] || 5
|
25
|
+
@timeout = options[:timeout] || 15
|
26
|
+
@delayer = options[:delayer] || RegularDelayer.new
|
27
|
+
@exceptions = Array(options[:rescue])
|
28
|
+
@tester = options[:tester] || TruthyTester
|
29
|
+
@logger = options[:logger] || logger
|
30
|
+
|
31
|
+
@counter = AttemptCounter.new(@attempts)
|
32
|
+
|
33
|
+
validate_strategies
|
34
|
+
end
|
35
|
+
|
36
|
+
# Validates all of the assigned strategy objects.
|
37
|
+
def validate_strategies
|
38
|
+
unless @delayer.respond_to?(:sleep)
|
39
|
+
raise(ArgumentError, "delay strategy does not respond to sleep message: #{@delayer.inspect}")
|
40
|
+
end
|
41
|
+
|
42
|
+
unless @tester.respond_to?(:new)
|
43
|
+
raise(ArgumentError, "tester strategy does not respond to new message: #{@tester.inspect}")
|
44
|
+
end
|
45
|
+
|
46
|
+
unless @tester.new.respond_to?(:valid?)
|
47
|
+
raise(ArgumentError, "tester strategy does not respond to valid? message: #{@tester.inspect}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a new (or existing) logger instance.
|
52
|
+
def logger
|
53
|
+
if @logger.nil?
|
54
|
+
@logger = Logger.new(STDOUT)
|
55
|
+
@logger.level = Logger::WARN
|
56
|
+
end
|
57
|
+
|
58
|
+
@logger
|
59
|
+
end
|
60
|
+
|
61
|
+
# == Description
|
62
|
+
#
|
63
|
+
# Wait#until executes a block until there's a valid (by default, truthy)
|
64
|
+
# result. Useful for blocking script execution until:
|
65
|
+
# * an HTTP request was successful
|
66
|
+
# * a port has opened
|
67
|
+
# * an external process has started
|
68
|
+
# * etc.
|
69
|
+
#
|
70
|
+
# == Examples
|
71
|
+
#
|
72
|
+
# wait = Wait.new
|
73
|
+
# # => #<Wait>
|
74
|
+
# wait.until { Time.now.sec.even? }
|
75
|
+
# # Rescued exception while waiting: Wait::TruthyTester::ResultNotTruthy: false
|
76
|
+
# # Attempt 1/5 failed, delaying for 1s
|
77
|
+
# # => true
|
78
|
+
#
|
79
|
+
# If you wish to handle an exception by attempting the block again, pass one
|
80
|
+
# or an array of exceptions with the +:rescue+ option.
|
81
|
+
#
|
82
|
+
# wait = Wait.new(:rescue => RuntimeError)
|
83
|
+
# # => #<Wait>
|
84
|
+
# wait.until do |attempt|
|
85
|
+
# case attempt
|
86
|
+
# when 1 then nil
|
87
|
+
# when 2 then raise RuntimeError
|
88
|
+
# when 3 then "foo"
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
# # Rescued exception while waiting: Wait::TruthyTester::ResultNotTruthy: nil
|
92
|
+
# # Attempt 1/5 failed, delaying for 1s
|
93
|
+
# # Rescued exception while waiting: RuntimeError: RuntimeError
|
94
|
+
# # Attempt 2/5 failed, delaying for 2s
|
95
|
+
# # => "foo"
|
96
|
+
#
|
97
|
+
# == Returns
|
98
|
+
#
|
99
|
+
# The result of the block if valid (by default, truthy).
|
100
|
+
#
|
101
|
+
# == Raises
|
102
|
+
#
|
103
|
+
# If no results are valid, the exception from the last attempt made.
|
104
|
+
#
|
105
|
+
def until(&block)
|
106
|
+
# Reset the attempt counter.
|
107
|
+
@counter.reset
|
108
|
+
|
109
|
+
begin
|
110
|
+
@counter.increment
|
111
|
+
|
112
|
+
result = Timeout.timeout(@timeout, Wait::TimeoutError) do
|
113
|
+
# Execute the block and pass the attempt count to it.
|
114
|
+
yield(@counter.attempt)
|
115
|
+
end
|
116
|
+
|
117
|
+
tester = @tester.new(result)
|
118
|
+
tester.raise_unless_valid
|
119
|
+
rescue Wait::TimeoutError, *(@tester.exceptions + @exceptions) => exception
|
120
|
+
logger.debug "Rescued exception while waiting: #{exception.class.name}: #{exception.message}"
|
121
|
+
logger.debug exception.backtrace.join("\n")
|
122
|
+
|
123
|
+
# If this was the last attempt, raise the exception from the last
|
124
|
+
# attempt.
|
125
|
+
if @counter.last_attempt?
|
126
|
+
raise(exception)
|
127
|
+
else
|
128
|
+
logger.debug "Attempt #{@counter} failed, delaying for #{@delayer}"
|
129
|
+
@delayer.sleep
|
130
|
+
retry
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Raised when a block doesn't return a result (+nil+ or +false+).
|
136
|
+
class NoResultError < StandardError; end
|
137
|
+
|
138
|
+
# Raised when a block times out.
|
139
|
+
class TimeoutError < Timeout::Error; end
|
140
|
+
|
141
|
+
class RegularDelayer
|
142
|
+
def initialize(initial_delay = 1)
|
143
|
+
@delay = initial_delay
|
144
|
+
end
|
145
|
+
|
146
|
+
def sleep
|
147
|
+
Kernel.sleep(@delay)
|
148
|
+
end
|
149
|
+
|
150
|
+
def to_s
|
151
|
+
"#{@delay}s"
|
152
|
+
end
|
153
|
+
end # RegularDelayer
|
154
|
+
|
155
|
+
class ExponentialDelayer < RegularDelayer
|
156
|
+
def sleep
|
157
|
+
super
|
158
|
+
increment
|
159
|
+
end
|
160
|
+
|
161
|
+
def increment
|
162
|
+
@delay *= 2
|
163
|
+
end
|
164
|
+
end # ExponentialDelayer
|
165
|
+
|
166
|
+
class AttemptCounter
|
167
|
+
attr_reader :attempt
|
168
|
+
|
169
|
+
def initialize(total)
|
170
|
+
# Prevent accidentally causing an infinite loop.
|
171
|
+
unless total.is_a?(Fixnum) and total > 0
|
172
|
+
raise(ArgumentError, "invalid number of attempts: #{total.inspect}")
|
173
|
+
end
|
174
|
+
|
175
|
+
@total = total
|
176
|
+
reset
|
177
|
+
end
|
178
|
+
|
179
|
+
def reset
|
180
|
+
@attempt = 0
|
181
|
+
end
|
182
|
+
|
183
|
+
def increment
|
184
|
+
@attempt += 1
|
185
|
+
end
|
186
|
+
|
187
|
+
def last_attempt?
|
188
|
+
@attempt == @total
|
189
|
+
end
|
190
|
+
|
191
|
+
def to_s
|
192
|
+
[@attempt, @total].join("/")
|
193
|
+
end
|
194
|
+
end # AttemptCounter
|
195
|
+
|
196
|
+
class TruthyTester
|
197
|
+
class ResultNotTruthy < RuntimeError; end
|
198
|
+
|
199
|
+
def self.exceptions
|
200
|
+
[ResultNotTruthy]
|
201
|
+
end
|
202
|
+
|
203
|
+
def initialize(result = nil)
|
204
|
+
@result = result
|
205
|
+
end
|
206
|
+
|
207
|
+
def raise_unless_valid
|
208
|
+
valid? ? @result : raise(ResultNotTruthy, @result.inspect)
|
209
|
+
end
|
210
|
+
|
211
|
+
def valid?
|
212
|
+
not (@result.nil? or @result == false)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end #Wait
|
metadata
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wait
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Todd Mazierski
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-21 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email: todd@paperlesspost.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/wait.rb
|
21
|
+
homepage: http://github.com/paperlesspost/wait
|
22
|
+
licenses: []
|
23
|
+
post_install_message:
|
24
|
+
rdoc_options: []
|
25
|
+
require_paths:
|
26
|
+
- lib
|
27
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
requirements: []
|
40
|
+
rubyforge_project:
|
41
|
+
rubygems_version: 1.8.23
|
42
|
+
signing_key:
|
43
|
+
specification_version: 3
|
44
|
+
summary: Executes a block until there's a valid result.
|
45
|
+
test_files: []
|