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.
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: []