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,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