beetle 0.4.2 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3f2aa62352bfa22c0bcf8b02160027aed3429cd6
4
- data.tar.gz: 491a52bbb628b89165e09671e505147d540844bf
3
+ metadata.gz: 0e984630440947f81643287a32934138fcdaee58
4
+ data.tar.gz: 421b42f6d56a837ba9e2a05a0d0f0087f8d5a737
5
5
  SHA512:
6
- metadata.gz: 383565b61fe2689ac5767b668710762bda352a8c1c37253e351e7c5d527e97dbd240e5346403881cf3cf428b0b2137eea1fddb4cba15ea6eb3a2afb7f8f04205
7
- data.tar.gz: 76fd7ccdb3f16680792695778685fca20102575b1c419ffd6e5fa3d4b48917de7abcbf808f109d2cb3fe70a76cd638d18fffd3165aa93fdefd984daec74722c3
6
+ metadata.gz: c91106551e7e66cf01cdf614130568abe45dd490bcf51c8e9669214c20af29d91a883c545fea13ae3555a2e8a5c5609784d7b7bbb7e01db733a4c06eefdee91f
7
+ data.tar.gz: 588b1921ed5a473953572fcf542c62a6a5344700eef6c50d821ee0430a3e38576ee740e0c3330295ff011a887aa63eebd7c56cc056f01a011a502cd4cd2adf46
@@ -1,5 +1,13 @@
1
1
  = Release Notes
2
2
 
3
+ == Version 0.4.3
4
+ * fixed a race condition which could lead to duplicate message processing
5
+ * fixed eventmachine shutdown sequence problem, which led to ACKs
6
+ occasionally being lost due to writing to a closed socket, which in
7
+ turn caused messages to be processed twice
8
+ * stop_listening now always triggers the subscribe shutdown sequence
9
+ via a eventmachine timer callback, if the eventmachine reactor is running
10
+
3
11
  == Version 0.4.2
4
12
  * Fail hard on missing master file
5
13
  * Set message timestamp header
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ require 'bundler/setup'
1
2
  require 'rake'
2
3
  require 'rake/testtask'
3
4
  require 'bundler/gem_tasks'
@@ -0,0 +1,56 @@
1
+ # consume_many_messages_and_shutdown_randomly.rb
2
+ # this example excercises the shutdown sequence and tests whether
3
+ # messages are handled more than once due to the shutdown
4
+ #
5
+ # ! check the examples/README.rdoc for information on starting your redis/rabbit !
6
+ #
7
+ # use it like so:
8
+ #
9
+ # while ruby consume_many_messages_and_shutdown_randomly.rb; do echo "no duplicate found yet"; done
10
+ #
11
+ # if the loop stops, a duplicate has been found.
12
+ # you can stop this process by sending an interrupt signal
13
+
14
+ trap("INT"){ puts "ignoring interrupt, please wait" }
15
+
16
+ require "rubygems"
17
+ require File.expand_path("../lib/beetle", File.dirname(__FILE__))
18
+
19
+ # set Beetle log level to info, less noisy than debug
20
+ Beetle.config.logger.level = Logger::INFO
21
+
22
+ # setup client
23
+ client = Beetle::Client.new
24
+ client.register_queue(:test)
25
+ client.register_message(:test)
26
+
27
+ # create a redis instance with a different database
28
+ redis = Redis.new(:db => 7)
29
+
30
+ exit_code = 0
31
+
32
+ # register our handler to the message, check out the message.rb for more stuff you can get from the message object
33
+ client.register_handler(:test) do |message|
34
+ uuid = message.uuid
35
+ if redis.incr(uuid) > 1
36
+ exit_code = 1
37
+ puts "\n\nRECEIVED A MESSAGE twice: #{uuid}\n\n"
38
+ client.stop_listening
39
+ end
40
+ end
41
+
42
+ # start listening
43
+ # this starts the event machine event loop using EM.run
44
+ # the block passed to listen will be yielded as the last step of the setup process
45
+ client.listen do
46
+ trap("TERM"){ client.stop_listening }
47
+ trap("INT"){ exit_code = 1; client.stop_listening }
48
+ # start a thread which randomly kills us
49
+ Thread.new do
50
+ sleep(2 + rand)
51
+ Process.kill("TERM", $$)
52
+ end
53
+ puts "trying to detect duplicates"
54
+ end
55
+
56
+ exit exit_code
@@ -0,0 +1,23 @@
1
+ # publish_many_messages.rb
2
+ # this script pbulishes ARGV[0] small test messages (or 100000 if no argument is provided)
3
+ #
4
+ # ! check the examples/README.rdoc for information on starting your redis/rabbit !
5
+ #
6
+ # start it with ruby publish_many_messages.rb 1000000
7
+
8
+ require "rubygems"
9
+ require File.expand_path("../lib/beetle", File.dirname(__FILE__))
10
+
11
+ # set Beetle log level to info, less noisy than debug
12
+ Beetle.config.logger.level = Logger::INFO
13
+
14
+ # setup client
15
+ client = Beetle::Client.new
16
+ client.register_queue(:test)
17
+ client.register_message(:test)
18
+
19
+ # publish a lot of identical messages
20
+ n = (ARGV[0] || 100000).to_i
21
+ n.times{ client.publish(:test, 'x') }
22
+
23
+ puts "published #{n} messages"
@@ -11,9 +11,9 @@ module Beetle
11
11
  # On the publisher side, publishing a message will ensure that the exchange it will be
12
12
  # sent to, and each of the queues bound to the exchange, will be created on demand. On
13
13
  # the subscriber side, exchanges, queues, bindings and queue subscriptions will be
14
- # created when the application calls the listen method. An application can decide to
15
- # subscribe to only a subset of the configured queues by passing a list of queue names
16
- # to the listen method.
14
+ # created when the application calls the listen_queues method. An application can decide
15
+ # to subscribe to only a subset of the configured queues by passing a list of queue
16
+ # names to the listen method.
17
17
  #
18
18
  # The net effect of this strategy is that producers and consumers can be started in any
19
19
  # order, so that no message is lost if message producers are accidentally started before
@@ -216,14 +216,15 @@ module Beetle
216
216
  subscriber.listen_queues(queues, &block)
217
217
  end
218
218
 
219
- # stops the eventmachine loop
219
+ # stops the subscriber by closing all channels and connections. note this an
220
+ # asynchronous operation due to the underlying eventmachine mechanism.
220
221
  def stop_listening
221
- subscriber.stop!
222
+ @subscriber.stop! if @subscriber
222
223
  end
223
224
 
224
225
  # disconnects the publisher from all servers it's currently connected to
225
226
  def stop_publishing
226
- publisher.stop
227
+ @publisher.stop if @publisher
227
228
  end
228
229
 
229
230
  # pause listening on a list of queues
@@ -267,8 +268,8 @@ module Beetle
267
268
  end
268
269
 
269
270
  def reset
270
- stop_publishing if @publisher
271
- stop_listening if @subscriber
271
+ stop_publishing
272
+ stop_listening
272
273
  config.reload
273
274
  load_brokers_from_config
274
275
  rescue Exception => e
@@ -124,6 +124,12 @@ module Beetle
124
124
  with_failover { redis.setnx(key(msg_id, suffix), value) }
125
125
  end
126
126
 
127
+ # store some key/value pairs
128
+ def mset(msg_id, values)
129
+ values = values.inject([]){|a,(k,v)| a.concat([key(msg_id, k), v])}
130
+ with_failover { redis.mset(*values) }
131
+ end
132
+
127
133
  # store some key/value pairs if none of the given keys exist.
128
134
  def msetnx(msg_id, values)
129
135
  values = values.inject([]){|a,(k,v)| a.concat([key(msg_id, k), v])}
@@ -140,6 +146,12 @@ module Beetle
140
146
  with_failover { redis.get(key(msg_id, suffix)) }
141
147
  end
142
148
 
149
+ # retrieve the values with given <tt>suffixes</tt> for given <tt>msg_id</tt>. returns a list of strings.
150
+ def mget(msg_id, keys)
151
+ keys = keys.map{|suffix| key(msg_id, suffix)}
152
+ with_failover { redis.mget(*keys) }
153
+ end
154
+
143
155
  # delete key with given <tt>suffix</tt> for given <tt>msg_id</tt>.
144
156
  def del(msg_id, suffix)
145
157
  with_failover { redis.del(key(msg_id, suffix)) }
@@ -172,8 +172,7 @@ module Beetle
172
172
 
173
173
  # mark message handling complete in the deduplication store
174
174
  def completed!
175
- @store.set(msg_id, :status, "completed")
176
- timed_out!
175
+ @store.mset(msg_id, :status => "completed", :timeout => 0)
177
176
  end
178
177
 
179
178
  # whether we should wait before running the handler
@@ -237,6 +236,10 @@ module Beetle
237
236
  logger.debug "Beetle: deleted mutex: #{msg_id}"
238
237
  end
239
238
 
239
+ def fetch_status_delay_timeout_attempts_exceptions
240
+ @store.mget(msg_id, [:status, :delay, :timeout, :attempts, :exceptions])
241
+ end
242
+
240
243
  # process this message and do not allow any exception to escape to the caller
241
244
  def process(handler)
242
245
  logger.debug "Beetle: processing message #{msg_id}"
@@ -269,30 +272,33 @@ module Beetle
269
272
  run_handler(handler) == RC::HandlerCrash ? RC::AttemptsLimitReached : RC::OK
270
273
  elsif !key_exists?
271
274
  run_handler!(handler)
272
- elsif completed?
273
- ack!
274
- RC::OK
275
- elsif delayed?
276
- logger.warn "Beetle: ignored delayed message (#{msg_id})!"
277
- RC::Delayed
278
- elsif !timed_out?
279
- RC::HandlerNotYetTimedOut
280
- elsif attempts_limit_reached?
281
- completed!
282
- ack!
283
- logger.warn "Beetle: reached the handler execution attempts limit: #{attempts_limit} on #{msg_id}"
284
- RC::AttemptsLimitReached
285
- elsif exceptions_limit_reached?
286
- completed!
287
- ack!
288
- logger.warn "Beetle: reached the handler exceptions limit: #{exceptions_limit} on #{msg_id}"
289
- RC::ExceptionsLimitReached
290
275
  else
291
- set_timeout!
292
- if aquire_mutex!
293
- run_handler!(handler)
276
+ status, delay, timeout, attempts, exceptions = fetch_status_delay_timeout_attempts_exceptions
277
+ if status == "completed"
278
+ ack!
279
+ RC::OK
280
+ elsif delay && delay.to_i > now
281
+ logger.warn "Beetle: ignored delayed message (#{msg_id})!"
282
+ RC::Delayed
283
+ elsif !(timeout && timeout.to_i < now)
284
+ RC::HandlerNotYetTimedOut
285
+ elsif attempts.to_i >= attempts_limit
286
+ completed!
287
+ ack!
288
+ logger.warn "Beetle: reached the handler execution attempts limit: #{attempts_limit} on #{msg_id}"
289
+ RC::AttemptsLimitReached
290
+ elsif exceptions.to_i > exceptions_limit
291
+ completed!
292
+ ack!
293
+ logger.warn "Beetle: reached the handler exceptions limit: #{exceptions_limit} on #{msg_id}"
294
+ RC::ExceptionsLimitReached
294
295
  else
295
- RC::MutexLocked
296
+ set_timeout!
297
+ if aquire_mutex!
298
+ run_handler!(handler)
299
+ else
300
+ RC::MutexLocked
301
+ end
296
302
  end
297
303
  end
298
304
  end
@@ -7,14 +7,13 @@ module Beetle
7
7
  # create a new subscriber instance
8
8
  def initialize(client, options = {}) #:nodoc:
9
9
  super
10
- @status = :idle
11
- @request_stop = false
12
10
  @servers.concat @client.additional_subscription_servers
13
11
  @handlers = {}
14
12
  @connections = {}
15
13
  @channels = {}
16
14
  @subscriptions = {}
17
15
  @listened_queues = []
16
+ @channels_closed = false
18
17
  end
19
18
 
20
19
  # the client calls this method to subscribe to a list of queues.
@@ -48,24 +47,19 @@ module Beetle
48
47
  end
49
48
  end
50
49
 
51
- # closes all AMQP connections and stops the eventmachine loop
50
+ # closes all AMQP connections and stop the eventmachine loop. note that the shutdown
51
+ # process is asynchronous. must not be called while a message handler is
52
+ # running. typically one would use <tt>EM.add_timer(0) { stop! }</tt> to ensure this.
52
53
  def stop! #:nodoc:
53
- if @connections.empty?
54
- EM.stop_event_loop
55
- else
56
- # Only kill connections if not currently processing a message
57
- # otherwise messages can get ACKed after the connection is closed
58
- # resulting in the ACK not being received and hence the
59
- # message being re-delivered
60
- if @status == :idle
61
- server, connection = @connections.shift
62
- logger.debug "Beetle: closing connection to #{server}"
63
- connection.close { stop! }
64
- else
65
- # else ask for stop. After processing the current message the
66
- # stop will be re-attempted
67
- @request_stop = true
54
+ if EM.reactor_running?
55
+ EM.add_timer(0) do
56
+ close_all_channels
57
+ close_all_connections
68
58
  end
59
+ else
60
+ # try to clean up as much a possible under the circumstances, by closing all connections
61
+ # this should a least close the sockets
62
+ close_connections_with_reactor_not_running
69
63
  end
70
64
  end
71
65
 
@@ -78,6 +72,38 @@ module Beetle
78
72
 
79
73
  private
80
74
 
75
+ # close all sockets.
76
+ def close_connections_with_reactor_not_running
77
+ @connections.each { |_, connection| connection.close }
78
+ ensure
79
+ @connections = {}
80
+ @channels = {}
81
+ end
82
+
83
+ # close all connections. this assumes the reactor is running
84
+ def close_all_connections
85
+ if @connections.empty?
86
+ EM.stop_event_loop
87
+ else
88
+ server, connection = @connections.shift
89
+ logger.debug "Beetle: closing connection to #{server}"
90
+ connection.close { close_all_connections }
91
+ end
92
+ end
93
+
94
+ # closes all channels. this needs to be the first action during a
95
+ # subscriber shutdown, so that susbscription callbacks can detect
96
+ # they should stop processing messages received from the prefetch
97
+ # queue.
98
+ def close_all_channels
99
+ return if @channels_closed
100
+ @channels.each do |server, channel|
101
+ logger.debug "Beetle: closing channel to server #{server}"
102
+ channel.close
103
+ end
104
+ @channels_closed = true
105
+ end
106
+
81
107
  def exchanges_for_queues(queues)
82
108
  @client.bindings.slice(*queues).map{|_, opts| opts.map{|opt| opt[:exchange]}}.flatten.uniq
83
109
  end
@@ -136,8 +162,11 @@ module Beetle
136
162
  def create_subscription_callback(queue_name, amqp_queue_name, handler, opts)
137
163
  server = @server
138
164
  lambda do |header, data|
165
+ if channel(server).closing?
166
+ logger.info "Beetle: ignoring message since channel to server #{server} already closed"
167
+ return
168
+ end
139
169
  begin
140
- @status = :busy
141
170
  # logger.debug "Beetle: received message"
142
171
  processor = Handler.create(handler, opts)
143
172
  message_options = opts.merge(:server => server, :store => @client.deduplication_store)
@@ -167,10 +196,6 @@ module Beetle
167
196
  ensure
168
197
  # processing_completed swallows all exceptions, so we don't need to protect this call
169
198
  processor.processing_completed
170
- @status = :idle
171
- if @request_stop
172
- stop!
173
- end
174
199
  end
175
200
  end
176
201
  end
@@ -1,3 +1,3 @@
1
1
  module Beetle
2
- VERSION = "0.4.2"
2
+ VERSION = "0.4.3"
3
3
  end
@@ -22,29 +22,32 @@ module Beetle
22
22
  assert_equal channel, @sub.send(:channel, "donald:1")
23
23
  end
24
24
 
25
- test "stop! should close all amqp connections and then stop the event loop if no handler is currently running" do
26
- connection1 = mock('con1')
25
+ test "stop! should close all amqp channels and connections and then stop the event loop if the reactor is running" do
26
+ connection1 = mock('conection1')
27
27
  connection1.expects(:close).yields
28
- connection2 = mock('con2')
28
+ connection2 = mock('connection2')
29
29
  connection2.expects(:close).yields
30
- @sub.instance_variable_set "@connections", [["server1", connection1], ["server2",connection2]]
30
+ channel1 = mock('channel1')
31
+ channel1.expects(:close)
32
+ channel2 = mock('channel2')
33
+ channel2.expects(:close)
34
+ @sub.instance_variable_set "@connections", [["server1", connection1], ["server2", connection2]]
35
+ @sub.instance_variable_set "@channels", {"server1" => channel1, "server2" => channel2}
36
+ EM.expects(:reactor_running?).returns(true)
31
37
  EM.expects(:stop_event_loop)
38
+ EM.expects(:add_timer).with(0).yields
32
39
  @sub.send(:stop!)
33
- assert !@sub.instance_variable_get("@request_stop")
34
40
  end
35
41
 
36
- test "stop! should not stop the event loop if a handler is currently running" do
37
- @sub.instance_variable_set "@status", :busy
38
- connection1 = mock('con1')
39
- connection1.expects(:close).never
40
- connection2 = mock('con2')
41
- connection2.expects(:close).never
42
- @sub.instance_variable_set "@connections", [["server1", connection1], ["server2",connection2]]
43
- EM.expects(:stop_event_loop).never
42
+ test "stop! should close all connections if the reactor is not running" do
43
+ connection1 = mock('conection1')
44
+ connection1.expects(:close).yields
45
+ connection2 = mock('connection2')
46
+ connection2.expects(:close).yields
47
+ @sub.instance_variable_set "@connections", [["server1", connection1], ["server2", connection2]]
48
+ EM.expects(:reactor_running?).returns(false)
44
49
  @sub.send(:stop!)
45
- assert @sub.instance_variable_get("@request_stop")
46
50
  end
47
-
48
51
  end
49
52
 
50
53
  class SubscriberPauseAndResumeTest < MiniTest::Unit::TestCase
@@ -219,13 +222,16 @@ module Beetle
219
222
  end
220
223
 
221
224
 
222
- class DeadLetterrngCallBackExecutionTest < MiniTest::Unit::TestCase
225
+ class DeadLetteringCallBackExecutionTest < MiniTest::Unit::TestCase
223
226
  def setup
224
227
  @client = Client.new
225
228
  @client.config.dead_lettering_enabled = true
226
229
  @queue = "somequeue"
227
230
  @client.register_queue(@queue)
228
231
  @sub = @client.send(:subscriber)
232
+ mq = mock("MQ")
233
+ mq.expects(:closing?).returns(false)
234
+ @sub.expects(:channel).with(@sub.server).returns(mq)
229
235
  @exception = Exception.new "murks"
230
236
  @handler = Handler.create(lambda{|*args| raise @exception})
231
237
  # handler method 'processing_completed' should be called under all circumstances
@@ -257,42 +263,52 @@ module Beetle
257
263
  @sub = client.send(:subscriber)
258
264
  @exception = Exception.new "murks"
259
265
  @handler = Handler.create(lambda{|*args| raise @exception})
260
- # handler method 'processing_completed' should be called under all circumstances
261
- @handler.expects(:processing_completed).once
262
266
  @callback = @sub.send(:create_subscription_callback, "my myessage", @queue, @handler, :exceptions => 1)
263
267
  end
264
268
 
265
269
  test "exceptions raised from message processing should be ignored" do
270
+ @handler.expects(:processing_completed).once
266
271
  header = header_with_params({})
267
272
  Message.any_instance.expects(:process).raises(Exception.new("don't worry"))
273
+ channel = mock("MQ")
274
+ channel.expects(:closing?).returns(false)
275
+ @sub.expects(:channel).with(@sub.server).returns(channel)
268
276
  assert_nothing_raised { @callback.call(header, 'foo') }
269
277
  end
270
278
 
271
- test "should call stop! if @request_stop has been set" do
279
+ test "callback should not process messages if the underlying channel has already been closed" do
280
+ @handler.expects(:processing_completed).never
272
281
  header = header_with_params({})
273
- Message.any_instance.expects(:process).raises(Exception.new("don't worry"))
274
- @sub.instance_variable_set("@request_stop", true)
275
- @sub.expects(:stop!)
282
+ Message.any_instance.expects(:process).never
283
+ channel = mock("channel")
284
+ channel.expects(:closing?).returns(true)
285
+ @sub.expects(:channel).with(@sub.server).returns(channel)
276
286
  assert_nothing_raised { @callback.call(header, 'foo') }
277
287
  end
278
288
 
279
289
  test "should call reject on the message header when processing the handler returns true on reject?" do
290
+ @handler.expects(:processing_completed).once
280
291
  header = header_with_params({})
281
292
  result = mock("result")
282
293
  result.expects(:reject?).returns(true)
283
294
  Message.any_instance.expects(:process).returns(result)
284
295
  @sub.expects(:sleep).with(1)
296
+ mq = mock("MQ")
297
+ mq.expects(:closing?).returns(false)
298
+ @sub.expects(:channel).with(@sub.server).returns(mq)
285
299
  header.expects(:reject).with(:requeue => true)
286
300
  @callback.call(header, 'foo')
287
301
  end
288
302
 
289
303
  test "should sent a reply with status OK if the message reply_to header is set and processing the handler succeeds" do
304
+ @handler.expects(:processing_completed).once
290
305
  header = header_with_params(:reply_to => "tmp-queue")
291
306
  result = RC::OK
292
307
  Message.any_instance.expects(:process).returns(result)
293
308
  Message.any_instance.expects(:handler_result).returns("response-data")
294
309
  mq = mock("MQ")
295
- @sub.expects(:channel).with(@sub.server).returns(mq)
310
+ mq.expects(:closing?).returns(false)
311
+ @sub.expects(:channel).with(@sub.server).returns(mq).twice
296
312
  exchange = mock("exchange")
297
313
  exchange.expects(:publish).with("response-data", :routing_key => "tmp-queue", :headers => {:status => "OK"}, :persistent => false)
298
314
  AMQP::Exchange.expects(:new).with(mq, :direct, "").returns(exchange)
@@ -300,12 +316,14 @@ module Beetle
300
316
  end
301
317
 
302
318
  test "should sent a reply with status FAILED if the message reply_to header is set and processing the handler fails" do
319
+ @handler.expects(:processing_completed).once
303
320
  header = header_with_params(:reply_to => "tmp-queue")
304
321
  result = RC::AttemptsLimitReached
305
322
  Message.any_instance.expects(:process).returns(result)
306
323
  Message.any_instance.expects(:handler_result).returns(nil)
307
324
  mq = mock("MQ")
308
- @sub.expects(:channel).with(@sub.server).returns(mq)
325
+ mq.expects(:closing?).returns(false)
326
+ @sub.expects(:channel).with(@sub.server).returns(mq).twice
309
327
  exchange = mock("exchange")
310
328
  exchange.expects(:publish).with("", :routing_key => "tmp-queue", :headers => {:status => "FAILED"}, :persistent => false)
311
329
  AMQP::Exchange.expects(:new).with(mq, :direct, "").returns(exchange)
@@ -323,6 +341,9 @@ module Beetle
323
341
  test "subscribe should subscribe with a subscription callback created from the registered block and remember the subscription" do
324
342
  @client.register_queue(:some_queue, :exchange => "some_exchange", :key => "some_key")
325
343
  server = @sub.server
344
+ channel = mock("channel")
345
+ channel.expects(:closing?).returns(false)
346
+ @sub.expects(:channel).with(server).returns(channel)
326
347
  header = header_with_params({})
327
348
  header.expects(:ack)
328
349
  block_called = false
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beetle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Kaes
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2015-08-25 00:00:00.000000000 Z
15
+ date: 2016-02-22 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: uuid4r
@@ -202,12 +202,14 @@ files:
202
202
  - bin/beetle
203
203
  - examples/README.rdoc
204
204
  - examples/attempts.rb
205
+ - examples/consume_many_messages_and_shutdown_randomly.rb
205
206
  - examples/handler_class.rb
206
207
  - examples/handling_exceptions.rb
207
208
  - examples/multiple_exchanges.rb
208
209
  - examples/multiple_queues.rb
209
210
  - examples/nonexistent_server.rb
210
211
  - examples/pause_and_resume.rb
212
+ - examples/publish_many_messages.rb
211
213
  - examples/redundant.rb
212
214
  - examples/rpc.rb
213
215
  - examples/simple.rb
@@ -284,7 +286,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
284
286
  version: 1.3.7
285
287
  requirements: []
286
288
  rubyforge_project:
287
- rubygems_version: 2.4.5
289
+ rubygems_version: 2.4.8
288
290
  signing_key:
289
291
  specification_version: 3
290
292
  summary: High Availability AMQP Messaging with Redundant Queues