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
+ # Taken from ActiveSupport
2
+ class String
3
+ def camelize(first_letter_in_uppercase = true)
4
+ if first_letter_in_uppercase
5
+ self.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
6
+ else
7
+ self.first.downcase + camelize(self)[1..-1]
8
+ end
9
+ end
10
+
11
+ def classify
12
+ camelize(self.sub(/.*\./, ''))
13
+ end
14
+
15
+ def underscore
16
+ self.gsub(/::/, '/').
17
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
18
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
19
+ tr("-", "_").
20
+ downcase
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ require 'time'
2
+
3
+ class Time
4
+
5
+ DATE_FORMATS = {
6
+ :db => "%Y-%m-%d %H:%M:%S",
7
+ :number => "%Y%m%d%H%M%S",
8
+ :time => "%H:%M",
9
+ :short => "%d %b %H:%M",
10
+ :long => "%B %d, %Y %H:%M",
11
+ :long_ordinal => lambda { |time| time.strftime("%B #{time.day.ordinalize}, %Y %H:%M") },
12
+ :rfc822 => lambda { |time| time.strftime("%a, %d %b %Y %H:%M:%S #{time.formatted_offset(false)}") }
13
+ }
14
+
15
+ def to_formatted_s(format = :default)
16
+ return to_default_s unless formatter = DATE_FORMATS[format]
17
+ formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
18
+ end
19
+
20
+ end
@@ -0,0 +1,33 @@
1
+ module RosettaQueue
2
+
3
+ class Destinations
4
+
5
+ @dest = {}
6
+
7
+ class << self
8
+
9
+ def define
10
+ yield self
11
+ end
12
+
13
+ def clear
14
+ @dest.clear
15
+ end
16
+
17
+ def lookup(dest_name)
18
+ mapping = @dest[dest_name.to_sym]
19
+ raise "No destination mapping for '#{dest_name}' has been defined!" unless mapping
20
+ return mapping
21
+ end
22
+
23
+ def map(key, dest)
24
+ @dest[key] = dest
25
+ end
26
+
27
+ def queue_names
28
+ @dest.values
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,105 @@
1
+ # == ExceptionHandler
2
+ #
3
+ # RQ allows you to register exception handlers for different parts of messaging.
4
+ # The handlers can be a class with a ::handle method or simply a block.
5
+ # In both cases the handler needs to accept as arguments the exception and
6
+ # an additional info hash which contains useful information about the context
7
+ # in which the error was raised (i.e. the message involved).
8
+ #
9
+ # An example of class exception handler:
10
+ #
11
+ # class MessagingExceptionHandler
12
+ # def self.handle(exception, info)
13
+ # RosettaQueue.logger.error("An exception occurred when #{info[:action]} to/from #{info[:destination]}: \n#{e.message}\n#{e.backtrace.join("\n")}")
14
+ # RosettaQueue.logger.error("Message that caused exception:\n #{info[:message]}")
15
+ # end
16
+ # end
17
+ #
18
+ # You can register it like so:
19
+ #
20
+ # RosettaQueue::ExceptionHandler.register(:all, MessagingExceptionHandler)
21
+ #
22
+ #
23
+ # Instead of :all you can specify the :consuming or :publishing action.
24
+ #
25
+ # Or you can register a block like so:
26
+ #
27
+ # RosettaQueue::ExceptionHandler.register(:all) do |exception, info|
28
+ # # ....
29
+ # end
30
+ #
31
+ # == Define DSL
32
+ #
33
+ # Like the other parts of RQ you can configure it with ::define (instead of using the
34
+ # ::register call). Here is an example of that using the block handler:
35
+ #
36
+ # RosettaQueue::ExceptionHandler.define do |handler|
37
+ # handler.for(:all) do |exception, info|
38
+ # case exception
39
+ # when SomeSpecificError
40
+ # #.....
41
+ # else
42
+ # #...
43
+ # #...
44
+ # end
45
+ # end
46
+ #
47
+ # handler.for(:consuming) do |exception, info|
48
+ # # this will be called just for consuming errors in
49
+ # # addition to the :all one above
50
+ # end
51
+ # end
52
+ #
53
+
54
+
55
+ module RosettaQueue
56
+ class ExceptionHandler
57
+ class << self
58
+
59
+ def handle(messaging_action=:all, info_or_proc={})
60
+ yield
61
+ rescue Exception => e
62
+ handlers = handlers_for(messaging_action)
63
+ raise e if handlers.empty?
64
+ info = info_or_proc.respond_to?(:call) ? info_or_proc.call : info_or_proc
65
+ handlers.each { |h| h.handle(e, info) }
66
+ end
67
+
68
+
69
+ def define
70
+ yield self
71
+ end
72
+
73
+ def reset_handlers
74
+ @handlers = Hash.new { |h, k| h[k] = [] }
75
+ end
76
+
77
+ def register(messaging_action, handler_klass=nil, &block)
78
+ handler = handler_klass
79
+ if block_given?
80
+ def block.handle(*args)
81
+ call(*args)
82
+ end
83
+ handler = block
84
+ end
85
+
86
+ handlers[messaging_action] << handler
87
+ end
88
+
89
+ alias for register
90
+
91
+ private
92
+
93
+ def handlers
94
+ @handlers || reset_handlers
95
+ end
96
+
97
+ def handlers_for(messaging_action)
98
+ return handlers[:all] if messaging_action == :all
99
+
100
+ handlers[messaging_action] + handlers[:all]
101
+ end
102
+
103
+ end
104
+ end
105
+ 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,58 @@
1
+ # Example:
2
+ # RosettaQueue::Filters.define do |filter_for|
3
+ # filter_for.receiving { |message| JSON.parse(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
+ def safe_process_sending(message)
41
+ safe(:process_sending, message)
42
+ end
43
+
44
+ def safe_process_receiving(message)
45
+ safe(:process_receiving, message)
46
+ end
47
+
48
+ private
49
+
50
+ def safe(filter_call, message)
51
+ send(filter_call)
52
+ rescue StandardError
53
+ message
54
+ end
55
+
56
+ end
57
+ end
58
+ 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,52 @@
1
+ module RosettaQueue
2
+ module MessageHandler
3
+
4
+ module ClassMethods
5
+ attr_reader :destination, :options_hash
6
+
7
+ def options(options = {})
8
+ @options_hash = options
9
+ end
10
+
11
+ def publishes_to(destination)
12
+ @destination = destination
13
+ end
14
+
15
+ def subscribes_to(destination)
16
+ @destination = destination
17
+ end
18
+ end
19
+
20
+ def self.included(receiver)
21
+ receiver.extend(ClassMethods)
22
+ end
23
+
24
+ attr_accessor :adapter_proxy
25
+
26
+ def destination
27
+ self.class.destination
28
+ end
29
+
30
+ def options_hash
31
+ self.class.options_hash
32
+ end
33
+
34
+ def handle_message(unfiltered_message)
35
+ ExceptionHandler::handle(:publishing,
36
+ lambda {
37
+ { :message => Filters.safe_process_receiving(unfiltered_message),
38
+ :destination => destination,
39
+ :action => :consuming,
40
+ :options => options_hash
41
+ }
42
+ } ) do
43
+ on_message(Filters.process_receiving(unfiltered_message))
44
+ end
45
+ end
46
+
47
+ def ack
48
+ adapter_proxy.ack unless adapter_proxy.nil?
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ module RosettaQueue
2
+
3
+ class Producer < Base
4
+ include MessageHandler
5
+
6
+ def self.publish(destination, message, options = {})
7
+ ExceptionHandler::handle(:publishing,
8
+ lambda {
9
+ {:message => Filters.safe_process_sending(message),
10
+ :action => :publishing,
11
+ :destination => destination,
12
+ :options => options}
13
+ }) do
14
+ RosettaQueue::Adapter.instance.send_message(
15
+ Destinations.lookup(destination),
16
+ Filters.process_sending(message),
17
+ options)
18
+ end
19
+ end
20
+ end
21
+ 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.handle_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
+