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