bunny 1.0.7 → 2.24.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 (168) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +92 -87
  3. data/lib/amq/protocol/extensions.rb +2 -0
  4. data/lib/bunny/authentication/credentials_encoder.rb +2 -0
  5. data/lib/bunny/authentication/external_mechanism_encoder.rb +2 -0
  6. data/lib/bunny/authentication/plain_mechanism_encoder.rb +2 -0
  7. data/lib/bunny/channel.rb +485 -186
  8. data/lib/bunny/channel_id_allocator.rb +8 -4
  9. data/lib/bunny/concurrent/atomic_fixnum.rb +2 -0
  10. data/lib/bunny/concurrent/condition.rb +2 -0
  11. data/lib/bunny/concurrent/continuation_queue.rb +37 -13
  12. data/lib/bunny/concurrent/synchronized_sorted_set.rb +2 -0
  13. data/lib/bunny/consumer.rb +20 -13
  14. data/lib/bunny/consumer_tag_generator.rb +6 -2
  15. data/lib/bunny/consumer_work_pool.rb +37 -7
  16. data/lib/bunny/cruby/socket.rb +51 -22
  17. data/lib/bunny/cruby/ssl_socket.rb +68 -5
  18. data/lib/bunny/delivery_info.rb +3 -1
  19. data/lib/bunny/exceptions.rb +27 -4
  20. data/lib/bunny/exchange.rb +35 -29
  21. data/lib/bunny/framing.rb +2 -0
  22. data/lib/bunny/get_response.rb +85 -0
  23. data/lib/bunny/heartbeat_sender.rb +9 -6
  24. data/lib/bunny/message_properties.rb +2 -0
  25. data/lib/bunny/queue.rb +89 -41
  26. data/lib/bunny/reader_loop.rb +72 -28
  27. data/lib/bunny/return_info.rb +2 -0
  28. data/lib/bunny/session.rb +621 -225
  29. data/lib/bunny/socket.rb +7 -12
  30. data/lib/bunny/ssl_socket.rb +7 -12
  31. data/lib/bunny/test_kit.rb +15 -0
  32. data/lib/bunny/timeout.rb +3 -12
  33. data/lib/bunny/timestamp.rb +24 -0
  34. data/lib/bunny/transport.rb +223 -98
  35. data/lib/bunny/version.rb +2 -1
  36. data/lib/bunny/versioned_delivery_tag.rb +2 -0
  37. data/lib/bunny.rb +54 -8
  38. metadata +38 -224
  39. data/.gitignore +0 -22
  40. data/.rspec +0 -3
  41. data/.ruby-version +0 -1
  42. data/.travis.yml +0 -23
  43. data/.yardopts +0 -8
  44. data/ChangeLog.md +0 -1092
  45. data/Gemfile +0 -54
  46. data/LICENSE +0 -21
  47. data/benchmarks/basic_publish/with_128K_messages.rb +0 -35
  48. data/benchmarks/basic_publish/with_1k_messages.rb +0 -35
  49. data/benchmarks/basic_publish/with_4K_messages.rb +0 -35
  50. data/benchmarks/basic_publish/with_64K_messages.rb +0 -35
  51. data/benchmarks/channel_open.rb +0 -28
  52. data/benchmarks/mutex_and_monitor.rb +0 -42
  53. data/benchmarks/queue_declare.rb +0 -29
  54. data/benchmarks/queue_declare_and_bind.rb +0 -29
  55. data/benchmarks/queue_declare_bind_and_delete.rb +0 -29
  56. data/benchmarks/synchronized_sorted_set.rb +0 -53
  57. data/benchmarks/write_vs_write_nonblock.rb +0 -49
  58. data/bin/ci/before_build.sh +0 -31
  59. data/bunny.gemspec +0 -40
  60. data/examples/connection/authentication_failure.rb +0 -16
  61. data/examples/connection/automatic_recovery_with_basic_get.rb +0 -40
  62. data/examples/connection/automatic_recovery_with_client_named_queues.rb +0 -36
  63. data/examples/connection/automatic_recovery_with_multiple_consumers.rb +0 -46
  64. data/examples/connection/automatic_recovery_with_server_named_queues.rb +0 -35
  65. data/examples/connection/channel_level_exception.rb +0 -35
  66. data/examples/connection/disabled_automatic_recovery.rb +0 -34
  67. data/examples/connection/heartbeat.rb +0 -17
  68. data/examples/connection/manually_reconnecting_consumer.rb +0 -23
  69. data/examples/connection/manually_reconnecting_publisher.rb +0 -28
  70. data/examples/connection/unknown_host.rb +0 -16
  71. data/examples/guides/exchanges/direct_exchange_routing.rb +0 -36
  72. data/examples/guides/exchanges/fanout_exchange_routing.rb +0 -28
  73. data/examples/guides/exchanges/headers_exchange_routing.rb +0 -31
  74. data/examples/guides/exchanges/mandatory_messages.rb +0 -30
  75. data/examples/guides/extensions/alternate_exchange.rb +0 -28
  76. data/examples/guides/extensions/basic_nack.rb +0 -33
  77. data/examples/guides/extensions/connection_blocked.rb +0 -35
  78. data/examples/guides/extensions/consumer_cancellation_notification.rb +0 -39
  79. data/examples/guides/extensions/dead_letter_exchange.rb +0 -32
  80. data/examples/guides/extensions/exchange_to_exchange_bindings.rb +0 -29
  81. data/examples/guides/extensions/per_message_ttl.rb +0 -36
  82. data/examples/guides/extensions/per_queue_message_ttl.rb +0 -36
  83. data/examples/guides/extensions/publisher_confirms.rb +0 -28
  84. data/examples/guides/extensions/queue_lease.rb +0 -26
  85. data/examples/guides/extensions/sender_selected_distribution.rb +0 -32
  86. data/examples/guides/getting_started/blabbr.rb +0 -27
  87. data/examples/guides/getting_started/hello_world.rb +0 -20
  88. data/examples/guides/getting_started/weathr.rb +0 -47
  89. data/examples/guides/queues/one_off_consumer.rb +0 -23
  90. data/examples/guides/queues/redeliveries.rb +0 -79
  91. data/lib/bunny/compatibility.rb +0 -24
  92. data/lib/bunny/concurrent/linked_continuation_queue.rb +0 -61
  93. data/lib/bunny/jruby/socket.rb +0 -40
  94. data/lib/bunny/jruby/ssl_socket.rb +0 -53
  95. data/lib/bunny/system_timer.rb +0 -20
  96. data/profiling/basic_publish/with_4K_messages.rb +0 -33
  97. data/repl +0 -3
  98. data/spec/compatibility/queue_declare_spec.rb +0 -44
  99. data/spec/compatibility/queue_declare_with_default_channel_spec.rb +0 -33
  100. data/spec/higher_level_api/integration/basic_ack_spec.rb +0 -71
  101. data/spec/higher_level_api/integration/basic_cancel_spec.rb +0 -76
  102. data/spec/higher_level_api/integration/basic_consume_spec.rb +0 -225
  103. data/spec/higher_level_api/integration/basic_consume_with_objects_spec.rb +0 -54
  104. data/spec/higher_level_api/integration/basic_get_spec.rb +0 -48
  105. data/spec/higher_level_api/integration/basic_nack_spec.rb +0 -79
  106. data/spec/higher_level_api/integration/basic_publish_spec.rb +0 -89
  107. data/spec/higher_level_api/integration/basic_qos_spec.rb +0 -29
  108. data/spec/higher_level_api/integration/basic_recover_spec.rb +0 -18
  109. data/spec/higher_level_api/integration/basic_reject_spec.rb +0 -74
  110. data/spec/higher_level_api/integration/basic_return_spec.rb +0 -33
  111. data/spec/higher_level_api/integration/channel_close_spec.rb +0 -25
  112. data/spec/higher_level_api/integration/channel_flow_spec.rb +0 -21
  113. data/spec/higher_level_api/integration/channel_open_spec.rb +0 -57
  114. data/spec/higher_level_api/integration/confirm_select_spec.rb +0 -19
  115. data/spec/higher_level_api/integration/connection_spec.rb +0 -400
  116. data/spec/higher_level_api/integration/connection_stop_spec.rb +0 -26
  117. data/spec/higher_level_api/integration/consistent_hash_exchange_spec.rb +0 -50
  118. data/spec/higher_level_api/integration/consumer_cancellation_notification_spec.rb +0 -128
  119. data/spec/higher_level_api/integration/dead_lettering_spec.rb +0 -52
  120. data/spec/higher_level_api/integration/exchange_bind_spec.rb +0 -31
  121. data/spec/higher_level_api/integration/exchange_declare_spec.rb +0 -204
  122. data/spec/higher_level_api/integration/exchange_delete_spec.rb +0 -105
  123. data/spec/higher_level_api/integration/exchange_unbind_spec.rb +0 -40
  124. data/spec/higher_level_api/integration/exclusive_queue_spec.rb +0 -28
  125. data/spec/higher_level_api/integration/heartbeat_spec.rb +0 -31
  126. data/spec/higher_level_api/integration/merry_go_round_spec.rb +0 -85
  127. data/spec/higher_level_api/integration/message_properties_access_spec.rb +0 -95
  128. data/spec/higher_level_api/integration/predeclared_exchanges_spec.rb +0 -24
  129. data/spec/higher_level_api/integration/publisher_confirms_spec.rb +0 -77
  130. data/spec/higher_level_api/integration/publishing_edge_cases_spec.rb +0 -65
  131. data/spec/higher_level_api/integration/queue_bind_spec.rb +0 -109
  132. data/spec/higher_level_api/integration/queue_declare_spec.rb +0 -190
  133. data/spec/higher_level_api/integration/queue_delete_spec.rb +0 -41
  134. data/spec/higher_level_api/integration/queue_purge_spec.rb +0 -30
  135. data/spec/higher_level_api/integration/queue_unbind_spec.rb +0 -54
  136. data/spec/higher_level_api/integration/read_only_consumer_spec.rb +0 -60
  137. data/spec/higher_level_api/integration/sender_selected_distribution_spec.rb +0 -36
  138. data/spec/higher_level_api/integration/tls_connection_spec.rb +0 -127
  139. data/spec/higher_level_api/integration/tx_commit_spec.rb +0 -21
  140. data/spec/higher_level_api/integration/tx_rollback_spec.rb +0 -21
  141. data/spec/higher_level_api/integration/with_channel_spec.rb +0 -25
  142. data/spec/issues/issue100_spec.rb +0 -42
  143. data/spec/issues/issue141_spec.rb +0 -44
  144. data/spec/issues/issue78_spec.rb +0 -75
  145. data/spec/issues/issue83_spec.rb +0 -31
  146. data/spec/issues/issue97_attachment.json +0 -1
  147. data/spec/issues/issue97_spec.rb +0 -176
  148. data/spec/lower_level_api/integration/basic_cancel_spec.rb +0 -69
  149. data/spec/lower_level_api/integration/basic_consume_spec.rb +0 -100
  150. data/spec/spec_helper.rb +0 -64
  151. data/spec/stress/channel_open_stress_spec.rb +0 -51
  152. data/spec/stress/channel_open_stress_with_single_threaded_connection_spec.rb +0 -28
  153. data/spec/stress/concurrent_consumers_stress_spec.rb +0 -69
  154. data/spec/stress/concurrent_publishers_stress_spec.rb +0 -57
  155. data/spec/stress/connection_open_close_spec.rb +0 -40
  156. data/spec/stress/long_running_consumer_spec.rb +0 -83
  157. data/spec/tls/cacert.pem +0 -18
  158. data/spec/tls/client_cert.pem +0 -18
  159. data/spec/tls/client_key.pem +0 -27
  160. data/spec/tls/server_cert.pem +0 -18
  161. data/spec/tls/server_key.pem +0 -27
  162. data/spec/unit/bunny_spec.rb +0 -15
  163. data/spec/unit/concurrent/atomic_fixnum_spec.rb +0 -35
  164. data/spec/unit/concurrent/condition_spec.rb +0 -82
  165. data/spec/unit/concurrent/linked_continuation_queue_spec.rb +0 -35
  166. data/spec/unit/concurrent/synchronized_sorted_set_spec.rb +0 -73
  167. data/spec/unit/system_timer_spec.rb +0 -10
  168. data/spec/unit/version_delivery_tag_spec.rb +0 -28
data/lib/bunny/session.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "socket"
2
4
  require "thread"
3
5
  require "monitor"
@@ -10,11 +12,7 @@ require "bunny/authentication/credentials_encoder"
10
12
  require "bunny/authentication/plain_mechanism_encoder"
11
13
  require "bunny/authentication/external_mechanism_encoder"
12
14
 
13
- if defined?(JRUBY_VERSION)
14
- require "bunny/concurrent/linked_continuation_queue"
15
- else
16
- require "bunny/concurrent/continuation_queue"
17
- end
15
+ require "bunny/concurrent/continuation_queue"
18
16
 
19
17
  require "amq/protocol/client"
20
18
  require "amq/settings"
@@ -36,21 +34,17 @@ module Bunny
36
34
  DEFAULT_HEARTBEAT = :server
37
35
  # @private
38
36
  DEFAULT_FRAME_MAX = 131072
39
- # 2^16 - 1, maximum representable signed 16 bit integer.
37
+ # Hard limit the user cannot go over regardless of server configuration.
40
38
  # @private
41
39
  CHANNEL_MAX_LIMIT = 65535
42
- DEFAULT_CHANNEL_MAX = CHANNEL_MAX_LIMIT
40
+ DEFAULT_CHANNEL_MAX = 2047
43
41
 
44
42
  # backwards compatibility
45
43
  # @private
46
44
  CONNECT_TIMEOUT = Transport::DEFAULT_CONNECTION_TIMEOUT
47
45
 
48
46
  # @private
49
- DEFAULT_CONTINUATION_TIMEOUT = if RUBY_VERSION.to_f < 1.9
50
- 8000
51
- else
52
- 4000
53
- end
47
+ DEFAULT_CONTINUATION_TIMEOUT = 15000
54
48
 
55
49
  # RabbitMQ client metadata
56
50
  DEFAULT_CLIENT_PROPERTIES = {
@@ -75,6 +69,7 @@ module Bunny
75
69
  # Default reconnection interval for TCP connection failures
76
70
  DEFAULT_NETWORK_RECOVERY_INTERVAL = 5.0
77
71
 
72
+ DEFAULT_RECOVERABLE_EXCEPTIONS = [StandardError, TCPConnectionFailedForAllHosts, TCPConnectionFailed, AMQ::Protocol::EmptyResponseError, SystemCallError, Timeout::Error, Bunny::ConnectionLevelException, Bunny::ConnectionClosedError]
78
73
 
79
74
  #
80
75
  # API
@@ -82,73 +77,121 @@ module Bunny
82
77
 
83
78
  # @return [Bunny::Transport]
84
79
  attr_reader :transport
85
- attr_reader :status, :host, :port, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
80
+ attr_reader :status, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
86
81
  attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
87
- attr_reader :default_channel
88
82
  attr_reader :channel_id_allocator
89
83
  # Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
90
84
  # @return [String]
91
85
  attr_reader :mechanism
92
86
  # @return [Logger]
93
87
  attr_reader :logger
94
- # @return [Integer] Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 4000.
88
+ # @return [Integer] Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 15000.
95
89
  attr_reader :continuation_timeout
96
-
90
+ attr_reader :network_recovery_interval
91
+ attr_reader :connection_name
92
+ attr_accessor :socket_configurator
93
+ attr_accessor :recoverable_exceptions
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.
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_protocol (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 [Proc] :recovery_attempt_started (nil) Will be called before every connection recovery attempt
128
+ # @option connection_string_or_opts [Proc] :recovery_completed (nil) Will be called after successful connection recovery
129
+ # @option connection_string_or_opts [Proc] :recovery_attempts_exhausted (nil) Will be called when the connection recovery failed after the specified amount of recovery attempts
130
+ # @option connection_string_or_opts [Boolean] :recover_from_connection_close (true) Should this connection recover after receiving a server-sent connection.close (e.g. connection was force closed)?
131
+ # @option connection_string_or_opts [Object] :session_error_handler (Thread.current) Object which responds to #raise that will act as a session error handler. Defaults to Thread.current, which will raise asynchronous exceptions in the thread that created the session.
113
132
  #
114
133
  # @option optz [String] :auth_mechanism ("PLAIN") Authentication mechanism, PLAIN or EXTERNAL
115
134
  # @option optz [String] :locale ("PLAIN") Locale RabbitMQ should use
135
+ # @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.
116
136
  #
117
137
  # @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
118
138
  # @see http://rubybunny.info/articles/tls.html TLS/SSL guide
119
139
  # @api public
120
- def initialize(connection_string_or_opts = Hash.new, optz = Hash.new)
121
- opts = case (ENV["RABBITMQ_URL"] || connection_string_or_opts)
140
+ def initialize(connection_string_or_opts = ENV['RABBITMQ_URL'], optz = Hash.new)
141
+ opts = case (connection_string_or_opts)
122
142
  when nil then
123
143
  Hash.new
124
144
  when String then
125
- self.class.parse_uri(ENV["RABBITMQ_URL"] || connection_string_or_opts)
145
+ self.class.parse_uri(connection_string_or_opts)
126
146
  when Hash then
127
147
  connection_string_or_opts
128
148
  end.merge(optz)
129
149
 
150
+ @default_hosts_shuffle_strategy = Proc.new { |hosts| hosts.shuffle }
151
+
130
152
  @opts = opts
131
- @host = self.hostname_from(opts)
132
- @port = self.port_from(opts)
153
+ log_file = opts[:log_file] || opts[:logfile] || STDOUT
154
+ log_level = opts[:log_level] || ENV["BUNNY_LOG_LEVEL"] || Logger::WARN
155
+ # we might need to log a warning about ill-formatted IPv6 address but
156
+ # progname includes hostname, so init like this first
157
+ @logger = opts.fetch(:logger, init_default_logger_without_progname(log_file, log_level))
158
+
159
+ @addresses = self.addresses_from(opts)
160
+ @address_index = 0
161
+
162
+ @transport = nil
133
163
  @user = self.username_from(opts)
134
164
  @pass = self.password_from(opts)
135
165
  @vhost = self.vhost_from(opts)
136
- @logfile = opts[:log_file] || opts[:logfile] || STDOUT
137
166
  @threaded = opts.fetch(:threaded, true)
138
167
 
139
- self.init_logger(opts[:log_level] || ENV["BUNNY_LOG_LEVEL"] || Logger::WARN)
168
+ # re-init, see above
169
+ @logger = opts.fetch(:logger, init_default_logger(log_file, log_level))
170
+
171
+ validate_connection_options(opts)
172
+ @last_connection_error = nil
140
173
 
141
174
  # should automatic recovery from network failures be used?
142
175
  @automatically_recover = if opts[:automatically_recover].nil? && opts[:automatic_recovery].nil?
143
176
  true
144
177
  else
145
- opts[:automatically_recover] || opts[:automatic_recovery]
178
+ opts[:automatically_recover] | opts[:automatic_recovery]
146
179
  end
180
+ @recovering_from_network_failure = false
181
+ @max_recovery_attempts = opts[:recovery_attempts]
182
+ @recovery_attempts = @max_recovery_attempts
183
+ # When this is set, connection attempts won't be reset after
184
+ # successful reconnection. Some find this behavior more sensible
185
+ # than the per-failure attempt counter. MK.
186
+ @reset_recovery_attempt_counter_after_reconnection = opts.fetch(:reset_recovery_attempts_after_reconnection, true)
187
+
147
188
  @network_recovery_interval = opts.fetch(:network_recovery_interval, DEFAULT_NETWORK_RECOVERY_INTERVAL)
189
+ @recover_from_connection_close = opts.fetch(:recover_from_connection_close, true)
148
190
  # in ms
149
- @continuation_timeout = opts.fetch(:continuation_timeout, DEFAULT_CONTINUATION_TIMEOUT)
191
+ @continuation_timeout = opts.fetch(:continuation_timeout, DEFAULT_CONTINUATION_TIMEOUT)
150
192
 
151
193
  @status = :not_connected
194
+ @manually_closed = false
152
195
  @blocked = false
153
196
 
154
197
  # these are negotiated with the broker during the connection tuning phase
@@ -156,10 +199,14 @@ module Bunny
156
199
  @client_channel_max = normalize_client_channel_max(opts.fetch(:channel_max, DEFAULT_CHANNEL_MAX))
157
200
  # will be-renegotiated during connection tuning steps. MK.
158
201
  @channel_max = @client_channel_max
202
+ @heartbeat_sender = nil
159
203
  @client_heartbeat = self.heartbeat_from(opts)
160
204
 
161
- @client_properties = opts[:properties] || DEFAULT_CLIENT_PROPERTIES
162
- @mechanism = opts.fetch(:auth_mechanism, "PLAIN")
205
+ client_props = opts[:properties] || opts[:client_properties] || {}
206
+ @connection_name = client_props[:connection_name] || opts[:connection_name]
207
+ @client_properties = DEFAULT_CLIENT_PROPERTIES.merge(client_props)
208
+ .merge(connection_name: connection_name)
209
+ @mechanism = normalize_auth_mechanism(opts.fetch(:auth_mechanism, "PLAIN"))
163
210
  @credentials_encoder = credentials_encoder_for(@mechanism)
164
211
  @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
165
212
 
@@ -170,12 +217,32 @@ module Bunny
170
217
  # transport operations/continuations mutex. A workaround for
171
218
  # the non-reentrant Ruby mutexes. MK.
172
219
  @transport_mutex = @mutex_impl.new
220
+ @status_mutex = @mutex_impl.new
221
+ @address_index_mutex = @mutex_impl.new
222
+
173
223
  @channels = Hash.new
174
224
 
175
- @origin_thread = Thread.current
225
+ @recovery_attempt_started = opts[:recovery_attempt_started]
226
+ @recovery_completed = opts[:recovery_completed]
227
+ @recovery_attempts_exhausted = opts[:recovery_attempts_exhausted]
228
+
229
+ @session_error_handler = opts.fetch(:session_error_handler, Thread.current)
230
+
231
+ @recoverable_exceptions = DEFAULT_RECOVERABLE_EXCEPTIONS.dup
176
232
 
177
233
  self.reset_continuations
178
234
  self.initialize_transport
235
+
236
+ end
237
+
238
+ def validate_connection_options(options)
239
+ if options[:hosts] && options[:addresses]
240
+ raise ArgumentError, "Connection options can't contain hosts and addresses at the same time"
241
+ end
242
+
243
+ if (options[:host] || options[:hostname]) && (options[:hosts] || options[:addresses])
244
+ @logger.warn "Connection options contain both a host and an array of hosts (addresses), please pick one."
245
+ end
179
246
  end
180
247
 
181
248
  # @return [String] RabbitMQ hostname (or IP address) used
@@ -187,9 +254,13 @@ module Bunny
187
254
  # @return [String] Virtual host used
188
255
  def virtual_host; self.vhost; end
189
256
 
190
- # @return [Integer] Heartbeat interval used
257
+ # @deprecated
258
+ # @return [Integer] Heartbeat timeout (not interval) used
191
259
  def heartbeat_interval; self.heartbeat; end
192
260
 
261
+ # @return [Integer] Heartbeat timeout used
262
+ def heartbeat_timeout; self.heartbeat; end
263
+
193
264
  # @return [Boolean] true if this connection uses TLS (SSL)
194
265
  def uses_tls?
195
266
  @transport.uses_tls?
@@ -207,6 +278,18 @@ module Bunny
207
278
  @threaded
208
279
  end
209
280
 
281
+ def host
282
+ @transport ? @transport.host : host_from_address(@addresses[@address_index])
283
+ end
284
+
285
+ def port
286
+ @transport ? @transport.port : port_from_address(@addresses[@address_index])
287
+ end
288
+
289
+ def reset_address_index
290
+ @address_index_mutex.synchronize { @address_index = 0 }
291
+ end
292
+
210
293
  # @private
211
294
  attr_reader :mutex_impl
212
295
 
@@ -218,6 +301,11 @@ module Bunny
218
301
  @transport.configure_socket(&block)
219
302
  end
220
303
 
304
+ # @return [Integer] Client socket port
305
+ def local_port
306
+ @transport.local_address.ip_port
307
+ end
308
+
221
309
  # Starts the connection process.
222
310
  #
223
311
  # @see http://rubybunny.info/articles/getting_started.html
@@ -226,44 +314,60 @@ module Bunny
226
314
  def start
227
315
  return self if connected?
228
316
 
229
- @status = :connecting
317
+ @status_mutex.synchronize { @status = :connecting }
230
318
  # reset here for cases when automatic network recovery kicks in
231
319
  # when we were blocked. MK.
232
320
  @blocked = false
233
321
  self.reset_continuations
234
322
 
235
323
  begin
236
- # close existing transport if we have one,
237
- # to not leak sockets
238
- @transport.maybe_initialize_socket
324
+ begin
325
+ # close existing transport if we have one,
326
+ # to not leak sockets
327
+ @transport.maybe_initialize_socket
239
328
 
240
- @transport.post_initialize_socket
241
- @transport.connect
329
+ @transport.post_initialize_socket
330
+ @transport.connect
242
331
 
243
- if @socket_configurator
244
- @transport.configure_socket(&@socket_configurator)
245
- end
332
+ self.init_connection
333
+ self.open_connection
246
334
 
247
- self.init_connection
248
- self.open_connection
335
+ @reader_loop = nil
336
+ self.start_reader_loop if threaded?
249
337
 
250
- @reader_loop = nil
251
- self.start_reader_loop if threaded?
338
+ rescue TCPConnectionFailed => e
339
+ @logger.warn e.message
340
+ self.initialize_transport
341
+ @logger.warn "Will try to connect to the next endpoint in line: #{@transport.host}:#{@transport.port}"
252
342
 
253
- @default_channel = self.create_channel
254
- rescue Exception => e
255
- @status = :not_connected
256
- raise e
343
+ return self.start
344
+ rescue
345
+ @status_mutex.synchronize { @status = :not_connected }
346
+ raise
347
+ end
348
+ rescue HostListDepleted
349
+ self.reset_address_index
350
+ @status_mutex.synchronize { @status = :not_connected }
351
+ raise TCPConnectionFailedForAllHosts
257
352
  end
353
+ @status_mutex.synchronize { @manually_closed = false }
258
354
 
259
355
  self
260
356
  end
261
357
 
262
- # Socket operation timeout used by this connection
358
+ def update_secret(value, reason)
359
+ @transport.send_frame(AMQ::Protocol::Connection::UpdateSecret.encode(value, reason))
360
+ @last_update_secret_ok = wait_on_continuations
361
+ raise_if_continuation_resulted_in_a_connection_error!
362
+
363
+ @last_update_secret_ok
364
+ end
365
+
366
+ # Socket operation write timeout used by this connection
263
367
  # @return [Integer]
264
368
  # @private
265
- def read_write_timeout
266
- @transport.read_write_timeout
369
+ def transport_write_timeout
370
+ @transport.write_timeout
267
371
  end
268
372
 
269
373
  # Opens a new channel and returns it. This method will block the calling
@@ -271,33 +375,44 @@ module Bunny
271
375
  # opened (this operation is very fast and inexpensive).
272
376
  #
273
377
  # @return [Bunny::Channel] Newly opened channel
274
- def create_channel(n = nil, consumer_pool_size = 1)
378
+ def create_channel(n = nil, consumer_pool_size = 1, consumer_pool_abort_on_exception = false, consumer_pool_shutdown_timeout = 60)
275
379
  raise ArgumentError, "channel number 0 is reserved in the protocol and cannot be used" if 0 == n
380
+ raise ConnectionAlreadyClosed if manually_closed?
381
+ raise RuntimeError, "this connection is not open. Was Bunny::Session#start invoked? Is automatic recovery enabled?" if !connected?
276
382
 
277
- if n && (ch = @channels[n])
278
- ch
279
- else
280
- ch = Bunny::Channel.new(self, n, ConsumerWorkPool.new(consumer_pool_size || 1))
281
- ch.open
282
- ch
383
+ @channel_mutex.synchronize do
384
+ if n && (ch = @channels[n])
385
+ ch
386
+ else
387
+ ch = Bunny::Channel.new(self, n, ConsumerWorkPool.new(consumer_pool_size || 1, consumer_pool_abort_on_exception, consumer_pool_shutdown_timeout))
388
+ ch.open
389
+ ch
390
+ end
283
391
  end
284
392
  end
285
393
  alias channel create_channel
286
394
 
287
395
  # Closes the connection. This involves closing all of its channels.
288
- def close
289
- if @transport.open?
290
- close_all_channels
396
+ def close(await_response = true)
397
+ @status_mutex.synchronize { @status = :closing }
291
398
 
292
- Bunny::Timeout.timeout(@transport.disconnect_timeout, ClientTimeout) do
293
- self.close_connection(true)
294
- end
399
+ ignoring_io_errors do
400
+ if @transport.open?
401
+ @logger.debug "Transport is still open..."
402
+ close_all_channels
295
403
 
296
- maybe_shutdown_reader_loop
297
- close_transport
404
+ @logger.debug "Will close all channels...."
405
+ self.close_connection(await_response)
406
+ end
298
407
 
408
+ clean_up_on_shutdown
409
+ end
410
+ @status_mutex.synchronize do
299
411
  @status = :closed
412
+ @manually_closed = true
300
413
  end
414
+ @logger.debug "Connection is closed"
415
+ true
301
416
  end
302
417
  alias stop close
303
418
 
@@ -321,14 +436,27 @@ module Bunny
321
436
  status == :connecting
322
437
  end
323
438
 
439
+ # @return [Boolean] true if this AMQP 0.9.1 connection is closing
440
+ # @api private
441
+ def closing?
442
+ @status_mutex.synchronize { @status == :closing }
443
+ end
444
+
324
445
  # @return [Boolean] true if this AMQP 0.9.1 connection is closed
325
446
  def closed?
326
- status == :closed
447
+ @status_mutex.synchronize { @status == :closed }
448
+ end
449
+
450
+ # @return [Boolean] true if this AMQP 0.9.1 connection has been closed by the user (as opposed to the server)
451
+ def manually_closed?
452
+ @status_mutex.synchronize { @manually_closed == true }
327
453
  end
328
454
 
329
455
  # @return [Boolean] true if this AMQP 0.9.1 connection is open
330
456
  def open?
331
- (status == :open || status == :connected || status == :connecting) && @transport.open?
457
+ @status_mutex.synchronize do
458
+ (status == :open || status == :connected || status == :connecting) && @transport.open?
459
+ end
332
460
  end
333
461
  alias connected? open?
334
462
 
@@ -337,40 +465,6 @@ module Bunny
337
465
  @automatically_recover
338
466
  end
339
467
 
340
- #
341
- # Backwards compatibility
342
- #
343
-
344
- # @private
345
- def queue(*args)
346
- @default_channel.queue(*args)
347
- end
348
-
349
- # @private
350
- def direct(*args)
351
- @default_channel.direct(*args)
352
- end
353
-
354
- # @private
355
- def fanout(*args)
356
- @default_channel.fanout(*args)
357
- end
358
-
359
- # @private
360
- def topic(*args)
361
- @default_channel.topic(*args)
362
- end
363
-
364
- # @private
365
- def headers(*args)
366
- @default_channel.headers(*args)
367
- end
368
-
369
- # @private
370
- def exchange(*args)
371
- @default_channel.exchange(*args)
372
- end
373
-
374
468
  # Defines a callback that will be executed when RabbitMQ blocks the connection
375
469
  # because it is running low on memory or disk space (as configured via config file
376
470
  # and/or rabbitmqctl).
@@ -404,7 +498,7 @@ module Bunny
404
498
  # @param [String] uri amqp or amqps URI to parse
405
499
  # @return [Hash] Parsed URI as a hash
406
500
  def self.parse_uri(uri)
407
- AMQ::Settings.parse_amqp_url(uri)
501
+ AMQ::Settings.configure(uri)
408
502
  end
409
503
 
410
504
  # Checks if a queue with given name exists.
@@ -420,6 +514,8 @@ module Bunny
420
514
  begin
421
515
  ch.queue(name, :passive => true)
422
516
  true
517
+ rescue Bunny::ResourceLocked => _
518
+ true
423
519
  rescue Bunny::NotFound => _
424
520
  false
425
521
  ensure
@@ -447,6 +543,24 @@ module Bunny
447
543
  end
448
544
  end
449
545
 
546
+ # Defines a callable (e.g. a block) that will be called
547
+ # before every connection recovery attempt.
548
+ def before_recovery_attempt_starts(&block)
549
+ @recovery_attempt_started = block
550
+ end
551
+
552
+ # Defines a callable (e.g. a block) that will be called
553
+ # after successful connection recovery.
554
+ def after_recovery_completed(&block)
555
+ @recovery_completed = block
556
+ end
557
+
558
+ # Defines a callable (e.g. a block) that will be called
559
+ # when the connection recovery failed after the specified
560
+ # numbers of recovery attempts.
561
+ def after_recovery_attempts_exhausted(&block)
562
+ @recovery_attempts_exhausted = block
563
+ end
450
564
 
451
565
  #
452
566
  # Implementation
@@ -454,49 +568,69 @@ module Bunny
454
568
 
455
569
  # @private
456
570
  def open_channel(ch)
457
- n = ch.number
458
- self.register_channel(ch)
571
+ @channel_mutex.synchronize do
572
+ n = ch.number
573
+ self.register_channel(ch)
459
574
 
460
- @transport_mutex.synchronize do
461
- @transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
462
- end
463
- @last_channel_open_ok = wait_on_continuations
464
- raise_if_continuation_resulted_in_a_connection_error!
575
+ @transport_mutex.synchronize do
576
+ @transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
577
+ end
578
+ @last_channel_open_ok = wait_on_continuations
579
+ raise_if_continuation_resulted_in_a_connection_error!
465
580
 
466
- @last_channel_open_ok
581
+ @last_channel_open_ok
582
+ end
467
583
  end
468
584
 
469
585
  # @private
470
586
  def close_channel(ch)
471
- n = ch.number
587
+ @channel_mutex.synchronize do
588
+ n = ch.number
472
589
 
473
- @transport.send_frame(AMQ::Protocol::Channel::Close.encode(n, 200, "Goodbye", 0, 0))
474
- @last_channel_close_ok = wait_on_continuations
475
- raise_if_continuation_resulted_in_a_connection_error!
590
+ @transport.send_frame(AMQ::Protocol::Channel::Close.encode(n, 200, "Goodbye", 0, 0))
591
+ @last_channel_close_ok = wait_on_continuations
592
+ raise_if_continuation_resulted_in_a_connection_error!
593
+
594
+ self.unregister_channel(ch)
595
+ self.release_channel_id(ch.id)
596
+ @last_channel_close_ok
597
+ end
598
+ end
476
599
 
477
- self.unregister_channel(ch)
478
- @last_channel_close_ok
600
+ # @private
601
+ def find_channel(number)
602
+ @channels[number]
603
+ end
604
+
605
+ # @private
606
+ def synchronised_find_channel(number)
607
+ @channel_mutex.synchronize { @channels[number] }
479
608
  end
480
609
 
481
610
  # @private
482
611
  def close_all_channels
483
- @channels.reject {|n, ch| n == 0 || !ch.open? }.each do |_, ch|
484
- Bunny::Timeout.timeout(@transport.disconnect_timeout, ClientTimeout) { ch.close }
612
+ @channel_mutex.synchronize do
613
+ @channels.reject {|n, ch| n == 0 || !ch.open? }.each do |_, ch|
614
+ Bunny::Timeout.timeout(@transport.disconnect_timeout, ClientTimeout) { ch.close }
615
+ end
485
616
  end
486
617
  end
487
618
 
488
619
  # @private
489
- def close_connection(sync = true)
620
+ def close_connection(await_response = true)
490
621
  if @transport.open?
622
+ @logger.debug "Transport is still open"
491
623
  @transport.send_frame(AMQ::Protocol::Connection::Close.encode(200, "Goodbye", 0, 0))
492
624
 
493
- maybe_shutdown_heartbeat_sender
494
- @status = :not_connected
495
-
496
- if sync
625
+ if await_response
626
+ @logger.debug "Waiting for a connection.close-ok..."
497
627
  @last_connection_close_ok = wait_on_continuations
498
628
  end
499
629
  end
630
+
631
+ shut_down_all_consumer_work_pools!
632
+ maybe_shutdown_heartbeat_sender
633
+ @status_mutex.synchronize { @status = :not_connected }
500
634
  end
501
635
 
502
636
  # Handles incoming frames and dispatches them.
@@ -508,17 +642,20 @@ module Bunny
508
642
  #
509
643
  # @private
510
644
  def handle_frame(ch_number, method)
511
- @logger.debug "Session#handle_frame on #{ch_number}: #{method.inspect}"
645
+ @logger.debug { "Session#handle_frame on #{ch_number}: #{method.inspect}" }
512
646
  case method
513
647
  when AMQ::Protocol::Channel::OpenOk then
514
648
  @continuations.push(method)
515
649
  when AMQ::Protocol::Channel::CloseOk then
516
650
  @continuations.push(method)
517
651
  when AMQ::Protocol::Connection::Close then
518
- @last_connection_error = instantiate_connection_level_exception(method)
519
- @continuations.push(method)
520
-
521
- @origin_thread.raise(@last_connection_error)
652
+ if recover_from_connection_close?
653
+ @logger.warn "Recovering from connection.close (#{method.reply_text})"
654
+ clean_up_on_shutdown
655
+ handle_network_failure(instantiate_connection_level_exception(method))
656
+ else
657
+ clean_up_and_fail_on_connection_close!(method)
658
+ end
522
659
  when AMQ::Protocol::Connection::CloseOk then
523
660
  @last_connection_close_ok = method
524
661
  begin
@@ -536,17 +673,28 @@ module Bunny
536
673
  when AMQ::Protocol::Connection::Unblocked then
537
674
  @blocked = false
538
675
  @unblock_callback.call(method) if @unblock_callback
676
+ when AMQ::Protocol::Connection::UpdateSecretOk then
677
+ @continuations.push(method)
539
678
  when AMQ::Protocol::Channel::Close then
540
679
  begin
541
- ch = @channels[ch_number]
680
+ ch = synchronised_find_channel(ch_number)
681
+ # this includes sending a channel.close-ok and
682
+ # potentially invoking a user-provided callback,
683
+ # avoid doing that while holding a mutex lock. MK.
542
684
  ch.handle_method(method)
543
685
  ensure
544
- self.unregister_channel(ch)
686
+ if ch.nil?
687
+ @logger.warn "Received a server-sent channel.close but the channel was not found locally. Ignoring the frame."
688
+ else
689
+ # synchronises on @channel_mutex under the hood
690
+ self.unregister_channel(ch)
691
+ end
545
692
  end
546
693
  when AMQ::Protocol::Basic::GetEmpty then
547
- @channels[ch_number].handle_basic_get_empty(method)
694
+ ch = find_channel(ch_number)
695
+ ch.handle_basic_get_empty(method)
548
696
  else
549
- if ch = @channels[ch_number]
697
+ if ch = find_channel(ch_number)
550
698
  ch.handle_method(method)
551
699
  else
552
700
  @logger.warn "Channel #{ch_number} is not open on this connection!"
@@ -575,32 +723,43 @@ module Bunny
575
723
  end
576
724
  end
577
725
 
726
+ # @private
727
+ def recover_from_connection_close?
728
+ @recover_from_connection_close
729
+ end
730
+
578
731
  # @private
579
732
  def handle_network_failure(exception)
580
733
  raise NetworkErrorWrapper.new(exception) unless @threaded
581
734
 
582
- @status = :disconnected
735
+ @status_mutex.synchronize { @status = :disconnected }
583
736
 
584
737
  if !recovering_from_network_failure?
585
- @recovering_from_network_failure = true
586
- if recoverable_network_failure?(exception)
587
- @logger.warn "Recovering from a network failure..."
588
- @channels.each do |n, ch|
589
- ch.maybe_kill_consumer_work_pool!
590
- end
591
- maybe_shutdown_heartbeat_sender
738
+ begin
739
+ @recovering_from_network_failure = true
740
+ if recoverable_network_failure?(exception)
741
+ announce_network_failure_recovery
742
+ @channel_mutex.synchronize do
743
+ @channels.each do |n, ch|
744
+ ch.maybe_kill_consumer_work_pool!
745
+ end
746
+ end
747
+ @reader_loop.stop if @reader_loop
748
+ maybe_shutdown_heartbeat_sender
592
749
 
593
- recover_from_network_failure
594
- else
595
- # TODO: investigate if we can be a bit smarter here. MK.
750
+ recover_from_network_failure
751
+ else
752
+ @logger.error "Exception #{exception.message} is considered unrecoverable..."
753
+ end
754
+ ensure
755
+ @recovering_from_network_failure = false
596
756
  end
597
757
  end
598
758
  end
599
759
 
600
760
  # @private
601
761
  def recoverable_network_failure?(exception)
602
- # TODO: investigate if we can be a bit smarter here. MK.
603
- true
762
+ @recoverable_exceptions.any? {|x| exception.kind_of? x}
604
763
  end
605
764
 
606
765
  # @private
@@ -608,38 +767,112 @@ module Bunny
608
767
  @recovering_from_network_failure
609
768
  end
610
769
 
770
+ # @private
771
+ def announce_network_failure_recovery
772
+ if recovery_attempts_limited?
773
+ @logger.warn "Will recover from a network failure (#{@recovery_attempts} out of #{@max_recovery_attempts} left)..."
774
+ else
775
+ @logger.warn "Will recover from a network failure (no retry limit)..."
776
+ end
777
+ end
778
+
611
779
  # @private
612
780
  def recover_from_network_failure
613
- begin
614
- sleep @network_recovery_interval
615
- @logger.debug "About to start connection recovery..."
616
- self.initialize_transport
617
- self.start
781
+ sleep @network_recovery_interval
782
+ @logger.debug "Will attempt connection recovery..."
783
+ notify_of_recovery_attempt_start
618
784
 
619
- if open?
620
- @recovering_from_network_failure = false
785
+ self.initialize_transport
621
786
 
622
- recover_channels
787
+ @logger.warn "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
788
+ self.start
789
+
790
+ if open?
791
+
792
+ @recovering_from_network_failure = false
793
+ @logger.debug "Connection is now open"
794
+ if @reset_recovery_attempt_counter_after_reconnection
795
+ @logger.debug "Resetting recovery attempt counter after successful reconnection"
796
+ reset_recovery_attempt_counter!
797
+ else
798
+ @logger.debug "Not resetting recovery attempt counter after successful reconnection, as configured"
799
+ end
800
+
801
+ recover_channels
802
+ notify_of_recovery_completion
803
+ end
804
+ rescue HostListDepleted
805
+ reset_address_index
806
+ retry
807
+ rescue => e
808
+ if recoverable_network_failure?(e)
809
+ @logger.warn "TCP connection failed"
810
+ if should_retry_recovery?
811
+ @logger.warn "Reconnecting in #{@network_recovery_interval} seconds"
812
+ decrement_recovery_attemp_counter!
813
+ announce_network_failure_recovery
814
+ retry
815
+ else
816
+ @logger.error "Ran out of recovery attempts (limit set to #{@max_recovery_attempts}), giving up"
817
+ @transport.close
818
+ self.close(false)
819
+ @manually_closed = false
820
+ notify_of_recovery_attempts_exhausted
623
821
  end
624
- rescue TCPConnectionFailed, AMQ::Protocol::EmptyResponseError => e
625
- @logger.warn "TCP connection failed, reconnecting in 5 seconds"
626
- sleep @network_recovery_interval
627
- retry if recoverable_network_failure?(e)
822
+ else
823
+ raise e
628
824
  end
629
825
  end
630
826
 
631
827
  # @private
632
- def recover_channels
633
- # default channel is reopened right after connection
634
- # negotiation is completed, so make sure we do not try to open
635
- # it twice. MK.
636
- @channels.reject { |n, ch| ch == @default_channel }.each do |n, ch|
637
- ch.open
828
+ def recovery_attempts_limited?
829
+ !!@max_recovery_attempts
830
+ end
831
+
832
+ # @private
833
+ def should_retry_recovery?
834
+ !recovery_attempts_limited? || @recovery_attempts > 1
835
+ end
836
+
837
+ # @private
838
+ def decrement_recovery_attemp_counter!
839
+ if @recovery_attempts
840
+ @recovery_attempts -= 1
841
+ @logger.debug "#{@recovery_attempts} recovery attempts left"
842
+ end
843
+ @recovery_attempts
844
+ end
845
+
846
+ # @private
847
+ def reset_recovery_attempt_counter!
848
+ @recovery_attempts = @max_recovery_attempts
849
+ end
638
850
 
639
- ch.recover_from_network_failure
851
+ # @private
852
+ def recover_channels
853
+ @channel_mutex.synchronize do
854
+ @channels.each do |n, ch|
855
+ ch.open
856
+ ch.recover_from_network_failure
857
+ end
640
858
  end
641
859
  end
642
860
 
861
+ # @private
862
+ def notify_of_recovery_attempt_start
863
+ @recovery_attempt_started.call if @recovery_attempt_started
864
+ end
865
+
866
+ # @private
867
+ def notify_of_recovery_completion
868
+ @recovery_completed.call if @recovery_completed
869
+ end
870
+
871
+ # @private
872
+ def notify_of_recovery_attempts_exhausted
873
+ @recovery_attempts_exhausted.call if @recovery_attempts_exhausted
874
+ end
875
+
643
876
  # @private
644
877
  def instantiate_connection_level_exception(frame)
645
878
  case frame
@@ -669,9 +902,45 @@ module Bunny
669
902
  end
670
903
  end
671
904
 
905
+ def clean_up_and_fail_on_connection_close!(method)
906
+ @last_connection_error = instantiate_connection_level_exception(method)
907
+ @continuations.push(method)
908
+
909
+ clean_up_on_shutdown
910
+ if threaded?
911
+ @session_error_handler.raise(@last_connection_error)
912
+ else
913
+ raise @last_connection_error
914
+ end
915
+ end
916
+
917
+ def clean_up_on_shutdown
918
+ begin
919
+ shut_down_all_consumer_work_pools!
920
+ maybe_shutdown_reader_loop
921
+ maybe_shutdown_heartbeat_sender
922
+ rescue ShutdownSignal => _sse
923
+ # no-op
924
+ rescue Exception => e
925
+ @logger.warn "Caught an exception when cleaning up after receiving connection.close: #{e.message}"
926
+ ensure
927
+ close_transport
928
+ end
929
+ end
930
+
672
931
  # @private
673
- def hostname_from(options)
674
- options[:host] || options[:hostname] || DEFAULT_HOST
932
+ def addresses_from(options)
933
+ shuffle_strategy = options.fetch(:hosts_shuffle_strategy, @default_hosts_shuffle_strategy)
934
+
935
+ addresses = options[:host] || options[:hostname] || options[:addresses] ||
936
+ options[:hosts] || ["#{DEFAULT_HOST}:#{port_from(options)}"]
937
+ addresses = [addresses] unless addresses.is_a? Array
938
+
939
+ addrs = addresses.map do |address|
940
+ host_with_port?(address) ? address : "#{address}:#{port_from(@opts)}"
941
+ end
942
+
943
+ shuffle_strategy.call(addrs)
675
944
  end
676
945
 
677
946
  # @private
@@ -685,6 +954,63 @@ module Bunny
685
954
  options.fetch(:port, fallback)
686
955
  end
687
956
 
957
+ # @private
958
+ def host_with_port?(address)
959
+ # we need to handle cases such as [2001:db8:85a3:8d3:1319:8a2e:370:7348]:5671
960
+ last_colon = address.rindex(":")
961
+ last_closing_square_bracket = address.rindex("]")
962
+
963
+ if last_closing_square_bracket.nil?
964
+ address.include?(":")
965
+ else
966
+ last_closing_square_bracket < last_colon
967
+ end
968
+ end
969
+
970
+ # @private
971
+ def host_from_address(address)
972
+ # we need to handle cases such as [2001:db8:85a3:8d3:1319:8a2e:370:7348]:5671
973
+ last_colon = address.rindex(":")
974
+ last_closing_square_bracket = address.rindex("]")
975
+
976
+ if last_closing_square_bracket.nil?
977
+ parts = address.split(":")
978
+ # this looks like an unquoted IPv6 address, so emit a warning
979
+ if parts.size > 2
980
+ @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]"
981
+ end
982
+ return parts[0]
983
+ end
984
+
985
+ if last_closing_square_bracket < last_colon
986
+ # there is a port
987
+ address[0, last_colon]
988
+ elsif last_closing_square_bracket > last_colon
989
+ address
990
+ end
991
+ end
992
+
993
+ # @private
994
+ def port_from_address(address)
995
+ # we need to handle cases such as [2001:db8:85a3:8d3:1319:8a2e:370:7348]:5671
996
+ last_colon = address.rindex(":")
997
+ last_closing_square_bracket = address.rindex("]")
998
+
999
+ if last_closing_square_bracket.nil?
1000
+ parts = address.split(":")
1001
+ # this looks like an unquoted IPv6 address, so emit a warning
1002
+ if parts.size > 2
1003
+ @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]"
1004
+ end
1005
+ return parts[1].to_i
1006
+ end
1007
+
1008
+ if last_closing_square_bracket < last_colon
1009
+ # there is a port
1010
+ address[(last_colon + 1)..-1].to_i
1011
+ end
1012
+ end
1013
+
688
1014
  # @private
689
1015
  def vhost_from(options)
690
1016
  options[:virtual_host] || options[:vhost] || DEFAULT_VHOST
@@ -702,7 +1028,7 @@ module Bunny
702
1028
 
703
1029
  # @private
704
1030
  def heartbeat_from(options)
705
- options[:heartbeat] || options[:heartbeat_interval] || options[:requested_heartbeat] || DEFAULT_HEARTBEAT
1031
+ options[:heartbeat] || options[:heartbeat_timeout] || options[:requested_heartbeat] || options[:heartbeat_interval] || DEFAULT_HEARTBEAT
706
1032
  end
707
1033
 
708
1034
  # @private
@@ -739,7 +1065,7 @@ module Bunny
739
1065
 
740
1066
  # @private
741
1067
  def reader_loop
742
- @reader_loop ||= ReaderLoop.new(@transport, self, Thread.current)
1068
+ @reader_loop ||= ReaderLoop.new(@transport, self, @session_error_handler)
743
1069
  end
744
1070
 
745
1071
  # @private
@@ -749,17 +1075,8 @@ module Bunny
749
1075
  if threaded?
750
1076
  # this is the easiest way to wait until the loop
751
1077
  # is guaranteed to have terminated
752
- @reader_loop.raise(ShutdownSignal)
753
- # joining the thread here may take forever
754
- # on JRuby because sun.nio.ch.KQueueArrayWrapper#kevent0 is
755
- # a native method that cannot be (easily) interrupted.
756
- # So we use this ugly hack or else our test suite takes forever
757
- # to run on JRuby (a new connection is opened/closed per example). MK.
758
- if defined?(JRUBY_VERSION)
759
- sleep 0.075
760
- else
761
- @reader_loop.join
762
- end
1078
+ @reader_loop.terminate_with(ShutdownSignal)
1079
+ @reader_loop.join
763
1080
  else
764
1081
  # single threaded mode, nothing to do. MK.
765
1082
  end
@@ -793,6 +1110,9 @@ module Bunny
793
1110
  # @private
794
1111
  def send_frame(frame, signal_activity = true)
795
1112
  if open?
1113
+ # @transport_mutex.synchronize do
1114
+ # @transport.write(frame.encode)
1115
+ # end
796
1116
  @transport.write(frame.encode)
797
1117
  signal_activity! if signal_activity
798
1118
  else
@@ -815,7 +1135,7 @@ module Bunny
815
1135
  end
816
1136
  end
817
1137
 
818
- # Sends multiple frames, one by one. For thread safety this method takes a channel
1138
+ # Sends multiple frames, in one go. For thread safety this method takes a channel
819
1139
  # object and synchronizes on it.
820
1140
  #
821
1141
  # @private
@@ -824,10 +1144,18 @@ module Bunny
824
1144
  # threads publish on the same channel aggressively, at some point frames will be
825
1145
  # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
826
1146
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
827
- # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
1147
+ # locking. Note that "single frame" methods technically do not need this kind of synchronization
1148
+ # (no incorrect frame interleaving of the same kind as with basic.publish isn't possible) but we
1149
+ # still recommend not sharing channels between threads except for consumer-only cases in the docs. MK.
828
1150
  channel.synchronize do
829
- frames.each { |frame| self.send_frame(frame, false) }
830
- signal_activity!
1151
+ # see rabbitmq/rabbitmq-server#156
1152
+ if open?
1153
+ data = frames.reduce(+"") { |acc, frame| acc << frame.encode }
1154
+ @transport.write(data)
1155
+ signal_activity!
1156
+ else
1157
+ raise ConnectionClosedError.new(frames)
1158
+ end
831
1159
  end
832
1160
  end # send_frameset(frames)
833
1161
 
@@ -841,10 +1169,14 @@ module Bunny
841
1169
  # threads publish on the same channel aggressively, at some point frames will be
842
1170
  # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
843
1171
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
844
- # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
1172
+ # locking. See a note about "single frame" methods in a comment in `send_frameset`. MK.
845
1173
  channel.synchronize do
846
- frames.each { |frame| self.send_frame_without_timeout(frame, false) }
847
- signal_activity!
1174
+ if open?
1175
+ frames.each { |frame| self.send_frame_without_timeout(frame, false) }
1176
+ signal_activity!
1177
+ else
1178
+ raise ConnectionClosedError.new(frames)
1179
+ end
848
1180
  end
849
1181
  end # send_frameset_without_timeout(frames)
850
1182
 
@@ -864,7 +1196,12 @@ module Bunny
864
1196
  # @return [String]
865
1197
  # @api public
866
1198
  def to_s
867
- "#<#{self.class.name}:#{object_id} #{@user}@#{@host}:#{@port}, vhost=#{@vhost}>"
1199
+ oid = ("0x%x" % (self.object_id << 1))
1200
+ "#<#{self.class.name}:#{oid} #{@user}@#{host}:#{port}, vhost=#{@vhost}, addresses=[#{@addresses.join(',')}]>"
1201
+ end
1202
+
1203
+ def inspect
1204
+ to_s
868
1205
  end
869
1206
 
870
1207
  protected
@@ -881,7 +1218,7 @@ module Bunny
881
1218
  @server_authentication_mechanisms = (connection_start.mechanisms || "").split(" ")
882
1219
  @server_locales = Array(connection_start.locales)
883
1220
 
884
- @status = :connected
1221
+ @status_mutex.synchronize { @status = :connected }
885
1222
  end
886
1223
 
887
1224
  # @private
@@ -890,16 +1227,18 @@ module Bunny
890
1227
  @logger.debug "Sent connection.start-ok"
891
1228
 
892
1229
  frame = begin
893
- @transport.read_next_frame
1230
+ fr = @transport.read_next_frame
1231
+ while fr.is_a?(AMQ::Protocol::HeartbeatFrame)
1232
+ fr = @transport.read_next_frame
1233
+ end
1234
+ fr
894
1235
  # frame timeout means the broker has closed the TCP connection, which it
895
1236
  # does per 0.9.1 spec.
896
- rescue Errno::ECONNRESET, ClientTimeout, AMQ::Protocol::EmptyResponseError, EOFError, IOError => e
1237
+ rescue
897
1238
  nil
898
1239
  end
899
1240
  if frame.nil?
900
- @state = :closed
901
- @logger.error "RabbitMQ closed TCP connection before AMQP 0.9.1 connection was finalized. Most likely this means authentication failure."
902
- raise Bunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
1241
+ raise TCPConnectionFailed.new('An empty frame was received while opening the connection. In RabbitMQ <= 3.1 this could mean an authentication issue.')
903
1242
  end
904
1243
 
905
1244
  response = frame.decode_payload
@@ -921,31 +1260,46 @@ module Bunny
921
1260
  else
922
1261
  negotiate_value(@client_heartbeat, connection_tune.heartbeat)
923
1262
  end
924
- @logger.debug "Heartbeat interval negotiation: client = #{@client_heartbeat}, server = #{connection_tune.heartbeat}, result = #{@heartbeat}"
925
- @logger.info "Heartbeat interval used (in seconds): #{@heartbeat}"
926
-
927
- @channel_id_allocator = ChannelIdAllocator.new(@channel_max)
1263
+ @logger.debug { "Heartbeat interval negotiation: client = #{@client_heartbeat}, server = #{connection_tune.heartbeat}, result = #{@heartbeat}" }
1264
+ @logger.debug "Heartbeat interval used (in seconds): #{@heartbeat}"
1265
+
1266
+ # We set the read_write_timeout to twice the heartbeat value,
1267
+ # and then some padding for edge cases.
1268
+ # This allows us to miss a single heartbeat before we time out the socket.
1269
+ # If heartbeats are disabled, assume that TCP keepalives or a similar mechanism will be used
1270
+ # and disable socket read timeouts. See ruby-amqp/bunny#551.
1271
+ @transport.read_timeout = @heartbeat * 2.2
1272
+ @logger.debug { "Will use socket read timeout of #{@transport.read_timeout.to_i} seconds" }
1273
+
1274
+ # if there are existing channels we've just recovered from
1275
+ # a network failure and need to fix the allocated set. See issue 205. MK.
1276
+ if @channels.empty?
1277
+ @logger.debug { "Initializing channel ID allocator with channel_max = #{@channel_max}" }
1278
+ @channel_id_allocator = ChannelIdAllocator.new(@channel_max)
1279
+ end
928
1280
 
929
1281
  @transport.send_frame(AMQ::Protocol::Connection::TuneOk.encode(@channel_max, @frame_max, @heartbeat))
930
- @logger.debug "Sent connection.tune-ok with heartbeat interval = #{@heartbeat}, frame_max = #{@frame_max}, channel_max = #{@channel_max}"
1282
+ @logger.debug { "Sent connection.tune-ok with heartbeat interval = #{@heartbeat}, frame_max = #{@frame_max}, channel_max = #{@channel_max}" }
931
1283
  @transport.send_frame(AMQ::Protocol::Connection::Open.encode(self.vhost))
932
- @logger.debug "Sent connection.open with vhost = #{self.vhost}"
1284
+ @logger.debug { "Sent connection.open with vhost = #{self.vhost}" }
933
1285
 
934
1286
  frame2 = begin
935
- @transport.read_next_frame
1287
+ fr = @transport.read_next_frame
1288
+ while fr.is_a?(AMQ::Protocol::HeartbeatFrame)
1289
+ fr = @transport.read_next_frame
1290
+ end
1291
+ fr
936
1292
  # frame timeout means the broker has closed the TCP connection, which it
937
1293
  # does per 0.9.1 spec.
938
- rescue Errno::ECONNRESET, ClientTimeout, AMQ::Protocol::EmptyResponseError, EOFError => e
1294
+ rescue
939
1295
  nil
940
1296
  end
941
1297
  if frame2.nil?
942
- @state = :closed
943
- @logger.warn "RabbitMQ closed TCP connection before AMQP 0.9.1 connection was finalized. Most likely this means authentication failure."
944
- raise Bunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
1298
+ raise TCPConnectionFailed.new('An empty frame was received while opening the connection. In RabbitMQ <= 3.1 this could mean an authentication issue.')
945
1299
  end
946
1300
  connection_open_ok = frame2.decode_payload
947
1301
 
948
- @status = :open
1302
+ @status_mutex.synchronize { @status = :open }
949
1303
  if @heartbeat && @heartbeat > 0
950
1304
  initialize_heartbeat_sender
951
1305
  end
@@ -956,7 +1310,7 @@ module Bunny
956
1310
  begin
957
1311
  shut_down_all_consumer_work_pools!
958
1312
  maybe_shutdown_reader_loop
959
- rescue ShutdownSignal => sse
1313
+ rescue ShutdownSignal => _sse
960
1314
  # no-op
961
1315
  rescue Exception => e
962
1316
  @logger.warn "Caught an exception when cleaning up after receiving connection.close: #{e.message}"
@@ -964,7 +1318,11 @@ module Bunny
964
1318
  close_transport
965
1319
  end
966
1320
 
967
- @origin_thread.raise(e)
1321
+ if threaded?
1322
+ @session_error_handler.raise(e)
1323
+ else
1324
+ raise e
1325
+ end
968
1326
  else
969
1327
  raise "could not open connection: server did not respond with connection.open-ok but #{connection_open_ok.inspect} instead"
970
1328
  end
@@ -977,7 +1335,7 @@ module Bunny
977
1335
 
978
1336
  # @private
979
1337
  def negotiate_value(client_value, server_value)
980
- return server_value if client_value == :server
1338
+ return server_value if [:server, "server"].include?(client_value)
981
1339
 
982
1340
  if client_value == 0 || server_value == 0
983
1341
  [client_value, server_value].max
@@ -1001,7 +1359,22 @@ module Bunny
1001
1359
 
1002
1360
  # @private
1003
1361
  def initialize_transport
1004
- @transport = Transport.new(self, @host, @port, @opts.merge(:session_thread => @origin_thread))
1362
+ if address = @addresses[ @address_index ]
1363
+ @address_index_mutex.synchronize { @address_index += 1 }
1364
+ @transport.close rescue nil # Let's make sure the previous transport socket is closed
1365
+ @transport = Transport.new(self,
1366
+ host_from_address(address),
1367
+ port_from_address(address),
1368
+ @opts.merge(:session_error_handler => @session_error_handler)
1369
+ )
1370
+
1371
+ # Reset the cached progname for the logger only when no logger was provided
1372
+ @default_logger.progname = self.to_s
1373
+
1374
+ @transport
1375
+ else
1376
+ raise HostListDepleted
1377
+ end
1005
1378
  end
1006
1379
 
1007
1380
  # @private
@@ -1027,16 +1400,9 @@ module Bunny
1027
1400
  Authentication::CredentialsEncoder.for_session(self)
1028
1401
  end
1029
1402
 
1030
- if defined?(JRUBY_VERSION)
1031
- # @private
1032
- def reset_continuations
1033
- @continuations = Concurrent::LinkedContinuationQueue.new
1034
- end
1035
- else
1036
- # @private
1037
- def reset_continuations
1038
- @continuations = Concurrent::ContinuationQueue.new
1039
- end
1403
+ # @private
1404
+ def reset_continuations
1405
+ @continuations = Concurrent::ContinuationQueue.new
1040
1406
  end
1041
1407
 
1042
1408
  # @private
@@ -1049,12 +1415,22 @@ module Bunny
1049
1415
  end
1050
1416
 
1051
1417
  # @private
1052
- def init_logger(level)
1053
- @logger = ::Logger.new(@logfile)
1054
- @logger.level = normalize_log_level(level)
1055
- @logger.progname = self.to_s
1418
+ def init_default_logger(logfile, level)
1419
+ @default_logger = begin
1420
+ lgr = ::Logger.new(logfile)
1421
+ lgr.level = normalize_log_level(level)
1422
+ lgr.progname = self.to_s
1423
+ lgr
1424
+ end
1425
+ end
1056
1426
 
1057
- @logger
1427
+ # @private
1428
+ def init_default_logger_without_progname(logfile, level)
1429
+ @default_logger = begin
1430
+ lgr = ::Logger.new(logfile)
1431
+ lgr.level = normalize_log_level(level)
1432
+ lgr
1433
+ end
1058
1434
  end
1059
1435
 
1060
1436
  # @private
@@ -1078,6 +1454,7 @@ module Bunny
1078
1454
  end
1079
1455
 
1080
1456
  def normalize_client_channel_max(n)
1457
+ return CHANNEL_MAX_LIMIT if n.nil?
1081
1458
  return CHANNEL_MAX_LIMIT if n > CHANNEL_MAX_LIMIT
1082
1459
 
1083
1460
  case n
@@ -1087,6 +1464,25 @@ module Bunny
1087
1464
  n
1088
1465
  end
1089
1466
  end
1467
+
1468
+ def normalize_auth_mechanism(value)
1469
+ case value
1470
+ when [] then
1471
+ "PLAIN"
1472
+ when nil then
1473
+ "PLAIN"
1474
+ else
1475
+ value
1476
+ end
1477
+ end
1478
+
1479
+ def ignoring_io_errors(&block)
1480
+ begin
1481
+ block.call
1482
+ rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError, Bunny::NetworkFailure => _
1483
+ # ignore
1484
+ end
1485
+ end
1090
1486
  end # Session
1091
1487
 
1092
1488
  # backwards compatibility