howler 1.0.0

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 (60) hide show
  1. data/.gemrc +1 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/.rvmrc +1 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +68 -0
  7. data/LICENSE +19 -0
  8. data/README.md +104 -0
  9. data/bin/howler +3 -0
  10. data/howler.gemspec +24 -0
  11. data/lib/howler.rb +34 -0
  12. data/lib/howler/async.rb +15 -0
  13. data/lib/howler/config.ru +4 -0
  14. data/lib/howler/exceptions.rb +5 -0
  15. data/lib/howler/exceptions/error.rb +25 -0
  16. data/lib/howler/exceptions/failed.rb +9 -0
  17. data/lib/howler/exceptions/notify.rb +15 -0
  18. data/lib/howler/exceptions/retry.rb +12 -0
  19. data/lib/howler/manager.rb +123 -0
  20. data/lib/howler/message.rb +15 -0
  21. data/lib/howler/queue.rb +138 -0
  22. data/lib/howler/runner.rb +34 -0
  23. data/lib/howler/support/config.rb +33 -0
  24. data/lib/howler/support/logger.rb +57 -0
  25. data/lib/howler/support/util.rb +23 -0
  26. data/lib/howler/support/version.rb +3 -0
  27. data/lib/howler/web.rb +47 -0
  28. data/lib/howler/web/public/application.css +24 -0
  29. data/lib/howler/web/public/bootstrap.css +3990 -0
  30. data/lib/howler/web/public/bootstrap.min.css +689 -0
  31. data/lib/howler/web/public/queues.css +19 -0
  32. data/lib/howler/web/views/failed_messages.erb +27 -0
  33. data/lib/howler/web/views/html.erb +10 -0
  34. data/lib/howler/web/views/index.erb +11 -0
  35. data/lib/howler/web/views/navigation.erb +25 -0
  36. data/lib/howler/web/views/notification_messages.erb +24 -0
  37. data/lib/howler/web/views/notifications.erb +15 -0
  38. data/lib/howler/web/views/pending_messages.erb +24 -0
  39. data/lib/howler/web/views/processed_messages.erb +28 -0
  40. data/lib/howler/web/views/queue.erb +36 -0
  41. data/lib/howler/web/views/queue_table.erb +27 -0
  42. data/lib/howler/web/views/queues.erb +15 -0
  43. data/lib/howler/worker.rb +17 -0
  44. data/spec/models/async_spec.rb +76 -0
  45. data/spec/models/exceptions/failed_spec.rb +15 -0
  46. data/spec/models/exceptions/message_spec.rb +53 -0
  47. data/spec/models/exceptions/notify_spec.rb +26 -0
  48. data/spec/models/exceptions/retry_spec.rb +49 -0
  49. data/spec/models/howler_spec.rb +69 -0
  50. data/spec/models/manager_spec.rb +397 -0
  51. data/spec/models/message_spec.rb +78 -0
  52. data/spec/models/queue_spec.rb +539 -0
  53. data/spec/models/runner_spec.rb +109 -0
  54. data/spec/models/support/config_spec.rb +56 -0
  55. data/spec/models/support/logger_spec.rb +147 -0
  56. data/spec/models/support/util_spec.rb +44 -0
  57. data/spec/models/worker_spec.rb +54 -0
  58. data/spec/requests/web_spec.rb +220 -0
  59. data/spec/spec_helper.rb +93 -0
  60. metadata +265 -0
@@ -0,0 +1,26 @@
1
+ require "spec_helper"
2
+
3
+ describe Howler::Message::Notify do
4
+ let!(:ex) { generate_exception }
5
+ subject { Howler::Message::Notify.new(ex) }
6
+
7
+ it "should inherit from Howler::Message::Error" do
8
+ Howler::Message::Notify.ancestors.should include(Howler::Message::Error)
9
+ end
10
+
11
+ describe "#cause" do
12
+ it "should store the cause" do
13
+ subject.cause.should == ex
14
+ end
15
+ end
16
+
17
+ describe "#env" do
18
+ it "should store the hostname" do
19
+ subject.env['hostname'].should == `hostname`.chomp
20
+ end
21
+
22
+ it "should store the ruby version" do
23
+ subject.env['ruby_version'].should == `ruby -v`.chomp
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,49 @@
1
+ require "spec_helper"
2
+
3
+ describe Howler::Message::Retry do
4
+ it "should inherit from Howler::Message::Error" do
5
+ Howler::Message::Retry.ancestors.should include(Howler::Message::Error)
6
+ end
7
+
8
+ describe "#at" do
9
+ subject { Howler::Message::Retry.new(:at => Time.now.utc + 1.day) }
10
+
11
+ it "should store the retry at time" do
12
+ Timecop.freeze(DateTime.now) do
13
+ subject.at.should == Time.now.utc + 1.day
14
+ end
15
+ end
16
+
17
+ describe "when given the after attribute" do
18
+ subject { Howler::Message::Retry.new(:after => 5.minutes) }
19
+
20
+ it "should set the retry at value" do
21
+ Timecop.freeze(DateTime.now) do
22
+ subject.at.should == Time.now.utc + 5.minutes
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ describe "#ttl" do
29
+ describe "when not given the ttl" do
30
+ it "should default to zero" do
31
+ subject.ttl.should == 0
32
+ end
33
+ end
34
+
35
+ describe "when given the ttl" do
36
+ subject { Howler::Message::Retry.new(:ttl => 5.days) }
37
+
38
+ it "should store the ttl" do
39
+ subject.ttl.should_not be_nil
40
+ end
41
+
42
+ it "should be the 'ttl' minutes in the future" do
43
+ Timecop.freeze(DateTime.now) do
44
+ subject.ttl.should == Time.now.utc + 5.days
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,69 @@
1
+ require "spec_helper"
2
+
3
+ describe Howler do
4
+ describe ".next" do
5
+ before do
6
+ Howler.unstub(:next)
7
+ end
8
+
9
+ it "should return a number" do
10
+ Howler.next(:id).class.should == Fixnum
11
+ end
12
+
13
+ it "should be increase by one" do
14
+ before = Howler.next(:id)
15
+
16
+ (before + 1).should == Howler.next(:id)
17
+ end
18
+
19
+ describe "for multiple keys" do
20
+ before do
21
+ Howler.next(:foo)
22
+ end
23
+
24
+ it "should have different counts" do
25
+ [
26
+ Howler.next(:id),
27
+ Howler.next(:id),
28
+ Howler.next(:foo),
29
+ Howler.next(:foo)
30
+ ].should == [1, 2, 2, 3]
31
+ end
32
+ end
33
+ end
34
+
35
+ describe ".args" do
36
+ it "should remove square brackets" do
37
+ Howler.args([]).should == ""
38
+ end
39
+
40
+ it "should remove only leading and trailing brackets" do
41
+ Howler.args([10, {'akey' => 'avalue'}]).should == '10, {"akey"=>"avalue"}'
42
+ end
43
+ end
44
+
45
+ describe ".redis" do
46
+ let(:pool) { mock("ConnectionPool2") }
47
+
48
+ xit "should be a ConnectionPool" do
49
+ ConnectionPool.should_receive(:new).with(:timeout => 1, :size => 5)
50
+
51
+ Howler.redis
52
+ end
53
+
54
+ xit "should cache the ConnectionPool" do
55
+ ConnectionPool.stub(:new).and_return(pool, mock("ConnPool"))
56
+
57
+ Howler.redis.should == Howler.redis
58
+ end
59
+
60
+ xit "should be an key-value store" do
61
+ Howler.redis.stub(:with).and_yield(Howler.send(:_redis))
62
+
63
+ Howler.redis.with do |redis|
64
+ redis.set("key", "value")
65
+ redis.get("key").should == "value"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,397 @@
1
+ require "spec_helper"
2
+
3
+ describe Howler::Manager do
4
+ subject { Howler::Manager.new }
5
+
6
+ before do
7
+ subject.wrapped_object.stub(:sleep)
8
+ Howler::Manager.stub(:current).and_return(subject)
9
+ end
10
+
11
+ describe ".new" do
12
+ it "should create a Logger" do
13
+ Howler::Logger.should_receive(:new)
14
+
15
+ Howler::Manager.new
16
+ end
17
+ end
18
+
19
+ describe ".current" do
20
+ before do
21
+ subject.wrapped_object.stub(:sleep)
22
+ end
23
+
24
+ it "should return the current manager instance" do
25
+ Howler::Manager.current.wrapped_object.class.should == Howler::Manager
26
+ end
27
+ end
28
+
29
+ describe "#run" do
30
+ def build_message(klass, method)
31
+ Howler::Message.new(
32
+ 'class' => klass.to_s,
33
+ 'method' => method,
34
+ 'args' => [],
35
+ 'created_at' => Time.now.to_f
36
+ )
37
+ end
38
+
39
+ before do
40
+ subject.wrapped_object.stub(:done?).and_return(true)
41
+ Howler::Config[:concurrency] = 10
42
+ end
43
+
44
+ it "should create workers" do
45
+ Howler::Worker.should_receive(:new_link).exactly(10)
46
+
47
+ subject.run
48
+ end
49
+
50
+ describe "when there are no pending messages" do
51
+ before do
52
+ subject.wrapped_object.stub(:done?).and_return(false, true)
53
+ end
54
+
55
+ class SampleEx < Exception; end
56
+
57
+ describe "when there are no messages" do
58
+ it "should sleep for one second" do
59
+ subject.wrapped_object.should_receive(:sleep).with(1)
60
+
61
+ subject.run
62
+ end
63
+ end
64
+ end
65
+
66
+ describe "when there are pending messages" do
67
+ before do
68
+ Howler::Config[:concurrency] = 3
69
+
70
+ @workers = 3.times.collect do
71
+ mock(Howler::Worker, :perform! => nil)
72
+ end
73
+
74
+ subject.wrapped_object.stub(:build_workers).and_return(@workers)
75
+ subject.wrapped_object.stub(:sleep)
76
+ subject.wrapped_object.stub(:done?).and_return(false, true)
77
+
78
+ @messages = {
79
+ 'length' => build_message(Array, :length),
80
+ 'collect' => build_message(Array, :collect),
81
+ 'max' => build_message(Array, :max),
82
+ 'to_s' => build_message(Array, :to_s)
83
+ }
84
+
85
+ %w(length collect max to_s).each do |method|
86
+ Howler::Message.stub(:new).with(hash_including('method' => method)).and_return(@messages[method])
87
+ end
88
+ end
89
+
90
+ describe "when there is a single message in the queue" do
91
+ before do
92
+ subject.push(Array, :length, [])
93
+ end
94
+
95
+ it "should not sleep" do
96
+ subject.wrapped_object.should_not_receive(:sleep)
97
+
98
+ subject.run
99
+ end
100
+
101
+ it "should perform the message on a worker" do
102
+ @workers[2].should_receive(:perform!).with(@messages['length'], Howler::Queue::DEFAULT)
103
+
104
+ @workers[0].should_not_receive(:perform!)
105
+ @workers[1].should_not_receive(:perform!)
106
+
107
+ subject.run
108
+ end
109
+
110
+ describe "when a message gets taken by a worker" do
111
+ before do
112
+ @original_workers = @workers.dup
113
+ end
114
+
115
+ it "should make the worker unavailable" do
116
+ subject.run
117
+
118
+ subject.should have(2).workers
119
+ subject.should have(1).chewing
120
+
121
+ subject.workers.should == @original_workers.first(2)
122
+ subject.chewing.should == @original_workers.last(1)
123
+ end
124
+ end
125
+ end
126
+
127
+ describe "when there are many messages in the queue" do
128
+ before do
129
+ [:length, :collect, :max].each do |method|
130
+ subject.push(Array, method, [])
131
+ end
132
+ end
133
+
134
+ describe "more workers then messages" do
135
+ it "should perform all messages" do
136
+ @workers[2].should_receive(:perform!).with(@messages['length'], anything)
137
+ @workers[1].should_receive(:perform!).with(@messages['collect'], anything)
138
+ @workers[0].should_receive(:perform!).with(@messages['max'], anything)
139
+
140
+ subject.run
141
+ end
142
+ end
143
+
144
+ describe "more messages then workers" do
145
+ before do
146
+ subject.wrapped_object.stub(:done?).and_return(false, false, true)
147
+
148
+ Howler::Config[:concurrency] = 2
149
+ end
150
+
151
+ it "should scale and only remove as many messages as workers" do
152
+ @workers[0].unstub(:perform!)
153
+
154
+ @workers[1].should_receive(:perform!).with(@messages['length'], anything)
155
+ @workers[0].should_receive(:perform!).with(@messages['collect'], anything)
156
+
157
+ subject.run
158
+ end
159
+ end
160
+
161
+ describe "run messages in the future" do
162
+ let!(:worker) { mock(Howler::Worker) }
163
+
164
+ before do
165
+ subject.wrapped_object.stub(:done?).and_return(false, false, true)
166
+ Howler::Config[:concurrency] = 4
167
+
168
+ Howler::Worker.should_receive(:new).once.and_return(worker)
169
+
170
+ subject.push(Array, :to_s, [], Time.now + 5.minutes)
171
+ end
172
+
173
+ it "should only enqueue messages that are scheduled before now" do
174
+ Timecop.freeze(Time.now) do
175
+ worker.should_receive(:perform!).with(@messages['length'], anything).ordered
176
+ @workers[2].should_receive(:perform!).with(@messages['collect'], anything)
177
+ @workers[1].should_receive(:perform!).with(@messages['max'], anything)
178
+
179
+ subject.run
180
+
181
+ subject.wrapped_object.stub(:done?).and_return(false, true)
182
+
183
+ Timecop.travel(5.minutes) do
184
+ @workers[0].should_receive(:perform!).with(@messages['to_s'], anything).ordered
185
+ subject.run
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ describe "logging" do
195
+ let!(:logger) { mock(Howler::Logger) }
196
+ let!(:log) { mock(Howler::Logger, :info => nil, :debug => nil) }
197
+
198
+ before do
199
+ Howler::Config[:concurrency] = 3
200
+
201
+ @workers = 3.times.collect do
202
+ mock(Howler::Worker, :perform! => nil)
203
+ end
204
+
205
+ subject.wrapped_object.stub(:build_workers).and_return(@workers)
206
+ subject.wrapped_object.stub(:done?).and_return(false, true)
207
+ subject.wrapped_object.instance_variable_set(:@logger, logger)
208
+ logger.stub(:log).and_yield(log)
209
+
210
+ [:send_notification, :enforce_avgs].each_with_index do |method, i|
211
+ subject.push(Array, method, [i, ((i+1)*100).to_s(36)])
212
+ end
213
+ end
214
+
215
+ describe "information" do
216
+ before do
217
+ Howler::Config[:log] = 'info'
218
+ end
219
+
220
+ it "should log the number of messages to be processed" do
221
+ log.should_receive(:info).with("Processing 2 Messages")
222
+
223
+ subject.run
224
+ end
225
+ end
226
+
227
+ describe "debug" do
228
+ before do
229
+ Howler::Config[:log] = 'debug'
230
+ end
231
+
232
+ it "should show a digest of the messages" do
233
+ log.should_receive(:debug).with('MESG - 123 Array.new.send_notification(0, "2s")')
234
+ log.should_receive(:debug).with('MESG - 123 Array.new.enforce_avgs(1, "5k")')
235
+
236
+ subject.run
237
+ end
238
+ end
239
+ end
240
+
241
+ describe "#done_chewing" do
242
+ before do
243
+ worker = mock(Howler::Worker)
244
+ @chewing_worker = mock(Howler::Worker, :alive? => true)
245
+
246
+ subject.wrapped_object.stub(:build_workers).and_return([worker])
247
+ subject.wrapped_object.instance_variable_set(:@chewing, [@chewing_worker])
248
+
249
+ end
250
+
251
+ it "should remove the worker from chewing" do
252
+ subject.chewing.should include(@chewing_worker)
253
+
254
+ subject.done_chewing(@chewing_worker)
255
+
256
+ subject.chewing.should_not include(@chewing_worker)
257
+ end
258
+
259
+ it "should make the worker available" do
260
+ subject.workers.should_not include(@chewing_worker)
261
+
262
+ subject.done_chewing(@chewing_worker)
263
+
264
+ subject.workers.should include(@chewing_worker)
265
+ end
266
+
267
+ describe "when a worker has died" do
268
+ before do
269
+ @chewing_worker.stub(:alive?).and_return(false)
270
+ end
271
+
272
+ it "should make in un-available" do
273
+ subject.chewing.should include(@chewing_worker)
274
+
275
+ subject.done_chewing(@chewing_worker)
276
+
277
+ subject.chewing.should_not include(@chewing_worker)
278
+ subject.workers.should_not include(@chewing_worker)
279
+ end
280
+ end
281
+ end
282
+
283
+ describe "#worker_death" do
284
+ before do
285
+ subject.wrapped_object.stub(:done?).and_return(true)
286
+
287
+ worker = mock(Howler::Worker)
288
+ @chewing_worker = mock(Howler::Worker)
289
+ @chewing_workers = [@chewing_worker]
290
+
291
+ subject.wrapped_object.stub(:build_workers).and_return([worker])
292
+ subject.wrapped_object.instance_variable_set(:@chewing, @chewing_workers)
293
+
294
+ Howler::Config[:concurrency] = 3
295
+ subject.run
296
+ end
297
+
298
+ describe "when the worker is alive" do
299
+ before do
300
+ @chewing_worker.stub(:alive?).and_return(true)
301
+ end
302
+
303
+ it "should create a new worker" do
304
+ Howler::Worker.should_receive(:new_link)
305
+
306
+ subject.worker_death
307
+ end
308
+
309
+ it "should add a worker" do
310
+ subject.should have(1).workers
311
+
312
+ subject.worker_death
313
+
314
+ subject.should have(2).workers
315
+ end
316
+
317
+ it "should make in un-available" do
318
+ @chewing_workers.should_receive(:delete).with(@chewing_worker)
319
+
320
+ subject.worker_death(@chewing_worker)
321
+ end
322
+ end
323
+ end
324
+
325
+ describe "#shutdown" do
326
+ before do
327
+ subject.wrapped_object.stub(:done?).and_return(true)
328
+
329
+ Howler::Config[:concurrency] = 2
330
+ subject.wrapped_object.instance_variable_set(:@chewing, [mock(Howler::Worker)])
331
+ end
332
+
333
+ it "should not accept more work" do
334
+ subject.wrapped_object.unstub(:done?)
335
+ subject.should_not be_done
336
+
337
+ subject.shutdown
338
+
339
+ subject.should be_done
340
+ end
341
+
342
+ it "should remove non active workers from the list" do
343
+ subject.run
344
+
345
+ subject.should have(2).workers
346
+ subject.should have(1).chewing
347
+
348
+ subject.shutdown.should == 2
349
+
350
+ subject.should have(0).workers
351
+ subject.should have(1).chewing
352
+ end
353
+ end
354
+
355
+ describe "#push" do
356
+ let!(:queue) { Howler::Queue.new(Howler::Manager::DEFAULT) }
357
+
358
+ def create_message(klass, method, args)
359
+ {
360
+ :id => 123,
361
+ :class => klass.to_s,
362
+ :method => method,
363
+ :args => args,
364
+ :created_at => Time.now.to_f
365
+ }
366
+ end
367
+
368
+ before do
369
+ Howler::Queue.stub(:new).and_return(queue)
370
+ end
371
+
372
+ describe "when given a class, method, and name" do
373
+ it "should push a message" do
374
+ Timecop.freeze(DateTime.now) do
375
+ message = create_message("Array", :length, [1234])
376
+ queue.should_receive(:push).with(message, Time.now)
377
+
378
+ subject.push(Array, :length, [1234])
379
+ end
380
+ end
381
+
382
+ it "should enqueue the message" do
383
+ should_change(Howler::Manager::DEFAULT).length_by(1) do
384
+ subject.push(Array, :length, [])
385
+ end
386
+ end
387
+ end
388
+
389
+ describe "when given the 'wait until' time" do
390
+ it "should enqueue the message" do
391
+ should_change(Howler::Manager::DEFAULT).length_by(1) do
392
+ subject.push(Array, :length, [], Time.now + 5.minutes)
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end