rosetta_queue 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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