gongren 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +82 -0
- data/lib/gongren.rb +4 -0
- data/lib/gongren/server.rb +58 -0
- data/lib/gongren/worker.rb +166 -0
- data/test/helper.rb +10 -0
- data/test/test_gongren.rb +7 -0
- data/vendor/qusion/LICENSE +20 -0
- data/vendor/qusion/README.rdoc +102 -0
- data/vendor/qusion/Rakefile +35 -0
- data/vendor/qusion/init.rb +1 -0
- data/vendor/qusion/install.rb +1 -0
- data/vendor/qusion/lib/qusion.rb +28 -0
- data/vendor/qusion/lib/qusion/amqp.rb +31 -0
- data/vendor/qusion/lib/qusion/amqp_config.rb +70 -0
- data/vendor/qusion/lib/qusion/channel_pool.rb +64 -0
- data/vendor/qusion/lib/qusion/em.rb +13 -0
- data/vendor/qusion/lib/qusion/server_spy.rb +24 -0
- data/vendor/qusion/spec/fixtures/framework-amqp.yml +28 -0
- data/vendor/qusion/spec/fixtures/hardcoded-amqp.yml +9 -0
- data/vendor/qusion/spec/spec.opts +1 -0
- data/vendor/qusion/spec/spec_helper.rb +10 -0
- data/vendor/qusion/spec/unit/amqp_config_spec.rb +63 -0
- data/vendor/qusion/spec/unit/amqp_spec.rb +42 -0
- data/vendor/qusion/spec/unit/channel_pool_spec.rb +48 -0
- data/vendor/qusion/spec/unit/em_spec.rb +14 -0
- data/vendor/qusion/spec/unit/qusion_spec.rb +26 -0
- data/vendor/qusion/spec/unit/server_spy_spec.rb +63 -0
- metadata +114 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 François Beausoleil
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
= gongren
|
2
|
+
|
3
|
+
Gongren distributes jobs to workers, with support for failed worker daemons and load balancing.
|
4
|
+
|
5
|
+
Gongren is Chinese worker: 工人. See http://www.ehow.com/video_4403851_say-worker-chinese.html for pronunciation.
|
6
|
+
|
7
|
+
== Note on Patches/Pull Requests
|
8
|
+
|
9
|
+
* Fork the project.
|
10
|
+
* Make your feature addition or bug fix.
|
11
|
+
* Add tests for it. This is important so I don't break it in a
|
12
|
+
future version unintentionally.
|
13
|
+
* Commit, do not mess with rakefile, version, or history.
|
14
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
15
|
+
* Send me a pull request. Bonus points for topic branches.
|
16
|
+
|
17
|
+
== Copyright
|
18
|
+
|
19
|
+
Copyright (c) 2010 François Beausoleil. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "gongren"
|
8
|
+
gem.summary = %Q{Gongren distributes jobs to workers, with support for failed worker daemons and load balancing.}
|
9
|
+
gem.description = %Q{A gem that's currently tied to Rails to distribute jobs.}
|
10
|
+
gem.email = "francois@teksol.info"
|
11
|
+
gem.homepage = "http://github.com/francois/gongren"
|
12
|
+
gem.authors = ["François Beausoleil"]
|
13
|
+
gem.require_paths << "vendor/qusion/lib"
|
14
|
+
gem.files = FileList["lib/**/*", "vendor/**/*", "LICENSE", "README.rdoc", "Rakefile"]
|
15
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
16
|
+
gem.add_development_dependency "yard", ">= 0"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
|
19
|
+
gem.add_dependency "amqp", "~> 0.6.6"
|
20
|
+
end
|
21
|
+
Jeweler::GemcutterTasks.new
|
22
|
+
rescue LoadError
|
23
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rake/testtask'
|
27
|
+
Rake::TestTask.new(:test) do |test|
|
28
|
+
test.libs << 'lib' << 'test'
|
29
|
+
test.pattern = 'test/**/test_*.rb'
|
30
|
+
test.verbose = true
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
require 'rcov/rcovtask'
|
35
|
+
Rcov::RcovTask.new do |test|
|
36
|
+
test.libs << 'test'
|
37
|
+
test.pattern = 'test/**/test_*.rb'
|
38
|
+
test.verbose = true
|
39
|
+
end
|
40
|
+
rescue LoadError
|
41
|
+
task :rcov do
|
42
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
task :test => :check_dependencies
|
47
|
+
|
48
|
+
begin
|
49
|
+
require 'reek/adapters/rake_task'
|
50
|
+
Reek::RakeTask.new do |t|
|
51
|
+
t.fail_on_error = true
|
52
|
+
t.verbose = false
|
53
|
+
t.source_files = 'lib/**/*.rb'
|
54
|
+
end
|
55
|
+
rescue LoadError
|
56
|
+
task :reek do
|
57
|
+
abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
begin
|
62
|
+
require 'roodi'
|
63
|
+
require 'roodi_task'
|
64
|
+
RoodiTask.new do |t|
|
65
|
+
t.verbose = false
|
66
|
+
end
|
67
|
+
rescue LoadError
|
68
|
+
task :roodi do
|
69
|
+
abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
task :default => :test
|
74
|
+
|
75
|
+
begin
|
76
|
+
require 'yard'
|
77
|
+
YARD::Rake::YardocTask.new
|
78
|
+
rescue LoadError
|
79
|
+
task :yardoc do
|
80
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
81
|
+
end
|
82
|
+
end
|
data/lib/gongren.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "qusion"
|
3
|
+
|
4
|
+
module Gongren
|
5
|
+
# This version is intimately tied to Rails.
|
6
|
+
class Server
|
7
|
+
def initialize(options={})
|
8
|
+
@options = options.inject(Hash.new) {|memo, (k,v)| memo[k.to_sym] = v; memo} # #symbolize_keys
|
9
|
+
@logger = options[:logger] || Logger.new(options[:log] || STDERR)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Submits a unit of work to the pool of workers.
|
13
|
+
def self.submit(name, unit)
|
14
|
+
data = Marshal.dump(unit)
|
15
|
+
topic.publish(data, :persistent => true, :key => "unit.#{name}")
|
16
|
+
end
|
17
|
+
|
18
|
+
# A quick way to instantiate a server with some options.
|
19
|
+
def self.start(options={})
|
20
|
+
new(options).start
|
21
|
+
end
|
22
|
+
|
23
|
+
# Starts the reactor / event loop.
|
24
|
+
def start
|
25
|
+
logger.info { "Gongren::Server #{Process.pid} starting" }
|
26
|
+
Qusion.start(@options)
|
27
|
+
self.class.control_topic # Instantiates the control topic, for later use
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :options, :logger
|
33
|
+
|
34
|
+
def self.exchange_name
|
35
|
+
"gongren.work"
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.exchange_options
|
39
|
+
{:durable => true}
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.topic
|
43
|
+
Qusion.channel.topic(exchange_name, exchange_options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.control_exchange_name
|
47
|
+
"gongren.worker.control"
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.control_exchange_options
|
51
|
+
{}
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.control_topic
|
55
|
+
Qusion.channel.topic(control_exchange_name, control_exchange_options)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require "json"
|
2
|
+
require "mq"
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module Gongren
|
6
|
+
# A worker does work. Units of work are received and processed locally. All units of work will be
|
7
|
+
# acknowledged back to the server, ensuring units of work are executed exactly once.
|
8
|
+
#
|
9
|
+
# Units of work are received as Hashes from the {Gongren::Server}. The Hash that is passed to the
|
10
|
+
# #run block will be dynamically injected with {Gongren::Worker::Unit}, which includes the {Gongren::Worker::Unit#ack}
|
11
|
+
# method. If the block returns and the message hasn't been acknowledged, it will be for you.
|
12
|
+
#
|
13
|
+
# == Notes on use
|
14
|
+
#
|
15
|
+
# If you do any database work, it is important to wrap your work in a transaction, because if your
|
16
|
+
# worker dies, the same work unit will be resubmitted.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
#
|
20
|
+
# # In a Rails context, this would live in script/gongren-worker:
|
21
|
+
# require File.dirname(__FILE__) + "/../config/environment"
|
22
|
+
# require "gongren/worker"
|
23
|
+
#
|
24
|
+
# Gengren::Worker.run do |unit|
|
25
|
+
# ActiveRecord::Base.transaction do
|
26
|
+
# klass_name = unit[:class_name]
|
27
|
+
# klass = klass_name.constantize
|
28
|
+
# instance = klass.find(unit[:id])
|
29
|
+
# results = instance.send(unit[:selector], *unit[:args])
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # We don't have two phase commit yet, this acknowledging outside the
|
33
|
+
# # transaction might execute a message twice.
|
34
|
+
# unit.ack
|
35
|
+
# end
|
36
|
+
class Worker
|
37
|
+
def initialize(options={})
|
38
|
+
@options = options.inject(Hash.new) {|memo, (k,v)| memo[k.to_sym] = v; memo} # #symbolize_keys
|
39
|
+
@logger = options[:logger] || Logger.new(options[:log] || STDERR)
|
40
|
+
end
|
41
|
+
|
42
|
+
def run
|
43
|
+
raise ArgumentError, "#run must be called with a block" unless block_given?
|
44
|
+
|
45
|
+
logger.info { "Gongren::Worker #{worker_id} ready to work" }
|
46
|
+
|
47
|
+
EM.run do
|
48
|
+
MQ.queue(control_queue_name, control_queue_options).bind(control_exchange_name, control_exchange_options) do |header, data|
|
49
|
+
message = Marshal.load(data)
|
50
|
+
logger.info { message.inspect }
|
51
|
+
|
52
|
+
if message[:selector].to_s.strip.empty? then
|
53
|
+
logger.error { "Received control request without :selector key: ignoring" }
|
54
|
+
else
|
55
|
+
begin
|
56
|
+
send(message[:selector], message)
|
57
|
+
rescue Exception => e
|
58
|
+
log_failure(header, message, e)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
MQ.queue(queue_name, queue_options).bind(exchange_name, exchange_options).subscribe do |header, data|
|
64
|
+
message = Marshal.load(data)
|
65
|
+
class << message; include Unit; end # Dynamically add our #ack method
|
66
|
+
message.gongren_header = header
|
67
|
+
|
68
|
+
logger.info { message.inspect }
|
69
|
+
|
70
|
+
begin
|
71
|
+
yield message
|
72
|
+
|
73
|
+
# Automatically ack messages, but do it only once
|
74
|
+
logger.debug { "Block ack'd? #{message.acked?}" }
|
75
|
+
unless message.acked?
|
76
|
+
logger.debug { "Ack'ing for the block" }
|
77
|
+
message.ack
|
78
|
+
end
|
79
|
+
rescue Exception => e
|
80
|
+
log_failure(header, message, e)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# A quick way to run a worker. Creates an instance with the options and runs the block
|
87
|
+
# whenever a message is received, passing the exact object that was sent from the server.
|
88
|
+
def self.run(options={}, &block)
|
89
|
+
new(options).run(&block)
|
90
|
+
end
|
91
|
+
|
92
|
+
module Unit
|
93
|
+
attr_writer :gongren_header
|
94
|
+
|
95
|
+
def ack
|
96
|
+
@gongren_header.ack
|
97
|
+
@acked = true
|
98
|
+
end
|
99
|
+
|
100
|
+
def acked?
|
101
|
+
!!@acked
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def log_failure(header, message, exception)
|
108
|
+
logger.error do
|
109
|
+
<<-EOF.gsub(/^\s{10}/, "")
|
110
|
+
============
|
111
|
+
#{header.inspect}
|
112
|
+
#{message.inspect}
|
113
|
+
message.acked? #{message.acked?}
|
114
|
+
============
|
115
|
+
#{e.class_name}: #{e.message}
|
116
|
+
#{e.backtrace.join("\n")}.
|
117
|
+
EOF
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def ping(message)
|
122
|
+
return logger.error { "Received #ping message without corresponding :exchange key: ignoring" } if message[:exchange].to_s.strip.empty?
|
123
|
+
MQ.direct(data[:exchange]).publish(worker_id)
|
124
|
+
end
|
125
|
+
|
126
|
+
attr_reader :logger, :options
|
127
|
+
|
128
|
+
def queue_name
|
129
|
+
"gongren.worker"
|
130
|
+
end
|
131
|
+
|
132
|
+
def queue_options
|
133
|
+
# We want a durable queue: one that survives server restarts, and that will hold messages until
|
134
|
+
# a worker is available to grab them, even if all worker process are down.
|
135
|
+
{:durable => true}
|
136
|
+
end
|
137
|
+
|
138
|
+
def exchange_name
|
139
|
+
"gongren.work"
|
140
|
+
end
|
141
|
+
|
142
|
+
def exchange_options
|
143
|
+
{:key => "unit.#"}
|
144
|
+
end
|
145
|
+
|
146
|
+
def control_queue_name
|
147
|
+
"gongren.worker.control.#{worker_id}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def control_queue_options
|
151
|
+
{}
|
152
|
+
end
|
153
|
+
|
154
|
+
def control_exchange_name
|
155
|
+
"gongren.worker.control"
|
156
|
+
end
|
157
|
+
|
158
|
+
def control_exchange_options
|
159
|
+
{}
|
160
|
+
end
|
161
|
+
|
162
|
+
def worker_id
|
163
|
+
"#{Process.pid}.#{Thread.current.object_id}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Daniel DeLeo
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
= Qusion
|
2
|
+
Qusion makes AMQP[http://github.com/tmm1/amqp] work with your webserver with no fuss. It's a simple library/plugin with three features:
|
3
|
+
* A set of monkey patches that sets up the required callbacks and/or worker threads so that AMQP will work with Passenger, Thin, or Mongrel. WEBrick, SCGI, and Evented Mongrel are experimentally supported, but not heavily tested.
|
4
|
+
* A Channel Pool. You can cause problems for yourself if you create new channels (with MQ.new) for every request. The pool sets up a few of these when your app starts and reuses them.
|
5
|
+
* YAML configuration files. If you're using Rails or Merb, create config/amqp.yml, then fill in the details for development, test, and production. Use Qusion.start() in your environment.rb file and you're good to go.
|
6
|
+
|
7
|
+
= Before You Start
|
8
|
+
Qusion makes it easy to just install the plugin and start using AMQP in your application. But there are many ways to use background jobs within a Rails app, so it's worth taking some time to consider the tradeoffs of each approach.
|
9
|
+
|
10
|
+
* If your background job needs are simple and you're using a relational database, Delayed::Job[http://github.com/tobi/delayed_job/] lets you schedule background tasks through the database. You won't need to run another application (the AMQP Broker) to keep your app running.
|
11
|
+
* The 0.6.x version of the ruby amqp library may drop messages when the AMQP broker goes down. Pivotal Labs has discussed this problem on their blog[http://pivots.pivotallabs.com/users/will/blog/articles/966-how-to-not-test-rabbitmq-part-1]. This issue will likely be addressed in the 0.7.0 release of amqp, but can be avoided entirely using a synchronous amqp library such as bunny[http://github.com/celldee/bunny]. For a ready-made background job solution using Bunny to publish jobs to the queue, see Minion[http://github.com/orionz/minion/].
|
12
|
+
* Qusion runs EventMachine in a separate thread on Phusion Passenger, Mongrel, and other non-evented servers. There are some inefficiencies in Ruby 1.8's threading model that make running EM in a thread quite slow. Joe Damato and Aman Gupta have created a patch[http://github.com/ice799/matzruby/tree/heap_stacks] for the problem which is included in an experimental branch of REE. You can learn more about the patch from Phusion's Blog[http://blog.phusion.nl/2009/12/15/google-tech-talk-on-ruby-enterprise-edition/].
|
13
|
+
|
14
|
+
= Getting Started
|
15
|
+
First you'll need the amqp library and a working RabbitMQ installation. This entails:
|
16
|
+
* Install Erlang for your platform
|
17
|
+
* Install RabbitMQ for your platform
|
18
|
+
* (sudo) gem install amqp
|
19
|
+
Ezmobius has a good walk-through on the readme for nanite[http://github.com/ezmobius/nanite/] if you haven't done this yet.
|
20
|
+
== Install Qusion
|
21
|
+
Start by installing Qusion as a plugin:
|
22
|
+
|
23
|
+
script/plugin install git://github.com/danielsdeleo/qusion.git
|
24
|
+
|
25
|
+
Next, in your config/environment.rb, add something like:
|
26
|
+
|
27
|
+
# Add eventmachine and amqp gems to config.gem to get config.gem goodies:
|
28
|
+
config.gem "eventmachine"
|
29
|
+
config.gem "amqp"
|
30
|
+
|
31
|
+
# Start AMQP after rails loads:
|
32
|
+
config.after_initialize do
|
33
|
+
Qusion.start # no options needed if you're using config/amqp.yml or the default settings.
|
34
|
+
end
|
35
|
+
|
36
|
+
And that's it! This will set up AMQP for any ruby app server (tested on mongrel, thin, and passenger). Now, you can use all of AMQP's functionality as normal. In your controllers or models, you might have:
|
37
|
+
|
38
|
+
MQ.new.queue("my-work-queue").publish("do work, son!")
|
39
|
+
|
40
|
+
and it should just work.
|
41
|
+
|
42
|
+
= Channel Pools
|
43
|
+
It's considered bad practice to use MQ.new over and over, as it creates a new AMQP channel, and that creates a new Erlang process in RabbitMQ. Erlang processes are super light weight, but you'll be wasting them and causing the Erlang VM GC headaches if you create them wantonly. So don't do that. Instead, use the channel pool provided by Qusion. It's simple: wherever you'd normally put MQ.new, just replace it with Qusion.channel. Examples:
|
44
|
+
|
45
|
+
# Create a queue:
|
46
|
+
Qusion.channel.queue("my-worker-queue")
|
47
|
+
# Topics:
|
48
|
+
Qusion.channel.topic("my-topic-exchange")
|
49
|
+
# etc.
|
50
|
+
|
51
|
+
This feature is a bit experimental, so the optimal pool size isn't known yet. The default is 5. You can change it by adding something like the following to your environment.rb:
|
52
|
+
|
53
|
+
Qusion.channel_pool_size(3)
|
54
|
+
|
55
|
+
= Configuration
|
56
|
+
If you're using rails or merb, you can put your AMQP server details in config/amqp.yml and Qusion will load it when you call Qusion.start(). Example:
|
57
|
+
|
58
|
+
# Put this in config/amqp.yml
|
59
|
+
development:
|
60
|
+
host: localhost
|
61
|
+
port: 5672
|
62
|
+
user: guest
|
63
|
+
pass: guest
|
64
|
+
vhost: /
|
65
|
+
timeout: 3600
|
66
|
+
logging: false
|
67
|
+
ssl: false
|
68
|
+
|
69
|
+
test:
|
70
|
+
host: localhost
|
71
|
+
port: 5672
|
72
|
+
...
|
73
|
+
|
74
|
+
production:
|
75
|
+
host: localhost
|
76
|
+
port: 5672
|
77
|
+
...
|
78
|
+
|
79
|
+
If you're too hardcore for rails or merb (maybe you're using Sinatra or Ramaze), you can still use a YAML config file, but there's no support for different environments. So do something like this:
|
80
|
+
|
81
|
+
# Tell Qusion where your config file is:
|
82
|
+
Qusion.start("/path/to/amqp.yml")
|
83
|
+
|
84
|
+
# Your configuration looks like this:
|
85
|
+
application:
|
86
|
+
host: localhost
|
87
|
+
port: 5672
|
88
|
+
...
|
89
|
+
|
90
|
+
If you just want to get started without configuring anything, Qusion.start() will use the default options if it can't find a config file. And, finally, you can give options directly to Qusion.start() like this:
|
91
|
+
|
92
|
+
Qusion.start(:host => "my-amqp-broker.mydomain.com", :user => "me", :pass => "am_I_really_putting_this_in_VCS?")
|
93
|
+
|
94
|
+
|
95
|
+
= Bugs? Hacking?
|
96
|
+
If you find any bugs, or feel the need to add a feature, fork away. You can also contact me directly via the email address in my profile if you have any quesions.
|
97
|
+
|
98
|
+
= Shouts
|
99
|
+
* Qusion's code for Phusion Passenger's starting_worker_process event was originally posted by Aman Gupta (tmm1[http://github.com/tmm1]) on the AMQP list[http://groups.google.com/group/ruby-amqp]
|
100
|
+
* Brightbox's Warren[http://github.com/brightbox/warren] library provides some similar functionality. It doesn't support webserver-specific EventMachine setup, but it does have built-in encryption and support for the synchronous (non-EventMachine) Bunny[http://github.com/celldee/bunny] AMQP client.
|
101
|
+
|
102
|
+
dan@kallistec.com
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require "spec/rake/spectask"
|
3
|
+
require "cucumber"
|
4
|
+
require "cucumber/rake/task"
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
desc "Run Cucumber Features"
|
9
|
+
Cucumber::Rake::Task.new do |t|
|
10
|
+
t.cucumber_opts = "-c -n"
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run all of the specs"
|
14
|
+
Spec::Rake::SpecTask.new do |t|
|
15
|
+
t.spec_opts = ['--options', "spec/spec.opts"]
|
16
|
+
t.fail_on_error = false
|
17
|
+
end
|
18
|
+
|
19
|
+
namespace :spec do
|
20
|
+
|
21
|
+
desc "Generate HTML report for failing examples"
|
22
|
+
Spec::Rake::SpecTask.new('report') do |t|
|
23
|
+
t.spec_files = FileList['failing_examples/**/*.rb']
|
24
|
+
t.spec_opts = ["--format", "html:doc/tools/reports/failing_examples.html", "--diff", '--options', '"spec/spec.opts"']
|
25
|
+
t.fail_on_error = false
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Run all spec with RCov"
|
29
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
30
|
+
t.rcov = true
|
31
|
+
t.rcov_dir = 'doc/tools/coverage/'
|
32
|
+
t.rcov_opts = ['--exclude', 'spec']
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# Don't need to do anything special.
|
@@ -0,0 +1 @@
|
|
1
|
+
# Nothing to see here (yet), folks
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
unless defined?(QUSION_ROOT)
|
3
|
+
QUSION_ROOT = File.dirname(__FILE__) + '/'
|
4
|
+
end
|
5
|
+
|
6
|
+
require "eventmachine"
|
7
|
+
require "mq"
|
8
|
+
|
9
|
+
require QUSION_ROOT + "qusion/server_spy"
|
10
|
+
require QUSION_ROOT + "qusion/em"
|
11
|
+
require QUSION_ROOT + "qusion/amqp"
|
12
|
+
require QUSION_ROOT + "qusion/channel_pool"
|
13
|
+
require QUSION_ROOT + "qusion/amqp_config"
|
14
|
+
|
15
|
+
module Qusion
|
16
|
+
def self.start(*opts)
|
17
|
+
amqp_opts = AmqpConfig.new(*opts).config_opts
|
18
|
+
AMQP.start_web_dispatcher(amqp_opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.channel
|
22
|
+
ChannelPool.instance.channel
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.channel_pool_size(new_pool_size)
|
26
|
+
ChannelPool.pool_size = new_pool_size
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module AMQP
|
4
|
+
def self.start_web_dispatcher(amqp_settings={})
|
5
|
+
@settings = settings.merge(amqp_settings)
|
6
|
+
case Qusion::ServerSpy.server_type
|
7
|
+
when :passenger
|
8
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
9
|
+
if forked
|
10
|
+
EM.kill_reactor
|
11
|
+
Thread.current[:mq], @conn = nil, nil
|
12
|
+
end
|
13
|
+
Thread.new { start }
|
14
|
+
die_gracefully_on_signal
|
15
|
+
end
|
16
|
+
when :standard
|
17
|
+
Thread.new { start }
|
18
|
+
die_gracefully_on_signal
|
19
|
+
when :evented
|
20
|
+
die_gracefully_on_signal
|
21
|
+
when :none
|
22
|
+
else
|
23
|
+
raise ArgumentError, "AMQP#start_web_dispatcher requires an argument of [:standard|:evented|:passenger|:none]"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.die_gracefully_on_signal
|
28
|
+
Signal.trap("INT") { AMQP.stop { EM.stop } }
|
29
|
+
Signal.trap("TERM") { AMQP.stop { EM.stop } }
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Qusion
|
5
|
+
|
6
|
+
class AmqpConfig
|
7
|
+
attr_reader :config_path, :framework_env
|
8
|
+
|
9
|
+
def initialize(opts=nil)
|
10
|
+
if opts && opts.respond_to?(:keys)
|
11
|
+
@config_path = nil
|
12
|
+
@config_opts = opts
|
13
|
+
elsif opts
|
14
|
+
@config_path = opts
|
15
|
+
else
|
16
|
+
load_framework_config
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def load_framework_config
|
21
|
+
if defined?(RAILS_ROOT)
|
22
|
+
@config_path = RAILS_ROOT + "/config/amqp.yml"
|
23
|
+
@framework_env = RAILS_ENV
|
24
|
+
elsif defined?(Merb)
|
25
|
+
@config_path = Merb.root + "/config/amqp.yml"
|
26
|
+
@framework_env = Merb.environment
|
27
|
+
else
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def config_opts
|
33
|
+
@config_opts ||= load_config_opts
|
34
|
+
end
|
35
|
+
|
36
|
+
def load_config_opts
|
37
|
+
if config_path && config_from_yaml = load_amqp_config_file
|
38
|
+
if framework_env
|
39
|
+
framework_amqp_opts(config_from_yaml)
|
40
|
+
else
|
41
|
+
amqp_opts(config_from_yaml)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def framework_amqp_opts(config_hash)
|
49
|
+
symbolize_keys(config_hash[framework_env.to_s])
|
50
|
+
end
|
51
|
+
|
52
|
+
def amqp_opts(config_hash)
|
53
|
+
symbolize_keys(config_hash.first.last)
|
54
|
+
end
|
55
|
+
|
56
|
+
def symbolize_keys(config_hash)
|
57
|
+
symbolized_hsh = {}
|
58
|
+
config_hash.each {|option, value| symbolized_hsh[option.to_sym] = value }
|
59
|
+
symbolized_hsh
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_amqp_config_file
|
63
|
+
begin
|
64
|
+
YAML.load_file(config_path)
|
65
|
+
rescue Errno::ENOENT
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require "singleton"
|
3
|
+
|
4
|
+
module Qusion
|
5
|
+
|
6
|
+
# ChannelPool maintains a pool of AMQP channel objects that can be reused.
|
7
|
+
# The motivation behind this is that if you were to use MQ.new to create a
|
8
|
+
# new channel for every request, your AMQP broker could be swamped trying to
|
9
|
+
# maintain a bunch of channels that you're only using once.
|
10
|
+
#
|
11
|
+
# To use the channel pool, just replace <tt>MQ.new</tt> in your code with <tt>Qusion.channel</tt>
|
12
|
+
#
|
13
|
+
# # Instead of this:
|
14
|
+
# MQ.new.queue("my-worker-queue")
|
15
|
+
# # Do this:
|
16
|
+
# Qusion.channel.queue("my-worker-queue")
|
17
|
+
#
|
18
|
+
# By default, ChannelPool maintains a pool of 5 channels. This can be adjusted with
|
19
|
+
# <tt>ChannelPool.pool_size=()</tt> or <tt>Qusion.channel_pool_size()</tt>
|
20
|
+
# The optimal pool size is not yet known, but I suspect you might need a
|
21
|
+
# larger value if using Thin in production, and a smaller value otherwise.
|
22
|
+
class ChannelPool
|
23
|
+
include Singleton
|
24
|
+
|
25
|
+
class << self
|
26
|
+
|
27
|
+
def pool_size=(new_pool_size)
|
28
|
+
reset
|
29
|
+
@pool_size = new_pool_size
|
30
|
+
end
|
31
|
+
|
32
|
+
def pool_size
|
33
|
+
@pool_size ||= 5
|
34
|
+
end
|
35
|
+
|
36
|
+
def reset
|
37
|
+
@pool_size = nil
|
38
|
+
instance.reset
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :pool
|
44
|
+
|
45
|
+
def channel
|
46
|
+
@i ||= 1
|
47
|
+
@i = (@i + 1) % pool_size
|
48
|
+
pool[@i]
|
49
|
+
end
|
50
|
+
|
51
|
+
def pool
|
52
|
+
@pool ||= Array.new(pool_size) { MQ.new }
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset
|
56
|
+
@pool = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def pool_size
|
60
|
+
self.class.pool_size
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Qusion
|
4
|
+
module ServerSpy
|
5
|
+
extend self
|
6
|
+
def server_type
|
7
|
+
if defined?(::PhusionPassenger)
|
8
|
+
:passenger
|
9
|
+
elsif defined?(::Mongrel) && defined?(::Mongrel::MongrelProtocol)
|
10
|
+
:evented
|
11
|
+
elsif defined?(::Mongrel)
|
12
|
+
:standard
|
13
|
+
elsif defined?(::SCGI)
|
14
|
+
:standard
|
15
|
+
elsif defined?(::WEBrick)
|
16
|
+
:standard
|
17
|
+
elsif defined?(::Thin)
|
18
|
+
:evented
|
19
|
+
else
|
20
|
+
:none
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
development:
|
2
|
+
host: localhost
|
3
|
+
port: 5672
|
4
|
+
user: guest
|
5
|
+
pass: guest
|
6
|
+
vhost: /
|
7
|
+
timeout: 600
|
8
|
+
logging: false
|
9
|
+
ssl: false
|
10
|
+
|
11
|
+
test:
|
12
|
+
host: localhost
|
13
|
+
port: 5672
|
14
|
+
user: guest
|
15
|
+
pass: guest
|
16
|
+
vhost: /
|
17
|
+
logging: false
|
18
|
+
ssl: false
|
19
|
+
|
20
|
+
production:
|
21
|
+
host: localhost
|
22
|
+
port: 5672
|
23
|
+
user: guest
|
24
|
+
pass: guest
|
25
|
+
vhost: /
|
26
|
+
timeout: 3600
|
27
|
+
logging: false
|
28
|
+
ssl: false
|
@@ -0,0 +1 @@
|
|
1
|
+
-f specdoc -c -t 2
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
3
|
+
|
4
|
+
describe AmqpConfig do
|
5
|
+
|
6
|
+
after(:each) do
|
7
|
+
Object.send(:remove_const, :RAILS_ROOT) if defined? ::RAILS_ROOT
|
8
|
+
Object.send(:remove_const, :Merb) if defined? ::Merb
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should use RAILS_ROOT/config/amqp.yml if RAILS_ROOT is defined" do
|
12
|
+
::RAILS_ROOT = "/path/to/rails"
|
13
|
+
::RAILS_ENV = nil
|
14
|
+
AmqpConfig.new.config_path.should == "/path/to/rails/config/amqp.yml"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should use \#{Merb.root}/config/amqp.yml if RAILS_ROOT is undefined and Merb is defined" do
|
18
|
+
::Merb = mock("merby")
|
19
|
+
::Merb.should_receive(:root).and_return("/path/to/merb")
|
20
|
+
::Merb.should_receive(:environment).and_return(nil)
|
21
|
+
AmqpConfig.new.config_path.should == "/path/to/merb/config/amqp.yml"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should use the provided path no matter what" do
|
25
|
+
::RAILS_ROOT = nil
|
26
|
+
::Merb = nil
|
27
|
+
path = AmqpConfig.new("/custom/path/to/amqp.yml").config_path
|
28
|
+
path.should == "/custom/path/to/amqp.yml"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should use a provided options hash if given" do
|
32
|
+
::RAILS_ROOT = nil
|
33
|
+
::Merb = nil
|
34
|
+
conf = AmqpConfig.new(:host => "my-broker.mydomain.com")
|
35
|
+
conf.config_path.should be_nil
|
36
|
+
conf.config_opts.should == {:host => "my-broker.mydomain.com"}
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should use the default amqp options in rails if amqp.yml doesn't exist" do
|
40
|
+
::RAILS_ROOT = File.dirname(__FILE__) + '/../'
|
41
|
+
AmqpConfig.new.config_opts.should == {}
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should load a YAML file when using a framework" do
|
45
|
+
conf = AmqpConfig.new
|
46
|
+
conf.stub!(:config_path).and_return(File.dirname(__FILE__) + "/../fixtures/framework-amqp.yml")
|
47
|
+
conf.stub!(:framework_env).and_return("production")
|
48
|
+
conf.config_opts.should == {:host => 'localhost',:port => 5672,
|
49
|
+
:user => 'guest', :pass => 'guest',
|
50
|
+
:vhost => '/', :timeout => 3600,
|
51
|
+
:logging => false, :ssl => false}
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should use the first set of opts when given a explicit file path" do
|
55
|
+
conf = AmqpConfig.new
|
56
|
+
conf.stub!(:config_path).and_return(File.dirname(__FILE__) + "/../fixtures/hardcoded-amqp.yml")
|
57
|
+
conf.config_opts.should == {:host => 'localhost',:port => 5672,
|
58
|
+
:user => 'guest', :pass => 'guest',
|
59
|
+
:vhost => '/', :timeout => 600,
|
60
|
+
:logging => false, :ssl => false}
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
3
|
+
|
4
|
+
describe AMQP do
|
5
|
+
|
6
|
+
before do
|
7
|
+
AMQP.stub!(:settings).and_return({})
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
Object.send(:remove_const, :PhusionPassenger) if defined? ::PhusionPassenger
|
12
|
+
Object.send(:remove_const, :Thin) if defined? ::Thin
|
13
|
+
Object.send(:remove_const, :Mongrel) if defined? ::Mongrel
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should kill the reactor and start a new AMQP connection when forked in Passenger" do
|
17
|
+
AMQP.should_receive(:die_gracefully_on_signal)
|
18
|
+
::PhusionPassenger = Module.new
|
19
|
+
forked = mock("starting_worker_process_callback_obj")
|
20
|
+
::PhusionPassenger.should_receive(:on_event).with(:starting_worker_process).and_yield(forked)
|
21
|
+
EM.should_receive(:kill_reactor)
|
22
|
+
AMQP.should_receive(:start)
|
23
|
+
AMQP.start_web_dispatcher
|
24
|
+
sleep 0.1 # give the thread time to run, esp. on ruby 1.9
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should set AMQP's connection settings when running under Thin" do
|
28
|
+
AMQP.should_receive(:die_gracefully_on_signal)
|
29
|
+
::Thin = Module.new
|
30
|
+
AMQP.start_web_dispatcher({:cookie => "yummy"})
|
31
|
+
AMQP.instance_variable_get(:@settings)[:cookie].should == "yummy"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should start a worker thread when running under Mongrel" do
|
35
|
+
AMQP.should_receive(:die_gracefully_on_signal)
|
36
|
+
::Mongrel = Module.new
|
37
|
+
AMQP.should_receive(:start)
|
38
|
+
AMQP.start_web_dispatcher
|
39
|
+
sleep 0.1 # give the thread time to run, esp. on ruby 1.9
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
3
|
+
|
4
|
+
describe ChannelPool do
|
5
|
+
MQ = Object.new
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
ChannelPool.reset
|
9
|
+
@channel_pool = ChannelPool.instance
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should be singleton" do
|
13
|
+
lambda { ChannelPool.new }.should raise_error
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should adjust the pool size" do
|
17
|
+
ChannelPool.pool_size = 5
|
18
|
+
ChannelPool.pool_size.should == 5
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should reset itself when the pool size is set" do
|
22
|
+
ChannelPool.should_receive(:reset)
|
23
|
+
ChannelPool.pool_size = 23
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should create a pool of AMQP channels" do
|
27
|
+
ChannelPool.pool_size = 3
|
28
|
+
::MQ.should_receive(:new).exactly(3).times
|
29
|
+
@channel_pool.pool
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should default to a pool size of 5" do
|
33
|
+
::MQ.should_receive(:new).exactly(5).times.and_return("swanky")
|
34
|
+
@channel_pool.pool
|
35
|
+
@channel_pool.instance_variable_get(:@pool).should == %w{ swanky swanky swanky swanky swanky}
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should return a channel in a round-robin" do
|
39
|
+
@channel_pool.instance_variable_set(:@pool, [1,2,3,4,5])
|
40
|
+
@channel_pool.channel.should == 3
|
41
|
+
@channel_pool.channel.should == 4
|
42
|
+
@channel_pool.channel.should == 5
|
43
|
+
@channel_pool.channel.should == 1
|
44
|
+
@channel_pool.channel.should == 2
|
45
|
+
@channel_pool.channel.should == 3
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
3
|
+
|
4
|
+
describe EM do
|
5
|
+
|
6
|
+
it "should kill the reactor for forking" do
|
7
|
+
EM.should_receive(:reactor_running?).and_return(true)
|
8
|
+
EM.should_receive(:stop_event_loop)
|
9
|
+
EM.should_receive(:release_machine)
|
10
|
+
EM.kill_reactor
|
11
|
+
EM.instance_variable_get(:@reactor_running).should be_false
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
3
|
+
|
4
|
+
describe "Qusion Convenience Methods" do
|
5
|
+
|
6
|
+
it "should get a channel from the pool" do
|
7
|
+
channel_pool = mock("channel pool")
|
8
|
+
ChannelPool.should_receive(:instance).and_return(channel_pool)
|
9
|
+
channel_pool.should_receive(:channel)
|
10
|
+
Qusion.channel
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should set the channel pool size" do
|
14
|
+
ChannelPool.should_receive(:pool_size=).with(7)
|
15
|
+
Qusion.channel_pool_size(7)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should load the configuration and setup AMQP for the webserver" do
|
19
|
+
config = mock("config")
|
20
|
+
AmqpConfig.should_receive(:new).and_return(config)
|
21
|
+
config.should_receive(:config_opts).and_return("tasty cookie")
|
22
|
+
AMQP.should_receive(:start_web_dispatcher).with("tasty cookie")
|
23
|
+
Qusion.start
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
3
|
+
|
4
|
+
describe ServerSpy do
|
5
|
+
|
6
|
+
after do
|
7
|
+
Object.send(:remove_const, :SCGI) if defined? ::SCGI
|
8
|
+
Object.send(:remove_const, :WEBrick) if defined? ::WEBrick
|
9
|
+
Object.send(:remove_const, :PhusionPassenger) if defined? ::PhusionPassenger
|
10
|
+
Object.send(:remove_const, :Thin) if defined? ::Thin
|
11
|
+
Mongrel.send(:remove_const, :MongrelProtocol) if defined?(::Mongrel::MongrelProtocol)
|
12
|
+
Object.send(:remove_const, :Mongrel) if defined? ::Mongrel
|
13
|
+
end
|
14
|
+
|
15
|
+
it "maps evented mongrel to :evented" do
|
16
|
+
::Mongrel = Module.new
|
17
|
+
::Mongrel::MongrelProtocol = Module.new
|
18
|
+
ServerSpy.server_type.should == :evented
|
19
|
+
end
|
20
|
+
|
21
|
+
it "maps Mongrel to :standard" do
|
22
|
+
::Mongrel = Module.new
|
23
|
+
ServerSpy.server_type.should == :standard
|
24
|
+
end
|
25
|
+
|
26
|
+
it "maps WEBrick to :standard" do
|
27
|
+
::WEBrick = Module.new
|
28
|
+
ServerSpy.server_type.should == :standard
|
29
|
+
end
|
30
|
+
|
31
|
+
it "maps SCGI to :standard" do
|
32
|
+
::SCGI = Module.new
|
33
|
+
ServerSpy.server_type.should == :standard
|
34
|
+
end
|
35
|
+
|
36
|
+
it "maps PhusionPassenger to :passenger" do
|
37
|
+
::PhusionPassenger = Module.new
|
38
|
+
ServerSpy.server_type.should == :passenger
|
39
|
+
end
|
40
|
+
|
41
|
+
it "maps Thin to :evented" do
|
42
|
+
::Thin = Module.new
|
43
|
+
ServerSpy.server_type.should == :evented
|
44
|
+
end
|
45
|
+
|
46
|
+
# Rails after 2.2(?) to edge circa Aug 2009 loads thin if it's installed no matter what
|
47
|
+
it "gives the server type as :standard if both Thin and Mongrel are defined" do
|
48
|
+
::Mongrel = Module.new
|
49
|
+
::Thin = Module.new
|
50
|
+
ServerSpy.server_type.should == :standard
|
51
|
+
end
|
52
|
+
|
53
|
+
it "gives the server type as :passenger if both Thin and PhusionPassenger" do
|
54
|
+
::PhusionPassenger = Module.new
|
55
|
+
::Thin = Module.new
|
56
|
+
ServerSpy.server_type.should == :passenger
|
57
|
+
end
|
58
|
+
|
59
|
+
it "gives the server type as :none if no supported server is found" do
|
60
|
+
ServerSpy.server_type.should == :none
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gongren
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Fran\xC3\xA7ois Beausoleil"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-16 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thoughtbot-shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: yard
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: amqp
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.6.6
|
44
|
+
version:
|
45
|
+
description: A gem that's currently tied to Rails to distribute jobs.
|
46
|
+
email: francois@teksol.info
|
47
|
+
executables: []
|
48
|
+
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files:
|
52
|
+
- LICENSE
|
53
|
+
- README.rdoc
|
54
|
+
files:
|
55
|
+
- LICENSE
|
56
|
+
- README.rdoc
|
57
|
+
- Rakefile
|
58
|
+
- lib/gongren.rb
|
59
|
+
- lib/gongren/server.rb
|
60
|
+
- lib/gongren/worker.rb
|
61
|
+
- vendor/qusion/LICENSE
|
62
|
+
- vendor/qusion/README.rdoc
|
63
|
+
- vendor/qusion/Rakefile
|
64
|
+
- vendor/qusion/init.rb
|
65
|
+
- vendor/qusion/install.rb
|
66
|
+
- vendor/qusion/lib/qusion.rb
|
67
|
+
- vendor/qusion/lib/qusion/amqp.rb
|
68
|
+
- vendor/qusion/lib/qusion/amqp_config.rb
|
69
|
+
- vendor/qusion/lib/qusion/channel_pool.rb
|
70
|
+
- vendor/qusion/lib/qusion/em.rb
|
71
|
+
- vendor/qusion/lib/qusion/server_spy.rb
|
72
|
+
- vendor/qusion/spec/fixtures/framework-amqp.yml
|
73
|
+
- vendor/qusion/spec/fixtures/hardcoded-amqp.yml
|
74
|
+
- vendor/qusion/spec/spec.opts
|
75
|
+
- vendor/qusion/spec/spec_helper.rb
|
76
|
+
- vendor/qusion/spec/unit/amqp_config_spec.rb
|
77
|
+
- vendor/qusion/spec/unit/amqp_spec.rb
|
78
|
+
- vendor/qusion/spec/unit/channel_pool_spec.rb
|
79
|
+
- vendor/qusion/spec/unit/em_spec.rb
|
80
|
+
- vendor/qusion/spec/unit/qusion_spec.rb
|
81
|
+
- vendor/qusion/spec/unit/server_spy_spec.rb
|
82
|
+
has_rdoc: true
|
83
|
+
homepage: http://github.com/francois/gongren
|
84
|
+
licenses: []
|
85
|
+
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options:
|
88
|
+
- --charset=UTF-8
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
- vendor/qusion/lib
|
92
|
+
- vendor/qusion/lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: "0"
|
98
|
+
version:
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: "0"
|
104
|
+
version:
|
105
|
+
requirements: []
|
106
|
+
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 1.3.5
|
109
|
+
signing_key:
|
110
|
+
specification_version: 3
|
111
|
+
summary: Gongren distributes jobs to workers, with support for failed worker daemons and load balancing.
|
112
|
+
test_files:
|
113
|
+
- test/helper.rb
|
114
|
+
- test/test_gongren.rb
|