bunny 2.24.0 → 3.0.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?
@@ -423,7 +474,7 @@ module Bunny
423
474
  # @param [String] name Exchange name
424
475
  # @param [Hash] opts Exchange parameters
425
476
  #
426
- # @option opts [String,Symbol] :type (:direct) Exchange type, e.g. :fanout or "x-consistent-hash"
477
+ # @option opts [String,Symbol] :type (:direct) Exchange type, e.g. :fanout or "x-consistent-hash" or "x-modulus-hash"
427
478
  # @option opts [Boolean] :durable (false) Should the exchange be durable?
428
479
  # @option opts [Boolean] :auto_delete (false) Should the exchange be automatically deleted when no longer in use?
429
480
  # @option opts [Hash] :arguments ({}) Optional exchange arguments
@@ -459,6 +510,7 @@ module Bunny
459
510
 
460
511
  q = find_queue(name) || Bunny::Queue.new(self, name, opts)
461
512
 
513
+ record_queue(q)
462
514
  register_queue(q)
463
515
  end
464
516
 
@@ -502,6 +554,67 @@ module Bunny
502
554
  durable_queue(name, Bunny::Queue::Types::STREAM, opts)
503
555
  end
504
556
 
557
+ # Declares a Tanzu RabbitMQ delayed queue (a durable, replicated queue type).
558
+ # This queue type must be durable, non-exclusive, and non-auto-delete.
559
+ #
560
+ # @param [String] name Queue name. Empty (server-generated) names are not supported by this method.
561
+ # @param [Hash] opts Queue properties
562
+ #
563
+ # @option opts [String] :delayed_retry_type ("all") Retry strategy: "all", "failed", or "returned"
564
+ # @option opts [Integer] :delayed_retry_min (nil) Minimum retry delay in milliseconds
565
+ # @option opts [Integer] :delayed_retry_max (nil) Maximum retry delay in milliseconds
566
+ # @option opts [Hash] :arguments ({}) Optional arguments (x-arguments)
567
+ #
568
+ # @return [Bunny::Queue] Queue that was declared
569
+ # @see #durable_queue
570
+ # @see #queue
571
+ # @api public
572
+ def delayed_queue(name, opts = {})
573
+ throw ArgumentError.new("delayed queue name must not be nil") if name.nil?
574
+ throw ArgumentError.new("delayed queue name must not be empty") if name.empty?
575
+
576
+ args = opts[:arguments] || {}
577
+ args[Bunny::Queue::XArgs::DELAYED_RETRY_TYPE] = opts[:delayed_retry_type] if opts[:delayed_retry_type]
578
+ args[Bunny::Queue::XArgs::DELAYED_RETRY_MIN] = opts[:delayed_retry_min] if opts[:delayed_retry_min]
579
+ args[Bunny::Queue::XArgs::DELAYED_RETRY_MAX] = opts[:delayed_retry_max] if opts[:delayed_retry_max]
580
+
581
+ final_opts = opts.merge(:arguments => args)
582
+ final_opts.delete(:delayed_retry_type)
583
+ final_opts.delete(:delayed_retry_min)
584
+ final_opts.delete(:delayed_retry_max)
585
+
586
+ durable_queue(name, Bunny::Queue::Types::DELAYED, final_opts)
587
+ end
588
+
589
+ # Declares a Tanzu RabbitMQ JMS queue (a durable, replicated queue type).
590
+ # This queue type must be durable, non-exclusive, and non-auto-delete.
591
+ #
592
+ # @param [String] name Queue name. Empty (server-generated) names are not supported by this method.
593
+ # @param [Hash] opts Queue properties
594
+ #
595
+ # @option opts [Array<String>] :selector_fields (nil) Fields available for JMS selector expressions (e.g. ["priority", "region"], or ["*"] for all)
596
+ # @option opts [Integer] :selector_field_max_bytes (nil) Maximum byte size per selector field
597
+ # @option opts [Hash] :arguments ({}) Optional arguments (x-arguments)
598
+ #
599
+ # @return [Bunny::Queue] Queue that was declared
600
+ # @see #durable_queue
601
+ # @see #queue
602
+ # @api public
603
+ def jms_queue(name, opts = {})
604
+ throw ArgumentError.new("JMS queue name must not be nil") if name.nil?
605
+ throw ArgumentError.new("JMS queue name must not be empty") if name.empty?
606
+
607
+ args = opts[:arguments] || {}
608
+ args[Bunny::Queue::XArgs::SELECTOR_FIELDS] = opts[:selector_fields] if opts[:selector_fields]
609
+ args[Bunny::Queue::XArgs::SELECTOR_FIELD_MAX_BYTES] = opts[:selector_field_max_bytes] if opts[:selector_field_max_bytes]
610
+
611
+ final_opts = opts.merge(:arguments => args)
612
+ final_opts.delete(:selector_fields)
613
+ final_opts.delete(:selector_field_max_bytes)
614
+
615
+ durable_queue(name, Bunny::Queue::Types::JMS, final_opts)
616
+ end
617
+
505
618
  # Declares a new server-named queue that is automatically deleted when the
506
619
  # connection is closed.
507
620
  #
@@ -526,6 +639,7 @@ module Bunny
526
639
  })
527
640
  q = find_queue(name) || Bunny::Queue.new(self, name, final_opts)
528
641
 
642
+ record_queue(q)
529
643
  register_queue(q)
530
644
  end
531
645
 
@@ -660,10 +774,25 @@ module Bunny
660
774
  opts[:content_type] ||= DEFAULT_CONTENT_TYPE
661
775
  opts[:priority] ||= 0
662
776
 
777
+ seq_no = nil
778
+ continuation = nil
779
+
663
780
  if @next_publish_seq_no > 0
664
781
  @unconfirmed_set_mutex.synchronize do
665
- @unconfirmed_set.add(@next_publish_seq_no)
782
+ # With outstanding_limit: wait for slot if at the limit
783
+ wait_for_outstanding_slot_locked if @throttle_publishes
784
+
785
+ seq_no = @next_publish_seq_no
786
+ @unconfirmed_set.add(seq_no)
666
787
  @next_publish_seq_no += 1
788
+
789
+ # Only create per-message continuation when blocking individually (no limit)
790
+ if @confirms_tracking_enabled && !@throttle_publishes
791
+ continuation = new_continuation
792
+ @per_message_continuations_mutex.synchronize do
793
+ @per_message_continuations[seq_no] = continuation
794
+ end
795
+ end
667
796
  end
668
797
  end
669
798
 
@@ -674,9 +803,90 @@ module Bunny
674
803
  routing_key,
675
804
  opts[:mandatory],
676
805
  false,
677
- @connection.frame_max)
806
+ @frame_max)
678
807
  @connection.send_frameset(frames, self)
679
808
 
809
+ wait_for_publish_confirm(seq_no, continuation) if continuation
810
+
811
+ self
812
+ end
813
+
814
+ # Publishes multiple messages in a batch with a single mutex acquisition.
815
+ # More efficient than calling basic_publish repeatedly when using publisher
816
+ # confirms with tracking. Recommended batch sizes: 500-3000.
817
+ #
818
+ # @param [Array<String>] payloads Array of message payloads to publish
819
+ # @param [String, Bunny::Exchange] exchange Exchange name or object
820
+ # @param [String] routing_key Routing key
821
+ # @param [Hash] opts Publishing options (applied to all messages)
822
+ # @return [self]
823
+ #
824
+ # @example Batch publishing with confirms
825
+ # ch.confirm_select(tracking: true)
826
+ # messages = 100.times.map { |i| "message #{i}" }
827
+ # ch.basic_publish_batch(messages, "", queue.name)
828
+ #
829
+ # @api public
830
+ def basic_publish_batch(payloads, exchange, routing_key, opts = {})
831
+ raise_if_no_longer_open!
832
+ raise ArgumentError, "payloads must be an Array" unless payloads.is_a?(Array)
833
+ raise ArgumentError, "routing key cannot be longer than #{SHORTSTR_LIMIT} characters" if routing_key && routing_key.size > SHORTSTR_LIMIT
834
+ return self if payloads.empty?
835
+
836
+ exchange_name = if exchange.respond_to?(:name)
837
+ exchange.name
838
+ else
839
+ exchange
840
+ end
841
+
842
+ mode = opts.fetch(:persistent, true) ? 2 : 1
843
+ opts = opts.dup
844
+ opts[:delivery_mode] ||= mode
845
+ opts[:content_type] ||= DEFAULT_CONTENT_TYPE
846
+ opts[:priority] ||= 0
847
+
848
+ batch_size = payloads.size
849
+
850
+ if @next_publish_seq_no > 0
851
+ @unconfirmed_set_mutex.synchronize do
852
+ # With throttling: wait until we have room for the batch
853
+ if @throttle_publishes
854
+ limit = @outstanding_limit
855
+ target = [limit - batch_size, 0].max
856
+ timeout_sec = (@confirm_timeout || @connection.continuation_timeout) / 1000.0
857
+ deadline = nil
858
+
859
+ while @unconfirmed_set.size > target
860
+ raise_if_no_longer_open!
861
+ deadline ||= Bunny::Timestamp.monotonic + timeout_sec
862
+ remaining = deadline - Bunny::Timestamp.monotonic
863
+ raise Timeout::Error, "Timed out waiting for publisher confirms (batch: #{batch_size}, limit: #{limit})" if remaining <= 0
864
+ @outstanding_limit_cond.wait(remaining)
865
+ end
866
+ end
867
+
868
+ # Register all sequence numbers at once
869
+ start_seq = @next_publish_seq_no
870
+ batch_size.times { |i| @unconfirmed_set.add(start_seq + i) }
871
+ @next_publish_seq_no = start_seq + batch_size
872
+ end
873
+ end
874
+
875
+ # Encode all messages into a single buffer and write once
876
+ data = +""
877
+ payloads.each do |payload|
878
+ frames = AMQ::Protocol::Basic::Publish.encode(@id,
879
+ payload,
880
+ opts,
881
+ exchange_name,
882
+ routing_key,
883
+ opts[:mandatory],
884
+ false,
885
+ @frame_max)
886
+ frames.each { |frame| data << frame.encode }
887
+ end
888
+ @connection.send_raw_without_timeout(data, self)
889
+
680
890
  self
681
891
  end
682
892
 
@@ -753,19 +963,19 @@ module Bunny
753
963
  # @see Bunny::Channel#prefetch
754
964
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
755
965
  # @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
966
+ def basic_qos(prefetch_count, global = false)
967
+ raise ArgumentError.new("prefetch count must be a positive integer, given: #{prefetch_count}") if prefetch_count < 0
968
+ raise ArgumentError.new("prefetch count must be no greater than #{MAX_PREFETCH_COUNT}, given: #{prefetch_count}") if prefetch_count > MAX_PREFETCH_COUNT
759
969
  raise_if_no_longer_open!
760
970
 
761
- @connection.send_frame(AMQ::Protocol::Basic::Qos.encode(@id, 0, count, global))
971
+ @connection.send_frame(AMQ::Protocol::Basic::Qos.encode(@id, 0, prefetch_count, global))
762
972
 
763
973
  with_continuation_timeout do
764
974
  @last_basic_qos_ok = wait_on_continuations
765
975
  end
766
976
  raise_if_continuation_resulted_in_a_channel_error!
767
977
 
768
- @prefetch_count = count
978
+ @prefetch_count = prefetch_count
769
979
  @prefetch_global = global
770
980
 
771
981
  @last_basic_qos_ok
@@ -828,12 +1038,10 @@ module Bunny
828
1038
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
829
1039
  # @api public
830
1040
  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))
1041
+ raise_if_no_longer_open!
1042
+ @connection.send_frame(AMQ::Protocol::Basic::Reject.encode(@id, delivery_tag, requeue))
834
1043
 
835
- nil
836
- end
1044
+ nil
837
1045
  end
838
1046
 
839
1047
  # Acknowledges a delivery (message).
@@ -952,7 +1160,7 @@ module Bunny
952
1160
  # Registers a consumer for queue. Delivered messages will be handled with the block
953
1161
  # provided to this method.
954
1162
  #
955
- # @param [String, Bunny::Queue] queue Queue to consume from
1163
+ # @param [String] queue Queue to consume from
956
1164
  # @param [String] consumer_tag Consumer tag (unique identifier), generated by Bunny by default
957
1165
  # @param [Boolean] no_ack (false) If true, delivered messages will be automatically acknowledged.
958
1166
  # If false, manual acknowledgements will be necessary.
@@ -975,7 +1183,7 @@ module Bunny
975
1183
  # helps avoid race condition between basic.consume-ok and basic.deliver if there are messages
976
1184
  # in the queue already. MK.
977
1185
  if consumer_tag && consumer_tag.strip != AMQ::Protocol::EMPTY_STRING
978
- add_consumer(queue_name, consumer_tag, no_ack, exclusive, arguments, &block)
1186
+ add_consumer(queue_name, consumer_tag, no_ack, exclusive, arguments || {}, &block)
979
1187
  end
980
1188
 
981
1189
  @connection.send_frame(AMQ::Protocol::Basic::Consume.encode(@id,
@@ -995,6 +1203,8 @@ module Bunny
995
1203
  # if basic.consume-ok never arrives, unregister the proactively
996
1204
  # registered consumer. MK.
997
1205
  unregister_consumer(@last_basic_consume_ok.consumer_tag)
1206
+ # #add_consumer records a consumer, make sure to undo it here. MK.
1207
+ delete_recorded_consumer(@last_basic_consume_ok.consumer_tag)
998
1208
 
999
1209
  raise e
1000
1210
  end
@@ -1004,7 +1214,7 @@ module Bunny
1004
1214
  raise_if_channel_close!(@last_basic_consume_ok)
1005
1215
 
1006
1216
  # covers server-generated consumer tags
1007
- add_consumer(queue_name, @last_basic_consume_ok.consumer_tag, no_ack, exclusive, arguments, &block)
1217
+ add_consumer(queue_name, @last_basic_consume_ok.consumer_tag, no_ack, exclusive, arguments || {}, &block)
1008
1218
 
1009
1219
  @last_basic_consume_ok
1010
1220
  end
@@ -1055,6 +1265,12 @@ module Bunny
1055
1265
 
1056
1266
  # covers server-generated consumer tags
1057
1267
  register_consumer(@last_basic_consume_ok.consumer_tag, consumer)
1268
+ record_consumer_with(self, @last_basic_consume_ok.consumer_tag,
1269
+ consumer.queue_name,
1270
+ consumer,
1271
+ consumer.manual_acknowledgement?,
1272
+ consumer.exclusive,
1273
+ consumer.arguments)
1058
1274
 
1059
1275
  raise_if_continuation_resulted_in_a_channel_error!
1060
1276
 
@@ -1066,12 +1282,12 @@ module Bunny
1066
1282
  # it was on is auto-deleted and this consumer was the last one, the queue will be deleted.
1067
1283
  #
1068
1284
  # @param [String] consumer_tag Consumer tag (unique identifier) to cancel
1069
- # @param [Hash] arguments ({}) Optional arguments
1285
+ # @param [Hash] opts ({}) Optional arguments
1070
1286
  #
1071
1287
  # @option opts [Boolean] :no_wait (false) if set to true, this method won't receive a response and will
1072
1288
  # immediately return nil
1073
1289
  #
1074
- # @return [AMQ::Protocol::Basic::CancelOk] RabbitMQ response or nil, if the no_wait option is used
1290
+ # @return [AMQ::Protocol::Basic::CancelOk, nil] RabbitMQ response or nil, if the no_wait option is used
1075
1291
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
1076
1292
  # @api public
1077
1293
  def basic_cancel(consumer_tag, opts = {})
@@ -1089,6 +1305,7 @@ module Bunny
1089
1305
  # reduces thread usage for channels that don't have any
1090
1306
  # consumers
1091
1307
  @work_pool.shutdown(true) unless self.any_consumers?
1308
+ self.delete_recorded_consumer(consumer_tag)
1092
1309
 
1093
1310
  @last_basic_cancel_ok
1094
1311
  end
@@ -1123,6 +1340,26 @@ module Bunny
1123
1340
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
1124
1341
  # @api public
1125
1342
  def queue_declare(name, opts = {})
1343
+ # strip trailing new line and carriage returns
1344
+ # just like RabbitMQ does
1345
+ safe_name = name.gsub(/[\r\n]/, "")
1346
+ is_server_named = (safe_name == AMQ::Protocol::EMPTY_STRING)
1347
+ passive = opts.fetch(:passive, false)
1348
+ durable = opts.fetch(:durable, false)
1349
+ exclusive = opts.fetch(:exclusive, false)
1350
+ auto_delete = opts.fetch(:auto_delete, false)
1351
+ args = opts[:arguments]
1352
+
1353
+ result = self.queue_declare_without_recording_topology(name, opts)
1354
+ self.record_queue_with(self, result.queue, is_server_named, durable, exclusive, auto_delete, args) unless passive
1355
+
1356
+ result
1357
+ end
1358
+
1359
+ # We need this bypassing topology version to avoid modifying the collections
1360
+ # as we iterate over them during topology recovery.
1361
+ # @private
1362
+ def queue_declare_without_recording_topology(name, opts = {})
1126
1363
  raise_if_no_longer_open!
1127
1364
 
1128
1365
  Bunny::Queue.verify_type!(opts[:arguments]) if opts[:arguments]
@@ -1130,16 +1367,24 @@ module Bunny
1130
1367
  # strip trailing new line and carriage returns
1131
1368
  # just like RabbitMQ does
1132
1369
  safe_name = name.gsub(/[\r\n]/, "")
1370
+ is_server_named = (safe_name == AMQ::Protocol::EMPTY_STRING)
1133
1371
  @pending_queue_declare_name = safe_name
1372
+
1373
+ passive = opts.fetch(:passive, false)
1374
+ durable = opts.fetch(:durable, false)
1375
+ exclusive = opts.fetch(:exclusive, false)
1376
+ auto_delete = opts.fetch(:auto_delete, false)
1377
+ args = opts[:arguments]
1378
+
1134
1379
  @connection.send_frame(
1135
1380
  AMQ::Protocol::Queue::Declare.encode(@id,
1136
1381
  @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),
1382
+ passive,
1383
+ durable,
1384
+ exclusive,
1385
+ auto_delete,
1141
1386
  false,
1142
- opts[:arguments]))
1387
+ args))
1143
1388
 
1144
1389
  begin
1145
1390
  with_continuation_timeout do
@@ -1177,6 +1422,8 @@ module Bunny
1177
1422
  @last_queue_delete_ok = wait_on_continuations
1178
1423
  end
1179
1424
  raise_if_continuation_resulted_in_a_channel_error!
1425
+ self.delete_recorded_queue_named(name)
1426
+ self.deregister_queue_named(name)
1180
1427
 
1181
1428
  @last_queue_delete_ok
1182
1429
  end
@@ -1222,18 +1469,43 @@ module Bunny
1222
1469
  else
1223
1470
  exchange
1224
1471
  end
1472
+ rk = (opts[:routing_key] || opts[:key])
1473
+ args = opts[:arguments]
1474
+
1475
+ result = self.queue_bind_without_recording_topology(name, exchange, opts)
1476
+ self.record_queue_binding_with(self, exchange_name, name, rk, args)
1225
1477
 
1478
+ result
1479
+ end
1480
+
1481
+ # We need this bypassing topology version to avoid modifying the collections
1482
+ # as we iterate over them during topology recovery.
1483
+ # @private
1484
+ def queue_bind_without_recording_topology(name, exchange, opts = {})
1485
+ raise_if_no_longer_open!
1486
+
1487
+ exchange_name = if exchange.respond_to?(:name)
1488
+ exchange.name
1489
+ else
1490
+ exchange
1491
+ end
1492
+
1493
+ rk = (opts[:routing_key] || opts[:key])
1494
+ args = opts[:arguments]
1226
1495
  @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]))
1496
+ name,
1497
+ exchange_name,
1498
+ rk,
1499
+ false,
1500
+ args))
1501
+
1232
1502
  with_continuation_timeout do
1233
1503
  @last_queue_bind_ok = wait_on_continuations
1234
1504
  end
1235
1505
 
1236
1506
  raise_if_continuation_resulted_in_a_channel_error!
1507
+
1508
+
1237
1509
  @last_queue_bind_ok
1238
1510
  end
1239
1511
 
@@ -1259,16 +1531,20 @@ module Bunny
1259
1531
  exchange
1260
1532
  end
1261
1533
 
1534
+ rk = (opts[:routing_key] || opts[:key])
1535
+ args = opts[:arguments]
1262
1536
  @connection.send_frame(AMQ::Protocol::Queue::Unbind.encode(@id,
1263
1537
  name,
1264
1538
  exchange_name,
1265
- opts[:routing_key],
1266
- opts[:arguments]))
1539
+ rk,
1540
+ args))
1267
1541
  with_continuation_timeout do
1268
1542
  @last_queue_unbind_ok = wait_on_continuations
1269
1543
  end
1270
1544
 
1271
1545
  raise_if_continuation_resulted_in_a_channel_error!
1546
+ self.delete_recorded_queue_binding(self, exchange_name, name, rk, args)
1547
+
1272
1548
  @last_queue_unbind_ok
1273
1549
  end
1274
1550
 
@@ -1294,25 +1570,65 @@ module Bunny
1294
1570
  # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
1295
1571
  # @api public
1296
1572
  def exchange_declare(name, type, opts = {})
1573
+ result = self.exchange_declare_without_recording_topology(name, type, opts)
1574
+
1575
+ # strip trailing new line and carriage returns
1576
+ # just like RabbitMQ does
1577
+ safe_name = name.gsub(/[\r\n]/, "")
1578
+ passive = opts.fetch(:passive, false)
1579
+ durable = opts.fetch(:durable, false)
1580
+ auto_delete = opts.fetch(:auto_delete, false)
1581
+ args = opts[:arguments]
1582
+ self.record_exchange_with(self,
1583
+ safe_name,
1584
+ type.to_s,
1585
+ durable,
1586
+ auto_delete,
1587
+ args) unless passive
1588
+
1589
+ result
1590
+ end
1591
+
1592
+ # We need this bypassing topology version to avoid modifying the collections
1593
+ # as we iterate over them during topology recovery.
1594
+ #
1595
+ # @param [String] name The name of the exchange. Note that LF and CR characters
1596
+ # will be stripped from the value.
1597
+ # @param [String,Symbol] type Exchange type, e.g. :fanout or :topic
1598
+ # @param [Hash] opts Exchange properties
1599
+ #
1600
+ # @option opts [Boolean] durable (false) Should information about this exchange be persisted to disk so that it
1601
+ # can survive broker restarts? Typically set to true for long-lived exchanges.
1602
+ # @option opts [Boolean] auto_delete (false) Should this exchange be deleted when it is no longer used?
1603
+ # @option opts [Boolean] passive (false) If true, exchange will be checked for existence. If it does not
1604
+ # exist, {Bunny::NotFound} will be raised.
1605
+ # @private
1606
+ def exchange_declare_without_recording_topology(name, type, opts = {})
1297
1607
  raise_if_no_longer_open!
1298
1608
 
1299
1609
  # strip trailing new line and carriage returns
1300
1610
  # just like RabbitMQ does
1301
1611
  safe_name = name.gsub(/[\r\n]/, "")
1612
+ passive = opts.fetch(:passive, false)
1613
+ durable = opts.fetch(:durable, false)
1614
+ auto_delete = opts.fetch(:auto_delete, false)
1615
+ args = opts[:arguments]
1616
+
1302
1617
  @connection.send_frame(AMQ::Protocol::Exchange::Declare.encode(@id,
1303
1618
  safe_name,
1304
1619
  type.to_s,
1305
- opts.fetch(:passive, false),
1306
- opts.fetch(:durable, false),
1307
- opts.fetch(:auto_delete, false),
1620
+ passive,
1621
+ durable,
1622
+ auto_delete,
1308
1623
  opts.fetch(:internal, false),
1309
1624
  opts.fetch(:no_wait, false),
1310
- opts[:arguments]))
1625
+ args))
1311
1626
  with_continuation_timeout do
1312
1627
  @last_exchange_declare_ok = wait_on_continuations
1313
1628
  end
1314
1629
 
1315
1630
  raise_if_continuation_resulted_in_a_channel_error!
1631
+
1316
1632
  @last_exchange_declare_ok
1317
1633
  end
1318
1634
 
@@ -1338,6 +1654,9 @@ module Bunny
1338
1654
  end
1339
1655
 
1340
1656
  raise_if_continuation_resulted_in_a_channel_error!
1657
+ self.delete_recorded_exchange_named(name)
1658
+ self.deregister_exchange_named(name)
1659
+
1341
1660
  @last_exchange_delete_ok
1342
1661
  end
1343
1662
 
@@ -1357,6 +1676,29 @@ module Bunny
1357
1676
  # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
1358
1677
  # @api public
1359
1678
  def exchange_bind(source, destination, opts = {})
1679
+ result = self.exchange_bind_without_recording_topology(source, destination, opts)
1680
+
1681
+ source_name = if source.respond_to?(:name)
1682
+ source.name
1683
+ else
1684
+ source
1685
+ end
1686
+ destination_name = if destination.respond_to?(:name)
1687
+ destination.name
1688
+ else
1689
+ destination
1690
+ end
1691
+ rk = (opts[:routing_key] || opts[:key])
1692
+ args = opts[:arguments]
1693
+ self.record_exchange_binding_with(self, source_name, destination_name, rk, args)
1694
+
1695
+ result
1696
+ end
1697
+
1698
+ # We need this bypassing topology version to avoid modifying the collections
1699
+ # as we iterate over them during topology recovery.
1700
+ # @private
1701
+ def exchange_bind_without_recording_topology(source, destination, opts = {})
1360
1702
  raise_if_no_longer_open!
1361
1703
 
1362
1704
  source_name = if source.respond_to?(:name)
@@ -1371,17 +1713,21 @@ module Bunny
1371
1713
  destination
1372
1714
  end
1373
1715
 
1716
+ rk = (opts[:routing_key] || opts[:key])
1717
+ args = opts[:arguments]
1374
1718
  @connection.send_frame(AMQ::Protocol::Exchange::Bind.encode(@id,
1375
1719
  destination_name,
1376
1720
  source_name,
1377
- opts[:routing_key],
1721
+ rk,
1378
1722
  false,
1379
- opts[:arguments]))
1723
+ args))
1380
1724
  with_continuation_timeout do
1381
1725
  @last_exchange_bind_ok = wait_on_continuations
1382
1726
  end
1383
1727
 
1384
1728
  raise_if_continuation_resulted_in_a_channel_error!
1729
+ self.record_exchange_binding_with(self, source_name, destination_name, rk, args)
1730
+
1385
1731
  @last_exchange_bind_ok
1386
1732
  end
1387
1733
 
@@ -1415,17 +1761,21 @@ module Bunny
1415
1761
  destination
1416
1762
  end
1417
1763
 
1764
+ rk = (opts[:routing_key] || opts[:key])
1765
+ args = opts[:arguments]
1418
1766
  @connection.send_frame(AMQ::Protocol::Exchange::Unbind.encode(@id,
1419
1767
  destination_name,
1420
1768
  source_name,
1421
- opts[:routing_key],
1769
+ rk,
1422
1770
  false,
1423
- opts[:arguments]))
1771
+ args))
1424
1772
  with_continuation_timeout do
1425
1773
  @last_exchange_unbind_ok = wait_on_continuations
1426
1774
  end
1427
1775
 
1428
1776
  raise_if_continuation_resulted_in_a_channel_error!
1777
+ self.delete_recorded_exchange_binding(self, source_name, destination_name, rk, args)
1778
+
1429
1779
  @last_exchange_unbind_ok
1430
1780
  end
1431
1781
 
@@ -1528,14 +1878,25 @@ module Bunny
1528
1878
  alias using_publisher_confirms? using_publisher_confirmations?
1529
1879
 
1530
1880
  # Enables publisher confirms for the channel.
1881
+ #
1882
+ # @param [Proc] callback Optional callback invoked for each confirm. Receives (delivery_tag, multiple, nack).
1883
+ # @param [Boolean] tracking When true, basic_publish blocks until the broker confirms receipt.
1884
+ # Raises Bunny::MessageNacked if the message is nacked.
1885
+ # @param [Integer] outstanding_limit Max unconfirmed messages before basic_publish blocks.
1886
+ # Defaults to 1000 when tracking: true (optimal for throughput). Pass explicit value to override.
1887
+ # @param [Integer] confirm_timeout Timeout in ms for confirms. Defaults to connection's continuation_timeout.
1888
+ #
1531
1889
  # @return [AMQ::Protocol::Confirm::SelectOk] RabbitMQ response
1532
1890
  # @see #wait_for_confirms
1533
1891
  # @see #unconfirmed_set
1534
1892
  # @see #nacked_set
1535
1893
  # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
1536
1894
  # @api public
1537
- def confirm_select(callback = nil)
1895
+ def confirm_select(callback = nil, tracking: false, outstanding_limit: nil, confirm_timeout: nil)
1538
1896
  raise_if_no_longer_open!
1897
+ raise ArgumentError, "outstanding_limit requires tracking: true" if outstanding_limit && !tracking
1898
+ raise ArgumentError, "outstanding_limit must be positive" if outstanding_limit && outstanding_limit < 1
1899
+ raise ArgumentError, "confirm_timeout must be positive" if confirm_timeout && confirm_timeout < 1
1539
1900
 
1540
1901
  if @next_publish_seq_no == 0
1541
1902
  @confirms_continuations = new_continuation
@@ -1546,6 +1907,17 @@ module Bunny
1546
1907
  end
1547
1908
 
1548
1909
  @confirms_callback = callback
1910
+ @confirms_tracking_enabled = tracking
1911
+ # Default to optimal limit when tracking enabled (avoids per-message mutex)
1912
+ @outstanding_limit = outstanding_limit || (tracking ? DEFAULT_OUTSTANDING_CONFIRMS_LIMIT : nil)
1913
+ @confirm_timeout = confirm_timeout
1914
+
1915
+ # Cache combined check for fast path in basic_publish
1916
+ @throttle_publishes = tracking && @outstanding_limit
1917
+
1918
+ if @outstanding_limit && !@outstanding_limit_cond
1919
+ @outstanding_limit_cond = @unconfirmed_set_mutex.new_cond
1920
+ end
1549
1921
 
1550
1922
  @connection.send_frame(AMQ::Protocol::Confirm::Select.encode(@id, false))
1551
1923
  with_continuation_timeout do
@@ -1585,9 +1957,9 @@ module Bunny
1585
1957
  #
1586
1958
  # @return [String] Unique string.
1587
1959
  # @api plugin
1588
- def generate_consumer_tag(name = "bunny")
1960
+ def generate_consumer_tag(prefix = "bunny")
1589
1961
  t = Bunny::Timestamp.now
1590
- "#{name}-#{t.to_i * 1000}-#{Kernel.rand(999_999_999_999)}"
1962
+ "#{prefix}-#{t.to_i * 1000}-#{Kernel.rand(999_999_999_999)}"
1591
1963
  end
1592
1964
 
1593
1965
  # @endgroup
@@ -1630,11 +2002,8 @@ module Bunny
1630
2002
  recover_prefetch_setting
1631
2003
  recover_confirm_mode
1632
2004
  recover_tx_mode
1633
- recover_exchanges
1634
- # this includes recovering bindings
1635
- recover_queues
1636
- recover_consumers
1637
- increment_recoveries_counter
2005
+
2006
+ # Topology is now recovered by [Bunny::Session] via the data in [Bunny::TopologyRegistry].
1638
2007
  end
1639
2008
 
1640
2009
  # Recovers basic.qos setting. Used by the Automatic Network Failure
@@ -1651,13 +2020,26 @@ module Bunny
1651
2020
  #
1652
2021
  # @api plugin
1653
2022
  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
2023
+ return unless using_publisher_confirmations?
2024
+
2025
+ @unconfirmed_set_mutex.synchronize do
2026
+ @unconfirmed_set.clear
2027
+ @delivery_tag_offset = @next_publish_seq_no - 1
2028
+
2029
+ if @confirms_tracking_enabled
2030
+ @per_message_continuations_mutex.synchronize do
2031
+ @per_message_continuations.each_value { |c| c.push(:network_error) }
2032
+ @per_message_continuations.clear
2033
+ end
1658
2034
  end
1659
- confirm_select(@confirms_callback)
2035
+
2036
+ @outstanding_limit_cond&.broadcast
1660
2037
  end
2038
+
2039
+ confirm_select(@confirms_callback,
2040
+ tracking: @confirms_tracking_enabled,
2041
+ outstanding_limit: @outstanding_limit,
2042
+ confirm_timeout: @confirm_timeout)
1661
2043
  end
1662
2044
 
1663
2045
  # Recovers transaction mode. Used by the Automatic Network Failure
@@ -1668,45 +2050,31 @@ module Bunny
1668
2050
  tx_select if @tx_mode
1669
2051
  end
1670
2052
 
1671
- # Recovers exchanges. Used by the Automatic Network Failure
2053
+ # Used by the Automatic Network Failure
1672
2054
  # Recovery feature.
1673
2055
  #
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
2056
+ # @param [String] old_name
2057
+ # @param [String] new_name
2058
+ # @private
2059
+ def record_queue_name_change(old_name, new_name)
2060
+ @queue_mutex.synchronize do
2061
+ if (orig = @queues[old_name])
2062
+ @queues.delete(old_name)
1680
2063
 
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
2064
+ orig.update_name_to(new_name)
2065
+ @queues[new_name] = orig.dup
2066
+ end
1689
2067
  end
1690
2068
  end
1691
2069
 
1692
- # Recovers consumers. Used by the Automatic Network Failure
1693
- # Recovery feature.
2070
+ # Used by the Automatic Network Failure Recovery feature.
1694
2071
  #
1695
- # @api plugin
1696
- def recover_consumers
2072
+ # @api private
2073
+ def maybe_reinitialize_consumer_pool!
1697
2074
  unless @consumers.empty?
1698
2075
  @work_pool = ConsumerWorkPool.new(@work_pool.size, @work_pool.abort_on_exception)
1699
2076
  @work_pool.start
1700
2077
  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
2078
  end
1711
2079
 
1712
2080
  # @api public
@@ -1745,6 +2113,9 @@ module Bunny
1745
2113
  def register_consumer(consumer_tag, consumer)
1746
2114
  @consumer_mutex.synchronize do
1747
2115
  @consumers[consumer_tag] = consumer
2116
+ if @last_consumer_tag == consumer_tag
2117
+ @last_consumer = consumer
2118
+ end
1748
2119
  end
1749
2120
  end
1750
2121
 
@@ -1752,16 +2123,35 @@ module Bunny
1752
2123
  def unregister_consumer(consumer_tag)
1753
2124
  @consumer_mutex.synchronize do
1754
2125
  @consumers.delete(consumer_tag)
2126
+ if @last_consumer_tag == consumer_tag
2127
+ @last_consumer_tag = nil
2128
+ @last_consumer = nil
2129
+ end
1755
2130
  end
1756
2131
  end
1757
2132
 
2133
+ # @param [String] queue_name
2134
+ # @param [String] consumer_tag
2135
+ # @param [Boolean] no_ack true means automative acknowledgement mode
2136
+ # @param [Boolean] exclusive
2137
+ # @param [Hash] arguments
1758
2138
  # @private
1759
- def add_consumer(queue, consumer_tag, no_ack, exclusive, arguments, &block)
2139
+ def add_consumer(queue_name, consumer_tag, no_ack, exclusive, arguments, &block)
1760
2140
  @consumer_mutex.synchronize do
1761
- c = Consumer.new(self, queue, consumer_tag, no_ack, exclusive, arguments)
2141
+ c = Consumer.new(self, queue_name, consumer_tag, no_ack, exclusive, arguments)
1762
2142
  c.on_delivery(&block) if block
1763
2143
  @consumers[consumer_tag] = c
2144
+ if @last_consumer_tag == consumer_tag
2145
+ @last_consumer = c
2146
+ end
2147
+ c
1764
2148
  end
2149
+ record_consumer_with(self, consumer_tag,
2150
+ queue_name,
2151
+ block,
2152
+ !no_ack,
2153
+ exclusive,
2154
+ arguments)
1765
2155
  end
1766
2156
 
1767
2157
  # @private
@@ -1826,6 +2216,10 @@ module Bunny
1826
2216
  consume_with(consumer)
1827
2217
  else
1828
2218
  @consumers.delete(method.consumer_tag)
2219
+ if @last_consumer_tag == method.consumer_tag
2220
+ @last_consumer_tag = nil
2221
+ @last_consumer = nil
2222
+ end
1829
2223
  consumer.handle_cancellation(method)
1830
2224
  end
1831
2225
  rescue Exception => e
@@ -1839,6 +2233,7 @@ module Bunny
1839
2233
  when AMQ::Protocol::Basic::CancelOk then
1840
2234
  @continuations.push(method)
1841
2235
  unregister_consumer(method.consumer_tag)
2236
+ delete_recorded_consumer(method.consumer_tag)
1842
2237
  when AMQ::Protocol::Tx::SelectOk, AMQ::Protocol::Tx::CommitOk, AMQ::Protocol::Tx::RollbackOk then
1843
2238
  @continuations.push(method)
1844
2239
  when AMQ::Protocol::Tx::SelectOk then
@@ -1870,12 +2265,12 @@ module Bunny
1870
2265
 
1871
2266
  # @private
1872
2267
  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/)
2268
+ method.unknown_delivery_tag? || method.delivery_ack_timeout? || method.message_too_large?
1874
2269
  end
1875
2270
 
1876
2271
  # @private
1877
2272
  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)
2273
+ basic_get_ok.delivery_tag = basic_get_ok.delivery_tag
1879
2274
  @basic_get_continuations.push([basic_get_ok, properties, content])
1880
2275
  end
1881
2276
 
@@ -1886,20 +2281,33 @@ module Bunny
1886
2281
 
1887
2282
  # @private
1888
2283
  def handle_frameset(basic_deliver, properties, content)
1889
- consumer = @consumers[basic_deliver.consumer_tag]
2284
+ tag = basic_deliver.consumer_tag
2285
+ if @last_consumer_tag == tag
2286
+ consumer = @last_consumer
2287
+ else
2288
+ consumer = @consumers[tag]
2289
+ @last_consumer_tag = tag
2290
+ @last_consumer = consumer
2291
+ end
2292
+
1890
2293
  if consumer
1891
2294
  @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
2295
+ deliver_to_consumer(consumer, basic_deliver, properties, content)
1897
2296
  end
1898
2297
  else
1899
2298
  @logger.warn "No consumer for tag #{basic_deliver.consumer_tag} on channel #{@id}!"
1900
2299
  end
1901
2300
  end
1902
2301
 
2302
+ # @private
2303
+ def deliver_to_consumer(consumer, basic_deliver, properties, content)
2304
+ begin
2305
+ consumer.call(DeliveryInfo.new(basic_deliver, consumer, self), MessageProperties.new(properties), content)
2306
+ rescue StandardError => e
2307
+ @uncaught_exception_handler.call(e, consumer) if @uncaught_exception_handler
2308
+ end
2309
+ end
2310
+
1903
2311
  # @private
1904
2312
  def handle_basic_return(basic_return, properties, content)
1905
2313
  x = find_exchange(basic_return.exchange)
@@ -1931,6 +2339,22 @@ module Bunny
1931
2339
 
1932
2340
  @confirms_continuations.push(true) if @unconfirmed_set.empty?
1933
2341
 
2342
+ # Signal per-message continuations (only used when tracking without limit)
2343
+ if @confirms_tracking_enabled && !@throttle_publishes
2344
+ to_signal = []
2345
+ @per_message_continuations_mutex.synchronize do
2346
+ @per_message_continuations.each do |tag, cont|
2347
+ to_signal << cont if tag >= confirmed_range_start && tag <= confirmed_range_end
2348
+ end
2349
+ end
2350
+ result = nack ? :nack : :ack
2351
+ to_signal.each { |c| c.push(result) }
2352
+ end
2353
+
2354
+ # Wake publisher(s) waiting for outstanding limit slot
2355
+ # Use broadcast since "multiple" acks can free many slots at once
2356
+ @outstanding_limit_cond&.broadcast if @throttle_publishes
2357
+
1934
2358
  if @confirms_callback
1935
2359
  confirmed_range.each { |tag| @confirms_callback.call(tag, false, nack) }
1936
2360
  end
@@ -2005,21 +2429,63 @@ module Bunny
2005
2429
  end
2006
2430
  end
2007
2431
 
2008
-
2009
- # Releases all continuations. Used by automatic network recovery.
2432
+ # Waits for a slot when outstanding limit is reached.
2433
+ # Assumes @unconfirmed_set_mutex is already held.
2010
2434
  # @private
2011
- def release_all_continuations
2012
- @threads_waiting_on_confirms_continuations.each do |t|
2013
- t.run
2435
+ def wait_for_outstanding_slot_locked
2436
+ limit = @outstanding_limit
2437
+ return if @unconfirmed_set.size < limit
2438
+
2439
+ timeout_sec = (@confirm_timeout || @connection.continuation_timeout) / 1000.0
2440
+ deadline = Bunny::Timestamp.monotonic + timeout_sec
2441
+
2442
+ while @unconfirmed_set.size >= limit
2443
+ raise_if_no_longer_open!
2444
+
2445
+ remaining = deadline - Bunny::Timestamp.monotonic
2446
+ raise Timeout::Error, "Timed out waiting for publisher confirms (limit: #{limit})" if remaining <= 0
2447
+
2448
+ @outstanding_limit_cond.wait(remaining)
2014
2449
  end
2015
- @threads_waiting_on_continuations.each do |t|
2016
- t.run
2450
+ end
2451
+
2452
+ # @private
2453
+ def wait_for_publish_confirm(seq_no, continuation)
2454
+ t = Thread.current
2455
+ @threads_waiting_on_confirms_continuations << t
2456
+
2457
+ begin
2458
+ timeout = @confirm_timeout || @connection.continuation_timeout
2459
+ case continuation.poll(timeout)
2460
+ when :ack, true
2461
+ # confirmed
2462
+ when :nack
2463
+ raise MessageNacked.new("Message #{seq_no} was nacked", seq_no)
2464
+ when :network_error
2465
+ raise NetworkFailure.new("Network failure waiting for confirm", nil)
2466
+ when nil
2467
+ raise Timeout::Error, "Timed out waiting for publisher confirm"
2468
+ end
2469
+ ensure
2470
+ @threads_waiting_on_confirms_continuations.delete(t)
2471
+ @per_message_continuations_mutex.synchronize do
2472
+ @per_message_continuations.delete(seq_no)
2473
+ end
2017
2474
  end
2018
- @threads_waiting_on_basic_get_continuations.each do |t|
2019
- t.run
2475
+ end
2476
+
2477
+
2478
+ # @private
2479
+ def release_all_continuations
2480
+ @threads_waiting_on_confirms_continuations.each(&:run)
2481
+ @threads_waiting_on_continuations.each(&:run)
2482
+ @threads_waiting_on_basic_get_continuations.each(&:run)
2483
+
2484
+ if @outstanding_limit_cond
2485
+ @unconfirmed_set_mutex.synchronize { @outstanding_limit_cond.broadcast }
2020
2486
  end
2021
2487
 
2022
- self.reset_continuations
2488
+ reset_continuations
2023
2489
  end
2024
2490
 
2025
2491
  # Starts consumer work pool. Lazily called by #basic_consume to avoid creating new threads
@@ -2049,39 +2515,167 @@ module Bunny
2049
2515
  @connection.read_next_frame(options = {})
2050
2516
  end
2051
2517
 
2518
+ # @param [String] name
2519
+ # @private
2520
+ def find_queue(name)
2521
+ @queue_mutex.synchronize { @queues[name] }
2522
+ end
2523
+
2524
+ # @param [String] name
2525
+ # @private
2526
+ def find_exchange(name)
2527
+ @exchange_mutex.synchronize { @exchanges[name] }
2528
+ end
2529
+
2530
+ # @param [Bunny::Queue] queue
2531
+ # @private
2532
+ def register_queue(queue)
2533
+ @queue_mutex.synchronize { @queues[queue.name] = queue }
2534
+ end
2535
+
2536
+ # @param [Bunny::Queue] queue
2052
2537
  # @private
2053
2538
  def deregister_queue(queue)
2054
2539
  @queue_mutex.synchronize { @queues.delete(queue.name) }
2055
2540
  end
2056
2541
 
2542
+ # @param [Bunny::String] name
2057
2543
  # @private
2058
2544
  def deregister_queue_named(name)
2059
2545
  @queue_mutex.synchronize { @queues.delete(name) }
2060
2546
  end
2061
2547
 
2548
+ # @param [Bunny::Queue] queue
2062
2549
  # @private
2063
- def register_queue(queue)
2064
- @queue_mutex.synchronize { @queues[queue.name] = queue }
2550
+ def record_queue(queue)
2551
+ @connection.record_queue(queue)
2065
2552
  end
2066
2553
 
2554
+ # @param [Bunny::Channel] ch
2555
+ # @param [String] name
2556
+ # @param [Boolean] server_named
2557
+ # @param [Boolean] durable
2558
+ # @param [Boolean] auto_delete
2559
+ # @param [Boolean] exclusive
2560
+ # @param [Hash] arguments
2561
+ def record_queue_with(ch, name, server_named, durable, auto_delete, exclusive, arguments)
2562
+ @connection.record_queue_with(ch, name, server_named, durable, auto_delete, exclusive, arguments)
2563
+ end
2564
+
2565
+ # @param [Bunny::Queue, Bunny::RecordedQueue] queue
2067
2566
  # @private
2068
- def find_queue(name)
2069
- @queue_mutex.synchronize { @queues[name] }
2567
+ def delete_recoreded_queue(queue)
2568
+ @connection.delete_recorded_queue(queue)
2070
2569
  end
2071
2570
 
2571
+ # @param [String] name
2072
2572
  # @private
2073
- def deregister_exchange(exchange)
2074
- @exchange_mutex.synchronize { @exchanges.delete(exchange.name) }
2573
+ def delete_recorded_queue_named(name)
2574
+ @connection.delete_recorded_queue_named(name)
2075
2575
  end
2076
2576
 
2577
+ # @param [Bunny::Exchange] exchange
2077
2578
  # @private
2078
2579
  def register_exchange(exchange)
2079
2580
  @exchange_mutex.synchronize { @exchanges[exchange.name] = exchange }
2080
2581
  end
2081
2582
 
2583
+ # @param [Bunny::Exchange] exchange
2082
2584
  # @private
2083
- def find_exchange(name)
2084
- @exchange_mutex.synchronize { @exchanges[name] }
2585
+ def deregister_exchange(exchange)
2586
+ @queue_mutex.synchronize { @exchanges.delete(exchange.name) }
2587
+ end
2588
+
2589
+ # @param [String] name
2590
+ # @private
2591
+ def deregister_exchange_named(name)
2592
+ @queue_mutex.synchronize { @exchanges.delete(name) }
2593
+ end
2594
+
2595
+ # @param [Bunny::Exchange] exchange
2596
+ # @private
2597
+ def record_exchange(exchange)
2598
+ @connection.record_exchange(exchange)
2599
+ end
2600
+
2601
+ # @param [Bunny::Channel] ch
2602
+ # @param [String] name
2603
+ # @param [String] type
2604
+ # @param [Boolean] durable
2605
+ # @param [Boolean] auto_delete
2606
+ # @param [Hash] arguments
2607
+ def record_exchange_with(ch, name, type, durable, auto_delete, arguments)
2608
+ @connection.record_exchange_with(ch, name, type, durable, auto_delete, arguments)
2609
+ end
2610
+
2611
+ # @param [Bunny::Exchange] exchange
2612
+ # @private
2613
+ def delete_recorded_exchange(exchange)
2614
+ @connection.delete_recorded_exchange(exchange)
2615
+ end
2616
+
2617
+ # @param [String] name
2618
+ # @private
2619
+ def delete_recorded_exchange_named(name)
2620
+ @connection.delete_recorded_exchange_named(name)
2621
+ end
2622
+
2623
+ # @param [Bunny::Channel] ch
2624
+ # @param [String] exchange_name
2625
+ # @param [String] queue_name
2626
+ # @param [String] routing_key
2627
+ # @param [Hash] arguments
2628
+ # @private
2629
+ def record_queue_binding_with(ch, exchange_name, queue_name, routing_key, arguments)
2630
+ @connection.record_queue_binding_with(ch, exchange_name, queue_name, routing_key, arguments)
2631
+ end
2632
+
2633
+ # @param [Bunny::Channel] ch
2634
+ # @param [String] exchange_name
2635
+ # @param [String] queue_name
2636
+ # @param [String] routing_key
2637
+ # @param [Hash] arguments
2638
+ # @private
2639
+ def delete_recorded_queue_binding(ch, exchange_name, queue_name, routing_key, arguments)
2640
+ @connection.delete_recorded_queue_binding(ch, exchange_name, queue_name, routing_key, arguments)
2641
+ end
2642
+
2643
+ # @param [Bunny::Channel] ch
2644
+ # @param [String] source_name
2645
+ # @param [String] destination_name
2646
+ # @param [String] routing_key
2647
+ # @param [Hash] arguments
2648
+ # @private
2649
+ def record_exchange_binding_with(ch, source_name, destination_name, routing_key, arguments)
2650
+ @connection.record_exchange_binding_with(ch, source_name, destination_name, routing_key, arguments)
2651
+ end
2652
+
2653
+ # @param [Bunny::Channel] ch
2654
+ # @param [String] source_name
2655
+ # @param [String] destination_name
2656
+ # @param [String] routing_key
2657
+ # @param [Hash] arguments
2658
+ # @private
2659
+ def delete_recorded_exchange_binding(ch, source_name, destination_name, routing_key, arguments)
2660
+ @connection.delete_recorded_exchange_binding(ch, source_name, destination_name, routing_key, arguments)
2661
+ end
2662
+
2663
+ # @param [Bunny::Channel] ch
2664
+ # @param [String] consumer_tag
2665
+ # @param [String] queue_name
2666
+ # @param [#call] callable
2667
+ # @param [Boolean] manual_ack
2668
+ # @param [Boolean] exclusive
2669
+ # @param [Hash] arguments
2670
+ # @private
2671
+ def record_consumer_with(ch, consumer_tag, queue_name, callable, manual_ack, exclusive, arguments)
2672
+ @connection.record_consumer_with(ch, consumer_tag, queue_name, callable, manual_ack, exclusive, arguments)
2673
+ end
2674
+
2675
+ # @param [String] consumer_tag
2676
+ # @private
2677
+ def delete_recorded_consumer(consumer_tag)
2678
+ @connection.delete_recorded_consumer(consumer_tag)
2085
2679
  end
2086
2680
 
2087
2681
  protected
@@ -2148,6 +2742,7 @@ module Bunny
2148
2742
  @continuations = new_continuation
2149
2743
  @confirms_continuations = new_continuation
2150
2744
  @basic_get_continuations = new_continuation
2745
+ @per_message_continuations_mutex.synchronize { @per_message_continuations.clear }
2151
2746
  end
2152
2747
 
2153
2748
  # @private
@@ -2158,16 +2753,7 @@ module Bunny
2158
2753
  # @private
2159
2754
  def guarding_against_stale_delivery_tags(tag, &block)
2160
2755
  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
2756
+ when Integer then block.call
2171
2757
  end
2172
2758
  end
2173
2759
  end # Channel