bunny 2.24.0 → 3.1.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/lib/bunny/channel.rb CHANGED
@@ -153,6 +153,12 @@ module Bunny
153
153
  attr_reader :unconfirmed_set
154
154
  # @return [Set<Integer>] Set of nacked message indexes that have been nacked
155
155
  attr_reader :nacked_set
156
+ # @return [Boolean] true if publisher confirm tracking is enabled
157
+ attr_reader :confirms_tracking_enabled
158
+ # @return [Integer, nil] Maximum outstanding unconfirmed messages before throttling
159
+ attr_reader :outstanding_limit
160
+ # @return [Integer, nil] Timeout in milliseconds for waiting on publisher confirms
161
+ attr_reader :confirm_timeout
156
162
  # @return [Hash<String, Bunny::Consumer>] Consumer instances declared on this channel
157
163
  attr_reader :consumers
158
164
 
@@ -164,12 +170,19 @@ module Bunny
164
170
  attr_reader :cancel_consumers_before_closing
165
171
 
166
172
  DEFAULT_CONTENT_TYPE = "application/octet-stream".freeze
173
+
174
+ # Default outstanding limit for publisher confirms with tracking.
175
+ # Batch size of 1000 provides optimal throughput per benchmarks.
176
+ DEFAULT_OUTSTANDING_CONFIRMS_LIMIT = 1000
167
177
  SHORTSTR_LIMIT = 255
168
178
 
169
179
  # @param [Bunny::Session] connection AMQP 0.9.1 connection
170
180
  # @param [Integer] id Channel id, pass nil to make Bunny automatically allocate it
171
- # @param [Bunny::ConsumerWorkPool] work_pool Thread pool for delivery processing, by default of size 1
172
- def initialize(connection = nil, id = nil, work_pool = ConsumerWorkPool.new(1))
181
+ # @param [HashMap] opts Additional options
182
+ # @option opts [Bunny::ConsumerWorkPool] work_pool Thread pool for delivery processing, by default of size 1
183
+ def initialize(connection = nil, id = nil, opts = {})
184
+ work_pool = opts.fetch(:work_pool, ConsumerWorkPool.new(1))
185
+
173
186
  @connection = connection
174
187
  @logger = connection.logger
175
188
  @id = id || @connection.next_channel_id
@@ -185,7 +198,6 @@ module Bunny
185
198
  end
186
199
 
187
200
  @status = :opening
188
-
189
201
  @connection.register_channel(self)
190
202
 
191
203
  @queues = Hash.new
@@ -202,6 +214,15 @@ module Bunny
202
214
 
203
215
  @unconfirmed_set_mutex = @connection.mutex_impl.new
204
216
 
217
+ # Publisher confirm tracking (initialized before reset_continuations)
218
+ @confirms_tracking_enabled = false
219
+ @outstanding_limit = nil
220
+ @confirm_timeout = nil
221
+ @throttle_publishes = false
222
+ @per_message_continuations = {}
223
+ @per_message_continuations_mutex = @connection.mutex_impl.new
224
+ @outstanding_limit_cond = nil
225
+
205
226
  self.reset_continuations
206
227
 
207
228
  # threads awaiting on continuations. Used to unblock
@@ -214,15 +235,15 @@ module Bunny
214
235
  @next_publish_seq_no = 0
215
236
  @delivery_tag_offset = 0
216
237
 
217
- @recoveries_counter = Bunny::Concurrent::AtomicFixnum.new(0)
218
238
  @uncaught_exception_handler = Proc.new do |e, consumer|
219
239
  @logger.error "Uncaught exception from consumer #{consumer.to_s}: #{e.inspect} @ #{e.backtrace[0]}"
220
240
  end
221
241
 
222
242
  @cancel_consumers_before_closing = false
223
- end
224
243
 
225
- attr_reader :recoveries_counter
244
+ @last_consumer_tag = nil
245
+ @last_consumer = nil
246
+ end
226
247
 
227
248
  # @private
228
249
  def wait_on_continuations_timeout
@@ -240,6 +261,7 @@ module Bunny
240
261
  @connection.open_channel(self)
241
262
  # clear last channel error
242
263
  @last_channel_error = nil
264
+ @frame_max = @connection.frame_max
243
265
 
244
266
  @status = :open
245
267
 
@@ -276,6 +298,35 @@ module Bunny
276
298
  maybe_kill_consumer_work_pool!
277
299
  end
278
300
 
301
+ # Reopens a channel that was closed by the server (e.g. due to a consumer
302
+ # delivery acknowledgement timeout). The channel is reopened on the same
303
+ # connection, reusing its original channel id, and its prefetch, confirm,
304
+ # and transactional settings are recovered.
305
+ #
306
+ # This does NOT recover topology (queues, exchanges, bindings, consumers).
307
+ # Use {Bunny::Session#recover_channel_topology} for that.
308
+ #
309
+ # @return [Bunny::Channel] self
310
+ # @see Bunny::Session#recover_channel_topology
311
+ # @api public
312
+ def reopen
313
+ raise "Cannot reopen a channel that is not closed" unless closed?
314
+
315
+ existing = @connection.synchronised_find_channel(@id)
316
+ if existing && existing != self
317
+ raise "Channel id #{@id} has been reassigned to another channel"
318
+ end
319
+
320
+ @work_pool = ConsumerWorkPool.new(@work_pool.size, @work_pool.abort_on_exception)
321
+ @work_pool.start
322
+
323
+ open
324
+
325
+ recover_from_network_failure
326
+
327
+ self
328
+ end
329
+
279
330
  # @return [Boolean] true if this channel is open, false otherwise
280
331
  # @api public
281
332
  def open?
@@ -288,6 +339,21 @@ module Bunny
288
339
  @status == :closed
289
340
  end
290
341
 
342
+ # @private
343
+ def connection_closed!
344
+ @status = :closed
345
+ end
346
+
347
+ # @private
348
+ def recovering!
349
+ @status = :recovering
350
+ end
351
+
352
+ # @private
353
+ def recovery_completed!
354
+ @status = :open
355
+ end
356
+
291
357
  #
292
358
  # @group Backwards compatibility with 0.8.0
293
359
  #
@@ -423,7 +489,7 @@ module Bunny
423
489
  # @param [String] name Exchange name
424
490
  # @param [Hash] opts Exchange parameters
425
491
  #
426
- # @option opts [String,Symbol] :type (:direct) Exchange type, e.g. :fanout or "x-consistent-hash"
492
+ # @option opts [String,Symbol] :type (:direct) Exchange type, e.g. :fanout or "x-consistent-hash" or "x-modulus-hash"
427
493
  # @option opts [Boolean] :durable (false) Should the exchange be durable?
428
494
  # @option opts [Boolean] :auto_delete (false) Should the exchange be automatically deleted when no longer in use?
429
495
  # @option opts [Hash] :arguments ({}) Optional exchange arguments
@@ -459,6 +525,7 @@ module Bunny
459
525
 
460
526
  q = find_queue(name) || Bunny::Queue.new(self, name, opts)
461
527
 
528
+ record_queue(q)
462
529
  register_queue(q)
463
530
  end
464
531
 
@@ -502,6 +569,67 @@ module Bunny
502
569
  durable_queue(name, Bunny::Queue::Types::STREAM, opts)
503
570
  end
504
571
 
572
+ # Declares a Tanzu RabbitMQ delayed queue (a durable, replicated queue type).
573
+ # This queue type must be durable, non-exclusive, and non-auto-delete.
574
+ #
575
+ # @param [String] name Queue name. Empty (server-generated) names are not supported by this method.
576
+ # @param [Hash] opts Queue properties
577
+ #
578
+ # @option opts [String] :delayed_retry_type ("all") Retry strategy: "all", "failed", or "returned"
579
+ # @option opts [Integer] :delayed_retry_min (nil) Minimum retry delay in milliseconds
580
+ # @option opts [Integer] :delayed_retry_max (nil) Maximum retry delay in milliseconds
581
+ # @option opts [Hash] :arguments ({}) Optional arguments (x-arguments)
582
+ #
583
+ # @return [Bunny::Queue] Queue that was declared
584
+ # @see #durable_queue
585
+ # @see #queue
586
+ # @api public
587
+ def delayed_queue(name, opts = {})
588
+ throw ArgumentError.new("delayed queue name must not be nil") if name.nil?
589
+ throw ArgumentError.new("delayed queue name must not be empty") if name.empty?
590
+
591
+ args = opts[:arguments] || {}
592
+ args[Bunny::Queue::XArgs::DELAYED_RETRY_TYPE] = opts[:delayed_retry_type] if opts[:delayed_retry_type]
593
+ args[Bunny::Queue::XArgs::DELAYED_RETRY_MIN] = opts[:delayed_retry_min] if opts[:delayed_retry_min]
594
+ args[Bunny::Queue::XArgs::DELAYED_RETRY_MAX] = opts[:delayed_retry_max] if opts[:delayed_retry_max]
595
+
596
+ final_opts = opts.merge(:arguments => args)
597
+ final_opts.delete(:delayed_retry_type)
598
+ final_opts.delete(:delayed_retry_min)
599
+ final_opts.delete(:delayed_retry_max)
600
+
601
+ durable_queue(name, Bunny::Queue::Types::DELAYED, final_opts)
602
+ end
603
+
604
+ # Declares a Tanzu RabbitMQ JMS queue (a durable, replicated queue type).
605
+ # This queue type must be durable, non-exclusive, and non-auto-delete.
606
+ #
607
+ # @param [String] name Queue name. Empty (server-generated) names are not supported by this method.
608
+ # @param [Hash] opts Queue properties
609
+ #
610
+ # @option opts [Array<String>] :selector_fields (nil) Fields available for JMS selector expressions (e.g. ["priority", "region"], or ["*"] for all)
611
+ # @option opts [Integer] :selector_field_max_bytes (nil) Maximum byte size per selector field
612
+ # @option opts [Hash] :arguments ({}) Optional arguments (x-arguments)
613
+ #
614
+ # @return [Bunny::Queue] Queue that was declared
615
+ # @see #durable_queue
616
+ # @see #queue
617
+ # @api public
618
+ def jms_queue(name, opts = {})
619
+ throw ArgumentError.new("JMS queue name must not be nil") if name.nil?
620
+ throw ArgumentError.new("JMS queue name must not be empty") if name.empty?
621
+
622
+ args = opts[:arguments] || {}
623
+ args[Bunny::Queue::XArgs::SELECTOR_FIELDS] = opts[:selector_fields] if opts[:selector_fields]
624
+ args[Bunny::Queue::XArgs::SELECTOR_FIELD_MAX_BYTES] = opts[:selector_field_max_bytes] if opts[:selector_field_max_bytes]
625
+
626
+ final_opts = opts.merge(:arguments => args)
627
+ final_opts.delete(:selector_fields)
628
+ final_opts.delete(:selector_field_max_bytes)
629
+
630
+ durable_queue(name, Bunny::Queue::Types::JMS, final_opts)
631
+ end
632
+
505
633
  # Declares a new server-named queue that is automatically deleted when the
506
634
  # connection is closed.
507
635
  #
@@ -526,6 +654,7 @@ module Bunny
526
654
  })
527
655
  q = find_queue(name) || Bunny::Queue.new(self, name, final_opts)
528
656
 
657
+ record_queue(q)
529
658
  register_queue(q)
530
659
  end
531
660
 
@@ -660,10 +789,25 @@ module Bunny
660
789
  opts[:content_type] ||= DEFAULT_CONTENT_TYPE
661
790
  opts[:priority] ||= 0
662
791
 
792
+ seq_no = nil
793
+ continuation = nil
794
+
663
795
  if @next_publish_seq_no > 0
664
796
  @unconfirmed_set_mutex.synchronize do
665
- @unconfirmed_set.add(@next_publish_seq_no)
797
+ # With outstanding_limit: wait for slot if at the limit
798
+ wait_for_outstanding_slot_locked if @throttle_publishes
799
+
800
+ seq_no = @next_publish_seq_no
801
+ @unconfirmed_set.add(seq_no)
666
802
  @next_publish_seq_no += 1
803
+
804
+ # Only create per-message continuation when blocking individually (no limit)
805
+ if @confirms_tracking_enabled && !@throttle_publishes
806
+ continuation = new_continuation
807
+ @per_message_continuations_mutex.synchronize do
808
+ @per_message_continuations[seq_no] = continuation
809
+ end
810
+ end
667
811
  end
668
812
  end
669
813
 
@@ -674,9 +818,90 @@ module Bunny
674
818
  routing_key,
675
819
  opts[:mandatory],
676
820
  false,
677
- @connection.frame_max)
821
+ @frame_max)
678
822
  @connection.send_frameset(frames, self)
679
823
 
824
+ wait_for_publish_confirm(seq_no, continuation) if continuation
825
+
826
+ self
827
+ end
828
+
829
+ # Publishes multiple messages in a batch with a single mutex acquisition.
830
+ # More efficient than calling basic_publish repeatedly when using publisher
831
+ # confirms with tracking. Recommended batch sizes: 500-3000.
832
+ #
833
+ # @param [Array<String>] payloads Array of message payloads to publish
834
+ # @param [String, Bunny::Exchange] exchange Exchange name or object
835
+ # @param [String] routing_key Routing key
836
+ # @param [Hash] opts Publishing options (applied to all messages)
837
+ # @return [self]
838
+ #
839
+ # @example Batch publishing with confirms
840
+ # ch.confirm_select(tracking: true)
841
+ # messages = 100.times.map { |i| "message #{i}" }
842
+ # ch.basic_publish_batch(messages, "", queue.name)
843
+ #
844
+ # @api public
845
+ def basic_publish_batch(payloads, exchange, routing_key, opts = {})
846
+ raise_if_no_longer_open!
847
+ raise ArgumentError, "payloads must be an Array" unless payloads.is_a?(Array)
848
+ raise ArgumentError, "routing key cannot be longer than #{SHORTSTR_LIMIT} characters" if routing_key && routing_key.size > SHORTSTR_LIMIT
849
+ return self if payloads.empty?
850
+
851
+ exchange_name = if exchange.respond_to?(:name)
852
+ exchange.name
853
+ else
854
+ exchange
855
+ end
856
+
857
+ mode = opts.fetch(:persistent, true) ? 2 : 1
858
+ opts = opts.dup
859
+ opts[:delivery_mode] ||= mode
860
+ opts[:content_type] ||= DEFAULT_CONTENT_TYPE
861
+ opts[:priority] ||= 0
862
+
863
+ batch_size = payloads.size
864
+
865
+ if @next_publish_seq_no > 0
866
+ @unconfirmed_set_mutex.synchronize do
867
+ # With throttling: wait until we have room for the batch
868
+ if @throttle_publishes
869
+ limit = @outstanding_limit
870
+ target = [limit - batch_size, 0].max
871
+ timeout_sec = (@confirm_timeout || @connection.continuation_timeout) / 1000.0
872
+ deadline = nil
873
+
874
+ while @unconfirmed_set.size > target
875
+ raise_if_no_longer_open!
876
+ deadline ||= Bunny::Timestamp.monotonic + timeout_sec
877
+ remaining = deadline - Bunny::Timestamp.monotonic
878
+ raise Timeout::Error, "Timed out waiting for publisher confirms (batch: #{batch_size}, limit: #{limit})" if remaining <= 0
879
+ @outstanding_limit_cond.wait(remaining)
880
+ end
881
+ end
882
+
883
+ # Register all sequence numbers at once
884
+ start_seq = @next_publish_seq_no
885
+ batch_size.times { |i| @unconfirmed_set.add(start_seq + i) }
886
+ @next_publish_seq_no = start_seq + batch_size
887
+ end
888
+ end
889
+
890
+ # Encode all messages into a single buffer and write once
891
+ data = +""
892
+ payloads.each do |payload|
893
+ frames = AMQ::Protocol::Basic::Publish.encode(@id,
894
+ payload,
895
+ opts,
896
+ exchange_name,
897
+ routing_key,
898
+ opts[:mandatory],
899
+ false,
900
+ @frame_max)
901
+ frames.each { |frame| data << frame.encode }
902
+ end
903
+ @connection.send_raw_without_timeout(data, self)
904
+
680
905
  self
681
906
  end
682
907
 
@@ -753,19 +978,19 @@ module Bunny
753
978
  # @see Bunny::Channel#prefetch
754
979
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
755
980
  # @api public
756
- def basic_qos(count, global = false)
757
- raise ArgumentError.new("prefetch count must be a positive integer, given: #{count}") if count < 0
758
- raise ArgumentError.new("prefetch count must be no greater than #{MAX_PREFETCH_COUNT}, given: #{count}") if count > MAX_PREFETCH_COUNT
981
+ def basic_qos(prefetch_count, global = false)
982
+ raise ArgumentError.new("prefetch count must be a positive integer, given: #{prefetch_count}") if prefetch_count < 0
983
+ raise ArgumentError.new("prefetch count must be no greater than #{MAX_PREFETCH_COUNT}, given: #{prefetch_count}") if prefetch_count > MAX_PREFETCH_COUNT
759
984
  raise_if_no_longer_open!
760
985
 
761
- @connection.send_frame(AMQ::Protocol::Basic::Qos.encode(@id, 0, count, global))
986
+ @connection.send_frame(AMQ::Protocol::Basic::Qos.encode(@id, 0, prefetch_count, global))
762
987
 
763
988
  with_continuation_timeout do
764
989
  @last_basic_qos_ok = wait_on_continuations
765
990
  end
766
991
  raise_if_continuation_resulted_in_a_channel_error!
767
992
 
768
- @prefetch_count = count
993
+ @prefetch_count = prefetch_count
769
994
  @prefetch_global = global
770
995
 
771
996
  @last_basic_qos_ok
@@ -828,12 +1053,10 @@ module Bunny
828
1053
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
829
1054
  # @api public
830
1055
  def basic_reject(delivery_tag, requeue = false)
831
- guarding_against_stale_delivery_tags(delivery_tag) do
832
- raise_if_no_longer_open!
833
- @connection.send_frame(AMQ::Protocol::Basic::Reject.encode(@id, delivery_tag, requeue))
1056
+ raise_if_no_longer_open!
1057
+ @connection.send_frame(AMQ::Protocol::Basic::Reject.encode(@id, delivery_tag, requeue))
834
1058
 
835
- nil
836
- end
1059
+ nil
837
1060
  end
838
1061
 
839
1062
  # Acknowledges a delivery (message).
@@ -952,7 +1175,7 @@ module Bunny
952
1175
  # Registers a consumer for queue. Delivered messages will be handled with the block
953
1176
  # provided to this method.
954
1177
  #
955
- # @param [String, Bunny::Queue] queue Queue to consume from
1178
+ # @param [String] queue Queue to consume from
956
1179
  # @param [String] consumer_tag Consumer tag (unique identifier), generated by Bunny by default
957
1180
  # @param [Boolean] no_ack (false) If true, delivered messages will be automatically acknowledged.
958
1181
  # If false, manual acknowledgements will be necessary.
@@ -975,7 +1198,7 @@ module Bunny
975
1198
  # helps avoid race condition between basic.consume-ok and basic.deliver if there are messages
976
1199
  # in the queue already. MK.
977
1200
  if consumer_tag && consumer_tag.strip != AMQ::Protocol::EMPTY_STRING
978
- add_consumer(queue_name, consumer_tag, no_ack, exclusive, arguments, &block)
1201
+ add_consumer(queue_name, consumer_tag, no_ack, exclusive, arguments || {}, &block)
979
1202
  end
980
1203
 
981
1204
  @connection.send_frame(AMQ::Protocol::Basic::Consume.encode(@id,
@@ -995,6 +1218,8 @@ module Bunny
995
1218
  # if basic.consume-ok never arrives, unregister the proactively
996
1219
  # registered consumer. MK.
997
1220
  unregister_consumer(@last_basic_consume_ok.consumer_tag)
1221
+ # #add_consumer records a consumer, make sure to undo it here. MK.
1222
+ delete_recorded_consumer(@last_basic_consume_ok.consumer_tag)
998
1223
 
999
1224
  raise e
1000
1225
  end
@@ -1004,7 +1229,7 @@ module Bunny
1004
1229
  raise_if_channel_close!(@last_basic_consume_ok)
1005
1230
 
1006
1231
  # covers server-generated consumer tags
1007
- add_consumer(queue_name, @last_basic_consume_ok.consumer_tag, no_ack, exclusive, arguments, &block)
1232
+ add_consumer(queue_name, @last_basic_consume_ok.consumer_tag, no_ack, exclusive, arguments || {}, &block)
1008
1233
 
1009
1234
  @last_basic_consume_ok
1010
1235
  end
@@ -1055,6 +1280,12 @@ module Bunny
1055
1280
 
1056
1281
  # covers server-generated consumer tags
1057
1282
  register_consumer(@last_basic_consume_ok.consumer_tag, consumer)
1283
+ record_consumer_with(self, @last_basic_consume_ok.consumer_tag,
1284
+ consumer.queue_name,
1285
+ consumer,
1286
+ consumer.manual_acknowledgement?,
1287
+ consumer.exclusive,
1288
+ consumer.arguments)
1058
1289
 
1059
1290
  raise_if_continuation_resulted_in_a_channel_error!
1060
1291
 
@@ -1066,12 +1297,12 @@ module Bunny
1066
1297
  # it was on is auto-deleted and this consumer was the last one, the queue will be deleted.
1067
1298
  #
1068
1299
  # @param [String] consumer_tag Consumer tag (unique identifier) to cancel
1069
- # @param [Hash] arguments ({}) Optional arguments
1300
+ # @param [Hash] opts ({}) Optional arguments
1070
1301
  #
1071
1302
  # @option opts [Boolean] :no_wait (false) if set to true, this method won't receive a response and will
1072
1303
  # immediately return nil
1073
1304
  #
1074
- # @return [AMQ::Protocol::Basic::CancelOk] RabbitMQ response or nil, if the no_wait option is used
1305
+ # @return [AMQ::Protocol::Basic::CancelOk, nil] RabbitMQ response or nil, if the no_wait option is used
1075
1306
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
1076
1307
  # @api public
1077
1308
  def basic_cancel(consumer_tag, opts = {})
@@ -1089,6 +1320,7 @@ module Bunny
1089
1320
  # reduces thread usage for channels that don't have any
1090
1321
  # consumers
1091
1322
  @work_pool.shutdown(true) unless self.any_consumers?
1323
+ self.delete_recorded_consumer(consumer_tag)
1092
1324
 
1093
1325
  @last_basic_cancel_ok
1094
1326
  end
@@ -1123,6 +1355,26 @@ module Bunny
1123
1355
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
1124
1356
  # @api public
1125
1357
  def queue_declare(name, opts = {})
1358
+ # strip trailing new line and carriage returns
1359
+ # just like RabbitMQ does
1360
+ safe_name = name.gsub(/[\r\n]/, "")
1361
+ is_server_named = (safe_name == AMQ::Protocol::EMPTY_STRING)
1362
+ passive = opts.fetch(:passive, false)
1363
+ durable = opts.fetch(:durable, false)
1364
+ exclusive = opts.fetch(:exclusive, false)
1365
+ auto_delete = opts.fetch(:auto_delete, false)
1366
+ args = opts[:arguments]
1367
+
1368
+ result = self.queue_declare_without_recording_topology(name, opts)
1369
+ self.record_queue_with(self, result.queue, is_server_named, durable, exclusive, auto_delete, args) unless passive
1370
+
1371
+ result
1372
+ end
1373
+
1374
+ # We need this bypassing topology version to avoid modifying the collections
1375
+ # as we iterate over them during topology recovery.
1376
+ # @private
1377
+ def queue_declare_without_recording_topology(name, opts = {})
1126
1378
  raise_if_no_longer_open!
1127
1379
 
1128
1380
  Bunny::Queue.verify_type!(opts[:arguments]) if opts[:arguments]
@@ -1130,16 +1382,24 @@ module Bunny
1130
1382
  # strip trailing new line and carriage returns
1131
1383
  # just like RabbitMQ does
1132
1384
  safe_name = name.gsub(/[\r\n]/, "")
1385
+ is_server_named = (safe_name == AMQ::Protocol::EMPTY_STRING)
1133
1386
  @pending_queue_declare_name = safe_name
1387
+
1388
+ passive = opts.fetch(:passive, false)
1389
+ durable = opts.fetch(:durable, false)
1390
+ exclusive = opts.fetch(:exclusive, false)
1391
+ auto_delete = opts.fetch(:auto_delete, false)
1392
+ args = opts[:arguments]
1393
+
1134
1394
  @connection.send_frame(
1135
1395
  AMQ::Protocol::Queue::Declare.encode(@id,
1136
1396
  @pending_queue_declare_name,
1137
- opts.fetch(:passive, false),
1138
- opts.fetch(:durable, false),
1139
- opts.fetch(:exclusive, false),
1140
- opts.fetch(:auto_delete, false),
1397
+ passive,
1398
+ durable,
1399
+ exclusive,
1400
+ auto_delete,
1141
1401
  false,
1142
- opts[:arguments]))
1402
+ args))
1143
1403
 
1144
1404
  begin
1145
1405
  with_continuation_timeout do
@@ -1177,6 +1437,8 @@ module Bunny
1177
1437
  @last_queue_delete_ok = wait_on_continuations
1178
1438
  end
1179
1439
  raise_if_continuation_resulted_in_a_channel_error!
1440
+ self.delete_recorded_queue_named(name)
1441
+ self.deregister_queue_named(name)
1180
1442
 
1181
1443
  @last_queue_delete_ok
1182
1444
  end
@@ -1217,23 +1479,48 @@ module Bunny
1217
1479
  def queue_bind(name, exchange, opts = {})
1218
1480
  raise_if_no_longer_open!
1219
1481
 
1482
+ exchange_name = if exchange.respond_to?(:name)
1483
+ exchange.name
1484
+ else
1485
+ exchange
1486
+ end
1487
+ rk = (opts[:routing_key] || opts[:key])
1488
+ args = opts[:arguments]
1489
+
1490
+ result = self.queue_bind_without_recording_topology(name, exchange, opts)
1491
+ self.record_queue_binding_with(self, exchange_name, name, rk, args)
1492
+
1493
+ result
1494
+ end
1495
+
1496
+ # We need this bypassing topology version to avoid modifying the collections
1497
+ # as we iterate over them during topology recovery.
1498
+ # @private
1499
+ def queue_bind_without_recording_topology(name, exchange, opts = {})
1500
+ raise_if_no_longer_open!
1501
+
1220
1502
  exchange_name = if exchange.respond_to?(:name)
1221
1503
  exchange.name
1222
1504
  else
1223
1505
  exchange
1224
1506
  end
1225
1507
 
1508
+ rk = (opts[:routing_key] || opts[:key])
1509
+ args = opts[:arguments]
1226
1510
  @connection.send_frame(AMQ::Protocol::Queue::Bind.encode(@id,
1227
- name,
1228
- exchange_name,
1229
- (opts[:routing_key] || opts[:key]),
1230
- false,
1231
- opts[:arguments]))
1511
+ name,
1512
+ exchange_name,
1513
+ rk,
1514
+ false,
1515
+ args))
1516
+
1232
1517
  with_continuation_timeout do
1233
1518
  @last_queue_bind_ok = wait_on_continuations
1234
1519
  end
1235
1520
 
1236
1521
  raise_if_continuation_resulted_in_a_channel_error!
1522
+
1523
+
1237
1524
  @last_queue_bind_ok
1238
1525
  end
1239
1526
 
@@ -1259,16 +1546,20 @@ module Bunny
1259
1546
  exchange
1260
1547
  end
1261
1548
 
1549
+ rk = (opts[:routing_key] || opts[:key])
1550
+ args = opts[:arguments]
1262
1551
  @connection.send_frame(AMQ::Protocol::Queue::Unbind.encode(@id,
1263
1552
  name,
1264
1553
  exchange_name,
1265
- opts[:routing_key],
1266
- opts[:arguments]))
1554
+ rk,
1555
+ args))
1267
1556
  with_continuation_timeout do
1268
1557
  @last_queue_unbind_ok = wait_on_continuations
1269
1558
  end
1270
1559
 
1271
1560
  raise_if_continuation_resulted_in_a_channel_error!
1561
+ self.delete_recorded_queue_binding(self, exchange_name, name, rk, args)
1562
+
1272
1563
  @last_queue_unbind_ok
1273
1564
  end
1274
1565
 
@@ -1294,25 +1585,65 @@ module Bunny
1294
1585
  # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
1295
1586
  # @api public
1296
1587
  def exchange_declare(name, type, opts = {})
1588
+ result = self.exchange_declare_without_recording_topology(name, type, opts)
1589
+
1590
+ # strip trailing new line and carriage returns
1591
+ # just like RabbitMQ does
1592
+ safe_name = name.gsub(/[\r\n]/, "")
1593
+ passive = opts.fetch(:passive, false)
1594
+ durable = opts.fetch(:durable, false)
1595
+ auto_delete = opts.fetch(:auto_delete, false)
1596
+ args = opts[:arguments]
1597
+ self.record_exchange_with(self,
1598
+ safe_name,
1599
+ type.to_s,
1600
+ durable,
1601
+ auto_delete,
1602
+ args) unless passive
1603
+
1604
+ result
1605
+ end
1606
+
1607
+ # We need this bypassing topology version to avoid modifying the collections
1608
+ # as we iterate over them during topology recovery.
1609
+ #
1610
+ # @param [String] name The name of the exchange. Note that LF and CR characters
1611
+ # will be stripped from the value.
1612
+ # @param [String,Symbol] type Exchange type, e.g. :fanout or :topic
1613
+ # @param [Hash] opts Exchange properties
1614
+ #
1615
+ # @option opts [Boolean] durable (false) Should information about this exchange be persisted to disk so that it
1616
+ # can survive broker restarts? Typically set to true for long-lived exchanges.
1617
+ # @option opts [Boolean] auto_delete (false) Should this exchange be deleted when it is no longer used?
1618
+ # @option opts [Boolean] passive (false) If true, exchange will be checked for existence. If it does not
1619
+ # exist, {Bunny::NotFound} will be raised.
1620
+ # @private
1621
+ def exchange_declare_without_recording_topology(name, type, opts = {})
1297
1622
  raise_if_no_longer_open!
1298
1623
 
1299
1624
  # strip trailing new line and carriage returns
1300
1625
  # just like RabbitMQ does
1301
1626
  safe_name = name.gsub(/[\r\n]/, "")
1627
+ passive = opts.fetch(:passive, false)
1628
+ durable = opts.fetch(:durable, false)
1629
+ auto_delete = opts.fetch(:auto_delete, false)
1630
+ args = opts[:arguments]
1631
+
1302
1632
  @connection.send_frame(AMQ::Protocol::Exchange::Declare.encode(@id,
1303
1633
  safe_name,
1304
1634
  type.to_s,
1305
- opts.fetch(:passive, false),
1306
- opts.fetch(:durable, false),
1307
- opts.fetch(:auto_delete, false),
1635
+ passive,
1636
+ durable,
1637
+ auto_delete,
1308
1638
  opts.fetch(:internal, false),
1309
1639
  opts.fetch(:no_wait, false),
1310
- opts[:arguments]))
1640
+ args))
1311
1641
  with_continuation_timeout do
1312
1642
  @last_exchange_declare_ok = wait_on_continuations
1313
1643
  end
1314
1644
 
1315
1645
  raise_if_continuation_resulted_in_a_channel_error!
1646
+
1316
1647
  @last_exchange_declare_ok
1317
1648
  end
1318
1649
 
@@ -1338,6 +1669,9 @@ module Bunny
1338
1669
  end
1339
1670
 
1340
1671
  raise_if_continuation_resulted_in_a_channel_error!
1672
+ self.delete_recorded_exchange_named(name)
1673
+ self.deregister_exchange_named(name)
1674
+
1341
1675
  @last_exchange_delete_ok
1342
1676
  end
1343
1677
 
@@ -1357,6 +1691,29 @@ module Bunny
1357
1691
  # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
1358
1692
  # @api public
1359
1693
  def exchange_bind(source, destination, opts = {})
1694
+ result = self.exchange_bind_without_recording_topology(source, destination, opts)
1695
+
1696
+ source_name = if source.respond_to?(:name)
1697
+ source.name
1698
+ else
1699
+ source
1700
+ end
1701
+ destination_name = if destination.respond_to?(:name)
1702
+ destination.name
1703
+ else
1704
+ destination
1705
+ end
1706
+ rk = (opts[:routing_key] || opts[:key])
1707
+ args = opts[:arguments]
1708
+ self.record_exchange_binding_with(self, source_name, destination_name, rk, args)
1709
+
1710
+ result
1711
+ end
1712
+
1713
+ # We need this bypassing topology version to avoid modifying the collections
1714
+ # as we iterate over them during topology recovery.
1715
+ # @private
1716
+ def exchange_bind_without_recording_topology(source, destination, opts = {})
1360
1717
  raise_if_no_longer_open!
1361
1718
 
1362
1719
  source_name = if source.respond_to?(:name)
@@ -1371,17 +1728,21 @@ module Bunny
1371
1728
  destination
1372
1729
  end
1373
1730
 
1731
+ rk = (opts[:routing_key] || opts[:key])
1732
+ args = opts[:arguments]
1374
1733
  @connection.send_frame(AMQ::Protocol::Exchange::Bind.encode(@id,
1375
1734
  destination_name,
1376
1735
  source_name,
1377
- opts[:routing_key],
1736
+ rk,
1378
1737
  false,
1379
- opts[:arguments]))
1738
+ args))
1380
1739
  with_continuation_timeout do
1381
1740
  @last_exchange_bind_ok = wait_on_continuations
1382
1741
  end
1383
1742
 
1384
1743
  raise_if_continuation_resulted_in_a_channel_error!
1744
+ self.record_exchange_binding_with(self, source_name, destination_name, rk, args)
1745
+
1385
1746
  @last_exchange_bind_ok
1386
1747
  end
1387
1748
 
@@ -1415,17 +1776,21 @@ module Bunny
1415
1776
  destination
1416
1777
  end
1417
1778
 
1779
+ rk = (opts[:routing_key] || opts[:key])
1780
+ args = opts[:arguments]
1418
1781
  @connection.send_frame(AMQ::Protocol::Exchange::Unbind.encode(@id,
1419
1782
  destination_name,
1420
1783
  source_name,
1421
- opts[:routing_key],
1784
+ rk,
1422
1785
  false,
1423
- opts[:arguments]))
1786
+ args))
1424
1787
  with_continuation_timeout do
1425
1788
  @last_exchange_unbind_ok = wait_on_continuations
1426
1789
  end
1427
1790
 
1428
1791
  raise_if_continuation_resulted_in_a_channel_error!
1792
+ self.delete_recorded_exchange_binding(self, source_name, destination_name, rk, args)
1793
+
1429
1794
  @last_exchange_unbind_ok
1430
1795
  end
1431
1796
 
@@ -1528,14 +1893,25 @@ module Bunny
1528
1893
  alias using_publisher_confirms? using_publisher_confirmations?
1529
1894
 
1530
1895
  # Enables publisher confirms for the channel.
1896
+ #
1897
+ # @param [Proc] callback Optional callback invoked for each confirm. Receives (delivery_tag, multiple, nack).
1898
+ # @param [Boolean] tracking When true, basic_publish blocks until the broker confirms receipt.
1899
+ # Raises Bunny::MessageNacked if the message is nacked.
1900
+ # @param [Integer] outstanding_limit Max unconfirmed messages before basic_publish blocks.
1901
+ # Defaults to 1000 when tracking: true (optimal for throughput). Pass explicit value to override.
1902
+ # @param [Integer] confirm_timeout Timeout in ms for confirms. Defaults to connection's continuation_timeout.
1903
+ #
1531
1904
  # @return [AMQ::Protocol::Confirm::SelectOk] RabbitMQ response
1532
1905
  # @see #wait_for_confirms
1533
1906
  # @see #unconfirmed_set
1534
1907
  # @see #nacked_set
1535
1908
  # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
1536
1909
  # @api public
1537
- def confirm_select(callback = nil)
1910
+ def confirm_select(callback = nil, tracking: false, outstanding_limit: nil, confirm_timeout: nil)
1538
1911
  raise_if_no_longer_open!
1912
+ raise ArgumentError, "outstanding_limit requires tracking: true" if outstanding_limit && !tracking
1913
+ raise ArgumentError, "outstanding_limit must be positive" if outstanding_limit && outstanding_limit < 1
1914
+ raise ArgumentError, "confirm_timeout must be positive" if confirm_timeout && confirm_timeout < 1
1539
1915
 
1540
1916
  if @next_publish_seq_no == 0
1541
1917
  @confirms_continuations = new_continuation
@@ -1546,6 +1922,17 @@ module Bunny
1546
1922
  end
1547
1923
 
1548
1924
  @confirms_callback = callback
1925
+ @confirms_tracking_enabled = tracking
1926
+ # Default to optimal limit when tracking enabled (avoids per-message mutex)
1927
+ @outstanding_limit = outstanding_limit || (tracking ? DEFAULT_OUTSTANDING_CONFIRMS_LIMIT : nil)
1928
+ @confirm_timeout = confirm_timeout
1929
+
1930
+ # Cache combined check for fast path in basic_publish
1931
+ @throttle_publishes = tracking && @outstanding_limit
1932
+
1933
+ if @outstanding_limit && !@outstanding_limit_cond
1934
+ @outstanding_limit_cond = @unconfirmed_set_mutex.new_cond
1935
+ end
1549
1936
 
1550
1937
  @connection.send_frame(AMQ::Protocol::Confirm::Select.encode(@id, false))
1551
1938
  with_continuation_timeout do
@@ -1585,9 +1972,9 @@ module Bunny
1585
1972
  #
1586
1973
  # @return [String] Unique string.
1587
1974
  # @api plugin
1588
- def generate_consumer_tag(name = "bunny")
1975
+ def generate_consumer_tag(prefix = "bunny")
1589
1976
  t = Bunny::Timestamp.now
1590
- "#{name}-#{t.to_i * 1000}-#{Kernel.rand(999_999_999_999)}"
1977
+ "#{prefix}-#{t.to_i * 1000}-#{Kernel.rand(999_999_999_999)}"
1591
1978
  end
1592
1979
 
1593
1980
  # @endgroup
@@ -1630,11 +2017,8 @@ module Bunny
1630
2017
  recover_prefetch_setting
1631
2018
  recover_confirm_mode
1632
2019
  recover_tx_mode
1633
- recover_exchanges
1634
- # this includes recovering bindings
1635
- recover_queues
1636
- recover_consumers
1637
- increment_recoveries_counter
2020
+
2021
+ # Topology is now recovered by [Bunny::Session] via the data in [Bunny::TopologyRegistry].
1638
2022
  end
1639
2023
 
1640
2024
  # Recovers basic.qos setting. Used by the Automatic Network Failure
@@ -1651,13 +2035,26 @@ module Bunny
1651
2035
  #
1652
2036
  # @api plugin
1653
2037
  def recover_confirm_mode
1654
- if using_publisher_confirmations?
1655
- @unconfirmed_set_mutex.synchronize do
1656
- @unconfirmed_set.clear
1657
- @delivery_tag_offset = @next_publish_seq_no - 1
2038
+ return unless using_publisher_confirmations?
2039
+
2040
+ @unconfirmed_set_mutex.synchronize do
2041
+ @unconfirmed_set.clear
2042
+ @delivery_tag_offset = @next_publish_seq_no - 1
2043
+
2044
+ if @confirms_tracking_enabled
2045
+ @per_message_continuations_mutex.synchronize do
2046
+ @per_message_continuations.each_value { |c| c.push(:network_error) }
2047
+ @per_message_continuations.clear
2048
+ end
1658
2049
  end
1659
- confirm_select(@confirms_callback)
2050
+
2051
+ @outstanding_limit_cond&.broadcast
1660
2052
  end
2053
+
2054
+ confirm_select(@confirms_callback,
2055
+ tracking: @confirms_tracking_enabled,
2056
+ outstanding_limit: @outstanding_limit,
2057
+ confirm_timeout: @confirm_timeout)
1661
2058
  end
1662
2059
 
1663
2060
  # Recovers transaction mode. Used by the Automatic Network Failure
@@ -1668,45 +2065,31 @@ module Bunny
1668
2065
  tx_select if @tx_mode
1669
2066
  end
1670
2067
 
1671
- # Recovers exchanges. Used by the Automatic Network Failure
2068
+ # Used by the Automatic Network Failure
1672
2069
  # Recovery feature.
1673
2070
  #
1674
- # @api plugin
1675
- def recover_exchanges
1676
- @exchange_mutex.synchronize { @exchanges.values }.each do |x|
1677
- x.recover_from_network_failure
1678
- end
1679
- end
2071
+ # @param [String] old_name
2072
+ # @param [String] new_name
2073
+ # @private
2074
+ def record_queue_name_change(old_name, new_name)
2075
+ @queue_mutex.synchronize do
2076
+ if (orig = @queues[old_name])
2077
+ @queues.delete(old_name)
1680
2078
 
1681
- # Recovers queues and bindings. Used by the Automatic Network Failure
1682
- # Recovery feature.
1683
- #
1684
- # @api plugin
1685
- def recover_queues
1686
- @queue_mutex.synchronize { @queues.values }.each do |q|
1687
- @logger.debug { "Recovering queue #{q.name}" }
1688
- q.recover_from_network_failure
2079
+ orig.update_name_to(new_name)
2080
+ @queues[new_name] = orig.dup
2081
+ end
1689
2082
  end
1690
2083
  end
1691
2084
 
1692
- # Recovers consumers. Used by the Automatic Network Failure
1693
- # Recovery feature.
2085
+ # Used by the Automatic Network Failure Recovery feature.
1694
2086
  #
1695
- # @api plugin
1696
- def recover_consumers
2087
+ # @api private
2088
+ def maybe_reinitialize_consumer_pool!
1697
2089
  unless @consumers.empty?
1698
2090
  @work_pool = ConsumerWorkPool.new(@work_pool.size, @work_pool.abort_on_exception)
1699
2091
  @work_pool.start
1700
2092
  end
1701
-
1702
- @consumer_mutex.synchronize { @consumers.values }.each do |c|
1703
- c.recover_from_network_failure
1704
- end
1705
- end
1706
-
1707
- # @private
1708
- def increment_recoveries_counter
1709
- @recoveries_counter.increment
1710
2093
  end
1711
2094
 
1712
2095
  # @api public
@@ -1745,6 +2128,9 @@ module Bunny
1745
2128
  def register_consumer(consumer_tag, consumer)
1746
2129
  @consumer_mutex.synchronize do
1747
2130
  @consumers[consumer_tag] = consumer
2131
+ if @last_consumer_tag == consumer_tag
2132
+ @last_consumer = consumer
2133
+ end
1748
2134
  end
1749
2135
  end
1750
2136
 
@@ -1752,16 +2138,35 @@ module Bunny
1752
2138
  def unregister_consumer(consumer_tag)
1753
2139
  @consumer_mutex.synchronize do
1754
2140
  @consumers.delete(consumer_tag)
2141
+ if @last_consumer_tag == consumer_tag
2142
+ @last_consumer_tag = nil
2143
+ @last_consumer = nil
2144
+ end
1755
2145
  end
1756
2146
  end
1757
2147
 
2148
+ # @param [String] queue_name
2149
+ # @param [String] consumer_tag
2150
+ # @param [Boolean] no_ack true means automative acknowledgement mode
2151
+ # @param [Boolean] exclusive
2152
+ # @param [Hash] arguments
1758
2153
  # @private
1759
- def add_consumer(queue, consumer_tag, no_ack, exclusive, arguments, &block)
2154
+ def add_consumer(queue_name, consumer_tag, no_ack, exclusive, arguments, &block)
1760
2155
  @consumer_mutex.synchronize do
1761
- c = Consumer.new(self, queue, consumer_tag, no_ack, exclusive, arguments)
2156
+ c = Consumer.new(self, queue_name, consumer_tag, no_ack, exclusive, arguments)
1762
2157
  c.on_delivery(&block) if block
1763
2158
  @consumers[consumer_tag] = c
2159
+ if @last_consumer_tag == consumer_tag
2160
+ @last_consumer = c
2161
+ end
2162
+ c
1764
2163
  end
2164
+ record_consumer_with(self, consumer_tag,
2165
+ queue_name,
2166
+ block,
2167
+ !no_ack,
2168
+ exclusive,
2169
+ arguments)
1765
2170
  end
1766
2171
 
1767
2172
  # @private
@@ -1826,6 +2231,10 @@ module Bunny
1826
2231
  consume_with(consumer)
1827
2232
  else
1828
2233
  @consumers.delete(method.consumer_tag)
2234
+ if @last_consumer_tag == method.consumer_tag
2235
+ @last_consumer_tag = nil
2236
+ @last_consumer = nil
2237
+ end
1829
2238
  consumer.handle_cancellation(method)
1830
2239
  end
1831
2240
  rescue Exception => e
@@ -1839,6 +2248,7 @@ module Bunny
1839
2248
  when AMQ::Protocol::Basic::CancelOk then
1840
2249
  @continuations.push(method)
1841
2250
  unregister_consumer(method.consumer_tag)
2251
+ delete_recorded_consumer(method.consumer_tag)
1842
2252
  when AMQ::Protocol::Tx::SelectOk, AMQ::Protocol::Tx::CommitOk, AMQ::Protocol::Tx::RollbackOk then
1843
2253
  @continuations.push(method)
1844
2254
  when AMQ::Protocol::Tx::SelectOk then
@@ -1855,7 +2265,9 @@ module Bunny
1855
2265
 
1856
2266
  # basic.ack, basic.reject, basic.nack. MK.
1857
2267
  if channel_level_exception_after_operation_that_has_no_response?(method)
1858
- @on_error.call(self, method) if @on_error
2268
+ # Runs outside the reader loop so that the callback can perform
2269
+ # blocking operations such as channel.reopen.
2270
+ Thread.new { @on_error.call(self, method) } if @on_error
1859
2271
  else
1860
2272
  @last_channel_error = instantiate_channel_level_exception(method)
1861
2273
  @continuations.push(method)
@@ -1870,12 +2282,12 @@ module Bunny
1870
2282
 
1871
2283
  # @private
1872
2284
  def channel_level_exception_after_operation_that_has_no_response?(method)
1873
- method.reply_code == 406 && (method.reply_text =~ /unknown delivery tag/ || method.reply_text =~ /delivery acknowledgement on channel \d+ timed out/)
2285
+ method.unknown_delivery_tag? || method.delivery_ack_timeout? || method.message_too_large?
1874
2286
  end
1875
2287
 
1876
2288
  # @private
1877
2289
  def handle_basic_get_ok(basic_get_ok, properties, content)
1878
- basic_get_ok.delivery_tag = VersionedDeliveryTag.new(basic_get_ok.delivery_tag, @recoveries_counter.get)
2290
+ basic_get_ok.delivery_tag = basic_get_ok.delivery_tag
1879
2291
  @basic_get_continuations.push([basic_get_ok, properties, content])
1880
2292
  end
1881
2293
 
@@ -1886,20 +2298,33 @@ module Bunny
1886
2298
 
1887
2299
  # @private
1888
2300
  def handle_frameset(basic_deliver, properties, content)
1889
- consumer = @consumers[basic_deliver.consumer_tag]
2301
+ tag = basic_deliver.consumer_tag
2302
+ if @last_consumer_tag == tag
2303
+ consumer = @last_consumer
2304
+ else
2305
+ consumer = @consumers[tag]
2306
+ @last_consumer_tag = tag
2307
+ @last_consumer = consumer
2308
+ end
2309
+
1890
2310
  if consumer
1891
2311
  @work_pool.submit do
1892
- begin
1893
- consumer.call(DeliveryInfo.new(basic_deliver, consumer, self), MessageProperties.new(properties), content)
1894
- rescue StandardError => e
1895
- @uncaught_exception_handler.call(e, consumer) if @uncaught_exception_handler
1896
- end
2312
+ deliver_to_consumer(consumer, basic_deliver, properties, content)
1897
2313
  end
1898
2314
  else
1899
2315
  @logger.warn "No consumer for tag #{basic_deliver.consumer_tag} on channel #{@id}!"
1900
2316
  end
1901
2317
  end
1902
2318
 
2319
+ # @private
2320
+ def deliver_to_consumer(consumer, basic_deliver, properties, content)
2321
+ begin
2322
+ consumer.call(DeliveryInfo.new(basic_deliver, consumer, self), MessageProperties.new(properties), content)
2323
+ rescue StandardError => e
2324
+ @uncaught_exception_handler.call(e, consumer) if @uncaught_exception_handler
2325
+ end
2326
+ end
2327
+
1903
2328
  # @private
1904
2329
  def handle_basic_return(basic_return, properties, content)
1905
2330
  x = find_exchange(basic_return.exchange)
@@ -1931,6 +2356,22 @@ module Bunny
1931
2356
 
1932
2357
  @confirms_continuations.push(true) if @unconfirmed_set.empty?
1933
2358
 
2359
+ # Signal per-message continuations (only used when tracking without limit)
2360
+ if @confirms_tracking_enabled && !@throttle_publishes
2361
+ to_signal = []
2362
+ @per_message_continuations_mutex.synchronize do
2363
+ @per_message_continuations.each do |tag, cont|
2364
+ to_signal << cont if tag >= confirmed_range_start && tag <= confirmed_range_end
2365
+ end
2366
+ end
2367
+ result = nack ? :nack : :ack
2368
+ to_signal.each { |c| c.push(result) }
2369
+ end
2370
+
2371
+ # Wake publisher(s) waiting for outstanding limit slot
2372
+ # Use broadcast since "multiple" acks can free many slots at once
2373
+ @outstanding_limit_cond&.broadcast if @throttle_publishes
2374
+
1934
2375
  if @confirms_callback
1935
2376
  confirmed_range.each { |tag| @confirms_callback.call(tag, false, nack) }
1936
2377
  end
@@ -2005,21 +2446,63 @@ module Bunny
2005
2446
  end
2006
2447
  end
2007
2448
 
2008
-
2009
- # Releases all continuations. Used by automatic network recovery.
2449
+ # Waits for a slot when outstanding limit is reached.
2450
+ # Assumes @unconfirmed_set_mutex is already held.
2010
2451
  # @private
2011
- def release_all_continuations
2012
- @threads_waiting_on_confirms_continuations.each do |t|
2013
- t.run
2452
+ def wait_for_outstanding_slot_locked
2453
+ limit = @outstanding_limit
2454
+ return if @unconfirmed_set.size < limit
2455
+
2456
+ timeout_sec = (@confirm_timeout || @connection.continuation_timeout) / 1000.0
2457
+ deadline = Bunny::Timestamp.monotonic + timeout_sec
2458
+
2459
+ while @unconfirmed_set.size >= limit
2460
+ raise_if_no_longer_open!
2461
+
2462
+ remaining = deadline - Bunny::Timestamp.monotonic
2463
+ raise Timeout::Error, "Timed out waiting for publisher confirms (limit: #{limit})" if remaining <= 0
2464
+
2465
+ @outstanding_limit_cond.wait(remaining)
2014
2466
  end
2015
- @threads_waiting_on_continuations.each do |t|
2016
- t.run
2467
+ end
2468
+
2469
+ # @private
2470
+ def wait_for_publish_confirm(seq_no, continuation)
2471
+ t = Thread.current
2472
+ @threads_waiting_on_confirms_continuations << t
2473
+
2474
+ begin
2475
+ timeout = @confirm_timeout || @connection.continuation_timeout
2476
+ case continuation.poll(timeout)
2477
+ when :ack, true
2478
+ # confirmed
2479
+ when :nack
2480
+ raise MessageNacked.new("Message #{seq_no} was nacked", seq_no)
2481
+ when :network_error
2482
+ raise NetworkFailure.new("Network failure waiting for confirm", nil)
2483
+ when nil
2484
+ raise Timeout::Error, "Timed out waiting for publisher confirm"
2485
+ end
2486
+ ensure
2487
+ @threads_waiting_on_confirms_continuations.delete(t)
2488
+ @per_message_continuations_mutex.synchronize do
2489
+ @per_message_continuations.delete(seq_no)
2490
+ end
2017
2491
  end
2018
- @threads_waiting_on_basic_get_continuations.each do |t|
2019
- t.run
2492
+ end
2493
+
2494
+
2495
+ # @private
2496
+ def release_all_continuations
2497
+ @threads_waiting_on_confirms_continuations.each(&:run)
2498
+ @threads_waiting_on_continuations.each(&:run)
2499
+ @threads_waiting_on_basic_get_continuations.each(&:run)
2500
+
2501
+ if @outstanding_limit_cond
2502
+ @unconfirmed_set_mutex.synchronize { @outstanding_limit_cond.broadcast }
2020
2503
  end
2021
2504
 
2022
- self.reset_continuations
2505
+ reset_continuations
2023
2506
  end
2024
2507
 
2025
2508
  # Starts consumer work pool. Lazily called by #basic_consume to avoid creating new threads
@@ -2049,39 +2532,167 @@ module Bunny
2049
2532
  @connection.read_next_frame(options = {})
2050
2533
  end
2051
2534
 
2535
+ # @param [String] name
2536
+ # @private
2537
+ def find_queue(name)
2538
+ @queue_mutex.synchronize { @queues[name] }
2539
+ end
2540
+
2541
+ # @param [String] name
2542
+ # @private
2543
+ def find_exchange(name)
2544
+ @exchange_mutex.synchronize { @exchanges[name] }
2545
+ end
2546
+
2547
+ # @param [Bunny::Queue] queue
2548
+ # @private
2549
+ def register_queue(queue)
2550
+ @queue_mutex.synchronize { @queues[queue.name] = queue }
2551
+ end
2552
+
2553
+ # @param [Bunny::Queue] queue
2052
2554
  # @private
2053
2555
  def deregister_queue(queue)
2054
2556
  @queue_mutex.synchronize { @queues.delete(queue.name) }
2055
2557
  end
2056
2558
 
2559
+ # @param [Bunny::String] name
2057
2560
  # @private
2058
2561
  def deregister_queue_named(name)
2059
2562
  @queue_mutex.synchronize { @queues.delete(name) }
2060
2563
  end
2061
2564
 
2565
+ # @param [Bunny::Queue] queue
2062
2566
  # @private
2063
- def register_queue(queue)
2064
- @queue_mutex.synchronize { @queues[queue.name] = queue }
2567
+ def record_queue(queue)
2568
+ @connection.record_queue(queue)
2065
2569
  end
2066
2570
 
2571
+ # @param [Bunny::Channel] ch
2572
+ # @param [String] name
2573
+ # @param [Boolean] server_named
2574
+ # @param [Boolean] durable
2575
+ # @param [Boolean] auto_delete
2576
+ # @param [Boolean] exclusive
2577
+ # @param [Hash] arguments
2578
+ def record_queue_with(ch, name, server_named, durable, auto_delete, exclusive, arguments)
2579
+ @connection.record_queue_with(ch, name, server_named, durable, auto_delete, exclusive, arguments)
2580
+ end
2581
+
2582
+ # @param [Bunny::Queue, Bunny::RecordedQueue] queue
2067
2583
  # @private
2068
- def find_queue(name)
2069
- @queue_mutex.synchronize { @queues[name] }
2584
+ def delete_recoreded_queue(queue)
2585
+ @connection.delete_recorded_queue(queue)
2070
2586
  end
2071
2587
 
2588
+ # @param [String] name
2072
2589
  # @private
2073
- def deregister_exchange(exchange)
2074
- @exchange_mutex.synchronize { @exchanges.delete(exchange.name) }
2590
+ def delete_recorded_queue_named(name)
2591
+ @connection.delete_recorded_queue_named(name)
2075
2592
  end
2076
2593
 
2594
+ # @param [Bunny::Exchange] exchange
2077
2595
  # @private
2078
2596
  def register_exchange(exchange)
2079
2597
  @exchange_mutex.synchronize { @exchanges[exchange.name] = exchange }
2080
2598
  end
2081
2599
 
2600
+ # @param [Bunny::Exchange] exchange
2082
2601
  # @private
2083
- def find_exchange(name)
2084
- @exchange_mutex.synchronize { @exchanges[name] }
2602
+ def deregister_exchange(exchange)
2603
+ @queue_mutex.synchronize { @exchanges.delete(exchange.name) }
2604
+ end
2605
+
2606
+ # @param [String] name
2607
+ # @private
2608
+ def deregister_exchange_named(name)
2609
+ @queue_mutex.synchronize { @exchanges.delete(name) }
2610
+ end
2611
+
2612
+ # @param [Bunny::Exchange] exchange
2613
+ # @private
2614
+ def record_exchange(exchange)
2615
+ @connection.record_exchange(exchange)
2616
+ end
2617
+
2618
+ # @param [Bunny::Channel] ch
2619
+ # @param [String] name
2620
+ # @param [String] type
2621
+ # @param [Boolean] durable
2622
+ # @param [Boolean] auto_delete
2623
+ # @param [Hash] arguments
2624
+ def record_exchange_with(ch, name, type, durable, auto_delete, arguments)
2625
+ @connection.record_exchange_with(ch, name, type, durable, auto_delete, arguments)
2626
+ end
2627
+
2628
+ # @param [Bunny::Exchange] exchange
2629
+ # @private
2630
+ def delete_recorded_exchange(exchange)
2631
+ @connection.delete_recorded_exchange(exchange)
2632
+ end
2633
+
2634
+ # @param [String] name
2635
+ # @private
2636
+ def delete_recorded_exchange_named(name)
2637
+ @connection.delete_recorded_exchange_named(name)
2638
+ end
2639
+
2640
+ # @param [Bunny::Channel] ch
2641
+ # @param [String] exchange_name
2642
+ # @param [String] queue_name
2643
+ # @param [String] routing_key
2644
+ # @param [Hash] arguments
2645
+ # @private
2646
+ def record_queue_binding_with(ch, exchange_name, queue_name, routing_key, arguments)
2647
+ @connection.record_queue_binding_with(ch, exchange_name, queue_name, routing_key, arguments)
2648
+ end
2649
+
2650
+ # @param [Bunny::Channel] ch
2651
+ # @param [String] exchange_name
2652
+ # @param [String] queue_name
2653
+ # @param [String] routing_key
2654
+ # @param [Hash] arguments
2655
+ # @private
2656
+ def delete_recorded_queue_binding(ch, exchange_name, queue_name, routing_key, arguments)
2657
+ @connection.delete_recorded_queue_binding(ch, exchange_name, queue_name, routing_key, arguments)
2658
+ end
2659
+
2660
+ # @param [Bunny::Channel] ch
2661
+ # @param [String] source_name
2662
+ # @param [String] destination_name
2663
+ # @param [String] routing_key
2664
+ # @param [Hash] arguments
2665
+ # @private
2666
+ def record_exchange_binding_with(ch, source_name, destination_name, routing_key, arguments)
2667
+ @connection.record_exchange_binding_with(ch, source_name, destination_name, routing_key, arguments)
2668
+ end
2669
+
2670
+ # @param [Bunny::Channel] ch
2671
+ # @param [String] source_name
2672
+ # @param [String] destination_name
2673
+ # @param [String] routing_key
2674
+ # @param [Hash] arguments
2675
+ # @private
2676
+ def delete_recorded_exchange_binding(ch, source_name, destination_name, routing_key, arguments)
2677
+ @connection.delete_recorded_exchange_binding(ch, source_name, destination_name, routing_key, arguments)
2678
+ end
2679
+
2680
+ # @param [Bunny::Channel] ch
2681
+ # @param [String] consumer_tag
2682
+ # @param [String] queue_name
2683
+ # @param [#call] callable
2684
+ # @param [Boolean] manual_ack
2685
+ # @param [Boolean] exclusive
2686
+ # @param [Hash] arguments
2687
+ # @private
2688
+ def record_consumer_with(ch, consumer_tag, queue_name, callable, manual_ack, exclusive, arguments)
2689
+ @connection.record_consumer_with(ch, consumer_tag, queue_name, callable, manual_ack, exclusive, arguments)
2690
+ end
2691
+
2692
+ # @param [String] consumer_tag
2693
+ # @private
2694
+ def delete_recorded_consumer(consumer_tag)
2695
+ @connection.delete_recorded_consumer(consumer_tag)
2085
2696
  end
2086
2697
 
2087
2698
  protected
@@ -2148,6 +2759,7 @@ module Bunny
2148
2759
  @continuations = new_continuation
2149
2760
  @confirms_continuations = new_continuation
2150
2761
  @basic_get_continuations = new_continuation
2762
+ @per_message_continuations_mutex.synchronize { @per_message_continuations.clear }
2151
2763
  end
2152
2764
 
2153
2765
  # @private
@@ -2158,16 +2770,7 @@ module Bunny
2158
2770
  # @private
2159
2771
  def guarding_against_stale_delivery_tags(tag, &block)
2160
2772
  case tag
2161
- # if a fixnum was passed, execute unconditionally. MK.
2162
- when Integer then
2163
- block.call
2164
- # versioned delivery tags should be checked to avoid
2165
- # sending out stale (invalid) tags after channel was reopened
2166
- # during network failure recovery. MK.
2167
- when VersionedDeliveryTag then
2168
- if !tag.stale?(@recoveries_counter.get)
2169
- block.call
2170
- end
2773
+ when Integer then block.call
2171
2774
  end
2172
2775
  end
2173
2776
  end # Channel