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 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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 2
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,15 @@
1
+ module RosettaQueue
2
+
3
+ class Base
4
+
5
+ def disconnect
6
+ connection.disconnect(@message_handler)
7
+ end
8
+
9
+ protected
10
+ def connection
11
+ @conn ||= Adapter.instance
12
+ end
13
+
14
+ end
15
+ 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,5 @@
1
+ require 'rosetta_queue/adapters/null'
2
+ require 'rosetta_queue/adapters/fake'
3
+ require 'rosetta_queue/spec_helpers/hash'
4
+ require 'rosetta_queue/spec_helpers/publishing_matchers'
5
+ require 'rosetta_queue/spec_helpers/helpers'
@@ -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
+