howler 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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