woodhouse 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +5 -0
  2. data/.travis.yml +5 -0
  3. data/Gemfile +7 -0
  4. data/Guardfile +4 -0
  5. data/MIT-LICENSE +21 -0
  6. data/PROGRESS-NOTES.txt +5 -0
  7. data/README.markdown +152 -0
  8. data/Rakefile +35 -0
  9. data/THOUGHTS +84 -0
  10. data/doc/example/script-woodhouse +9 -0
  11. data/doc/example/woodhouse-initializer.rb +12 -0
  12. data/lib/generators/woodhouse_generator.rb +38 -0
  13. data/lib/woodhouse/dispatcher.rb +37 -0
  14. data/lib/woodhouse/dispatchers/bunny_dispatcher.rb +48 -0
  15. data/lib/woodhouse/dispatchers/common_amqp_dispatcher.rb +28 -0
  16. data/lib/woodhouse/dispatchers/hot_bunnies_dispatcher.rb +48 -0
  17. data/lib/woodhouse/dispatchers/local_dispatcher.rb +13 -0
  18. data/lib/woodhouse/dispatchers/local_pool_dispatcher.rb +25 -0
  19. data/lib/woodhouse/dispatchers.rb +19 -0
  20. data/lib/woodhouse/extension.rb +24 -0
  21. data/lib/woodhouse/extensions/new_relic/instrumentation_middleware.rb +10 -0
  22. data/lib/woodhouse/extensions/new_relic.rb +23 -0
  23. data/lib/woodhouse/extensions/progress.rb +165 -0
  24. data/lib/woodhouse/job.rb +76 -0
  25. data/lib/woodhouse/job_execution.rb +60 -0
  26. data/lib/woodhouse/layout.rb +290 -0
  27. data/lib/woodhouse/layout_builder.rb +55 -0
  28. data/lib/woodhouse/layout_serializer.rb +82 -0
  29. data/lib/woodhouse/middleware/airbrake_exceptions.rb +12 -0
  30. data/lib/woodhouse/middleware/assign_logger.rb +10 -0
  31. data/lib/woodhouse/middleware/log_dispatch.rb +21 -0
  32. data/lib/woodhouse/middleware/log_jobs.rb +22 -0
  33. data/lib/woodhouse/middleware.rb +16 -0
  34. data/lib/woodhouse/middleware_stack.rb +35 -0
  35. data/lib/woodhouse/mixin_registry.rb +27 -0
  36. data/lib/woodhouse/node_configuration.rb +80 -0
  37. data/lib/woodhouse/process.rb +41 -0
  38. data/lib/woodhouse/queue_criteria.rb +52 -0
  39. data/lib/woodhouse/rails.rb +46 -0
  40. data/lib/woodhouse/rails2.rb +21 -0
  41. data/lib/woodhouse/registry.rb +12 -0
  42. data/lib/woodhouse/runner.rb +60 -0
  43. data/lib/woodhouse/runners/bunny_runner.rb +60 -0
  44. data/lib/woodhouse/runners/dummy_runner.rb +11 -0
  45. data/lib/woodhouse/runners/hot_bunnies_runner.rb +79 -0
  46. data/lib/woodhouse/runners.rb +16 -0
  47. data/lib/woodhouse/scheduler.rb +113 -0
  48. data/lib/woodhouse/server.rb +80 -0
  49. data/lib/woodhouse/trigger_set.rb +19 -0
  50. data/lib/woodhouse/version.rb +3 -0
  51. data/lib/woodhouse/worker.rb +86 -0
  52. data/lib/woodhouse.rb +99 -0
  53. data/spec/integration/bunny_worker_process_spec.rb +32 -0
  54. data/spec/layout_builder_spec.rb +55 -0
  55. data/spec/layout_spec.rb +143 -0
  56. data/spec/middleware_stack_spec.rb +56 -0
  57. data/spec/mixin_registry_spec.rb +15 -0
  58. data/spec/node_configuration_spec.rb +22 -0
  59. data/spec/progress_spec.rb +40 -0
  60. data/spec/queue_criteria_spec.rb +11 -0
  61. data/spec/scheduler_spec.rb +41 -0
  62. data/spec/server_spec.rb +72 -0
  63. data/spec/shared_contexts.rb +70 -0
  64. data/spec/worker_spec.rb +28 -0
  65. data/woodhouse.gemspec +37 -0
  66. metadata +285 -0
@@ -0,0 +1,143 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/shared_contexts'
3
+
4
+ describe Woodhouse::Layout do
5
+
6
+ context "#add_node" do
7
+
8
+ it "should only accept Woodhouse::Node objects"
9
+
10
+ end
11
+
12
+ context "#frozen_clone" do
13
+
14
+ it "should return a frozen copy where all sub-objects are also frozen copies"
15
+
16
+ end
17
+
18
+ context "#changes_from" do
19
+
20
+ it "should return a Woodhouse::Layout::Changes object where this layout is the new one"
21
+
22
+ end
23
+
24
+ end
25
+
26
+ describe Woodhouse::Layout::Node do
27
+ it_should_behave_like "common"
28
+
29
+ context "#add_node" do
30
+
31
+ it "should take a string or a symbol and create a node with that name" do
32
+ empty_layout.add_node :orz
33
+ empty_layout.add_node 'vux'
34
+ empty_layout.add_node Woodhouse::Layout::Node.new(:androsynth)
35
+ empty_layout.node(:orz).should be_kind_of(Woodhouse::Layout::Node)
36
+ empty_layout.node(:vux).should be_kind_of(Woodhouse::Layout::Node)
37
+ empty_layout.node(:androsynth).should be_kind_of(Woodhouse::Layout::Node)
38
+ end
39
+
40
+ end
41
+
42
+ context "#default_configuration!" do
43
+
44
+ it "should configure one worker thread for every job available" do
45
+ layout = empty_layout
46
+ config = common_config
47
+ config.registry = {
48
+ :FooBarWorker => FakeWorker,
49
+ :BarBazWorker => FakeWorker,
50
+ :BazBatWorker => FakeWorker
51
+ }
52
+ layout.add_node Woodhouse::Layout::Node.new(:default)
53
+ layout.node(:default).default_configuration! config
54
+ layout.node(:default).workers.should have(6).workers
55
+ # FooBar#foo, FooBar#bar, BarBaz#foo...
56
+ end
57
+
58
+ it "should pay attention to the config's default_threads" do
59
+ layout = empty_layout
60
+ config = common_config
61
+ config.default_threads = 10
62
+ config.registry = {
63
+ :OrzWorker => FakeWorker
64
+ }
65
+ layout.add_node :default
66
+ layout.node(:default).default_configuration! config
67
+ layout.node(:default).workers.should have(2).workers
68
+ layout.node(:default).workers.first.threads.should == 10
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ describe Woodhouse::Layout::Worker do
76
+
77
+ it "should default to 1 thread"
78
+
79
+ it "should default to a wide-open criteria"
80
+
81
+ it "should automatically convert the :only key to a Woodhouse::QueueCriteria"
82
+
83
+ end
84
+
85
+ describe Woodhouse::Layout::Changes do
86
+ it_should_behave_like "common"
87
+
88
+ context "when the new layout is empty" do
89
+
90
+ subject { Woodhouse::Layout::Changes.new(empty_layout, populated_layout, :default) }
91
+
92
+ it "should drop all workers and add none" do
93
+ subject.adds.should be_empty
94
+ subject.drops.should have(3).dropped_workers
95
+ end
96
+
97
+ end
98
+
99
+ context "when the old layout is empty" do
100
+
101
+ subject { Woodhouse::Layout::Changes.new(populated_layout, empty_layout, :default) }
102
+
103
+ it "should add all workers and drop none" do
104
+ subject.drops.should be_empty
105
+ subject.adds.should have(3).added_workers
106
+ end
107
+
108
+ end
109
+
110
+ context "when the new layout is nil" do
111
+
112
+ subject { Woodhouse::Layout::Changes.new(nil, populated_layout, :default) }
113
+
114
+ it "should drop all workers and add none" do
115
+ subject.adds.should be_empty
116
+ subject.drops.should have(3).dropped_workers
117
+ end
118
+
119
+ end
120
+
121
+ context "when the old layout is nil" do
122
+
123
+ subject { Woodhouse::Layout::Changes.new(populated_layout, nil, :default) }
124
+
125
+ it "should add all workers and drop none" do
126
+ subject.drops.should be_empty
127
+ subject.adds.should have(3).added_workers
128
+ end
129
+
130
+ end
131
+
132
+ context "when both layouts are specified and they overlap" do
133
+
134
+ subject { Woodhouse::Layout::Changes.new(overlapping_layout, populated_layout, :default) }
135
+
136
+ it "should add some workers, drop some, and leave some alone" do
137
+ subject.drops.should have(1).dropped_worker
138
+ subject.adds.should have(1).added_worker
139
+ end
140
+
141
+ end
142
+
143
+ end
@@ -0,0 +1,56 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/shared_contexts'
3
+
4
+ describe Woodhouse::MiddlewareStack do
5
+ it_should_behave_like "common"
6
+
7
+ subject { Woodhouse::MiddlewareStack.new(common_config) }
8
+ let(:dummy) { MiddlewareDummy.new }
9
+
10
+ class MiddlewareDummy
11
+ attr_reader :was_called, :sent_item
12
+ def initialize
13
+ @was_called = false
14
+ @sent_item = nil
15
+ end
16
+
17
+ def call(job)
18
+ @was_called = true
19
+ @sent_item = job
20
+ yield job
21
+ end
22
+
23
+ end
24
+
25
+ it "should work if empty" do
26
+ called = :not_called
27
+ subject.call("LANAAAA!") {|object|
28
+ object.should == "LANAAAA!"
29
+ called = :called
30
+ }
31
+ called.should == :called
32
+ end
33
+
34
+ it "should send #call to stack items which respond to that" do
35
+ subject << dummy
36
+ subject.call("is it not?") {|object| }
37
+ dummy.was_called.should be_true
38
+ dummy.sent_item.should == "is it not?"
39
+ end
40
+
41
+ it "should send #new to stack items which respond to that" do
42
+ fake_class = stub('mware item', :new => dummy)
43
+ subject << fake_class
44
+ subject.call("danger zone") {|object| }
45
+ dummy.was_called.should be_true
46
+ dummy.sent_item.should == "danger zone"
47
+ end
48
+
49
+ it "should complain with ArgumentError if entries respond to neither #call nor #new" do
50
+ subject << nil
51
+ expect do
52
+ subject.call("danger zone") {|object| }
53
+ end.to raise_error(ArgumentError)
54
+ end
55
+
56
+ end
@@ -0,0 +1,15 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/shared_contexts'
3
+
4
+ describe Woodhouse::MixinRegistry do
5
+
6
+ subject { Woodhouse::MixinRegistry.new }
7
+
8
+ it "should include all classes that include Woodhouse::Worker" do
9
+ ::SomeFakeNewClass = Class.new
10
+ SomeFakeNewClass.send(:include, Woodhouse::Worker)
11
+ subject[:SomeFakeNewClass].should be SomeFakeNewClass
12
+ Object.send :remove_const, :SomeFakeNewClass
13
+ end
14
+
15
+ end
@@ -0,0 +1,22 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/shared_contexts'
3
+
4
+ describe Woodhouse::NodeConfiguration do
5
+ it_should_behave_like "common"
6
+
7
+ subject { Woodhouse::NodeConfiguration.new }
8
+
9
+ describe "server_info" do
10
+
11
+ it "should default to an empty hash" do
12
+ subject.server_info.should == {}
13
+ end
14
+
15
+ it "should convert keys into symbols" do
16
+ subject.server_info = { "lana" => "LANAAAA" }
17
+ subject.server_info.should have_key(:lana)
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,40 @@
1
+ require 'woodhouse/extensions/progress'
2
+
3
+ describe Woodhouse::Progress do
4
+
5
+ describe "JobWithProgress" do
6
+ subject { Object.new.tap do |obj| obj.extend Woodhouse::Progress::JobWithProgress end }
7
+
8
+ it "should provide a method for creating a StatusTicker" do
9
+ subject.status_ticker("orz").should be_kind_of(Woodhouse::Progress::StatusTicker)
10
+ end
11
+
12
+ end
13
+
14
+ describe "StatusTicker" do
15
+ let(:sink) { double("progress sink") }
16
+ let(:job) {
17
+ Object.new.tap do |obj|
18
+ obj.extend Woodhouse::Progress::JobWithProgress
19
+ obj.progress_sink = sink
20
+ end
21
+ }
22
+
23
+ it "should take initial status and tick arguments" do
24
+ ticker = job.status_ticker("orz", :top => 100, :start => 10, :status => "working")
25
+ ticker.to_hash.should == { "orz" => { "top" => 100, "current" => 10, "status" => "working" } }
26
+ end
27
+
28
+ context "#tick" do
29
+
30
+ it "should send progress updates" do
31
+ ticker = job.status_ticker("orz")
32
+ sink.should_receive(:update_job).with(job, { "orz" => { "status" => "funky", "current" => 1 } })
33
+ ticker.tick(:status => "funky").value
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,11 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/shared_contexts'
3
+
4
+ describe Woodhouse::QueueCriteria do
5
+ it_should_behave_like "common"
6
+
7
+ it "should stringify keys and values" do
8
+ criteria = Woodhouse::QueueCriteria.new("abc" => :def, :fed => 1)
9
+ criteria.criteria.should == { "abc" => "def", "fed" => "1" }
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/shared_contexts'
3
+
4
+ describe Woodhouse::Scheduler do
5
+ it_should_behave_like "common"
6
+
7
+ subject { Woodhouse::Scheduler.new(common_config) }
8
+
9
+ let(:worker) {
10
+ Woodhouse::Layout::Worker.new(:FooBarWorker, :foo)
11
+ }
12
+
13
+ let(:worker_2) {
14
+ Woodhouse::Layout::Worker.new(:FooBarWorker, :foo, :only => { :job => "big" })
15
+ }
16
+
17
+ it "should create a new worker set when a new worker is sent to #start_worker" do
18
+ subject.start_worker worker
19
+ subject.should be_running_worker(worker)
20
+ end
21
+
22
+ it "should not create a new worker set when an existing worker is sent to #start_worker" do
23
+ subject.start_worker(worker).should be_true
24
+ subject.start_worker(worker).should be_false
25
+ end
26
+
27
+ it "should spin down and remove a worker set when a worker is sent to #stop_worker" do
28
+ subject.start_worker worker
29
+ subject.stop_worker worker, true
30
+ subject.should_not be_running_worker(worker)
31
+ end
32
+
33
+ it "should spin down and remove all worker sets when #spin_down is called" do
34
+ subject.start_worker worker
35
+ subject.start_worker worker_2
36
+ subject.spin_down
37
+ subject.should_not be_running_worker(worker)
38
+ subject.should_not be_running_worker(worker_2)
39
+ end
40
+
41
+ end
@@ -0,0 +1,72 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/shared_contexts'
3
+
4
+ describe Woodhouse::Server do
5
+ it_should_behave_like "common"
6
+
7
+ subject { Woodhouse::Server.new }
8
+
9
+ it "should default to the :default node" do
10
+ subject.node.should == :default
11
+ end
12
+
13
+ it "should expect the value to #layout= to be nil or a Layout" do
14
+ subject.layout = Woodhouse::Layout.new
15
+ subject.layout.should be_kind_of Woodhouse::Layout
16
+ subject.layout = nil
17
+ subject.layout.should be_nil
18
+ if false # this craps out on JRuby
19
+ begin
20
+ oldlogger = Celluloid.logger
21
+ Celluloid.logger = nil # It's going to crash
22
+ expect do
23
+ subject.layout = "foo"
24
+ end.to raise_error
25
+ ensure
26
+ Celluloid.logger = oldlogger
27
+ end
28
+ end
29
+ end
30
+
31
+ it "should take a frozen clone of the layout" do
32
+ layout = Woodhouse::Layout.new
33
+ subject.layout = layout
34
+ subject.layout.should_not be layout
35
+ subject.layout.should be_frozen
36
+ end
37
+
38
+ context "#start" do
39
+
40
+ it "should return false if a layout is not configured" do
41
+ subject.start.should be_false
42
+ end
43
+
44
+ it "should return false if the set node doesn't exist in the layout" do
45
+ subject.layout = populated_layout
46
+ subject.node = :foo_bar_baz
47
+ subject.start.should be_false
48
+ end
49
+
50
+ it "should return true and spin up workers if the node is valid" do
51
+ subject.layout = populated_layout
52
+ subject.start.should be_true
53
+ # TODO: test for workers starting up
54
+ end
55
+
56
+ end
57
+
58
+ context "#reload" do
59
+
60
+ it "should shut down the server if a layout is not configured"
61
+
62
+ it "should shut down the server if the set node doesn't exist in the layout"
63
+
64
+ it "should shut down the server if the set node has no workers"
65
+
66
+ it "should spin up new workers if they have been added to the node"
67
+
68
+ it "should spin down workers if they have been removed from the node"
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,70 @@
1
+ #Celluloid.logger = nil
2
+
3
+ class FakeWorker
4
+
5
+ class << self
6
+ attr_accessor :last_worker
7
+ attr_accessor :jobs
8
+ end
9
+
10
+ self.jobs ||= []
11
+
12
+ def initialize
13
+ FakeWorker.last_worker = self
14
+ FakeWorker.jobs ||= []
15
+ end
16
+
17
+ def foo(args)
18
+ FakeWorker.jobs << args
19
+ end
20
+
21
+ def bar(args)
22
+ FakeWorker.jobs << args
23
+ end
24
+
25
+ end
26
+
27
+ Woodhouse.configure do |config|
28
+ config.registry = { :FooBarWorker => FakeWorker }
29
+ config.runner_type = :dummy
30
+ config.dispatcher_type = :local
31
+ config.logger = Logger.new("/dev/null")
32
+ end
33
+
34
+ shared_examples_for "common" do
35
+
36
+ let(:empty_layout) {
37
+ Woodhouse::Layout.new
38
+ }
39
+
40
+ let(:populated_layout) {
41
+ Woodhouse::Layout.new.tap do |layout|
42
+ layout.add_node Woodhouse::Layout::Node.new(:default)
43
+ layout.node(:default).tap do |default|
44
+ default.add_worker Woodhouse::Layout::Worker.new(:FooWorker, :foo)
45
+ default.add_worker Woodhouse::Layout::Worker.new(:FooWorker, :foo, :only => { :size => "huge" })
46
+ default.add_worker Woodhouse::Layout::Worker.new(:FooWorker, :bar, :threads => 3)
47
+ end
48
+ layout.add_node Woodhouse::Layout::Node.new(:other)
49
+ layout.node(:other).tap do |default|
50
+ default.add_worker Woodhouse::Layout::Worker.new(:OtherWorker, :bat)
51
+ end
52
+ end
53
+ }
54
+
55
+ let(:overlapping_layout) {
56
+ Woodhouse::Layout.new.tap do |layout|
57
+ layout.add_node Woodhouse::Layout::Node.new(:default)
58
+ layout.node(:default).tap do |default|
59
+ default.add_worker Woodhouse::Layout::Worker.new(:FooWorker, :foo)
60
+ default.add_worker Woodhouse::Layout::Worker.new(:FooWorker, :bar, :threads => 3)
61
+ default.add_worker Woodhouse::Layout::Worker.new(:BarWorker, :baz)
62
+ end
63
+ end
64
+ }
65
+
66
+ let!(:common_config) {
67
+ Woodhouse.global_configuration
68
+ }
69
+
70
+ end
@@ -0,0 +1,28 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/shared_contexts'
3
+
4
+ describe Woodhouse::Worker do
5
+
6
+ subject {
7
+ Class.new do
8
+ include Woodhouse::Worker
9
+ def fake_job(*); end
10
+ end
11
+ }
12
+
13
+ it "should provide class-level async_ convenience methods" do
14
+ lambda do
15
+ subject.async_fake_job
16
+ end.should_not raise_error(NoMethodError)
17
+ lambda do
18
+ subject.async_something_else
19
+ end.should raise_error(NoMethodError)
20
+ lambda do
21
+ subject.blah_blah_blah
22
+ end.should raise_error(NoMethodError)
23
+ lambda do
24
+ subject.async_method # Don't want inherited methods to work
25
+ end.should raise_error(NoMethodError)
26
+ end
27
+
28
+ end
data/woodhouse.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "woodhouse/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "woodhouse"
7
+ s.version = Woodhouse::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Matthew Boeh"]
10
+ s.email = ["matt@crowdcompass.com", "matthew.boeh@gmail.com"]
11
+ s.homepage = "http://github.com/mboeh/woodhouse"
12
+ s.summary = %q{An AMQP-based background worker system for Ruby 1.8 and 1.9}
13
+ s.description = %q{An AMQP-based background worker system for Ruby 1.8 and 1.9 designed to make managing heterogenous tasks relatively easy.
14
+
15
+ The use case for Woodhouse is for reliable and sane performance in situations where jobs on a single queue may vary significantly in length. The goal is to permit large numbers of quick jobs to be serviced even when many slow jobs are in the queue. A secondary goal is to provide a sane way for jobs on a given queue to be given special priority or dispatched to a server more suited to them.
16
+
17
+ Clients (i.e., your application) may be using either Ruby 1.8 or 1.9 in any VM. Worker processes currently only support JRuby in 1.8 or 1.9 mode efficiently. MRI 1.9 and Rubinius support is planned.}
18
+
19
+ s.rubyforge_project = "woodhouse"
20
+
21
+ s.add_dependency 'fiber18', '>= 1.0.1'
22
+ s.add_dependency 'celluloid'
23
+ s.add_dependency 'bunny', "~> 0.9.0.pre4"
24
+ s.add_dependency 'connection_pool'
25
+ s.add_dependency 'json'
26
+
27
+ s.add_development_dependency 'rspec', '~> 1.3.1'
28
+ s.add_development_dependency 'rake'
29
+ s.add_development_dependency 'guard'
30
+ s.add_development_dependency 'guard-rspec'
31
+ s.add_development_dependency 'mocha'
32
+
33
+ s.files = `git ls-files`.split("\n")
34
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
35
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
36
+ s.require_paths = ["lib"]
37
+ end