cwyckoff-rosetta_queue 0.3.0 → 0.3.3

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 (51) hide show
  1. data/History.txt +26 -3
  2. data/README.rdoc +4 -149
  3. data/Rakefile +3 -0
  4. data/VERSION.yml +2 -2
  5. data/cucumber.yml +1 -1
  6. data/examples/sample_amqp_consumer.rb +45 -0
  7. data/examples/sample_amqp_fanout_consumer.rb +52 -0
  8. data/examples/sample_amqp_fanout_producer.rb +18 -0
  9. data/examples/sample_amqp_producer.rb +16 -0
  10. data/features/filtering.feature +31 -0
  11. data/features/messaging.feature +47 -0
  12. data/features/step_definitions/common_messaging_steps.rb +82 -0
  13. data/features/step_definitions/filtering_steps.rb +17 -0
  14. data/features/step_definitions/point_to_point_steps.rb +22 -0
  15. data/features/step_definitions/publish_subscribe_steps.rb +25 -0
  16. data/features/support/env.rb +25 -0
  17. data/features/support/sample_consumers.rb +29 -0
  18. data/lib/rosetta_queue.rb +3 -2
  19. data/lib/rosetta_queue/adapter.rb +1 -1
  20. data/lib/rosetta_queue/adapters/amqp.rb +48 -0
  21. data/lib/rosetta_queue/adapters/amqp_evented.rb +132 -0
  22. data/lib/rosetta_queue/adapters/amqp_synch.rb +48 -69
  23. data/lib/rosetta_queue/adapters/beanstalk.rb +56 -0
  24. data/lib/rosetta_queue/adapters/stomp.rb +16 -1
  25. data/lib/rosetta_queue/consumer_managers/base.rb +3 -1
  26. data/lib/rosetta_queue/consumer_managers/threaded.rb +23 -4
  27. data/lib/rosetta_queue/core_ext/string.rb +22 -0
  28. data/lib/rosetta_queue/core_ext/time.rb +20 -0
  29. data/lib/rosetta_queue/filters.rb +1 -1
  30. data/lib/rosetta_queue/logger.rb +1 -1
  31. data/lib/rosetta_queue/message_handler.rb +6 -0
  32. data/spec/rosetta_queue/adapter_spec.rb +101 -0
  33. data/spec/rosetta_queue/adapters/amqp_synchronous_spec.rb +278 -0
  34. data/spec/rosetta_queue/adapters/beanstalk_spec.rb +47 -0
  35. data/spec/rosetta_queue/adapters/fake_spec.rb +72 -0
  36. data/spec/rosetta_queue/adapters/null_spec.rb +31 -0
  37. data/spec/rosetta_queue/adapters/shared_adapter_behavior.rb +38 -0
  38. data/spec/rosetta_queue/adapters/shared_fanout_behavior.rb +20 -0
  39. data/spec/rosetta_queue/adapters/stomp_spec.rb +126 -0
  40. data/spec/rosetta_queue/consumer_managers/evented_spec.rb +56 -0
  41. data/spec/rosetta_queue/consumer_managers/shared_manager_behavior.rb +26 -0
  42. data/spec/rosetta_queue/consumer_managers/threaded_spec.rb +51 -0
  43. data/spec/rosetta_queue/consumer_spec.rb +99 -0
  44. data/spec/rosetta_queue/core_ext/string_spec.rb +15 -0
  45. data/spec/rosetta_queue/destinations_spec.rb +34 -0
  46. data/spec/rosetta_queue/filters_spec.rb +44 -0
  47. data/spec/rosetta_queue/producer_spec.rb +66 -0
  48. data/spec/rosetta_queue/shared_messaging_behavior.rb +21 -0
  49. data/spec/spec.opts +4 -0
  50. data/spec/spec_helper.rb +47 -0
  51. metadata +68 -19
@@ -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.on_message(filter_receiving(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
@@ -6,11 +6,12 @@ module RosettaQueue
6
6
  class StompAdapter < BaseAdapter
7
7
 
8
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?
9
10
  @conn.ack(msg.headers["message-id"])
10
11
  end
11
12
 
12
13
  def initialize(adapter_settings = {})
13
- raise "Missing adapter settings" if adapter_settings.empty?
14
+ raise AdapterException, "Missing adapter settings" if adapter_settings.empty?
14
15
  @conn = Stomp::Connection.open(adapter_settings[:user],
15
16
  adapter_settings[:password],
16
17
  adapter_settings[:host],
@@ -44,8 +45,10 @@ module RosettaQueue
44
45
 
45
46
  running do
46
47
  msg = receive(options).body
48
+ Thread.current[:processing] = true
47
49
  RosettaQueue.logger.info("Receiving from #{destination} :: #{msg}")
48
50
  message_handler.on_message(filter_receiving(msg))
51
+ Thread.current[:processing] = false
49
52
  end
50
53
  end
51
54
 
@@ -69,5 +72,17 @@ module RosettaQueue
69
72
  end
70
73
 
71
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
+
72
87
  end
73
88
  end
@@ -5,7 +5,9 @@ module RosettaQueue
5
5
 
6
6
  class << self
7
7
  def create
8
- yield self.new
8
+ manager = self.new
9
+ yield manager
10
+ manager
9
11
  end
10
12
  end
11
13
 
@@ -6,6 +6,7 @@ module RosettaQueue
6
6
  def initialize
7
7
  @threads = {}
8
8
  @running = true
9
+ @processing = true
9
10
  super
10
11
  end
11
12
 
@@ -24,12 +25,24 @@ module RosettaQueue
24
25
  def join_threads
25
26
  @threads.each { |thread| thread.join }
26
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
27
38
 
28
39
  def monitor_threads
29
40
  while @running
30
- trap("TERM", "EXIT")
41
+ trap("TERM") { shutdown_requested }
42
+ trap("INT") { shutdown_requested }
31
43
  living = false
32
44
  @threads.each { |name, thread| living ||= thread.alive? }
45
+ @processing = @threads.any? { |name, thread| thread[:processing] }
33
46
  @running = living
34
47
  sleep 1
35
48
  end
@@ -64,11 +77,17 @@ module RosettaQueue
64
77
  end
65
78
 
66
79
  def stop_threads
80
+ RosettaQueue.logger.debug("Attempting to stop all threads...")
67
81
  @running = false
68
- @threads.each do |key, thread|
69
- RosettaQueue.logger.info("Stopping thread and disconnecting from #{key}...")
70
- @consumers[key].disconnect
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")
71
89
  thread.kill
90
+ @consumers[key].disconnect
72
91
  end
73
92
  end
74
93
  end
@@ -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
@@ -1,6 +1,6 @@
1
1
  # Example:
2
2
  # RosettaQueue::Filters.define do |filter_for|
3
- # filter_for.receiving { |message| ActiveSupport::JSON.decode(message) }
3
+ # filter_for.receiving { |message| JSON.parse(message) }
4
4
  # filter_for.sending { |hash| hash.to_json }
5
5
  # end
6
6
 
@@ -19,7 +19,7 @@ module RosettaQueue
19
19
  class Logger < ::Logger
20
20
 
21
21
  def format_message(severity, timestamp, progname, msg)
22
- "[#{timestamp.to_formatted_s(:db)}] #{severity} #{msg}\n"
22
+ "[#{timestamp.to_formatted_s(:db)}] #{severity} -- : #{msg}\n"
23
23
  end
24
24
 
25
25
  end
@@ -20,6 +20,7 @@ module RosettaQueue
20
20
 
21
21
  def self.included(receiver)
22
22
  receiver.extend(ClassMethods)
23
+ attr_accessor :adapter_proxy
23
24
 
24
25
  def destination
25
26
  self.class.destination
@@ -28,6 +29,11 @@ module RosettaQueue
28
29
  def options_hash
29
30
  self.class.options_hash
30
31
  end
32
+
33
+ def ack
34
+ adapter_proxy.ack unless adapter_proxy.nil?
35
+ end
36
+
31
37
  end
32
38
  end
33
39
  end
@@ -0,0 +1,101 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ module RosettaQueue
4
+
5
+ describe Adapter do
6
+
7
+ before(:each) do
8
+ @stomp_adapter = mock("Gateway::StompAdapter")
9
+ Adapter.reset
10
+ end
11
+
12
+ describe ".reset" do
13
+ it "should clear all definitions" do
14
+ Adapter.define { |a| a.type = "null" }
15
+ Adapter.instance.should be_instance_of(RosettaQueue::Gateway::NullAdapter)
16
+ Adapter.reset
17
+ running { Adapter.instance }.should raise_error(AdapterException)
18
+ end
19
+ end
20
+
21
+ describe ".type=" do
22
+
23
+ it "should raise error when adapter does not exist" do
24
+ running {
25
+ Adapter.define do |a|
26
+ a.type = "foo"
27
+ end
28
+ }.should raise_error(AdapterException)
29
+ end
30
+
31
+ end
32
+
33
+ describe "adapter not type set" do
34
+ it "should raise an error when .instance is called" do
35
+ # given
36
+ Adapter.define { |a| }
37
+ # then & when
38
+ running { Adapter.instance }.should raise_error(AdapterException)
39
+ end
40
+ end
41
+
42
+ describe "adapter type set" do
43
+
44
+ before(:each) do
45
+ Adapter.define { |a| a.type = "null" }
46
+ end
47
+
48
+ it "should return adapter instance" do
49
+ Adapter.instance.class.should == RosettaQueue::Gateway::NullAdapter
50
+ end
51
+
52
+ end
53
+
54
+ describe "adapter instantiation" do
55
+
56
+ before(:each) do
57
+ Adapter.define do |a|
58
+ a.user = "foo"
59
+ a.password = "bar"
60
+ a.host = "localhost"
61
+ a.port = "9000"
62
+ a.type = "fake"
63
+ end
64
+ end
65
+
66
+ def do_process
67
+ Adapter.instance
68
+ end
69
+
70
+ it "should set opts as an empty has unless variable is set" do
71
+ during_process {
72
+ RosettaQueue::Gateway::FakeAdapter.should_receive(:new).with({:user => "foo", :password => "bar", :host => "localhost", :port => "9000", :opts => {}})
73
+ }
74
+ end
75
+
76
+ describe "when setting options" do
77
+ before(:each) do
78
+ Adapter.define { |a| a.options = {:vhost => "baz"} }
79
+ end
80
+
81
+ it "should map adapter_settings to a hash" do
82
+ during_process {
83
+ RosettaQueue::Gateway::FakeAdapter.should_receive(:new).with({:user => "foo", :password => "bar", :host => "localhost", :port => "9000", :opts => {:vhost => "baz"}})
84
+ }
85
+ end
86
+ end
87
+
88
+ describe "setting options incorrectly (options should always be set as a Hash)" do
89
+
90
+ before(:each) do
91
+ Adapter.define { |a| a.options = "baz" }
92
+ end
93
+
94
+ it "should raise an adapter exception" do
95
+ running { Adapter.instance }.should raise_error("Adapter options should be a hash")
96
+ end
97
+ end
98
+
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,278 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+ require File.dirname(__FILE__) + '/shared_adapter_behavior'
3
+ require File.dirname(__FILE__) + '/shared_fanout_behavior'
4
+
5
+ begin
6
+ require 'rosetta_queue/adapters/amqp_synch'
7
+ rescue LoadError => ex
8
+ if ex.message =~ /bunny/i
9
+ warn "--WARNING-- Skipping all of tha AmqpSynchAdapter code examples since bunny is not present. Install bunny if you need to be testing this adapter!"
10
+ else
11
+ raise ex
12
+ end
13
+ end
14
+
15
+ if defined? Bunny
16
+
17
+ module RosettaQueue::Gateway
18
+
19
+ describe "an exchange", :shared => true do
20
+
21
+ describe "#do_exchange" do
22
+
23
+ it "should filter the message and forward it to the handler" do
24
+ when_receiving_exchange {
25
+ ::RosettaQueue::Filters.should_receive(:process_receiving).with(@msg).and_return("Filtered Message")
26
+ @handler.should_receive(:on_message).with("Filtered Message")
27
+ }
28
+ end
29
+ end
30
+ end
31
+
32
+ describe "AmqpSynch adapter and components" do
33
+
34
+ before(:each) do
35
+ RosettaQueue.logger.stub!(:info)
36
+ @msg = "Hello World!"
37
+ @adapter = AmqpSynchAdapter.new({:user => "foo", :password => "bar", :host => "localhost"})
38
+ @handler = mock("handler", :on_message => true, :destination => :foo, :options_hash => {:durable => true})
39
+ end
40
+
41
+ describe AmqpSynchAdapter do
42
+
43
+ before(:each) do
44
+ @exchange_strategy = mock('DirectExchange', :receive_once => @msg, :receive => @msg, :send_message => true)
45
+ SynchExchange::DirectExchange.stub!(:new).and_return(@exchange_strategy)
46
+ end
47
+
48
+ it_should_behave_like "an adapter"
49
+
50
+ describe "#receive_once" do
51
+
52
+ def do_receiving_once
53
+ @adapter.receive_once("queue.foo", {:durable => false})
54
+ end
55
+
56
+ it "should pass destination and options to exchange strategy" do
57
+ when_receiving_once {
58
+ @exchange_strategy.should_receive(:receive_once).with("queue.foo")
59
+ }
60
+ end
61
+
62
+ end
63
+
64
+ describe "#receive_with" do
65
+
66
+ def do_receiving_with_handler
67
+ @adapter.receive_with(@handler)
68
+ end
69
+
70
+ before(:each) do
71
+ @handler = mock("handler", :on_message => true, :destination => :foo, :options_hash => {:durable => true })
72
+ end
73
+
74
+ it "should pass message handler to exchange strategy" do
75
+ when_receiving_with_handler {
76
+ @exchange_strategy.should_receive(:receive).with("foo", @handler)
77
+ }
78
+ end
79
+
80
+ end
81
+
82
+ describe "#send_message" do
83
+
84
+ it "should pass message handler to exchange strategy" do
85
+ when_publishing {
86
+ @exchange_strategy.should_receive(:publish).with('queue', 'message')
87
+ }
88
+ end
89
+
90
+ end
91
+ end
92
+
93
+
94
+ describe SynchExchange::DirectExchange do
95
+
96
+ before(:each) do
97
+ @queue = mock("Bunny::Queue", :pop => @msg, :publish => true, :unsubscribe => true)
98
+ Bunny.stub!(:new).and_return(@conn = mock("Bunny::Client", :queue => @queue, :exchange => @exchange, :status => :connected, :stop => nil))
99
+ @queue.stub!(:subscribe).and_yield(@msg)
100
+ @handler = mock("handler", :on_message => true, :destination => :foo)
101
+ @exchange = SynchExchange::DirectExchange.new({:user => 'user', :password => 'pass', :host => 'host', :opts => {:vhost => "foo"}})
102
+ end
103
+
104
+
105
+ def do_receiving_exchange
106
+ @exchange.receive("queue.foo", @handler)
107
+ end
108
+
109
+ it_should_behave_like "an exchange"
110
+
111
+ describe "#receive_once" do
112
+
113
+ def do_receiving_single_exchange
114
+ @exchange.receive_once("queue.foo") { |msg| }
115
+
116
+ end
117
+
118
+ it "should return the message from the connection" do
119
+ @exchange.receive_once("queue.foo") do |msg|
120
+ msg.should == @msg
121
+ end
122
+ end
123
+
124
+ it "should subscribe to queue" do
125
+ when_receiving_single_exchange {
126
+ @queue.should_receive(:pop)
127
+ }
128
+ end
129
+
130
+ end
131
+
132
+ describe "#receive" do
133
+
134
+ it "should subscribe to queue" do
135
+ when_receiving_exchange {
136
+ @queue.should_receive(:subscribe).and_yield(@msg)
137
+ }
138
+ end
139
+
140
+ end
141
+
142
+
143
+ describe "#publish" do
144
+
145
+ def do_publishing
146
+ @exchange.publish('queue.foo', 'message')
147
+ end
148
+
149
+ it "should instantiate queue" do
150
+ when_publishing {
151
+ @conn.should_receive(:queue).and_return(@queue)
152
+ }
153
+ end
154
+
155
+ it "should publish message to queue" do
156
+ when_publishing {
157
+ @conn.queue.should_receive(:publish).with("message", {})
158
+ }
159
+ end
160
+
161
+ end
162
+
163
+ end
164
+
165
+
166
+ describe SynchExchange::FanoutExchange do
167
+
168
+ before(:each) do
169
+ @exchange = SynchExchange::FanoutExchange.new({:user => 'user', :password => 'pass', :host => 'host', :opts => {:vhost => 'foo'}})
170
+ @queue = mock("Bunny::Queue", :pop => @msg, :bind => @bound_queue = mock("Bunny::Queue", :pop => @msg), :publish => true, :unbind => true)
171
+ Bunny.stub!(:new).and_return(@conn = mock("Bunny::Client", :queue => @queue, :exchange => @exchange, :status => :connected))
172
+ @queue.stub!(:subscribe).and_yield(@msg)
173
+ @handler = mock("handler", :on_message => true, :destination => :foo, :options => {:durable => false})
174
+ end
175
+
176
+ def do_receiving_exchange
177
+ @exchange.receive("topic.foo", @handler)
178
+ end
179
+
180
+ it_should_behave_like "an exchange"
181
+
182
+ describe "#receive_once" do
183
+
184
+ def do_receiving_exchange
185
+ @exchange.receive_once("topic.foo") { |msg| }
186
+ end
187
+
188
+ it_should_behave_like "a fanout exchange adapter"
189
+
190
+ it "should return the message from the connection" do
191
+ @exchange.receive_once("topic.foo") do |msg|
192
+ msg.should == @msg
193
+ end
194
+ end
195
+
196
+ it "should subscribe to queue" do
197
+ when_receiving_exchange {
198
+ @queue.should_receive(:pop)
199
+ }
200
+ end
201
+
202
+ it "should unbind queue from exchange" do
203
+ pending
204
+ when_receiving_single_exchange {
205
+ @queue.should_receive(:unbind)
206
+ }
207
+ end
208
+
209
+ end
210
+
211
+ describe "#receive" do
212
+
213
+ it_should_behave_like "a fanout exchange adapter"
214
+
215
+ it "should forward the message body onto the handler" do
216
+ when_receiving_exchange {
217
+ @handler.should_receive(:on_message).with("Hello World!")
218
+ }
219
+ end
220
+
221
+ it "should subscribe to queue" do
222
+ when_receiving_exchange {
223
+ @queue.should_receive(:subscribe).and_yield(@msg)
224
+ }
225
+ end
226
+
227
+ end
228
+
229
+ # describe "#publish_to_exchange" do
230
+ #
231
+ # def do_publishing
232
+ # @exchange.publish_to_exchange('/queue/foo', 'message', {:durable => false})
233
+ # end
234
+ #
235
+ # it "should instantiate queue" do
236
+ # when_publishing {
237
+ # @channel.should_receive(:queue).and_return(@queue)
238
+ # }
239
+ # end
240
+ #
241
+ # it "should publish message to queue" do
242
+ # when_publishing {
243
+ # @channel.queue.should_receive(:publish).with('message')
244
+ # }
245
+ # end
246
+ #
247
+ # it "should stop event loop" do
248
+ # when_publishing {
249
+ # EM.should_receive(:stop_event_loop)
250
+ # }
251
+ # end
252
+ # end
253
+
254
+ # describe SynchExchange::AmqpAdapterProxy do
255
+
256
+ # before(:each) do
257
+ # @queue = mock("Queue", :ack => nil)
258
+ # @proxy = SynchExchange::AmqpAdapterProxy.new(@queue)
259
+ # end
260
+
261
+ # context "#ack" do
262
+
263
+ # it "should delegate to AMQP queue object" do
264
+ # # expect
265
+ # @queue.should_receive(:ack)
266
+
267
+ # # when
268
+ # @proxy.ack
269
+ # end
270
+
271
+ # end
272
+ # end
273
+
274
+ end
275
+ end
276
+ end
277
+
278
+ end # for Guard up top to prevent this spec from running if Bunny not loaded from amqp_synch