rosetta_queue 0.4.0

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 (68) hide show
  1. data/History.txt +38 -0
  2. data/MIT-LICENSE.txt +19 -0
  3. data/README.rdoc +11 -0
  4. data/Rakefile +39 -0
  5. data/VERSION.yml +4 -0
  6. data/cucumber.yml +1 -0
  7. data/examples/sample_amqp_consumer.rb +45 -0
  8. data/examples/sample_amqp_fanout_consumer.rb +52 -0
  9. data/examples/sample_amqp_fanout_producer.rb +18 -0
  10. data/examples/sample_amqp_producer.rb +16 -0
  11. data/features/filtering.feature +31 -0
  12. data/features/messaging.feature +48 -0
  13. data/features/step_definitions/common_messaging_steps.rb +82 -0
  14. data/features/step_definitions/filtering_steps.rb +17 -0
  15. data/features/step_definitions/point_to_point_steps.rb +22 -0
  16. data/features/step_definitions/publish_subscribe_steps.rb +25 -0
  17. data/features/support/env.rb +25 -0
  18. data/features/support/sample_consumers.rb +29 -0
  19. data/lib/rosetta_queue.rb +23 -0
  20. data/lib/rosetta_queue/adapter.rb +39 -0
  21. data/lib/rosetta_queue/adapters/amqp.rb +48 -0
  22. data/lib/rosetta_queue/adapters/amqp_evented.rb +132 -0
  23. data/lib/rosetta_queue/adapters/amqp_synch.rb +123 -0
  24. data/lib/rosetta_queue/adapters/base.rb +27 -0
  25. data/lib/rosetta_queue/adapters/beanstalk.rb +56 -0
  26. data/lib/rosetta_queue/adapters/fake.rb +26 -0
  27. data/lib/rosetta_queue/adapters/null.rb +57 -0
  28. data/lib/rosetta_queue/adapters/stomp.rb +88 -0
  29. data/lib/rosetta_queue/base.rb +15 -0
  30. data/lib/rosetta_queue/consumer.rb +30 -0
  31. data/lib/rosetta_queue/consumer_managers/base.rb +24 -0
  32. data/lib/rosetta_queue/consumer_managers/evented.rb +43 -0
  33. data/lib/rosetta_queue/consumer_managers/threaded.rb +94 -0
  34. data/lib/rosetta_queue/core_ext/string.rb +22 -0
  35. data/lib/rosetta_queue/core_ext/time.rb +20 -0
  36. data/lib/rosetta_queue/destinations.rb +33 -0
  37. data/lib/rosetta_queue/exception_handler.rb +105 -0
  38. data/lib/rosetta_queue/exceptions.rb +10 -0
  39. data/lib/rosetta_queue/filters.rb +58 -0
  40. data/lib/rosetta_queue/logger.rb +27 -0
  41. data/lib/rosetta_queue/message_handler.rb +52 -0
  42. data/lib/rosetta_queue/producer.rb +21 -0
  43. data/lib/rosetta_queue/spec_helpers.rb +5 -0
  44. data/lib/rosetta_queue/spec_helpers/hash.rb +21 -0
  45. data/lib/rosetta_queue/spec_helpers/helpers.rb +47 -0
  46. data/lib/rosetta_queue/spec_helpers/publishing_matchers.rb +144 -0
  47. data/spec/rosetta_queue/adapter_spec.rb +101 -0
  48. data/spec/rosetta_queue/adapters/amqp_synchronous_spec.rb +277 -0
  49. data/spec/rosetta_queue/adapters/beanstalk_spec.rb +47 -0
  50. data/spec/rosetta_queue/adapters/fake_spec.rb +72 -0
  51. data/spec/rosetta_queue/adapters/null_spec.rb +31 -0
  52. data/spec/rosetta_queue/adapters/shared_adapter_behavior.rb +38 -0
  53. data/spec/rosetta_queue/adapters/shared_fanout_behavior.rb +20 -0
  54. data/spec/rosetta_queue/adapters/stomp_spec.rb +126 -0
  55. data/spec/rosetta_queue/consumer_managers/evented_spec.rb +56 -0
  56. data/spec/rosetta_queue/consumer_managers/shared_manager_behavior.rb +26 -0
  57. data/spec/rosetta_queue/consumer_managers/threaded_spec.rb +51 -0
  58. data/spec/rosetta_queue/consumer_spec.rb +99 -0
  59. data/spec/rosetta_queue/core_ext/string_spec.rb +15 -0
  60. data/spec/rosetta_queue/destinations_spec.rb +34 -0
  61. data/spec/rosetta_queue/exception_handler_spec.rb +106 -0
  62. data/spec/rosetta_queue/filters_spec.rb +57 -0
  63. data/spec/rosetta_queue/message_handler_spec.rb +47 -0
  64. data/spec/rosetta_queue/producer_spec.rb +77 -0
  65. data/spec/rosetta_queue/shared_messaging_behavior.rb +21 -0
  66. data/spec/spec.opts +4 -0
  67. data/spec/spec_helper.rb +47 -0
  68. metadata +142 -0
@@ -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
@@ -0,0 +1,23 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__))) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ require('rosetta_queue/core_ext/string') unless defined? ActiveSupport
4
+ require('rosetta_queue/core_ext/time') unless defined? ActiveSupport
5
+
6
+ require 'rosetta_queue/adapter'
7
+ require 'rosetta_queue/base'
8
+ require 'rosetta_queue/consumer'
9
+ require 'rosetta_queue/destinations'
10
+ require 'rosetta_queue/exceptions'
11
+ require 'rosetta_queue/filters'
12
+ require 'rosetta_queue/logger'
13
+ require 'rosetta_queue/message_handler'
14
+ require 'rosetta_queue/exception_handler'
15
+ require 'rosetta_queue/producer'
16
+ require 'rosetta_queue/consumer_managers/base'
17
+ require 'rosetta_queue/consumer_managers/threaded'
18
+
19
+ if defined?(Rails)
20
+ RosettaQueue.logger = RosettaQueue::Logger.new(File.join(Rails.root, 'log', 'rosetta_queue.log'))
21
+ require('rosetta_queue/spec_helpers') if Rails.env == "test"
22
+ end
23
+
@@ -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 LoadError
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,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.handle_message(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.handle_message(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
@@ -0,0 +1,123 @@
1
+ require 'bunny'
2
+ require File.expand_path(File.dirname(__FILE__) + "/amqp.rb")
3
+
4
+ module RosettaQueue
5
+ module Gateway
6
+
7
+ # This AMQP adapter utilizes the synchronous AMPQ client 'Bunny'
8
+ # by celldee (http://github.com/celldee/bunny)
9
+ class AmqpSynchAdapter < Amqp
10
+ private
11
+
12
+ def exchange_strategy_for(destination, options={})
13
+ case destination
14
+ when /^fanout\./
15
+ @exchange ||= SynchExchange::FanoutExchange.new(@adapter_settings, options)
16
+ when /^topic\./
17
+ raise "Sorry. RosettaQueue can not process AMQP topics yet"
18
+ when /^queue\./
19
+ @exchange ||= SynchExchange::DirectExchange.new(@adapter_settings, options)
20
+ else
21
+ @exchange ||= SynchExchange::DirectExchange.new(@adapter_settings, options)
22
+ end
23
+ end
24
+ end
25
+
26
+ module SynchExchange
27
+
28
+ class BaseExchange
29
+
30
+ def initialize(adapter_settings, options={})
31
+ @adapter_settings, @options = adapter_settings, options
32
+ end
33
+
34
+ def delete(destination, options={})
35
+ conn.queue(destination).delete(options)
36
+ end
37
+
38
+ def unsubscribe
39
+ @queue.unsubscribe
40
+ conn.stop
41
+ end
42
+
43
+ protected
44
+
45
+ def conn
46
+ vhost = @adapter_settings[:opts][:vhost] || "/"
47
+ @conn ||= Bunny.new( :user => @adapter_settings[:user],
48
+ :pass => @adapter_settings[:password],
49
+ :host => @adapter_settings[:host],
50
+ :vhost => vhost)
51
+ @conn.start unless @conn.status == :connected
52
+ @conn
53
+ end
54
+
55
+ end
56
+
57
+ class DirectExchange < BaseExchange
58
+
59
+ def publish(destination, message, options={})
60
+ RosettaQueue.logger.info("Publishing to #{destination} :: #{message}")
61
+ queue = conn.queue(destination, options)
62
+ queue.publish(message, options)
63
+ conn.stop
64
+ end
65
+
66
+ def receive(destination, message_handler)
67
+ ack = @options[:ack]
68
+ @queue = conn.queue(destination, @options)
69
+ @queue.subscribe(@options) do |msg|
70
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
71
+ message_handler.handle_message(msg)
72
+ @queue.ack if ack
73
+ end
74
+ end
75
+
76
+ def receive_once(destination, options = {})
77
+ ack = options[:ack]
78
+ @queue = conn.queue(destination, options)
79
+ msg = @queue.pop(options)
80
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
81
+ @queue.ack if ack
82
+ yield Filters.process_receiving(msg)
83
+ end
84
+
85
+ end
86
+
87
+ class FanoutExchange < BaseExchange
88
+ include Fanout
89
+
90
+ def publish(destination, message, options={})
91
+ exchange = conn.exchange(fanout_name_for(destination), options.merge({:type => :fanout}))
92
+ exchange.publish(message, options)
93
+ RosettaQueue.logger.info("Publishing to fanout #{destination} :: #{message}")
94
+ end
95
+
96
+ def receive(destination, message_handler)
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|
102
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
103
+ message_handler.handle_message(msg)
104
+ @queue.ack if ack
105
+ end
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
+
119
+ end
120
+
121
+ end
122
+ end
123
+ end