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.
- data/History.txt +38 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.rdoc +11 -0
- data/Rakefile +39 -0
- data/VERSION.yml +4 -0
- data/cucumber.yml +1 -0
- data/examples/sample_amqp_consumer.rb +45 -0
- data/examples/sample_amqp_fanout_consumer.rb +52 -0
- data/examples/sample_amqp_fanout_producer.rb +18 -0
- data/examples/sample_amqp_producer.rb +16 -0
- data/features/filtering.feature +31 -0
- data/features/messaging.feature +48 -0
- data/features/step_definitions/common_messaging_steps.rb +82 -0
- data/features/step_definitions/filtering_steps.rb +17 -0
- data/features/step_definitions/point_to_point_steps.rb +22 -0
- data/features/step_definitions/publish_subscribe_steps.rb +25 -0
- data/features/support/env.rb +25 -0
- data/features/support/sample_consumers.rb +29 -0
- data/lib/rosetta_queue.rb +23 -0
- data/lib/rosetta_queue/adapter.rb +39 -0
- data/lib/rosetta_queue/adapters/amqp.rb +48 -0
- data/lib/rosetta_queue/adapters/amqp_evented.rb +132 -0
- data/lib/rosetta_queue/adapters/amqp_synch.rb +123 -0
- data/lib/rosetta_queue/adapters/base.rb +27 -0
- data/lib/rosetta_queue/adapters/beanstalk.rb +56 -0
- data/lib/rosetta_queue/adapters/fake.rb +26 -0
- data/lib/rosetta_queue/adapters/null.rb +57 -0
- data/lib/rosetta_queue/adapters/stomp.rb +88 -0
- data/lib/rosetta_queue/base.rb +15 -0
- data/lib/rosetta_queue/consumer.rb +30 -0
- data/lib/rosetta_queue/consumer_managers/base.rb +24 -0
- data/lib/rosetta_queue/consumer_managers/evented.rb +43 -0
- data/lib/rosetta_queue/consumer_managers/threaded.rb +94 -0
- data/lib/rosetta_queue/core_ext/string.rb +22 -0
- data/lib/rosetta_queue/core_ext/time.rb +20 -0
- data/lib/rosetta_queue/destinations.rb +33 -0
- data/lib/rosetta_queue/exception_handler.rb +105 -0
- data/lib/rosetta_queue/exceptions.rb +10 -0
- data/lib/rosetta_queue/filters.rb +58 -0
- data/lib/rosetta_queue/logger.rb +27 -0
- data/lib/rosetta_queue/message_handler.rb +52 -0
- data/lib/rosetta_queue/producer.rb +21 -0
- data/lib/rosetta_queue/spec_helpers.rb +5 -0
- data/lib/rosetta_queue/spec_helpers/hash.rb +21 -0
- data/lib/rosetta_queue/spec_helpers/helpers.rb +47 -0
- data/lib/rosetta_queue/spec_helpers/publishing_matchers.rb +144 -0
- data/spec/rosetta_queue/adapter_spec.rb +101 -0
- data/spec/rosetta_queue/adapters/amqp_synchronous_spec.rb +277 -0
- data/spec/rosetta_queue/adapters/beanstalk_spec.rb +47 -0
- data/spec/rosetta_queue/adapters/fake_spec.rb +72 -0
- data/spec/rosetta_queue/adapters/null_spec.rb +31 -0
- data/spec/rosetta_queue/adapters/shared_adapter_behavior.rb +38 -0
- data/spec/rosetta_queue/adapters/shared_fanout_behavior.rb +20 -0
- data/spec/rosetta_queue/adapters/stomp_spec.rb +126 -0
- data/spec/rosetta_queue/consumer_managers/evented_spec.rb +56 -0
- data/spec/rosetta_queue/consumer_managers/shared_manager_behavior.rb +26 -0
- data/spec/rosetta_queue/consumer_managers/threaded_spec.rb +51 -0
- data/spec/rosetta_queue/consumer_spec.rb +99 -0
- data/spec/rosetta_queue/core_ext/string_spec.rb +15 -0
- data/spec/rosetta_queue/destinations_spec.rb +34 -0
- data/spec/rosetta_queue/exception_handler_spec.rb +106 -0
- data/spec/rosetta_queue/filters_spec.rb +57 -0
- data/spec/rosetta_queue/message_handler_spec.rb +47 -0
- data/spec/rosetta_queue/producer_spec.rb +77 -0
- data/spec/rosetta_queue/shared_messaging_behavior.rb +21 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +47 -0
- 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,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
|