beetle 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
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