hoodie 0.3.21 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitattributes +17 -0
- data/.gitignore +193 -0
- data/Gemfile +0 -1
- data/Rakefile +0 -1
- data/hoodie.gemspec +11 -10
- data/lib/hoodie/hash.rb +1 -1
- data/lib/hoodie/identity_map.rb +94 -0
- data/lib/hoodie/memoizable.rb +0 -1
- data/lib/hoodie/obfuscate.rb +14 -14
- data/lib/hoodie/rash.rb +2 -2
- data/lib/hoodie/stash/disk_store.rb +34 -34
- data/lib/hoodie/stash/mem_store.rb +1 -1
- data/lib/hoodie/stash.rb +0 -1
- data/lib/hoodie/timers.rb +355 -0
- data/lib/hoodie/utils.rb +214 -83
- data/lib/hoodie/version.rb +1 -1
- data/lib/hoodie.rb +2 -0
- metadata +20 -2
@@ -0,0 +1,355 @@
|
|
1
|
+
|
2
|
+
require 'set'
|
3
|
+
require 'hitimes'
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module Hoodie::Timers
|
7
|
+
# An individual timer set to fire a given proc at a given time. A timer is
|
8
|
+
# always connected to a Timer::Group but it would ONLY be in @group.timers
|
9
|
+
# if it also has a @handle specified. Otherwise it is either PAUSED or has
|
10
|
+
# been FIRED and is not recurring. You can manually enter this state by
|
11
|
+
# calling #cancel and resume normal operation by calling #reset.
|
12
|
+
class Timer
|
13
|
+
include Comparable
|
14
|
+
attr_reader :interval, :offset, :recurring
|
15
|
+
|
16
|
+
def initialize(group, interval, recurring = false, offset = nil, &block)
|
17
|
+
@group = group
|
18
|
+
@interval = interval
|
19
|
+
@recurring = recurring
|
20
|
+
@block = block
|
21
|
+
@offset = offset
|
22
|
+
@handle = nil
|
23
|
+
|
24
|
+
# If a start offset was supplied, use that, otherwise use the current
|
25
|
+
# timers offset.
|
26
|
+
reset(@offset || @group.current_offset)
|
27
|
+
end
|
28
|
+
|
29
|
+
def paused?
|
30
|
+
@group.paused_timers.include? self
|
31
|
+
end
|
32
|
+
|
33
|
+
def pause
|
34
|
+
return if paused?
|
35
|
+
@group.timers.delete self
|
36
|
+
@group.paused_timers.add self
|
37
|
+
@handle.cancel! if @handle
|
38
|
+
@handle = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def resume
|
42
|
+
return unless paused?
|
43
|
+
@group.paused_timers.delete self
|
44
|
+
# This will add us back to the group:
|
45
|
+
reset
|
46
|
+
end
|
47
|
+
alias_method :continue, :resume
|
48
|
+
|
49
|
+
# Extend this timer
|
50
|
+
def delay(seconds)
|
51
|
+
@handle.cancel! if @handle
|
52
|
+
@offset += seconds
|
53
|
+
@handle = @group.events.schedule(@offset, self)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Cancel this timer. Do not call while paused.
|
57
|
+
def cancel
|
58
|
+
return unless @handle
|
59
|
+
@handle.cancel! if @handle
|
60
|
+
@handle = nil
|
61
|
+
# This timer is no longer valid:
|
62
|
+
@group.timers.delete self if @group
|
63
|
+
end
|
64
|
+
|
65
|
+
# Reset this timer. Do not call while paused.
|
66
|
+
def reset(offset = @group.current_offset)
|
67
|
+
# This logic allows us to minimise the interaction with @group.timers.
|
68
|
+
# A timer with a handle is always registered with the group.
|
69
|
+
if @handle
|
70
|
+
@handle.cancel!
|
71
|
+
else
|
72
|
+
@group.timers << self
|
73
|
+
end
|
74
|
+
@offset = Float(offset) + @interval
|
75
|
+
@handle = @group.events.schedule(@offset, self)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Fire the block.
|
79
|
+
def fire(offset = @group.current_offset)
|
80
|
+
if recurring == :strict
|
81
|
+
# ... make the next interval strictly the last offset + the interval:
|
82
|
+
reset(@offset)
|
83
|
+
elsif recurring
|
84
|
+
reset(offset)
|
85
|
+
else
|
86
|
+
@offset = offset
|
87
|
+
end
|
88
|
+
@block.call(offset)
|
89
|
+
cancel unless recurring
|
90
|
+
end
|
91
|
+
alias_method :call, :fire
|
92
|
+
|
93
|
+
# Number of seconds until next fire / since last fire
|
94
|
+
def fires_in
|
95
|
+
@offset - @group.current_offset if @offset
|
96
|
+
end
|
97
|
+
|
98
|
+
# Inspect a timer
|
99
|
+
def inspect
|
100
|
+
str = "#<Timers::Timer:#{object_id.to_s(16)} "
|
101
|
+
if @offset
|
102
|
+
if fires_in >= 0
|
103
|
+
str << "fires in #{fires_in} seconds"
|
104
|
+
else
|
105
|
+
str << "fired #{fires_in.abs} seconds ago"
|
106
|
+
end
|
107
|
+
str << ", recurs every #{interval}" if recurring
|
108
|
+
else
|
109
|
+
str << 'dead'
|
110
|
+
end
|
111
|
+
str << '>'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# An exclusive, monotonic timeout class.
|
116
|
+
class Wait
|
117
|
+
def self.for(duration, &block)
|
118
|
+
if duration
|
119
|
+
timeout = new(duration)
|
120
|
+
timeout.while_time_remaining(&block)
|
121
|
+
else
|
122
|
+
loop do
|
123
|
+
yield(nil)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def initialize(duration)
|
129
|
+
@duration = duration
|
130
|
+
@remaining = true
|
131
|
+
end
|
132
|
+
|
133
|
+
attr_reader :duration
|
134
|
+
attr_reader :remaining
|
135
|
+
|
136
|
+
# Yields while time remains for work to be done:
|
137
|
+
def while_time_remaining(&_block)
|
138
|
+
@interval = Hitimes::Interval.new
|
139
|
+
@interval.start
|
140
|
+
while time_remaining?
|
141
|
+
yield @remaining
|
142
|
+
end
|
143
|
+
ensure
|
144
|
+
@interval.stop
|
145
|
+
@interval = nil
|
146
|
+
end
|
147
|
+
|
148
|
+
private # P R O P R I E T À P R I V A T A Vietato L'accesso
|
149
|
+
|
150
|
+
def time_remaining?
|
151
|
+
@remaining = (@duration - @interval.duration)
|
152
|
+
@remaining > 0
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Group
|
157
|
+
include Enumerable
|
158
|
+
extend Forwardable
|
159
|
+
def_delegators :@timers, :each, :empty?
|
160
|
+
|
161
|
+
def initialize
|
162
|
+
@events = Events.new
|
163
|
+
@timers = Set.new
|
164
|
+
@paused_timers = Set.new
|
165
|
+
@interval = Hitimes::Interval.new
|
166
|
+
@interval.start
|
167
|
+
end
|
168
|
+
|
169
|
+
# Scheduled events:
|
170
|
+
attr_reader :events
|
171
|
+
|
172
|
+
# Active timers:
|
173
|
+
attr_reader :timers
|
174
|
+
|
175
|
+
# Paused timers:
|
176
|
+
attr_reader :paused_timers
|
177
|
+
|
178
|
+
# Call the given block after the given interval. The first argument will be
|
179
|
+
# the time at which the group was asked to fire timers for.
|
180
|
+
def after(interval, &block)
|
181
|
+
Timer.new(self, interval, false, &block)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Call the given block periodically at the given interval. The first
|
185
|
+
# argument will be the time at which the group was asked to fire timers for.
|
186
|
+
def every(interval, recur = true, &block)
|
187
|
+
Timer.new(self, interval, recur, &block)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Wait for the next timer and fire it. Can take a block, which should behave
|
191
|
+
# like sleep(n), except that n may be nil (sleep forever) or a negative
|
192
|
+
# number (fire immediately after return).
|
193
|
+
def wait(&_block)
|
194
|
+
if block_given?
|
195
|
+
yield wait_interval
|
196
|
+
|
197
|
+
while interval = wait_interval and interval > 0
|
198
|
+
yield interval
|
199
|
+
end
|
200
|
+
else
|
201
|
+
while interval = wait_interval and interval > 0
|
202
|
+
# We cannot assume that sleep will wait for the specified time, it might be +/- a bit.
|
203
|
+
sleep interval
|
204
|
+
end
|
205
|
+
end
|
206
|
+
fire
|
207
|
+
end
|
208
|
+
|
209
|
+
# Interval to wait until when the next timer will fire.
|
210
|
+
# - nil: no timers
|
211
|
+
# - -ve: timers expired already
|
212
|
+
# - 0: timers ready to fire
|
213
|
+
# - +ve: timers waiting to fire
|
214
|
+
def wait_interval(offset = current_offset)
|
215
|
+
if handle = @events.first
|
216
|
+
return handle.time - Float(offset)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Fire all timers that are ready.
|
221
|
+
def fire(offset = current_offset)
|
222
|
+
@events.fire(offset)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Pause all timers.
|
226
|
+
def pause
|
227
|
+
@timers.dup.each(&:pause)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Resume all timers.
|
231
|
+
def resume
|
232
|
+
@paused_timers.dup.each(&:resume)
|
233
|
+
end
|
234
|
+
alias_method :continue, :resume
|
235
|
+
|
236
|
+
# Delay all timers.
|
237
|
+
def delay(seconds)
|
238
|
+
@timers.each do |timer|
|
239
|
+
timer.delay(seconds)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Cancel all timers.
|
244
|
+
def cancel
|
245
|
+
@timers.dup.each(&:cancel)
|
246
|
+
end
|
247
|
+
|
248
|
+
# The group's current time.
|
249
|
+
def current_offset
|
250
|
+
@interval.to_f
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Maintains an ordered list of events, which can be cancelled.
|
255
|
+
class Events
|
256
|
+
# Represents a cancellable handle for a specific timer event.
|
257
|
+
class Handle
|
258
|
+
def initialize(time, callback)
|
259
|
+
@time = time
|
260
|
+
@callback = callback
|
261
|
+
end
|
262
|
+
|
263
|
+
# The absolute time that the handle should be fired at.
|
264
|
+
attr_reader :time
|
265
|
+
|
266
|
+
# Cancel this timer, O(1).
|
267
|
+
def cancel!
|
268
|
+
# The simplest way to keep track of cancelled status is to nullify the
|
269
|
+
# callback. This should also be optimal for garbage collection.
|
270
|
+
@callback = nil
|
271
|
+
end
|
272
|
+
|
273
|
+
# Has this timer been cancelled? Cancelled timer's don't fire.
|
274
|
+
def cancelled?
|
275
|
+
@callback.nil?
|
276
|
+
end
|
277
|
+
|
278
|
+
def >(other)
|
279
|
+
@time > other.to_f
|
280
|
+
end
|
281
|
+
|
282
|
+
def to_f
|
283
|
+
@time
|
284
|
+
end
|
285
|
+
|
286
|
+
# Fire the callback if not cancelled with the given time parameter.
|
287
|
+
def fire(time)
|
288
|
+
if @callback
|
289
|
+
@callback.call(time)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def initialize
|
295
|
+
# A sequence of handles, maintained in sorted order, future to present.
|
296
|
+
# @sequence.last is the next event to be fired.
|
297
|
+
@sequence = []
|
298
|
+
end
|
299
|
+
|
300
|
+
# Add an event at the given time.
|
301
|
+
def schedule(time, callback)
|
302
|
+
handle = Handle.new(time.to_f, callback)
|
303
|
+
index = bisect_left(@sequence, handle)
|
304
|
+
# Maintain sorted order, O(logN) insertion time.
|
305
|
+
@sequence.insert(index, handle)
|
306
|
+
handle
|
307
|
+
end
|
308
|
+
|
309
|
+
# Returns the first non-cancelled handle.
|
310
|
+
def first
|
311
|
+
while handle = @sequence.last
|
312
|
+
if handle.cancelled?
|
313
|
+
@sequence.pop
|
314
|
+
else
|
315
|
+
return handle
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Returns the number of pending (possibly cancelled) events.
|
321
|
+
def size
|
322
|
+
@sequence.size
|
323
|
+
end
|
324
|
+
|
325
|
+
# Fire all handles for which Handle#time is less than the given time.
|
326
|
+
def fire(time)
|
327
|
+
pop(time).reverse_each do |handle|
|
328
|
+
handle.fire(time)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
private # P R O P R I E T À P R I V A T A Vietato L'accesso
|
333
|
+
|
334
|
+
# Efficiently take k handles for which Handle#time is less than the given
|
335
|
+
# time.
|
336
|
+
def pop(time)
|
337
|
+
index = bisect_left(@sequence, time)
|
338
|
+
@sequence.pop(@sequence.size - index)
|
339
|
+
end
|
340
|
+
|
341
|
+
# Return the left-most index where to insert item e, in a list a, assuming
|
342
|
+
# a is sorted in descending order.
|
343
|
+
def bisect_left(a, e, l = 0, u = a.length)
|
344
|
+
while l < u
|
345
|
+
m = l + (u - l).div(2)
|
346
|
+
if a[m] > e
|
347
|
+
l = m + 1
|
348
|
+
else
|
349
|
+
u = m
|
350
|
+
end
|
351
|
+
end
|
352
|
+
l
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
data/lib/hoodie/utils.rb
CHANGED
@@ -17,118 +17,249 @@
|
|
17
17
|
# limitations under the License.
|
18
18
|
#
|
19
19
|
|
20
|
-
module Hoodie
|
21
|
-
# Returns an aligned_string of text relative to the size of the terminal
|
22
|
-
# window. If a line in the string exceeds the width of the terminal window
|
23
|
-
# the line will be chopped off at the whitespace chacter closest to the
|
24
|
-
# end of the line and prepended to the next line, keeping all indentation.
|
25
|
-
#
|
26
|
-
# The terminal size is detected by default, but custom line widths can
|
27
|
-
# passed. All strings will also be left aligned with 5 whitespace characters
|
28
|
-
# by default.
|
29
|
-
def self.align_text(text, console_cols = nil, preamble = 5)
|
30
|
-
unless console_cols
|
31
|
-
console_cols = terminal_dimensions[0]
|
32
20
|
|
33
|
-
|
34
|
-
|
21
|
+
require 'securerandom'
|
22
|
+
require 'time'
|
23
|
+
|
24
|
+
module Hoodie::Utils
|
25
|
+
def self.included(base)
|
26
|
+
base.extend(ClassMethods)
|
27
|
+
end
|
28
|
+
private_class_method :included
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
def callable(call_her)
|
32
|
+
call_her.respond_to?(:call) ? call_her : lambda { call_her }
|
35
33
|
end
|
36
34
|
|
37
|
-
|
35
|
+
def camelize(underscored_word)
|
36
|
+
underscored_word.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
39
|
+
def classify(table_name)
|
40
|
+
camelize singularize(table_name.to_s.sub(/.*\./, ''))
|
41
|
+
end
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
def class_name
|
44
|
+
demodulize(self.class)
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
47
|
+
def caller_name
|
48
|
+
caller_locations(2, 1).first.label
|
49
|
+
end
|
49
50
|
|
50
|
-
|
51
|
-
|
51
|
+
def demodulize(class_name_in_module)
|
52
|
+
class_name_in_module.to_s.sub(/^.*::/, '')
|
53
|
+
end
|
52
54
|
|
53
|
-
|
54
|
-
|
55
|
-
|
55
|
+
def pluralize(word)
|
56
|
+
word.to_s.sub(/([^s])$/, '\1s')
|
57
|
+
end
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
line = (" " * whitespace)
|
61
|
-
end
|
59
|
+
def singularize(word)
|
60
|
+
word.to_s.sub(/s$/, '').sub(/ie$/, 'y')
|
61
|
+
end
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
if text[i + 1] && line == (" " * whitespace)
|
73
|
-
text.insert(i + 1, "")
|
74
|
-
end
|
75
|
-
|
76
|
-
# Add the snipped text to the current line
|
77
|
-
line.insert(whitespace, "#{piece} ")
|
78
|
-
end
|
63
|
+
def underscore(camel_cased_word)
|
64
|
+
word = camel_cased_word.to_s.dup
|
65
|
+
word.gsub!(/::/, '/')
|
66
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
67
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
68
|
+
word.tr! '-', '_'
|
69
|
+
word.downcase!
|
70
|
+
word
|
71
|
+
end
|
79
72
|
|
80
|
-
|
73
|
+
# Return the date and time in "HTTP-date" format as defined by RFC 7231.
|
74
|
+
#
|
75
|
+
# @return [Date,Time] in "HTTP-date" format
|
76
|
+
def utc_httpdate
|
77
|
+
Time.now.utc.httpdate
|
78
|
+
end
|
81
79
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
if line.length > (console_cols + preamble)
|
86
|
-
reverse = console_cols
|
80
|
+
def request_id
|
81
|
+
SecureRandom.uuid
|
82
|
+
end
|
87
83
|
|
88
|
-
|
89
|
-
|
90
|
-
|
84
|
+
def twenty_four_hours_ago
|
85
|
+
Time.now - ( 60 * 60 * 24)
|
86
|
+
end
|
91
87
|
|
92
|
-
|
88
|
+
def verify_options(accepted, actual) # @private
|
89
|
+
return unless debug || $DEBUG
|
90
|
+
unless (act=Set[*actual.keys]).subset?(acc=Set[*accepted])
|
91
|
+
raise Croesus::Errors::UnknownOption,
|
92
|
+
"\nDetected unknown option(s): #{(act - acc).to_a.inspect}\n" <<
|
93
|
+
"Accepted options are: #{accepted.inspect}"
|
93
94
|
end
|
95
|
+
yield if block_given?
|
96
|
+
end
|
97
|
+
end # module ClassMethods
|
94
98
|
|
95
|
-
|
96
|
-
# updated, create a new line and append the snippet to it, using
|
97
|
-
# the same left alignment as the last line in the text.
|
98
|
-
if piece != '' && text[i+1].nil?
|
99
|
-
text[i+1] = "#{' ' * (whitespace)}#{piece}"
|
100
|
-
piece = ''
|
101
|
-
end
|
99
|
+
# ============================================================================
|
102
100
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
101
|
+
def callable(call_her)
|
102
|
+
call_her.respond_to?(:call) ? call_her : lambda { call_her }
|
103
|
+
end
|
104
|
+
|
105
|
+
def camelize(underscored_word)
|
106
|
+
underscored_word.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
107
|
+
end
|
108
|
+
|
109
|
+
def classify(table_name)
|
110
|
+
camelize singularize(table_name.to_s.sub(/.*\./, ''))
|
111
|
+
end
|
112
|
+
|
113
|
+
def class_name
|
114
|
+
demodulize(self.class)
|
115
|
+
end
|
116
|
+
|
117
|
+
def caller_name
|
118
|
+
caller_locations(2, 1).first.label
|
119
|
+
end
|
120
|
+
|
121
|
+
def demodulize(class_name_in_module)
|
122
|
+
class_name_in_module.to_s.sub(/^.*::/, '')
|
123
|
+
end
|
107
124
|
|
108
|
-
|
125
|
+
def pluralize(word)
|
126
|
+
word.to_s.sub(/([^s])$/, '\1s')
|
109
127
|
end
|
110
128
|
|
111
|
-
|
129
|
+
def singularize(word)
|
130
|
+
word.to_s.sub(/s$/, '').sub(/ie$/, 'y')
|
131
|
+
end
|
132
|
+
|
133
|
+
def underscore(camel_cased_word)
|
134
|
+
word = camel_cased_word.to_s.dup
|
135
|
+
word.gsub!(/::/, '/')
|
136
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
137
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
138
|
+
word.tr! '-', '_'
|
139
|
+
word.downcase!
|
140
|
+
word
|
141
|
+
end
|
142
|
+
|
143
|
+
# Return the date and time in "HTTP-date" format as defined by RFC 7231.
|
112
144
|
#
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
145
|
+
# @return [Date,Time] in "HTTP-date" format
|
146
|
+
def utc_httpdate
|
147
|
+
Time.now.utc.httpdate
|
148
|
+
end
|
149
|
+
|
150
|
+
def request_id
|
151
|
+
SecureRandom.uuid
|
152
|
+
end
|
117
153
|
|
118
|
-
|
154
|
+
def twenty_four_hours_ago
|
155
|
+
Time.now - ( 60 * 60 * 24)
|
156
|
+
end
|
119
157
|
|
120
|
-
|
121
|
-
|
158
|
+
def verify_options(accepted, actual) # @private
|
159
|
+
return unless debug || $DEBUG
|
160
|
+
unless (act=Set[*actual.keys]).subset?(acc=Set[*accepted])
|
161
|
+
raise Croesus::Errors::UnknownOption,
|
162
|
+
"\nDetected unknown option(s): #{(act - acc).to_a.inspect}\n" <<
|
163
|
+
"Accepted options are: #{accepted.inspect}"
|
164
|
+
end
|
165
|
+
yield if block_given?
|
166
|
+
end
|
122
167
|
|
123
|
-
|
124
|
-
|
168
|
+
# Returns the columns and lines of the current tty.
|
169
|
+
#
|
170
|
+
# @return [Integer]
|
171
|
+
# number of columns and lines of tty, returns [0, 0] if no tty is present.
|
172
|
+
#
|
173
|
+
# @api public
|
174
|
+
def terminal_dimensions
|
175
|
+
[0, 0] unless STDOUT.tty?
|
176
|
+
[80, 40] if OS.windows?
|
125
177
|
|
178
|
+
if ENV['COLUMNS'] && ENV['LINES']
|
179
|
+
[ENV['COLUMNS'].to_i, ENV['LINES'].to_i]
|
180
|
+
elsif ENV['TERM'] && command_in_path?('tput')
|
181
|
+
[`tput cols`.to_i, `tput lines`.to_i]
|
126
182
|
elsif command_in_path?('stty')
|
127
|
-
|
183
|
+
`stty size`.scan(/\d+/).map {|s| s.to_i }
|
128
184
|
else
|
129
|
-
|
185
|
+
[0, 0]
|
130
186
|
end
|
131
187
|
rescue
|
132
188
|
[0, 0]
|
133
189
|
end
|
190
|
+
|
191
|
+
# Checks in PATH returns true if the command is found
|
192
|
+
def command_in_path?(command)
|
193
|
+
found = ENV['PATH'].split(File::PATH_SEPARATOR).map do |p|
|
194
|
+
File.exist?(File.join(p, command))
|
195
|
+
end
|
196
|
+
found.include?(true)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Runs a code block, and retries it when an exception occurs. Should the
|
200
|
+
# number of retries be reached without success, the last exception will be
|
201
|
+
# raised.
|
202
|
+
#
|
203
|
+
# @param opts [Hash{Symbol => Value}]
|
204
|
+
# @option opts [Fixnum] :tries
|
205
|
+
# number of attempts to retry before raising the last exception
|
206
|
+
# @option opts [Fixnum] :sleep
|
207
|
+
# number of seconds to wait between retries, use lambda to exponentially
|
208
|
+
# increasing delay between retries
|
209
|
+
# @option opts [Array(Exception)] :on
|
210
|
+
# the type of exception(s) to catch and retry on
|
211
|
+
# @option opts [Regex] :matching
|
212
|
+
# match based on the exception message
|
213
|
+
# @option opts [Block] :ensure
|
214
|
+
# ensure a block of code is executed, regardless of whether an exception
|
215
|
+
# is raised
|
216
|
+
#
|
217
|
+
# @return [Block]
|
218
|
+
#
|
219
|
+
def retrier(opts = {}, &block)
|
220
|
+
defaults = {
|
221
|
+
tries: 2,
|
222
|
+
sleep: 1,
|
223
|
+
on: StandardError,
|
224
|
+
matching: /.*/,
|
225
|
+
:ensure => Proc.new {}
|
226
|
+
}
|
227
|
+
|
228
|
+
check_for_invalid_options(opts, defaults)
|
229
|
+
defaults.merge!(opts)
|
230
|
+
|
231
|
+
return if defaults[:tries] == 0
|
232
|
+
|
233
|
+
on_exception, tries = [defaults[:on]].flatten, defaults[:tries]
|
234
|
+
retries = 0
|
235
|
+
retry_exception = nil
|
236
|
+
|
237
|
+
begin
|
238
|
+
yield retries, retry_exception
|
239
|
+
rescue *on_exception => exception
|
240
|
+
raise unless exception.message =~ defaults[:matching]
|
241
|
+
raise if retries+1 >= defaults[:tries]
|
242
|
+
|
243
|
+
# Interrupt Exception could be raised while sleeping
|
244
|
+
begin
|
245
|
+
sleep defaults[:sleep].respond_to?(:call) ?
|
246
|
+
defaults[:sleep].call(retries) : defaults[:sleep]
|
247
|
+
rescue *on_exception
|
248
|
+
end
|
249
|
+
|
250
|
+
retries += 1
|
251
|
+
retry_exception = exception
|
252
|
+
retry
|
253
|
+
ensure
|
254
|
+
defaults[:ensure].call(retries)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
private # P R O P R I E T À P R I V A T A Vietato L'accesso
|
259
|
+
|
260
|
+
def check_for_invalid_options(custom_options, defaults)
|
261
|
+
invalid_options = defaults.merge(custom_options).keys - defaults.keys
|
262
|
+
raise ArgumentError.new('[Retrier] Invalid options: ' \
|
263
|
+
"#{invalid_options.join(", ")}") unless invalid_options.empty?
|
264
|
+
end
|
134
265
|
end
|
data/lib/hoodie/version.rb
CHANGED
data/lib/hoodie.rb
CHANGED
@@ -19,9 +19,11 @@
|
|
19
19
|
|
20
20
|
require 'hoodie/stash/mem_store'
|
21
21
|
require 'hoodie/stash/disk_store'
|
22
|
+
require 'hoodie/identity_map'
|
22
23
|
require 'hoodie/memoizable'
|
23
24
|
require 'hoodie/obfuscate'
|
24
25
|
require 'hoodie/version'
|
26
|
+
require 'hoodie/timers'
|
25
27
|
require 'hoodie/utils'
|
26
28
|
require 'hoodie/stash'
|
27
29
|
require 'hoodie/blank'
|