wait 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/wait.rb +215 -0
  2. 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: []