cwyckoff-rosetta_queue 0.3.0 → 0.3.3

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.
Files changed (51) hide show
  1. data/History.txt +26 -3
  2. data/README.rdoc +4 -149
  3. data/Rakefile +3 -0
  4. data/VERSION.yml +2 -2
  5. data/cucumber.yml +1 -1
  6. data/examples/sample_amqp_consumer.rb +45 -0
  7. data/examples/sample_amqp_fanout_consumer.rb +52 -0
  8. data/examples/sample_amqp_fanout_producer.rb +18 -0
  9. data/examples/sample_amqp_producer.rb +16 -0
  10. data/features/filtering.feature +31 -0
  11. data/features/messaging.feature +47 -0
  12. data/features/step_definitions/common_messaging_steps.rb +82 -0
  13. data/features/step_definitions/filtering_steps.rb +17 -0
  14. data/features/step_definitions/point_to_point_steps.rb +22 -0
  15. data/features/step_definitions/publish_subscribe_steps.rb +25 -0
  16. data/features/support/env.rb +25 -0
  17. data/features/support/sample_consumers.rb +29 -0
  18. data/lib/rosetta_queue.rb +3 -2
  19. data/lib/rosetta_queue/adapter.rb +1 -1
  20. data/lib/rosetta_queue/adapters/amqp.rb +48 -0
  21. data/lib/rosetta_queue/adapters/amqp_evented.rb +132 -0
  22. data/lib/rosetta_queue/adapters/amqp_synch.rb +48 -69
  23. data/lib/rosetta_queue/adapters/beanstalk.rb +56 -0
  24. data/lib/rosetta_queue/adapters/stomp.rb +16 -1
  25. data/lib/rosetta_queue/consumer_managers/base.rb +3 -1
  26. data/lib/rosetta_queue/consumer_managers/threaded.rb +23 -4
  27. data/lib/rosetta_queue/core_ext/string.rb +22 -0
  28. data/lib/rosetta_queue/core_ext/time.rb +20 -0
  29. data/lib/rosetta_queue/filters.rb +1 -1
  30. data/lib/rosetta_queue/logger.rb +1 -1
  31. data/lib/rosetta_queue/message_handler.rb +6 -0
  32. data/spec/rosetta_queue/adapter_spec.rb +101 -0
  33. data/spec/rosetta_queue/adapters/amqp_synchronous_spec.rb +278 -0
  34. data/spec/rosetta_queue/adapters/beanstalk_spec.rb +47 -0
  35. data/spec/rosetta_queue/adapters/fake_spec.rb +72 -0
  36. data/spec/rosetta_queue/adapters/null_spec.rb +31 -0
  37. data/spec/rosetta_queue/adapters/shared_adapter_behavior.rb +38 -0
  38. data/spec/rosetta_queue/adapters/shared_fanout_behavior.rb +20 -0
  39. data/spec/rosetta_queue/adapters/stomp_spec.rb +126 -0
  40. data/spec/rosetta_queue/consumer_managers/evented_spec.rb +56 -0
  41. data/spec/rosetta_queue/consumer_managers/shared_manager_behavior.rb +26 -0
  42. data/spec/rosetta_queue/consumer_managers/threaded_spec.rb +51 -0
  43. data/spec/rosetta_queue/consumer_spec.rb +99 -0
  44. data/spec/rosetta_queue/core_ext/string_spec.rb +15 -0
  45. data/spec/rosetta_queue/destinations_spec.rb +34 -0
  46. data/spec/rosetta_queue/filters_spec.rb +44 -0
  47. data/spec/rosetta_queue/producer_spec.rb +66 -0
  48. data/spec/rosetta_queue/shared_messaging_behavior.rb +21 -0
  49. data/spec/spec.opts +4 -0
  50. data/spec/spec_helper.rb +47 -0
  51. metadata +68 -19
@@ -0,0 +1,17 @@
1
+ Then /^a (receiving|sending) filter is defined to prepend 'Foo' to all messages$/ do |filter_type|
2
+ RosettaQueue::Filters.reset
3
+ RosettaQueue::Filters.define do |f|
4
+ f.send(filter_type) { |message| "Foo #{message}" }
5
+ end
6
+ end
7
+
8
+ When /^the message on '(.+)' is consumed$/ do |queue_name|
9
+ # TODO
10
+ # @consumed_message = queue(:foo_queue).pop
11
+ @consumed_message = consume_once(queue_name.to_sym)
12
+ end
13
+
14
+ Then /^the consumed message should equal "(.+)"$/ do |consumed_message|
15
+ @consumed_message.should == consumed_message
16
+ end
17
+
@@ -0,0 +1,22 @@
1
+ Given /^a consumer is listening to queue '(.*)'$/ do |queue|
2
+ klass = eval_consumer_class(queue)
3
+ @thread = Thread.new do
4
+ cons = klass.new
5
+ case @adapter_type
6
+ when /evented/
7
+ EM.run do
8
+ RosettaQueue::Consumer.new(cons).receive
9
+ end
10
+ else
11
+ RosettaQueue::Consumer.new(cons).receive
12
+ end
13
+ end
14
+ end
15
+
16
+ Then /^the message should be consumed from '(.*)'$/ do |queue|
17
+ sleep 1
18
+ file_path = "#{CONSUMER_LOG_DIR}/point-to-point.log"
19
+ # sleep 1 unless File.exists?(file_path)
20
+ File.readlines(file_path).last.should =~ /Hello World! from #{queue.capitalize}Consumer/
21
+ @thread.kill
22
+ end
@@ -0,0 +1,25 @@
1
+ Given /^multiple consumers are listening to queue '(.*)'$/ do |queue|
2
+ @managers = []
3
+ Thread.new do
4
+ @managers << RosettaQueue::ThreadedManager.create do |m|
5
+ m.add(eval_consumer_class(queue, "fooconsumer.log", "FooConsumer").new)
6
+ m.start
7
+ end
8
+ end
9
+
10
+ Thread.new do
11
+ @managers << RosettaQueue::ThreadedManager.create do |m|
12
+ m.add(eval_consumer_class(queue, "barconsumer.log", "BarConsumer").new)
13
+ m.start
14
+ end
15
+ end
16
+ sleep 5
17
+ end
18
+
19
+ Then /^multiple messages should be consumed from '(\w+)'$/ do |queue|
20
+ ["FooConsumer", "BarConsumer"].each do |class_name, value|
21
+ file_path = "#{CONSUMER_LOG_DIR}/#{class_name.downcase}.log"
22
+ File.readlines(file_path).last.should =~ /Hello World! from #{class_name}/
23
+ end
24
+ @managers.each {|m| m.stop_threads }
25
+ end
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ $:.unshift(File.dirname(__FILE__) + '/../../lib')
3
+ require 'rosetta_queue'
4
+ require 'rosetta_queue/spec_helpers'
5
+ require 'spec/expectations'
6
+ require 'rosetta_queue/spec_helpers'
7
+
8
+ CONSUMER_LOG_DIR = File.expand_path(File.dirname(__FILE__) + "/../support/tmp")
9
+
10
+ begin
11
+ RosettaQueue.logger = RosettaQueue::Logger.new(File.join(File.dirname(__FILE__), '../../../log', 'rosetta_queue.log'))
12
+ rescue Errno::ENOENT
13
+ Kernel.warn "No log directory setup at the root of rosetta_queue. Using the null logger instead."
14
+ class NullLogger
15
+ def info(*args); end
16
+ def debug(*args); end
17
+ def fatal(*args); end
18
+ def error(*args); end
19
+ def warn(*args); end
20
+ end
21
+
22
+ RosettaQueue.logger = NullLogger.new
23
+ end
24
+
25
+ World(RosettaQueue::SpecHelpers)
@@ -0,0 +1,29 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/env.rb')
2
+
3
+ class SampleConsumer
4
+ include ::RosettaQueue::MessageHandler
5
+ subscribes_to :foo
6
+ options :durable => true
7
+
8
+ attr_reader :msg
9
+
10
+ def on_message(msg)
11
+ @msg = msg
12
+ puts "MESSAGE #{msg}"
13
+ end
14
+
15
+ end
16
+
17
+
18
+ class SampleConsumerTwo
19
+ include ::RosettaQueue::MessageHandler
20
+ subscribes_to :foo
21
+ options :durable => true
22
+
23
+ attr_reader :msg
24
+
25
+ def on_message(msg)
26
+ @msg = msg
27
+ end
28
+
29
+ end
data/lib/rosetta_queue.rb CHANGED
@@ -1,7 +1,8 @@
1
- require 'activesupport' # TODO: remove dependency
2
-
3
1
  $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__))) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
4
2
 
3
+ require('rosetta_queue/core_ext/string') unless defined? ActiveSupport
4
+ require('rosetta_queue/core_ext/time') unless defined? ActiveSupport
5
+
5
6
  require 'rosetta_queue/adapter'
6
7
  require 'rosetta_queue/base'
7
8
  require 'rosetta_queue/consumer'
@@ -18,7 +18,7 @@ module RosettaQueue
18
18
  require "rosetta_queue/adapters/#{adapter_prefix}"
19
19
  @adapter_class = RosettaQueue::Gateway.const_get("#{adapter_prefix.to_s.classify}Adapter")
20
20
 
21
- rescue MissingSourceFile
21
+ rescue LoadError
22
22
  raise AdapterException, "Adapter type '#{adapter_prefix}' does not match existing adapters!"
23
23
  end
24
24
 
@@ -0,0 +1,48 @@
1
+ module RosettaQueue
2
+ module Gateway
3
+
4
+ module Fanout
5
+ def fanout_name_for(destination)
6
+ fanout_name = destination.gsub(/(topic|fanout)\/(.*)/, '\2')
7
+ raise AdapterException, "Unable to discover fanout exchange. Cannot bind queue to exchange!" unless fanout_name
8
+ fanout_name
9
+ end
10
+ end
11
+
12
+ class Amqp < BaseAdapter
13
+
14
+ def initialize(adapter_settings = {})
15
+ raise AdapterException, "Missing adapter settings" if adapter_settings.empty?
16
+ @adapter_settings = adapter_settings
17
+ end
18
+
19
+ def delete(destination, opts={})
20
+ exchange_strategy_for(destination, opts).delete(destination)
21
+ end
22
+
23
+ def disconnect(message_handler)
24
+ destination = destination_for(message_handler)
25
+ exchange_strategy_for(destination).unsubscribe
26
+ end
27
+
28
+ def receive_once(destination, opts={})
29
+ exchange_strategy_for(destination, opts).receive_once(destination) do |msg|
30
+ return msg
31
+ end
32
+ end
33
+
34
+ def receive_with(message_handler)
35
+ options = options_for(message_handler)
36
+ destination = destination_for(message_handler)
37
+ exchange_strategy_for(destination, options).receive(destination, message_handler)
38
+ end
39
+
40
+ def send_message(destination, message, options=nil)
41
+ exchange_strategy_for(destination, options).publish(destination, message)
42
+ end
43
+
44
+ def unsubscribe; end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,132 @@
1
+ require 'eventmachine'
2
+ require 'mq'
3
+ require File.expand_path(File.dirname(__FILE__) + "/amqp.rb")
4
+
5
+ module RosettaQueue
6
+ module Gateway
7
+
8
+ class AmqpEventedAdapter < Amqp
9
+ private
10
+
11
+ def exchange_strategy_for(destination, options)
12
+ case destination
13
+ when /^fanout\./
14
+ @exchange ||= EventedExchange::FanoutExchange.new(@adapter_settings, options)
15
+ when /^topic\./
16
+ raise "Sorry. RosettaQueue can not process AMQP topics yet"
17
+ when /^queue\./
18
+ @exchange ||= EventedExchange::DirectExchange.new(@adapter_settings, options)
19
+ else
20
+ @exchange ||= EventedExchange::DirectExchange.new(@adapter_settings, options)
21
+ end
22
+ end
23
+ end
24
+
25
+ module EventedExchange
26
+
27
+ class BaseExchange
28
+
29
+ def initialize(adapter_settings, options={})
30
+ @adapter_settings, @options = adapter_settings, options
31
+ end
32
+
33
+ def delete(destination)
34
+ conn.queue(destination).delete(@options)
35
+ end
36
+
37
+ protected
38
+
39
+ def channel
40
+ @channel ||= MQ.new(conn)
41
+ end
42
+
43
+ def conn
44
+ vhost = @adapter_settings[:opts][:vhost] || "/"
45
+ @conn ||= AMQP.connect(:user => @adapter_settings[:user],
46
+ :pass => @adapter_settings[:password],
47
+ :host => @adapter_settings[:host],
48
+ :vhost => vhost)
49
+ end
50
+ end
51
+
52
+
53
+ class DirectExchange < BaseExchange
54
+
55
+ def publish(destination, message, options={})
56
+ raise AdapterException, "Messages need to be published in an EventMachine run block (e.g., EM.run { RosettaQueue::Producer.publish(:foo, msg) } " unless EM.reactor_running?
57
+
58
+ queue = channel.queue(destination, options)
59
+ queue.publish(message, options)
60
+ RosettaQueue.logger.info("Publishing to #{destination} :: #{message}")
61
+ queue.unsubscribe
62
+ end
63
+
64
+ def receive(destination, message_handler)
65
+ raise AdapterException, "Consumers need to run in an EventMachine 'run' block. Try wrapping them inside the evented consumer manager." unless EM.reactor_running?
66
+
67
+ queue = channel.queue(destination, @options)
68
+ ack = @options[:ack]
69
+ queue.subscribe(@options) do |header, msg|
70
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
71
+ message_handler.on_message(Filters.process_receiving(msg))
72
+ header.ack if ack
73
+ end
74
+ end
75
+
76
+ def receive_once(destination, options={})
77
+ raise AdapterException, "Consumers need to run in an EventMachine 'run' block. (e.g., EM.run { RosettaQueue::Consumer.receive }" unless EM.reactor_running?
78
+
79
+ queue = channel.queue(destination, @options)
80
+ ack = @options[:ack]
81
+ queue.pop(@options) do |header, msg|
82
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
83
+ header.ack if ack
84
+ yield Filters.process_receiving(msg)
85
+ end
86
+ end
87
+ end
88
+
89
+
90
+ class FanoutExchange < BaseExchange
91
+ include Fanout
92
+
93
+ def publish(dest, msg, opts)
94
+ raise AdapterException, "Messages need to be published in an EventMachine run block (e.g., EM.run { RosettaQueue::Producer.publish(:foo, msg) } " unless EM.reactor_running?
95
+
96
+ exchange = channel.fanout(fanout_name_for(dest), opts)
97
+ exchange.publish(msg, opts)
98
+ RosettaQueue.logger.info("Publishing to fanout #{dest} :: #{msg}")
99
+ end
100
+
101
+ def receive(destination, message_handler)
102
+ raise AdapterException, "Consumers need to run in an EventMachine 'run' block. Try wrapping them inside the evented consumer manager." unless EM.reactor_running?
103
+
104
+ queue = channel.queue("queue_#{self.object_id}")
105
+ exchange = channel.fanout(fanout_name_for(destination), @options)
106
+ ack = @options[:ack]
107
+
108
+ queue.bind(exchange).subscribe(@options) do |header, msg|
109
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
110
+ message_handler.on_message(Filters.process_receiving(msg))
111
+ header.ack if ack
112
+ end
113
+ end
114
+
115
+ def receive_once(destination, opts={})
116
+ raise AdapterException, "Consumers need to run in an EventMachine 'run' block. (e.g., EM.run { RosettaQueue::Consumer.receive }" unless EM.reactor_running?
117
+
118
+ queue = channel.queue("queue_#{self.object_id}")
119
+ exchange = channel.fanout(fanout_name_for(destination), opts)
120
+ ack = @options[:ack]
121
+
122
+ queue.bind(exchange).pop(opts) do |header, msg|
123
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
124
+ header.ack if ack
125
+ yield Filters.process_receiving(msg)
126
+ end
127
+ end
128
+
129
+ end
130
+ end
131
+ end
132
+ end
@@ -1,59 +1,29 @@
1
1
  require 'bunny'
2
+ require File.expand_path(File.dirname(__FILE__) + "/amqp.rb")
2
3
 
3
4
  module RosettaQueue
4
5
  module Gateway
5
6
 
6
7
  # This AMQP adapter utilizes the synchronous AMPQ client 'Bunny'
7
8
  # by celldee (http://github.com/celldee/bunny)
8
- class AmqpSynchAdapter < 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
-
9
+ class AmqpSynchAdapter < Amqp
39
10
  private
40
11
 
41
- def exchange_strategy_for(destination, options)
12
+ def exchange_strategy_for(destination, options={})
42
13
  case destination
43
14
  when /^fanout\./
44
- @exchange ||= AmqpExchangeStrategies::FanoutExchange.new(@adapter_settings, options)
15
+ @exchange ||= SynchExchange::FanoutExchange.new(@adapter_settings, options)
45
16
  when /^topic\./
46
17
  raise "Sorry. RosettaQueue can not process AMQP topics yet"
47
18
  when /^queue\./
48
- @exchange ||= AmqpExchangeStrategies::DirectExchange.new(@adapter_settings, options)
19
+ @exchange ||= SynchExchange::DirectExchange.new(@adapter_settings, options)
49
20
  else
50
- @exchange ||= AmqpExchangeStrategies::DirectExchange.new(@adapter_settings, options)
21
+ @exchange ||= SynchExchange::DirectExchange.new(@adapter_settings, options)
51
22
  end
52
23
  end
53
-
54
24
  end
55
25
 
56
- module AmqpExchangeStrategies
26
+ module SynchExchange
57
27
 
58
28
  class BaseExchange
59
29
 
@@ -61,10 +31,15 @@ module RosettaQueue
61
31
  @adapter_settings, @options = adapter_settings, options
62
32
  end
63
33
 
64
- def delete(destination)
65
- conn.queue(destination).delete(@options)
34
+ def delete(destination, options={})
35
+ conn.queue(destination).delete(options)
66
36
  end
67
37
 
38
+ def unsubscribe
39
+ @queue.unsubscribe
40
+ conn.stop
41
+ end
42
+
68
43
  protected
69
44
 
70
45
  def conn
@@ -76,69 +51,73 @@ module RosettaQueue
76
51
  @conn.start unless @conn.status == :connected
77
52
  @conn
78
53
  end
54
+
79
55
  end
80
56
 
81
57
  class DirectExchange < BaseExchange
82
58
 
83
59
  def publish(destination, message, options={})
84
60
  RosettaQueue.logger.info("Publishing to #{destination} :: #{message}")
85
- conn.queue(destination, options).publish(message, options)
61
+ queue = conn.queue(destination, options)
62
+ queue.publish(message, options)
63
+ conn.stop
86
64
  end
87
65
 
88
66
  def receive(destination, message_handler)
89
- queue = conn.queue(destination, @options)
90
67
  ack = @options[:ack]
91
- queue.subscribe(@options) do |msg|
68
+ @queue = conn.queue(destination, @options)
69
+ @queue.subscribe(@options) do |msg|
92
70
  RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
93
71
  message_handler.on_message(Filters.process_receiving(msg))
94
- queue.ack if ack
72
+ @queue.ack if ack
95
73
  end
96
74
  end
97
75
 
98
- def receive_once(destination, options={})
99
- q = conn.queue(destination, @options)
100
- ack = @options[:ack]
101
- msg = q.pop(options)
76
+ def receive_once(destination, options = {})
77
+ ack = options[:ack]
78
+ @queue = conn.queue(destination, options)
79
+ msg = @queue.pop(options)
102
80
  RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
103
- q.ack if ack
81
+ @queue.ack if ack
104
82
  yield Filters.process_receiving(msg)
105
83
  end
84
+
106
85
  end
107
86
 
108
87
  class FanoutExchange < BaseExchange
88
+ include Fanout
109
89
 
110
- def fanout_name_for(destination)
111
- fanout_name = destination.gsub(/fanout\/(.*)/, '\1')
112
- raise "Unable to discover fanout exchange. Cannot bind queue to exchange!" unless fanout_name
113
- fanout_name
114
- end
115
-
116
- def receive_once(destination, options={})
117
- queue = conn.queue("queue_#{self.object_id}", options)
118
- exchange = conn.fanout(fanout_name_for(destination), options)
119
-
120
- msg = queue.bind(exchange).pop(@options)
121
- RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
122
- yield Filters.process_receiving(msg)
123
- end
124
-
125
90
  def publish(destination, message, options={})
126
- exchange = conn.fanout(fanout_name_for(destination), options)
91
+ exchange = conn.exchange(fanout_name_for(destination), options.merge({:type => :fanout}))
127
92
  exchange.publish(message, options)
128
93
  RosettaQueue.logger.info("Publishing to fanout #{destination} :: #{message}")
129
94
  end
130
95
 
131
96
  def receive(destination, message_handler)
132
- queue = conn.queue("queue_#{self.object_id}", @options)
133
- exchange = conn.fanout(fanout_name_for(destination), @options)
134
-
135
- msg = queue.bind(exchange).subscribe(@options) do |msg|
136
-
97
+ ack = @options[:ack]
98
+ @queue = conn.queue("queue_#{self.object_id}", @options)
99
+ exchange = conn.exchange(fanout_name_for(destination), @options.merge({:type => :fanout}))
100
+ @queue.bind(exchange)
101
+ @queue.subscribe(@options) do |msg|
137
102
  RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
138
103
  message_handler.on_message(Filters.process_receiving(msg))
104
+ @queue.ack if ack
139
105
  end
140
106
  end
107
+
108
+ def receive_once(destination, options={})
109
+ ack = options[:ack]
110
+ @queue = conn.queue("queue_#{self.object_id}", options)
111
+ exchange = conn.exchange(fanout_name_for(destination), options.merge({:type => :fanout}))
112
+ @queue.bind(exchange)
113
+ msg = @queue.pop(options)
114
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
115
+ @queue.ack if ack
116
+ yield Filters.process_receiving(msg)
117
+ end
118
+
141
119
  end
120
+
142
121
  end
143
122
  end
144
123
  end