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.
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+
5
+ module Bunny
6
+ module Concurrent
7
+ # A thread-safe exception accumulator that stores exceptions for later retrieval
8
+ # instead of immediately raising them in the calling thread.
9
+ #
10
+ # This is the default session error handler in Bunny. When errors occur in
11
+ # background threads (such as the reader loop or transport), they are stored
12
+ # in the accumulator rather than being raised asynchronously in the thread
13
+ # that created the session.
14
+ #
15
+ # This prevents dangerous control flow interruptions that can occur when
16
+ # exceptions are raised asynchronously via Thread#raise.
17
+ #
18
+ # @example Checking for accumulated exceptions
19
+ # conn = Bunny.new
20
+ # conn.start
21
+ # # ... do work ...
22
+ # if conn.exception_occurred?
23
+ # exceptions = conn.exceptions
24
+ # # handle exceptions appropriately
25
+ # end
26
+ #
27
+ # @see https://github.com/ruby-amqp/bunny/issues/721
28
+ # @api public
29
+ class ExceptionAccumulator
30
+ def initialize
31
+ @exceptions = []
32
+ @mutex = Mutex.new
33
+ end
34
+
35
+ # Called by background threads to record an exception.
36
+ # This method is compatible with Thread#raise interface.
37
+ #
38
+ # @param exception [Exception] the exception to accumulate
39
+ def raise(exception)
40
+ @mutex.synchronize do
41
+ @exceptions << exception
42
+ end
43
+ end
44
+
45
+ # Returns true if any exceptions have been accumulated.
46
+ #
47
+ # @return [Boolean]
48
+ def any?
49
+ @mutex.synchronize { @exceptions.any? }
50
+ end
51
+
52
+ # Returns true if no exceptions have been accumulated.
53
+ #
54
+ # @return [Boolean]
55
+ def empty?
56
+ @mutex.synchronize { @exceptions.empty? }
57
+ end
58
+
59
+ # Returns the number of accumulated exceptions.
60
+ #
61
+ # @return [Integer]
62
+ def count
63
+ @mutex.synchronize { @exceptions.count }
64
+ end
65
+
66
+ # Returns all accumulated exceptions.
67
+ #
68
+ # @return [Array<Exception>]
69
+ def all
70
+ @mutex.synchronize { @exceptions.dup }
71
+ end
72
+
73
+ # Returns and removes the first accumulated exception (FIFO order).
74
+ # Returns nil if no exceptions have been accumulated.
75
+ #
76
+ # @return [Exception, nil]
77
+ def pop
78
+ @mutex.synchronize { @exceptions.shift }
79
+ end
80
+
81
+ # Clears all accumulated exceptions.
82
+ #
83
+ # @return [Array<Exception>] the exceptions that were cleared
84
+ def clear
85
+ @mutex.synchronize do
86
+ cleared = @exceptions.dup
87
+ @exceptions.clear
88
+ cleared
89
+ end
90
+ end
91
+
92
+ # Raises the first accumulated exception if any exist, removing it from the accumulator.
93
+ # Does nothing if no exceptions have been accumulated.
94
+ #
95
+ # @raise [Exception] the first accumulated exception
96
+ def raise_first!
97
+ exception = pop
98
+ Kernel.raise exception if exception
99
+ end
100
+
101
+ # Raises all accumulated exceptions wrapped in a single exception if any exist,
102
+ # clearing the accumulator.
103
+ #
104
+ # @raise [Bunny::AccumulatedExceptions] wrapper containing all accumulated exceptions
105
+ def raise_all!
106
+ exceptions = clear
107
+ return if exceptions.empty?
108
+ Kernel.raise AccumulatedExceptions.new(exceptions)
109
+ end
110
+ end
111
+ end
112
+
113
+ # Alias for backward compatibility and convenience
114
+ ExceptionAccumulator = Concurrent::ExceptionAccumulator
115
+ end
@@ -99,13 +99,13 @@ module Bunny
99
99
  # @return [Boolean] true if this consumer uses automatic acknowledgement mode
100
100
  # @api public
101
101
  def automatic_acknowledgement?
102
- @no_ack == true
102
+ @no_ack
103
103
  end
104
104
 
105
105
  # @return [Boolean] true if this consumer uses manual (explicit) acknowledgement mode
106
106
  # @api public
107
107
  def manual_acknowledgement?
108
- @no_ack == false
108
+ !@no_ack
109
109
  end
110
110
 
111
111
  # @return [String] Name of the queue this consumer is on
@@ -117,14 +117,5 @@ module Bunny
117
117
  @queue
118
118
  end
119
119
  end
120
-
121
- #
122
- # Recovery
123
- #
124
-
125
- # @private
126
- def recover_from_network_failure
127
- @channel.basic_consume_with(self)
128
- end
129
120
  end
130
121
  end
@@ -29,7 +29,8 @@ module Bunny
29
29
 
30
30
  def self.open(host, port, options = {})
31
31
  socket = ::Socket.tcp(host, port, nil, nil,
32
- connect_timeout: options[:connect_timeout])
32
+ connect_timeout: options[:connect_timeout],
33
+ resolv_timeout: options[:connect_timeout])
33
34
  if ::Socket.constants.include?('TCP_NODELAY') || ::Socket.constants.include?(:TCP_NODELAY)
34
35
  socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
35
36
  end
@@ -73,6 +74,37 @@ module Bunny
73
74
  value
74
75
  end # read_fully
75
76
 
77
+ # Reads given number of bytes into an existing buffer with an optional timeout
78
+ #
79
+ # @param [String] buffer Buffer to read into (will be appended to)
80
+ # @param [Integer] count How many bytes to read
81
+ # @param [Integer] timeout Timeout
82
+ #
83
+ # @return [String] The buffer with data appended
84
+ # @api public
85
+ def read_fully_into(buffer, count, timeout = nil)
86
+ return nil if @__bunny_socket_eof_flag__
87
+
88
+ bytes_read = 0
89
+ begin
90
+ loop do
91
+ chunk = read_nonblock(count - bytes_read)
92
+ buffer << chunk
93
+ bytes_read += chunk.bytesize
94
+ break if bytes_read >= count
95
+ end
96
+ rescue EOFError
97
+ @__bunny_socket_eof_flag__ = true
98
+ rescue *READ_RETRY_EXCEPTION_CLASSES
99
+ if IO.select([self], nil, nil, timeout)
100
+ retry
101
+ else
102
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
103
+ end
104
+ end
105
+ buffer
106
+ end # read_fully_into
107
+
76
108
  # Writes provided data using IO#write_nonblock, taking care of handling
77
109
  # of exceptions it raises when writing would fail (e.g. due to socket buffer
78
110
  # being full).
@@ -69,6 +69,47 @@ module Bunny
69
69
  value
70
70
  end
71
71
 
72
+ # Reads given number of bytes into an existing buffer with an optional timeout
73
+ #
74
+ # @param [String] buffer Buffer to read into (will be appended to)
75
+ # @param [Integer] count How many bytes to read
76
+ # @param [Integer] timeout Timeout
77
+ #
78
+ # @return [String] The buffer with data appended
79
+ # @api public
80
+ def read_fully_into(buffer, count, timeout = nil)
81
+ return nil if @__bunny_socket_eof_flag__
82
+
83
+ bytes_read = 0
84
+ begin
85
+ loop do
86
+ chunk = read_nonblock(count - bytes_read)
87
+ buffer << chunk
88
+ bytes_read += chunk.bytesize
89
+ break if bytes_read >= count
90
+ end
91
+ rescue EOFError
92
+ @__bunny_socket_eof_flag__ = true
93
+ rescue OpenSSL::SSL::SSLError => e
94
+ if e.message == "read would block"
95
+ if IO.select([self], nil, nil, timeout)
96
+ retry
97
+ else
98
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
99
+ end
100
+ else
101
+ raise e
102
+ end
103
+ rescue *READ_RETRY_EXCEPTION_CLASSES
104
+ if IO.select([self], nil, nil, timeout)
105
+ retry
106
+ else
107
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
108
+ end
109
+ end
110
+ buffer
111
+ end
112
+
72
113
  # Writes provided data using IO#write_nonblock, taking care of handling
73
114
  # of exceptions it raises when writing would fail (e.g. due to socket buffer
74
115
  # being full).
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bunny/versioned_delivery_tag"
4
-
5
3
  module Bunny
6
- # Wraps {AMQ::Protocol::Basic::Deliver} to
4
+ # Wraps [AMQ::Protocol::Basic::Deliver] to
7
5
  # provide access to the delivery properties as immutable hash as
8
- # well as methods.
6
+ # well as methods. Hash representation is created lazily.
9
7
  class DeliveryInfo
10
8
 
11
9
  #
@@ -26,15 +24,6 @@ module Bunny
26
24
  # @private
27
25
  def initialize(basic_deliver, consumer, channel)
28
26
  @basic_deliver = basic_deliver
29
- @hash = {
30
- :consumer_tag => basic_deliver.consumer_tag,
31
- :delivery_tag => VersionedDeliveryTag.new(basic_deliver.delivery_tag, channel.recoveries_counter),
32
- :redelivered => basic_deliver.redelivered,
33
- :exchange => basic_deliver.exchange,
34
- :routing_key => basic_deliver.routing_key,
35
- :consumer => consumer,
36
- :channel => channel
37
- }
38
27
  @consumer = consumer
39
28
  @channel = channel
40
29
  end
@@ -42,18 +31,35 @@ module Bunny
42
31
  # Iterates over delivery properties
43
32
  # @see Enumerable#each
44
33
  def each(*args, &block)
45
- @hash.each(*args, &block)
34
+ to_hash.each(*args, &block)
46
35
  end
47
36
 
48
37
  # Accesses delivery properties by key
49
38
  # @see Hash#[]
50
39
  def [](k)
51
- @hash[k]
40
+ case k
41
+ when :consumer_tag then @basic_deliver.consumer_tag
42
+ when :delivery_tag then @basic_deliver.delivery_tag
43
+ when :redelivered then @basic_deliver.redelivered
44
+ when :exchange then @basic_deliver.exchange
45
+ when :routing_key then @basic_deliver.routing_key
46
+ when :consumer then @consumer
47
+ when :channel then @channel
48
+ else nil
49
+ end
52
50
  end
53
51
 
54
52
  # @return [Hash] Hash representation of this delivery info
55
53
  def to_hash
56
- @hash
54
+ @hash ||= {
55
+ :consumer_tag => @basic_deliver.consumer_tag,
56
+ :delivery_tag => @basic_deliver.delivery_tag,
57
+ :redelivered => @basic_deliver.redelivered,
58
+ :exchange => @basic_deliver.exchange,
59
+ :routing_key => @basic_deliver.routing_key,
60
+ :consumer => @consumer,
61
+ :channel => @channel
62
+ }
57
63
  end
58
64
 
59
65
  # @private
@@ -6,6 +6,9 @@ module Bunny
6
6
  class Exception < ::StandardError
7
7
  end
8
8
 
9
+ # Used when a list of endpoints (hostnames) to connect to
10
+ # has been fully traversed, that is, there are no more endpoints (hostnames)
11
+ # to try.
9
12
  class HostListDepleted < Exception
10
13
  def initialize
11
14
  super("No more hosts to try in the supplied list of hosts")
@@ -163,7 +166,7 @@ module Bunny
163
166
  class MessageError < ConnectionLevelException; end
164
167
  # @private
165
168
  class ProtocolError < ConnectionLevelException; end
166
- # Raised when RabbitMQ reports and internal error
169
+ # Raised when RabbitMQ reports an internal error
167
170
  class InternalError < ConnectionLevelException; end
168
171
 
169
172
  # Raised when read or write I/O operations time out (but only if
@@ -177,7 +180,7 @@ module Bunny
177
180
  class InconsistentDataError < Exception
178
181
  end
179
182
 
180
- # Raised by adapters when frame does not end with {final octet AMQ::Protocol::Frame::FINAL_OCTET}.
183
+ # Raised by adapters when frame does not end with final octet [AMQ::Protocol::Frame::FINAL_OCTET].
181
184
  # This suggest that there is a bug in adapter or AMQ broker implementation.
182
185
  #
183
186
  # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.3)
@@ -209,6 +212,18 @@ module Bunny
209
212
  end
210
213
  end
211
214
 
215
+ # Raised when a published message is nacked by the broker
216
+ # and publisher confirm tracking is enabled.
217
+ # @api public
218
+ class MessageNacked < Exception
219
+ attr_reader :delivery_tag
220
+
221
+ def initialize(message, delivery_tag)
222
+ super(message)
223
+ @delivery_tag = delivery_tag
224
+ end
225
+ end
226
+
212
227
  # Raised when RabbitMQ responds with 406 PRECONDITION_FAILED
213
228
  class PreconditionFailed < ChannelLevelException
214
229
  end
@@ -268,4 +283,18 @@ module Bunny
268
283
  # @private
269
284
  class MissingTLSKeyFile < Exception
270
285
  end
286
+
287
+ # Exception wrapper that holds multiple accumulated exceptions.
288
+ # Raised by ExceptionAccumulator#raise_all! when multiple exceptions occurred.
289
+ #
290
+ # @api public
291
+ class AccumulatedExceptions < Exception
292
+ attr_reader :exceptions
293
+
294
+ def initialize(exceptions)
295
+ @exceptions = exceptions
296
+ messages = exceptions.map { |e| "#{e.class}: #{e.message}" }
297
+ super("#{exceptions.count} exception(s) accumulated:\n #{messages.join("\n ")}")
298
+ end
299
+ end
271
300
  end
@@ -9,6 +9,24 @@ module Bunny
9
9
  # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
10
10
  class Exchange
11
11
 
12
+ #
13
+ # Exchange type constants
14
+ #
15
+
16
+ # Standard AMQP 0-9-1 exchange types
17
+ TYPE_DIRECT = :direct
18
+ TYPE_FANOUT = :fanout
19
+ TYPE_TOPIC = :topic
20
+ TYPE_HEADERS = :headers
21
+
22
+ # In RabbitMQ core since 4.3.0
23
+ TYPE_MODULUS_HASH = :"x-modulus-hash"
24
+ # In RabbitMQ core since 4.2.0
25
+ TYPE_LOCAL_RANDOM = :"x-local-random"
26
+ # Provided by commonly used plugins
27
+ TYPE_CONSISTENT_HASH = :"x-consistent-hash"
28
+ TYPE_RANDOM = :"x-random"
29
+
12
30
  #
13
31
  # API
14
32
  #
@@ -19,7 +37,7 @@ module Bunny
19
37
  # @return [String]
20
38
  attr_reader :name
21
39
 
22
- # Type of this exchange (one of: :direct, :fanout, :topic, :headers).
40
+ # Type of this exchange (e.g. :direct, :fanout, :topic, :headers, :"x-consistent-hash", :"x-modulus-hash").
23
41
  # @return [Symbol]
24
42
  attr_reader :type
25
43
 
@@ -88,7 +106,10 @@ module Bunny
88
106
 
89
107
  declare! unless opts[:no_declare] || predeclared? || (@name == AMQ::Protocol::EMPTY_STRING)
90
108
 
109
+ # for basic.return dispatch and such
91
110
  @channel.register_exchange(self)
111
+ # for topology recovery
112
+ @channel.record_exchange(self)
92
113
  end
93
114
 
94
115
  # @return [Boolean] true if this exchange was declared as durable (will survive broker restart).
@@ -140,7 +161,8 @@ module Bunny
140
161
  # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
141
162
  # @api public
142
163
  def publish(payload, opts = {})
143
- @channel.basic_publish(payload, self.name, (opts.delete(:routing_key) || opts.delete(:key)), opts)
164
+ rk = opts[:routing_key] || opts[:key]
165
+ @channel.basic_publish(payload, self.name, rk, opts)
144
166
 
145
167
  self
146
168
  end
@@ -155,7 +177,7 @@ module Bunny
155
177
  # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
156
178
  # @api public
157
179
  def delete(opts = {})
158
- @channel.deregister_exchange(self)
180
+ @channel.delete_recorded_exchange(self)
159
181
  @channel.exchange_delete(@name, opts) unless predeclared?
160
182
  end
161
183
 
@@ -220,16 +242,6 @@ module Bunny
220
242
  @channel.wait_for_confirms
221
243
  end
222
244
 
223
- # @private
224
- def recover_from_network_failure
225
- declare! unless @options[:no_declare] ||predefined?
226
-
227
- @bindings.each do |b|
228
- bind(b[:source], b[:opts])
229
- end
230
- end
231
-
232
-
233
245
  #
234
246
  # Implementation
235
247
  #
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bunny/versioned_delivery_tag"
4
-
5
3
  module Bunny
6
4
  # Wraps {AMQ::Protocol::Basic::GetOk} to
7
5
  # provide access to the delivery properties as immutable hash as
8
- # well as methods.
6
+ # well as methods. Hash representation is created lazily.
9
7
  class GetResponse
10
8
 
11
9
  #
@@ -23,32 +21,38 @@ module Bunny
23
21
 
24
22
  # @private
25
23
  def initialize(get_ok, channel)
26
- @get_ok = get_ok
27
- @hash = {
28
- :delivery_tag => @get_ok.delivery_tag,
29
- :redelivered => @get_ok.redelivered,
30
- :exchange => @get_ok.exchange,
31
- :routing_key => @get_ok.routing_key,
32
- :channel => channel
33
- }
34
- @channel = channel
24
+ @get_ok = get_ok
25
+ @channel = channel
35
26
  end
36
27
 
37
28
  # Iterates over the delivery properties
38
29
  # @see Enumerable#each
39
30
  def each(*args, &block)
40
- @hash.each(*args, &block)
31
+ to_hash.each(*args, &block)
41
32
  end
42
33
 
43
34
  # Accesses delivery properties by key
44
35
  # @see Hash#[]
45
36
  def [](k)
46
- @hash[k]
37
+ case k
38
+ when :delivery_tag then @get_ok.delivery_tag
39
+ when :redelivered then @get_ok.redelivered
40
+ when :exchange then @get_ok.exchange
41
+ when :routing_key then @get_ok.routing_key
42
+ when :channel then @channel
43
+ else nil
44
+ end
47
45
  end
48
46
 
49
47
  # @return [Hash] Hash representation of this delivery info
50
48
  def to_hash
51
- @hash
49
+ @hash ||= {
50
+ :delivery_tag => @get_ok.delivery_tag,
51
+ :redelivered => @get_ok.redelivered,
52
+ :exchange => @get_ok.exchange,
53
+ :routing_key => @get_ok.routing_key,
54
+ :channel => @channel
55
+ }
52
56
  end
53
57
 
54
58
  # @private
@@ -53,10 +53,10 @@ module Bunny
53
53
  sleep @interval
54
54
  end
55
55
  rescue IOError => ioe
56
- @logger.error "I/O error in the hearbeat sender: #{ioe.message}"
56
+ @logger.error "I/O error in the heartbeat sender: #{ioe.message}"
57
57
  stop
58
58
  rescue ::Exception => e
59
- @logger.error "Error in the hearbeat sender: #{e.message}"
59
+ @logger.error "Error in the heartbeat sender: #{e.message}"
60
60
  stop
61
61
  end
62
62
  end
data/lib/bunny/queue.rb CHANGED
@@ -17,8 +17,10 @@ module Bunny
17
17
  QUORUM = "quorum"
18
18
  CLASSIC = "classic"
19
19
  STREAM = "stream"
20
+ DELAYED = "delayed"
21
+ JMS = "jms"
20
22
 
21
- KNOWN = [CLASSIC, QUORUM, STREAM]
23
+ KNOWN = [CLASSIC, QUORUM, STREAM, DELAYED, JMS]
22
24
 
23
25
  def self.known?(q_type)
24
26
  KNOWN.include?(q_type)
@@ -28,6 +30,13 @@ module Bunny
28
30
  module XArgs
29
31
  MAX_LENGTH = "x-max-length",
30
32
  QUEUE_TYPE = "x-queue-type"
33
+
34
+ DELAYED_RETRY_TYPE = "x-delayed-retry-type"
35
+ DELAYED_RETRY_MIN = "x-delayed-retry-min"
36
+ DELAYED_RETRY_MAX = "x-delayed-retry-max"
37
+
38
+ SELECTOR_FIELDS = "x-selector-fields"
39
+ SELECTOR_FIELD_MAX_BYTES = "x-selector-field-max-bytes"
31
40
  end
32
41
 
33
42
  # @return [Bunny::Channel] Channel this queue uses
@@ -44,7 +53,7 @@ module Bunny
44
53
  # @option opts [Boolean] :durable (false) Should this queue be durable?
45
54
  # @option opts [Boolean] :auto_delete (false) Should this queue be automatically deleted when the last consumer disconnects?
46
55
  # @option opts [Boolean] :exclusive (false) Should this queue be exclusive (only can be used by this connection, removed when the connection is closed)?
47
- # @option opts [String] :type (nil) Type of the declared queue (classic, quorum or stream)
56
+ # @option opts [String] :type (nil) Type of the declared queue (classic, quorum, stream, delayed, or jms)
48
57
  # @option opts [Hash] :arguments (nil) Additional optional arguments (typically used by RabbitMQ extensions and plugins)
49
58
  #
50
59
  # @see Bunny::Channel#queue
@@ -80,7 +89,10 @@ module Bunny
80
89
 
81
90
  declare! unless opts[:no_declare]
82
91
 
92
+ # for basic.deliver dispatch and such
83
93
  @channel.register_queue(self)
94
+ # for topology recovery
95
+ @channel.record_queue(self)
84
96
  end
85
97
 
86
98
  # @return [Boolean] true if this queue was declared as durable (will survive broker restart).
@@ -117,6 +129,13 @@ module Bunny
117
129
  @arguments
118
130
  end
119
131
 
132
+ # @param value [String]
133
+ # @private
134
+ def update_name_to(value)
135
+ @name = value
136
+ self
137
+ end
138
+
120
139
  def to_s
121
140
  oid = ("0x%x" % (self.object_id << 1))
122
141
  "<#{self.class.name}:#{oid} @name=\"#{name}\" channel=#{@channel.to_s} @durable=#{@durable} @auto_delete=#{@auto_delete} @exclusive=#{@exclusive} @arguments=#{@arguments}>"
@@ -320,6 +339,7 @@ module Bunny
320
339
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
321
340
  # @api public
322
341
  def delete(opts = {})
342
+ @channel.delete_recorded_queue_named(self.name)
323
343
  @channel.deregister_queue(self)
324
344
  @channel.queue_delete(@name, opts)
325
345
  end
@@ -362,42 +382,6 @@ module Bunny
362
382
  "unsupported queue type #{q_type.inspect}, supported ones: #{Types::KNOWN.join(', ')}") if (q_type and !Types.known?(q_type))
363
383
  end
364
384
 
365
- #
366
- # Recovery
367
- #
368
-
369
- # @private
370
- def recover_from_network_failure
371
- if self.server_named?
372
- old_name = @name.dup
373
- @name = AMQ::Protocol::EMPTY_STRING
374
-
375
- @channel.deregister_queue_named(old_name)
376
- end
377
-
378
- # TODO: inject and use logger
379
- # puts "Recovering queue #{@name}"
380
- begin
381
- declare! unless @options[:no_declare]
382
-
383
- @channel.register_queue(self)
384
- rescue Exception => e
385
- # TODO: inject and use logger
386
- puts "Caught #{e.inspect} while redeclaring and registering #{@name}!"
387
- end
388
- recover_bindings
389
- end
390
-
391
- # @private
392
- def recover_bindings
393
- @bindings.each do |b|
394
- # TODO: inject and use logger
395
- # puts "Recovering binding #{b.inspect}"
396
- self.bind(b[:exchange], b)
397
- end
398
- end
399
-
400
-
401
385
  #
402
386
  # Implementation
403
387
  #
@@ -43,9 +43,13 @@ module Bunny
43
43
  OpenSSL::OpenSSLError => e
44
44
  break if terminate? || @session.closing? || @session.closed?
45
45
 
46
- @network_is_down = true
46
+ @mutex.synchronize do
47
+ @stopping = true
48
+ @network_is_down = true
49
+ end
50
+
47
51
  if @session.automatically_recover?
48
- log_exception(e, level: :warn)
52
+ log_exception(e, level: :debug)
49
53
  @session.handle_network_failure(e)
50
54
  else
51
55
  log_exception(e)
@@ -62,10 +66,6 @@ module Bunny
62
66
  @network_is_down = true
63
67
  @session_error_handler.raise(Bunny::NetworkFailure.new("caught an unexpected exception in the network loop: #{e.message}", e))
64
68
  end
65
- rescue Errno::EBADF => _ebadf
66
- break if terminate?
67
- # ignored, happens when we loop after the transport has already been closed
68
- @mutex.synchronize { @stopping = true }
69
69
  end
70
70
  end
71
71