cwyckoff-rosetta_queue 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +9 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.rdoc +131 -0
- data/Rakefile +30 -0
- data/VERSION.yml +4 -0
- data/cucumber.yml +1 -0
- data/lib/rosetta_queue.rb +19 -0
- data/lib/rosetta_queue/adapter.rb +39 -0
- data/lib/rosetta_queue/adapters/amqp_bunny.rb +138 -0
- data/lib/rosetta_queue/adapters/base.rb +27 -0
- data/lib/rosetta_queue/adapters/fake.rb +26 -0
- data/lib/rosetta_queue/adapters/null.rb +57 -0
- data/lib/rosetta_queue/adapters/stomp.rb +73 -0
- data/lib/rosetta_queue/base.rb +15 -0
- data/lib/rosetta_queue/consumer.rb +30 -0
- data/lib/rosetta_queue/consumer_managers/base.rb +22 -0
- data/lib/rosetta_queue/consumer_managers/evented.rb +43 -0
- data/lib/rosetta_queue/consumer_managers/threaded.rb +75 -0
- data/lib/rosetta_queue/destinations.rb +34 -0
- data/lib/rosetta_queue/exceptions.rb +10 -0
- data/lib/rosetta_queue/filters.rb +42 -0
- data/lib/rosetta_queue/logger.rb +27 -0
- data/lib/rosetta_queue/message_handler.rb +33 -0
- data/lib/rosetta_queue/producer.rb +14 -0
- data/lib/rosetta_queue/spec_helpers.rb +5 -0
- data/lib/rosetta_queue/spec_helpers/hash.rb +21 -0
- data/lib/rosetta_queue/spec_helpers/helpers.rb +47 -0
- data/lib/rosetta_queue/spec_helpers/publishing_matchers.rb +144 -0
- metadata +87 -0
data/History.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
== 0.1.x (git)
|
|
2
|
+
|
|
3
|
+
=== New features
|
|
4
|
+
* Beanstalk Adaper (David Brady)
|
|
5
|
+
* Still needs some work to have it take advantage of beanstalk's subscribe funtionality.
|
|
6
|
+
=== Bufixes
|
|
7
|
+
|
|
8
|
+
== 0.1.0 / 2008-01-28 - Initial Release
|
|
9
|
+
RosettaQueue was realased in the wild! RQ's initial development was primarily sponsored by Alliance Health Networks (thanks!!). The original authors were Chris Wyckoff and Ben Mabey.The initial release included adapters for stomp and amqp, in addition to a null and fake adapters for testing.
|
data/MIT-LICENSE.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2008-2009 Chris Wyckoff, Ben Mabey
|
|
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.
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
= Rosetta Queue
|
|
2
|
+
|
|
3
|
+
Rosetta Queue is a messaging gateway API with adapters for many messaging systems available in Ruby. Messaging systems can be easily switched out with a small configuration change. Code for testing on the object and application level is also provided.
|
|
4
|
+
|
|
5
|
+
The adapters provided currently are for stomp, amqp, and beanstalk. We would like to add adapters for other messaging gateways. The stomp adapter has been used in production along side with Apache's ActiveMQ. The amqp adapter currently works along side RabbitMQ and passes the acceptance tests but as of yet has not been used in production.
|
|
6
|
+
|
|
7
|
+
== Quick Tutorial
|
|
8
|
+
Note: The API will most likely change until we reach 1.0. We will be moving to a more concise API (i.e. queue(:test_queue) << message, etc...) We will also be changing how exceptions are handled.
|
|
9
|
+
|
|
10
|
+
When using Rosetta Queue in an application you will need to configure the queues, adapters, and filters (optional). These configurations should be placed in a file that gets loaded once when your program starts. If you are using Rails then a good place for this is config/initializers/rosetta_queue.rb.
|
|
11
|
+
|
|
12
|
+
To set up destinations to produce messages to and consume messages from:
|
|
13
|
+
|
|
14
|
+
RosettaQueue::Destinations.define do |queue|
|
|
15
|
+
queue.map :test_queue, '/queue/my_test_queue'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Defining your adapter:
|
|
19
|
+
|
|
20
|
+
RosettaQueue::Adapter.define do |a|
|
|
21
|
+
a.user = ""
|
|
22
|
+
a.password = ""
|
|
23
|
+
a.host = "localhost"
|
|
24
|
+
a.port = 61613
|
|
25
|
+
a.type = "stomp"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Define a logger for Rosetta Queue to use. The logger should be a standard ruby logger:
|
|
29
|
+
|
|
30
|
+
RosettaQueue.logger = Logger.new('/my_project/rosetta_queue.log')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
You can optionally set up filters that are applied to all messages that are sent and received. For example, if you want to use hashes as messages and serialize them as JSON the following filters (along with ActiveSupport) would accomplish this:
|
|
34
|
+
|
|
35
|
+
RosettaQueue::Filters.define do |filter_for|
|
|
36
|
+
filter_for.receiving { |message| ActiveSupport::JSON.decode(message) }
|
|
37
|
+
filter_for.sending { |hash| hash.to_json }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
To publish a message:
|
|
42
|
+
|
|
43
|
+
message = {"hello" => "world!"} # Assuming you have a filter setup
|
|
44
|
+
RosettaQueue::Producer.publish(:test_queue, message)
|
|
45
|
+
|
|
46
|
+
When consuming messages from a queue you will generally want to create a consumer to handle the messages:
|
|
47
|
+
|
|
48
|
+
class TestConsumer
|
|
49
|
+
include RosettaQueue::MessageHandler
|
|
50
|
+
|
|
51
|
+
subscribes_to :vendor_status
|
|
52
|
+
options :persistent => true
|
|
53
|
+
|
|
54
|
+
def on_message(message)
|
|
55
|
+
puts "We consumed a message: #{message.inspect}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
To fire the consumers up you will want to run a separate process create a manager with all of your consumers.
|
|
61
|
+
|
|
62
|
+
require 'rosetta_queue'
|
|
63
|
+
require 'rosetta_queue/consumer_managers/threaded'
|
|
64
|
+
require 'test_consumer'
|
|
65
|
+
|
|
66
|
+
RosettaQueue::ThreadedManager.create do |m|
|
|
67
|
+
m.add(TestConsumer.new)
|
|
68
|
+
m.start
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
It is recommended that you set your adapter to the 'null' adapter for your specs and then include RosettaQueue::Matchers in any example group you are needing to specify behaviour with RosettaQueue. The matchers currently switch out the null adapter for the fake adapter to verify the behaviour. All the matchers for the unit tests are lambda based, like so:
|
|
73
|
+
|
|
74
|
+
lambda { model.save }.should publish("foo", :to => :test_queue).
|
|
75
|
+
|
|
76
|
+
Please look at the publishing matchers for more information. For examples on how to write acceptance tests for your Rosetta Queue's code please see RosettaQueue's own Cucumber features and read this {blog post}[http://www.benmabey.com/2009/02/17/using-cucumber-to-integrate-distributed-systems-and-test-messaging/].
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
== How to contribute
|
|
81
|
+
----------------------------------------------------------------
|
|
82
|
+
Gems you will need:
|
|
83
|
+
cucumber, rspec, yaml, stomp, tmm1-amqp, beanstalk-client
|
|
84
|
+
|
|
85
|
+
You should be able to run the rspec code examples (specs) without any brokers running with autospec or 'rake spec'.
|
|
86
|
+
|
|
87
|
+
To run the cucumber features you will need the a messaging system setup that can speak stomp and AMQP. We have been using the following brokers in testing:
|
|
88
|
+
|
|
89
|
+
=== Apache ActiveMQ (for the stomp adapter)
|
|
90
|
+
Go to http://activemq.apache.org/download.html to download the latest version, and see http://activemq.apache.org/getting-started.html for installation and configuration instructions.
|
|
91
|
+
The stomp client and features should work with ApacheMQ out of the box. If you are running any ApacheMQ servers in production on your network you will want to disable the multicast autodiscovery in conf/activemq.xml. (Around lines 56 and 98.)
|
|
92
|
+
|
|
93
|
+
=== RabbitMQ (for the amqp adapter)
|
|
94
|
+
Download the right tar from here: http://www.rabbitmq.com/download.html and follow the installation directions that comes with it.
|
|
95
|
+
The features rely on a user of 'rosetta' being setup with the password of 'password' and added to the default virtualhost. You can set them up like so:
|
|
96
|
+
|
|
97
|
+
rabbitmqctl add_user rosetta password
|
|
98
|
+
rabbitmqctl map_user_vhost rosetta /
|
|
99
|
+
|
|
100
|
+
=== Beanstalk (for the beanstalk adapter) ===
|
|
101
|
+
|
|
102
|
+
You should set up and run a local instance of beanstalk for your
|
|
103
|
+
tests.
|
|
104
|
+
|
|
105
|
+
==== beanstalkd ====
|
|
106
|
+
|
|
107
|
+
Mac OSX users can install beanstalkd from ports. Linux users should
|
|
108
|
+
check their package manager of choice. This is probably the optimal
|
|
109
|
+
method.
|
|
110
|
+
|
|
111
|
+
Diehards can clone git://github.com/kr/beanstalkd and build it from
|
|
112
|
+
source. It is nontrivial.
|
|
113
|
+
|
|
114
|
+
Once installed, you can launch beanstalk by running
|
|
115
|
+
|
|
116
|
+
beanstalkd -p 11300
|
|
117
|
+
|
|
118
|
+
This will make it listen on localhost on the standard port 11300,
|
|
119
|
+
which the specs use. You might also want to daemonize beanstalk with
|
|
120
|
+
the -d option but for testing this is not recommended, as the queue
|
|
121
|
+
can get stuck with old messages, which will break your specs.
|
|
122
|
+
|
|
123
|
+
==== beanstalk-client ====
|
|
124
|
+
|
|
125
|
+
You will also need the Ruby beanstalk-client gem.
|
|
126
|
+
|
|
127
|
+
sudo gem install beanstalk-client
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'spec/rake/spectask'
|
|
3
|
+
require 'cucumber/rake/task'
|
|
4
|
+
|
|
5
|
+
Cucumber::Rake::Task.new do |t|
|
|
6
|
+
t.cucumber_opts = "--format pretty"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
desc "Run the specs under spec"
|
|
10
|
+
Spec::Rake::SpecTask.new do |t|
|
|
11
|
+
t.spec_opts = ['--options', "spec/spec.opts"]
|
|
12
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
require 'jeweler'
|
|
17
|
+
Jeweler::Tasks.new do |s|
|
|
18
|
+
s.name = "rosetta_queue"
|
|
19
|
+
s.rubyforge_project = "rosetta-queue"
|
|
20
|
+
s.summary = %Q{Messaging gateway API with adapters for many messaging systems available in Ruby.}
|
|
21
|
+
s.email = "cbwyckoff@gmail.com"
|
|
22
|
+
s.homepage = "http://github.com/cwyckoff/rosetta_queue"
|
|
23
|
+
s.description = %Q{Messaging gateway API with adapters for many messaging systems available in Ruby. Messaging systems can be easily switched out with a small configuration change. Code for testing on the object and application level is also provided.}
|
|
24
|
+
s.extra_rdoc_files = ["README.rdoc", "MIT-LICENSE.txt"]
|
|
25
|
+
s.files = FileList["[A-Z]*.*", "{bin,generators,lib,features,spec}/**/*", "Rakefile", "cucumber.yml"]
|
|
26
|
+
s.authors = ["Ben Mabey", "Chris Wyckoff"]
|
|
27
|
+
end
|
|
28
|
+
rescue LoadError
|
|
29
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
|
30
|
+
end
|
data/VERSION.yml
ADDED
data/cucumber.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default: features --format pretty
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'activesupport' # TODO: remove dependency
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__))) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
|
|
4
|
+
|
|
5
|
+
require 'rosetta_queue/adapter'
|
|
6
|
+
require 'rosetta_queue/base'
|
|
7
|
+
require 'rosetta_queue/consumer'
|
|
8
|
+
require 'rosetta_queue/destinations'
|
|
9
|
+
require 'rosetta_queue/exceptions'
|
|
10
|
+
require 'rosetta_queue/filters'
|
|
11
|
+
require 'rosetta_queue/logger'
|
|
12
|
+
require 'rosetta_queue/message_handler'
|
|
13
|
+
require 'rosetta_queue/producer'
|
|
14
|
+
|
|
15
|
+
if defined?(Rails)
|
|
16
|
+
RosettaQueue.logger = RosettaQueue::Logger.new(File.join(Rails.root, 'log', 'rosetta_queue.log'))
|
|
17
|
+
require('rosetta_queue/spec_helpers') if Rails.env == "test"
|
|
18
|
+
end
|
|
19
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'rosetta_queue/adapters/base'
|
|
2
|
+
|
|
3
|
+
module RosettaQueue
|
|
4
|
+
class Adapter
|
|
5
|
+
|
|
6
|
+
class << self
|
|
7
|
+
attr_writer :user, :password, :host, :port, :options
|
|
8
|
+
|
|
9
|
+
def define
|
|
10
|
+
yield self
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reset
|
|
14
|
+
@user, @password, @host, @port, @options, @adapter_class = nil, nil, nil, nil, nil, nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def type=(adapter_prefix)
|
|
18
|
+
require "rosetta_queue/adapters/#{adapter_prefix}"
|
|
19
|
+
@adapter_class = RosettaQueue::Gateway.const_get("#{adapter_prefix.to_s.classify}Adapter")
|
|
20
|
+
|
|
21
|
+
rescue MissingSourceFile
|
|
22
|
+
raise AdapterException, "Adapter type '#{adapter_prefix}' does not match existing adapters!"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def instance
|
|
26
|
+
raise AdapterException, "Adapter type was never defined!" unless @adapter_class
|
|
27
|
+
@adapter_class.new({:user => @user, :password => @password, :host => @host, :port => @port, :opts => opts})
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def opts
|
|
33
|
+
raise AdapterException, "Adapter options should be a hash" unless @options.nil? || @options.is_a?(Hash)
|
|
34
|
+
@options ||= {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require 'bunny'
|
|
2
|
+
|
|
3
|
+
module RosettaQueue
|
|
4
|
+
module Gateway
|
|
5
|
+
|
|
6
|
+
# This AMQP adapter utilizes a forked version of the synchronous AMPQ client 'Bunny'
|
|
7
|
+
# by bunny (http://github.com/celldee/bunny)
|
|
8
|
+
class AmqpBunnyAdapter < BaseAdapter
|
|
9
|
+
|
|
10
|
+
def initialize(adapter_settings = {})
|
|
11
|
+
raise AdapterException, "Missing adapter settings" if adapter_settings.empty?
|
|
12
|
+
@adapter_settings = adapter_settings
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def delete(destination, opts={})
|
|
16
|
+
exchange_strategy_for(destination, opts).delete(destination)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def disconnect(message_handler); end
|
|
20
|
+
|
|
21
|
+
def receive_once(destination, opts={})
|
|
22
|
+
exchange_strategy_for(destination, opts).receive_once(destination) do |msg|
|
|
23
|
+
return msg
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def receive_with(message_handler)
|
|
28
|
+
options = options_for(message_handler)
|
|
29
|
+
destination = destination_for(message_handler)
|
|
30
|
+
exchange_strategy_for(destination, options).receive(destination, message_handler)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def send_message(destination, message, options=nil)
|
|
34
|
+
exchange_strategy_for(destination, options).publish(destination, message)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def unsubscribe; end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def exchange_strategy_for(destination, options)
|
|
42
|
+
case destination
|
|
43
|
+
when /^fanout\./
|
|
44
|
+
@exchange ||= AmqpExchangeStrategies::FanoutExchange.new(@adapter_settings, options)
|
|
45
|
+
when /^topic\./
|
|
46
|
+
raise "Sorry. RosettaQueue can not process AMQP topics yet"
|
|
47
|
+
when /^queue\./
|
|
48
|
+
@exchange ||= AmqpExchangeStrategies::DirectExchange.new(@adapter_settings, options)
|
|
49
|
+
else
|
|
50
|
+
@exchange ||= AmqpExchangeStrategies::DirectExchange.new(@adapter_settings, options)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module AmqpExchangeStrategies
|
|
57
|
+
|
|
58
|
+
class BaseExchange
|
|
59
|
+
|
|
60
|
+
def initialize(adapter_settings, options={})
|
|
61
|
+
@adapter_settings, @options = adapter_settings, options
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def delete(destination)
|
|
65
|
+
conn.queue(destination).delete(@options)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
protected
|
|
69
|
+
|
|
70
|
+
def conn
|
|
71
|
+
vhost = @adapter_settings[:opts][:vhost] || "/"
|
|
72
|
+
@conn ||= Bunny.new( :user => @adapter_settings[:user],
|
|
73
|
+
:pass => @adapter_settings[:password],
|
|
74
|
+
:host => @adapter_settings[:host],
|
|
75
|
+
:vhost => vhost)
|
|
76
|
+
@conn.start unless @conn.status == :connected
|
|
77
|
+
@conn
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class DirectExchange < BaseExchange
|
|
82
|
+
|
|
83
|
+
def publish(destination, message, options={})
|
|
84
|
+
RosettaQueue.logger.info("Publishing to #{destination} :: #{message}")
|
|
85
|
+
conn.queue(destination, options).publish(message, options)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def receive(destination, message_handler)
|
|
89
|
+
conn.queue(destination, @options).subscribe(@options) do |msg|
|
|
90
|
+
RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
|
|
91
|
+
message_handler.on_message(Filters.process_receiving(msg))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def receive_once(destination, options={})
|
|
96
|
+
msg = conn.queue(destination, options).pop(options)
|
|
97
|
+
RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
|
|
98
|
+
yield Filters.process_receiving(msg)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class FanoutExchange < BaseExchange
|
|
103
|
+
|
|
104
|
+
def fanout_name_for(destination)
|
|
105
|
+
fanout_name = destination.gsub(/fanout\/(.*)/, '\1')
|
|
106
|
+
raise "Unable to discover fanout exchange. Cannot bind queue to exchange!" unless fanout_name
|
|
107
|
+
fanout_name
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def receive_once(destination, options={})
|
|
111
|
+
queue = conn.queue("queue_#{self.object_id}", options)
|
|
112
|
+
exchange = conn.fanout(fanout_name_for(destination), options)
|
|
113
|
+
|
|
114
|
+
msg = queue.bind(exchange).pop(@options)
|
|
115
|
+
RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
|
|
116
|
+
yield Filters.process_receiving(msg)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def publish(destination, message, options={})
|
|
120
|
+
exchange = conn.fanout(fanout_name_for(destination), options)
|
|
121
|
+
exchange.publish(message, options)
|
|
122
|
+
RosettaQueue.logger.info("Publishing to fanout #{destination} :: #{message}")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def receive(destination, message_handler)
|
|
126
|
+
queue = conn.queue("queue_#{self.object_id}", @options)
|
|
127
|
+
exchange = conn.fanout(fanout_name_for(destination), @options)
|
|
128
|
+
|
|
129
|
+
msg = queue.bind(exchange).subscribe(@options) do |msg|
|
|
130
|
+
|
|
131
|
+
RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
|
|
132
|
+
message_handler.on_message(Filters.process_receiving(msg))
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
module Gateway
|
|
3
|
+
|
|
4
|
+
class BaseAdapter
|
|
5
|
+
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def options_for(message_handler)
|
|
9
|
+
(message_handler.options_hash) || {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def destination_for(message_handler)
|
|
13
|
+
raise DestinationNotFound.new("Missing destination!") unless message_handler.destination
|
|
14
|
+
@dest ||= Destinations.lookup(message_handler.destination.to_sym)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def filter_receiving(msg)
|
|
18
|
+
Filters.process_receiving(msg)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def filter_sending(msg)
|
|
22
|
+
Filters.process_sending(msg)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
module Gateway
|
|
3
|
+
|
|
4
|
+
class FakeAdapter
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@messages = []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def send_message(queue, message, headers)
|
|
11
|
+
@messages << {'queue' => queue, 'message' => RosettaQueue::Filters::process_receiving(message), 'headers' => headers}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def messages_sent_to(queue)
|
|
15
|
+
(queue ? @messages.select{|message| message['queue'] == queue} : @messages).map{|m| m['message']}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def queues
|
|
19
|
+
@messages.map {|message| message['queue']}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
module Gateway
|
|
3
|
+
|
|
4
|
+
# The null adapter lets all send messages enter into the ether and so is ideal for modes
|
|
5
|
+
# when you do not want to incur the overhead of a real adapter. You can not consume with
|
|
6
|
+
# this adapter however.
|
|
7
|
+
#
|
|
8
|
+
# In your RosettaQueue definition block, and your using rails, you could base your adapter type on Rails.env:
|
|
9
|
+
#
|
|
10
|
+
# RosettaQueue::Adapter.define do |a|
|
|
11
|
+
# if Rails.env == 'production' || ENV["RUNNING_STORIES"] == "true"
|
|
12
|
+
# a.user = ""
|
|
13
|
+
# a.password = ""
|
|
14
|
+
# a.host = "localhost"
|
|
15
|
+
# a.port = 61613
|
|
16
|
+
# a.type = "stomp"
|
|
17
|
+
# else
|
|
18
|
+
# a.type = "null"
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# (if you follow this example and are using stories be sure
|
|
23
|
+
# to set ENV["RUNNING_STORIES"] = "true" in your helper.rb or env.rb file)
|
|
24
|
+
class NullAdapter
|
|
25
|
+
|
|
26
|
+
def initialize(adapter_settings)
|
|
27
|
+
# no-op
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def disconnect
|
|
31
|
+
# no-op
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def receive
|
|
35
|
+
raise "Null Adpater is in use, you can not consume messages!"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def receive_with(message_handler)
|
|
39
|
+
raise "Null Adpater is in use, you can not consume messages!"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def send_message(queue, message, options)
|
|
43
|
+
# no-op
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def subscribe(queue, options)
|
|
47
|
+
# no-op
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def unsubscribe(queue)
|
|
51
|
+
# no-op
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'stomp'
|
|
2
|
+
|
|
3
|
+
module RosettaQueue
|
|
4
|
+
module Gateway
|
|
5
|
+
|
|
6
|
+
class StompAdapter < BaseAdapter
|
|
7
|
+
|
|
8
|
+
def ack(msg)
|
|
9
|
+
@conn.ack(msg.headers["message-id"])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(adapter_settings = {})
|
|
13
|
+
raise "Missing adapter settings" if adapter_settings.empty?
|
|
14
|
+
@conn = Stomp::Connection.open(adapter_settings[:user],
|
|
15
|
+
adapter_settings[:password],
|
|
16
|
+
adapter_settings[:host],
|
|
17
|
+
adapter_settings[:port],
|
|
18
|
+
true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def disconnect(message_handler)
|
|
22
|
+
unsubscribe(destination_for(message_handler))
|
|
23
|
+
@conn.disconnect
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def receive(options)
|
|
27
|
+
msg = @conn.receive
|
|
28
|
+
ack(msg) unless options[:ack].nil?
|
|
29
|
+
msg
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def receive_once(destination, opts)
|
|
33
|
+
subscribe(destination, opts)
|
|
34
|
+
msg = receive(opts).body
|
|
35
|
+
unsubscribe(destination)
|
|
36
|
+
RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
|
|
37
|
+
filter_receiving(msg)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def receive_with(message_handler)
|
|
41
|
+
options = options_for(message_handler)
|
|
42
|
+
destination = destination_for(message_handler)
|
|
43
|
+
@conn.subscribe(destination, options)
|
|
44
|
+
|
|
45
|
+
running do
|
|
46
|
+
msg = receive(options).body
|
|
47
|
+
RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
|
|
48
|
+
message_handler.on_message(filter_receiving(msg))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def send_message(destination, message, options)
|
|
53
|
+
RosettaQueue.logger.info("Publishing to #{destination} :: #{message}")
|
|
54
|
+
@conn.send(destination, message, options)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def subscribe(destination, options)
|
|
58
|
+
@conn.subscribe(destination, options)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def unsubscribe(destination)
|
|
62
|
+
@conn.unsubscribe(destination)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def running(&block)
|
|
68
|
+
loop(&block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
class Consumer < Base
|
|
3
|
+
|
|
4
|
+
def self.receive(destination, options = {})
|
|
5
|
+
RosettaQueue::Adapter.instance.receive_once(Destinations.lookup(destination), options)
|
|
6
|
+
|
|
7
|
+
rescue Exception=>e
|
|
8
|
+
RosettaQueue.logger.error("Caught exception in Consumer.receive: #{$!}\n" + e.backtrace.join("\n\t"))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.delete(destination, options={})
|
|
12
|
+
RosettaQueue::Adapter.instance.delete(Destinations.lookup(destination), options)
|
|
13
|
+
|
|
14
|
+
rescue Exception=>e
|
|
15
|
+
RosettaQueue.logger.error("Caught exception in Consumer.delete: #{$!}\n" + e.backtrace.join("\n\t"))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(message_handler)
|
|
19
|
+
@message_handler = message_handler
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def receive
|
|
23
|
+
connection.receive_with(@message_handler)
|
|
24
|
+
|
|
25
|
+
rescue Exception=>e
|
|
26
|
+
RosettaQueue.logger.error("Caught exception in Consumer#receive: #{$!}\n" + e.backtrace.join("\n\t"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
|
|
3
|
+
class BaseManager
|
|
4
|
+
attr_reader :consumers
|
|
5
|
+
|
|
6
|
+
class << self
|
|
7
|
+
def create
|
|
8
|
+
yield self.new
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@consumers = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add(message_handler)
|
|
17
|
+
key = message_handler.class.to_s.underscore.to_sym
|
|
18
|
+
@consumers[key] = Consumer.new(message_handler)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'rosetta_queue/consumer_managers/base'
|
|
2
|
+
require 'mq'
|
|
3
|
+
|
|
4
|
+
module RosettaQueue
|
|
5
|
+
|
|
6
|
+
class EventedManager < BaseManager
|
|
7
|
+
|
|
8
|
+
def start
|
|
9
|
+
EM.run {
|
|
10
|
+
trap_interruptions
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
@consumers.each do |key, consumer|
|
|
14
|
+
RosettaQueue.logger.info("Running consumer #{key} in event machine...")
|
|
15
|
+
consumer.receive
|
|
16
|
+
end
|
|
17
|
+
rescue Exception => e
|
|
18
|
+
RosettaQueue.logger.error("Exception thrown: #{$!}\n" + e.backtrace.join("\n\t"))
|
|
19
|
+
end
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def stop
|
|
24
|
+
RosettaQueue.logger.info("Shutting down event machine...")
|
|
25
|
+
EM.stop
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def trap_interruptions
|
|
31
|
+
trap("INT") {
|
|
32
|
+
RosettaQueue.logger.warn("Interrupt received. Shutting down...")
|
|
33
|
+
EM.stop
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
trap("TERM") {
|
|
37
|
+
RosettaQueue.logger.warn("Interrupt received. Shutting down...")
|
|
38
|
+
EM.stop
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'rosetta_queue/consumer_managers/base'
|
|
2
|
+
|
|
3
|
+
module RosettaQueue
|
|
4
|
+
class ThreadedManager < BaseManager
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@threads = {}
|
|
8
|
+
@running = true
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def start
|
|
13
|
+
start_threads
|
|
14
|
+
join_threads
|
|
15
|
+
monitor_threads
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stop
|
|
19
|
+
stop_threads
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def join_threads
|
|
25
|
+
@threads.each { |thread| thread.join }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def monitor_threads
|
|
29
|
+
while @running
|
|
30
|
+
trap("TERM", "EXIT")
|
|
31
|
+
living = false
|
|
32
|
+
@threads.each { |name, thread| living ||= thread.alive? }
|
|
33
|
+
@running = living
|
|
34
|
+
sleep 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
puts "All connection threads have died..."
|
|
38
|
+
rescue Interrupt=>e
|
|
39
|
+
RosettaQueue.logger.warn("Interrupt received. Shutting down...")
|
|
40
|
+
puts "\nInterrupt received\n"
|
|
41
|
+
rescue Exception=>e
|
|
42
|
+
RosettaQueue.logger.error("Exception thrown -- #{e.class.name}: #{e.message}")
|
|
43
|
+
ensure
|
|
44
|
+
RosettaQueue.logger.warn("Cleaning up threads...")
|
|
45
|
+
stop_threads
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def start_threads
|
|
49
|
+
@consumers.each do |key, consumer|
|
|
50
|
+
@threads[key] = Thread.new(key, consumer) do |a_key, a_consumer|
|
|
51
|
+
while @running
|
|
52
|
+
begin
|
|
53
|
+
RosettaQueue.logger.info("Threading consumer #{a_consumer}...")
|
|
54
|
+
Mutex.new.synchronize { a_consumer.receive }
|
|
55
|
+
rescue StopProcessingException=>e
|
|
56
|
+
RosettaQueue.logger.error("#{a_key}: Processing Stopped - receive interrupted")
|
|
57
|
+
rescue Exception=>e
|
|
58
|
+
RosettaQueue.logger.error("#{a_key}: Exception from connection.receive: #{$!}\n" + e.backtrace.join("\n\t"))
|
|
59
|
+
end
|
|
60
|
+
Thread.pass
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def stop_threads
|
|
67
|
+
@running = false
|
|
68
|
+
@threads.each do |key, thread|
|
|
69
|
+
RosettaQueue.logger.info("Stopping thread and disconnecting from #{key}...")
|
|
70
|
+
@consumers[key].disconnect
|
|
71
|
+
thread.kill
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
|
|
3
|
+
class Destinations
|
|
4
|
+
|
|
5
|
+
@dest = {}
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
attr_reader :dest
|
|
9
|
+
|
|
10
|
+
def define
|
|
11
|
+
yield self
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def clear
|
|
15
|
+
@dest.clear
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def lookup(dest_name)
|
|
19
|
+
mapping = dest[dest_name.to_sym]
|
|
20
|
+
raise "No destination mapping for '#{dest_name}' has been defined!" unless mapping
|
|
21
|
+
return mapping
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def map(key, dest)
|
|
25
|
+
@dest[key] = dest
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def queue_names
|
|
29
|
+
@dest.values
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
|
|
3
|
+
class RosettaQueueError < StandardError; end
|
|
4
|
+
class DestinationNotFound < RosettaQueueError; end
|
|
5
|
+
class RosettaQueueVariableNotFound < RosettaQueueError; end
|
|
6
|
+
class CallbackNotImplemented < RosettaQueueError; end
|
|
7
|
+
class AdapterException < RosettaQueueError; end
|
|
8
|
+
class StopProcessingException < Interrupt; end
|
|
9
|
+
|
|
10
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Example:
|
|
2
|
+
# RosettaQueue::Filters.define do |filter_for|
|
|
3
|
+
# filter_for.receiving { |message| ActiveSupport::JSON.decode(message) }
|
|
4
|
+
# filter_for.sending { |hash| hash.to_json }
|
|
5
|
+
# end
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
module RosettaQueue
|
|
9
|
+
class Filters
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
|
|
13
|
+
def define
|
|
14
|
+
yield self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reset
|
|
18
|
+
@receiving = nil
|
|
19
|
+
@sending = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def receiving(&receiving_filter)
|
|
23
|
+
@receiving = receiving_filter
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sending(&sending_filter)
|
|
27
|
+
@sending = sending_filter
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def process_sending(message)
|
|
31
|
+
return message unless @sending
|
|
32
|
+
@sending.call(message)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def process_receiving(message)
|
|
36
|
+
return message unless @receiving
|
|
37
|
+
@receiving.call(message)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module RosettaQueue
|
|
4
|
+
class MissingLogger < ::StandardError; end
|
|
5
|
+
|
|
6
|
+
def self.logger=(new_logger)
|
|
7
|
+
@logger = new_logger
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.logger
|
|
11
|
+
return @logger if @logger
|
|
12
|
+
raise MissingLogger, "No logger has been set for RosettaQueue. Please define one with RosettaQueue.logger=."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module RosettaQueue
|
|
18
|
+
|
|
19
|
+
class Logger < ::Logger
|
|
20
|
+
|
|
21
|
+
def format_message(severity, timestamp, progname, msg)
|
|
22
|
+
"[#{timestamp.to_formatted_s(:db)}] #{severity} #{msg}\n"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
module MessageHandler
|
|
3
|
+
|
|
4
|
+
module ClassMethods
|
|
5
|
+
|
|
6
|
+
attr_reader :destination, :options_hash
|
|
7
|
+
|
|
8
|
+
def options(options = {})
|
|
9
|
+
@options_hash = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def publishes_to(destination)
|
|
13
|
+
@destination = destination
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def subscribes_to(destination)
|
|
17
|
+
@destination = destination
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.included(receiver)
|
|
22
|
+
receiver.extend(ClassMethods)
|
|
23
|
+
|
|
24
|
+
def destination
|
|
25
|
+
self.class.destination
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def options_hash
|
|
29
|
+
self.class.options_hash
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
|
|
3
|
+
class Producer < Base
|
|
4
|
+
include MessageHandler
|
|
5
|
+
|
|
6
|
+
def self.publish(destination, message, options = {})
|
|
7
|
+
RosettaQueue::Adapter.instance.send_message(Destinations.lookup(destination), Filters.process_sending(message), options)
|
|
8
|
+
|
|
9
|
+
rescue Exception=>e
|
|
10
|
+
RosettaQueue.logger.error("Caught exception in Consumer.publish: #{$!}\n" + e.backtrace.join("\n\t"))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class Hash
|
|
2
|
+
# To be used in conjuction with rspec's predicate matcher.
|
|
3
|
+
#
|
|
4
|
+
# For example, in story/feature or a functional spec you could say:
|
|
5
|
+
#
|
|
6
|
+
# expected_message = {'name' => 'Advertiser'}
|
|
7
|
+
# expected_message.should be_published_to(:advertiser_create)
|
|
8
|
+
#
|
|
9
|
+
def published_to?(destination)
|
|
10
|
+
received_message = nil
|
|
11
|
+
begin
|
|
12
|
+
Timeout::timeout(2) { received_message = RosettaQueue::Consumer.receive(destination)}
|
|
13
|
+
rescue Timeout::Error
|
|
14
|
+
raise "#{destination} should have received a message but did not NOTE: make sure there are no other processes which are polling messages"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# calling should == is kinda wierd, I know.. but in order to get a decent error message it is needed
|
|
18
|
+
received_message.should == self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
# Adds helpful methods when doing application level testing.
|
|
3
|
+
# If you are using cucumber just include it in your World in the env.rb file:
|
|
4
|
+
# World {|world| world.extend RosettaQueue::SpecHelpers }
|
|
5
|
+
module SpecHelpers
|
|
6
|
+
require 'open-uri'
|
|
7
|
+
|
|
8
|
+
# *Currently* only works with ActiveMQ being used as gateway.
|
|
9
|
+
# This will clear the queues defined in the RosettaQueue::Destinations mapping.
|
|
10
|
+
# TODO: Figure out a better spot for this to allow for other gateways...
|
|
11
|
+
def clear_queues
|
|
12
|
+
RosettaQueue::Destinations.queue_names.each do |name|
|
|
13
|
+
queue = name.gsub('/queue/','')
|
|
14
|
+
open("http://127.0.0.1:8161/admin/deleteDestination.action?JMSDestination=#{queue}&JMSDestinationType=queue")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Publishes a given hash as json to the specified destination.
|
|
19
|
+
# Example:
|
|
20
|
+
# publish_message(expected_message, :to => :client_status, :options => {...})
|
|
21
|
+
# The :options will be passed to the publisher and are optional.
|
|
22
|
+
def publish_message(message, options)
|
|
23
|
+
options[:options] ||= {:persistent => false}
|
|
24
|
+
RosettaQueue::Producer.publish(options[:to], message, options[:options])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Consumes the first message on queue of consumer that is passed in and uses the consumer to handle it.
|
|
28
|
+
# Example:
|
|
29
|
+
# consume_once_with ClientStatusConsumer
|
|
30
|
+
def consume_once_with(consumer)
|
|
31
|
+
consumer.new.on_message(RosettaQueue::Consumer.receive(consumer.destination))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Consumes the first message on queue and returns it.
|
|
35
|
+
# Example:
|
|
36
|
+
# message = consume_once :foo_queue
|
|
37
|
+
def consume_once(dest)
|
|
38
|
+
RosettaQueue::Consumer.receive(dest)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def consuming_from(destination)
|
|
42
|
+
sleep 1
|
|
43
|
+
Messaging::Consumer.receive(destination, :persistent => false).to_hash_from_json
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
module RosettaQueue
|
|
2
|
+
module Matchers
|
|
3
|
+
|
|
4
|
+
class PublishAMessageTo
|
|
5
|
+
|
|
6
|
+
def initialize(expected_queue_name, options=nil)
|
|
7
|
+
@options = options || {}
|
|
8
|
+
@how_many_messages_expected = (@options[:exactly] || 1).to_i
|
|
9
|
+
@expected_queue_name = expected_queue_name
|
|
10
|
+
@expected_queue = expected_queue_name.is_a?(Symbol) ? RosettaQueue::Destinations.lookup(expected_queue_name) : expected_queue_name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def matches?(lambda_to_run)
|
|
14
|
+
#given
|
|
15
|
+
RosettaQueue::Adapter.stub!(:instance).and_return(fake_adapter = RosettaQueue::Gateway::FakeAdapter.new)
|
|
16
|
+
#when
|
|
17
|
+
lambda_to_run.call
|
|
18
|
+
#then
|
|
19
|
+
@actual_queues = fake_adapter.queues
|
|
20
|
+
@number_of_messages_published = @actual_queues.select{ |q| q == @expected_queue}.size
|
|
21
|
+
@number_of_messages_published == @how_many_messages_expected
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def failure_message
|
|
25
|
+
"expected #{message_plural} published to the #{@expected_queue.inspect} queue but #{@number_of_messages_published} messages were"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def negative_failure_message
|
|
29
|
+
"expected ##{message_plural} NOT to be published to the #{@expected_queue.inspect} queue but that queue was published to #{@number_of_messages_published} times"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def description
|
|
33
|
+
"publish #{message_plural} to the '#{@expected_queue_name}' queue"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
def message_plural
|
|
38
|
+
@how_many_messages_expected == 1 ? "a message" : "#{@how_many_messages_expected} messages"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def publish_a_message_to(expected_queue)
|
|
43
|
+
PublishAMessageTo.new(expected_queue)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
alias :publish_message_to :publish_a_message_to
|
|
47
|
+
|
|
48
|
+
def publish_messages_to(expected_queue, options)
|
|
49
|
+
PublishAMessageTo.new(expected_queue, options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class PublishMessageMatcher
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def matches?(lambda_to_run)
|
|
56
|
+
#given
|
|
57
|
+
RosettaQueue::Adapter.stub!(:instance).and_return(fake_adapter = RosettaQueue::Gateway::FakeAdapter.new)
|
|
58
|
+
#when
|
|
59
|
+
lambda_to_run.call
|
|
60
|
+
#then
|
|
61
|
+
message = fake_adapter.messages_sent_to(@expected_queue).first || ''
|
|
62
|
+
@actual_message = message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
protected
|
|
66
|
+
def extract_options(options)
|
|
67
|
+
if (expected_queue_name = options[:to])
|
|
68
|
+
@expected_queue = expected_queue_name.is_a?(Symbol) ? RosettaQueue::Destinations.lookup(expected_queue_name) : expected_queue_name
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class PublishMessageWith < PublishMessageMatcher
|
|
74
|
+
|
|
75
|
+
def initialize(message_subset, options)
|
|
76
|
+
@message_subset = message_subset
|
|
77
|
+
extract_options(options)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def matches?(lambda_to_run)
|
|
81
|
+
super
|
|
82
|
+
Spec::Mocks::ArgumentConstraints::HashIncludingConstraint.new(@message_subset) == @actual_message
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def failure_message
|
|
86
|
+
if @actual_message.blank?
|
|
87
|
+
"expected #{@message_subset.inspect} to be contained in a message but no message was published"
|
|
88
|
+
else
|
|
89
|
+
"expected #{@message_subset.inspect} to be contained in the message: #{@actual_message.inspect}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def negative_failure_message
|
|
94
|
+
"expected #{@message_subset.inspect} not to be contained in the message but was"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def description
|
|
98
|
+
"publish a message with #{@message_subset.inspect}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def publish_message_with(message_subset, options={})
|
|
104
|
+
PublishMessageWith.new(message_subset, options)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PublishMessage < PublishMessageMatcher
|
|
109
|
+
|
|
110
|
+
def initialize(expected_message, options)
|
|
111
|
+
@expected_message = expected_message
|
|
112
|
+
extract_options(options)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def matches?(lambda_to_run)
|
|
116
|
+
super
|
|
117
|
+
@actual_message == @expected_message
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def failure_message
|
|
121
|
+
if @actual_message.blank?
|
|
122
|
+
"expected #{@expected_message.inspect} to be published but no message was"
|
|
123
|
+
else
|
|
124
|
+
"expected #{@expected_message.inspect} to be published but the following was instead: #{@actual_message.inspect}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def negative_failure_message
|
|
129
|
+
"expected #{@expected_message.inspect} not to be published but it was"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def description
|
|
133
|
+
"publish the message: #{@expected_message.inspect}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def publish_message(exact_expected_message, options={})
|
|
139
|
+
PublishMessage.new(exact_expected_message, options)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
metadata
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cwyckoff-rosetta_queue
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ben Mabey
|
|
8
|
+
- Chris Wyckoff
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
|
|
13
|
+
date: 2009-01-28 00:00:00 -08:00
|
|
14
|
+
default_executable:
|
|
15
|
+
dependencies: []
|
|
16
|
+
|
|
17
|
+
description: Messaging gateway API with adapters for many messaging systems available in Ruby. Messaging systems can be easily switched out with a small configuration change. Code for testing on the object and application level is also provided.
|
|
18
|
+
email: ben@benmabey.com
|
|
19
|
+
executables: []
|
|
20
|
+
|
|
21
|
+
extensions: []
|
|
22
|
+
|
|
23
|
+
extra_rdoc_files:
|
|
24
|
+
- README.rdoc
|
|
25
|
+
- MIT-LICENSE.txt
|
|
26
|
+
files:
|
|
27
|
+
- History.txt
|
|
28
|
+
- MIT-LICENSE.txt
|
|
29
|
+
- README.rdoc
|
|
30
|
+
- VERSION.yml
|
|
31
|
+
- lib/rosetta_queue
|
|
32
|
+
- lib/rosetta_queue/adapter.rb
|
|
33
|
+
- lib/rosetta_queue/adapters
|
|
34
|
+
- lib/rosetta_queue/adapters/amqp_bunny.rb
|
|
35
|
+
- lib/rosetta_queue/adapters/base.rb
|
|
36
|
+
- lib/rosetta_queue/adapters/fake.rb
|
|
37
|
+
- lib/rosetta_queue/adapters/null.rb
|
|
38
|
+
- lib/rosetta_queue/adapters/stomp.rb
|
|
39
|
+
- lib/rosetta_queue/base.rb
|
|
40
|
+
- lib/rosetta_queue/consumer.rb
|
|
41
|
+
- lib/rosetta_queue/consumer_managers
|
|
42
|
+
- lib/rosetta_queue/consumer_managers/base.rb
|
|
43
|
+
- lib/rosetta_queue/consumer_managers/evented.rb
|
|
44
|
+
- lib/rosetta_queue/consumer_managers/threaded.rb
|
|
45
|
+
- lib/rosetta_queue/destinations.rb
|
|
46
|
+
- lib/rosetta_queue/exceptions.rb
|
|
47
|
+
- lib/rosetta_queue/filters.rb
|
|
48
|
+
- lib/rosetta_queue/logger.rb
|
|
49
|
+
- lib/rosetta_queue/message_handler.rb
|
|
50
|
+
- lib/rosetta_queue/producer.rb
|
|
51
|
+
- lib/rosetta_queue/spec_helpers
|
|
52
|
+
- lib/rosetta_queue/spec_helpers/hash.rb
|
|
53
|
+
- lib/rosetta_queue/spec_helpers/helpers.rb
|
|
54
|
+
- lib/rosetta_queue/spec_helpers/publishing_matchers.rb
|
|
55
|
+
- lib/rosetta_queue/spec_helpers.rb
|
|
56
|
+
- lib/rosetta_queue.rb
|
|
57
|
+
- Rakefile
|
|
58
|
+
- cucumber.yml
|
|
59
|
+
has_rdoc: true
|
|
60
|
+
homepage: http://github.com/bmabey/rosetta_queue
|
|
61
|
+
post_install_message:
|
|
62
|
+
rdoc_options:
|
|
63
|
+
- --inline-source
|
|
64
|
+
- --charset=UTF-8
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: "0"
|
|
72
|
+
version:
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: "0"
|
|
78
|
+
version:
|
|
79
|
+
requirements: []
|
|
80
|
+
|
|
81
|
+
rubyforge_project: rosetta-queue
|
|
82
|
+
rubygems_version: 1.2.0
|
|
83
|
+
signing_key:
|
|
84
|
+
specification_version: 2
|
|
85
|
+
summary: Messaging gateway API with adapters for many messaging systems available in Ruby.
|
|
86
|
+
test_files: []
|
|
87
|
+
|