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.
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Bunny
4
4
  # Wraps AMQ::Protocol::Basic::Return to
5
- # provide access to the delivery properties as immutable hash as
6
- # well as methods.
5
+ # provide access to the return properties as immutable hash as
6
+ # well as methods. Hash representation is created lazily.
7
7
  class ReturnInfo
8
8
 
9
9
  #
@@ -18,29 +18,34 @@ module Bunny
18
18
 
19
19
  def initialize(basic_return)
20
20
  @basic_return = basic_return
21
- @hash = {
22
- :reply_code => basic_return.reply_code,
23
- :reply_text => basic_return.reply_text,
24
- :exchange => basic_return.exchange,
25
- :routing_key => basic_return.routing_key
26
- }
27
21
  end
28
22
 
29
23
  # Iterates over the returned delivery properties
30
24
  # @see Enumerable#each
31
25
  def each(*args, &block)
32
- @hash.each(*args, &block)
26
+ to_hash.each(*args, &block)
33
27
  end
34
28
 
35
29
  # Accesses returned delivery properties by key
36
30
  # @see Hash#[]
37
31
  def [](k)
38
- @hash[k]
32
+ case k
33
+ when :reply_code then @basic_return.reply_code
34
+ when :reply_text then @basic_return.reply_text
35
+ when :exchange then @basic_return.exchange
36
+ when :routing_key then @basic_return.routing_key
37
+ else nil
38
+ end
39
39
  end
40
40
 
41
41
  # @return [Hash] Hash representation of this returned delivery info
42
42
  def to_hash
43
- @hash
43
+ @hash ||= {
44
+ :reply_code => @basic_return.reply_code,
45
+ :reply_text => @basic_return.reply_text,
46
+ :exchange => @basic_return.exchange,
47
+ :routing_key => @basic_return.routing_key
48
+ }
44
49
  end
45
50
 
46
51
  # @private
data/lib/bunny/session.rb CHANGED
@@ -8,11 +8,14 @@ require "bunny/transport"
8
8
  require "bunny/channel_id_allocator"
9
9
  require "bunny/heartbeat_sender"
10
10
  require "bunny/reader_loop"
11
+ require "bunny/topology_registry"
12
+ require "bunny/topology_recovery_filter"
11
13
  require "bunny/authentication/credentials_encoder"
12
14
  require "bunny/authentication/plain_mechanism_encoder"
13
15
  require "bunny/authentication/external_mechanism_encoder"
14
16
 
15
17
  require "bunny/concurrent/continuation_queue"
18
+ require "bunny/concurrent/exception_accumulator"
16
19
 
17
20
  require "amq/protocol/client"
18
21
  require "amq/settings"
@@ -60,7 +63,7 @@ module Bunny
60
63
  :product => "Bunny",
61
64
  :platform => ::RUBY_DESCRIPTION,
62
65
  :version => Bunny::VERSION,
63
- :information => "http://rubybunny.info",
66
+ :information => "https://github.com/ruby-amqp/bunny",
64
67
  }
65
68
 
66
69
  # @private
@@ -80,6 +83,8 @@ module Bunny
80
83
  attr_reader :status, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
81
84
  attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
82
85
  attr_reader :channel_id_allocator
86
+ # @return [Bunny::TopologyRegistry]
87
+ attr_reader :topology_registry
83
88
  # Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
84
89
  # @return [String]
85
90
  attr_reader :mechanism
@@ -128,8 +133,9 @@ module Bunny
128
133
  # @option connection_string_or_opts [Proc] :recovery_completed (nil) Will be called after successful connection recovery
129
134
  # @option connection_string_or_opts [Proc] :recovery_attempts_exhausted (nil) Will be called when the connection recovery failed after the specified amount of recovery attempts
130
135
  # @option connection_string_or_opts [Boolean] :recover_from_connection_close (true) Should this connection recover after receiving a server-sent connection.close (e.g. connection was force closed)?
131
- # @option connection_string_or_opts [Object] :session_error_handler (Thread.current) Object which responds to #raise that will act as a session error handler. Defaults to Thread.current, which will raise asynchronous exceptions in the thread that created the session.
136
+ # @option connection_string_or_opts [Object] :session_error_handler (ExceptionAccumulator.new) Object which responds to #raise that will act as a session error handler. Defaults to a Bunny::ExceptionAccumulator instance which stores exceptions for safe retrieval later. Can be set to Thread.current for legacy behavior (raises asynchronous exceptions in the creating thread), or any object that responds to #raise.
132
137
  #
138
+ # @option connection_string_or_opts [Bunny::TopologyRecoveryFilter] :topology_recovery_filter if provided, will be used for object filtering during topology recovery
133
139
  # @option optz [String] :auth_mechanism ("PLAIN") Authentication mechanism, PLAIN or EXTERNAL
134
140
  # @option optz [String] :locale ("PLAIN") Locale RabbitMQ should use
135
141
  # @option optz [String] :connection_name (nil) Client-provided connection name, if any. Note that the value returned does not uniquely identify a connection and cannot be used as a connection identifier in HTTP API requests.
@@ -222,11 +228,14 @@ module Bunny
222
228
 
223
229
  @channels = Hash.new
224
230
 
231
+ trf = @opts.fetch(:topology_recovery_filter, DefaultTopologyRecoveryFilter.new)
232
+ @topology_registry = TopologyRegistry.new(topology_recovery_filter: trf)
233
+
225
234
  @recovery_attempt_started = opts[:recovery_attempt_started]
226
235
  @recovery_completed = opts[:recovery_completed]
227
- @recovery_attempts_exhausted = opts[:recovery_attempts_exhausted]
236
+ @recovery_attempts_exhausted = opts[:recovery_attempts_exhausted]
228
237
 
229
- @session_error_handler = opts.fetch(:session_error_handler, Thread.current)
238
+ @session_error_handler = opts.fetch(:session_error_handler, ExceptionAccumulator.new)
230
239
 
231
240
  @recoverable_exceptions = DEFAULT_RECOVERABLE_EXCEPTIONS.dup
232
241
 
@@ -298,7 +307,9 @@ module Bunny
298
307
  def configure_socket(&block)
299
308
  raise ArgumentError, "No block provided!" if block.nil?
300
309
 
301
- @transport.configure_socket(&block)
310
+ @transport_mutex.synchronize do
311
+ @transport.configure_socket(&block)
312
+ end
302
313
  end
303
314
 
304
315
  # @return [Integer] Client socket port
@@ -324,10 +335,11 @@ module Bunny
324
335
  begin
325
336
  # close existing transport if we have one,
326
337
  # to not leak sockets
327
- @transport.maybe_initialize_socket
328
-
329
- @transport.post_initialize_socket
330
- @transport.connect
338
+ @transport_mutex.synchronize do
339
+ @transport.maybe_initialize_socket
340
+ @transport.post_initialize_socket
341
+ @transport.connect
342
+ end
331
343
 
332
344
  self.init_connection
333
345
  self.open_connection
@@ -364,7 +376,7 @@ module Bunny
364
376
  end
365
377
 
366
378
  # Socket operation write timeout used by this connection
367
- # @return [Integer]
379
+ # @return [Float]
368
380
  # @private
369
381
  def transport_write_timeout
370
382
  @transport.write_timeout
@@ -384,7 +396,10 @@ module Bunny
384
396
  if n && (ch = @channels[n])
385
397
  ch
386
398
  else
387
- ch = Bunny::Channel.new(self, n, ConsumerWorkPool.new(consumer_pool_size || 1, consumer_pool_abort_on_exception, consumer_pool_shutdown_timeout))
399
+ work_pool = ConsumerWorkPool.new(consumer_pool_size || 1, consumer_pool_abort_on_exception, consumer_pool_shutdown_timeout)
400
+ ch = Bunny::Channel.new(self, n, {
401
+ work_pool: work_pool
402
+ })
388
403
  ch.open
389
404
  ch
390
405
  end
@@ -493,9 +508,59 @@ module Bunny
493
508
  @blocked
494
509
  end
495
510
 
511
+ # Returns the session error handler.
512
+ # By default, this is a Bunny::ExceptionAccumulator instance.
513
+ #
514
+ # @return [Object] the session error handler
515
+ # @api public
516
+ attr_reader :session_error_handler
517
+
518
+ # Returns true if any exceptions have been accumulated by the session error handler.
519
+ # Only works when using the default ExceptionAccumulator handler.
520
+ #
521
+ # @return [Boolean] true if exceptions have been accumulated
522
+ # @raise [NoMethodError] if the session error handler doesn't respond to #any?
523
+ # @api public
524
+ def exception_occurred?
525
+ @session_error_handler.any?
526
+ end
527
+
528
+ # Returns all accumulated exceptions from the session error handler.
529
+ # Only works when using the default ExceptionAccumulator handler.
530
+ #
531
+ # @return [Array<Exception>] all accumulated exceptions
532
+ # @raise [NoMethodError] if the session error handler doesn't respond to #all
533
+ # @api public
534
+ def exceptions
535
+ @session_error_handler.all
536
+ end
537
+
538
+ # Clears all accumulated exceptions from the session error handler.
539
+ # Only works when using the default ExceptionAccumulator handler.
540
+ #
541
+ # @return [Array<Exception>] the exceptions that were cleared
542
+ # @raise [NoMethodError] if the session error handler doesn't respond to #clear
543
+ # @api public
544
+ def clear_exceptions
545
+ @session_error_handler.clear
546
+ end
547
+
548
+ # Raises the first accumulated exception if any exist.
549
+ # Only works when using the default ExceptionAccumulator handler.
550
+ #
551
+ # This is useful for checking errors at safe points in your application,
552
+ # such as after completing a batch of operations.
553
+ #
554
+ # @raise [Exception] the first accumulated exception
555
+ # @raise [NoMethodError] if the session error handler doesn't respond to #raise_first!
556
+ # @api public
557
+ def raise_on_exception!
558
+ @session_error_handler.raise_first!
559
+ end
560
+
496
561
  # Parses an amqp[s] URI into a hash that {Bunny::Session#initialize} accepts.
497
562
  #
498
- # @param [String] uri amqp or amqps URI to parse
563
+ # @param [String | Hash] uri amqp or amqps URI to parse
499
564
  # @return [Hash] Parsed URI as a hash
500
565
  def self.parse_uri(uri)
501
566
  AMQ::Settings.configure(uri)
@@ -562,6 +627,17 @@ module Bunny
562
627
  @recovery_attempts_exhausted = block
563
628
  end
564
629
 
630
+ # Recovers topology (exchanges, queues, bindings, consumers) for a single channel.
631
+ # Intended for use after {Bunny::Channel#reopen} to restore the channel to a
632
+ # working state with its original consumers.
633
+ #
634
+ # @param [Bunny::Channel] ch Channel whose topology should be recovered
635
+ # @api public
636
+ def recover_channel_topology(ch)
637
+ filter = Proc.new { |entity| entity.channel == ch }
638
+ recover_topology_with(filter)
639
+ end
640
+
565
641
  #
566
642
  # Implementation
567
643
  #
@@ -741,13 +817,17 @@ module Bunny
741
817
  announce_network_failure_recovery
742
818
  @channel_mutex.synchronize do
743
819
  @channels.each do |n, ch|
820
+ ch.connection_closed!
744
821
  ch.maybe_kill_consumer_work_pool!
745
822
  end
746
823
  end
747
824
  @reader_loop.stop if @reader_loop
748
825
  maybe_shutdown_heartbeat_sender
749
826
 
750
- recover_from_network_failure
827
+ recover_connection_and_channels
828
+ recover_topology
829
+ mark_channels_after_recovery!
830
+ notify_of_recovery_completion
751
831
  else
752
832
  @logger.error "Exception #{exception.message} is considered unrecoverable..."
753
833
  end
@@ -757,16 +837,135 @@ module Bunny
757
837
  end
758
838
  end
759
839
 
840
+ # @return [Boolean]
760
841
  # @private
761
842
  def recoverable_network_failure?(exception)
762
843
  @recoverable_exceptions.any? {|x| exception.kind_of? x}
763
844
  end
764
845
 
846
+ # @return [Boolean]
765
847
  # @private
766
848
  def recovering_from_network_failure?
767
849
  @recovering_from_network_failure
768
850
  end
769
851
 
852
+ # @param [Bunny::Queue] queue
853
+ # @private
854
+ def record_queue(queue)
855
+ @topology_registry.record_queue(queue)
856
+ end
857
+
858
+ # @param [Bunny::Channel] ch
859
+ # @param [String] name
860
+ # @param [Boolean] server_named
861
+ # @param [Boolean] durable
862
+ # @param [Boolean] auto_delete
863
+ # @param [Boolean] exclusive
864
+ # @param [Hash] arguments
865
+ def record_queue_with(ch, name, server_named, durable, auto_delete, exclusive, arguments)
866
+ @topology_registry.record_queue_with(ch, name, server_named, durable, auto_delete, exclusive, arguments)
867
+ end
868
+
869
+ # @param [Bunny::Queue, Bunny::RecordedQueue] queue
870
+ # @private
871
+ def delete_recoreded_queue(queue)
872
+ @topology_registry.delete_recorded_queue(queue)
873
+ end
874
+
875
+ # @param [String] name
876
+ # @private
877
+ def delete_recorded_queue_named(name)
878
+ @topology_registry.delete_recorded_queue_named(name)
879
+ end
880
+
881
+ # @param [Bunny::Exchange] exchange
882
+ # @private
883
+ def record_exchange(exchange)
884
+ @topology_registry.record_exchange(exchange)
885
+ end
886
+
887
+ # @param [Bunny::Channel] ch
888
+ # @param [String] name
889
+ # @param [String] type
890
+ # @param [Boolean] durable
891
+ # @param [Boolean] auto_delete
892
+ # @param [Hash] arguments
893
+ def record_exchange_with(ch, name, type, durable, auto_delete, arguments)
894
+ @topology_registry.record_exchange_with(ch, name, type, durable, auto_delete, arguments)
895
+ end
896
+
897
+ # @param [Bunny::Exchange] exchange
898
+ # @private
899
+ def delete_recorded_exchange(exchange)
900
+ @topology_registry.delete_recorded_exchange(exchange)
901
+ end
902
+
903
+ # @param [String] name
904
+ # @private
905
+ def delete_recorded_exchange_named(name)
906
+ @topology_registry.delete_recorded_exchange_named(name)
907
+ end
908
+
909
+ # @param [Bunny::Channel] ch
910
+ # @param [String] exchange_name
911
+ # @param [String] queue_name
912
+ # @param [String] routing_key
913
+ # @param [Hash] arguments
914
+ # @private
915
+ def record_queue_binding_with(ch, exchange_name, queue_name, routing_key, arguments)
916
+ @topology_registry.record_queue_binding_with(ch, exchange_name, queue_name, routing_key, arguments)
917
+ end
918
+
919
+ # @param [Bunny::Channel] ch
920
+ # @param [String] exchange_name
921
+ # @param [String] queue_name
922
+ # @param [String] routing_key
923
+ # @param [Hash] arguments
924
+ # @private
925
+ def delete_recorded_queue_binding(ch, exchange_name, queue_name, routing_key, arguments)
926
+ @topology_registry.delete_recorded_queue_binding(ch, exchange_name, queue_name, routing_key, arguments)
927
+ end
928
+
929
+ # @param [Bunny::Channel] ch
930
+ # @param [String] source_name
931
+ # @param [String] destination_name
932
+ # @param [String] routing_key
933
+ # @param [Hash] arguments
934
+ # @private
935
+ def record_exchange_binding_with(ch, source_name, destination_name, routing_key, arguments)
936
+ @topology_registry.record_exchange_binding_with(ch, source_name, destination_name, routing_key, arguments)
937
+ end
938
+
939
+ # @param [Bunny::Channel] ch
940
+ # @param [String] source_name
941
+ # @param [String] destination_name
942
+ # @param [String] routing_key
943
+ # @param [Hash] arguments
944
+ # @private
945
+ def delete_recorded_exchange_binding(ch, source_name, destination_name, routing_key, arguments)
946
+ @topology_registry.delete_recorded_exchange_binding(ch, source_name, destination_name, routing_key, arguments)
947
+ end
948
+
949
+ # @param [Bunny::Channel] ch
950
+ # @param [String] consumer_tag
951
+ # @param [String] queue_name
952
+ # @param [#call] callable
953
+ # @param [Boolean] manual_ack
954
+ # @param [Boolean] exclusive
955
+ # @param [Hash] arguments
956
+ # @private
957
+ def record_consumer_with(ch, consumer_tag, queue_name, callable, manual_ack, exclusive, arguments)
958
+ @topology_registry.record_consumer_with(ch, consumer_tag, queue_name, callable, manual_ack, exclusive, arguments)
959
+ end
960
+
961
+ # @param [String] consumer_tag
962
+ # @private
963
+ def delete_recorded_consumer(consumer_tag)
964
+ @topology_registry.delete_recorded_consumer(consumer_tag)
965
+ end
966
+
967
+
968
+
770
969
  # @private
771
970
  def announce_network_failure_recovery
772
971
  if recovery_attempts_limited?
@@ -777,18 +976,17 @@ module Bunny
777
976
  end
778
977
 
779
978
  # @private
780
- def recover_from_network_failure
979
+ def recover_connection_and_channels
781
980
  sleep @network_recovery_interval
782
981
  @logger.debug "Will attempt connection recovery..."
783
982
  notify_of_recovery_attempt_start
784
983
 
785
984
  self.initialize_transport
786
985
 
787
- @logger.warn "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
986
+ @logger.debug "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
788
987
  self.start
789
988
 
790
989
  if open?
791
-
792
990
  @recovering_from_network_failure = false
793
991
  @logger.debug "Connection is now open"
794
992
  if @reset_recovery_attempt_counter_after_reconnection
@@ -799,7 +997,6 @@ module Bunny
799
997
  end
800
998
 
801
999
  recover_channels
802
- notify_of_recovery_completion
803
1000
  end
804
1001
  rescue HostListDepleted
805
1002
  reset_address_index
@@ -853,11 +1050,156 @@ module Bunny
853
1050
  @channel_mutex.synchronize do
854
1051
  @channels.each do |n, ch|
855
1052
  ch.open
1053
+ ch.recovering!
856
1054
  ch.recover_from_network_failure
857
1055
  end
858
1056
  end
859
1057
  end
860
1058
 
1059
+ # @private
1060
+ def mark_channels_after_recovery!
1061
+ @channel_mutex.synchronize do
1062
+ @channels.each do |n, ch|
1063
+ ch.recovery_completed!
1064
+ end
1065
+ end
1066
+ end
1067
+
1068
+ # @private
1069
+ def recover_topology
1070
+ @logger.debug "Will recover topology now"
1071
+ recover_topology_with
1072
+ end
1073
+
1074
+ # @private
1075
+ def recover_topology_with(filter = nil)
1076
+ exchanges = @topology_registry.filtered_exchanges.reject(&:predeclared?)
1077
+ queues = @topology_registry.filtered_queues
1078
+ queue_bindings = @topology_registry.filtered_queue_bindings
1079
+ exchange_bindings = @topology_registry.filtered_exchange_bindings
1080
+ consumers = @topology_registry.filtered_consumers
1081
+
1082
+ if filter
1083
+ exchanges = exchanges.select(&filter)
1084
+ queues = queues.select(&filter)
1085
+ queue_bindings = queue_bindings.select(&filter)
1086
+ exchange_bindings = exchange_bindings.select(&filter)
1087
+ consumers = consumers.select(&filter)
1088
+ end
1089
+
1090
+ @logger.debug { "Will recover #{exchanges.size} exchange(s)" }
1091
+ exchanges.each do |x|
1092
+ begin
1093
+ recover_exchange(x)
1094
+ rescue Exception => e
1095
+ @logger.error "Caught an exception while recovering exchange #{x.name}: #{e.inspect}"
1096
+ end
1097
+ end
1098
+
1099
+ @logger.debug { "Will recover #{queues.size} queue(s)" }
1100
+ queues.each do |q|
1101
+ begin
1102
+ recover_queue(q)
1103
+ rescue Exception => e
1104
+ @logger.error "Caught an exception while recovering queue #{q.name}: #{e.inspect}"
1105
+ end
1106
+ end
1107
+
1108
+ @logger.debug { "Will recover #{queue_bindings.size + exchange_bindings.size} binding(s)" }
1109
+ queue_bindings.each do |b|
1110
+ begin
1111
+ recover_queue_binding(b)
1112
+ rescue Exception => e
1113
+ @logger.error "Caught an exception while recovering a binding of queue #{b.destination}: #{e.inspect}"
1114
+ end
1115
+ end
1116
+
1117
+ exchange_bindings.each do |b|
1118
+ begin
1119
+ recover_exchange_binding(b)
1120
+ rescue Exception => e
1121
+ @logger.error "Caught an exception while recovering a binding of exchange #{b.source}: #{e.inspect}"
1122
+ end
1123
+ end
1124
+
1125
+ @logger.debug { "Will recover #{consumers.size} consumer(s)" }
1126
+ consumers.each do |c|
1127
+ recover_consumer(c)
1128
+ end
1129
+ end
1130
+
1131
+ # @param [Bunny::RecordedExchange] x
1132
+ # @private
1133
+ def recover_exchange(x)
1134
+ opts = {
1135
+ durable: x.durable,
1136
+ auto_delete: x.auto_delete,
1137
+ arguments: x.arguments
1138
+ }
1139
+ x.channel.exchange_declare(x.name, x.type, opts)
1140
+ end
1141
+
1142
+ # @param [Bunny::RecordedQueue] q
1143
+ # @private
1144
+ def recover_queue(q)
1145
+ opts = {
1146
+ durable: q.durable,
1147
+ auto_delete: q.auto_delete,
1148
+ exclusive: q.exclusive,
1149
+ arguments: q.arguments
1150
+ }
1151
+
1152
+ old_name = q.name
1153
+ # this response carries the server-generated name
1154
+ queue_declare_ok = q.channel.queue_declare(q.name_to_use_for_recovery, opts)
1155
+ new_name = queue_declare_ok.queue
1156
+
1157
+ # if the name has changed, update all the bindings where
1158
+ # this queue is the destination, then all consumers
1159
+ if new_name != old_name
1160
+ record_queue_name_change(old_name, new_name)
1161
+ q.channel.record_queue_name_change(old_name, new_name)
1162
+ end
1163
+ end
1164
+
1165
+ # @param [String] old_name
1166
+ # @param [String] new_name
1167
+ # @private
1168
+ def record_queue_name_change(old_name, new_name)
1169
+ @topology_registry.record_queue_name_change(old_name, new_name)
1170
+ end
1171
+
1172
+ # @param [Bunny::RecordedQueueBinding] rb
1173
+ # @private
1174
+ def recover_queue_binding(rb)
1175
+ opts = {
1176
+ routing_key: rb.routing_key,
1177
+ arguments: rb.arguments
1178
+ }
1179
+
1180
+ rb.channel.queue_bind_without_recording_topology(rb.destination, rb.source, opts)
1181
+ end
1182
+
1183
+ # @param [Bunny::RecordedExchangeBindingBinding] rb
1184
+ # @private
1185
+ def recover_exchange_binding(rb)
1186
+ opts = {
1187
+ routing_key: rb.routing_key,
1188
+ arguments: rb.arguments
1189
+ }
1190
+
1191
+ rb.channel.exchange_bind_without_recording_topology(rb.source, rb.destination, opts)
1192
+ end
1193
+
1194
+ # @param [Bunny::RecordedConsumer] c
1195
+ # @private
1196
+ def recover_consumer(c)
1197
+ c.channel.maybe_reinitialize_consumer_pool!
1198
+ c.channel.basic_consume(c.queue_name, c.consumer_tag, !c.manual_ack, c.exclusive, c.arguments) do |*args|
1199
+ c.callable.call(*args)
1200
+ end
1201
+ end
1202
+
861
1203
  # @private
862
1204
  def notify_of_recovery_attempt_start
863
1205
  @recovery_attempt_started.call if @recovery_attempt_started
@@ -1172,7 +1514,8 @@ module Bunny
1172
1514
  # locking. See a note about "single frame" methods in a comment in `send_frameset`. MK.
1173
1515
  channel.synchronize do
1174
1516
  if open?
1175
- frames.each { |frame| self.send_frame_without_timeout(frame, false) }
1517
+ data = frames.reduce(+"") { |acc, frame| acc << frame.encode }
1518
+ @transport.write_without_timeout(data)
1176
1519
  signal_activity!
1177
1520
  else
1178
1521
  raise ConnectionClosedError.new(frames)
@@ -1188,10 +1531,14 @@ module Bunny
1188
1531
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
1189
1532
  # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
1190
1533
  channel.synchronize do
1191
- @transport.write(data)
1192
- signal_activity!
1534
+ if open?
1535
+ @transport.write(data)
1536
+ signal_activity!
1537
+ else
1538
+ raise ConnectionClosedError.new("pre-encoded data (#{data.bytesize} bytes)")
1539
+ end
1193
1540
  end
1194
- end # send_frameset_without_timeout(frames)
1541
+ end # send_raw_without_timeout(data)
1195
1542
 
1196
1543
  # @return [String]
1197
1544
  # @api public
@@ -1359,19 +1706,22 @@ module Bunny
1359
1706
 
1360
1707
  # @private
1361
1708
  def initialize_transport
1362
- if address = @addresses[ @address_index ]
1363
- @address_index_mutex.synchronize { @address_index += 1 }
1364
- @transport.close rescue nil # Let's make sure the previous transport socket is closed
1365
- @transport = Transport.new(self,
1366
- host_from_address(address),
1367
- port_from_address(address),
1368
- @opts.merge(:session_error_handler => @session_error_handler)
1369
- )
1370
-
1371
- # Reset the cached progname for the logger only when no logger was provided
1372
- @default_logger.progname = self.to_s
1373
-
1374
- @transport
1709
+ address = @addresses[@address_index]
1710
+ if address
1711
+ @transport_mutex.synchronize do
1712
+ @address_index_mutex.synchronize { @address_index += 1 }
1713
+ @transport.close rescue nil # Let's make sure the previous transport socket is closed
1714
+ @transport = Transport.new(self,
1715
+ host_from_address(address),
1716
+ port_from_address(address),
1717
+ @opts.merge(:session_error_handler => @session_error_handler)
1718
+ )
1719
+
1720
+ # Reset the cached progname for the logger only when no logger was provided
1721
+ @default_logger.progname = self.to_s
1722
+
1723
+ @transport
1724
+ end
1375
1725
  else
1376
1726
  raise HostListDepleted
1377
1727
  end
@@ -1379,7 +1729,9 @@ module Bunny
1379
1729
 
1380
1730
  # @private
1381
1731
  def maybe_close_transport
1382
- @transport.close if @transport
1732
+ @transport_mutex.synchronize do
1733
+ @transport.close if @transport
1734
+ end
1383
1735
  end
1384
1736
 
1385
1737
  # Sends AMQ protocol header (also known as preamble).
@@ -1393,7 +1745,7 @@ module Bunny
1393
1745
  # @private
1394
1746
  def encode_credentials(username, password)
1395
1747
  @credentials_encoder.encode_credentials(username, password)
1396
- end # encode_credentials(username, password)
1748
+ end
1397
1749
 
1398
1750
  # @private
1399
1751
  def credentials_encoder_for(mechanism)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bunny
4
- # Abstracts away the Ruby (OS) method of retriving timestamps.
4
+ # Abstracts away the Ruby (OS) method of retrieving timestamps.
5
5
  #
6
6
  # @private
7
7
  class Timestamp