bunny 1.7.0 → 2.17.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.
Files changed (141) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE.md +18 -0
  3. data/.gitignore +6 -1
  4. data/.rspec +1 -3
  5. data/.travis.yml +21 -14
  6. data/CONTRIBUTING.md +132 -0
  7. data/ChangeLog.md +745 -1
  8. data/Gemfile +13 -13
  9. data/LICENSE +1 -1
  10. data/README.md +41 -75
  11. data/Rakefile +54 -0
  12. data/bunny.gemspec +4 -10
  13. data/docker-compose.yml +28 -0
  14. data/docker/Dockerfile +24 -0
  15. data/docker/apt/preferences.d/erlang +3 -0
  16. data/docker/apt/sources.list.d/bintray.rabbitmq.list +2 -0
  17. data/docker/docker-entrypoint.sh +26 -0
  18. data/docker/rabbitmq.conf +29 -0
  19. data/examples/connection/automatic_recovery_with_basic_get.rb +1 -1
  20. data/examples/connection/automatic_recovery_with_client_named_queues.rb +1 -1
  21. data/examples/connection/automatic_recovery_with_multiple_consumers.rb +1 -1
  22. data/examples/connection/automatic_recovery_with_republishing.rb +1 -1
  23. data/examples/connection/automatic_recovery_with_server_named_queues.rb +1 -1
  24. data/examples/connection/channel_level_exception.rb +1 -9
  25. data/examples/connection/disabled_automatic_recovery.rb +1 -1
  26. data/examples/connection/heartbeat.rb +1 -1
  27. data/examples/consumers/high_and_low_priority.rb +1 -1
  28. data/examples/guides/extensions/alternate_exchange.rb +2 -0
  29. data/examples/guides/getting_started/hello_world.rb +2 -0
  30. data/examples/guides/getting_started/weathr.rb +2 -0
  31. data/examples/guides/queues/one_off_consumer.rb +2 -0
  32. data/examples/guides/queues/redeliveries.rb +2 -0
  33. data/lib/bunny.rb +6 -2
  34. data/lib/bunny/channel.rb +192 -109
  35. data/lib/bunny/channel_id_allocator.rb +6 -4
  36. data/lib/bunny/concurrent/continuation_queue.rb +34 -13
  37. data/lib/bunny/consumer_work_pool.rb +34 -6
  38. data/lib/bunny/cruby/socket.rb +29 -16
  39. data/lib/bunny/cruby/ssl_socket.rb +20 -7
  40. data/lib/bunny/exceptions.rb +7 -1
  41. data/lib/bunny/exchange.rb +11 -7
  42. data/lib/bunny/get_response.rb +1 -1
  43. data/lib/bunny/heartbeat_sender.rb +3 -2
  44. data/lib/bunny/jruby/socket.rb +23 -6
  45. data/lib/bunny/jruby/ssl_socket.rb +5 -0
  46. data/lib/bunny/queue.rb +12 -10
  47. data/lib/bunny/reader_loop.rb +31 -18
  48. data/lib/bunny/session.rb +389 -134
  49. data/lib/bunny/test_kit.rb +14 -0
  50. data/lib/bunny/timeout.rb +1 -12
  51. data/lib/bunny/transport.rb +114 -67
  52. data/lib/bunny/version.rb +1 -1
  53. data/repl +1 -1
  54. data/spec/config/rabbitmq.conf +13 -0
  55. data/spec/higher_level_api/integration/basic_ack_spec.rb +154 -22
  56. data/spec/higher_level_api/integration/basic_cancel_spec.rb +77 -11
  57. data/spec/higher_level_api/integration/basic_consume_spec.rb +60 -55
  58. data/spec/higher_level_api/integration/basic_consume_with_objects_spec.rb +6 -6
  59. data/spec/higher_level_api/integration/basic_get_spec.rb +31 -7
  60. data/spec/higher_level_api/integration/basic_nack_spec.rb +22 -19
  61. data/spec/higher_level_api/integration/basic_publish_spec.rb +11 -100
  62. data/spec/higher_level_api/integration/basic_qos_spec.rb +32 -4
  63. data/spec/higher_level_api/integration/basic_reject_spec.rb +94 -16
  64. data/spec/higher_level_api/integration/basic_return_spec.rb +4 -4
  65. data/spec/higher_level_api/integration/channel_close_spec.rb +51 -10
  66. data/spec/higher_level_api/integration/channel_open_spec.rb +12 -12
  67. data/spec/higher_level_api/integration/connection_recovery_spec.rb +412 -286
  68. data/spec/higher_level_api/integration/connection_spec.rb +284 -134
  69. data/spec/higher_level_api/integration/connection_stop_spec.rb +31 -19
  70. data/spec/higher_level_api/integration/consumer_cancellation_notification_spec.rb +17 -17
  71. data/spec/higher_level_api/integration/dead_lettering_spec.rb +14 -14
  72. data/spec/higher_level_api/integration/exchange_bind_spec.rb +5 -5
  73. data/spec/higher_level_api/integration/exchange_declare_spec.rb +32 -31
  74. data/spec/higher_level_api/integration/exchange_delete_spec.rb +12 -12
  75. data/spec/higher_level_api/integration/exchange_unbind_spec.rb +5 -5
  76. data/spec/higher_level_api/integration/exclusive_queue_spec.rb +5 -5
  77. data/spec/higher_level_api/integration/heartbeat_spec.rb +4 -4
  78. data/spec/higher_level_api/integration/message_properties_access_spec.rb +49 -49
  79. data/spec/higher_level_api/integration/predeclared_exchanges_spec.rb +2 -2
  80. data/spec/higher_level_api/integration/publisher_confirms_spec.rb +92 -27
  81. data/spec/higher_level_api/integration/publishing_edge_cases_spec.rb +19 -19
  82. data/spec/higher_level_api/integration/queue_bind_spec.rb +23 -23
  83. data/spec/higher_level_api/integration/queue_declare_spec.rb +129 -34
  84. data/spec/higher_level_api/integration/queue_delete_spec.rb +2 -2
  85. data/spec/higher_level_api/integration/queue_purge_spec.rb +5 -5
  86. data/spec/higher_level_api/integration/queue_unbind_spec.rb +6 -6
  87. data/spec/higher_level_api/integration/read_only_consumer_spec.rb +9 -9
  88. data/spec/higher_level_api/integration/sender_selected_distribution_spec.rb +10 -10
  89. data/spec/higher_level_api/integration/tls_connection_spec.rb +218 -112
  90. data/spec/higher_level_api/integration/toxiproxy_spec.rb +76 -0
  91. data/spec/higher_level_api/integration/tx_commit_spec.rb +1 -1
  92. data/spec/higher_level_api/integration/tx_rollback_spec.rb +1 -1
  93. data/spec/higher_level_api/integration/with_channel_spec.rb +2 -2
  94. data/spec/issues/issue100_spec.rb +11 -12
  95. data/spec/issues/issue141_spec.rb +13 -14
  96. data/spec/issues/issue202_spec.rb +1 -1
  97. data/spec/issues/issue224_spec.rb +5 -5
  98. data/spec/issues/issue465_spec.rb +32 -0
  99. data/spec/issues/issue549_spec.rb +30 -0
  100. data/spec/issues/issue78_spec.rb +21 -24
  101. data/spec/issues/issue83_spec.rb +5 -6
  102. data/spec/issues/issue97_spec.rb +44 -45
  103. data/spec/lower_level_api/integration/basic_cancel_spec.rb +15 -16
  104. data/spec/lower_level_api/integration/basic_consume_spec.rb +20 -21
  105. data/spec/spec_helper.rb +2 -19
  106. data/spec/stress/channel_close_stress_spec.rb +3 -3
  107. data/spec/stress/channel_open_stress_spec.rb +4 -4
  108. data/spec/stress/channel_open_stress_with_single_threaded_connection_spec.rb +7 -7
  109. data/spec/stress/concurrent_consumers_stress_spec.rb +18 -16
  110. data/spec/stress/concurrent_publishers_stress_spec.rb +16 -19
  111. data/spec/stress/connection_open_close_spec.rb +9 -9
  112. data/spec/stress/merry_go_round_spec.rb +105 -0
  113. data/spec/tls/ca_certificate.pem +27 -16
  114. data/spec/tls/ca_key.pem +52 -27
  115. data/spec/tls/client_certificate.pem +27 -16
  116. data/spec/tls/client_key.pem +49 -25
  117. data/spec/tls/generate-server-cert.sh +8 -0
  118. data/spec/tls/server-openssl.cnf +10 -0
  119. data/spec/tls/server.csr +16 -0
  120. data/spec/tls/server_certificate.pem +27 -16
  121. data/spec/tls/server_key.pem +49 -25
  122. data/spec/toxiproxy_helper.rb +28 -0
  123. data/spec/unit/bunny_spec.rb +5 -5
  124. data/spec/unit/concurrent/atomic_fixnum_spec.rb +6 -6
  125. data/spec/unit/concurrent/condition_spec.rb +8 -8
  126. data/spec/unit/concurrent/linked_continuation_queue_spec.rb +2 -2
  127. data/spec/unit/concurrent/synchronized_sorted_set_spec.rb +16 -16
  128. data/spec/unit/exchange_recovery_spec.rb +39 -0
  129. data/spec/unit/version_delivery_tag_spec.rb +3 -3
  130. metadata +42 -35
  131. data/lib/bunny/system_timer.rb +0 -20
  132. data/spec/config/rabbitmq.config +0 -18
  133. data/spec/higher_level_api/integration/basic_recover_spec.rb +0 -18
  134. data/spec/higher_level_api/integration/confirm_select_spec.rb +0 -19
  135. data/spec/higher_level_api/integration/consistent_hash_exchange_spec.rb +0 -50
  136. data/spec/higher_level_api/integration/merry_go_round_spec.rb +0 -85
  137. data/spec/stress/long_running_consumer_spec.rb +0 -83
  138. data/spec/tls/cacert.pem +0 -18
  139. data/spec/tls/client_cert.pem +0 -18
  140. data/spec/tls/server_cert.pem +0 -18
  141. data/spec/unit/system_timer_spec.rb +0 -10
@@ -8,6 +8,11 @@ module Bunny
8
8
  # methods found in Bunny::Socket.
9
9
  class SSLSocket < Bunny::SSLSocket
10
10
 
11
+ def initialize(*args)
12
+ super
13
+ @__bunny_socket_eof_flag__ = false
14
+ end
15
+
11
16
  # Reads given number of bytes with an optional timeout
12
17
  #
13
18
  # @param [Integer] count How many bytes to read
@@ -37,7 +37,6 @@ module Bunny
37
37
  @channel = channel
38
38
  @name = name
39
39
  @options = self.class.add_default_options(name, opts)
40
- @consumers = Hash.new
41
40
 
42
41
  @durable = @options[:durable]
43
42
  @exclusive = @options[:exclusive]
@@ -88,6 +87,15 @@ module Bunny
88
87
  @arguments
89
88
  end
90
89
 
90
+ def to_s
91
+ oid = ("0x%x" % (self.object_id << 1))
92
+ "<#{self.class.name}:#{oid} @name=\"#{name}\" channel=#{@channel.to_s} @durable=#{@durable} @auto_delete=#{@auto_delete} @exclusive=#{@exclusive} @arguments=#{@arguments}>"
93
+ end
94
+
95
+ def inspect
96
+ to_s
97
+ end
98
+
91
99
  # Binds queue to an exchange
92
100
  #
93
101
  # @param [Bunny::Exchange,String] exchange Exchange to bind to
@@ -150,7 +158,6 @@ module Bunny
150
158
  # @option opts [Boolean] :ack (false) [DEPRECATED] Use :manual_ack instead
151
159
  # @option opts [Boolean] :manual_ack (false) Will this consumer use manual acknowledgements?
152
160
  # @option opts [Boolean] :exclusive (false) Should this consumer be exclusive for this queue?
153
- # @option opts [Boolean] :block (false) Should the call block calling thread?
154
161
  # @option opts [#call] :on_cancellation Block to execute when this consumer is cancelled remotely (e.g. via the RabbitMQ Management plugin)
155
162
  # @option opts [String] :consumer_tag Unique consumer identifier. It is usually recommended to let Bunny generate it for you.
156
163
  # @option opts [Hash] :arguments ({}) Additional (optional) arguments, typically used by RabbitMQ extensions
@@ -241,7 +248,7 @@ module Bunny
241
248
 
242
249
  if block
243
250
  if properties
244
- di = GetResponse.new(get_response, properties, @channel)
251
+ di = GetResponse.new(get_response, @channel)
245
252
  mp = MessageProperties.new(properties)
246
253
 
247
254
  block.call(di, mp, content)
@@ -250,7 +257,7 @@ module Bunny
250
257
  end
251
258
  else
252
259
  if properties
253
- di = GetResponse.new(get_response, properties, @channel)
260
+ di = GetResponse.new(get_response, @channel)
254
261
  mp = MessageProperties.new(properties)
255
262
  [di, mp, content]
256
263
  else
@@ -333,7 +340,7 @@ module Bunny
333
340
  # TODO: inject and use logger
334
341
  # puts "Recovering queue #{@name}"
335
342
  begin
336
- declare!
343
+ declare! unless @options[:no_declare]
337
344
 
338
345
  @channel.register_queue(self)
339
346
  rescue Exception => e
@@ -365,11 +372,6 @@ module Bunny
365
372
 
366
373
  protected
367
374
 
368
- # @private
369
- def self.add_default_options(name, opts, block)
370
- { :queue => name, :nowait => (block.nil? && !name.empty?) }.merge(opts)
371
- end
372
-
373
375
  # @private
374
376
  def self.add_default_options(name, opts)
375
377
  # :nowait is always false for Bunny
@@ -9,13 +9,17 @@ module Bunny
9
9
  # @private
10
10
  class ReaderLoop
11
11
 
12
- def initialize(transport, session, session_thread)
13
- @transport = transport
14
- @session = session
15
- @session_thread = session_thread
16
- @logger = @session.logger
12
+ def initialize(transport, session, session_error_handler)
13
+ @transport = transport
14
+ @session = session
15
+ @session_error_handler = session_error_handler
16
+ @logger = @session.logger
17
17
 
18
- @mutex = Mutex.new
18
+ @mutex = Mutex.new
19
+
20
+ @stopping = false
21
+ @stopped = false
22
+ @network_is_down = false
19
23
  end
20
24
 
21
25
 
@@ -33,15 +37,17 @@ module Bunny
33
37
  begin
34
38
  break if @mutex.synchronize { @stopping || @stopped || @network_is_down }
35
39
  run_once
36
- rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError => e
40
+ rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError, Timeout::Error,
41
+ OpenSSL::OpenSSLError => e
37
42
  break if terminate? || @session.closing? || @session.closed?
38
43
 
39
- log_exception(e)
40
44
  @network_is_down = true
41
45
  if @session.automatically_recover?
46
+ log_exception(e, level: :warn)
42
47
  @session.handle_network_failure(e)
43
48
  else
44
- @session_thread.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
49
+ log_exception(e)
50
+ @session_error_handler.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
45
51
  end
46
52
  rescue ShutdownSignal => _
47
53
  @mutex.synchronize { @stopping = true }
@@ -52,9 +58,9 @@ module Bunny
52
58
  log_exception(e)
53
59
 
54
60
  @network_is_down = true
55
- @session_thread.raise(Bunny::NetworkFailure.new("caught an unexpected exception in the network loop: #{e.message}", e))
61
+ @session_error_handler.raise(Bunny::NetworkFailure.new("caught an unexpected exception in the network loop: #{e.message}", e))
56
62
  end
57
- rescue Errno::EBADF => ebadf
63
+ rescue Errno::EBADF => _ebadf
58
64
  break if terminate?
59
65
  # ignored, happens when we loop after the transport has already been closed
60
66
  @mutex.synchronize { @stopping = true }
@@ -92,11 +98,11 @@ module Bunny
92
98
  end
93
99
 
94
100
  def stopped?
95
- @mutex.synchronize { @stopped = true }
101
+ @mutex.synchronize { @stopped }
96
102
  end
97
103
 
98
104
  def stopping?
99
- @mutex.synchronize { @stopping = true }
105
+ @mutex.synchronize { @stopping }
100
106
  end
101
107
 
102
108
  def terminate_with(e)
@@ -110,7 +116,14 @@ module Bunny
110
116
  end
111
117
 
112
118
  def join
113
- @thread.join if @thread
119
+ # Thread#join can/would trigger a re-raise of an unhandled exception in this thread.
120
+ # In addition, Thread.handle_interrupt can be used by other libraries or application code
121
+ # that would make this join operation fail with an obscure exception.
122
+ # So we try to save everyone some really unpleasant debugging time by introducing
123
+ # this condition which typically would not evaluate to true anyway.
124
+ #
125
+ # See ruby-amqp/bunny#589 and ruby-amqp/bunny#590 for background.
126
+ @thread.join if @thread && @thread != Thread.current
114
127
  end
115
128
 
116
129
  def kill
@@ -122,12 +135,12 @@ module Bunny
122
135
 
123
136
  protected
124
137
 
125
- def log_exception(e)
138
+ def log_exception(e, level: :error)
126
139
  if !(io_error?(e) && (@session.closing? || @session.closed?))
127
- @logger.error "Exception in the reader loop: #{e.class.name}: #{e.message}"
128
- @logger.error "Backtrace: "
140
+ @logger.send level, "Exception in the reader loop: #{e.class.name}: #{e.message}"
141
+ @logger.send level, "Backtrace: "
129
142
  e.backtrace.each do |line|
130
- @logger.error "\t#{line}"
143
+ @logger.send level, "\t#{line}"
131
144
  end
132
145
  end
133
146
  end
@@ -36,21 +36,17 @@ module Bunny
36
36
  DEFAULT_HEARTBEAT = :server
37
37
  # @private
38
38
  DEFAULT_FRAME_MAX = 131072
39
- # 2^16 - 1, maximum representable signed 16 bit integer.
39
+ # Hard limit the user cannot go over regardless of server configuration.
40
40
  # @private
41
41
  CHANNEL_MAX_LIMIT = 65535
42
- DEFAULT_CHANNEL_MAX = CHANNEL_MAX_LIMIT
42
+ DEFAULT_CHANNEL_MAX = 2047
43
43
 
44
44
  # backwards compatibility
45
45
  # @private
46
46
  CONNECT_TIMEOUT = Transport::DEFAULT_CONNECTION_TIMEOUT
47
47
 
48
48
  # @private
49
- DEFAULT_CONTINUATION_TIMEOUT = if RUBY_VERSION.to_f < 1.9
50
- 8000
51
- else
52
- 4000
53
- end
49
+ DEFAULT_CONTINUATION_TIMEOUT = 15000
54
50
 
55
51
  # RabbitMQ client metadata
56
52
  DEFAULT_CLIENT_PROPERTIES = {
@@ -82,7 +78,7 @@ module Bunny
82
78
 
83
79
  # @return [Bunny::Transport]
84
80
  attr_reader :transport
85
- attr_reader :status, :port, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
81
+ attr_reader :status, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
86
82
  attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
87
83
  attr_reader :channel_id_allocator
88
84
  # Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
@@ -90,46 +86,60 @@ module Bunny
90
86
  attr_reader :mechanism
91
87
  # @return [Logger]
92
88
  attr_reader :logger
93
- # @return [Integer] Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 4000.
89
+ # @return [Integer] Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 15000.
94
90
  attr_reader :continuation_timeout
95
-
91
+ attr_reader :network_recovery_interval
92
+ attr_reader :connection_name
93
+ attr_accessor :socket_configurator
96
94
 
97
95
  # @param [String, Hash] connection_string_or_opts Connection string or a hash of connection options
98
96
  # @param [Hash] optz Extra options not related to connection
99
97
  #
100
98
  # @option connection_string_or_opts [String] :host ("127.0.0.1") Hostname or IP address to connect to
101
99
  # @option connection_string_or_opts [Array<String>] :hosts (["127.0.0.1"]) list of hostname or IP addresses to select hostname from when connecting
100
+ # @option connection_string_or_opts [Array<String>] :addresses (["127.0.0.1:5672"]) list of addresses to select hostname and port from when connecting
102
101
  # @option connection_string_or_opts [Integer] :port (5672) Port RabbitMQ listens on
103
102
  # @option connection_string_or_opts [String] :username ("guest") Username
104
103
  # @option connection_string_or_opts [String] :password ("guest") Password
105
104
  # @option connection_string_or_opts [String] :vhost ("/") Virtual host to use
106
- # @option connection_string_or_opts [Integer] :heartbeat (600) Heartbeat interval. 0 means no heartbeat.
105
+ # @option connection_string_or_opts [Integer, Symbol] :heartbeat (:server) Heartbeat timeout to offer to the server. :server means use the value suggested by RabbitMQ. 0 means heartbeats and socket read timeouts will be disabled (not recommended).
107
106
  # @option connection_string_or_opts [Integer] :network_recovery_interval (4) Recovery interval periodic network recovery will use. This includes initial pause after network failure.
108
107
  # @option connection_string_or_opts [Boolean] :tls (false) Should TLS/SSL be used?
109
108
  # @option connection_string_or_opts [String] :tls_cert (nil) Path to client TLS/SSL certificate file (.pem)
110
109
  # @option connection_string_or_opts [String] :tls_key (nil) Path to client TLS/SSL private key file (.pem)
111
110
  # @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
112
111
  # @option connection_string_or_opts [String] :verify_peer (true) Whether TLS peer verification should be performed
113
- # @option connection_string_or_opts [Integer] :continuation_timeout (4000) Timeout for client operations that expect a response (e.g. {Bunny::Queue#get}), in milliseconds.
114
- # @option connection_string_or_opts [Integer] :connection_timeout (5) Timeout in seconds for connecting to the server.
115
- # @option connection_string_or_opts [Proc] :hosts_shuffle_strategy A Proc that reorders a list of host strings, defaults to Array#shuffle
112
+ # @option connection_string_or_opts [Symbol] :tls_version (negotiated) What TLS version should be used (:TLSv1, :TLSv1_1, or :TLSv1_2)
113
+ # @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.
114
+ # @option connection_string_or_opts [Integer] :continuation_timeout (15000) Timeout for client operations that expect a response (e.g. {Bunny::Queue#get}), in milliseconds.
115
+ # @option connection_string_or_opts [Integer] :connection_timeout (30) Timeout in seconds for connecting to the server.
116
+ # @option connection_string_or_opts [Integer] :read_timeout (30) TCP socket read timeout in seconds. If heartbeats are disabled this will be ignored.
117
+ # @option connection_string_or_opts [Integer] :write_timeout (30) TCP socket write timeout in seconds.
118
+ # @option connection_string_or_opts [Proc] :hosts_shuffle_strategy a callable that reorders a list of host strings, defaults to Array#shuffle
119
+ # @option connection_string_or_opts [Proc] :recovery_completed a callable that will be called when a network recovery is performed
116
120
  # @option connection_string_or_opts [Logger] :logger The logger. If missing, one is created using :log_file and :log_level.
117
121
  # @option connection_string_or_opts [IO, String] :log_file The file or path to use when creating a logger. Defaults to STDOUT.
118
122
  # @option connection_string_or_opts [IO, String] :logfile DEPRECATED: use :log_file instead. The file or path to use when creating a logger. Defaults to STDOUT.
119
123
  # @option connection_string_or_opts [Integer] :log_level The log level to use when creating a logger. Defaults to LOGGER::WARN
124
+ # @option connection_string_or_opts [Boolean] :automatically_recover (true) Should automatically recover from network failures?
125
+ # @option connection_string_or_opts [Integer] :recovery_attempts (nil) Max number of recovery attempts, nil means forever
126
+ # @option connection_string_or_opts [Integer] :reset_recovery_attempts_after_reconnection (true) Should recovery attempt counter be reset after successful reconnection? When set to false, the attempt counter will last through the entire lifetime of the connection object.
127
+ # @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)?
128
+ # @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.
120
129
  #
121
130
  # @option optz [String] :auth_mechanism ("PLAIN") Authentication mechanism, PLAIN or EXTERNAL
122
131
  # @option optz [String] :locale ("PLAIN") Locale RabbitMQ should use
132
+ # @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.
123
133
  #
124
134
  # @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
125
135
  # @see http://rubybunny.info/articles/tls.html TLS/SSL guide
126
136
  # @api public
127
- def initialize(connection_string_or_opts = Hash.new, optz = Hash.new)
128
- opts = case (ENV["RABBITMQ_URL"] || connection_string_or_opts)
137
+ def initialize(connection_string_or_opts = ENV['RABBITMQ_URL'], optz = Hash.new)
138
+ opts = case (connection_string_or_opts)
129
139
  when nil then
130
140
  Hash.new
131
141
  when String then
132
- self.class.parse_uri(ENV["RABBITMQ_URL"] || connection_string_or_opts)
142
+ self.class.parse_uri(connection_string_or_opts)
133
143
  when Hash then
134
144
  connection_string_or_opts
135
145
  end.merge(optz)
@@ -137,17 +147,26 @@ module Bunny
137
147
  @default_hosts_shuffle_strategy = Proc.new { |hosts| hosts.shuffle }
138
148
 
139
149
  @opts = opts
140
- @hosts = self.hostnames_from(opts)
141
- @host_index = 0
150
+ log_file = opts[:log_file] || opts[:logfile] || STDOUT
151
+ log_level = opts[:log_level] || ENV["BUNNY_LOG_LEVEL"] || Logger::WARN
152
+ # we might need to log a warning about ill-formatted IPv6 address but
153
+ # progname includes hostname, so init like this first
154
+ @logger = opts.fetch(:logger, init_default_logger_without_progname(log_file, log_level))
155
+
156
+ @addresses = self.addresses_from(opts)
157
+ @address_index = 0
142
158
 
143
- @port = self.port_from(opts)
159
+ @transport = nil
144
160
  @user = self.username_from(opts)
145
161
  @pass = self.password_from(opts)
146
162
  @vhost = self.vhost_from(opts)
147
- @logfile = opts[:log_file] || opts[:logfile] || STDOUT
148
163
  @threaded = opts.fetch(:threaded, true)
149
164
 
150
- @logger = opts.fetch(:logger, init_logger(opts[:log_level] || ENV["BUNNY_LOG_LEVEL"] || Logger::WARN))
165
+ # re-init, see above
166
+ @logger = opts.fetch(:logger, init_default_logger(log_file, log_level))
167
+
168
+ validate_connection_options(opts)
169
+ @last_connection_error = nil
151
170
 
152
171
  # should automatic recovery from network failures be used?
153
172
  @automatically_recover = if opts[:automatically_recover].nil? && opts[:automatic_recovery].nil?
@@ -155,12 +174,21 @@ module Bunny
155
174
  else
156
175
  opts[:automatically_recover] || opts[:automatic_recovery]
157
176
  end
177
+ @recovering_from_network_failure = false
178
+ @max_recovery_attempts = opts[:recovery_attempts]
179
+ @recovery_attempts = @max_recovery_attempts
180
+ # When this is set, connection attempts won't be reset after
181
+ # successful reconnection. Some find this behavior more sensible
182
+ # than the per-failure attempt counter. MK.
183
+ @reset_recovery_attempt_counter_after_reconnection = opts.fetch(:reset_recovery_attempts_after_reconnection, true)
184
+
158
185
  @network_recovery_interval = opts.fetch(:network_recovery_interval, DEFAULT_NETWORK_RECOVERY_INTERVAL)
159
- @recover_from_connection_close = opts.fetch(:recover_from_connection_close, false)
186
+ @recover_from_connection_close = opts.fetch(:recover_from_connection_close, true)
160
187
  # in ms
161
188
  @continuation_timeout = opts.fetch(:continuation_timeout, DEFAULT_CONTINUATION_TIMEOUT)
162
189
 
163
190
  @status = :not_connected
191
+ @manually_closed = false
164
192
  @blocked = false
165
193
 
166
194
  # these are negotiated with the broker during the connection tuning phase
@@ -168,10 +196,14 @@ module Bunny
168
196
  @client_channel_max = normalize_client_channel_max(opts.fetch(:channel_max, DEFAULT_CHANNEL_MAX))
169
197
  # will be-renegotiated during connection tuning steps. MK.
170
198
  @channel_max = @client_channel_max
199
+ @heartbeat_sender = nil
171
200
  @client_heartbeat = self.heartbeat_from(opts)
172
201
 
173
- @client_properties = opts[:properties] || DEFAULT_CLIENT_PROPERTIES
174
- @mechanism = opts.fetch(:auth_mechanism, "PLAIN")
202
+ client_props = opts[:properties] || opts[:client_properties] || {}
203
+ @connection_name = client_props[:connection_name] || opts[:connection_name]
204
+ @client_properties = DEFAULT_CLIENT_PROPERTIES.merge(client_props)
205
+ .merge(connection_name: connection_name)
206
+ @mechanism = normalize_auth_mechanism(opts.fetch(:auth_mechanism, "PLAIN"))
175
207
  @credentials_encoder = credentials_encoder_for(@mechanism)
176
208
  @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
177
209
 
@@ -183,14 +215,26 @@ module Bunny
183
215
  # the non-reentrant Ruby mutexes. MK.
184
216
  @transport_mutex = @mutex_impl.new
185
217
  @status_mutex = @mutex_impl.new
186
- @host_index_mutex = @mutex_impl.new
218
+ @address_index_mutex = @mutex_impl.new
187
219
 
188
220
  @channels = Hash.new
221
+ @recovery_completed = opts[:recovery_completed]
189
222
 
190
- @origin_thread = Thread.current
223
+ @session_error_handler = opts.fetch(:session_error_handler, Thread.current)
191
224
 
192
225
  self.reset_continuations
193
226
  self.initialize_transport
227
+
228
+ end
229
+
230
+ def validate_connection_options(options)
231
+ if options[:hosts] && options[:addresses]
232
+ raise ArgumentError, "Connection options can't contain hosts and addresses at the same time"
233
+ end
234
+
235
+ if (options[:host] || options[:hostname]) && (options[:hosts] || options[:addresses])
236
+ @logger.warn "Connection options contain both a host and an array of hosts (addresses), please pick one."
237
+ end
194
238
  end
195
239
 
196
240
  # @return [String] RabbitMQ hostname (or IP address) used
@@ -202,9 +246,13 @@ module Bunny
202
246
  # @return [String] Virtual host used
203
247
  def virtual_host; self.vhost; end
204
248
 
205
- # @return [Integer] Heartbeat interval used
249
+ # @deprecated
250
+ # @return [Integer] Heartbeat timeout (not interval) used
206
251
  def heartbeat_interval; self.heartbeat; end
207
252
 
253
+ # @return [Integer] Heartbeat timeout used
254
+ def heartbeat_timeout; self.heartbeat; end
255
+
208
256
  # @return [Boolean] true if this connection uses TLS (SSL)
209
257
  def uses_tls?
210
258
  @transport.uses_tls?
@@ -223,11 +271,15 @@ module Bunny
223
271
  end
224
272
 
225
273
  def host
226
- @transport ? @transport.host : @hosts[@host_index]
274
+ @transport ? @transport.host : host_from_address(@addresses[@address_index])
275
+ end
276
+
277
+ def port
278
+ @transport ? @transport.port : port_from_address(@addresses[@address_index])
227
279
  end
228
280
 
229
- def reset_host_index
230
- @host_index_mutex.synchronize { @host_index = 0 }
281
+ def reset_address_index
282
+ @address_index_mutex.synchronize { @address_index = 0 }
231
283
  end
232
284
 
233
285
  # @private
@@ -252,7 +304,6 @@ module Bunny
252
304
  # @see http://rubybunny.info/articles/connecting.html
253
305
  # @api public
254
306
  def start
255
-
256
307
  return self if connected?
257
308
 
258
309
  @status_mutex.synchronize { @status = :connecting }
@@ -262,9 +313,7 @@ module Bunny
262
313
  self.reset_continuations
263
314
 
264
315
  begin
265
-
266
316
  begin
267
-
268
317
  # close existing transport if we have one,
269
318
  # to not leak sockets
270
319
  @transport.maybe_initialize_socket
@@ -272,10 +321,6 @@ module Bunny
272
321
  @transport.post_initialize_socket
273
322
  @transport.connect
274
323
 
275
- if @socket_configurator
276
- @transport.configure_socket(&@socket_configurator)
277
- end
278
-
279
324
  self.init_connection
280
325
  self.open_connection
281
326
 
@@ -283,28 +328,33 @@ module Bunny
283
328
  self.start_reader_loop if threaded?
284
329
 
285
330
  rescue TCPConnectionFailed => e
286
-
287
331
  @logger.warn e.message
288
-
289
332
  self.initialize_transport
290
-
291
- @logger.warn "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
333
+ @logger.warn "Will try to connect to the next endpoint in line: #{@transport.host}:#{@transport.port}"
292
334
 
293
335
  return self.start
294
336
  rescue
295
337
  @status_mutex.synchronize { @status = :not_connected }
296
338
  raise
297
339
  end
298
-
299
340
  rescue HostListDepleted
300
- self.reset_host_index
341
+ self.reset_address_index
301
342
  @status_mutex.synchronize { @status = :not_connected }
302
343
  raise TCPConnectionFailedForAllHosts
303
344
  end
345
+ @status_mutex.synchronize { @manually_closed = false }
304
346
 
305
347
  self
306
348
  end
307
349
 
350
+ def update_secret(value, reason)
351
+ @transport.send_frame(AMQ::Protocol::Connection::UpdateSecret.encode(value, reason))
352
+ @last_update_secret_ok = wait_on_continuations
353
+ raise_if_continuation_resulted_in_a_connection_error!
354
+
355
+ @last_update_secret_ok
356
+ end
357
+
308
358
  # Socket operation write timeout used by this connection
309
359
  # @return [Integer]
310
360
  # @private
@@ -317,14 +367,16 @@ module Bunny
317
367
  # opened (this operation is very fast and inexpensive).
318
368
  #
319
369
  # @return [Bunny::Channel] Newly opened channel
320
- def create_channel(n = nil, consumer_pool_size = 1)
370
+ def create_channel(n = nil, consumer_pool_size = 1, consumer_pool_abort_on_exception = false, consumer_pool_shutdown_timeout = 60)
321
371
  raise ArgumentError, "channel number 0 is reserved in the protocol and cannot be used" if 0 == n
372
+ raise ConnectionAlreadyClosed if manually_closed?
373
+ raise RuntimeError, "this connection is not open. Was Bunny::Session#start invoked? Is automatic recovery enabled?" if !connected?
322
374
 
323
375
  @channel_mutex.synchronize do
324
376
  if n && (ch = @channels[n])
325
377
  ch
326
378
  else
327
- ch = Bunny::Channel.new(self, n, ConsumerWorkPool.new(consumer_pool_size || 1))
379
+ ch = Bunny::Channel.new(self, n, ConsumerWorkPool.new(consumer_pool_size || 1, consumer_pool_abort_on_exception, consumer_pool_shutdown_timeout))
328
380
  ch.open
329
381
  ch
330
382
  end
@@ -333,19 +385,26 @@ module Bunny
333
385
  alias channel create_channel
334
386
 
335
387
  # Closes the connection. This involves closing all of its channels.
336
- def close
388
+ def close(await_response = true)
337
389
  @status_mutex.synchronize { @status = :closing }
338
390
 
339
391
  ignoring_io_errors do
340
392
  if @transport.open?
393
+ @logger.debug "Transport is still open..."
341
394
  close_all_channels
342
395
 
343
- self.close_connection(true)
396
+ @logger.debug "Will close all channels...."
397
+ self.close_connection(await_response)
344
398
  end
345
399
 
346
400
  clean_up_on_shutdown
347
401
  end
348
- @status_mutex.synchronize { @status = :closed }
402
+ @status_mutex.synchronize do
403
+ @status = :closed
404
+ @manually_closed = true
405
+ end
406
+ @logger.debug "Connection is closed"
407
+ true
349
408
  end
350
409
  alias stop close
351
410
 
@@ -380,6 +439,11 @@ module Bunny
380
439
  @status_mutex.synchronize { @status == :closed }
381
440
  end
382
441
 
442
+ # @return [Boolean] true if this AMQP 0.9.1 connection has been closed by the user (as opposed to the server)
443
+ def manually_closed?
444
+ @status_mutex.synchronize { @manually_closed == true }
445
+ end
446
+
383
447
  # @return [Boolean] true if this AMQP 0.9.1 connection is open
384
448
  def open?
385
449
  @status_mutex.synchronize do
@@ -426,7 +490,7 @@ module Bunny
426
490
  # @param [String] uri amqp or amqps URI to parse
427
491
  # @return [Hash] Parsed URI as a hash
428
492
  def self.parse_uri(uri)
429
- AMQ::Settings.parse_amqp_url(uri)
493
+ AMQ::Settings.configure(uri)
430
494
  end
431
495
 
432
496
  # Checks if a queue with given name exists.
@@ -476,16 +540,18 @@ module Bunny
476
540
 
477
541
  # @private
478
542
  def open_channel(ch)
479
- n = ch.number
480
- self.register_channel(ch)
543
+ @channel_mutex.synchronize do
544
+ n = ch.number
545
+ self.register_channel(ch)
481
546
 
482
- @transport_mutex.synchronize do
483
- @transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
484
- end
485
- @last_channel_open_ok = wait_on_continuations
486
- raise_if_continuation_resulted_in_a_connection_error!
547
+ @transport_mutex.synchronize do
548
+ @transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
549
+ end
550
+ @last_channel_open_ok = wait_on_continuations
551
+ raise_if_continuation_resulted_in_a_connection_error!
487
552
 
488
- @last_channel_open_ok
553
+ @last_channel_open_ok
554
+ end
489
555
  end
490
556
 
491
557
  # @private
@@ -503,19 +569,33 @@ module Bunny
503
569
  end
504
570
  end
505
571
 
572
+ # @private
573
+ def find_channel(number)
574
+ @channels[number]
575
+ end
576
+
577
+ # @private
578
+ def synchronised_find_channel(number)
579
+ @channel_mutex.synchronize { @channels[number] }
580
+ end
581
+
506
582
  # @private
507
583
  def close_all_channels
508
- @channels.reject {|n, ch| n == 0 || !ch.open? }.each do |_, ch|
509
- Bunny::Timeout.timeout(@transport.disconnect_timeout, ClientTimeout) { ch.close }
584
+ @channel_mutex.synchronize do
585
+ @channels.reject {|n, ch| n == 0 || !ch.open? }.each do |_, ch|
586
+ Bunny::Timeout.timeout(@transport.disconnect_timeout, ClientTimeout) { ch.close }
587
+ end
510
588
  end
511
589
  end
512
590
 
513
591
  # @private
514
- def close_connection(sync = true)
592
+ def close_connection(await_response = true)
515
593
  if @transport.open?
594
+ @logger.debug "Transport is still open"
516
595
  @transport.send_frame(AMQ::Protocol::Connection::Close.encode(200, "Goodbye", 0, 0))
517
596
 
518
- if sync
597
+ if await_response
598
+ @logger.debug "Waiting for a connection.close-ok..."
519
599
  @last_connection_close_ok = wait_on_continuations
520
600
  end
521
601
  end
@@ -534,7 +614,7 @@ module Bunny
534
614
  #
535
615
  # @private
536
616
  def handle_frame(ch_number, method)
537
- @logger.debug "Session#handle_frame on #{ch_number}: #{method.inspect}"
617
+ @logger.debug { "Session#handle_frame on #{ch_number}: #{method.inspect}" }
538
618
  case method
539
619
  when AMQ::Protocol::Channel::OpenOk then
540
620
  @continuations.push(method)
@@ -565,17 +645,24 @@ module Bunny
565
645
  when AMQ::Protocol::Connection::Unblocked then
566
646
  @blocked = false
567
647
  @unblock_callback.call(method) if @unblock_callback
648
+ when AMQ::Protocol::Connection::UpdateSecretOk then
649
+ @continuations.push(method)
568
650
  when AMQ::Protocol::Channel::Close then
569
651
  begin
570
- ch = @channels[ch_number]
652
+ ch = synchronised_find_channel(ch_number)
653
+ # this includes sending a channel.close-ok and
654
+ # potentially invoking a user-provided callback,
655
+ # avoid doing that while holding a mutex lock. MK.
571
656
  ch.handle_method(method)
572
657
  ensure
658
+ # synchronises on @channel_mutex under the hood
573
659
  self.unregister_channel(ch)
574
660
  end
575
661
  when AMQ::Protocol::Basic::GetEmpty then
576
- @channels[ch_number].handle_basic_get_empty(method)
662
+ ch = find_channel(ch_number)
663
+ ch.handle_basic_get_empty(method)
577
664
  else
578
- if ch = @channels[ch_number]
665
+ if ch = find_channel(ch_number)
579
666
  ch.handle_method(method)
580
667
  else
581
668
  @logger.warn "Channel #{ch_number} is not open on this connection!"
@@ -619,15 +706,18 @@ module Bunny
619
706
  begin
620
707
  @recovering_from_network_failure = true
621
708
  if recoverable_network_failure?(exception)
622
- @logger.warn "Recovering from a network failure..."
623
- @channels.each do |n, ch|
624
- ch.maybe_kill_consumer_work_pool!
709
+ announce_network_failure_recovery
710
+ @channel_mutex.synchronize do
711
+ @channels.each do |n, ch|
712
+ ch.maybe_kill_consumer_work_pool!
713
+ end
625
714
  end
715
+ @reader_loop.stop if @reader_loop
626
716
  maybe_shutdown_heartbeat_sender
627
717
 
628
718
  recover_from_network_failure
629
719
  else
630
- # TODO: investigate if we can be a bit smarter here. MK.
720
+ @logger.error "Exception #{exception.message} is considered unrecoverable..."
631
721
  end
632
722
  ensure
633
723
  @recovering_from_network_failure = false
@@ -637,7 +727,8 @@ module Bunny
637
727
 
638
728
  # @private
639
729
  def recoverable_network_failure?(exception)
640
- # TODO: investigate if we can be a bit smarter here. MK.
730
+ # No reasonably smart strategy was suggested in a few years.
731
+ # So just recover unconditionally. MK.
641
732
  true
642
733
  end
643
734
 
@@ -646,42 +737,99 @@ module Bunny
646
737
  @recovering_from_network_failure
647
738
  end
648
739
 
740
+ # @private
741
+ def announce_network_failure_recovery
742
+ if recovery_attempts_limited?
743
+ @logger.warn "Will recover from a network failure (#{@recovery_attempts} out of #{@max_recovery_attempts} left)..."
744
+ else
745
+ @logger.warn "Will recover from a network failure (no retry limit)..."
746
+ end
747
+ end
748
+
649
749
  # @private
650
750
  def recover_from_network_failure
651
- begin
652
- sleep @network_recovery_interval
653
- @logger.debug "About to start connection recovery..."
751
+ sleep @network_recovery_interval
752
+ @logger.debug "Will attempt connection recovery..."
654
753
 
655
- self.initialize_transport
754
+ self.initialize_transport
656
755
 
657
- @logger.warn "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
658
- self.start
756
+ @logger.warn "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
757
+ self.start
659
758
 
660
- if open?
661
- @recovering_from_network_failure = false
759
+ if open?
662
760
 
663
- recover_channels
761
+ @recovering_from_network_failure = false
762
+ @logger.debug "Connection is now open"
763
+ if @reset_recovery_attempt_counter_after_reconnection
764
+ @logger.debug "Resetting recovery attempt counter after successful reconnection"
765
+ reset_recovery_attempt_counter!
766
+ else
767
+ @logger.debug "Not resetting recovery attempt counter after successful reconnection, as configured"
664
768
  end
665
- rescue HostListDepleted
666
- reset_host_index
667
- retry
668
- rescue TCPConnectionFailedForAllHosts, TCPConnectionFailed, AMQ::Protocol::EmptyResponseError => e
669
- @logger.warn "TCP connection failed, reconnecting in #{@network_recovery_interval} seconds"
670
- sleep @network_recovery_interval
671
- retry if recoverable_network_failure?(e)
769
+
770
+ recover_channels
771
+ notify_of_recovery_completion
772
+ end
773
+ rescue HostListDepleted
774
+ reset_address_index
775
+ retry
776
+ rescue TCPConnectionFailedForAllHosts, TCPConnectionFailed, AMQ::Protocol::EmptyResponseError, SystemCallError, Timeout::Error => e
777
+ @logger.warn "TCP connection failed, reconnecting in #{@network_recovery_interval} seconds"
778
+ if should_retry_recovery?
779
+ decrement_recovery_attemp_counter!
780
+ if recoverable_network_failure?(e)
781
+ announce_network_failure_recovery
782
+ retry
783
+ end
784
+ else
785
+ @logger.error "Ran out of recovery attempts (limit set to #{@max_recovery_attempts}), giving up"
786
+ @transport.close
787
+ self.close(false)
788
+ @manually_closed = false
672
789
  end
673
790
  end
674
791
 
675
792
  # @private
676
- def recover_channels
677
- # default channel is reopened right after connection
678
- # negotiation is completed, so make sure we do not try to open
679
- # it twice. MK.
680
- @channels.each do |n, ch|
681
- ch.open
793
+ def recovery_attempts_limited?
794
+ !!@max_recovery_attempts
795
+ end
682
796
 
683
- ch.recover_from_network_failure
797
+ # @private
798
+ def should_retry_recovery?
799
+ !recovery_attempts_limited? || @recovery_attempts > 1
800
+ end
801
+
802
+ # @private
803
+ def decrement_recovery_attemp_counter!
804
+ if @recovery_attempts
805
+ @recovery_attempts -= 1
806
+ @logger.debug "#{@recovery_attempts} recovery attempts left"
684
807
  end
808
+ @recovery_attempts
809
+ end
810
+
811
+ # @private
812
+ def reset_recovery_attempt_counter!
813
+ @recovery_attempts = @max_recovery_attempts
814
+ end
815
+
816
+ # @private
817
+ def recover_channels
818
+ @channel_mutex.synchronize do
819
+ @channels.each do |n, ch|
820
+ ch.open
821
+ ch.recover_from_network_failure
822
+ end
823
+ end
824
+ end
825
+
826
+ def after_recovery_completed(&block)
827
+ @recovery_completed = block
828
+ end
829
+
830
+ # @private
831
+ def notify_of_recovery_completion
832
+ @recovery_completed.call if @recovery_completed
685
833
  end
686
834
 
687
835
  # @private
@@ -719,7 +867,7 @@ module Bunny
719
867
 
720
868
  clean_up_on_shutdown
721
869
  if threaded?
722
- @origin_thread.raise(@last_connection_error)
870
+ @session_error_handler.raise(@last_connection_error)
723
871
  else
724
872
  raise @last_connection_error
725
873
  end
@@ -730,7 +878,7 @@ module Bunny
730
878
  shut_down_all_consumer_work_pools!
731
879
  maybe_shutdown_reader_loop
732
880
  maybe_shutdown_heartbeat_sender
733
- rescue ShutdownSignal => sse
881
+ rescue ShutdownSignal => _sse
734
882
  # no-op
735
883
  rescue Exception => e
736
884
  @logger.warn "Caught an exception when cleaning up after receiving connection.close: #{e.message}"
@@ -740,10 +888,18 @@ module Bunny
740
888
  end
741
889
 
742
890
  # @private
743
- def hostnames_from(options)
744
- options.fetch(:hosts_shuffle_strategy, @default_hosts_shuffle_strategy).call(
745
- [ options[:hosts] || options[:host] || options[:hostname] || DEFAULT_HOST ].flatten
746
- )
891
+ def addresses_from(options)
892
+ shuffle_strategy = options.fetch(:hosts_shuffle_strategy, @default_hosts_shuffle_strategy)
893
+
894
+ addresses = options[:host] || options[:hostname] || options[:addresses] ||
895
+ options[:hosts] || ["#{DEFAULT_HOST}:#{port_from(options)}"]
896
+ addresses = [addresses] unless addresses.is_a? Array
897
+
898
+ addrs = addresses.map do |address|
899
+ host_with_port?(address) ? address : "#{address}:#{port_from(@opts)}"
900
+ end
901
+
902
+ shuffle_strategy.call(addrs)
747
903
  end
748
904
 
749
905
  # @private
@@ -757,6 +913,63 @@ module Bunny
757
913
  options.fetch(:port, fallback)
758
914
  end
759
915
 
916
+ # @private
917
+ def host_with_port?(address)
918
+ # we need to handle cases such as [2001:db8:85a3:8d3:1319:8a2e:370:7348]:5671
919
+ last_colon = address.rindex(":")
920
+ last_closing_square_bracket = address.rindex("]")
921
+
922
+ if last_closing_square_bracket.nil?
923
+ address.include?(":")
924
+ else
925
+ last_closing_square_bracket < last_colon
926
+ end
927
+ end
928
+
929
+ # @private
930
+ def host_from_address(address)
931
+ # we need to handle cases such as [2001:db8:85a3:8d3:1319:8a2e:370:7348]:5671
932
+ last_colon = address.rindex(":")
933
+ last_closing_square_bracket = address.rindex("]")
934
+
935
+ if last_closing_square_bracket.nil?
936
+ parts = address.split(":")
937
+ # this looks like an unquoted IPv6 address, so emit a warning
938
+ if parts.size > 2
939
+ @logger.warn "Address #{address} looks like an unquoted IPv6 address. Make sure you quote IPv6 addresses like so: [2001:db8:85a3:8d3:1319:8a2e:370:7348]"
940
+ end
941
+ return parts[0]
942
+ end
943
+
944
+ if last_closing_square_bracket < last_colon
945
+ # there is a port
946
+ address[0, last_colon]
947
+ elsif last_closing_square_bracket > last_colon
948
+ address
949
+ end
950
+ end
951
+
952
+ # @private
953
+ def port_from_address(address)
954
+ # we need to handle cases such as [2001:db8:85a3:8d3:1319:8a2e:370:7348]:5671
955
+ last_colon = address.rindex(":")
956
+ last_closing_square_bracket = address.rindex("]")
957
+
958
+ if last_closing_square_bracket.nil?
959
+ parts = address.split(":")
960
+ # this looks like an unquoted IPv6 address, so emit a warning
961
+ if parts.size > 2
962
+ @logger.warn "Address #{address} looks like an unquoted IPv6 address. Make sure you quote IPv6 addresses like so: [2001:db8:85a3:8d3:1319:8a2e:370:7348]"
963
+ end
964
+ return parts[1].to_i
965
+ end
966
+
967
+ if last_closing_square_bracket < last_colon
968
+ # there is a port
969
+ address[(last_colon + 1)..-1].to_i
970
+ end
971
+ end
972
+
760
973
  # @private
761
974
  def vhost_from(options)
762
975
  options[:virtual_host] || options[:vhost] || DEFAULT_VHOST
@@ -774,7 +987,7 @@ module Bunny
774
987
 
775
988
  # @private
776
989
  def heartbeat_from(options)
777
- options[:heartbeat] || options[:heartbeat_interval] || options[:requested_heartbeat] || DEFAULT_HEARTBEAT
990
+ options[:heartbeat] || options[:heartbeat_timeout] || options[:requested_heartbeat] || options[:heartbeat_interval] || DEFAULT_HEARTBEAT
778
991
  end
779
992
 
780
993
  # @private
@@ -811,7 +1024,7 @@ module Bunny
811
1024
 
812
1025
  # @private
813
1026
  def reader_loop
814
- @reader_loop ||= ReaderLoop.new(@transport, self, Thread.current)
1027
+ @reader_loop ||= ReaderLoop.new(@transport, self, @session_error_handler)
815
1028
  end
816
1029
 
817
1030
  # @private
@@ -890,7 +1103,7 @@ module Bunny
890
1103
  end
891
1104
  end
892
1105
 
893
- # Sends multiple frames, one by one. For thread safety this method takes a channel
1106
+ # Sends multiple frames, in one go. For thread safety this method takes a channel
894
1107
  # object and synchronizes on it.
895
1108
  #
896
1109
  # @private
@@ -899,10 +1112,18 @@ module Bunny
899
1112
  # threads publish on the same channel aggressively, at some point frames will be
900
1113
  # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
901
1114
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
902
- # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
1115
+ # locking. Note that "single frame" methods technically do not need this kind of synchronization
1116
+ # (no incorrect frame interleaving of the same kind as with basic.publish isn't possible) but we
1117
+ # still recommend not sharing channels between threads except for consumer-only cases in the docs. MK.
903
1118
  channel.synchronize do
904
- frames.each { |frame| self.send_frame(frame, false) }
905
- signal_activity!
1119
+ # see rabbitmq/rabbitmq-server#156
1120
+ if open?
1121
+ data = frames.reduce("") { |acc, frame| acc << frame.encode }
1122
+ @transport.write(data)
1123
+ signal_activity!
1124
+ else
1125
+ raise ConnectionClosedError.new(frames)
1126
+ end
906
1127
  end
907
1128
  end # send_frameset(frames)
908
1129
 
@@ -916,10 +1137,14 @@ module Bunny
916
1137
  # threads publish on the same channel aggressively, at some point frames will be
917
1138
  # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
918
1139
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
919
- # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
1140
+ # locking. See a note about "single frame" methods in a comment in `send_frameset`. MK.
920
1141
  channel.synchronize do
921
- frames.each { |frame| self.send_frame_without_timeout(frame, false) }
922
- signal_activity!
1142
+ if open?
1143
+ frames.each { |frame| self.send_frame_without_timeout(frame, false) }
1144
+ signal_activity!
1145
+ else
1146
+ raise ConnectionClosedError.new(frames)
1147
+ end
923
1148
  end
924
1149
  end # send_frameset_without_timeout(frames)
925
1150
 
@@ -939,7 +1164,12 @@ module Bunny
939
1164
  # @return [String]
940
1165
  # @api public
941
1166
  def to_s
942
- "#<#{self.class.name}:#{object_id} #{@user}@#{host}:#{@port}, vhost=#{@vhost}, hosts=[#{@hosts.join(',')}]>"
1167
+ oid = ("0x%x" % (self.object_id << 1))
1168
+ "#<#{self.class.name}:#{oid} #{@user}@#{host}:#{port}, vhost=#{@vhost}, addresses=[#{@addresses.join(',')}]>"
1169
+ end
1170
+
1171
+ def inspect
1172
+ to_s
943
1173
  end
944
1174
 
945
1175
  protected
@@ -998,29 +1228,28 @@ module Bunny
998
1228
  else
999
1229
  negotiate_value(@client_heartbeat, connection_tune.heartbeat)
1000
1230
  end
1001
- @logger.debug "Heartbeat interval negotiation: client = #{@client_heartbeat}, server = #{connection_tune.heartbeat}, result = #{@heartbeat}"
1231
+ @logger.debug { "Heartbeat interval negotiation: client = #{@client_heartbeat}, server = #{connection_tune.heartbeat}, result = #{@heartbeat}" }
1002
1232
  @logger.info "Heartbeat interval used (in seconds): #{@heartbeat}"
1003
1233
 
1004
- # We set the read_write_timeout to twice the heartbeat value
1234
+ # We set the read_write_timeout to twice the heartbeat value,
1235
+ # and then some padding for edge cases.
1005
1236
  # This allows us to miss a single heartbeat before we time out the socket.
1006
- @transport.read_timeout = if heartbeat_disabled?(@client_heartbeat)
1007
- Transport::DEFAULT_READ_TIMEOUT
1008
- else
1009
- # pad to account for edge cases. MK.
1010
- @heartbeat * 2.2
1011
- end
1012
-
1237
+ # If heartbeats are disabled, assume that TCP keepalives or a similar mechanism will be used
1238
+ # and disable socket read timeouts. See ruby-amqp/bunny#551.
1239
+ @transport.read_timeout = @heartbeat * 2.2
1240
+ @logger.debug { "Will use socket read timeout of #{@transport.read_timeout.to_i} seconds" }
1013
1241
 
1014
1242
  # if there are existing channels we've just recovered from
1015
1243
  # a network failure and need to fix the allocated set. See issue 205. MK.
1016
1244
  if @channels.empty?
1245
+ @logger.debug { "Initializing channel ID allocator with channel_max = #{@channel_max}" }
1017
1246
  @channel_id_allocator = ChannelIdAllocator.new(@channel_max)
1018
1247
  end
1019
1248
 
1020
1249
  @transport.send_frame(AMQ::Protocol::Connection::TuneOk.encode(@channel_max, @frame_max, @heartbeat))
1021
- @logger.debug "Sent connection.tune-ok with heartbeat interval = #{@heartbeat}, frame_max = #{@frame_max}, channel_max = #{@channel_max}"
1250
+ @logger.debug { "Sent connection.tune-ok with heartbeat interval = #{@heartbeat}, frame_max = #{@frame_max}, channel_max = #{@channel_max}" }
1022
1251
  @transport.send_frame(AMQ::Protocol::Connection::Open.encode(self.vhost))
1023
- @logger.debug "Sent connection.open with vhost = #{self.vhost}"
1252
+ @logger.debug { "Sent connection.open with vhost = #{self.vhost}" }
1024
1253
 
1025
1254
  frame2 = begin
1026
1255
  fr = @transport.read_next_frame
@@ -1049,7 +1278,7 @@ module Bunny
1049
1278
  begin
1050
1279
  shut_down_all_consumer_work_pools!
1051
1280
  maybe_shutdown_reader_loop
1052
- rescue ShutdownSignal => sse
1281
+ rescue ShutdownSignal => _sse
1053
1282
  # no-op
1054
1283
  rescue Exception => e
1055
1284
  @logger.warn "Caught an exception when cleaning up after receiving connection.close: #{e.message}"
@@ -1058,7 +1287,7 @@ module Bunny
1058
1287
  end
1059
1288
 
1060
1289
  if threaded?
1061
- @origin_thread.raise(e)
1290
+ @session_error_handler.raise(e)
1062
1291
  else
1063
1292
  raise e
1064
1293
  end
@@ -1074,7 +1303,7 @@ module Bunny
1074
1303
 
1075
1304
  # @private
1076
1305
  def negotiate_value(client_value, server_value)
1077
- return server_value if client_value == :server
1306
+ return server_value if [:server, "server"].include?(client_value)
1078
1307
 
1079
1308
  if client_value == 0 || server_value == 0
1080
1309
  [client_value, server_value].max
@@ -1098,13 +1327,17 @@ module Bunny
1098
1327
 
1099
1328
  # @private
1100
1329
  def initialize_transport
1101
- if host = @hosts[ @host_index ]
1102
- @host_index_mutex.synchronize { @host_index += 1 }
1330
+ if address = @addresses[ @address_index ]
1331
+ @address_index_mutex.synchronize { @address_index += 1 }
1103
1332
  @transport.close rescue nil # Let's make sure the previous transport socket is closed
1104
- @transport = Transport.new(self, host, @port, @opts.merge(:session_thread => @origin_thread))
1333
+ @transport = Transport.new(self,
1334
+ host_from_address(address),
1335
+ port_from_address(address),
1336
+ @opts.merge(:session_error_handler => @session_error_handler)
1337
+ )
1105
1338
 
1106
- # Reset the cached progname for the logger
1107
- @logger.progname = to_s if @logger.respond_to?(:progname)
1339
+ # Reset the cached progname for the logger only when no logger was provided
1340
+ @default_logger.progname = self.to_s
1108
1341
 
1109
1342
  @transport
1110
1343
  else
@@ -1157,12 +1390,22 @@ module Bunny
1157
1390
  end
1158
1391
 
1159
1392
  # @private
1160
- def init_logger(level)
1161
- lgr = ::Logger.new(@logfile)
1162
- lgr.level = normalize_log_level(level)
1163
- lgr.progname = self.to_s
1393
+ def init_default_logger(logfile, level)
1394
+ @default_logger = begin
1395
+ lgr = ::Logger.new(logfile)
1396
+ lgr.level = normalize_log_level(level)
1397
+ lgr.progname = self.to_s
1398
+ lgr
1399
+ end
1400
+ end
1164
1401
 
1165
- lgr
1402
+ # @private
1403
+ def init_default_logger_without_progname(logfile, level)
1404
+ @default_logger = begin
1405
+ lgr = ::Logger.new(logfile)
1406
+ lgr.level = normalize_log_level(level)
1407
+ lgr
1408
+ end
1166
1409
  end
1167
1410
 
1168
1411
  # @private
@@ -1186,6 +1429,7 @@ module Bunny
1186
1429
  end
1187
1430
 
1188
1431
  def normalize_client_channel_max(n)
1432
+ return CHANNEL_MAX_LIMIT if n.nil?
1189
1433
  return CHANNEL_MAX_LIMIT if n > CHANNEL_MAX_LIMIT
1190
1434
 
1191
1435
  case n
@@ -1196,6 +1440,17 @@ module Bunny
1196
1440
  end
1197
1441
  end
1198
1442
 
1443
+ def normalize_auth_mechanism(value)
1444
+ case value
1445
+ when [] then
1446
+ "PLAIN"
1447
+ when nil then
1448
+ "PLAIN"
1449
+ else
1450
+ value
1451
+ end
1452
+ end
1453
+
1199
1454
  def ignoring_io_errors(&block)
1200
1455
  begin
1201
1456
  block.call