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,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 on message handler #{message_handler.inspect}.") 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,56 @@
1
+ require 'beanstalk-client'
2
+
3
+ module RosettaQueue
4
+ module Gateway
5
+
6
+ class BeanstalkAdapter < BaseAdapter
7
+
8
+ def ack(msg)
9
+ @conn.ack(msg.headers["message-id"])
10
+ end
11
+
12
+ def initialize(adapter_settings = {})
13
+ @host, @port = adapter_settings[:host], adapter_settings[:port]
14
+ @conn = Beanstalk::Pool.new(["#{@host}:#{@port}"])
15
+ end
16
+
17
+ def disconnect; end
18
+
19
+ # TODO: support options[:timeout] ?
20
+ def receive(options=nil)
21
+ msg = @conn.reserve
22
+ msg.delete
23
+ msg
24
+ end
25
+
26
+ def receive_once(destination=nil, opts={})
27
+ receive.body
28
+ end
29
+
30
+ def receive_with(message_handler)
31
+ # Note that, while we call destination_for (to comply with
32
+ # Rosetta's generic specs), beanstalk doesn't actually support
33
+ # destinations. This is just for compatibility.
34
+ destination = destination_for(message_handler)
35
+
36
+ running do
37
+ msg = receive.body
38
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
39
+ message_handler.handle_message(msg)
40
+ end
41
+ end
42
+
43
+ def send_message(destination, message, options)
44
+ RosettaQueue.logger.info("Publishing to #{destination} :: #{message}")
45
+ @conn.put(message)
46
+ end
47
+
48
+ private
49
+
50
+ def running(&block)
51
+ loop(&block)
52
+ end
53
+
54
+ end
55
+ end
56
+ 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,88 @@
1
+ require 'stomp'
2
+
3
+ module RosettaQueue
4
+ module Gateway
5
+
6
+ class StompAdapter < BaseAdapter
7
+
8
+ def ack(msg)
9
+ raise AdapterException, "Unable to ack client because message-id is blank. Are your message handler options correct? (i.e., :ack => 'client')" if msg.headers["message-id"].nil?
10
+ @conn.ack(msg.headers["message-id"])
11
+ end
12
+
13
+ def initialize(adapter_settings = {})
14
+ raise AdapterException, "Missing adapter settings" if adapter_settings.empty?
15
+ @conn = Stomp::Connection.open(adapter_settings[:user],
16
+ adapter_settings[:password],
17
+ adapter_settings[:host],
18
+ adapter_settings[:port],
19
+ true)
20
+ end
21
+
22
+ def disconnect(message_handler)
23
+ unsubscribe(destination_for(message_handler))
24
+ @conn.disconnect
25
+ end
26
+
27
+ def receive(options)
28
+ msg = @conn.receive
29
+ ack(msg) unless options[:ack].nil?
30
+ msg
31
+ end
32
+
33
+ def receive_once(destination, opts)
34
+ subscribe(destination, opts)
35
+ msg = receive(opts).body
36
+ unsubscribe(destination)
37
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
38
+ filter_receiving(msg)
39
+ end
40
+
41
+ def receive_with(message_handler)
42
+ options = options_for(message_handler)
43
+ destination = destination_for(message_handler)
44
+ @conn.subscribe(destination, options)
45
+
46
+ running do
47
+ msg = receive(options).body
48
+ Thread.current[:processing] = true
49
+ RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
50
+ message_handler.handle_message(msg)
51
+ Thread.current[:processing] = false
52
+ end
53
+ end
54
+
55
+ def send_message(destination, message, options)
56
+ RosettaQueue.logger.info("Publishing to #{destination} :: #{message}")
57
+ @conn.send(destination, message, options)
58
+ end
59
+
60
+ def subscribe(destination, options)
61
+ @conn.subscribe(destination, options)
62
+ end
63
+
64
+ def unsubscribe(destination)
65
+ @conn.unsubscribe(destination)
66
+ end
67
+
68
+ private
69
+
70
+ def running(&block)
71
+ loop(&block)
72
+ end
73
+
74
+ end
75
+
76
+ class StompAdapterProxy
77
+
78
+ def initialize(adapter, msg)
79
+ @adapter, @msg = adapter, msg
80
+ end
81
+
82
+ def ack
83
+ @adapter.ack(@msg)
84
+ end
85
+ end
86
+
87
+ end
88
+ 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,24 @@
1
+ module RosettaQueue
2
+
3
+ class BaseManager
4
+ attr_reader :consumers
5
+
6
+ class << self
7
+ def create
8
+ manager = self.new
9
+ yield manager
10
+ manager
11
+ end
12
+ end
13
+
14
+ def initialize
15
+ @consumers = {}
16
+ end
17
+
18
+ def add(message_handler)
19
+ key = message_handler.class.to_s.underscore.to_sym
20
+ @consumers[key] = Consumer.new(message_handler)
21
+ end
22
+
23
+ end
24
+ 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,94 @@
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
+ @processing = true
10
+ super
11
+ end
12
+
13
+ def start
14
+ start_threads
15
+ join_threads
16
+ monitor_threads
17
+ end
18
+
19
+ def stop
20
+ stop_threads
21
+ end
22
+
23
+ private
24
+
25
+ def join_threads
26
+ @threads.each { |thread| thread.join }
27
+ end
28
+
29
+ def shutdown_requested
30
+ RosettaQueue.logger.error "Shutdown requested, starting to prune threads..."
31
+
32
+ while @threads.any? { |n, t| t.alive? }
33
+ RosettaQueue.logger.info "Calling stop_threads"
34
+ stop_threads
35
+ sleep 5
36
+ end
37
+ end
38
+
39
+ def monitor_threads
40
+ while @running
41
+ trap("TERM") { shutdown_requested }
42
+ trap("INT") { shutdown_requested }
43
+ living = false
44
+ @threads.each { |name, thread| living ||= thread.alive? }
45
+ @processing = @threads.any? { |name, thread| thread[:processing] }
46
+ @running = living
47
+ sleep 1
48
+ end
49
+
50
+ puts "All connection threads have died..."
51
+ rescue Interrupt=>e
52
+ RosettaQueue.logger.warn("Interrupt received. Shutting down...")
53
+ puts "\nInterrupt received\n"
54
+ rescue Exception=>e
55
+ RosettaQueue.logger.error("Exception thrown -- #{e.class.name}: #{e.message}")
56
+ ensure
57
+ RosettaQueue.logger.warn("Cleaning up threads...")
58
+ stop_threads
59
+ end
60
+
61
+ def start_threads
62
+ @consumers.each do |key, consumer|
63
+ @threads[key] = Thread.new(key, consumer) do |a_key, a_consumer|
64
+ while @running
65
+ begin
66
+ RosettaQueue.logger.info("Threading consumer #{a_consumer}...")
67
+ Mutex.new.synchronize { a_consumer.receive }
68
+ rescue StopProcessingException=>e
69
+ RosettaQueue.logger.error("#{a_key}: Processing Stopped - receive interrupted")
70
+ rescue Exception=>e
71
+ RosettaQueue.logger.error("#{a_key}: Exception from connection.receive: #{$!}\n" + e.backtrace.join("\n\t"))
72
+ end
73
+ Thread.pass
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def stop_threads
80
+ RosettaQueue.logger.debug("Attempting to stop all threads...")
81
+ @running = false
82
+ @threads.select { |key, thread| thread.alive? }.each do |key, thread|
83
+ if thread[:processing]
84
+ RosettaQueue.logger.debug("#{key} Skipping thread #{thread} because the consumer is processing")
85
+ @running = true
86
+ next
87
+ end
88
+ RosettaQueue.logger.debug("#{key} Stopping thread #{thread} and disconnecting the consumer")
89
+ thread.kill
90
+ @consumers[key].disconnect
91
+ end
92
+ end
93
+ end
94
+ end