woodhouse 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.travis.yml +5 -0
- data/Gemfile +7 -0
- data/Guardfile +4 -0
- data/MIT-LICENSE +21 -0
- data/PROGRESS-NOTES.txt +5 -0
- data/README.markdown +152 -0
- data/Rakefile +35 -0
- data/THOUGHTS +84 -0
- data/doc/example/script-woodhouse +9 -0
- data/doc/example/woodhouse-initializer.rb +12 -0
- data/lib/generators/woodhouse_generator.rb +38 -0
- data/lib/woodhouse/dispatcher.rb +37 -0
- data/lib/woodhouse/dispatchers/bunny_dispatcher.rb +48 -0
- data/lib/woodhouse/dispatchers/common_amqp_dispatcher.rb +28 -0
- data/lib/woodhouse/dispatchers/hot_bunnies_dispatcher.rb +48 -0
- data/lib/woodhouse/dispatchers/local_dispatcher.rb +13 -0
- data/lib/woodhouse/dispatchers/local_pool_dispatcher.rb +25 -0
- data/lib/woodhouse/dispatchers.rb +19 -0
- data/lib/woodhouse/extension.rb +24 -0
- data/lib/woodhouse/extensions/new_relic/instrumentation_middleware.rb +10 -0
- data/lib/woodhouse/extensions/new_relic.rb +23 -0
- data/lib/woodhouse/extensions/progress.rb +165 -0
- data/lib/woodhouse/job.rb +76 -0
- data/lib/woodhouse/job_execution.rb +60 -0
- data/lib/woodhouse/layout.rb +290 -0
- data/lib/woodhouse/layout_builder.rb +55 -0
- data/lib/woodhouse/layout_serializer.rb +82 -0
- data/lib/woodhouse/middleware/airbrake_exceptions.rb +12 -0
- data/lib/woodhouse/middleware/assign_logger.rb +10 -0
- data/lib/woodhouse/middleware/log_dispatch.rb +21 -0
- data/lib/woodhouse/middleware/log_jobs.rb +22 -0
- data/lib/woodhouse/middleware.rb +16 -0
- data/lib/woodhouse/middleware_stack.rb +35 -0
- data/lib/woodhouse/mixin_registry.rb +27 -0
- data/lib/woodhouse/node_configuration.rb +80 -0
- data/lib/woodhouse/process.rb +41 -0
- data/lib/woodhouse/queue_criteria.rb +52 -0
- data/lib/woodhouse/rails.rb +46 -0
- data/lib/woodhouse/rails2.rb +21 -0
- data/lib/woodhouse/registry.rb +12 -0
- data/lib/woodhouse/runner.rb +60 -0
- data/lib/woodhouse/runners/bunny_runner.rb +60 -0
- data/lib/woodhouse/runners/dummy_runner.rb +11 -0
- data/lib/woodhouse/runners/hot_bunnies_runner.rb +79 -0
- data/lib/woodhouse/runners.rb +16 -0
- data/lib/woodhouse/scheduler.rb +113 -0
- data/lib/woodhouse/server.rb +80 -0
- data/lib/woodhouse/trigger_set.rb +19 -0
- data/lib/woodhouse/version.rb +3 -0
- data/lib/woodhouse/worker.rb +86 -0
- data/lib/woodhouse.rb +99 -0
- data/spec/integration/bunny_worker_process_spec.rb +32 -0
- data/spec/layout_builder_spec.rb +55 -0
- data/spec/layout_spec.rb +143 -0
- data/spec/middleware_stack_spec.rb +56 -0
- data/spec/mixin_registry_spec.rb +15 -0
- data/spec/node_configuration_spec.rb +22 -0
- data/spec/progress_spec.rb +40 -0
- data/spec/queue_criteria_spec.rb +11 -0
- data/spec/scheduler_spec.rb +41 -0
- data/spec/server_spec.rb +72 -0
- data/spec/shared_contexts.rb +70 -0
- data/spec/worker_spec.rb +28 -0
- data/woodhouse.gemspec +37 -0
- metadata +285 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2008 play/type GmbH
|
2
|
+
Copyright (c) 2012 CrowdCompass, Inc.
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/PROGRESS-NOTES.txt
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# Woodhouse
|
2
|
+
|
3
|
+
[<img src="https://secure.travis-ci.org/mboeh/woodhouse.png?branch=master" alt="Build Status" />](http://travis-ci.org/mboeh/woodhouse)
|
4
|
+
|
5
|
+
An AMQP-based background worker system for Ruby designed to make managing heterogenous tasks relatively easy.
|
6
|
+
|
7
|
+
The use case for Woodhouse is for reliable and sane performance in situations where jobs on a single queue may vary significantly
|
8
|
+
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
|
9
|
+
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.
|
10
|
+
|
11
|
+
Woodhouse 0.0.x is production-ready for Rails 2 and Ruby 1.8, while 0.1.x is in active development for Ruby 1.9.
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
### Rails
|
16
|
+
|
17
|
+
Add
|
18
|
+
|
19
|
+
gem 'woodhouse', github: 'mboeh/woodhouse'
|
20
|
+
|
21
|
+
to your Gemfile.
|
22
|
+
|
23
|
+
Run
|
24
|
+
|
25
|
+
% rails generate woodhouse
|
26
|
+
|
27
|
+
to create script/woodhouse and config/initializers/woodhouse.rb.
|
28
|
+
|
29
|
+
### Basic Usage
|
30
|
+
|
31
|
+
The simplest way to set up a worker class is to include Woodhouse::Worker and define public methods.
|
32
|
+
|
33
|
+
class IsisWorker
|
34
|
+
include Woodhouse::Worker
|
35
|
+
|
36
|
+
def pam_gossip(job)
|
37
|
+
puts "Pam gossips about #{job[:who]}."
|
38
|
+
end
|
39
|
+
|
40
|
+
def sterling_insult(job)
|
41
|
+
puts "Sterling insults #{job[:who]}."
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Jobs are dispatched asynchronously to a worker by adding `async_` to the method name:
|
46
|
+
|
47
|
+
IsisWorker.async_pam_gossip :who => "Cyril"
|
48
|
+
|
49
|
+
Woodhouse jobs always take a hash of arguments. The worker receives a Woodhouse::Job, which acts like a hash
|
50
|
+
but also supplies additional functionality.
|
51
|
+
|
52
|
+
### Dispatchers
|
53
|
+
|
54
|
+
The dispatcher used for sending out jobs can be set in the Woodhouse config block:
|
55
|
+
|
56
|
+
Woodhouse.configure do |woodhouse|
|
57
|
+
woodhouse.dispatcher_type = :local # :local_pool | :amqp
|
58
|
+
end
|
59
|
+
|
60
|
+
Calling the `async` version of a job method sends it to the currently configured dispatcher. The default dispatcher
|
61
|
+
type is `:local`, which simply executes the job synchronously (although still passing it through middleware; see below).
|
62
|
+
|
63
|
+
If you want `girl_friday` style in-process threaded backgrounding, you can get that by selecting the `:local_pool`
|
64
|
+
dispatcher.
|
65
|
+
|
66
|
+
Finally, if you want to run your jobs in a background process, you'll need to set up the `:amqp` dispatcher. This will
|
67
|
+
use either the Hot Bunnies library (on JRuby) or the Bunny library (on all other Ruby engines). Bunny is suitable for
|
68
|
+
dispatch but can be a little bit CPU-hungry in the background process. Hot Bunnies works great for both. You don't have
|
69
|
+
to use the same Ruby version for your background process as for the dispatching application -- we use Woodhouse in production
|
70
|
+
with a JRuby background process and MRI frontend processes.
|
71
|
+
|
72
|
+
You'll also need to have RabbitMQ running. If it's running with the default (open) permissions on the local server, you don't
|
73
|
+
need to configure it at all. Otherwise, you'll have to set the server connection info. You have two options for this. On Rails,
|
74
|
+
you can create a config/woodhouse.yml file, formatted similar to config/database.yml:
|
75
|
+
|
76
|
+
production:
|
77
|
+
host: myrabbitmq.server.local
|
78
|
+
vhost: /some-vhost
|
79
|
+
|
80
|
+
(The parameters accepted here are the same used for Bunny.connect; I promise to document them here soon.)
|
81
|
+
|
82
|
+
Otherwise, you can do it in the Woodhouse config block:
|
83
|
+
|
84
|
+
Woodhouse.configure do |woodhouse|
|
85
|
+
woodhouse.server_info = { :host => "myrabbitmq.server.local" }
|
86
|
+
end
|
87
|
+
|
88
|
+
### Running The Background Process
|
89
|
+
|
90
|
+
All you have to do is run `script/woodhouse`. It'll load your Rails environment and start the server process. It responds to QUIT
|
91
|
+
and INT signals correctly; I'm working on seeing if I can get it to restart worker processes with HUP and to dump/load the current
|
92
|
+
layout with USR1/USR2.
|
93
|
+
|
94
|
+
`script/woodhouse` logs job execution and results to `log/woodhouse.log`.
|
95
|
+
|
96
|
+
### Performance Errata
|
97
|
+
|
98
|
+
If you're using JRuby with a large application, I've found that the JVM's permanent generation can get exhausted. If you have
|
99
|
+
plenty of heap but still get GC overhead errors, try bumping up the PermGen by including this on the JRuby command line:
|
100
|
+
|
101
|
+
-J-XX:MaxPermSize=128m
|
102
|
+
|
103
|
+
Performance will generally be better with the `--server` flag:
|
104
|
+
|
105
|
+
--server
|
106
|
+
|
107
|
+
A lot of the jobs I run tend to allocate and dispose a lot of memory very quickly, and Woodhouse will be a long-running process.
|
108
|
+
I've gotten good results from enabling aggressive heap tuning:
|
109
|
+
|
110
|
+
-J-XX:+AggressiveHeap
|
111
|
+
|
112
|
+
## Features
|
113
|
+
|
114
|
+
* Configurable worker sets per server
|
115
|
+
* Configurable number of threads per worker
|
116
|
+
* Segmenting a single queue among multiple workers based on job characteristics (using AMQP header exchanges)
|
117
|
+
* Progress reporting on jobs
|
118
|
+
* New Relic background job reporting
|
119
|
+
* Job dispatch and execution middleware stacks
|
120
|
+
|
121
|
+
## Upcoming
|
122
|
+
|
123
|
+
* Live reconfiguration of workers -- add or remove workers across one or more nodes without restarting
|
124
|
+
* Persistent configuration changes -- configuration changes saved to a data store and kept across deploys
|
125
|
+
* Watchdog/status workers on every node
|
126
|
+
* Web interface
|
127
|
+
|
128
|
+
## To Do
|
129
|
+
|
130
|
+
* Examples and guides
|
131
|
+
* More documentation
|
132
|
+
* Watchdog system
|
133
|
+
|
134
|
+
## Supported Versions
|
135
|
+
|
136
|
+
### woodhouse 0.1.x
|
137
|
+
|
138
|
+
* bunny 0.9.x, RabbitMQ 2.x or later
|
139
|
+
* ruby 1.9
|
140
|
+
* MRI, JRuby, Rubinius 2
|
141
|
+
|
142
|
+
### woodhouse 0.0.x
|
143
|
+
|
144
|
+
* ruby 1.8
|
145
|
+
* MRI, JRuby, Rubinius
|
146
|
+
|
147
|
+
### Acknowledgements
|
148
|
+
|
149
|
+
Woodhouse originated in a substantially modified version of the Workling background worker system, although all code has since
|
150
|
+
been replaced.
|
151
|
+
|
152
|
+
This library was developed for [CrowdCompass](http://crowdcompass.com) and was released as open source with their permission.
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'bundler'
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
|
7
|
+
require 'spec/rake/spectask'
|
8
|
+
namespace :spec do
|
9
|
+
|
10
|
+
Spec::Rake::SpecTask.new(:server) do |t|
|
11
|
+
t.spec_files = FileList["spec/**/*_spec.rb"] - FileList["spec/integration/*_spec.rb"]
|
12
|
+
end
|
13
|
+
|
14
|
+
Spec::Rake::SpecTask.new(:client) do |t|
|
15
|
+
t.spec_files = %w[spec/layout_spec.rb spec/middleware_stack_spec.rb spec/mixin_registry_spec.rb]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
# Full server specs are supported on Ruby 1.9 or JRuby.
|
21
|
+
if RUBY_VERSION.to_f >= 1.9 or %w[jruby rbx].include?(RUBY_ENGINE)
|
22
|
+
task :spec => "spec:server"
|
23
|
+
else
|
24
|
+
task :spec => "spec:client"
|
25
|
+
end
|
26
|
+
|
27
|
+
task :default => :spec
|
28
|
+
|
29
|
+
if ENV['RDOC']
|
30
|
+
require 'rdoc/task'
|
31
|
+
Rake::RDocTask.new(:rdoc) do |t|
|
32
|
+
t.main = "README.rdoc"
|
33
|
+
t.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
34
|
+
end
|
35
|
+
end
|
data/THOUGHTS
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
How I want Ganymede to work:
|
2
|
+
|
3
|
+
# app/workers/foo_bar_worker.rb
|
4
|
+
class FooBarWorker < Ganymede::Worker
|
5
|
+
|
6
|
+
def foo(options)
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
def bar(options)
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
# somewhere that jobs are dispatched
|
17
|
+
|
18
|
+
def do_a_thing(because)
|
19
|
+
@because = because
|
20
|
+
FooBarWorker.foo(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_ganymede
|
24
|
+
{
|
25
|
+
:id => id,
|
26
|
+
:class => self.class.name,
|
27
|
+
:event_name => self.event.name,
|
28
|
+
:size => estimate_job_size,
|
29
|
+
:trigger => @because
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
# config/initializers/ganymede.rb
|
34
|
+
|
35
|
+
# Default configuration: all workers listen to all jobs, one thread per job,
|
36
|
+
# no filtering.
|
37
|
+
Ganymede.configure do |config|
|
38
|
+
config.layout do |layout|
|
39
|
+
# layout.no_default_node! if you do not want a default node type
|
40
|
+
layout.node :bigjobs do |bigjobs|
|
41
|
+
bigjobs.worker :FooBarWorker, :only => { :size => "big" }, :threads => 2
|
42
|
+
bigjobs.worker :FooBarWorker, :only => { :size => "big", :trigger => "scheduled" }, :threads => 1
|
43
|
+
end
|
44
|
+
layout.default.worker :FooBarWorker, :only => { :size => "small", :trigger => "scheduled" }, :threads => 2
|
45
|
+
layout.default.worker :FooBarWorker, :only => { :size => "small", :trigger => "on-demand" }, :threads => 4
|
46
|
+
layout.default.worker :FooBarWorker, :only => { :size => "small", :trigger => "requested" }, :threads => 1
|
47
|
+
end
|
48
|
+
# config.middleware_out << Ganymede::Middleware::AutoSplatObjects
|
49
|
+
end
|
50
|
+
|
51
|
+
class Ganymede::Middleware::AutoSplatObjects
|
52
|
+
|
53
|
+
def call(*opts)
|
54
|
+
if opts.length == 1 and !opts.first.kind_of?(Hash)
|
55
|
+
if opts.respond_to?(:to_ganymede)
|
56
|
+
opts = opts.to_ganymede
|
57
|
+
end
|
58
|
+
end
|
59
|
+
yield opts
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
# equivalent to:
|
65
|
+
layout = Ganymede::Layout.new
|
66
|
+
default = Ganymede::Layout::Node.new(:default)
|
67
|
+
default.default_configuration!
|
68
|
+
default.add_worker Ganymede::Layout::Worker.new(:FooBarWorker, :foo, :only => { :size => "small", :trigger => "scheduled" }, :threads => 2)
|
69
|
+
# ...
|
70
|
+
layout.add_node default
|
71
|
+
bigjobs = Ganymede::Layout::Node.new(:bigjobs)
|
72
|
+
bigjobs.add_worker Ganymede::Layout::Worker.new(:FooBarWorker, :foo, :only => { :size => "big", :trigger => "scheduled" }, :threads => 1)
|
73
|
+
|
74
|
+
server = Ganymede::Server.new(layout, :default)
|
75
|
+
server.start # FIAT LUX
|
76
|
+
|
77
|
+
# ... later ...
|
78
|
+
# ... We really need a special worker just allocated for HugeEvent
|
79
|
+
|
80
|
+
default.add_worker Ganymede::Worker.new(:FooBarWorker, :foo, :only => { :event_name => "HugeEvent" }, :threads => 1
|
81
|
+
|
82
|
+
# Loads the new layout and starts up new workers as required
|
83
|
+
server.layout = layout
|
84
|
+
server.reload
|
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require File.dirname(__FILE__) + '/../config/environment'
|
3
|
+
|
4
|
+
logg = Logger.new(File.dirname(__FILE__) + "/../log/woodhouse.log")
|
5
|
+
logg.level = Logger::DEBUG
|
6
|
+
Woodhouse.global_configuration.logger = logg
|
7
|
+
Celluloid.logger = logg
|
8
|
+
|
9
|
+
Woodhouse::Process.new.execute
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Woodhouse.configure do |woodhouse|
|
2
|
+
woodhouse.runner_middleware << Woodhouse::Middleware::AirbrakeExceptions
|
3
|
+
end
|
4
|
+
|
5
|
+
Woodhouse.layout do |layout|
|
6
|
+
layout.node(:default) do |node|
|
7
|
+
node.all_workers
|
8
|
+
node.remove :ImportWorker
|
9
|
+
node.add :ImportWorker, :threads => 2, :only => { :format => "csv" }
|
10
|
+
node.add :ImportWorker, :threads => 3, :only => { :format => "xml" }
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class WoodhouseGenerator < Rails::Generators::Base
|
2
|
+
desc "Creates initializer and script files for Woodhouse."
|
3
|
+
|
4
|
+
def create_initializer
|
5
|
+
initializer "woodhouse.rb", <<-EOF
|
6
|
+
Woodhouse.configure do |woodhouse|
|
7
|
+
# woodhouse.dispatcher_type = :amqp
|
8
|
+
# woodhouse.extension :progress
|
9
|
+
# woodhouse.extension :new_relic
|
10
|
+
end
|
11
|
+
|
12
|
+
Woodhouse.layout do |layout|
|
13
|
+
layout.node(:default) do |node|
|
14
|
+
node.all_workers
|
15
|
+
end
|
16
|
+
end
|
17
|
+
EOF
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_script
|
21
|
+
create_file "script/woodhouse", <<-EOF
|
22
|
+
#!/usr/bin/env ruby
|
23
|
+
require File.expand_path(File.dirname(__FILE__) + '/../config/environment')
|
24
|
+
|
25
|
+
logg = Logger.new(File.dirname(__FILE__) + "/../log/woodhouse.log")
|
26
|
+
logg.level = Logger::DEBUG
|
27
|
+
logg.formatter = Logger::Formatter.new
|
28
|
+
|
29
|
+
Celluloid.logger = logg
|
30
|
+
Woodhouse.global_configuration.logger = logg
|
31
|
+
|
32
|
+
Woodhouse.global_configuration.dispatcher_type = :amqp
|
33
|
+
|
34
|
+
Woodhouse::Process.new.execute
|
35
|
+
EOF
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Woodhouse::Dispatcher
|
2
|
+
|
3
|
+
def initialize(config, opts = {}, &blk)
|
4
|
+
@config = config
|
5
|
+
after_initialize(config, opts = {}, &blk)
|
6
|
+
end
|
7
|
+
|
8
|
+
def dispatch(class_name, job_method, arguments)
|
9
|
+
dispatch_job Woodhouse::Job.new(class_name, job_method, arguments)
|
10
|
+
end
|
11
|
+
|
12
|
+
def dispatch_job(job)
|
13
|
+
@config.dispatcher_middleware.call(job) {|job|
|
14
|
+
deliver_job(job)
|
15
|
+
}
|
16
|
+
job
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_job(job, data = {})
|
20
|
+
deliver_job_update(job, data)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def after_initialize
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
def deliver_job(job)
|
30
|
+
raise NotImplementedError, "implement #deliver_job in a subclass of Woodhouse::Dispatcher"
|
31
|
+
end
|
32
|
+
|
33
|
+
def deliver_job_update(job, data)
|
34
|
+
raise NotImplementedError, "implement #deliver_job_update in a subclass of Woodhouse::Dispatcher"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
require 'connection_pool'
|
3
|
+
require 'woodhouse/dispatchers/common_amqp_dispatcher'
|
4
|
+
|
5
|
+
class Woodhouse::Dispatchers::BunnyDispatcher < Woodhouse::Dispatchers::CommonAmqpDispatcher
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
super
|
9
|
+
@pool = new_pool
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def publish_job(job, exchange)
|
15
|
+
exchange.publish(job.payload, :headers => job.arguments)
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
retried = false
|
20
|
+
@pool.with do |conn|
|
21
|
+
yield conn
|
22
|
+
end
|
23
|
+
rescue Bunny::ClientTimeout
|
24
|
+
if retried
|
25
|
+
raise
|
26
|
+
else
|
27
|
+
new_pool!
|
28
|
+
retried = true
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def new_pool!
|
36
|
+
@pool = new_pool
|
37
|
+
end
|
38
|
+
|
39
|
+
def new_pool
|
40
|
+
@bunny.stop if @bunny
|
41
|
+
|
42
|
+
bunny = @bunny = Bunny.new(@config.server_info || {})
|
43
|
+
@bunny.start
|
44
|
+
|
45
|
+
ConnectionPool.new { bunny.create_channel }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Provides common behavior shared by the Bunny and HotBunnies dispatchers.
|
2
|
+
class Woodhouse::Dispatchers::CommonAmqpDispatcher < Woodhouse::Dispatcher
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
# Yields an AMQP channel to the block, doing any error handling or synchronization
|
7
|
+
# necessary.
|
8
|
+
def run(&blk)
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
def deliver_job(job)
|
13
|
+
run do |client|
|
14
|
+
exchange = client.exchange(job.exchange_name, :type => :headers)
|
15
|
+
publish_job(job, exchange)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def deliver_job_update(job, data)
|
20
|
+
run do |client|
|
21
|
+
exchange = client.exchange("woodhouse.progress", :type => :direct)
|
22
|
+
# establish durable queue to pick up updates
|
23
|
+
client.queue(job.job_id, :durable => true).bind(exchange, :routing_key => job.job_id)
|
24
|
+
exchange.publish(data.to_json, :routing_key => job.job_id)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#
|
2
|
+
# A Dispatcher 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.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'woodhouse/dispatchers/common_amqp_dispatcher'
|
14
|
+
|
15
|
+
class Woodhouse::Dispatchers::HotBunniesDispatcher < Woodhouse::Dispatchers::CommonAmqpDispatcher
|
16
|
+
|
17
|
+
begin
|
18
|
+
require 'hot_bunnies'
|
19
|
+
rescue LoadError => err
|
20
|
+
define_method(:initialize) {|*args|
|
21
|
+
raise err
|
22
|
+
}
|
23
|
+
else
|
24
|
+
def initialize(config)
|
25
|
+
super
|
26
|
+
new_connection
|
27
|
+
@mutex = Mutex.new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def run
|
34
|
+
@mutex.synchronize do
|
35
|
+
yield @channel
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def publish_job(job, exchange)
|
40
|
+
exchange.publish(job.payload, :properties => { :headers => job.arguments })
|
41
|
+
end
|
42
|
+
|
43
|
+
def new_connection
|
44
|
+
@connection = HotBunnies.connect(@config.server_info)
|
45
|
+
@channel = @connection.create_channel
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Woodhouse::Dispatchers::LocalDispatcher < Woodhouse::Dispatcher
|
2
|
+
|
3
|
+
private
|
4
|
+
|
5
|
+
def deliver_job(job)
|
6
|
+
Woodhouse::JobExecution.new(@config, job).execute
|
7
|
+
end
|
8
|
+
|
9
|
+
def deliver_job_update(job, data)
|
10
|
+
@config.logger.info "[Woodhouse job update] #{job.job_id} -- #{data.inspect}"
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Woodhouse::Dispatchers::LocalPoolDispatcher < Woodhouse::Dispatcher
|
2
|
+
|
3
|
+
class Worker
|
4
|
+
include Celluloid
|
5
|
+
|
6
|
+
def execute(executor)
|
7
|
+
executor.execute
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def after_initialize(config, opts = {}, &blk)
|
14
|
+
@pool = Worker.pool(size: opts[:size] || 10)
|
15
|
+
end
|
16
|
+
|
17
|
+
def deliver_job(job)
|
18
|
+
@pool.async.execute Woodhouse::JobExecution.new(@config, job)
|
19
|
+
end
|
20
|
+
|
21
|
+
def deliver_job_update(job, data)
|
22
|
+
@config.logger.info "[Woodhouse job update] #{job.job_id} -- #{data.inspect}"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Woodhouse::Dispatchers
|
2
|
+
|
3
|
+
def self.default_amqp_dispatcher
|
4
|
+
if RUBY_ENGINE =~ /jruby/
|
5
|
+
Woodhouse::Dispatchers::HotBunniesDispatcher
|
6
|
+
else
|
7
|
+
Woodhouse::Dispatchers::BunnyDispatcher
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'woodhouse/dispatcher'
|
14
|
+
require 'woodhouse/dispatchers/local_dispatcher'
|
15
|
+
require 'woodhouse/dispatchers/bunny_dispatcher'
|
16
|
+
require 'woodhouse/dispatchers/hot_bunnies_dispatcher'
|
17
|
+
require 'woodhouse/dispatchers/local_pool_dispatcher'
|
18
|
+
|
19
|
+
Woodhouse::Dispatchers::AmqpDispatcher = Woodhouse::Dispatchers.default_amqp_dispatcher
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Implements a very basic registry for Woodhouse extensions. This is a Class
|
2
|
+
# rather than a Module because it will eventually be used to provide a more
|
3
|
+
# structured approach than the one Woodhouse::Progress uses.
|
4
|
+
class Woodhouse::Extension
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
attr_accessor :registry
|
9
|
+
|
10
|
+
def register(name, extension)
|
11
|
+
registry[name] = extension
|
12
|
+
end
|
13
|
+
|
14
|
+
def install_extension(name, configuration, opts = {}, &blk)
|
15
|
+
if ext = registry[name]
|
16
|
+
ext.install_extension(configuration, opts, &blk)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
self.registry = {}
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class Woodhouse::NewRelic::InstrumentationMiddleware < Woodhouse::Middleware
|
2
|
+
include NewRelic::Agent::Instrumentation::ControllerInstrumentation
|
3
|
+
|
4
|
+
def call(job, worker)
|
5
|
+
perform_action_with_newrelic_trace(:name => job.job_method, :class_name => job.worker_class_name, :params => job.arguments, :category => :task, :path => job.queue_name) do
|
6
|
+
yield job, worker
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'woodhouse'
|
2
|
+
|
3
|
+
module Woodhouse::NewRelic
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def install_extension(configuration, opts = {}, &blk)
|
8
|
+
require 'woodhouse/extensions/new_relic/instrumentation_middleware'
|
9
|
+
configuration.runner_middleware << Woodhouse::NewRelic::InstrumentationMiddleware
|
10
|
+
configuration.at(:server_start) do
|
11
|
+
::NewRelic::Agent.manual_start
|
12
|
+
configuration.logger.info "New Relic agent started."
|
13
|
+
end
|
14
|
+
configuration.at(:server_end) do
|
15
|
+
::NewRelic::Agent.shutdown
|
16
|
+
configuration.logger.info "New Relic agent shut down."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
Woodhouse::Extension.register :new_relic, Woodhouse::NewRelic
|