qusion 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +11 -0
- data/Gemfile.lock +37 -0
- data/LICENSE +20 -0
- data/README.rdoc +132 -0
- data/Rakefile +71 -0
- data/VERSION +1 -0
- data/lib/qusion.rb +78 -0
- data/lib/qusion/amqp_config.rb +63 -0
- data/lib/qusion/channel_pool.rb +64 -0
- data/qusion.gemspec +79 -0
- data/spec/fixtures/framework-amqp.yml +28 -0
- data/spec/fixtures/hardcoded-amqp.yml +9 -0
- data/spec/mock_rails.rb +14 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/unit/amqp_config_spec.rb +48 -0
- data/spec/unit/channel_pool_spec.rb +48 -0
- data/spec/unit/qusion_spec.rb +81 -0
- metadata +177 -0
data/Gemfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem "rake", ">= 0.8.7"
|
4
|
+
gem "bundler", "~> 1.0.0"
|
5
|
+
gem "amqp", "0.6.7", :require => 'mq'
|
6
|
+
|
7
|
+
group :development do
|
8
|
+
gem "jeweler", "~> 1.5.1" # adds Bundler support for gemspec generation
|
9
|
+
gem "cucumber", ">=0.9.4"
|
10
|
+
gem "rspec", "~>1.3.0"
|
11
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
amqp (0.6.7)
|
5
|
+
eventmachine (>= 0.12.4)
|
6
|
+
builder (3.0.0)
|
7
|
+
cucumber (0.10.0)
|
8
|
+
builder (>= 2.1.2)
|
9
|
+
diff-lcs (~> 1.1.2)
|
10
|
+
gherkin (~> 2.3.2)
|
11
|
+
json (~> 1.4.6)
|
12
|
+
term-ansicolor (~> 1.0.5)
|
13
|
+
diff-lcs (1.1.2)
|
14
|
+
eventmachine (0.12.10)
|
15
|
+
gherkin (2.3.2)
|
16
|
+
json (~> 1.4.6)
|
17
|
+
term-ansicolor (~> 1.0.5)
|
18
|
+
git (1.2.5)
|
19
|
+
jeweler (1.5.1)
|
20
|
+
bundler (~> 1.0.0)
|
21
|
+
git (>= 1.2.5)
|
22
|
+
rake
|
23
|
+
json (1.4.6)
|
24
|
+
rake (0.8.7)
|
25
|
+
rspec (1.3.1)
|
26
|
+
term-ansicolor (1.0.5)
|
27
|
+
|
28
|
+
PLATFORMS
|
29
|
+
ruby
|
30
|
+
|
31
|
+
DEPENDENCIES
|
32
|
+
amqp (= 0.6.7)
|
33
|
+
bundler (~> 1.0.0)
|
34
|
+
cucumber (>= 0.9.4)
|
35
|
+
jeweler (~> 1.5.1)
|
36
|
+
rake (>= 0.8.7)
|
37
|
+
rspec (~> 1.3.0)
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009-2011 Daniel DeLeo, CustomInk, LLC
|
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
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
= Qusion
|
2
|
+
|
3
|
+
Qusion makes AMQP[http://github.com/tmm1/amqp] work with your webserver with
|
4
|
+
no fuss. It offers three features:
|
5
|
+
* It 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.
|
6
|
+
* 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.
|
7
|
+
* YAML configuration files. In Rails, create config/amqp.yml then fill in the details for development, test and production. Use Qusion.start() in your environment.rb file (or an initializer) and you're good to go.
|
8
|
+
|
9
|
+
= This fork of Qusion
|
10
|
+
|
11
|
+
This is a fork of James Tucker's fork[https://github.com/raggi/qusion] of
|
12
|
+
Dan DeLeo's original version[https://github.com/danielsdeleo/qusion]. Tucker
|
13
|
+
did a fair amount of cleanup, mostly to the threading logic, so I went with his
|
14
|
+
fork. Improvements I've made include:
|
15
|
+
* Conversion from a Rails plugin to this gem.
|
16
|
+
* Support for Rails 3.
|
17
|
+
* Removed support for Merb (it complicated the code and tests; just use Rails 3).
|
18
|
+
|
19
|
+
= Before You Start
|
20
|
+
|
21
|
+
Qusion makes it easy to just install and start using AMQP in your application.
|
22
|
+
But there are many ways to use background jobs within a Rails app, so it's
|
23
|
+
worth taking some time to consider the tradeoffs of each approach.
|
24
|
+
|
25
|
+
* 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.
|
26
|
+
* 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/].
|
27
|
+
* 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/].
|
28
|
+
|
29
|
+
= Getting Started
|
30
|
+
|
31
|
+
First you'll need the amqp library and a working RabbitMQ installation. This
|
32
|
+
entails:
|
33
|
+
|
34
|
+
* Install Erlang for your platform
|
35
|
+
* Install RabbitMQ for your platform
|
36
|
+
* On OSX, use Homebrew to install Erlang and RabbitMQ: brew install rabbitmq
|
37
|
+
* Install bundler: http://gembundler.com/
|
38
|
+
== Install Qusion
|
39
|
+
Include the qusion gem in your Gemfile:
|
40
|
+
|
41
|
+
gem "qusion"
|
42
|
+
|
43
|
+
Create an initializer (e.g. config/initializers/qusion.rb) and add:
|
44
|
+
Qusion.start
|
45
|
+
|
46
|
+
EM.next_tick do
|
47
|
+
# do some AMQP stuff
|
48
|
+
end
|
49
|
+
|
50
|
+
And that's it! This will set up AMQP for any ruby app server (tested on
|
51
|
+
mongrel, thin, and passenger). Now, you can use all of AMQP's functionality as
|
52
|
+
normal. In your controllers or models, you might have:
|
53
|
+
|
54
|
+
MQ.new.queue("my-work-queue").publish("do work, son!")
|
55
|
+
|
56
|
+
and it should just work.
|
57
|
+
|
58
|
+
= Channel Pools
|
59
|
+
|
60
|
+
It's considered bad practice to use MQ.new over and over, as it creates a new
|
61
|
+
AMQP channel, and that creates a new Erlang process in RabbitMQ. Erlang
|
62
|
+
processes are super light weight, but you'll be wasting them and causing the
|
63
|
+
Erlang VM GC headaches if you create them wantonly. So don't do that. Instead,
|
64
|
+
use the channel pool provided by Qusion. It's simple: wherever you'd normally
|
65
|
+
put MQ.new, just replace it with Qusion.channel. Examples:
|
66
|
+
|
67
|
+
# Create a queue:
|
68
|
+
Qusion.channel.queue("my-worker-queue")
|
69
|
+
# Topics:
|
70
|
+
Qusion.channel.topic("my-topic-exchange")
|
71
|
+
# etc.
|
72
|
+
|
73
|
+
This feature is a bit experimental, so the optimal pool size isn't known yet.
|
74
|
+
The default is 5. You can change it by adding something like the following to
|
75
|
+
your environment.rb:
|
76
|
+
|
77
|
+
Qusion.channel_pool_size(3)
|
78
|
+
|
79
|
+
= Configuration
|
80
|
+
|
81
|
+
You can put your AMQP server details in config/amqp.yml and Qusion will load
|
82
|
+
it when you call Qusion.start(). Example:
|
83
|
+
|
84
|
+
# Put this in config/amqp.yml
|
85
|
+
default: &default
|
86
|
+
host: localhost
|
87
|
+
port: 5672
|
88
|
+
user: guest
|
89
|
+
pass: guest
|
90
|
+
vhost: /
|
91
|
+
timeout: 3600 # seconds
|
92
|
+
logging: false
|
93
|
+
ssl: false
|
94
|
+
|
95
|
+
development:
|
96
|
+
<<: *default
|
97
|
+
|
98
|
+
test:
|
99
|
+
<<: *default
|
100
|
+
|
101
|
+
If you're too hardcore for Rails (maybe you're using Sinatra or Ramaze), you
|
102
|
+
can still use a YAML config file, but there's no support for different
|
103
|
+
environments. So do something like this:
|
104
|
+
|
105
|
+
# Tell Qusion where your config file is:
|
106
|
+
Qusion.start("/path/to/amqp.yml")
|
107
|
+
|
108
|
+
# Your configuration looks like this:
|
109
|
+
application:
|
110
|
+
host: localhost
|
111
|
+
port: 5672
|
112
|
+
...
|
113
|
+
|
114
|
+
If you just want to get started without configuring anything, Qusion.start()
|
115
|
+
will use the default options if it can't find a config file. And, finally, you
|
116
|
+
can give options directly to Qusion.start() like this:
|
117
|
+
|
118
|
+
Qusion.start(:host => "my-amqp-broker.mydomain.com", :user => "me", :pass => "am_I_really_putting_this_in_VCS?")
|
119
|
+
|
120
|
+
|
121
|
+
= Bugs? Hacking?
|
122
|
+
|
123
|
+
If you find any bugs, or feel the need to add a feature, fork away. You can
|
124
|
+
also contact me directly via the email address in my profile if you have any
|
125
|
+
questions.
|
126
|
+
|
127
|
+
= Shouts
|
128
|
+
* 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]
|
129
|
+
* 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.
|
130
|
+
|
131
|
+
Original author: dan@kallistec.com
|
132
|
+
Forked by: cmurphy@customink.com
|
data/Rakefile
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
require "spec/rake/spectask"
|
6
|
+
require "cucumber"
|
7
|
+
require "cucumber/rake/task"
|
8
|
+
|
9
|
+
desc 'Default: run specs.'
|
10
|
+
task :default => :spec
|
11
|
+
|
12
|
+
begin
|
13
|
+
Bundler.setup(:default, :development)
|
14
|
+
rescue Bundler::BundlerError => e
|
15
|
+
$stderr.puts e.message
|
16
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
17
|
+
exit e.status_code
|
18
|
+
end
|
19
|
+
|
20
|
+
begin
|
21
|
+
require 'jeweler'
|
22
|
+
Jeweler::Tasks.new do |gem|
|
23
|
+
gem.name = "qusion"
|
24
|
+
gem.summary = "Makes AMQP work with Ruby on Rails with no fuss."
|
25
|
+
gem.description = %Q{See the README for more details.}
|
26
|
+
gem.email = "cmurphy@customink.com"
|
27
|
+
gem.homepage = "http://github.com/customink/qusion"
|
28
|
+
gem.authors = ["Dan DeLeo", "Christopher R. Murphy"]
|
29
|
+
end
|
30
|
+
rescue LoadError
|
31
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "Run Cucumber Features"
|
35
|
+
Cucumber::Rake::Task.new do |t|
|
36
|
+
t.cucumber_opts = "-c -n"
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "Run all of the specs"
|
40
|
+
Spec::Rake::SpecTask.new do |t|
|
41
|
+
t.ruby_opts << '-rubygems'
|
42
|
+
t.spec_opts = ['--options', "spec/spec.opts"]
|
43
|
+
t.fail_on_error = false
|
44
|
+
end
|
45
|
+
|
46
|
+
namespace :spec do
|
47
|
+
desc "Generate HTML report for failing examples"
|
48
|
+
Spec::Rake::SpecTask.new('report') do |t|
|
49
|
+
t.ruby_opts << '-rubygems'
|
50
|
+
t.spec_files = FileList['failing_examples/**/*.rb']
|
51
|
+
t.spec_opts = ["--format", "html:doc/tools/reports/failing_examples.html", "--diff", '--options', '"spec/spec.opts"']
|
52
|
+
t.fail_on_error = false
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "Run all spec with RCov"
|
56
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
57
|
+
t.ruby_opts << '-rubygems'
|
58
|
+
t.rcov = true
|
59
|
+
t.rcov_dir = 'doc/tools/coverage/'
|
60
|
+
t.rcov_opts = ['--exclude', 'spec']
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Rake::RDocTask.new do |rdoc|
|
65
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
66
|
+
|
67
|
+
rdoc.rdoc_dir = 'rdoc'
|
68
|
+
rdoc.title = "qusion #{version}"
|
69
|
+
rdoc.rdoc_files.include('README*')
|
70
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
71
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
data/lib/qusion.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require "eventmachine"
|
3
|
+
require "mq"
|
4
|
+
|
5
|
+
require "qusion/channel_pool"
|
6
|
+
require "qusion/amqp_config"
|
7
|
+
|
8
|
+
module Qusion
|
9
|
+
class << self
|
10
|
+
attr_reader :thread
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.start(*opts)
|
14
|
+
amqp_opts = AmqpConfig.new(*opts).config_opts
|
15
|
+
start_amqp_dispatcher(amqp_opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.start_amqp_dispatcher(amqp_settings={})
|
19
|
+
AMQP.settings.merge!(amqp_settings)
|
20
|
+
|
21
|
+
if defined?(::PhusionPassenger) && ::PhusionPassenger.respond_to?(:on_event)
|
22
|
+
::PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
23
|
+
if forked
|
24
|
+
EM.stop if EM.reactor_running?
|
25
|
+
thread && thread.join
|
26
|
+
Thread.current[:mq] = nil
|
27
|
+
AMQP.instance_variable_set(:@conn, nil)
|
28
|
+
end
|
29
|
+
start_in_background
|
30
|
+
die_gracefully_on_signal
|
31
|
+
end
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
start_in_background
|
36
|
+
die_gracefully_on_signal
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.die_gracefully_on_signal
|
40
|
+
Signal.trap("INT") { graceful_stop }
|
41
|
+
Signal.trap("TERM") { graceful_stop }
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.channel
|
45
|
+
ChannelPool.instance.channel
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.channel_pool_size(new_pool_size)
|
49
|
+
ChannelPool.pool_size = new_pool_size
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.start_in_background
|
53
|
+
if EM.reactor_running?
|
54
|
+
raise ArgumentError, 'AMQP already connected' if ready_to_dispatch?
|
55
|
+
AMQP.start
|
56
|
+
else
|
57
|
+
raise ArgumentError, 'Qusion already started' if thread && thread.alive?
|
58
|
+
@thread = Thread.new do
|
59
|
+
EM.run { AMQP.start }
|
60
|
+
raise "Premature AMQP shutdown" unless @graceful_stop
|
61
|
+
end
|
62
|
+
thread.abort_on_exception = true
|
63
|
+
thread.join(0.1) until ready_to_dispatch?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.graceful_stop
|
68
|
+
EM.schedule do
|
69
|
+
@graceful_stop = true
|
70
|
+
AMQP.stop { EM.stop }
|
71
|
+
end
|
72
|
+
thread && thread.join
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.ready_to_dispatch?
|
76
|
+
EM.reactor_running? && AMQP.conn && AMQP.conn.connected?
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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
|
+
@config_path = Rails.root + "/config/amqp.yml"
|
22
|
+
@framework_env = Rails.env
|
23
|
+
end
|
24
|
+
|
25
|
+
def config_opts
|
26
|
+
@config_opts ||= load_config_opts
|
27
|
+
end
|
28
|
+
|
29
|
+
def load_config_opts
|
30
|
+
if config_path && config_from_yaml = load_amqp_config_file
|
31
|
+
if framework_env
|
32
|
+
framework_amqp_opts(config_from_yaml)
|
33
|
+
else
|
34
|
+
amqp_opts(config_from_yaml)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def framework_amqp_opts(config_hash)
|
42
|
+
symbolize_keys(config_hash[framework_env.to_s])
|
43
|
+
end
|
44
|
+
|
45
|
+
def amqp_opts(config_hash)
|
46
|
+
symbolize_keys(config_hash.first.last)
|
47
|
+
end
|
48
|
+
|
49
|
+
def symbolize_keys(config_hash)
|
50
|
+
symbolized_hsh = {}
|
51
|
+
config_hash.each {|option, value| symbolized_hsh[option.to_sym] = value }
|
52
|
+
symbolized_hsh
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_amqp_config_file
|
56
|
+
begin
|
57
|
+
YAML.load_file(config_path)
|
58
|
+
rescue Errno::ENOENT
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
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
|
data/qusion.gemspec
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{qusion}
|
8
|
+
s.version = "0.1.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Dan DeLeo", "Christopher R. Murphy"]
|
12
|
+
s.date = %q{2010-12-09}
|
13
|
+
s.description = %q{See the README for more details.}
|
14
|
+
s.email = %q{cmurphy@customink.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
"Gemfile",
|
21
|
+
"Gemfile.lock",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"lib/qusion.rb",
|
27
|
+
"lib/qusion/amqp_config.rb",
|
28
|
+
"lib/qusion/channel_pool.rb",
|
29
|
+
"qusion.gemspec",
|
30
|
+
"spec/fixtures/framework-amqp.yml",
|
31
|
+
"spec/fixtures/hardcoded-amqp.yml",
|
32
|
+
"spec/mock_rails.rb",
|
33
|
+
"spec/spec.opts",
|
34
|
+
"spec/spec_helper.rb",
|
35
|
+
"spec/unit/amqp_config_spec.rb",
|
36
|
+
"spec/unit/channel_pool_spec.rb",
|
37
|
+
"spec/unit/qusion_spec.rb"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/customink/qusion}
|
40
|
+
s.require_paths = ["lib"]
|
41
|
+
s.rubygems_version = %q{1.3.7}
|
42
|
+
s.summary = %q{Makes AMQP work with Ruby on Rails with no fuss.}
|
43
|
+
s.test_files = [
|
44
|
+
"spec/mock_rails.rb",
|
45
|
+
"spec/spec_helper.rb",
|
46
|
+
"spec/unit/amqp_config_spec.rb",
|
47
|
+
"spec/unit/channel_pool_spec.rb",
|
48
|
+
"spec/unit/qusion_spec.rb"
|
49
|
+
]
|
50
|
+
|
51
|
+
if s.respond_to? :specification_version then
|
52
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
53
|
+
s.specification_version = 3
|
54
|
+
|
55
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
56
|
+
s.add_runtime_dependency(%q<rake>, [">= 0.8.7"])
|
57
|
+
s.add_runtime_dependency(%q<bundler>, ["~> 1.0.0"])
|
58
|
+
s.add_runtime_dependency(%q<amqp>, ["= 0.6.7"])
|
59
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
|
60
|
+
s.add_development_dependency(%q<cucumber>, [">= 0.9.4"])
|
61
|
+
s.add_development_dependency(%q<rspec>, ["~> 1.3.0"])
|
62
|
+
else
|
63
|
+
s.add_dependency(%q<rake>, [">= 0.8.7"])
|
64
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
65
|
+
s.add_dependency(%q<amqp>, ["= 0.6.7"])
|
66
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
67
|
+
s.add_dependency(%q<cucumber>, [">= 0.9.4"])
|
68
|
+
s.add_dependency(%q<rspec>, ["~> 1.3.0"])
|
69
|
+
end
|
70
|
+
else
|
71
|
+
s.add_dependency(%q<rake>, [">= 0.8.7"])
|
72
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
73
|
+
s.add_dependency(%q<amqp>, ["= 0.6.7"])
|
74
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
75
|
+
s.add_dependency(%q<cucumber>, [">= 0.9.4"])
|
76
|
+
s.add_dependency(%q<rspec>, ["~> 1.3.0"])
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
@@ -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
|
data/spec/mock_rails.rb
ADDED
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-f specdoc -c -t 2
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require File.dirname(__FILE__) + "/../../spec_helper"
|
3
|
+
require File.dirname(__FILE__) + "/../../mock_rails"
|
4
|
+
|
5
|
+
describe AmqpConfig do
|
6
|
+
|
7
|
+
it "should use #{Rails.root}/config/amqp.yml" do
|
8
|
+
AmqpConfig.new.config_path.should == "/path/to/rails/config/amqp.yml"
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should use the provided path no matter what" do
|
12
|
+
path = AmqpConfig.new("/custom/path/to/amqp.yml").config_path
|
13
|
+
path.should == "/custom/path/to/amqp.yml"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should use a provided options hash if given" do
|
17
|
+
conf = AmqpConfig.new(:host => "my-broker.mydomain.com")
|
18
|
+
conf.config_path.should be_nil
|
19
|
+
conf.config_opts.should == {:host => "my-broker.mydomain.com"}
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should use the default amqp options in rails if amqp.yml doesn't exist" do
|
23
|
+
Rails.stub!(:root).and_return(File.dirname(__FILE__) + '/../')
|
24
|
+
AmqpConfig.new.config_opts.should == {}
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should load a YAML file when using a framework" do
|
28
|
+
conf = AmqpConfig.new
|
29
|
+
conf.stub!(:config_path).and_return(File.dirname(__FILE__) + "/../fixtures/framework-amqp.yml")
|
30
|
+
conf.stub!(:framework_env).and_return("production")
|
31
|
+
conf.config_opts.should == {:host => 'localhost',:port => 5672,
|
32
|
+
:user => 'guest', :pass => 'guest',
|
33
|
+
:vhost => '/', :timeout => 3600,
|
34
|
+
:logging => false, :ssl => false}
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should use the first set of opts when given a explicit file path" do
|
38
|
+
conf = AmqpConfig.new
|
39
|
+
conf.stub!(:config_path).and_return(File.dirname(__FILE__) + "/../fixtures/hardcoded-amqp.yml")
|
40
|
+
conf.stub!(:framework_env).and_return(nil)
|
41
|
+
p conf.config_opts
|
42
|
+
conf.config_opts.should == {:host => 'localhost',:port => 5672,
|
43
|
+
:user => 'guest', :pass => 'guest',
|
44
|
+
:vhost => '/', :timeout => 600,
|
45
|
+
:logging => false, :ssl => false}
|
46
|
+
end
|
47
|
+
|
48
|
+
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,81 @@
|
|
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(:config => :opts)
|
22
|
+
Qusion.should_receive(:start_amqp_dispatcher).with(:config => :opts)
|
23
|
+
Qusion.start
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
describe Qusion, 'amqp startup' do
|
29
|
+
|
30
|
+
before do
|
31
|
+
AMQP.stub!(:settings).and_return({})
|
32
|
+
end
|
33
|
+
|
34
|
+
after(:each) do
|
35
|
+
Object.send(:remove_const, :PhusionPassenger) if defined? ::PhusionPassenger
|
36
|
+
Object.send(:remove_const, :Thin) if defined? ::Thin
|
37
|
+
Object.send(:remove_const, :Mongrel) if defined? ::Mongrel
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should kill the reactor and start a new AMQP connection when forked in Passenger" do
|
41
|
+
Qusion.should_receive(:die_gracefully_on_signal).once
|
42
|
+
::PhusionPassenger = Module.new
|
43
|
+
forked = mock("starting_worker_process_callback_obj")
|
44
|
+
::PhusionPassenger.should_receive(:on_event).with(:starting_worker_process).and_yield(forked)
|
45
|
+
EM.should_receive(:reactor_running?).exactly(3).times.and_return(true)
|
46
|
+
amqp_conn = mock('amqp_conn')
|
47
|
+
amqp_conn.should_receive(:connected?).and_return(false)
|
48
|
+
AMQP.should_receive(:conn).any_number_of_times.and_return(amqp_conn)
|
49
|
+
EM.should_receive(:stop)
|
50
|
+
AMQP.should_receive(:start).once
|
51
|
+
Qusion.start_amqp_dispatcher
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should set AMQP's connection settings when running under Thin" do
|
55
|
+
Qusion.should_receive(:die_gracefully_on_signal)
|
56
|
+
Qusion.should_receive(:start_in_background)
|
57
|
+
::Thin = Module.new
|
58
|
+
Qusion.start_amqp_dispatcher(:cookie => "yummy")
|
59
|
+
AMQP.settings[:cookie].should == "yummy"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should start a worker thread when running under Mongrel" do
|
63
|
+
Qusion.should_receive(:die_gracefully_on_signal)
|
64
|
+
mock_thread = mock('thread')
|
65
|
+
mock_thread.should_receive(:abort_on_exception=).with(true)
|
66
|
+
Qusion.should_receive(:ready_to_dispatch?).twice.and_return(false, true)
|
67
|
+
mock_thread.should_receive(:join)
|
68
|
+
Thread.should_receive(:new).and_return(mock_thread)
|
69
|
+
::Mongrel = Module.new
|
70
|
+
Qusion.start_amqp_dispatcher
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should be ready to dispatch when the reactor is running and amqp is connected" do
|
74
|
+
EM.should_receive(:reactor_running?).and_return(true)
|
75
|
+
amqp_conn = mock('amqp_conn')
|
76
|
+
amqp_conn.should_receive(:connected?).and_return(true)
|
77
|
+
AMQP.should_receive(:conn).any_number_of_times.and_return(amqp_conn)
|
78
|
+
Qusion.ready_to_dispatch?.should == true
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
metadata
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: qusion
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Dan DeLeo
|
13
|
+
- Christopher R. Murphy
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-12-09 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rake
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 8
|
31
|
+
- 7
|
32
|
+
version: 0.8.7
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: bundler
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 1
|
45
|
+
- 0
|
46
|
+
- 0
|
47
|
+
version: 1.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: amqp
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - "="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
- 6
|
61
|
+
- 7
|
62
|
+
version: 0.6.7
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: *id003
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: jeweler
|
68
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ~>
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
segments:
|
74
|
+
- 1
|
75
|
+
- 5
|
76
|
+
- 1
|
77
|
+
version: 1.5.1
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *id004
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: cucumber
|
83
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
segments:
|
89
|
+
- 0
|
90
|
+
- 9
|
91
|
+
- 4
|
92
|
+
version: 0.9.4
|
93
|
+
type: :development
|
94
|
+
prerelease: false
|
95
|
+
version_requirements: *id005
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: rspec
|
98
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
segments:
|
104
|
+
- 1
|
105
|
+
- 3
|
106
|
+
- 0
|
107
|
+
version: 1.3.0
|
108
|
+
type: :development
|
109
|
+
prerelease: false
|
110
|
+
version_requirements: *id006
|
111
|
+
description: See the README for more details.
|
112
|
+
email: cmurphy@customink.com
|
113
|
+
executables: []
|
114
|
+
|
115
|
+
extensions: []
|
116
|
+
|
117
|
+
extra_rdoc_files:
|
118
|
+
- LICENSE
|
119
|
+
- README.rdoc
|
120
|
+
files:
|
121
|
+
- Gemfile
|
122
|
+
- Gemfile.lock
|
123
|
+
- LICENSE
|
124
|
+
- README.rdoc
|
125
|
+
- Rakefile
|
126
|
+
- VERSION
|
127
|
+
- lib/qusion.rb
|
128
|
+
- lib/qusion/amqp_config.rb
|
129
|
+
- lib/qusion/channel_pool.rb
|
130
|
+
- qusion.gemspec
|
131
|
+
- spec/fixtures/framework-amqp.yml
|
132
|
+
- spec/fixtures/hardcoded-amqp.yml
|
133
|
+
- spec/mock_rails.rb
|
134
|
+
- spec/spec.opts
|
135
|
+
- spec/spec_helper.rb
|
136
|
+
- spec/unit/amqp_config_spec.rb
|
137
|
+
- spec/unit/channel_pool_spec.rb
|
138
|
+
- spec/unit/qusion_spec.rb
|
139
|
+
has_rdoc: true
|
140
|
+
homepage: http://github.com/customink/qusion
|
141
|
+
licenses: []
|
142
|
+
|
143
|
+
post_install_message:
|
144
|
+
rdoc_options: []
|
145
|
+
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
hash: 2610415720212610617
|
154
|
+
segments:
|
155
|
+
- 0
|
156
|
+
version: "0"
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
none: false
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
segments:
|
163
|
+
- 0
|
164
|
+
version: "0"
|
165
|
+
requirements: []
|
166
|
+
|
167
|
+
rubyforge_project:
|
168
|
+
rubygems_version: 1.3.7
|
169
|
+
signing_key:
|
170
|
+
specification_version: 3
|
171
|
+
summary: Makes AMQP work with Ruby on Rails with no fuss.
|
172
|
+
test_files:
|
173
|
+
- spec/mock_rails.rb
|
174
|
+
- spec/spec_helper.rb
|
175
|
+
- spec/unit/amqp_config_spec.rb
|
176
|
+
- spec/unit/channel_pool_spec.rb
|
177
|
+
- spec/unit/qusion_spec.rb
|