stud 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,20 +10,26 @@ module Stud
10
10
  module Benchmark
11
11
  def self.run(iterations=1, &block)
12
12
  timer = Metriks::Timer.new
13
+ start = Time.now
13
14
  iterations.times { timer.time(&block) }
14
- return Results.new(timer)
15
+ duration = Time.now - start
16
+ return Results.new(timer, duration)
15
17
  end # def run
16
18
 
17
19
  def self.runtimed(seconds=10, &block)
18
20
  timer = Metriks::Timer.new
19
21
  expiration = Time.now + seconds
22
+
23
+ start = Time.now
20
24
  timer.time(&block) while Time.now < expiration
21
- return Results.new(timer)
25
+ duration = Time.now - start
26
+ return Results.new(timer, duration)
22
27
  end # def runtimed
23
28
 
24
29
  def self.cputimed(seconds=10, &block)
25
30
  timer = Metriks::Timer.new
26
31
  expiration = Time.now + seconds
32
+ start_usage = Stud::Benchmark::RUsage.get
27
33
  while Time.now < expiration
28
34
  start = Stud::Benchmark::RUsage.get
29
35
  block.call
@@ -31,7 +37,10 @@ module Stud
31
37
  cputime = (finish.user + finish.system) - (start.user + start.system)
32
38
  timer.update(cputime)
33
39
  end # while not expired
34
- return Results.new(timer)
40
+ finish_usage = Stud::Benchmark::RUsage.get
41
+ duration = (finish_usage.user + finish_usage.system) \
42
+ - (start_usage.user + start_usage.system)
43
+ return Results.new(timer, duration)
35
44
  end # self.cpu
36
45
 
37
46
  class Results
@@ -53,8 +62,9 @@ module Stud
53
62
  #tick
54
63
  #end.flatten
55
64
 
56
- def initialize(data)
65
+ def initialize(data, duration)
57
66
  @data = data
67
+ @duration = duration
58
68
  end # def initialize
59
69
 
60
70
  def environment
@@ -79,6 +89,10 @@ module Stud
79
89
  return @data.max
80
90
  end
81
91
 
92
+ def rate
93
+ return @data.count / @duration
94
+ end
95
+
82
96
  def mean
83
97
  return @data.mean
84
98
  end # def mean
@@ -0,0 +1,260 @@
1
+ module Stud
2
+
3
+ # @author {Alex Dean}[http://github.com/alexdean]
4
+ #
5
+ # Implements a generic framework for accepting events which are later flushed
6
+ # in batches. Flushing occurrs whenever +:max_items+ or +:max_interval+ (seconds)
7
+ # has been reached.
8
+ #
9
+ # Including class must implement +flush+, which will be called with all accumulated
10
+ # items either when the output buffer fills (+:max_items+) or when a fixed amount
11
+ # of time (+:max_interval+) passes.
12
+ #
13
+ # == batch_receive and flush
14
+ # General receive/flush can be implemented in one of two ways.
15
+ #
16
+ # === batch_receive(event) / flush(events)
17
+ # +flush+ will receive an array of events which were passed to +buffer_receive+.
18
+ #
19
+ # batch_receive('one')
20
+ # batch_receive('two')
21
+ #
22
+ # will cause a flush invocation like
23
+ #
24
+ # flush(['one', 'two'])
25
+ #
26
+ # === batch_receive(event, group) / flush(events, group)
27
+ # flush() will receive an array of events, plus a grouping key.
28
+ #
29
+ # batch_receive('one', :server => 'a')
30
+ # batch_receive('two', :server => 'b')
31
+ # batch_receive('three', :server => 'a')
32
+ # batch_receive('four', :server => 'b')
33
+ #
34
+ # will result in the following flush calls
35
+ #
36
+ # flush(['one', 'three'], {:server => 'a'})
37
+ # flush(['two', 'four'], {:server => 'b'})
38
+ #
39
+ # Grouping keys can be anything which are valid Hash keys. (They don't have to
40
+ # be hashes themselves.) Strings or Fixnums work fine. Use anything which you'd
41
+ # like to receive in your +flush+ method to help enable different handling for
42
+ # various groups of events.
43
+ #
44
+ # == on_flush_error
45
+ # Including class may implement +on_flush_error+, which will be called with an
46
+ # Exception instance whenever buffer_flush encounters an error.
47
+ #
48
+ # * +buffer_flush+ will automatically re-try failed flushes, so +on_flush_error+
49
+ # should not try to implement retry behavior.
50
+ # * Exceptions occurring within +on_flush_error+ are not handled by
51
+ # +buffer_flush+.
52
+ #
53
+ # == on_full_buffer_receive
54
+ # Including class may implement +on_full_buffer_receive+, which will be called
55
+ # whenever +buffer_receive+ is called while the buffer is full.
56
+ #
57
+ # +on_full_buffer_receive+ will receive a Hash like <code>{:pending => 30,
58
+ # :outgoing => 20}</code> which describes the internal state of the module at
59
+ # the moment.
60
+ #
61
+ # == final flush
62
+ # Including class should call <code>buffer_flush(:final => true)</code> during a teardown/
63
+ # shutdown routine (after the last call to buffer_receive) to ensure that all
64
+ # accumulated messages are flushed.
65
+ module Buffer
66
+
67
+ public
68
+ # Initialize the buffer.
69
+ #
70
+ # Call directly from your constructor if you wish to set some non-default
71
+ # options. Otherwise buffer_initialize will be called automatically during the
72
+ # first buffer_receive call.
73
+ #
74
+ # Options:
75
+ # * :max_items, Max number of items to buffer before flushing. Default 50.
76
+ # * :max_interval, Max number of seconds to wait between flushes. Default 5.
77
+ # * :logger, A logger to write log messages to. No default. Optional.
78
+ #
79
+ # @param [Hash] options
80
+ def buffer_initialize(options={})
81
+ if ! self.class.method_defined?(:flush)
82
+ raise ArgumentError, "Any class including Stud::Buffer must define a flush() method."
83
+ end
84
+
85
+ @buffer_config = {
86
+ :max_items => options[:max_items] || 50,
87
+ :max_interval => options[:max_interval] || 5,
88
+ :logger => options[:logger] || nil,
89
+ :has_on_flush_error => self.class.method_defined?(:on_flush_error),
90
+ :has_on_full_buffer_receive => self.class.method_defined?(:on_full_buffer_receive)
91
+ }
92
+ @buffer_state = {
93
+ # items accepted from including class
94
+ :pending_items => {},
95
+ :pending_count => 0,
96
+
97
+ # guard access to pending_items & pending_count
98
+ :pending_mutex => Mutex.new,
99
+
100
+ # items which are currently being flushed
101
+ :outgoing_items => {},
102
+ :outgoing_count => 0,
103
+
104
+ # ensure only 1 flush is operating at once
105
+ :flush_mutex => Mutex.new,
106
+
107
+ # data for timed flushes
108
+ :last_flush => Time.now.to_i,
109
+ :timer => Thread.new do
110
+ loop do
111
+ sleep(@buffer_config[:max_interval])
112
+ buffer_flush(:force => true)
113
+ end
114
+ end
115
+ }
116
+
117
+ # events we've accumulated
118
+ buffer_clear_pending
119
+ end
120
+
121
+ # Determine if +:max_items+ has been reached.
122
+ #
123
+ # buffer_receive calls will block while <code>buffer_full? == true</code>.
124
+ #
125
+ # @return [bool] Is the buffer full?
126
+ def buffer_full?
127
+ @buffer_state[:pending_count] + @buffer_state[:outgoing_count] >= @buffer_config[:max_items]
128
+ end
129
+
130
+ # Save an event for later delivery
131
+ #
132
+ # Events are grouped by the (optional) group parameter you provide.
133
+ # Groups of events, plus the group name, are later passed to +flush+.
134
+ #
135
+ # This call will block if +:max_items+ has been reached.
136
+ #
137
+ # @see Stud::Buffer The overview has more information on grouping and flushing.
138
+ #
139
+ # @param event An item to buffer for flushing later.
140
+ # @param group Optional grouping key. All events with the same key will be
141
+ # passed to +flush+ together, along with the grouping key itself.
142
+ def buffer_receive(event, group=nil)
143
+ buffer_initialize if ! @buffer_state
144
+
145
+ # block if we've accumulated too many events
146
+ while buffer_full? do
147
+ on_full_buffer_receive(
148
+ :pending => @buffer_state[:pending_count],
149
+ :outgoing => @buffer_state[:outgoing_count]
150
+ ) if @buffer_config[:has_on_full_buffer_receive]
151
+ sleep 0.1
152
+ end
153
+
154
+ @buffer_state[:pending_mutex].synchronize do
155
+ @buffer_state[:pending_items][group] << event
156
+ @buffer_state[:pending_count] += 1
157
+ end
158
+
159
+ buffer_flush
160
+ end
161
+
162
+ # Try to flush events.
163
+ #
164
+ # Returns immediately if flushing is not necessary/possible at the moment:
165
+ # * :max_items have not been accumulated
166
+ # * :max_interval seconds have not elapased since the last flush
167
+ # * another flush is in progress
168
+ #
169
+ # <code>buffer_flush(:force => true)</code> will cause a flush to occur even
170
+ # if +:max_items+ or +:max_interval+ have not been reached. A forced flush
171
+ # will still return immediately (without flushing) if another flush is
172
+ # currently in progress.
173
+ #
174
+ # <code>buffer_flush(:final => true)</code> is identical to <code>buffer_flush(:force => true)</code>,
175
+ # except that if another flush is already in progress, <code>buffer_flush(:final => true)</code>
176
+ # will block/wait for the other flush to finish before proceeding.
177
+ #
178
+ # @param [Hash] options Optional. May be <code>{:force => true}</code> or <code>{:final => true}</code>.
179
+ # @return [Fixnum] The number of items successfully passed to +flush+.
180
+ def buffer_flush(options={})
181
+ force = options[:force] || options[:final]
182
+ final = options[:final]
183
+
184
+ # final flush will wait for lock, so we are sure to flush out all buffered events
185
+ if options[:final]
186
+ @buffer_state[:flush_mutex].lock
187
+ elsif ! @buffer_state[:flush_mutex].try_lock # failed to get lock, another flush already in progress
188
+ return 0
189
+ end
190
+
191
+ items_flushed = 0
192
+
193
+ begin
194
+ time_since_last_flush = Time.now.to_i - @buffer_state[:last_flush]
195
+
196
+ return 0 if @buffer_state[:pending_count] == 0
197
+ return 0 if (!force) &&
198
+ (@buffer_state[:pending_count] < @buffer_config[:max_items]) &&
199
+ (time_since_last_flush < @buffer_config[:max_interval])
200
+
201
+ @buffer_state[:pending_mutex].synchronize do
202
+ @buffer_state[:outgoing_items] = @buffer_state[:pending_items]
203
+ @buffer_state[:outgoing_count] = @buffer_state[:pending_count]
204
+ buffer_clear_pending
205
+ end
206
+
207
+ @buffer_config[:logger].debug("Flushing output",
208
+ :outgoing_count => @buffer_state[:outgoing_count],
209
+ :time_since_last_flush => time_since_last_flush,
210
+ :outgoing_events => @buffer_state[:outgoing_items],
211
+ :batch_timeout => @buffer_config[:max_interval],
212
+ :force => force,
213
+ :final => final
214
+ ) if @buffer_config[:logger]
215
+
216
+ @buffer_state[:outgoing_items].each do |group, events|
217
+ begin
218
+ if group.nil?
219
+ flush(events)
220
+ else
221
+ flush(events, group)
222
+ end
223
+
224
+ @buffer_state[:outgoing_items].delete(group)
225
+ events_size = events.size
226
+ @buffer_state[:outgoing_count] -= events_size
227
+ items_flushed += events_size
228
+
229
+ rescue => e
230
+
231
+ @buffer_config[:logger].warn("Failed to flush outgoing items",
232
+ :outgoing_count => @buffer_state[:outgoing_count],
233
+ :exception => e,
234
+ :backtrace => e.backtrace
235
+ ) if @buffer_config[:logger]
236
+
237
+ if @buffer_config[:has_on_flush_error]
238
+ on_flush_error e
239
+ end
240
+
241
+ sleep 1
242
+ retry
243
+ end
244
+ @buffer_state[:last_flush] = Time.now.to_i
245
+ end
246
+
247
+ ensure
248
+ @buffer_state[:flush_mutex].unlock
249
+ end
250
+
251
+ items_flushed
252
+ end
253
+
254
+ private
255
+ def buffer_clear_pending
256
+ @buffer_state[:pending_items] = Hash.new { |h, k| h[k] = [] }
257
+ @buffer_state[:pending_count] = 0
258
+ end
259
+ end
260
+ end
data/lib/stud/trap.rb CHANGED
@@ -1,4 +1,23 @@
1
1
  module Stud
2
+ # Bind a block to be called when a certain signal is received.
3
+ #
4
+ # Same arguments to Signal::trap.
5
+ #
6
+ # The behavior of this method is different than Signal::trap because
7
+ # multiple handlers can request notification for the same signal.
8
+ #
9
+ # For example, this is valid:
10
+ #
11
+ # Stud.trap("INT") { puts "Hello" }
12
+ # Stud.trap("INT") { puts "World" }
13
+ #
14
+ # When SIGINT is received, both callbacks will be invoked, in order.
15
+ #
16
+ # This helps avoid the situation where a library traps a signal outside of
17
+ # your control.
18
+ #
19
+ # If something has already used Signal::trap, that callback will be saved
20
+ # and scheduled the same way as any other Stud::trap.
2
21
  def self.trap(signal, &block)
3
22
  @traps ||= Hash.new { |h,k| h[k] = [] }
4
23
 
@@ -16,13 +35,25 @@ module Stud
16
35
  end
17
36
 
18
37
  @traps[signal] << block
19
- end
20
38
 
39
+ return block.id
40
+ end # def self.trap
41
+
42
+ # Simulate a signal. This lets you force an interrupt without
43
+ # sending a signal to yourself.
21
44
  def self.simulate_signal(signal)
22
- puts "Simulate: #{signal} w/ #{@traps[signal].count} callbacks"
45
+ #puts "Simulate: #{signal} w/ #{@traps[signal].count} callbacks"
23
46
  @traps[signal].each(&:call)
24
- end
25
- end
47
+ end # def self.simulate_signal
48
+
49
+ # Remove a previously set signal trap.
50
+ #
51
+ # 'signal' is the name of the signal ("INT", etc)
52
+ # 'id' is the value returned by a previous Stud.trap() call
53
+ def self.untrap(signal, id)
54
+ @traps[signal].delete_if { |block| block.id == id }
55
+ end # def self.untrap
56
+ end # module Studd
26
57
 
27
58
  # Monkey-patch the main 'trap' stuff? This could be useful.
28
59
  #module Signal
metadata CHANGED
@@ -1,130 +1,123 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stud
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
5
- prerelease:
4
+ version: 0.0.11
5
+ prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Jordan Sissel
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-15 00:00:00.000000000 Z
12
+ date: 2013-02-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: metriks
16
- version_requirements: !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
17
18
  requirements:
18
- - - ">="
19
+ - - ! '>='
19
20
  - !ruby/object:Gem::Version
20
- version: !binary |-
21
- MA==
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
22
25
  none: false
23
- requirement: !ruby/object:Gem::Requirement
24
26
  requirements:
25
- - - ">="
27
+ - - ! '>='
26
28
  - !ruby/object:Gem::Version
27
- version: !binary |-
28
- MA==
29
- none: false
30
- prerelease: false
31
- type: :runtime
29
+ version: '0'
32
30
  - !ruby/object:Gem::Dependency
33
31
  name: ffi
34
- version_requirements: !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
35
34
  requirements:
36
- - - ">="
35
+ - - ! '>='
37
36
  - !ruby/object:Gem::Version
38
- version: !binary |-
39
- MA==
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
40
41
  none: false
41
- requirement: !ruby/object:Gem::Requirement
42
42
  requirements:
43
- - - ">="
43
+ - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
- version: !binary |-
46
- MA==
47
- none: false
48
- prerelease: false
49
- type: :runtime
45
+ version: '0'
50
46
  - !ruby/object:Gem::Dependency
51
47
  name: rspec
52
- version_requirements: !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
53
50
  requirements:
54
- - - ">="
51
+ - - ! '>='
55
52
  - !ruby/object:Gem::Version
56
- version: !binary |-
57
- MA==
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
58
57
  none: false
59
- requirement: !ruby/object:Gem::Requirement
60
58
  requirements:
61
- - - ">="
59
+ - - ! '>='
62
60
  - !ruby/object:Gem::Version
63
- version: !binary |-
64
- MA==
65
- none: false
66
- prerelease: false
67
- type: :development
61
+ version: '0'
68
62
  - !ruby/object:Gem::Dependency
69
63
  name: insist
70
- version_requirements: !ruby/object:Gem::Requirement
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
71
66
  requirements:
72
- - - ">="
67
+ - - ! '>='
73
68
  - !ruby/object:Gem::Version
74
- version: !binary |-
75
- MA==
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
76
73
  none: false
77
- requirement: !ruby/object:Gem::Requirement
78
74
  requirements:
79
- - - ">="
75
+ - - ! '>='
80
76
  - !ruby/object:Gem::Version
81
- version: !binary |-
82
- MA==
83
- none: false
84
- prerelease: false
85
- type: :development
86
- description: small reusable bits of code I'm tired of writing over and over. A library form of my software-patterns github repo.
77
+ version: '0'
78
+ description: small reusable bits of code I'm tired of writing over and over. A library
79
+ form of my software-patterns github repo.
87
80
  email: jls@semicomplete.com
88
81
  executables: []
89
82
  extensions: []
90
83
  extra_rdoc_files: []
91
84
  files:
92
- - lib/stud/task.rb
93
- - lib/stud/benchmark.rb
85
+ - lib/stud/benchmark/rusage.rb
86
+ - lib/stud/interval.rb
94
87
  - lib/stud/pool.rb
88
+ - lib/stud/secret.rb
95
89
  - lib/stud/try.rb
96
- - lib/stud/interval.rb
90
+ - lib/stud/task.rb
91
+ - lib/stud/buffer.rb
92
+ - lib/stud/benchmark.rb
97
93
  - lib/stud/trap.rb
98
- - lib/stud/secret.rb
99
- - lib/stud/benchmark/rusage.rb
100
94
  - LICENSE
101
95
  - CHANGELIST
102
96
  - README.md
103
97
  homepage: https://github.com/jordansissel/ruby-stud
104
98
  licenses: []
105
- post_install_message:
99
+ post_install_message:
106
100
  rdoc_options: []
107
101
  require_paths:
108
102
  - lib
109
103
  - lib
110
104
  required_ruby_version: !ruby/object:Gem::Requirement
105
+ none: false
111
106
  requirements:
112
- - - ">="
107
+ - - ! '>='
113
108
  - !ruby/object:Gem::Version
114
- version: !binary |-
115
- MA==
116
- none: false
109
+ version: '0'
117
110
  required_rubygems_version: !ruby/object:Gem::Requirement
111
+ none: false
118
112
  requirements:
119
- - - ">="
113
+ - - ! '>='
120
114
  - !ruby/object:Gem::Version
121
- version: !binary |-
122
- MA==
123
- none: false
115
+ version: '0'
124
116
  requirements: []
125
- rubyforge_project:
117
+ rubyforge_project:
126
118
  rubygems_version: 1.8.24
127
- signing_key:
119
+ signing_key:
128
120
  specification_version: 3
129
121
  summary: stud - common code techniques
130
122
  test_files: []
123
+ has_rdoc: