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,82 @@
1
+ require 'json'
2
+
3
+ class Woodhouse::LayoutSerializer
4
+
5
+ def initialize(layout)
6
+ @layout = layout
7
+ end
8
+
9
+ def as_hash
10
+ {
11
+ :nodes => node_list(layout.nodes)
12
+ }
13
+ end
14
+
15
+ def to_json
16
+ as_hash.to_json
17
+ end
18
+
19
+ def self.dump(layout)
20
+ new(layout).to_json
21
+ end
22
+
23
+ def self.load(json)
24
+ LayoutLoader.new(json).layout
25
+ end
26
+
27
+ class LayoutLoader
28
+
29
+ def initialize(json)
30
+ @entries = JSON.parse(json)
31
+ end
32
+
33
+ def layout
34
+ Woodhouse::Layout.new.tap do |layout|
35
+ @entries['nodes'].each do |node|
36
+ new_node = layout.add_node(node['name'])
37
+ node['workers'].each do |worker|
38
+ new_node.add_worker Woodhouse::Layout::Worker.new(worker['worker_class_name'], worker['job_method'], :threads => worker['threads'], :only => worker['criteria'])
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :layout
49
+
50
+ def node_list(nodes)
51
+ nodes.map{|node|
52
+ node_hash(node)
53
+ }
54
+ end
55
+
56
+ def node_hash(node)
57
+ {
58
+ :name => node.name,
59
+ :workers => worker_list(node.workers)
60
+ }
61
+ end
62
+
63
+ def worker_list(workers)
64
+ workers.map{|worker|
65
+ worker_hash(worker)
66
+ }
67
+ end
68
+
69
+ def worker_hash(worker)
70
+ {
71
+ :worker_class_name => worker.worker_class_name,
72
+ :job_method => worker.job_method,
73
+ :threads => worker.threads,
74
+ :criteria => criteria_hash(worker.criteria)
75
+ }
76
+ end
77
+
78
+ def criteria_hash(criteria)
79
+ criteria.criteria
80
+ end
81
+
82
+ end
@@ -0,0 +1,12 @@
1
+ class Woodhouse::Middleware::AirbrakeExceptions < Woodhouse::Middleware
2
+
3
+ def call(job, worker)
4
+ begin
5
+ yield job, worker
6
+ rescue => err
7
+ Airbrake.notify(err)
8
+ raise err
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,10 @@
1
+ class Woodhouse::Middleware::AssignLogger < Woodhouse::Middleware
2
+
3
+ def call(job, worker)
4
+ if @config.logger and worker.respond_to?(:logger)
5
+ worker.logger = @config.logger
6
+ end
7
+ yield job, worker
8
+ end
9
+
10
+ end
@@ -0,0 +1,21 @@
1
+ class Woodhouse::Middleware::LogDispatch < Woodhouse::Middleware
2
+
3
+ def call(job)
4
+ begin
5
+ yield job
6
+ rescue => err
7
+ log "#{job.describe} could not be dispatched: #{err.inspect}"
8
+ raise err
9
+ end
10
+ log "#{job.describe} dispatched"
11
+ end
12
+
13
+ private
14
+
15
+ def log(msg)
16
+ if @config.logger
17
+ @config.logger.info msg
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,22 @@
1
+ class Woodhouse::Middleware::LogJobs < Woodhouse::Middleware
2
+
3
+ def call(job, worker)
4
+ log "#{job.describe} starting"
5
+ begin
6
+ yield job, worker
7
+ rescue => err
8
+ log "#{job.describe} failed: #{err.inspect}"
9
+ raise err
10
+ end
11
+ log "#{job.describe} done"
12
+ end
13
+
14
+ private
15
+
16
+ def log(msg)
17
+ if @config.logger
18
+ @config.logger.info msg
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,16 @@
1
+ class Woodhouse::Middleware
2
+
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def call(*args)
8
+ yield *args
9
+ end
10
+
11
+ end
12
+
13
+ require 'woodhouse/middleware/log_jobs'
14
+ require 'woodhouse/middleware/log_dispatch'
15
+ require 'woodhouse/middleware/assign_logger'
16
+ require 'woodhouse/middleware/airbrake_exceptions'
@@ -0,0 +1,35 @@
1
+ class Woodhouse::MiddlewareStack < Array
2
+
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def call(*args, &final)
8
+ stack = make_stack.dup
9
+ next_step = lambda {|*args|
10
+ next_item = stack.shift
11
+ if next_item.nil?
12
+ final.call(*args)
13
+ else
14
+ next_item.call(*args, &next_step)
15
+ end
16
+ }
17
+ next_step.call(*args)
18
+ end
19
+
20
+ private
21
+
22
+ def make_stack
23
+ @stack ||=
24
+ map do |item|
25
+ if item.respond_to?(:call)
26
+ item
27
+ elsif item.respond_to?(:new)
28
+ item.new(@config)
29
+ else
30
+ raise ArgumentError, "bad entry #{item.inspect} in middleware stack"
31
+ end
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,27 @@
1
+ class Woodhouse::MixinRegistry < Woodhouse::Registry
2
+
3
+ class << self
4
+
5
+ def classes
6
+ @classes ||= {}
7
+ end
8
+
9
+ def register(klass)
10
+ register_worker klass.name, klass
11
+ end
12
+
13
+ def register_worker(class_name, klass)
14
+ classes[class_name.to_s] = klass
15
+ end
16
+
17
+ end
18
+
19
+ def [](worker)
20
+ Woodhouse::MixinRegistry.classes[worker.to_s]
21
+ end
22
+
23
+ def each(&blk)
24
+ Woodhouse::MixinRegistry.classes.each &blk
25
+ end
26
+
27
+ end
@@ -0,0 +1,80 @@
1
+ class Woodhouse::NodeConfiguration
2
+ include Woodhouse::Util
3
+
4
+ attr_accessor :registry, :server_info, :runner_type, :dispatcher_type, :logger, :default_threads
5
+ attr_accessor :dispatcher_middleware, :runner_middleware
6
+ attr_accessor :triggers
7
+
8
+ def initialize
9
+ self.default_threads = 1
10
+ self.dispatcher_middleware = Woodhouse::MiddlewareStack.new(self)
11
+ self.runner_middleware = Woodhouse::MiddlewareStack.new(self)
12
+ self.server_info = {}
13
+ self.triggers = Woodhouse::TriggerSet.new
14
+ yield self if block_given?
15
+ end
16
+
17
+ def at(event_name, &blk)
18
+ triggers.add(event_name, &blk)
19
+ end
20
+
21
+ def dispatcher
22
+ @dispatcher ||= dispatcher_type.new(self)
23
+ end
24
+
25
+ def dispatcher_type=(value)
26
+ if value.respond_to?(:to_sym)
27
+ value = lookup_key(value, :Dispatcher)
28
+ end
29
+ @dispatcher = nil
30
+ @dispatcher_type = value
31
+ end
32
+
33
+ def runner_type=(value)
34
+ if value.respond_to?(:to_sym)
35
+ value = lookup_key(value, :Runner)
36
+ end
37
+ @dispatcher = nil
38
+ @runner_type = value
39
+ end
40
+
41
+ def server_info=(hash)
42
+ @server_info = hash ? symbolize_keys(hash) : {}
43
+ end
44
+
45
+ def extension(name, opts = {}, &blk)
46
+ Woodhouse::Extension.install_extension(name, self, opts, &blk)
47
+ end
48
+
49
+ private
50
+
51
+ def lookup_key(key, namespace)
52
+ const = Woodhouse.const_get("#{namespace}s").const_get("#{camelize(key.to_s)}#{namespace}")
53
+ unless const
54
+ raise NameError, "couldn't find Woodhouse::#{namespace}s::#{camelize(key.to_s)}#{namespace} (from #{key})"
55
+ end
56
+ const
57
+ end
58
+
59
+ def symbolize_keys(hash)
60
+ hash.inject({}){|h,(k,v)|
61
+ h[k.to_sym] = v
62
+ h
63
+ }
64
+ end
65
+
66
+ # TODO: detect defaults based on platform
67
+ def self.default
68
+ new do |config|
69
+ config.registry = Woodhouse::MixinRegistry.new
70
+ config.server_info = nil
71
+ config.runner_type = Woodhouse::Runners.guess
72
+ config.dispatcher_type = :local
73
+ config.logger = Logger.new("/dev/null")
74
+ config.dispatcher_middleware << Woodhouse::Middleware::LogDispatch
75
+ config.runner_middleware << Woodhouse::Middleware::LogJobs
76
+ config.runner_middleware << Woodhouse::Middleware::AssignLogger
77
+ end
78
+ end
79
+
80
+ end
@@ -0,0 +1,41 @@
1
+ # TODO: take arguments. Also consider using thor.
2
+ class Woodhouse::Process
3
+
4
+ def initialize(keyw = {})
5
+ @server = keyw[:server] || build_default_server(keyw)
6
+ end
7
+
8
+ def execute
9
+ # Borrowed this from sidekiq. https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/cli.rb
10
+ trap "INT" do
11
+ Thread.main.raise Interrupt
12
+ end
13
+
14
+ trap "TERM" do
15
+ Thread.main.raise Interrupt
16
+ end
17
+
18
+ begin
19
+ @server.start!
20
+ puts "Woodhouse serving as of #{Time.now}. Ctrl-C to stop."
21
+ @server.wait(:shutdown)
22
+ rescue Interrupt
23
+ puts "Shutting down."
24
+ @server.shutdown!
25
+ @server.wait(:shutdown)
26
+ ensure
27
+ @server.terminate
28
+ exit
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def build_default_server(keyw)
35
+ Woodhouse::Server.new(
36
+ :layout => keyw[:layout] || Woodhouse.global_layout,
37
+ :node => keyw[:node] || :default
38
+ )
39
+ end
40
+
41
+ end
@@ -0,0 +1,52 @@
1
+ module Woodhouse
2
+
3
+ class QueueCriteria
4
+ attr_reader :criteria
5
+
6
+ def initialize(opts = {})
7
+ if opts.kind_of?(self.class)
8
+ opts = opts.criteria
9
+ end
10
+ unless opts.nil?
11
+ @criteria = stringify_values(opts).freeze
12
+ end
13
+ end
14
+
15
+ def ==(other)
16
+ @criteria == other.criteria
17
+ end
18
+
19
+ def describe
20
+ @criteria.inspect
21
+ end
22
+
23
+ def amqp_headers
24
+ # TODO: needs to be smarter
25
+ @criteria ? @criteria.merge('x-match' => 'all') : {}
26
+ end
27
+
28
+ def queue_key
29
+ @criteria ? @criteria.map{|k,v|
30
+ "#{k.downcase}_#{v.downcase}"
31
+ }.join("_") : ""
32
+ end
33
+
34
+ def matches?(args)
35
+ return true if @criteria.nil?
36
+ @criteria.all? do |key, val|
37
+ args[key] == val
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def stringify_values(hash)
44
+ hash.inject({}) {|h,(k,v)|
45
+ h[k.to_s] = v.to_s
46
+ h
47
+ }
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,46 @@
1
+ if defined?(Rails::Railtie)
2
+ module Woodhouse::RailsExtensions
3
+ def layout(&blk)
4
+ unless @delay_finished
5
+ @delayed_layout = blk
6
+ else
7
+ super
8
+ end
9
+ end
10
+
11
+ def finish_loading_layout!
12
+ @delay_finished = true
13
+ if @delayed_layout
14
+ layout &@delayed_layout
15
+ end
16
+ end
17
+ end
18
+
19
+ Woodhouse.extend Woodhouse::RailsExtensions
20
+
21
+ class Woodhouse::Rails < Rails::Engine
22
+ # config.autoload_paths << Rails.root.join("app/workers")
23
+
24
+ initializer 'woodhouse' do
25
+ config_paths = %w[woodhouse.yml workling.yml].map{|file|
26
+ Rails.root.join("config/" + file)
27
+ }
28
+ # Preload everything in app/workers so default layout includes them
29
+ Rails.root.join("app/workers").tap do |workers|
30
+ Pathname.glob(workers.join("**/*.rb")).each do |worker_path|
31
+ worker_path.relative_path_from(workers).basename(".rb").to_s.camelize.constantize
32
+ end
33
+ end
34
+ # Set up reasonable defaults
35
+ Woodhouse.configure do |config|
36
+ config.logger = ::Rails.logger
37
+ config_paths.each do |path|
38
+ if File.exist?(path)
39
+ config.server_info = YAML.load(File.read(path))[::Rails.env]
40
+ end
41
+ end
42
+ end
43
+ Woodhouse.finish_loading_layout!
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ require 'woodhouse'
2
+
3
+ ActiveSupport::Dependencies.autoload_paths << RAILS_ROOT + "/app/workers"
4
+
5
+ Woodhouse.configure do |config|
6
+ config_paths = %w[woodhouse.yml workling.yml].map{|file|
7
+ RAILS_ROOT + "/config/" + file
8
+ }
9
+ config.logger = ::Rails.logger
10
+ if ::Rails.env =~ /development|test/
11
+ config.dispatcher_type = :local
12
+ else
13
+ config.dispatcher_type = :bunny
14
+ end
15
+ config_paths.each do |path|
16
+ if File.exist?(path)
17
+ config.server_info = YAML.load(File.read(path))[::Rails.env]
18
+ end
19
+ end
20
+ config.runner_type = Woodhouse::Runners.guess
21
+ end
@@ -0,0 +1,12 @@
1
+ class Woodhouse::Registry
2
+ include Woodhouse::Util
3
+
4
+ def [](worker)
5
+ raise NotImplementedError, "subclass Woodhouse::Registry and override #[]"
6
+ end
7
+
8
+ def each
9
+ raise NotImplementedError, "subclass Woodhouse::Registry and override #each"
10
+ end
11
+
12
+ end
@@ -0,0 +1,60 @@
1
+ #
2
+ # The abstract base class for actors in charge of finding and running jobs
3
+ # of a given type. Runners will be allocated for each Woodhouse::Layout::Worker
4
+ # in a layout. Woodhouse::Layout::Worker#threads indicates how many Runners should
5
+ # be spawned for each job type.
6
+ #
7
+ # The lifecycle of a Runner is to be created by Woodhouse::Scheduler::WorkerSet,
8
+ # and to automatically begin subscribing as soon as it is initialized. At some
9
+ # point, the actor will receive the +spin_down+ message, at which case it must
10
+ # cease all work and return from +subscribe+.
11
+ #
12
+ # Whenever a Runner receives a job on its queue, it should convert it into a
13
+ # Workling::Job and pass it to +service_job+ after confirming with
14
+ # +can_service_job?+ that this is an appropriate job for this queue.
15
+ #
16
+ # Runners should always subscribe to queues in ack mode. Messages should be
17
+ # acked after they finish, and rejected if the job is inappropriate for this
18
+ # worker or if it raises an exception.
19
+ #
20
+ # TODO: document in more detail the contract between Runner and Dispatcher over
21
+ # AMQP exchanges, and how Woodhouse uses AMQP to distribute jobs.
22
+ #
23
+ class Woodhouse::Runner
24
+ include Woodhouse::Util
25
+ include Celluloid
26
+
27
+ def initialize(worker, config)
28
+ @worker = worker
29
+ @config = config
30
+ @config.logger.debug "Thread for #{@worker.describe} ready and waiting for jobs"
31
+ end
32
+
33
+ # Implement this in a subclass. When this message is received by an actor, it should
34
+ # finish whatever job it is currently doing, gracefully disconnect from AMQP, and
35
+ # stop the subscribe loop.
36
+ def spin_down
37
+ raise NotImplementedError, "implement #spin_down in a subclass of Woodhouse::Runner"
38
+ end
39
+
40
+ private
41
+
42
+ # Implement this in a subclass. When this message is received by an actor, it should
43
+ # connect to AMQP and start pulling jobs off the queue. This method should not finish
44
+ # until spin_down is called.
45
+ def subscribe # :doc:
46
+ raise NotImplementedError, "implement #subscribe in a subclass of Woodhouse::Runner"
47
+ end
48
+
49
+ # Returns +true+ if the Job's arguments match this worker's QueueCriteria, else +false+.
50
+ def can_service_job?(job) # :doc:
51
+ @worker.accepts_job?(job)
52
+ end
53
+
54
+ # Executes a Job. See Woodhouse::JobExecution.
55
+ def service_job(job) # :doc:
56
+ @config.logger.debug "Servicing job for #{@worker.describe}"
57
+ Woodhouse::JobExecution.new(@config, job).execute
58
+ end
59
+
60
+ end
@@ -0,0 +1,60 @@
1
+ require 'bunny'
2
+
3
+ class Woodhouse::Runners::BunnyRunner < Woodhouse::Runner
4
+ include Celluloid
5
+
6
+ def subscribe
7
+ bunny = Bunny.new(@config.server_info)
8
+ bunny.start
9
+ channel = bunny.create_channel
10
+ channel.prefetch(1)
11
+ queue = channel.queue(@worker.queue_name)
12
+ exchange = channel.exchange(@worker.exchange_name, :type => :headers)
13
+ queue.bind(exchange, :arguments => @worker.criteria.amqp_headers)
14
+ worker = Celluloid.current_actor
15
+ queue.subscribe(:ack => true, :block => false) do |delivery, props, payload|
16
+ begin
17
+ job = make_job(props, payload)
18
+ if can_service_job?(job)
19
+ if service_job(job)
20
+ channel.acknowledge(delivery.delivery_tag, false)
21
+ else
22
+ channel.reject(delivery.delivery_tag, false)
23
+ end
24
+ else
25
+ @config.logger.error("Cannot service job #{job.describe} in queue for #{@worker.describe}")
26
+ channel.reject(delivery.delivery_tag, false)
27
+ end
28
+ rescue => err
29
+ begin
30
+ @config.logger.error("Error bubbled up out of worker. This shouldn't happen. #{err.message}")
31
+ err.backtrace.each do |btr|
32
+ @config.logger.error(" #{btr}")
33
+ end
34
+ # Don't risk grabbing this job again.
35
+ channel.reject(delivery.delivery_tag, false)
36
+ ensure
37
+ worker.bail_out(err)
38
+ end
39
+ end
40
+ end
41
+ wait :spin_down
42
+ end
43
+
44
+ def bail_out(err)
45
+ raise Woodhouse::BailOut, "#{err.class}: #{err.message}"
46
+ end
47
+
48
+ def spin_down
49
+ signal :spin_down
50
+ end
51
+
52
+ def make_job(properties, payload)
53
+ Woodhouse::Job.new(@worker.worker_class_name, @worker.job_method) do |job|
54
+ args = properties.headers
55
+ job.arguments = args
56
+ job.payload = payload
57
+ end
58
+ end
59
+
60
+ end
@@ -0,0 +1,11 @@
1
+ class Woodhouse::Runners::DummyRunner < Woodhouse::Runner
2
+
3
+ def subscribe
4
+ wait :spin_down
5
+ end
6
+
7
+ def spin_down
8
+ signal :spin_down
9
+ end
10
+
11
+ end