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,109 @@
1
+ require "spec_helper"
2
+
3
+ describe Howler::Runner do
4
+ before do
5
+ subject.stub(:require)
6
+ subject.stub(:sleep)
7
+ end
8
+
9
+ describe "#run" do
10
+ let!(:manager) { Howler::Manager.current }
11
+
12
+ before do
13
+ Howler::Manager.stub(:current).and_return(manager)
14
+ manager.stub(:run!)
15
+ end
16
+
17
+ it "should create a manager" do
18
+ Howler::Manager.should_receive(:current)
19
+
20
+ subject.run
21
+ end
22
+
23
+ it "should run the manager" do
24
+ manager.should_receive(:run!)
25
+
26
+ subject.run
27
+ end
28
+
29
+ it "should sleep forever" do
30
+ subject.should_receive(:sleep).with(no_args)
31
+
32
+ subject.run
33
+ end
34
+
35
+ it "should load the Rails 3 environment" do
36
+ subject.should_receive(:require).with("./config/environment.rb")
37
+
38
+ subject.run
39
+ end
40
+
41
+ describe "when the runner receives an Interrupt" do
42
+ before do
43
+ manager.stub(:run!).and_raise(Interrupt)
44
+ Howler::Manager.current.stub(:chewing).and_return([])
45
+ end
46
+
47
+ it "should trap the interrupt" do
48
+ expect {
49
+ subject.run
50
+ }.not_to raise_error
51
+ end
52
+
53
+ describe "logging" do
54
+ let!(:plain_log) { Howler::Logger.new }
55
+ let!(:log) { mock(Howler::Logger::Proxy, :info => nil, :debug => nil) }
56
+
57
+ before do
58
+ Howler::Logger.stub(:new).and_return(plain_log)
59
+ plain_log.stub(:log).and_yield(log)
60
+
61
+ Howler::Config[:shutdown_timeout] = 6
62
+ end
63
+
64
+ it "should log information" do
65
+ log.should_receive(:info).with("INT - Stopping all workers")
66
+ plain_log.should_receive(:info).with("INT - All workers have shut down - Exiting")
67
+
68
+ subject.run
69
+ end
70
+
71
+ it "should log debugging" do
72
+ log.should_receive(:debug).ordered.with("INT - 0 workers still working.")
73
+
74
+ subject.run
75
+ end
76
+
77
+ describe "when there are workers still working" do
78
+ before do
79
+ Howler::Config[:concurrency] = 4
80
+
81
+ @chewing = 1.times.collect { mock(Howler::Worker) }
82
+
83
+ Howler::Manager.current.stub(:chewing).and_return(@chewing)
84
+ Howler::Manager.current.stub(:shutdown).and_return(3)
85
+ end
86
+
87
+ it "should log the number of seconds it will wait" do
88
+ log.should_receive(:info).with("INT - Waiting 6 seconds for workers to complete.")
89
+
90
+ subject.run
91
+ end
92
+
93
+ it "should wait the specified number of seconds" do
94
+ subject.should_receive(:sleep).with(6)
95
+
96
+ subject.run
97
+ end
98
+
99
+ it "should notify the user about the number of worker still working" do
100
+ log.should_receive(:debug).with("INT - 3 workers were shutdown immediately.")
101
+ log.should_receive(:debug).with("INT - 1 workers still working.")
102
+
103
+ subject.run
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,56 @@
1
+ require "spec_helper"
2
+
3
+ describe Howler::Config do
4
+ it "should have an attribute white-list" do
5
+ Howler::Config::WHITELIST.should == %w(concurrency shutdown_timeout)
6
+ end
7
+
8
+ describe ".[]" do
9
+ before do
10
+ Howler.redis.with {|redis| redis.hset("howler:config", "concurrency", "10") }
11
+ end
12
+
13
+ it "should configure options" do
14
+ Howler::Config[:concurrency].should == "10"
15
+ end
16
+ end
17
+
18
+ describe ".[]=" do
19
+ before do
20
+ Howler::Config[:message] = '{"key": 3}'
21
+ end
22
+
23
+ it "should configure options" do
24
+ Howler.redis.with {|redis| redis.hget("howler:config", "message") }.should == '{"key": 3}'
25
+ end
26
+
27
+ describe "when the value is nil" do
28
+ it "should remove the key" do
29
+ Howler.redis.with {|redis| redis.hexists("howler:config", "message") }.should == true
30
+
31
+ Howler::Config[:message] = nil
32
+
33
+ Howler.redis.with {|redis| redis.hexists("howler:config", "message") }.should == false
34
+ end
35
+ end
36
+ end
37
+
38
+ describe ".flush" do
39
+ before do
40
+ Howler::Config[:concurrency] = 10
41
+
42
+ [:message, :flag, :boolean].each do |key|
43
+ Howler::Config[key] = "unimportant value"
44
+ end
45
+ end
46
+ it "should clear all non-whitelisted config" do
47
+ Howler::Config.flush
48
+
49
+ [:message, :flag, :boolean].each do |key|
50
+ Howler.redis.with {|redis| redis.hget("howler:config", key.to_s) }.should be_nil
51
+ end
52
+
53
+ Howler::Config[:concurrency].to_i.should == 10
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,147 @@
1
+ require "spec_helper"
2
+
3
+ describe Howler::Logger do
4
+ let!(:logger) { mock(Logger, :info => nil, :formatter= => nil) }
5
+
6
+ before do
7
+ Logger.stub(:new).and_return(logger)
8
+ end
9
+
10
+ describe ".new" do
11
+ it "should log to stdout" do
12
+ Logger.should_receive(:new).with(STDOUT)
13
+
14
+ Howler::Logger.new
15
+ end
16
+
17
+ it "should use a custom formatter" do
18
+ logger.should_receive(:formatter=).with(Howler::Logger::DefaultFormatter)
19
+
20
+ Howler::Logger.new
21
+ end
22
+
23
+ describe "when there is log output" do
24
+ let!(:logger) { Logger.new(STDOUT) }
25
+
26
+ before do
27
+ Logger.stub(:new).and_return(logger)
28
+ end
29
+ end
30
+ end
31
+
32
+ describe "#info" do
33
+ describe "when called with a message" do
34
+ it "should log to info" do
35
+ logger.should_receive(:info).with("A pertinent piece of information.")
36
+
37
+ subject.info("A pertinent piece of information.")
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "#debug" do
43
+ describe "when called with a message" do
44
+ it "should log to debug" do
45
+ logger.should_receive(:debug).with("A pertinent piece of debug information.")
46
+
47
+ subject.debug("A pertinent piece of debug information.")
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#log" do
53
+ let(:worker) { "#<Worker id: 1>" }
54
+
55
+ describe "when given a block" do
56
+ describe "when on the main process" do
57
+ it "should log to the main process" do
58
+ logger.should_receive(:info).with("#<Worker id: 0 name: 'supervisor'>\n INFO: A supervisor level information bite.")
59
+
60
+ subject.log do |log|
61
+ log.info("A supervisor level information bite.")
62
+ end
63
+ end
64
+ end
65
+
66
+ it "should log information" do
67
+ logger.should_receive(:info).with("#<Worker id: 1>\n INFO: A pertinent piece of information.")
68
+
69
+ subject.log(worker) do |log|
70
+ log.info("A pertinent piece of information.")
71
+ end
72
+ end
73
+
74
+ describe "debugging" do
75
+ before do
76
+ Howler::Config[:log] = 'debug'
77
+ end
78
+
79
+ it "should be true" do
80
+ subject.log(worker) do |log|
81
+ log.debug("anything").should == true
82
+ end
83
+ end
84
+
85
+ it "should log debugging information" do
86
+ logger.should_receive(:info).with("#<Worker id: 1>\n DBUG: A pertinent piece of debug information.")
87
+
88
+ subject.log(worker) do |log|
89
+ log.debug("A pertinent piece of debug information.")
90
+ end
91
+ end
92
+
93
+ it "should log information and debugging information" do
94
+ logger.should_receive(:info).with("#<Worker id: 1>\n INFO: A pertinent piece of information.\n DBUG: A pertinent piece of debug information.")
95
+
96
+ subject.log(worker) do |log|
97
+ log.info("A pertinent piece of information.")
98
+ log.debug("A pertinent piece of debug information.")
99
+ end
100
+ end
101
+ end
102
+
103
+ describe "information" do
104
+ before do
105
+ Howler::Config[:log] = 'info'
106
+ end
107
+
108
+ it "should only log information" do
109
+ logger.should_not_receive(:info)
110
+
111
+ subject.log(worker) do |log|
112
+ log.debug("It's a debug thing.")
113
+ end
114
+ end
115
+
116
+ describe "when there is debugging" do
117
+ it "should return false" do
118
+ subject.log(worker) do |log|
119
+ log.debug("It's a debug thing.").should == false
120
+ end
121
+ end
122
+
123
+ it "should not store the debug string" do
124
+ subject.log(worker) do |log|
125
+ log.info("something")
126
+ log.debug("It's a debug thing.")
127
+
128
+ log.flush.should_not match(/It's a debug thing\./)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ describe Howler::Logger::DefaultFormatter do
138
+ describe ".call" do
139
+ it "should have a compact format" do
140
+ Timecop.freeze(DateTime.now) do
141
+ format = Howler::Logger::DefaultFormatter.call(nil, Time.now, nil, "A Message")
142
+ format.should =~ /\[#{Time.now.strftime('%Y-%m-%d %H:%I:%M:%9N')}\]/
143
+ format.should =~ /A Message\n/
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,44 @@
1
+ require "spec_helper"
2
+
3
+ describe Howler::Util do
4
+ describe ".constantize" do
5
+ it "should convert a string into a constant" do
6
+ Howler::Util.constantize("Array").should == Array
7
+ Howler::Util.constantize("Howler").should == Howler
8
+ Howler::Util.constantize("::Howler").should == Howler
9
+ Howler::Util.constantize("Howler::Util").should == Howler::Util
10
+ end
11
+
12
+ it "should raise a NameError" do
13
+ expect {
14
+ Howler::Util.constantize("a")
15
+ }.to raise_error(NameError)
16
+ end
17
+
18
+ it "should raise a NoMethodError" do
19
+ expect {
20
+ Howler::Util.constantize(nil)
21
+ }.to raise_error(NoMethodError)
22
+ end
23
+ end
24
+
25
+ describe ".now" do
26
+ it "should be properly formatted" do
27
+ Timecop.freeze(2012, 3, 24, 14, 30, 55) do
28
+ Howler::Util.now.should == "Mar 24 2012 14:30:55"
29
+ end
30
+ end
31
+ end
32
+
33
+ describe ".at" do
34
+ it "should be properly formatted" do
35
+ Timecop.freeze(2012, 3, 24, 14, 30, 55) do
36
+ Howler::Util.at(Time.now.to_f).should == "Mar 24 2012 14:30:55"
37
+ end
38
+ end
39
+
40
+ it "should handle a nil time" do
41
+ Howler::Util.at(nil).should == ""
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Howler::Worker do
4
+ describe "#perform" do
5
+ let!(:queue) { Howler::Queue.new }
6
+
7
+ def build_message
8
+ Howler::Message.new(
9
+ "class" => "Howler",
10
+ "method" => "length",
11
+ "args" => [1234]
12
+ )
13
+ end
14
+
15
+ before do
16
+ Howler::Manager.current.wrapped_object.stub(:done_chewing)
17
+ Howler::Queue.stub(:new).and_return(queue)
18
+ @message = build_message
19
+ end
20
+
21
+ it "should setup a Queue with the given queue name" do
22
+ Howler::Queue.should_receive(:new).with("a_queue")
23
+
24
+ subject.perform(@message, "a_queue")
25
+ end
26
+
27
+ it "should log statistics" do
28
+ queue.should_receive(:statistics).with(Howler, :length, [1234])
29
+
30
+ subject.perform(@message, "a_queue")
31
+ end
32
+
33
+ it "should execute the given message" do
34
+ array = mock(Howler)
35
+ Howler.should_receive(:new).and_return(array)
36
+
37
+ array.should_receive(:length).with(1234)
38
+
39
+ subject.perform(@message, "a_queue")
40
+ end
41
+
42
+ it "should use the specified queue" do
43
+ Howler::Queue.should_not_receive(:new)
44
+
45
+ subject.perform(@message, queue)
46
+ end
47
+
48
+ it "should register with the manager when done" do
49
+ Howler::Manager.current.wrapped_object.should_receive(:done_chewing).with(subject)
50
+
51
+ subject.perform(@message, "a_queue")
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,220 @@
1
+ require "spec_helper"
2
+
3
+ describe "web" do
4
+ include Capybara::DSL
5
+ let!(:queue) { Howler::Queue.new }
6
+ let(:message) do
7
+ Howler::Message.new(
8
+ 'class' => 'Array',
9
+ 'method' => :length,
10
+ 'args' => [2345]
11
+ )
12
+ end
13
+
14
+ describe "#root" do
15
+ it "when viewing the navigation bar" do
16
+ visit "/"
17
+
18
+ within "#navigation" do
19
+ page.should have_content("Howler")
20
+ page.should have_content("Queues")
21
+ page.should have_content("Notifications")
22
+ page.should have_content("Statistics")
23
+ page.should have_content("Settings")
24
+ end
25
+ end
26
+
27
+ it "when navigating to Queues#index" do
28
+ visit "/"
29
+ click_link "Queues"
30
+
31
+ current_path.should == "/queues"
32
+ end
33
+
34
+ it "when navigating to Notifications#index" do
35
+ visit "/"
36
+ click_link "Notifications"
37
+
38
+ current_path.should == "/notifications"
39
+ end
40
+
41
+ it "when navigating to Statistics#index" do
42
+ visit "/"
43
+ click_link "Statistics"
44
+
45
+ current_path.should == "/statistics"
46
+ end
47
+
48
+ it "when navigating to Settings#index" do
49
+ visit "/"
50
+ click_link "Settings"
51
+
52
+ current_path.should == "/settings"
53
+ end
54
+ end
55
+
56
+ describe "Queues#index" do
57
+ it "when navigating to Queues#show" do
58
+ visit "/queues"
59
+
60
+ within "#queue_#{queue.id}" do
61
+ click_link "More..."
62
+ end
63
+
64
+ current_path.should == "/queues/#{queue.id}"
65
+
66
+ within "##{queue.id}" do
67
+ page.should have_content(queue.id)
68
+ end
69
+ end
70
+
71
+ it "when viewing queue statistics" do
72
+ 6.times { queue.statistics { lambda {}}}
73
+ 4.times { queue.statistics { raise "failed" }}
74
+
75
+ visit "/queues"
76
+
77
+ within "#queue_#{queue.id}" do
78
+ page.should have_content(queue.id)
79
+ page.should have_content(queue.created_at.to_s)
80
+ page.should have_content("6")
81
+
82
+ within ".error" do
83
+ page.should have_content("4")
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ describe "Queues#show" do
90
+ before do
91
+ Timecop.travel(DateTime.now)
92
+ @time = Time.now.to_f
93
+
94
+ queue.statistics(Fiber, :yield, [4567], @time) { raise Howler::Message::Retry }
95
+
96
+ Benchmark.stub(:measure).and_return("1.1 1.3 1.5 ( 1.7)", "2.1 2.3 2.5 ( 2.7)")
97
+
98
+ [[Hash, :keys, [1234]], [Array, :length, [2345]]].each do |(klass, method, args)|
99
+ queue.statistics(klass, method, args, @time) { lambda {}}
100
+ end
101
+
102
+ Howler::Manager.current.push(Logger, :info, [3456])
103
+ end
104
+
105
+ it "when viewing processed messages" do
106
+ visit "/queues/#{queue.id}"
107
+
108
+ within "#processed_messages" do
109
+ within ".table_title" do
110
+ page.should have_content("Processed Messages")
111
+ end
112
+
113
+ within ".table tr" do
114
+ ["Message", "Created At", "System Runtime", "Real Runtime", "Status"].each do |value|
115
+ page.should have_content(value)
116
+ end
117
+ end
118
+
119
+ within ".table tbody" do
120
+ page.should have_content(Howler::Util.at(@time))
121
+
122
+ %w(Hash.keys(1234) 1.5 1.7 success).each do |value|
123
+ page.should have_content(value)
124
+ end
125
+
126
+ %w(Array.length(2345) 2.5 2.7 success).each do |value|
127
+ page.should have_content(value)
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ it "when viewing pending messages" do
134
+ visit "/queues/#{queue.id}"
135
+
136
+ within "#pending_messages" do
137
+ within ".table_title" do
138
+ page.should have_content("Pending Messages")
139
+ end
140
+
141
+ within ".table tr" do
142
+ ["Message", "Created At", "Status"].each do |value|
143
+ page.should have_content(value)
144
+ end
145
+ end
146
+
147
+ within ".table tbody" do
148
+ page.should have_content(Howler::Util.at(@time))
149
+
150
+ %w(Logger.info(3456) pending).each do |value|
151
+ page.should have_content(value)
152
+ end
153
+
154
+ %w(Fiber.yield(4567) retrying).each do |value|
155
+ page.should have_content(value)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ it "when viewing failed messages" do
162
+ Benchmark.unstub(:measure)
163
+ queue.statistics(Array, :length, [2345], @time) { raise Howler::Message::Failed }
164
+
165
+ visit "/queues/#{queue.id}"
166
+
167
+ within "#failed_messages" do
168
+ within ".table_title" do
169
+ page.should have_content("Failed Messages")
170
+ end
171
+
172
+ within ".table tr" do
173
+ ["Message", "Created At", "Failed At", "Cause", "Status"].each do |value|
174
+ page.should have_content(value)
175
+ end
176
+ end
177
+
178
+ within ".table tbody" do
179
+ within ".failed_at" do
180
+ page.should have_content(Howler::Util.at(@time))
181
+ end
182
+
183
+ %w(Array.length(2345) Howler::Message::Failed failed).each do |value|
184
+ page.should have_content(value)
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ describe "Notifications#index" do
192
+ describe "when there are notifications" do
193
+ before do
194
+ [:length, :collect, :max].each_with_index do |method, i|
195
+ queue.statistics(Array, method, [i*10]) { raise Howler::Message::Notify.new(generate_exception, :type => method)}
196
+ end
197
+
198
+ visit "/notifications"
199
+ end
200
+
201
+ it "when viewing the notifications table" do
202
+ within "#notifications" do
203
+ within ".table tr" do
204
+ %w(Message Notification Occurred).each do |value|
205
+ page.should have_content(value)
206
+ end
207
+ end
208
+
209
+ within ".table tbody" do
210
+ page.should have_content(Howler::Util.at(@time))
211
+
212
+ %w(Array.length(0) Array.collect(10) Array.max(20) notified).each do |value|
213
+ page.should have_content(value)
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end