bunny 1.3.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 (143) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE.md +18 -0
  3. data/.gitignore +7 -1
  4. data/.rspec +1 -3
  5. data/.travis.yml +21 -14
  6. data/CONTRIBUTING.md +132 -0
  7. data/ChangeLog.md +887 -1
  8. data/Gemfile +13 -13
  9. data/LICENSE +1 -1
  10. data/README.md +46 -60
  11. data/Rakefile +54 -0
  12. data/bunny.gemspec +5 -11
  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/extensions/basic_nack.rb +1 -1
  30. data/examples/guides/extensions/dead_letter_exchange.rb +1 -1
  31. data/examples/guides/getting_started/hello_world.rb +2 -0
  32. data/examples/guides/getting_started/weathr.rb +2 -0
  33. data/examples/guides/queues/one_off_consumer.rb +2 -0
  34. data/examples/guides/queues/redeliveries.rb +4 -2
  35. data/lib/bunny.rb +8 -4
  36. data/lib/bunny/channel.rb +268 -153
  37. data/lib/bunny/channel_id_allocator.rb +6 -4
  38. data/lib/bunny/concurrent/continuation_queue.rb +34 -13
  39. data/lib/bunny/consumer_work_pool.rb +34 -6
  40. data/lib/bunny/cruby/socket.rb +48 -21
  41. data/lib/bunny/cruby/ssl_socket.rb +65 -4
  42. data/lib/bunny/exceptions.rb +25 -4
  43. data/lib/bunny/exchange.rb +24 -28
  44. data/lib/bunny/get_response.rb +1 -1
  45. data/lib/bunny/heartbeat_sender.rb +3 -2
  46. data/lib/bunny/jruby/socket.rb +23 -6
  47. data/lib/bunny/jruby/ssl_socket.rb +5 -0
  48. data/lib/bunny/queue.rb +31 -22
  49. data/lib/bunny/reader_loop.rb +31 -18
  50. data/lib/bunny/session.rb +448 -159
  51. data/lib/bunny/test_kit.rb +14 -0
  52. data/lib/bunny/timeout.rb +1 -12
  53. data/lib/bunny/transport.rb +205 -98
  54. data/lib/bunny/version.rb +1 -1
  55. data/repl +1 -1
  56. data/spec/config/enabled_plugins +1 -0
  57. data/spec/config/rabbitmq.conf +13 -0
  58. data/spec/higher_level_api/integration/basic_ack_spec.rb +175 -16
  59. data/spec/higher_level_api/integration/basic_cancel_spec.rb +77 -11
  60. data/spec/higher_level_api/integration/basic_consume_spec.rb +60 -55
  61. data/spec/higher_level_api/integration/basic_consume_with_objects_spec.rb +6 -6
  62. data/spec/higher_level_api/integration/basic_get_spec.rb +31 -7
  63. data/spec/higher_level_api/integration/basic_nack_spec.rb +22 -19
  64. data/spec/higher_level_api/integration/basic_publish_spec.rb +11 -100
  65. data/spec/higher_level_api/integration/basic_qos_spec.rb +32 -4
  66. data/spec/higher_level_api/integration/basic_reject_spec.rb +94 -16
  67. data/spec/higher_level_api/integration/basic_return_spec.rb +4 -4
  68. data/spec/higher_level_api/integration/channel_close_spec.rb +51 -10
  69. data/spec/higher_level_api/integration/channel_open_spec.rb +12 -12
  70. data/spec/higher_level_api/integration/connection_recovery_spec.rb +424 -221
  71. data/spec/higher_level_api/integration/connection_spec.rb +300 -126
  72. data/spec/higher_level_api/integration/connection_stop_spec.rb +31 -19
  73. data/spec/higher_level_api/integration/consumer_cancellation_notification_spec.rb +17 -17
  74. data/spec/higher_level_api/integration/dead_lettering_spec.rb +34 -11
  75. data/spec/higher_level_api/integration/exchange_bind_spec.rb +5 -5
  76. data/spec/higher_level_api/integration/exchange_declare_spec.rb +32 -31
  77. data/spec/higher_level_api/integration/exchange_delete_spec.rb +12 -12
  78. data/spec/higher_level_api/integration/exchange_unbind_spec.rb +5 -5
  79. data/spec/higher_level_api/integration/exclusive_queue_spec.rb +5 -5
  80. data/spec/higher_level_api/integration/heartbeat_spec.rb +26 -8
  81. data/spec/higher_level_api/integration/message_properties_access_spec.rb +49 -49
  82. data/spec/higher_level_api/integration/predeclared_exchanges_spec.rb +2 -2
  83. data/spec/higher_level_api/integration/publisher_confirms_spec.rb +156 -42
  84. data/spec/higher_level_api/integration/publishing_edge_cases_spec.rb +19 -19
  85. data/spec/higher_level_api/integration/queue_bind_spec.rb +23 -23
  86. data/spec/higher_level_api/integration/queue_declare_spec.rb +129 -34
  87. data/spec/higher_level_api/integration/queue_delete_spec.rb +2 -2
  88. data/spec/higher_level_api/integration/queue_purge_spec.rb +5 -5
  89. data/spec/higher_level_api/integration/queue_unbind_spec.rb +6 -6
  90. data/spec/higher_level_api/integration/read_only_consumer_spec.rb +9 -9
  91. data/spec/higher_level_api/integration/sender_selected_distribution_spec.rb +10 -10
  92. data/spec/higher_level_api/integration/tls_connection_spec.rb +224 -89
  93. data/spec/higher_level_api/integration/toxiproxy_spec.rb +76 -0
  94. data/spec/higher_level_api/integration/tx_commit_spec.rb +1 -1
  95. data/spec/higher_level_api/integration/tx_rollback_spec.rb +1 -1
  96. data/spec/higher_level_api/integration/with_channel_spec.rb +2 -2
  97. data/spec/issues/issue100_spec.rb +11 -11
  98. data/spec/issues/issue141_spec.rb +13 -14
  99. data/spec/issues/issue202_spec.rb +1 -1
  100. data/spec/issues/issue224_spec.rb +40 -0
  101. data/spec/issues/issue465_spec.rb +32 -0
  102. data/spec/issues/issue549_spec.rb +30 -0
  103. data/spec/issues/issue78_spec.rb +21 -24
  104. data/spec/issues/issue83_spec.rb +5 -6
  105. data/spec/issues/issue97_spec.rb +44 -45
  106. data/spec/lower_level_api/integration/basic_cancel_spec.rb +15 -16
  107. data/spec/lower_level_api/integration/basic_consume_spec.rb +20 -21
  108. data/spec/spec_helper.rb +8 -26
  109. data/spec/stress/channel_close_stress_spec.rb +64 -0
  110. data/spec/stress/channel_open_stress_spec.rb +15 -9
  111. data/spec/stress/channel_open_stress_with_single_threaded_connection_spec.rb +7 -7
  112. data/spec/stress/concurrent_consumers_stress_spec.rb +18 -16
  113. data/spec/stress/concurrent_publishers_stress_spec.rb +16 -19
  114. data/spec/stress/connection_open_close_spec.rb +9 -9
  115. data/spec/stress/merry_go_round_spec.rb +105 -0
  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_key.pem +49 -25
  121. data/spec/toxiproxy_helper.rb +28 -0
  122. data/spec/unit/bunny_spec.rb +5 -5
  123. data/spec/unit/concurrent/atomic_fixnum_spec.rb +6 -6
  124. data/spec/unit/concurrent/condition_spec.rb +8 -8
  125. data/spec/unit/concurrent/linked_continuation_queue_spec.rb +2 -2
  126. data/spec/unit/concurrent/synchronized_sorted_set_spec.rb +16 -16
  127. data/spec/unit/exchange_recovery_spec.rb +39 -0
  128. data/spec/unit/version_delivery_tag_spec.rb +3 -3
  129. metadata +65 -47
  130. data/.ruby-version +0 -1
  131. data/lib/bunny/compatibility.rb +0 -24
  132. data/lib/bunny/system_timer.rb +0 -20
  133. data/spec/compatibility/queue_declare_spec.rb +0 -44
  134. data/spec/compatibility/queue_declare_with_default_channel_spec.rb +0 -33
  135. data/spec/higher_level_api/integration/basic_recover_spec.rb +0 -18
  136. data/spec/higher_level_api/integration/confirm_select_spec.rb +0 -19
  137. data/spec/higher_level_api/integration/consistent_hash_exchange_spec.rb +0 -50
  138. data/spec/higher_level_api/integration/merry_go_round_spec.rb +0 -85
  139. data/spec/stress/long_running_consumer_spec.rb +0 -83
  140. data/spec/tls/cacert.pem +0 -18
  141. data/spec/tls/client_cert.pem +0 -18
  142. data/spec/tls/server_cert.pem +0 -18
  143. data/spec/unit/system_timer_spec.rb +0 -10
data/lib/bunny/session.rb CHANGED
@@ -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,62 +78,95 @@ module Bunny
82
78
 
83
79
  # @return [Bunny::Transport]
84
80
  attr_reader :transport
85
- attr_reader :status, :host, :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
- attr_reader :default_channel
88
83
  attr_reader :channel_id_allocator
89
84
  # Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
90
85
  # @return [String]
91
86
  attr_reader :mechanism
92
87
  # @return [Logger]
93
88
  attr_reader :logger
94
- # @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.
95
90
  attr_reader :continuation_timeout
96
-
91
+ attr_reader :network_recovery_interval
92
+ attr_reader :connection_name
93
+ attr_accessor :socket_configurator
97
94
 
98
95
  # @param [String, Hash] connection_string_or_opts Connection string or a hash of connection options
99
96
  # @param [Hash] optz Extra options not related to connection
100
97
  #
101
98
  # @option connection_string_or_opts [String] :host ("127.0.0.1") Hostname or IP address to connect to
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
- # @option connection_string_or_opts [Integer] :continuation_timeout (4000) Timeout for client operations that expect a response (e.g. {Bunny::Queue#get}), in milliseconds.
113
- # @option connection_string_or_opts [Integer] :connection_timeout (5) Timeout in seconds for connecting to the server.
111
+ # @option connection_string_or_opts [String] :verify_peer (true) Whether TLS peer verification should be performed
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
120
+ # @option connection_string_or_opts [Logger] :logger The logger. If missing, one is created using :log_file and :log_level.
121
+ # @option connection_string_or_opts [IO, String] :log_file The file or path to use when creating a logger. Defaults to STDOUT.
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.
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.
114
129
  #
115
130
  # @option optz [String] :auth_mechanism ("PLAIN") Authentication mechanism, PLAIN or EXTERNAL
116
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.
117
133
  #
118
134
  # @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
119
135
  # @see http://rubybunny.info/articles/tls.html TLS/SSL guide
120
136
  # @api public
121
- def initialize(connection_string_or_opts = Hash.new, optz = Hash.new)
122
- 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)
123
139
  when nil then
124
140
  Hash.new
125
141
  when String then
126
- self.class.parse_uri(ENV["RABBITMQ_URL"] || connection_string_or_opts)
142
+ self.class.parse_uri(connection_string_or_opts)
127
143
  when Hash then
128
144
  connection_string_or_opts
129
145
  end.merge(optz)
130
146
 
147
+ @default_hosts_shuffle_strategy = Proc.new { |hosts| hosts.shuffle }
148
+
131
149
  @opts = opts
132
- @host = self.hostname_from(opts)
133
- @port = self.port_from(opts)
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
158
+
159
+ @transport = nil
134
160
  @user = self.username_from(opts)
135
161
  @pass = self.password_from(opts)
136
162
  @vhost = self.vhost_from(opts)
137
- @logfile = opts[:log_file] || opts[:logfile] || STDOUT
138
163
  @threaded = opts.fetch(:threaded, true)
139
164
 
140
- @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
141
170
 
142
171
  # should automatic recovery from network failures be used?
143
172
  @automatically_recover = if opts[:automatically_recover].nil? && opts[:automatic_recovery].nil?
@@ -145,12 +174,21 @@ module Bunny
145
174
  else
146
175
  opts[:automatically_recover] || opts[:automatic_recovery]
147
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
+
148
185
  @network_recovery_interval = opts.fetch(:network_recovery_interval, DEFAULT_NETWORK_RECOVERY_INTERVAL)
149
- @recover_from_connection_close = opts.fetch(:recover_from_connection_close, false)
186
+ @recover_from_connection_close = opts.fetch(:recover_from_connection_close, true)
150
187
  # in ms
151
- @continuation_timeout = opts.fetch(:continuation_timeout, DEFAULT_CONTINUATION_TIMEOUT)
188
+ @continuation_timeout = opts.fetch(:continuation_timeout, DEFAULT_CONTINUATION_TIMEOUT)
152
189
 
153
190
  @status = :not_connected
191
+ @manually_closed = false
154
192
  @blocked = false
155
193
 
156
194
  # these are negotiated with the broker during the connection tuning phase
@@ -158,10 +196,14 @@ module Bunny
158
196
  @client_channel_max = normalize_client_channel_max(opts.fetch(:channel_max, DEFAULT_CHANNEL_MAX))
159
197
  # will be-renegotiated during connection tuning steps. MK.
160
198
  @channel_max = @client_channel_max
199
+ @heartbeat_sender = nil
161
200
  @client_heartbeat = self.heartbeat_from(opts)
162
201
 
163
- @client_properties = opts[:properties] || DEFAULT_CLIENT_PROPERTIES
164
- @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"))
165
207
  @credentials_encoder = credentials_encoder_for(@mechanism)
166
208
  @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
167
209
 
@@ -173,12 +215,26 @@ module Bunny
173
215
  # the non-reentrant Ruby mutexes. MK.
174
216
  @transport_mutex = @mutex_impl.new
175
217
  @status_mutex = @mutex_impl.new
218
+ @address_index_mutex = @mutex_impl.new
219
+
176
220
  @channels = Hash.new
221
+ @recovery_completed = opts[:recovery_completed]
177
222
 
178
- @origin_thread = Thread.current
223
+ @session_error_handler = opts.fetch(:session_error_handler, Thread.current)
179
224
 
180
225
  self.reset_continuations
181
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
182
238
  end
183
239
 
184
240
  # @return [String] RabbitMQ hostname (or IP address) used
@@ -190,9 +246,13 @@ module Bunny
190
246
  # @return [String] Virtual host used
191
247
  def virtual_host; self.vhost; end
192
248
 
193
- # @return [Integer] Heartbeat interval used
249
+ # @deprecated
250
+ # @return [Integer] Heartbeat timeout (not interval) used
194
251
  def heartbeat_interval; self.heartbeat; end
195
252
 
253
+ # @return [Integer] Heartbeat timeout used
254
+ def heartbeat_timeout; self.heartbeat; end
255
+
196
256
  # @return [Boolean] true if this connection uses TLS (SSL)
197
257
  def uses_tls?
198
258
  @transport.uses_tls?
@@ -210,6 +270,18 @@ module Bunny
210
270
  @threaded
211
271
  end
212
272
 
273
+ def host
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])
279
+ end
280
+
281
+ def reset_address_index
282
+ @address_index_mutex.synchronize { @address_index = 0 }
283
+ end
284
+
213
285
  # @private
214
286
  attr_reader :mutex_impl
215
287
 
@@ -241,37 +313,53 @@ module Bunny
241
313
  self.reset_continuations
242
314
 
243
315
  begin
244
- # close existing transport if we have one,
245
- # to not leak sockets
246
- @transport.maybe_initialize_socket
316
+ begin
317
+ # close existing transport if we have one,
318
+ # to not leak sockets
319
+ @transport.maybe_initialize_socket
247
320
 
248
- @transport.post_initialize_socket
249
- @transport.connect
321
+ @transport.post_initialize_socket
322
+ @transport.connect
250
323
 
251
- if @socket_configurator
252
- @transport.configure_socket(&@socket_configurator)
253
- end
324
+ self.init_connection
325
+ self.open_connection
254
326
 
255
- self.init_connection
256
- self.open_connection
327
+ @reader_loop = nil
328
+ self.start_reader_loop if threaded?
257
329
 
258
- @reader_loop = nil
259
- self.start_reader_loop if threaded?
330
+ rescue TCPConnectionFailed => e
331
+ @logger.warn e.message
332
+ self.initialize_transport
333
+ @logger.warn "Will try to connect to the next endpoint in line: #{@transport.host}:#{@transport.port}"
260
334
 
261
- @default_channel = self.create_channel
262
- rescue Exception => e
335
+ return self.start
336
+ rescue
337
+ @status_mutex.synchronize { @status = :not_connected }
338
+ raise
339
+ end
340
+ rescue HostListDepleted
341
+ self.reset_address_index
263
342
  @status_mutex.synchronize { @status = :not_connected }
264
- raise e
343
+ raise TCPConnectionFailedForAllHosts
265
344
  end
345
+ @status_mutex.synchronize { @manually_closed = false }
266
346
 
267
347
  self
268
348
  end
269
349
 
270
- # Socket operation timeout used by this connection
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
+
358
+ # Socket operation write timeout used by this connection
271
359
  # @return [Integer]
272
360
  # @private
273
- def read_write_timeout
274
- @transport.read_write_timeout
361
+ def transport_write_timeout
362
+ @transport.write_timeout
275
363
  end
276
364
 
277
365
  # Opens a new channel and returns it. This method will block the calling
@@ -279,14 +367,16 @@ module Bunny
279
367
  # opened (this operation is very fast and inexpensive).
280
368
  #
281
369
  # @return [Bunny::Channel] Newly opened channel
282
- 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)
283
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?
284
374
 
285
375
  @channel_mutex.synchronize do
286
376
  if n && (ch = @channels[n])
287
377
  ch
288
378
  else
289
- 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))
290
380
  ch.open
291
381
  ch
292
382
  end
@@ -295,19 +385,26 @@ module Bunny
295
385
  alias channel create_channel
296
386
 
297
387
  # Closes the connection. This involves closing all of its channels.
298
- def close
388
+ def close(await_response = true)
299
389
  @status_mutex.synchronize { @status = :closing }
300
390
 
301
391
  ignoring_io_errors do
302
392
  if @transport.open?
393
+ @logger.debug "Transport is still open..."
303
394
  close_all_channels
304
395
 
305
- self.close_connection(true)
396
+ @logger.debug "Will close all channels...."
397
+ self.close_connection(await_response)
306
398
  end
307
399
 
308
400
  clean_up_on_shutdown
309
401
  end
310
- @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
311
408
  end
312
409
  alias stop close
313
410
 
@@ -342,6 +439,11 @@ module Bunny
342
439
  @status_mutex.synchronize { @status == :closed }
343
440
  end
344
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
+
345
447
  # @return [Boolean] true if this AMQP 0.9.1 connection is open
346
448
  def open?
347
449
  @status_mutex.synchronize do
@@ -355,40 +457,6 @@ module Bunny
355
457
  @automatically_recover
356
458
  end
357
459
 
358
- #
359
- # Backwards compatibility
360
- #
361
-
362
- # @private
363
- def queue(*args)
364
- @default_channel.queue(*args)
365
- end
366
-
367
- # @private
368
- def direct(*args)
369
- @default_channel.direct(*args)
370
- end
371
-
372
- # @private
373
- def fanout(*args)
374
- @default_channel.fanout(*args)
375
- end
376
-
377
- # @private
378
- def topic(*args)
379
- @default_channel.topic(*args)
380
- end
381
-
382
- # @private
383
- def headers(*args)
384
- @default_channel.headers(*args)
385
- end
386
-
387
- # @private
388
- def exchange(*args)
389
- @default_channel.exchange(*args)
390
- end
391
-
392
460
  # Defines a callback that will be executed when RabbitMQ blocks the connection
393
461
  # because it is running low on memory or disk space (as configured via config file
394
462
  # and/or rabbitmqctl).
@@ -422,7 +490,7 @@ module Bunny
422
490
  # @param [String] uri amqp or amqps URI to parse
423
491
  # @return [Hash] Parsed URI as a hash
424
492
  def self.parse_uri(uri)
425
- AMQ::Settings.parse_amqp_url(uri)
493
+ AMQ::Settings.configure(uri)
426
494
  end
427
495
 
428
496
  # Checks if a queue with given name exists.
@@ -472,16 +540,18 @@ module Bunny
472
540
 
473
541
  # @private
474
542
  def open_channel(ch)
475
- n = ch.number
476
- self.register_channel(ch)
543
+ @channel_mutex.synchronize do
544
+ n = ch.number
545
+ self.register_channel(ch)
477
546
 
478
- @transport_mutex.synchronize do
479
- @transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
480
- end
481
- @last_channel_open_ok = wait_on_continuations
482
- 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!
483
552
 
484
- @last_channel_open_ok
553
+ @last_channel_open_ok
554
+ end
485
555
  end
486
556
 
487
557
  # @private
@@ -494,23 +564,38 @@ module Bunny
494
564
  raise_if_continuation_resulted_in_a_connection_error!
495
565
 
496
566
  self.unregister_channel(ch)
567
+ self.release_channel_id(ch.id)
497
568
  @last_channel_close_ok
498
569
  end
499
570
  end
500
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
+
501
582
  # @private
502
583
  def close_all_channels
503
- @channels.reject {|n, ch| n == 0 || !ch.open? }.each do |_, ch|
504
- 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
505
588
  end
506
589
  end
507
590
 
508
591
  # @private
509
- def close_connection(sync = true)
592
+ def close_connection(await_response = true)
510
593
  if @transport.open?
594
+ @logger.debug "Transport is still open"
511
595
  @transport.send_frame(AMQ::Protocol::Connection::Close.encode(200, "Goodbye", 0, 0))
512
596
 
513
- if sync
597
+ if await_response
598
+ @logger.debug "Waiting for a connection.close-ok..."
514
599
  @last_connection_close_ok = wait_on_continuations
515
600
  end
516
601
  end
@@ -529,7 +614,7 @@ module Bunny
529
614
  #
530
615
  # @private
531
616
  def handle_frame(ch_number, method)
532
- @logger.debug "Session#handle_frame on #{ch_number}: #{method.inspect}"
617
+ @logger.debug { "Session#handle_frame on #{ch_number}: #{method.inspect}" }
533
618
  case method
534
619
  when AMQ::Protocol::Channel::OpenOk then
535
620
  @continuations.push(method)
@@ -560,17 +645,24 @@ module Bunny
560
645
  when AMQ::Protocol::Connection::Unblocked then
561
646
  @blocked = false
562
647
  @unblock_callback.call(method) if @unblock_callback
648
+ when AMQ::Protocol::Connection::UpdateSecretOk then
649
+ @continuations.push(method)
563
650
  when AMQ::Protocol::Channel::Close then
564
651
  begin
565
- 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.
566
656
  ch.handle_method(method)
567
657
  ensure
658
+ # synchronises on @channel_mutex under the hood
568
659
  self.unregister_channel(ch)
569
660
  end
570
661
  when AMQ::Protocol::Basic::GetEmpty then
571
- @channels[ch_number].handle_basic_get_empty(method)
662
+ ch = find_channel(ch_number)
663
+ ch.handle_basic_get_empty(method)
572
664
  else
573
- if ch = @channels[ch_number]
665
+ if ch = find_channel(ch_number)
574
666
  ch.handle_method(method)
575
667
  else
576
668
  @logger.warn "Channel #{ch_number} is not open on this connection!"
@@ -614,15 +706,18 @@ module Bunny
614
706
  begin
615
707
  @recovering_from_network_failure = true
616
708
  if recoverable_network_failure?(exception)
617
- @logger.warn "Recovering from a network failure..."
618
- @channels.each do |n, ch|
619
- 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
620
714
  end
715
+ @reader_loop.stop if @reader_loop
621
716
  maybe_shutdown_heartbeat_sender
622
717
 
623
718
  recover_from_network_failure
624
719
  else
625
- # TODO: investigate if we can be a bit smarter here. MK.
720
+ @logger.error "Exception #{exception.message} is considered unrecoverable..."
626
721
  end
627
722
  ensure
628
723
  @recovering_from_network_failure = false
@@ -632,7 +727,8 @@ module Bunny
632
727
 
633
728
  # @private
634
729
  def recoverable_network_failure?(exception)
635
- # 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.
636
732
  true
637
733
  end
638
734
 
@@ -641,38 +737,101 @@ module Bunny
641
737
  @recovering_from_network_failure
642
738
  end
643
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
+
644
749
  # @private
645
750
  def recover_from_network_failure
646
- begin
647
- sleep @network_recovery_interval
648
- @logger.debug "About to start connection recovery..."
649
- self.initialize_transport
650
- self.start
751
+ sleep @network_recovery_interval
752
+ @logger.debug "Will attempt connection recovery..."
651
753
 
652
- if open?
653
- @recovering_from_network_failure = false
754
+ self.initialize_transport
755
+
756
+ @logger.warn "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
757
+ self.start
654
758
 
655
- recover_channels
759
+ if open?
760
+
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"
768
+ end
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
656
783
  end
657
- rescue TCPConnectionFailed, AMQ::Protocol::EmptyResponseError => e
658
- @logger.warn "TCP connection failed, reconnecting in #{@network_recovery_interval} seconds"
659
- sleep @network_recovery_interval
660
- retry if recoverable_network_failure?(e)
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
661
789
  end
662
790
  end
663
791
 
664
792
  # @private
665
- def recover_channels
666
- # default channel is reopened right after connection
667
- # negotiation is completed, so make sure we do not try to open
668
- # it twice. MK.
669
- @channels.reject { |n, ch| ch == @default_channel }.each do |n, ch|
670
- ch.open
793
+ def recovery_attempts_limited?
794
+ !!@max_recovery_attempts
795
+ end
796
+
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"
807
+ end
808
+ @recovery_attempts
809
+ end
810
+
811
+ # @private
812
+ def reset_recovery_attempt_counter!
813
+ @recovery_attempts = @max_recovery_attempts
814
+ end
671
815
 
672
- ch.recover_from_network_failure
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
673
823
  end
674
824
  end
675
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
833
+ end
834
+
676
835
  # @private
677
836
  def instantiate_connection_level_exception(frame)
678
837
  case frame
@@ -708,7 +867,7 @@ module Bunny
708
867
 
709
868
  clean_up_on_shutdown
710
869
  if threaded?
711
- @origin_thread.terminate_with(@last_connection_error)
870
+ @session_error_handler.raise(@last_connection_error)
712
871
  else
713
872
  raise @last_connection_error
714
873
  end
@@ -719,7 +878,7 @@ module Bunny
719
878
  shut_down_all_consumer_work_pools!
720
879
  maybe_shutdown_reader_loop
721
880
  maybe_shutdown_heartbeat_sender
722
- rescue ShutdownSignal => sse
881
+ rescue ShutdownSignal => _sse
723
882
  # no-op
724
883
  rescue Exception => e
725
884
  @logger.warn "Caught an exception when cleaning up after receiving connection.close: #{e.message}"
@@ -729,8 +888,18 @@ module Bunny
729
888
  end
730
889
 
731
890
  # @private
732
- def hostname_from(options)
733
- options[:host] || options[:hostname] || DEFAULT_HOST
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)
734
903
  end
735
904
 
736
905
  # @private
@@ -744,6 +913,63 @@ module Bunny
744
913
  options.fetch(:port, fallback)
745
914
  end
746
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
+
747
973
  # @private
748
974
  def vhost_from(options)
749
975
  options[:virtual_host] || options[:vhost] || DEFAULT_VHOST
@@ -761,7 +987,7 @@ module Bunny
761
987
 
762
988
  # @private
763
989
  def heartbeat_from(options)
764
- options[:heartbeat] || options[:heartbeat_interval] || options[:requested_heartbeat] || DEFAULT_HEARTBEAT
990
+ options[:heartbeat] || options[:heartbeat_timeout] || options[:requested_heartbeat] || options[:heartbeat_interval] || DEFAULT_HEARTBEAT
765
991
  end
766
992
 
767
993
  # @private
@@ -798,7 +1024,7 @@ module Bunny
798
1024
 
799
1025
  # @private
800
1026
  def reader_loop
801
- @reader_loop ||= ReaderLoop.new(@transport, self, Thread.current)
1027
+ @reader_loop ||= ReaderLoop.new(@transport, self, @session_error_handler)
802
1028
  end
803
1029
 
804
1030
  # @private
@@ -877,7 +1103,7 @@ module Bunny
877
1103
  end
878
1104
  end
879
1105
 
880
- # 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
881
1107
  # object and synchronizes on it.
882
1108
  #
883
1109
  # @private
@@ -886,10 +1112,18 @@ module Bunny
886
1112
  # threads publish on the same channel aggressively, at some point frames will be
887
1113
  # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
888
1114
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
889
- # 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.
890
1118
  channel.synchronize do
891
- frames.each { |frame| self.send_frame(frame, false) }
892
- 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
893
1127
  end
894
1128
  end # send_frameset(frames)
895
1129
 
@@ -903,10 +1137,14 @@ module Bunny
903
1137
  # threads publish on the same channel aggressively, at some point frames will be
904
1138
  # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
905
1139
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
906
- # 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.
907
1141
  channel.synchronize do
908
- frames.each { |frame| self.send_frame_without_timeout(frame, false) }
909
- 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
910
1148
  end
911
1149
  end # send_frameset_without_timeout(frames)
912
1150
 
@@ -926,7 +1164,12 @@ module Bunny
926
1164
  # @return [String]
927
1165
  # @api public
928
1166
  def to_s
929
- "#<#{self.class.name}:#{object_id} #{@user}@#{@host}:#{@port}, vhost=#{@vhost}>"
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
930
1173
  end
931
1174
 
932
1175
  protected
@@ -959,13 +1202,11 @@ module Bunny
959
1202
  fr
960
1203
  # frame timeout means the broker has closed the TCP connection, which it
961
1204
  # does per 0.9.1 spec.
962
- rescue Errno::ECONNRESET, ClientTimeout, AMQ::Protocol::EmptyResponseError, EOFError, IOError => e
1205
+ rescue
963
1206
  nil
964
1207
  end
965
1208
  if frame.nil?
966
- @state = :closed
967
- @logger.error "RabbitMQ closed TCP connection before AMQP 0.9.1 connection was finalized. Most likely this means authentication failure."
968
- raise Bunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
1209
+ raise TCPConnectionFailed.new('An empty frame was received while opening the connection. In RabbitMQ <= 3.1 this could mean an authentication issue.')
969
1210
  end
970
1211
 
971
1212
  response = frame.decode_payload
@@ -987,19 +1228,28 @@ module Bunny
987
1228
  else
988
1229
  negotiate_value(@client_heartbeat, connection_tune.heartbeat)
989
1230
  end
990
- @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}" }
991
1232
  @logger.info "Heartbeat interval used (in seconds): #{@heartbeat}"
992
1233
 
1234
+ # We set the read_write_timeout to twice the heartbeat value,
1235
+ # and then some padding for edge cases.
1236
+ # This allows us to miss a single heartbeat before we time out the socket.
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" }
1241
+
993
1242
  # if there are existing channels we've just recovered from
994
1243
  # a network failure and need to fix the allocated set. See issue 205. MK.
995
1244
  if @channels.empty?
1245
+ @logger.debug { "Initializing channel ID allocator with channel_max = #{@channel_max}" }
996
1246
  @channel_id_allocator = ChannelIdAllocator.new(@channel_max)
997
1247
  end
998
1248
 
999
1249
  @transport.send_frame(AMQ::Protocol::Connection::TuneOk.encode(@channel_max, @frame_max, @heartbeat))
1000
- @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}" }
1001
1251
  @transport.send_frame(AMQ::Protocol::Connection::Open.encode(self.vhost))
1002
- @logger.debug "Sent connection.open with vhost = #{self.vhost}"
1252
+ @logger.debug { "Sent connection.open with vhost = #{self.vhost}" }
1003
1253
 
1004
1254
  frame2 = begin
1005
1255
  fr = @transport.read_next_frame
@@ -1009,13 +1259,11 @@ module Bunny
1009
1259
  fr
1010
1260
  # frame timeout means the broker has closed the TCP connection, which it
1011
1261
  # does per 0.9.1 spec.
1012
- rescue Errno::ECONNRESET, ClientTimeout, AMQ::Protocol::EmptyResponseError, EOFError => e
1262
+ rescue
1013
1263
  nil
1014
1264
  end
1015
1265
  if frame2.nil?
1016
- @state = :closed
1017
- @logger.warn "RabbitMQ closed TCP connection before AMQP 0.9.1 connection was finalized. Most likely this means authentication failure."
1018
- raise Bunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
1266
+ raise TCPConnectionFailed.new('An empty frame was received while opening the connection. In RabbitMQ <= 3.1 this could mean an authentication issue.')
1019
1267
  end
1020
1268
  connection_open_ok = frame2.decode_payload
1021
1269
 
@@ -1030,7 +1278,7 @@ module Bunny
1030
1278
  begin
1031
1279
  shut_down_all_consumer_work_pools!
1032
1280
  maybe_shutdown_reader_loop
1033
- rescue ShutdownSignal => sse
1281
+ rescue ShutdownSignal => _sse
1034
1282
  # no-op
1035
1283
  rescue Exception => e
1036
1284
  @logger.warn "Caught an exception when cleaning up after receiving connection.close: #{e.message}"
@@ -1038,7 +1286,11 @@ module Bunny
1038
1286
  close_transport
1039
1287
  end
1040
1288
 
1041
- @origin_thread.terminate_with(e)
1289
+ if threaded?
1290
+ @session_error_handler.raise(e)
1291
+ else
1292
+ raise e
1293
+ end
1042
1294
  else
1043
1295
  raise "could not open connection: server did not respond with connection.open-ok but #{connection_open_ok.inspect} instead"
1044
1296
  end
@@ -1051,7 +1303,7 @@ module Bunny
1051
1303
 
1052
1304
  # @private
1053
1305
  def negotiate_value(client_value, server_value)
1054
- return server_value if client_value == :server
1306
+ return server_value if [:server, "server"].include?(client_value)
1055
1307
 
1056
1308
  if client_value == 0 || server_value == 0
1057
1309
  [client_value, server_value].max
@@ -1075,7 +1327,22 @@ module Bunny
1075
1327
 
1076
1328
  # @private
1077
1329
  def initialize_transport
1078
- @transport = Transport.new(self, @host, @port, @opts.merge(:session_thread => @origin_thread))
1330
+ if address = @addresses[ @address_index ]
1331
+ @address_index_mutex.synchronize { @address_index += 1 }
1332
+ @transport.close rescue nil # Let's make sure the previous transport socket is closed
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
+ )
1338
+
1339
+ # Reset the cached progname for the logger only when no logger was provided
1340
+ @default_logger.progname = self.to_s
1341
+
1342
+ @transport
1343
+ else
1344
+ raise HostListDepleted
1345
+ end
1079
1346
  end
1080
1347
 
1081
1348
  # @private
@@ -1123,12 +1390,22 @@ module Bunny
1123
1390
  end
1124
1391
 
1125
1392
  # @private
1126
- def init_logger(level)
1127
- lgr = ::Logger.new(@logfile)
1128
- lgr.level = normalize_log_level(level)
1129
- 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
1130
1401
 
1131
- 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
1132
1409
  end
1133
1410
 
1134
1411
  # @private
@@ -1152,6 +1429,7 @@ module Bunny
1152
1429
  end
1153
1430
 
1154
1431
  def normalize_client_channel_max(n)
1432
+ return CHANNEL_MAX_LIMIT if n.nil?
1155
1433
  return CHANNEL_MAX_LIMIT if n > CHANNEL_MAX_LIMIT
1156
1434
 
1157
1435
  case n
@@ -1162,6 +1440,17 @@ module Bunny
1162
1440
  end
1163
1441
  end
1164
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
+
1165
1454
  def ignoring_io_errors(&block)
1166
1455
  begin
1167
1456
  block.call