woodhouse 0.1.1

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 (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,79 @@
1
+ #
2
+ # A Runner implementation that uses hot_bunnies, a JRuby AMQP client using the
3
+ # Java client for RabbitMQ. This class can be loaded if hot_bunnies is not
4
+ # available, but it will fail upon initialization. If you want to use this
5
+ # runner (it's currently the only one that works very well), make sure to
6
+ # add
7
+ #
8
+ # gem 'hot_bunnies'
9
+ #
10
+ # to your Gemfile. This runner will automatically be used in JRuby.
11
+ #
12
+ class Woodhouse::Runners::HotBunniesRunner < Woodhouse::Runner
13
+ begin
14
+ require 'hot_bunnies'
15
+ rescue LoadError => err
16
+ define_method(:initialize) {|*args|
17
+ raise err
18
+ }
19
+ end
20
+
21
+ def subscribe
22
+ client = HotBunnies.connect(@config.server_info)
23
+ channel = client.create_channel
24
+ channel.prefetch = 1
25
+ queue = channel.queue(@worker.queue_name)
26
+ exchange = channel.exchange(@worker.exchange_name, :type => :headers)
27
+ queue.bind(exchange, :arguments => @worker.criteria.amqp_headers)
28
+ worker = Celluloid.current_actor
29
+ queue.subscribe(:ack => true).each(:blocking => false) do |headers, payload|
30
+ begin
31
+ job = make_job(headers, payload)
32
+ if can_service_job?(job)
33
+ if service_job(job)
34
+ headers.ack
35
+ else
36
+ headers.reject
37
+ end
38
+ else
39
+ @config.logger.error("Cannot service job #{job.describe} in queue for #{@worker.describe}")
40
+ headers.reject
41
+ end
42
+ rescue => err
43
+ begin
44
+ @config.logger.error("Error bubbled up out of worker. This shouldn't happen. #{err.message}")
45
+ err.backtrace.each do |btr|
46
+ @config.logger.error(" #{btr}")
47
+ end
48
+ headers.reject
49
+ ensure
50
+ worker.bail_out(err)
51
+ end
52
+ end
53
+ end
54
+ wait :spin_down
55
+ ensure
56
+ client.close
57
+ end
58
+
59
+ def spin_down
60
+ signal :spin_down
61
+ end
62
+
63
+ def bail_out(err)
64
+ raise Woodhouse::BailOut, "#{err.class}: #{err.message}"
65
+ end
66
+
67
+ private
68
+
69
+ def make_job(message, payload)
70
+ Woodhouse::Job.new(@worker.worker_class_name, @worker.job_method) do |job|
71
+ job.arguments = message.properties.headers.inject({}) {|h,(k,v)|
72
+ h[k.to_s] = v.to_s
73
+ h
74
+ }
75
+ job.payload = payload
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,16 @@
1
+ module Woodhouse::Runners
2
+
3
+ def self.guess
4
+ if defined? ::JRUBY_VERSION
5
+ Woodhouse::Runners::HotBunniesRunner
6
+ else
7
+ Woodhouse::Runners::BunnyRunner
8
+ end
9
+ end
10
+
11
+ end
12
+
13
+ require 'woodhouse/runner'
14
+ require 'woodhouse/runners/bunny_runner'
15
+ require 'woodhouse/runners/hot_bunnies_runner'
16
+ require 'woodhouse/runners/dummy_runner'
@@ -0,0 +1,113 @@
1
+ class Woodhouse::Scheduler
2
+ include Woodhouse::Util
3
+ include Celluloid
4
+
5
+ class SpunDown < StandardError
6
+
7
+ end
8
+
9
+ class WorkerSet
10
+ include Woodhouse::Util
11
+ include Celluloid
12
+
13
+ attr_reader :worker
14
+ trap_exit :worker_died
15
+
16
+ def initialize(scheduler, worker, config)
17
+ expect_arg_or_nil :worker, Woodhouse::Layout::Worker, worker
18
+ @scheduler = scheduler
19
+ @worker_def = worker
20
+ @config = config
21
+ @threads = []
22
+ spin_up
23
+ end
24
+
25
+ def spin_down
26
+ @spinning_down = true
27
+ @threads.each_with_index do |thread, idx|
28
+ @config.logger.debug "Spinning down thread #{idx} for worker #{@worker_def.describe}"
29
+ thread.spin_down
30
+ thread.terminate
31
+ end
32
+ @scheduler.remove_worker(@worker_def)
33
+ signal :spun_down
34
+ end
35
+
36
+ def wait_until_done
37
+ wait :spun_down
38
+ end
39
+
40
+ private
41
+
42
+ def worker_died(actor, reason)
43
+ if reason
44
+ @config.logger.info "Worker died (#{reason.class}: #{reason.message}). Spinning down."
45
+ @threads.delete actor
46
+ raise Woodhouse::BailOut
47
+ end
48
+ end
49
+
50
+ def spin_up
51
+ @worker_def.threads.times do |idx|
52
+ @config.logger.debug "Spinning up thread #{idx} for worker #{@worker_def.describe}"
53
+ worker = @config.runner_type.new_link(@worker_def, @config)
54
+ @threads << worker
55
+ worker.subscribe!
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ trap_exit :worker_set_died
62
+
63
+ def initialize(config)
64
+ @config = config
65
+ @worker_sets = {}
66
+ end
67
+
68
+ def start_worker(worker)
69
+ @config.logger.debug "Starting worker #{worker.describe}"
70
+ unless @worker_sets.has_key?(worker)
71
+ @worker_sets[worker] = WorkerSet.new_link(Celluloid.current_actor, worker, @config)
72
+ true
73
+ else
74
+ false
75
+ end
76
+ end
77
+
78
+ def stop_worker(worker, wait = false)
79
+ if set = @worker_sets[worker]
80
+ @config.logger.debug "Spinning down worker #{worker.describe}"
81
+ set.spin_down
82
+ end
83
+ end
84
+
85
+ def running_worker?(worker)
86
+ @worker_sets.has_key?(worker)
87
+ end
88
+
89
+ def spin_down
90
+ @spinning_down = true
91
+ @config.logger.debug "Spinning down all workers"
92
+ @worker_sets.each do |worker, set|
93
+ set.spin_down
94
+ set.terminate
95
+ end
96
+ end
97
+
98
+ def remove_worker(worker)
99
+ @worker_sets.delete(worker)
100
+ end
101
+
102
+ def worker_set_died(actor, reason)
103
+ if reason
104
+ @config.logger.info "Worker set died (#{reason.class}: #{reason.message}). Spinning down."
105
+ begin
106
+ spin_down
107
+ ensure
108
+ raise reason
109
+ end
110
+ end
111
+ end
112
+
113
+ end
@@ -0,0 +1,80 @@
1
+ module Woodhouse
2
+
3
+ class Server
4
+ include Celluloid
5
+ include Woodhouse::Util
6
+
7
+ attr_reader :layout, :node
8
+ attr_accessor :configuration
9
+
10
+ trap_exit :scheduler_died
11
+
12
+ def initialize(keyw = {})
13
+ self.layout = keyw[:layout]
14
+ self.node = keyw[:node]
15
+ self.configuration = keyw[:configuration] || Woodhouse.global_configuration
16
+ end
17
+
18
+ def layout=(value)
19
+ expect_arg_or_nil :value, Woodhouse::Layout, value
20
+ @previous_layout = @layout
21
+ @layout = value ? value.frozen_clone : nil
22
+ end
23
+
24
+ def node=(value)
25
+ @node = value || :default
26
+ end
27
+
28
+ def start
29
+ # TODO: don't pass global config
30
+ @scheduler ||= Woodhouse::Scheduler.new_link(configuration)
31
+ return false unless ready_to_start?
32
+ configuration.triggers.trigger :server_start
33
+ dispatch_layout_changes
34
+ true
35
+ end
36
+
37
+ def reload
38
+ dispatch_layout_changes!
39
+ end
40
+
41
+ def ready_to_start?
42
+ @node and @layout and @layout.node(@node)
43
+ end
44
+
45
+ # TODO: do this better
46
+ def shutdown
47
+ @scheduler.spin_down
48
+ @scheduler.terminate
49
+ configuration.triggers.trigger :server_end
50
+ signal :shutdown
51
+ end
52
+
53
+ private
54
+
55
+ def scheduler_died(actor, reason)
56
+ signal :shutdown
57
+ end
58
+
59
+ def dispatch_layout_changes
60
+ if @layout.nil?
61
+ shutdown
62
+ else
63
+ apply_layout_changes @layout.changes_from(@previous_layout, @node)
64
+ end
65
+ end
66
+
67
+ def apply_layout_changes(changes)
68
+ if @scheduler
69
+ changes.adds.each do |add|
70
+ @scheduler.start_worker(add)
71
+ end
72
+ changes.drops.each do |drop|
73
+ @scheduler.stop_worker(drop)
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,19 @@
1
+ class Woodhouse::TriggerSet
2
+
3
+ def initialize
4
+ @triggers = {}
5
+ end
6
+
7
+ def add(event_name, &blk)
8
+ @triggers[event_name.to_sym] ||= []
9
+ @triggers[event_name.to_sym] << blk
10
+ end
11
+
12
+
13
+ def trigger(event_name, *args)
14
+ (@triggers[event_name.to_sym] || []).each do |trigger|
15
+ trigger.call(*args)
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,3 @@
1
+ module Woodhouse
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,86 @@
1
+ #
2
+ # Classes which include this module become automatically visible to
3
+ # MixinRegistry (the default way of finding jobs in Woodhouse).
4
+ # All public methods of the class are automatically made available as
5
+ # jobs.
6
+ #
7
+ # Classes including Woodhouse::Worker also get access to the +logger+
8
+ # method, which will be the same logger globally configured for the current
9
+ # Layout.
10
+ #
11
+ # Classes including Woodhouse::Worker also have convenience shortcuts
12
+ # for dispatching jobs. Any job defined on the class can be dispatched
13
+ # asynchronously by calling ClassName.async_job_name(options).
14
+ #
15
+ # == Example
16
+ #
17
+ # class PamPoovey
18
+ # include Woodhouse::Worker
19
+ #
20
+ # # This is available as the job PamPoovey#do_hr
21
+ # def do_hr(options)
22
+ # logger.info "Out comes the dolphin puppet"
23
+ # end
24
+ #
25
+ # private
26
+ #
27
+ # # This is not picked up as a job
28
+ # def fight_club
29
+ # # ...
30
+ # end
31
+ # end
32
+ #
33
+ # # later ...
34
+ #
35
+ # Woodhouse::MixinRegistry.new[:PamPoovey] # => PamPoovey
36
+ # PamPoovey.async_do_hr(:employee => "Lana")
37
+ #
38
+ module Woodhouse::Worker
39
+
40
+ def self.included(into)
41
+ into.extend ClassMethods
42
+ into.set_worker_name into.name unless into.name.nil?
43
+ end
44
+
45
+ # The current Woodhouse logger. Set by the runner. Don't expect it to be set
46
+ # if you create the object yourself. If you want to be able to run job methods
47
+ # directly, you should account for setting +logger+.
48
+ attr_accessor :logger
49
+
50
+ module ClassMethods
51
+
52
+ def worker_name
53
+ @worker_name
54
+ end
55
+
56
+ # Sets the name for this worker class if not already set (i.e., if it's
57
+ # an anonymous class). The first time the name for the worker is set,
58
+ # it becomes registered with MixinRegistry. After that, attempting to
59
+ # change the worker class will raise ArgumentError.
60
+ def set_worker_name(name)
61
+ if @worker_name
62
+ raise ArgumentError, "cannot change worker name"
63
+ else
64
+ if name and !name.empty?
65
+ @worker_name = name.to_sym
66
+ Woodhouse::MixinRegistry.register self
67
+ end
68
+ end
69
+ end
70
+
71
+ # You can dispatch a job +baz+ on class +FooBar+ by calling FooBar.async_baz.
72
+ def method_missing(method, *args, &block)
73
+ if method.to_s =~ /^asynch?_(.*)/
74
+ if instance_methods(false).detect{|meth| meth.to_s == $1 }
75
+ Woodhouse.dispatch(@worker_name, $1, args.first)
76
+ else
77
+ super
78
+ end
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ end
data/lib/woodhouse.rb ADDED
@@ -0,0 +1,99 @@
1
+ module Woodhouse
2
+ WoodhouseError = Class.new(StandardError)
3
+ WorkerNotFoundError = Class.new(WoodhouseError)
4
+ ConnectionError = Class.new(WoodhouseError)
5
+ ConfigurationError = Class.new(WoodhouseError)
6
+ FatalError = Class.new(WoodhouseError)
7
+ BailOut = Class.new(FatalError)
8
+
9
+ module Util
10
+
11
+ private
12
+
13
+ def expect_arg(name, klass, value)
14
+ unless value.kind_of?(klass)
15
+ raise ArgumentError, "expected #{name} to be a #{klass.name}, got #{value.class}"
16
+ end
17
+ end
18
+
19
+ def expect_arg_or_nil(name, klass, value)
20
+ expect_arg(name, klass, value) unless value.nil?
21
+ end
22
+
23
+ # Cheap knockoff, suffices for my simple purposes
24
+ def camelize(string)
25
+ string.split(/_/).map{ |word| word.capitalize }.join('')
26
+ end
27
+
28
+ end
29
+
30
+ # TODO: hate keeping global state in this class. I need to push
31
+ # some of this down into NodeConfiguration or something like it.
32
+ module GlobalMethods
33
+
34
+ def logger
35
+ global_configuration.logger
36
+ end
37
+
38
+ def global_configuration
39
+ @global_configuration ||= Woodhouse::NodeConfiguration.default
40
+ end
41
+
42
+ def configure
43
+ @global_configuration ||= Woodhouse::NodeConfiguration.default
44
+ yield @global_configuration
45
+ end
46
+
47
+ def global_layout
48
+ @global_layout ||= Woodhouse::Layout.default
49
+ end
50
+
51
+ def layout
52
+ @global_layout ||= Woodhouse::Layout.new
53
+ yield Woodhouse::LayoutBuilder.new(Woodhouse.global_configuration, @global_layout)
54
+ end
55
+
56
+ # Returns +true+ on JRuby, Rubinius, or MRI 1.9. +false+ otherwise.
57
+ def threading_safe?
58
+ RUBY_VERSION.to_f >= 1.9 or %w[jruby rbx].include?(RUBY_ENGINE)
59
+ end
60
+
61
+ def dispatch(*a)
62
+ global_configuration.dispatcher.dispatch(*a)
63
+ end
64
+
65
+ def update_job(*a)
66
+ global_configuration.dispatcher.update_job(*a)
67
+ end
68
+
69
+ end
70
+
71
+ extend GlobalMethods
72
+
73
+ end
74
+
75
+ require 'fiber18'
76
+ require 'celluloid'
77
+ require 'woodhouse/job'
78
+ require 'woodhouse/layout'
79
+ require 'woodhouse/layout_builder'
80
+ require 'woodhouse/scheduler'
81
+ require 'woodhouse/server'
82
+ require 'woodhouse/queue_criteria'
83
+ require 'woodhouse/node_configuration'
84
+ require 'woodhouse/registry'
85
+ require 'woodhouse/mixin_registry'
86
+ require 'woodhouse/worker'
87
+ require 'woodhouse/job_execution'
88
+ require 'woodhouse/runners'
89
+ require 'woodhouse/dispatchers'
90
+ require 'woodhouse/middleware_stack'
91
+ require 'woodhouse/middleware'
92
+ require 'woodhouse/rails'
93
+ require 'woodhouse/process'
94
+ require 'woodhouse/layout_serializer'
95
+ require 'woodhouse/trigger_set'
96
+
97
+ require 'woodhouse/extension'
98
+ require 'woodhouse/extensions/progress'
99
+ require 'woodhouse/extensions/new_relic'
@@ -0,0 +1,32 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/../shared_contexts'
3
+
4
+ describe Woodhouse::Runners::BunnyRunner do
5
+ it_should_behave_like "common"
6
+
7
+ let(:scheduler) {
8
+ common_config.runner_type = :bunny
9
+ Woodhouse::Scheduler.new(common_config)
10
+ }
11
+
12
+ let(:worker) {
13
+ Woodhouse::Layout::Worker.new(:FooBarWorker, :foo, :only => { :orz => "*happy campers*" })
14
+ }
15
+
16
+ it "should pull jobs off a queue" do
17
+ scheduler.start_worker worker
18
+ sleep 0.5
19
+ # TODO: this should use the bunny dispatcher, once I write it
20
+ bunny = Bunny.new
21
+ bunny.start
22
+ exchange = bunny.exchange(worker.exchange_name, :type => :headers)
23
+ exchange.publish("hi", :headers => { :orz => "*happy campers*" })
24
+ exchange.publish("hi", :headers => { :orz => "*silly cows*" })
25
+ bunny.stop
26
+ sleep 0.2
27
+ FakeWorker.jobs.should_not be_empty
28
+ FakeWorker.jobs.last[:orz].should == "*happy campers*"
29
+ scheduler.spin_down
30
+ end
31
+
32
+ end
@@ -0,0 +1,55 @@
1
+ require 'woodhouse'
2
+ require File.dirname(File.expand_path(__FILE__)) + '/shared_contexts'
3
+
4
+ describe Woodhouse::LayoutBuilder do
5
+ it_should_behave_like "common"
6
+
7
+ it "should provide a DSL to set up layouts" do
8
+ registry = {
9
+ :Pam => FakeWorker,
10
+ :Cyril => FakeWorker,
11
+ :Ray => FakeWorker,
12
+ :Lana => FakeWorker,
13
+ }
14
+ common_config.registry = registry
15
+ builder = Woodhouse::LayoutBuilder.new(common_config) do |layout|
16
+ layout.node(:default) do |default|
17
+ # Eight workers...
18
+ default.all_workers :threads => 2
19
+ # Six workers...
20
+ default.remove :Cyril
21
+ # Five workers...
22
+ default.remove :Ray, :foo
23
+ # Six workers.
24
+ default.add :Ray, :bar, :only => { :baz => "bat" }
25
+ end
26
+ layout.node(:odin) do |odin|
27
+ # Two workers.
28
+ odin.add :Lana, :threads => 2
29
+ # Still two workers
30
+ odin.add :Lana, :threads => 5
31
+ end
32
+ end
33
+ layout = builder.layout
34
+ layout.nodes.should have(2).nodes
35
+ default = layout.node(:default)
36
+ default.workers.should have(6).workers
37
+ default.workers.first.threads.should == 2
38
+ default.workers.map(&:worker_class_name).should_not include(:Cyril)
39
+ default.workers.map(&:worker_class_name).should include(:Ray)
40
+ default.workers.select{|wk|
41
+ wk.worker_class_name == :Ray
42
+ }.map(&:job_method).should_not include(:foo)
43
+ ray = default.workers.detect{|wk|
44
+ wk.worker_class_name == :Ray && wk.criteria.criteria
45
+ }
46
+ ray.should_not be_nil
47
+ ray.criteria.matches?("baz" => "bat").should be_true
48
+ odin = layout.node(:odin)
49
+ odin.workers.should have(2).workers
50
+ odin.workers.first.threads.should == 5
51
+
52
+ Woodhouse::Layout.load(layout.dump).dump.should == layout.dump
53
+ end
54
+
55
+ end