hoodie 0.3.21 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # if unknown size we default to the typical unix default
34
- console_cols = 80 if console_cols == 0
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
- console_cols -= preamble
35
+ def camelize(underscored_word)
36
+ underscored_word.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
37
+ end
38
38
 
39
- # Return unaligned text if console window is too small
40
- return text if console_cols <= 0
39
+ def classify(table_name)
40
+ camelize singularize(table_name.to_s.sub(/.*\./, ''))
41
+ end
41
42
 
42
- # If console is 0 this implies unknown so we assume the common
43
- # minimal unix configuration of 80 characters
44
- console_cols = 80 if console_cols <= 0
43
+ def class_name
44
+ demodulize(self.class)
45
+ end
45
46
 
46
- text = text.split("\n")
47
- piece = ''
48
- whitespace = 0
47
+ def caller_name
48
+ caller_locations(2, 1).first.label
49
+ end
49
50
 
50
- text.each_with_index do |line, i|
51
- whitespace = 0
51
+ def demodulize(class_name_in_module)
52
+ class_name_in_module.to_s.sub(/^.*::/, '')
53
+ end
52
54
 
53
- while whitespace < line.length && line[whitespace].chr == ' '
54
- whitespace += 1
55
- end
55
+ def pluralize(word)
56
+ word.to_s.sub(/([^s])$/, '\1s')
57
+ end
56
58
 
57
- # If the current line is empty, indent it so that a snippet
58
- # from the previous line is aligned correctly.
59
- if line == ""
60
- line = (" " * whitespace)
61
- end
59
+ def singularize(word)
60
+ word.to_s.sub(/s$/, '').sub(/ie$/, 'y')
61
+ end
62
62
 
63
- # If text was snipped from the previous line, prepend it to the
64
- # current line after any current indentation.
65
- if piece != ''
66
- # Reset whitespaces to 0 if there are more whitespaces than there are
67
- # console columns
68
- whitespace = 0 if whitespace >= console_cols
69
-
70
- # If the current line is empty and being prepended to, create a new
71
- # empty line in the text so that formatting is preserved.
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
- piece = ''
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
- # Compare the line length to the allowed line length.
83
- # If it exceeds it, snip the offending text from the line
84
- # and store it so that it can be prepended to the next line.
85
- if line.length > (console_cols + preamble)
86
- reverse = console_cols
80
+ def request_id
81
+ SecureRandom.uuid
82
+ end
87
83
 
88
- while line[reverse].chr != ' '
89
- reverse -= 1
90
- end
84
+ def twenty_four_hours_ago
85
+ Time.now - ( 60 * 60 * 24)
86
+ end
91
87
 
92
- piece = line.slice!(reverse, (line.length - 1)).lstrip
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
- # If a snippet exists when all the columns in the text have been
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
- # Add the preamble to the line and add it to the text
104
- line = ((' ' * preamble) + line)
105
- text[i] = line
106
- end
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
- text.join("\n")
125
+ def pluralize(word)
126
+ word.to_s.sub(/([^s])$/, '\1s')
109
127
  end
110
128
 
111
- # Figures out the columns and lines of the current tty
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
- # Returns [0, 0] if it can't figure it out or if you're
114
- # not running on a tty
115
- def self.terminal_dimensions(stdout = STDOUT, environment = ENV)
116
- return [0, 0] unless stdout.tty?
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
- return [80, 40] if Util.windows?
154
+ def twenty_four_hours_ago
155
+ Time.now - ( 60 * 60 * 24)
156
+ end
119
157
 
120
- if environment["COLUMNS"] && environment["LINES"]
121
- return [environment["COLUMNS"].to_i, environment["LINES"].to_i]
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
- elsif environment["TERM"] && command_in_path?("tput")
124
- return [`tput cols`.to_i, `tput lines`.to_i]
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
- return `stty size`.scan(/\d+/).map {|s| s.to_i }
183
+ `stty size`.scan(/\d+/).map {|s| s.to_i }
128
184
  else
129
- return [0, 0]
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
@@ -18,5 +18,5 @@
18
18
  #
19
19
 
20
20
  module Hoodie
21
- VERSION = '0.3.21'
21
+ VERSION = '0.4.0'
22
22
  end
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'