bunny 3.5.0 → 3.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c409adf026a09b52061cdbca1d1ea72e6312f11085712f3ba586111fc62fd91
4
- data.tar.gz: c90db5d9e534fb527899221f0a13fb91e13ef18cc178f064f077a7984da692d5
3
+ metadata.gz: d0192b4541ec169e249317d397768bf5e945b43c08b797559a47adb5aa16d553
4
+ data.tar.gz: 6a460e9bdbb8ee073c28f6aa7c9bdcc4ab017929d6a6e298eb49ba01db1059df
5
5
  SHA512:
6
- metadata.gz: 9fadf7743831aef18668dc5394c3a888b89f39f2afeb71c73f99a0553722ed365ed26fa571cd1a0d0ed27240c5b35b2c0fa4b330b9b2acfe25339bebf5e49fca
7
- data.tar.gz: 465c8430c93d6895c510f4fc8d66493f0c2a17493ce1685175305f8ed6a704017e85098972c074c8bca67fb12611c01e9820d33da21120b3cbdfa37a226a17d9
6
+ metadata.gz: a01fbb4c9cbfe312b803026b86dca339b1ba306aa1ad3f44cb77abaf26f28ec823a57bee2231ca1b29de03e5b31355a70e2a6b8eb066be465f9fb85f052fa616
7
+ data.tar.gz: 3b752e1f17c5d40a155c49fbdf53f437ad450a4c31a74ffc791b8dfd5079029b0f5242dc0d573b349ce7248d7474ae15211e214a8797b0487759d7a69a0aadfe
data/ChangeLog.md CHANGED
@@ -1,6 +1,33 @@
1
- ## Changes between Bunny 3.5.0 and 3.6.0
1
+ ## Changes between Bunny 3.5.0 and 3.6.0 (Nov 9, 2026)
2
2
 
3
- No changes yet.
3
+ ### `amq-protocol` Bump to `2.10.0`
4
+
5
+ [`2.10.0` release notes](https://github.com/ruby-amqp/amq-protocol/releases/tag/v2.10.0).
6
+
7
+ `amqps://` URIs without `?verify=` no longer disable peer verification.
8
+
9
+ ### TLSv1.2 Is Now a Minimum, Not a Pinned Version
10
+
11
+ Without an explicit `tls_protocol`, TLS 1.3 is no longer ruled out.
12
+
13
+ GitHub issue: [#760](https://github.com/ruby-amqp/bunny/issues/760)
14
+
15
+ ### New `tls_min_version` and `tls_max_version` Options
16
+
17
+ They set the range of TLS versions to negotiate and accept strings as well as symbols.
18
+ `tls_protocol`, which pins a single version, is deprecated in their favor and now emits a warning.
19
+
20
+ ### Stale Delivery Tags Are Not Acknowledged After `Channel#reopen` or Connection Recovery
21
+
22
+ Delivery tags now keep increasing across a reopen or recovery instead of restarting at 1.
23
+ Acknowledgements for tags from before it are dropped instead of closing the channel or
24
+ acknowledging a different delivery.
25
+
26
+ GitHub issue: [#710](https://github.com/ruby-amqp/bunny/issues/710)
27
+
28
+ ### `Channel#reopen` No Longer Leaks a Consumer Work Pool Thread
29
+
30
+ Topology recovery replaced the consumer work pool `Channel#reopen` had just started, leaking its thread.
4
31
 
5
32
 
6
33
  ## Changes between Bunny 3.4.0 and 3.5.0 (Oct 25, 2026)
data/README.md CHANGED
@@ -95,7 +95,7 @@ may introduce, e.g. the idempotency of `queue.delete` operations.
95
95
  To use Bunny in a project managed with Bundler:
96
96
 
97
97
  ``` ruby
98
- gem "bunny", ">= 3.0"
98
+ gem "bunny", ">= 3.5"
99
99
  ```
100
100
 
101
101
  ### With Rubygems
@@ -218,7 +218,7 @@ and steps to reproduce (or failing test cases).
218
218
  ## Other Ruby RabbitMQ Clients
219
219
 
220
220
  The other widely used Ruby RabbitMQ client is [March Hare](http://rubymarchhare.info) (JRuby-only).
221
- It's a mature library that requires RabbitMQ 3.3.x or later.
221
+ It's a mature library that requires RabbitMQ 3.13.x or later.
222
222
 
223
223
 
224
224
  ## Contributing
data/lib/bunny/channel.rb CHANGED
@@ -196,6 +196,7 @@ module Bunny
196
196
  end
197
197
 
198
198
  @status = :opening
199
+ @status_mutex = Mutex.new
199
200
  @connection.register_channel(self)
200
201
 
201
202
  @queues = Hash.new
@@ -233,6 +234,11 @@ module Bunny
233
234
  @next_publish_seq_no = 0
234
235
  @delivery_tag_offset = 0
235
236
 
237
+ # Delivery tags restart at 1 on a reopened or recovered channel. Shifting them
238
+ # by the highest tag seen so far keeps them unique, see #guarding_against_stale_delivery_tags.
239
+ @consumer_delivery_tag_offset = 0
240
+ @last_consumer_delivery_tag = 0
241
+
236
242
  @uncaught_exception_handler = Proc.new do |e, consumer|
237
243
  @logger.error "Uncaught exception from consumer #{consumer.to_s}: #{e.inspect} @ #{e.backtrace[0]}"
238
244
  end
@@ -255,6 +261,8 @@ module Bunny
255
261
  @threads_waiting_on_continuations = Set.new
256
262
  @threads_waiting_on_confirms_continuations = Set.new
257
263
  @threads_waiting_on_basic_get_continuations = Set.new
264
+ # Set before the channel is open again, so that no stale tag slips through
265
+ @consumer_delivery_tag_offset = @last_consumer_delivery_tag
258
266
 
259
267
  @connection.open_channel(self)
260
268
  # clear last channel error
@@ -290,6 +298,8 @@ module Bunny
290
298
  end
291
299
  end
292
300
 
301
+ # once channel.close is on the wire, the server rejects any other frame on this channel
302
+ @status_mutex.synchronize { @status = :closing }
293
303
  @connection.close_channel(self)
294
304
  @status = :closed
295
305
  @work_pool.shutdown
@@ -1041,10 +1051,12 @@ module Bunny
1041
1051
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
1042
1052
  # @api public
1043
1053
  def basic_reject(delivery_tag, requeue = false)
1044
- raise_if_no_longer_open!
1045
- @connection.send_frame(AMQ::Protocol::Basic::Reject.encode(@id, delivery_tag, requeue))
1054
+ guarding_against_stale_delivery_tags(delivery_tag) do |tag|
1055
+ raise_if_no_longer_open!
1056
+ @connection.send_frame(AMQ::Protocol::Basic::Reject.encode(@id, tag, requeue))
1046
1057
 
1047
- nil
1058
+ nil
1059
+ end
1048
1060
  end
1049
1061
 
1050
1062
  # Acknowledges a delivery (message).
@@ -1087,9 +1099,9 @@ module Bunny
1087
1099
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
1088
1100
  # @api public
1089
1101
  def basic_ack(delivery_tag, multiple = false)
1090
- guarding_against_stale_delivery_tags(delivery_tag) do
1102
+ guarding_against_stale_delivery_tags(delivery_tag) do |tag|
1091
1103
  raise_if_no_longer_open!
1092
- @connection.send_frame(AMQ::Protocol::Basic::Ack.encode(@id, delivery_tag, multiple))
1104
+ @connection.send_frame(AMQ::Protocol::Basic::Ack.encode(@id, tag, multiple))
1093
1105
 
1094
1106
  nil
1095
1107
  end
@@ -1149,10 +1161,10 @@ module Bunny
1149
1161
  # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
1150
1162
  # @api public
1151
1163
  def basic_nack(delivery_tag, multiple = false, requeue = false)
1152
- guarding_against_stale_delivery_tags(delivery_tag) do
1164
+ guarding_against_stale_delivery_tags(delivery_tag) do |tag|
1153
1165
  raise_if_no_longer_open!
1154
1166
  @connection.send_frame(AMQ::Protocol::Basic::Nack.encode(@id,
1155
- delivery_tag,
1167
+ tag,
1156
1168
  multiple,
1157
1169
  requeue))
1158
1170
 
@@ -2034,7 +2046,8 @@ module Bunny
2034
2046
  #
2035
2047
  # @api private
2036
2048
  def maybe_reinitialize_consumer_pool!
2037
- unless @consumers.empty?
2049
+ # keep a pool that is already running, e.g. the one #reopen has just started
2050
+ unless @consumers.empty? || @work_pool.running?
2038
2051
  @work_pool = ConsumerWorkPool.new(@work_pool.size, @work_pool.abort_on_exception)
2039
2052
  @work_pool.start
2040
2053
  end
@@ -2132,7 +2145,9 @@ module Bunny
2132
2145
  def send_basic_cancel_ok(consumer_tag)
2133
2146
  tag = consumer_tag.to_s
2134
2147
  payload = [AMQ::Protocol::Basic::CancelOk.index, tag.bytesize, tag].pack("NCa*")
2135
- @connection.send_frame(AMQ::Protocol::MethodFrame.new(payload, @id))
2148
+ @status_mutex.synchronize do
2149
+ @connection.send_frame(AMQ::Protocol::MethodFrame.new(payload, @id)) if open?
2150
+ end
2136
2151
  end
2137
2152
 
2138
2153
  # @private
@@ -2244,7 +2259,7 @@ module Bunny
2244
2259
 
2245
2260
  # @private
2246
2261
  def handle_basic_get_ok(basic_get_ok, properties, content)
2247
- basic_get_ok.delivery_tag = basic_get_ok.delivery_tag
2262
+ basic_get_ok.delivery_tag = next_consumer_delivery_tag(basic_get_ok.delivery_tag)
2248
2263
  @basic_get_continuations.push([basic_get_ok, properties, content])
2249
2264
  end
2250
2265
 
@@ -2264,9 +2279,11 @@ module Bunny
2264
2279
  @last_consumer = consumer
2265
2280
  end
2266
2281
 
2282
+ # the server counts deliveries for unknown consumers as well
2283
+ delivery_tag = next_consumer_delivery_tag(basic_deliver.delivery_tag)
2267
2284
  if consumer
2268
2285
  @work_pool.submit do
2269
- deliver_to_consumer(consumer, basic_deliver, properties, content)
2286
+ deliver_to_consumer(consumer, basic_deliver, properties, content, delivery_tag)
2270
2287
  end
2271
2288
  else
2272
2289
  @logger.warn "No consumer for tag #{basic_deliver.consumer_tag} on channel #{@id}!"
@@ -2274,9 +2291,9 @@ module Bunny
2274
2291
  end
2275
2292
 
2276
2293
  # @private
2277
- def deliver_to_consumer(consumer, basic_deliver, properties, content)
2294
+ def deliver_to_consumer(consumer, basic_deliver, properties, content, delivery_tag = basic_deliver.delivery_tag)
2278
2295
  begin
2279
- consumer.call(DeliveryInfo.new(basic_deliver, consumer, self), MessageProperties.new(properties), content)
2296
+ consumer.call(DeliveryInfo.new(basic_deliver, consumer, self, delivery_tag), MessageProperties.new(properties), content)
2280
2297
  rescue StandardError => e
2281
2298
  @uncaught_exception_handler.call(e, consumer) if @uncaught_exception_handler
2282
2299
  end
@@ -2724,11 +2741,24 @@ module Bunny
2724
2741
  Concurrent::ContinuationQueue.new
2725
2742
  end
2726
2743
 
2744
+ # @private
2745
+ def next_consumer_delivery_tag(tag)
2746
+ @last_consumer_delivery_tag = tag + @consumer_delivery_tag_offset
2747
+ end
2748
+
2749
+ # A tag at or below the offset was delivered before the channel was reopened or recovered.
2750
+ # RabbitMQ has requeued that delivery already, and acknowledging it here would close the channel
2751
+ # with an unknown delivery tag, or acknowledge a different delivery.
2727
2752
  # @private
2728
2753
  def guarding_against_stale_delivery_tags(tag, &block)
2729
- case tag
2730
- when Integer then block.call
2731
- end
2754
+ return unless tag.is_a?(Integer)
2755
+ # 0 with multiple set covers all outstanding deliveries
2756
+ return block.call(tag) if tag.zero?
2757
+ # read once, a concurrent #open may change it
2758
+ offset = @consumer_delivery_tag_offset
2759
+ return if tag <= offset
2760
+
2761
+ block.call(tag - offset)
2732
2762
  end
2733
2763
  end # Channel
2734
2764
  end # Bunny
@@ -22,10 +22,11 @@ module Bunny
22
22
  attr_reader :channel
23
23
 
24
24
  # @private
25
- def initialize(basic_deliver, consumer, channel)
25
+ def initialize(basic_deliver, consumer, channel, delivery_tag = basic_deliver.delivery_tag)
26
26
  @basic_deliver = basic_deliver
27
27
  @consumer = consumer
28
28
  @channel = channel
29
+ @delivery_tag = delivery_tag
29
30
  end
30
31
 
31
32
  # Iterates over delivery properties
@@ -39,7 +40,7 @@ module Bunny
39
40
  def [](k)
40
41
  case k
41
42
  when :consumer_tag then @basic_deliver.consumer_tag
42
- when :delivery_tag then @basic_deliver.delivery_tag
43
+ when :delivery_tag then @delivery_tag
43
44
  when :redelivered then @basic_deliver.redelivered
44
45
  when :exchange then @basic_deliver.exchange
45
46
  when :routing_key then @basic_deliver.routing_key
@@ -53,7 +54,7 @@ module Bunny
53
54
  def to_hash
54
55
  @hash ||= {
55
56
  consumer_tag: @basic_deliver.consumer_tag,
56
- delivery_tag: @basic_deliver.delivery_tag,
57
+ delivery_tag: @delivery_tag,
57
58
  redelivered: @basic_deliver.redelivered,
58
59
  exchange: @basic_deliver.exchange,
59
60
  routing_key: @basic_deliver.routing_key,
@@ -79,7 +80,7 @@ module Bunny
79
80
 
80
81
  # @return [String] Delivery identifier that is used to acknowledge, reject and nack deliveries
81
82
  def delivery_tag
82
- @basic_deliver.delivery_tag
83
+ @delivery_tag
83
84
  end
84
85
 
85
86
  # @return [Boolean] true if this delivery is a redelivery (the message was requeued at least once)
data/lib/bunny/session.rb CHANGED
@@ -112,7 +112,9 @@ module Bunny
112
112
  # @option connection_string_or_opts [String] :tls_key (nil) Path to client TLS/SSL private key file (.pem)
113
113
  # @option connection_string_or_opts [Array<String>] :tls_ca_certificates Array of paths to TLS/SSL CA files (.pem), by default detected from OpenSSL configuration
114
114
  # @option connection_string_or_opts [String] :verify_peer (true) Whether TLS peer verification should be performed
115
- # @option connection_string_or_opts [Symbol] :tls_protocol (negotiated) What TLS version should be used (:TLSv1, :TLSv1_1, or :TLSv1_2)
115
+ # @option connection_string_or_opts [Symbol] :tls_protocol (nil) [DEPRECATED] Use :tls_min_version and :tls_max_version instead. Pins the TLS version (:TLSv1, :TLSv1_1, :TLSv1_2 or :TLSv1_3)
116
+ # @option connection_string_or_opts [Symbol, String] :tls_min_version (:TLSv1_2) Lowest TLS version to negotiate, ignored when :tls_protocol is set
117
+ # @option connection_string_or_opts [Symbol, String] :tls_max_version (nil) Highest TLS version to negotiate, ignored when :tls_protocol is set
116
118
  # @option connection_string_or_opts [Integer] :channel_max (2047) Maximum number of channels allowed on this connection, minus 1 to account for the special channel 0.
117
119
  # @option connection_string_or_opts [Integer] :continuation_timeout (15000) Timeout for client operations that expect a response (e.g. {Bunny::Queue#get}), in milliseconds.
118
120
  # @option connection_string_or_opts [Integer] :connection_timeout (30) Timeout in seconds for connecting to the server.
@@ -251,6 +253,10 @@ module Bunny
251
253
  if (options[:host] || options[:hostname]) && (options[:hosts] || options[:addresses])
252
254
  @logger.warn "Connection options contain both a host and an array of hosts (addresses), please pick one."
253
255
  end
256
+
257
+ if options[:tls_protocol] || options[:ssl_version]
258
+ warn "[DEPRECATION] `:tls_protocol` is deprecated. Please use `:tls_min_version` and `:tls_max_version` instead."
259
+ end
254
260
  end
255
261
 
256
262
  # @return [String] RabbitMQ hostname (or IP address) used
@@ -42,7 +42,7 @@ module Bunny
42
42
  # older OpenSSL versions won't support for TLS 1.3 and won't
43
43
  # have this constant defined.
44
44
  if defined?(OpenSSL::SSL::TLS1_3_VERSION)
45
- TLS_VERSION_ALIASES["1.3"] = OpenSSL::SSL::TLS1_3_VERSION
45
+ TLS_VERSION_ALIASES[:"1.3"] = OpenSSL::SSL::TLS1_3_VERSION
46
46
  TLS_VERSION_ALIASES[:TLSv1_3] = OpenSSL::SSL::TLS1_3_VERSION
47
47
  TLS_VERSION_ALIASES[OpenSSL::SSL::TLS1_3_VERSION] = OpenSSL::SSL::TLS1_3_VERSION
48
48
  end
@@ -538,12 +538,11 @@ but prone to man-in-the-middle attacks. Please set verify_peer: true in producti
538
538
  MSG
539
539
  end
540
540
 
541
- ssl_version = opts[:tls_protocol] || opts[:ssl_version] || :TLSv1_2
542
- if ssl_version
543
- v = tls_version_constant(ssl_version)
544
- ctx.min_version = v
545
- ctx.max_version = v
546
- end
541
+ pinned = opts[:tls_protocol] || opts[:ssl_version]
542
+ min = pinned || opts[:tls_min_version] || :TLSv1_2
543
+ max = pinned || opts[:tls_max_version]
544
+ ctx.min_version = tls_version_constant(min)
545
+ ctx.max_version = tls_version_constant(max) if max
547
546
 
548
547
  ctx
549
548
  end
@@ -576,6 +575,7 @@ but prone to man-in-the-middle attacks. Please set verify_peer: true in producti
576
575
  # OpenSSL::SSL::TLS1_3_VERSION and similar constants
577
576
  # are just integers, so use the value itself as fallback since
578
577
  # there is no class to case switch on
578
+ value = value.to_sym if value.is_a?(String)
579
579
  TLS_VERSION_ALIASES[value] || value
580
580
  end
581
581
 
data/lib/bunny/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Bunny
4
4
  # @return [String] Version of the library
5
- VERSION = "3.5.0"
5
+ VERSION = "3.6.0"
6
6
  end
data/lib/bunny.rb CHANGED
@@ -71,7 +71,9 @@ module Bunny
71
71
  # @option connection_string_or_opts [String] :tls_key (nil) Path to client TLS/SSL private key file (.pem)
72
72
  # @option connection_string_or_opts [Array<String>] :tls_ca_certificates Array of paths to TLS/SSL CA files (.pem), by default detected from OpenSSL configuration
73
73
  # @option connection_string_or_opts [String] :verify_peer (true) Whether TLS peer verification should be performed
74
- # @option connection_string_or_opts [Symbol] :tls_protocol (negotiated) What TLS version should be used (:TLSv1, :TLSv1_1, or :TLSv1_2)
74
+ # @option connection_string_or_opts [Symbol] :tls_protocol (nil) [DEPRECATED] Use :tls_min_version and :tls_max_version instead. Pins the TLS version (:TLSv1, :TLSv1_1, :TLSv1_2 or :TLSv1_3)
75
+ # @option connection_string_or_opts [Symbol, String] :tls_min_version (:TLSv1_2) Lowest TLS version to negotiate, ignored when :tls_protocol is set
76
+ # @option connection_string_or_opts [Symbol, String] :tls_max_version (nil) Highest TLS version to negotiate, ignored when :tls_protocol is set
75
77
  # @option connection_string_or_opts [Integer] :channel_max (2047) Maximum number of channels allowed on this connection, minus 1 to account for the special channel 0.
76
78
  # @option connection_string_or_opts [Integer] :continuation_timeout (15000) Timeout for client operations that expect a response (e.g. {Bunny::Queue#get}), in milliseconds.
77
79
  # @option connection_string_or_opts [Integer] :connection_timeout (30) Timeout in seconds for connecting to the server.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.0
4
+ version: 3.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Duncan
@@ -19,14 +19,14 @@ dependencies:
19
19
  requirements:
20
20
  - - "~>"
21
21
  - !ruby/object:Gem::Version
22
- version: '2.9'
22
+ version: '2.10'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - "~>"
28
28
  - !ruby/object:Gem::Version
29
- version: '2.9'
29
+ version: '2.10'
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: logger
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -150,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
152
  requirements: []
153
- rubygems_version: 3.6.9
153
+ rubygems_version: 4.0.16
154
154
  specification_version: 4
155
155
  summary: Popular easy to use Ruby client for RabbitMQ
156
156
  test_files: []