garaio_bunny 2.19.1

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +231 -0
  3. data/lib/amq/protocol/extensions.rb +16 -0
  4. data/lib/bunny/authentication/credentials_encoder.rb +55 -0
  5. data/lib/bunny/authentication/external_mechanism_encoder.rb +27 -0
  6. data/lib/bunny/authentication/plain_mechanism_encoder.rb +19 -0
  7. data/lib/bunny/channel.rb +2055 -0
  8. data/lib/bunny/channel_id_allocator.rb +82 -0
  9. data/lib/bunny/concurrent/atomic_fixnum.rb +75 -0
  10. data/lib/bunny/concurrent/condition.rb +66 -0
  11. data/lib/bunny/concurrent/continuation_queue.rb +62 -0
  12. data/lib/bunny/concurrent/linked_continuation_queue.rb +61 -0
  13. data/lib/bunny/concurrent/synchronized_sorted_set.rb +56 -0
  14. data/lib/bunny/consumer.rb +128 -0
  15. data/lib/bunny/consumer_tag_generator.rb +23 -0
  16. data/lib/bunny/consumer_work_pool.rb +122 -0
  17. data/lib/bunny/cruby/socket.rb +110 -0
  18. data/lib/bunny/cruby/ssl_socket.rb +118 -0
  19. data/lib/bunny/delivery_info.rb +93 -0
  20. data/lib/bunny/exceptions.rb +269 -0
  21. data/lib/bunny/exchange.rb +275 -0
  22. data/lib/bunny/framing.rb +56 -0
  23. data/lib/bunny/get_response.rb +83 -0
  24. data/lib/bunny/heartbeat_sender.rb +71 -0
  25. data/lib/bunny/jruby/socket.rb +57 -0
  26. data/lib/bunny/jruby/ssl_socket.rb +58 -0
  27. data/lib/bunny/message_properties.rb +119 -0
  28. data/lib/bunny/queue.rb +393 -0
  29. data/lib/bunny/reader_loop.rb +158 -0
  30. data/lib/bunny/return_info.rb +74 -0
  31. data/lib/bunny/session.rb +1483 -0
  32. data/lib/bunny/socket.rb +14 -0
  33. data/lib/bunny/ssl_socket.rb +14 -0
  34. data/lib/bunny/test_kit.rb +41 -0
  35. data/lib/bunny/timeout.rb +7 -0
  36. data/lib/bunny/transport.rb +526 -0
  37. data/lib/bunny/version.rb +6 -0
  38. data/lib/bunny/versioned_delivery_tag.rb +28 -0
  39. data/lib/bunny.rb +92 -0
  40. metadata +127 -0
@@ -0,0 +1,1483 @@
1
+ require "socket"
2
+ require "thread"
3
+ require "monitor"
4
+
5
+ require "bunny/transport"
6
+ require "bunny/channel_id_allocator"
7
+ require "bunny/heartbeat_sender"
8
+ require "bunny/reader_loop"
9
+ require "bunny/authentication/credentials_encoder"
10
+ require "bunny/authentication/plain_mechanism_encoder"
11
+ require "bunny/authentication/external_mechanism_encoder"
12
+
13
+ if defined?(JRUBY_VERSION)
14
+ require "bunny/concurrent/linked_continuation_queue"
15
+ else
16
+ require "bunny/concurrent/continuation_queue"
17
+ end
18
+
19
+ require "amq/protocol/client"
20
+ require "amq/settings"
21
+
22
+ module Bunny
23
+ # Represents AMQP 0.9.1 connection (to a RabbitMQ node).
24
+ # @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
25
+ class Session
26
+
27
+ # Default host used for connection
28
+ DEFAULT_HOST = "127.0.0.1"
29
+ # Default virtual host used for connection
30
+ DEFAULT_VHOST = "/"
31
+ # Default username used for connection
32
+ DEFAULT_USER = "guest"
33
+ # Default password used for connection
34
+ DEFAULT_PASSWORD = "guest"
35
+ # Default heartbeat interval, the same value as RabbitMQ 3.0 uses.
36
+ DEFAULT_HEARTBEAT = :server
37
+ # @private
38
+ DEFAULT_FRAME_MAX = 131072
39
+ # Hard limit the user cannot go over regardless of server configuration.
40
+ # @private
41
+ CHANNEL_MAX_LIMIT = 65535
42
+ DEFAULT_CHANNEL_MAX = 2047
43
+
44
+ # backwards compatibility
45
+ # @private
46
+ CONNECT_TIMEOUT = Transport::DEFAULT_CONNECTION_TIMEOUT
47
+
48
+ # @private
49
+ DEFAULT_CONTINUATION_TIMEOUT = 15000
50
+
51
+ # RabbitMQ client metadata
52
+ DEFAULT_CLIENT_PROPERTIES = {
53
+ :capabilities => {
54
+ :publisher_confirms => true,
55
+ :consumer_cancel_notify => true,
56
+ :exchange_exchange_bindings => true,
57
+ :"basic.nack" => true,
58
+ :"connection.blocked" => true,
59
+ # See http://www.rabbitmq.com/auth-notification.html
60
+ :authentication_failure_close => true
61
+ },
62
+ :product => "Bunny",
63
+ :platform => ::RUBY_DESCRIPTION,
64
+ :version => Bunny::VERSION,
65
+ :information => "http://rubybunny.info",
66
+ }
67
+
68
+ # @private
69
+ DEFAULT_LOCALE = "en_GB"
70
+
71
+ # Default reconnection interval for TCP connection failures
72
+ DEFAULT_NETWORK_RECOVERY_INTERVAL = 5.0
73
+
74
+
75
+ #
76
+ # API
77
+ #
78
+
79
+ # @return [Bunny::Transport]
80
+ attr_reader :transport
81
+ attr_reader :status, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
82
+ attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
83
+ attr_reader :channel_id_allocator
84
+ # Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
85
+ # @return [String]
86
+ attr_reader :mechanism
87
+ # @return [Logger]
88
+ attr_reader :logger
89
+ # @return [Integer] Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 15000.
90
+ attr_reader :continuation_timeout
91
+ attr_reader :network_recovery_interval
92
+ attr_reader :connection_name
93
+ attr_accessor :socket_configurator
94
+
95
+ # @param [String, Hash] connection_string_or_opts Connection string or a hash of connection options
96
+ # @param [Hash] optz Extra options not related to connection
97
+ #
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
101
+ # @option connection_string_or_opts [Integer] :port (5672) Port RabbitMQ listens on
102
+ # @option connection_string_or_opts [String] :username ("guest") Username
103
+ # @option connection_string_or_opts [String] :password ("guest") Password
104
+ # @option connection_string_or_opts [String] :vhost ("/") Virtual host to use
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).
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.
107
+ # @option connection_string_or_opts [Boolean] :tls (false) Should TLS/SSL be used?
108
+ # @option connection_string_or_opts [String] :tls_cert (nil) Path to client TLS/SSL certificate file (.pem)
109
+ # @option connection_string_or_opts [String] :tls_key (nil) Path to client TLS/SSL private key file (.pem)
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
111
+ # @option connection_string_or_opts [String] :verify_peer (true) Whether TLS peer verification should be performed
112
+ # @option connection_string_or_opts [Symbol] :tls_version (negotiated) What TLS version should be used (:TLSv1, :TLSv1_1, or :TLSv1_2)
113
+ # @option connection_string_or_opts [Integer] :channel_max (2047) Maximum number of channels allowed on this connection, minus 1 to account for the special channel 0.
114
+ # @option connection_string_or_opts [Integer] :continuation_timeout (15000) Timeout for client operations that expect a response (e.g. {Bunny::Queue#get}), in milliseconds.
115
+ # @option connection_string_or_opts [Integer] :connection_timeout (30) Timeout in seconds for connecting to the server.
116
+ # @option connection_string_or_opts [Integer] :read_timeout (30) TCP socket read timeout in seconds. If heartbeats are disabled this will be ignored.
117
+ # @option connection_string_or_opts [Integer] :write_timeout (30) TCP socket write timeout in seconds.
118
+ # @option connection_string_or_opts [Proc] :hosts_shuffle_strategy a callable that reorders a list of host strings, defaults to Array#shuffle
119
+ # @option connection_string_or_opts [Proc] :recovery_completed a callable that will be called when a network recovery is performed
120
+ # @option connection_string_or_opts [Logger] :logger The logger. If missing, one is created using :log_file and :log_level.
121
+ # @option connection_string_or_opts [IO, String] :log_file The file or path to use when creating a logger. Defaults to STDOUT.
122
+ # @option connection_string_or_opts [IO, String] :logfile DEPRECATED: use :log_file instead. The file or path to use when creating a logger. Defaults to STDOUT.
123
+ # @option connection_string_or_opts [Integer] :log_level The log level to use when creating a logger. Defaults to LOGGER::WARN
124
+ # @option connection_string_or_opts [Boolean] :automatically_recover (true) Should automatically recover from network failures?
125
+ # @option connection_string_or_opts [Integer] :recovery_attempts (nil) Max number of recovery attempts, nil means forever
126
+ # @option connection_string_or_opts [Integer] :reset_recovery_attempts_after_reconnection (true) Should recovery attempt counter be reset after successful reconnection? When set to false, the attempt counter will last through the entire lifetime of the connection object.
127
+ # @option connection_string_or_opts [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 [Boolean] :recover_from_connection_close (true) Should this connection recover after receiving a server-sent connection.close (e.g. connection was force closed)?
130
+ # @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.
131
+ #
132
+ # @option optz [String] :auth_mechanism ("PLAIN") Authentication mechanism, PLAIN or EXTERNAL
133
+ # @option optz [String] :locale ("PLAIN") Locale RabbitMQ should use
134
+ # @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.
135
+ #
136
+ # @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
137
+ # @see http://rubybunny.info/articles/tls.html TLS/SSL guide
138
+ # @api public
139
+ def initialize(connection_string_or_opts = ENV['RABBITMQ_URL'], optz = Hash.new)
140
+ opts = case (connection_string_or_opts)
141
+ when nil then
142
+ Hash.new
143
+ when String then
144
+ self.class.parse_uri(connection_string_or_opts)
145
+ when Hash then
146
+ connection_string_or_opts
147
+ end.merge(optz)
148
+
149
+ @default_hosts_shuffle_strategy = Proc.new { |hosts| hosts.shuffle }
150
+
151
+ @opts = opts
152
+ log_file = opts[:log_file] || opts[:logfile] || STDOUT
153
+ log_level = opts[:log_level] || ENV["BUNNY_LOG_LEVEL"] || Logger::WARN
154
+ # we might need to log a warning about ill-formatted IPv6 address but
155
+ # progname includes hostname, so init like this first
156
+ @logger = opts.fetch(:logger, init_default_logger_without_progname(log_file, log_level))
157
+
158
+ @addresses = self.addresses_from(opts)
159
+ @address_index = 0
160
+
161
+ @transport = nil
162
+ @user = self.username_from(opts)
163
+ @pass = self.password_from(opts)
164
+ @vhost = self.vhost_from(opts)
165
+ @threaded = opts.fetch(:threaded, true)
166
+
167
+ # re-init, see above
168
+ @logger = opts.fetch(:logger, init_default_logger(log_file, log_level))
169
+
170
+ validate_connection_options(opts)
171
+ @last_connection_error = nil
172
+
173
+ # should automatic recovery from network failures be used?
174
+ @automatically_recover = if opts[:automatically_recover].nil? && opts[:automatic_recovery].nil?
175
+ true
176
+ else
177
+ opts[:automatically_recover] | opts[:automatic_recovery]
178
+ end
179
+ @recovering_from_network_failure = false
180
+ @max_recovery_attempts = opts[:recovery_attempts]
181
+ @recovery_attempts = @max_recovery_attempts
182
+ # When this is set, connection attempts won't be reset after
183
+ # successful reconnection. Some find this behavior more sensible
184
+ # than the per-failure attempt counter. MK.
185
+ @reset_recovery_attempt_counter_after_reconnection = opts.fetch(:reset_recovery_attempts_after_reconnection, true)
186
+
187
+ @network_recovery_interval = opts.fetch(:network_recovery_interval, DEFAULT_NETWORK_RECOVERY_INTERVAL)
188
+ @recover_from_connection_close = opts.fetch(:recover_from_connection_close, true)
189
+ # in ms
190
+ @continuation_timeout = opts.fetch(:continuation_timeout, DEFAULT_CONTINUATION_TIMEOUT)
191
+
192
+ @status = :not_connected
193
+ @manually_closed = false
194
+ @blocked = false
195
+
196
+ # these are negotiated with the broker during the connection tuning phase
197
+ @client_frame_max = opts.fetch(:frame_max, DEFAULT_FRAME_MAX)
198
+ @client_channel_max = normalize_client_channel_max(opts.fetch(:channel_max, DEFAULT_CHANNEL_MAX))
199
+ # will be-renegotiated during connection tuning steps. MK.
200
+ @channel_max = @client_channel_max
201
+ @heartbeat_sender = nil
202
+ @client_heartbeat = self.heartbeat_from(opts)
203
+
204
+ client_props = opts[:properties] || opts[:client_properties] || {}
205
+ @connection_name = client_props[:connection_name] || opts[:connection_name]
206
+ @client_properties = DEFAULT_CLIENT_PROPERTIES.merge(client_props)
207
+ .merge(connection_name: connection_name)
208
+ @mechanism = normalize_auth_mechanism(opts.fetch(:auth_mechanism, "PLAIN"))
209
+ @credentials_encoder = credentials_encoder_for(@mechanism)
210
+ @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
211
+
212
+ @mutex_impl = @opts.fetch(:mutex_impl, Monitor)
213
+
214
+ # mutex for the channel id => channel hash
215
+ @channel_mutex = @mutex_impl.new
216
+ # transport operations/continuations mutex. A workaround for
217
+ # the non-reentrant Ruby mutexes. MK.
218
+ @transport_mutex = @mutex_impl.new
219
+ @status_mutex = @mutex_impl.new
220
+ @address_index_mutex = @mutex_impl.new
221
+
222
+ @channels = Hash.new
223
+
224
+ @recovery_attempt_started = opts[:recovery_attempt_started]
225
+ @recovery_completed = opts[:recovery_completed]
226
+
227
+ @session_error_handler = opts.fetch(:session_error_handler, Thread.current)
228
+
229
+ self.reset_continuations
230
+ self.initialize_transport
231
+
232
+ end
233
+
234
+ def validate_connection_options(options)
235
+ if options[:hosts] && options[:addresses]
236
+ raise ArgumentError, "Connection options can't contain hosts and addresses at the same time"
237
+ end
238
+
239
+ if (options[:host] || options[:hostname]) && (options[:hosts] || options[:addresses])
240
+ @logger.warn "Connection options contain both a host and an array of hosts (addresses), please pick one."
241
+ end
242
+ end
243
+
244
+ # @return [String] RabbitMQ hostname (or IP address) used
245
+ def hostname; self.host; end
246
+ # @return [String] Username used
247
+ def username; self.user; end
248
+ # @return [String] Password used
249
+ def password; self.pass; end
250
+ # @return [String] Virtual host used
251
+ def virtual_host; self.vhost; end
252
+
253
+ # @deprecated
254
+ # @return [Integer] Heartbeat timeout (not interval) used
255
+ def heartbeat_interval; self.heartbeat; end
256
+
257
+ # @return [Integer] Heartbeat timeout used
258
+ def heartbeat_timeout; self.heartbeat; end
259
+
260
+ # @return [Boolean] true if this connection uses TLS (SSL)
261
+ def uses_tls?
262
+ @transport.uses_tls?
263
+ end
264
+ alias tls? uses_tls?
265
+
266
+ # @return [Boolean] true if this connection uses TLS (SSL)
267
+ def uses_ssl?
268
+ @transport.uses_ssl?
269
+ end
270
+ alias ssl? uses_ssl?
271
+
272
+ # @return [Boolean] true if this connection uses a separate thread for I/O activity
273
+ def threaded?
274
+ @threaded
275
+ end
276
+
277
+ def host
278
+ @transport ? @transport.host : host_from_address(@addresses[@address_index])
279
+ end
280
+
281
+ def port
282
+ @transport ? @transport.port : port_from_address(@addresses[@address_index])
283
+ end
284
+
285
+ def reset_address_index
286
+ @address_index_mutex.synchronize { @address_index = 0 }
287
+ end
288
+
289
+ # @private
290
+ attr_reader :mutex_impl
291
+
292
+ # Provides a way to fine tune the socket used by connection.
293
+ # Accepts a block that the socket will be yielded to.
294
+ def configure_socket(&block)
295
+ raise ArgumentError, "No block provided!" if block.nil?
296
+
297
+ @transport.configure_socket(&block)
298
+ end
299
+
300
+ # @return [Integer] Client socket port
301
+ def local_port
302
+ @transport.local_address.ip_port
303
+ end
304
+
305
+ # Starts the connection process.
306
+ #
307
+ # @see http://rubybunny.info/articles/getting_started.html
308
+ # @see http://rubybunny.info/articles/connecting.html
309
+ # @api public
310
+ def start
311
+ return self if connected?
312
+
313
+ @status_mutex.synchronize { @status = :connecting }
314
+ # reset here for cases when automatic network recovery kicks in
315
+ # when we were blocked. MK.
316
+ @blocked = false
317
+ self.reset_continuations
318
+
319
+ begin
320
+ begin
321
+ # close existing transport if we have one,
322
+ # to not leak sockets
323
+ @transport.maybe_initialize_socket
324
+
325
+ @transport.post_initialize_socket
326
+ @transport.connect
327
+
328
+ self.init_connection
329
+ self.open_connection
330
+
331
+ @reader_loop = nil
332
+ self.start_reader_loop if threaded?
333
+
334
+ rescue TCPConnectionFailed => e
335
+ @logger.warn e.message
336
+ self.initialize_transport
337
+ @logger.warn "Will try to connect to the next endpoint in line: #{@transport.host}:#{@transport.port}"
338
+
339
+ return self.start
340
+ rescue
341
+ @status_mutex.synchronize { @status = :not_connected }
342
+ raise
343
+ end
344
+ rescue HostListDepleted
345
+ self.reset_address_index
346
+ @status_mutex.synchronize { @status = :not_connected }
347
+ raise TCPConnectionFailedForAllHosts
348
+ end
349
+ @status_mutex.synchronize { @manually_closed = false }
350
+
351
+ self
352
+ end
353
+
354
+ def update_secret(value, reason)
355
+ @transport.send_frame(AMQ::Protocol::Connection::UpdateSecret.encode(value, reason))
356
+ @last_update_secret_ok = wait_on_continuations
357
+ raise_if_continuation_resulted_in_a_connection_error!
358
+
359
+ @last_update_secret_ok
360
+ end
361
+
362
+ # Socket operation write timeout used by this connection
363
+ # @return [Integer]
364
+ # @private
365
+ def transport_write_timeout
366
+ @transport.write_timeout
367
+ end
368
+
369
+ # Opens a new channel and returns it. This method will block the calling
370
+ # thread until the response is received and the channel is guaranteed to be
371
+ # opened (this operation is very fast and inexpensive).
372
+ #
373
+ # @return [Bunny::Channel] Newly opened channel
374
+ def create_channel(n = nil, consumer_pool_size = 1, consumer_pool_abort_on_exception = false, consumer_pool_shutdown_timeout = 60)
375
+ raise ArgumentError, "channel number 0 is reserved in the protocol and cannot be used" if 0 == n
376
+ raise ConnectionAlreadyClosed if manually_closed?
377
+ raise RuntimeError, "this connection is not open. Was Bunny::Session#start invoked? Is automatic recovery enabled?" if !connected?
378
+
379
+ @channel_mutex.synchronize do
380
+ if n && (ch = @channels[n])
381
+ ch
382
+ else
383
+ ch = Bunny::Channel.new(self, n, ConsumerWorkPool.new(consumer_pool_size || 1, consumer_pool_abort_on_exception, consumer_pool_shutdown_timeout))
384
+ ch.open
385
+ ch
386
+ end
387
+ end
388
+ end
389
+ alias channel create_channel
390
+
391
+ # Closes the connection. This involves closing all of its channels.
392
+ def close(await_response = true)
393
+ @status_mutex.synchronize { @status = :closing }
394
+
395
+ ignoring_io_errors do
396
+ if @transport.open?
397
+ @logger.debug "Transport is still open..."
398
+ close_all_channels
399
+
400
+ @logger.debug "Will close all channels...."
401
+ self.close_connection(await_response)
402
+ end
403
+
404
+ clean_up_on_shutdown
405
+ end
406
+ @status_mutex.synchronize do
407
+ @status = :closed
408
+ @manually_closed = true
409
+ end
410
+ @logger.debug "Connection is closed"
411
+ true
412
+ end
413
+ alias stop close
414
+
415
+ # Creates a temporary channel, yields it to the block given to this
416
+ # method and closes it.
417
+ #
418
+ # @return [Bunny::Session] self
419
+ def with_channel(n = nil)
420
+ ch = create_channel(n)
421
+ begin
422
+ yield ch
423
+ ensure
424
+ ch.close if ch.open?
425
+ end
426
+
427
+ self
428
+ end
429
+
430
+ # @return [Boolean] true if this connection is still not fully open
431
+ def connecting?
432
+ status == :connecting
433
+ end
434
+
435
+ # @return [Boolean] true if this AMQP 0.9.1 connection is closing
436
+ # @api private
437
+ def closing?
438
+ @status_mutex.synchronize { @status == :closing }
439
+ end
440
+
441
+ # @return [Boolean] true if this AMQP 0.9.1 connection is closed
442
+ def closed?
443
+ @status_mutex.synchronize { @status == :closed }
444
+ end
445
+
446
+ # @return [Boolean] true if this AMQP 0.9.1 connection has been closed by the user (as opposed to the server)
447
+ def manually_closed?
448
+ @status_mutex.synchronize { @manually_closed == true }
449
+ end
450
+
451
+ # @return [Boolean] true if this AMQP 0.9.1 connection is open
452
+ def open?
453
+ @status_mutex.synchronize do
454
+ (status == :open || status == :connected || status == :connecting) && @transport.open?
455
+ end
456
+ end
457
+ alias connected? open?
458
+
459
+ # @return [Boolean] true if this connection has automatic recovery from network failure enabled
460
+ def automatically_recover?
461
+ @automatically_recover
462
+ end
463
+
464
+ # Defines a callback that will be executed when RabbitMQ blocks the connection
465
+ # because it is running low on memory or disk space (as configured via config file
466
+ # and/or rabbitmqctl).
467
+ #
468
+ # @yield [AMQ::Protocol::Connection::Blocked] connection.blocked method which provides a reason for blocking
469
+ #
470
+ # @api public
471
+ def on_blocked(&block)
472
+ @block_callback = block
473
+ end
474
+
475
+ # Defines a callback that will be executed when RabbitMQ unblocks the connection
476
+ # that was previously blocked, e.g. because the memory or disk space alarm has cleared.
477
+ #
478
+ # @see #on_blocked
479
+ # @api public
480
+ def on_unblocked(&block)
481
+ @unblock_callback = block
482
+ end
483
+
484
+ # @return [Boolean] true if the connection is currently blocked by RabbitMQ because it's running low on
485
+ # RAM, disk space, or other resource; false otherwise
486
+ # @see #on_blocked
487
+ # @see #on_unblocked
488
+ def blocked?
489
+ @blocked
490
+ end
491
+
492
+ # Parses an amqp[s] URI into a hash that {Bunny::Session#initialize} accepts.
493
+ #
494
+ # @param [String] uri amqp or amqps URI to parse
495
+ # @return [Hash] Parsed URI as a hash
496
+ def self.parse_uri(uri)
497
+ AMQ::Settings.configure(uri)
498
+ end
499
+
500
+ # Checks if a queue with given name exists.
501
+ #
502
+ # Implemented using queue.declare
503
+ # with passive set to true and a one-off (short lived) channel
504
+ # under the hood.
505
+ #
506
+ # @param [String] name Queue name
507
+ # @return [Boolean] true if queue exists
508
+ def queue_exists?(name)
509
+ ch = create_channel
510
+ begin
511
+ ch.queue(name, :passive => true)
512
+ true
513
+ rescue Bunny::NotFound => _
514
+ false
515
+ ensure
516
+ ch.close if ch.open?
517
+ end
518
+ end
519
+
520
+ # Checks if a exchange with given name exists.
521
+ #
522
+ # Implemented using exchange.declare
523
+ # with passive set to true and a one-off (short lived) channel
524
+ # under the hood.
525
+ #
526
+ # @param [String] name Exchange name
527
+ # @return [Boolean] true if exchange exists
528
+ def exchange_exists?(name)
529
+ ch = create_channel
530
+ begin
531
+ ch.exchange(name, :passive => true)
532
+ true
533
+ rescue Bunny::NotFound => _
534
+ false
535
+ ensure
536
+ ch.close if ch.open?
537
+ end
538
+ end
539
+
540
+ # Defines a callable (e.g. a block) that will be called
541
+ # before every connection recovery attempt.
542
+ def before_recovery_attempt_starts(&block)
543
+ @recovery_attempt_started = block
544
+ end
545
+
546
+ # Defines a callable (e.g. a block) that will be called
547
+ # after successful connection recovery.
548
+ def after_recovery_completed(&block)
549
+ @recovery_completed = block
550
+ end
551
+
552
+
553
+ #
554
+ # Implementation
555
+ #
556
+
557
+ # @private
558
+ def open_channel(ch)
559
+ @channel_mutex.synchronize do
560
+ n = ch.number
561
+ self.register_channel(ch)
562
+
563
+ @transport_mutex.synchronize do
564
+ @transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
565
+ end
566
+ @last_channel_open_ok = wait_on_continuations
567
+ raise_if_continuation_resulted_in_a_connection_error!
568
+
569
+ @last_channel_open_ok
570
+ end
571
+ end
572
+
573
+ # @private
574
+ def close_channel(ch)
575
+ @channel_mutex.synchronize do
576
+ n = ch.number
577
+
578
+ @transport.send_frame(AMQ::Protocol::Channel::Close.encode(n, 200, "Goodbye", 0, 0))
579
+ @last_channel_close_ok = wait_on_continuations
580
+ raise_if_continuation_resulted_in_a_connection_error!
581
+
582
+ self.unregister_channel(ch)
583
+ self.release_channel_id(ch.id)
584
+ @last_channel_close_ok
585
+ end
586
+ end
587
+
588
+ # @private
589
+ def find_channel(number)
590
+ @channels[number]
591
+ end
592
+
593
+ # @private
594
+ def synchronised_find_channel(number)
595
+ @channel_mutex.synchronize { @channels[number] }
596
+ end
597
+
598
+ # @private
599
+ def close_all_channels
600
+ @channel_mutex.synchronize do
601
+ @channels.reject {|n, ch| n == 0 || !ch.open? }.each do |_, ch|
602
+ Bunny::Timeout.timeout(@transport.disconnect_timeout, ClientTimeout) { ch.close }
603
+ end
604
+ end
605
+ end
606
+
607
+ # @private
608
+ def close_connection(await_response = true)
609
+ if @transport.open?
610
+ @logger.debug "Transport is still open"
611
+ @transport.send_frame(AMQ::Protocol::Connection::Close.encode(200, "Goodbye", 0, 0))
612
+
613
+ if await_response
614
+ @logger.debug "Waiting for a connection.close-ok..."
615
+ @last_connection_close_ok = wait_on_continuations
616
+ end
617
+ end
618
+
619
+ shut_down_all_consumer_work_pools!
620
+ maybe_shutdown_heartbeat_sender
621
+ @status_mutex.synchronize { @status = :not_connected }
622
+ end
623
+
624
+ # Handles incoming frames and dispatches them.
625
+ #
626
+ # Channel methods (`channel.open-ok`, `channel.close-ok`) are
627
+ # handled by the session itself.
628
+ # Connection level errors result in exceptions being raised.
629
+ # Deliveries and other methods are passed on to channels to dispatch.
630
+ #
631
+ # @private
632
+ def handle_frame(ch_number, method)
633
+ @logger.debug { "Session#handle_frame on #{ch_number}: #{method.inspect}" }
634
+ case method
635
+ when AMQ::Protocol::Channel::OpenOk then
636
+ @continuations.push(method)
637
+ when AMQ::Protocol::Channel::CloseOk then
638
+ @continuations.push(method)
639
+ when AMQ::Protocol::Connection::Close then
640
+ if recover_from_connection_close?
641
+ @logger.warn "Recovering from connection.close (#{method.reply_text})"
642
+ clean_up_on_shutdown
643
+ handle_network_failure(instantiate_connection_level_exception(method))
644
+ else
645
+ clean_up_and_fail_on_connection_close!(method)
646
+ end
647
+ when AMQ::Protocol::Connection::CloseOk then
648
+ @last_connection_close_ok = method
649
+ begin
650
+ @continuations.clear
651
+ rescue StandardError => e
652
+ @logger.error e.class.name
653
+ @logger.error e.message
654
+ @logger.error e.backtrace
655
+ ensure
656
+ @continuations.push(:__unblock__)
657
+ end
658
+ when AMQ::Protocol::Connection::Blocked then
659
+ @blocked = true
660
+ @block_callback.call(method) if @block_callback
661
+ when AMQ::Protocol::Connection::Unblocked then
662
+ @blocked = false
663
+ @unblock_callback.call(method) if @unblock_callback
664
+ when AMQ::Protocol::Connection::UpdateSecretOk then
665
+ @continuations.push(method)
666
+ when AMQ::Protocol::Channel::Close then
667
+ begin
668
+ ch = synchronised_find_channel(ch_number)
669
+ # this includes sending a channel.close-ok and
670
+ # potentially invoking a user-provided callback,
671
+ # avoid doing that while holding a mutex lock. MK.
672
+ ch.handle_method(method)
673
+ ensure
674
+ # synchronises on @channel_mutex under the hood
675
+ self.unregister_channel(ch)
676
+ end
677
+ when AMQ::Protocol::Basic::GetEmpty then
678
+ ch = find_channel(ch_number)
679
+ ch.handle_basic_get_empty(method)
680
+ else
681
+ if ch = find_channel(ch_number)
682
+ ch.handle_method(method)
683
+ else
684
+ @logger.warn "Channel #{ch_number} is not open on this connection!"
685
+ end
686
+ end
687
+ end
688
+
689
+ # @private
690
+ def raise_if_continuation_resulted_in_a_connection_error!
691
+ raise @last_connection_error if @last_connection_error
692
+ end
693
+
694
+ # @private
695
+ def handle_frameset(ch_number, frames)
696
+ method = frames.first
697
+
698
+ case method
699
+ when AMQ::Protocol::Basic::GetOk then
700
+ @channels[ch_number].handle_basic_get_ok(*frames)
701
+ when AMQ::Protocol::Basic::GetEmpty then
702
+ @channels[ch_number].handle_basic_get_empty(*frames)
703
+ when AMQ::Protocol::Basic::Return then
704
+ @channels[ch_number].handle_basic_return(*frames)
705
+ else
706
+ @channels[ch_number].handle_frameset(*frames)
707
+ end
708
+ end
709
+
710
+ # @private
711
+ def recover_from_connection_close?
712
+ @recover_from_connection_close
713
+ end
714
+
715
+ # @private
716
+ def handle_network_failure(exception)
717
+ raise NetworkErrorWrapper.new(exception) unless @threaded
718
+
719
+ @status_mutex.synchronize { @status = :disconnected }
720
+
721
+ if !recovering_from_network_failure?
722
+ begin
723
+ @recovering_from_network_failure = true
724
+ if recoverable_network_failure?(exception)
725
+ announce_network_failure_recovery
726
+ @channel_mutex.synchronize do
727
+ @channels.each do |n, ch|
728
+ ch.maybe_kill_consumer_work_pool!
729
+ end
730
+ end
731
+ @reader_loop.stop if @reader_loop
732
+ maybe_shutdown_heartbeat_sender
733
+
734
+ recover_from_network_failure
735
+ else
736
+ @logger.error "Exception #{exception.message} is considered unrecoverable..."
737
+ end
738
+ ensure
739
+ @recovering_from_network_failure = false
740
+ end
741
+ end
742
+ end
743
+
744
+ # @private
745
+ def recoverable_network_failure?(exception)
746
+ # No reasonably smart strategy was suggested in a few years.
747
+ # So just recover unconditionally. MK.
748
+ true
749
+ end
750
+
751
+ # @private
752
+ def recovering_from_network_failure?
753
+ @recovering_from_network_failure
754
+ end
755
+
756
+ # @private
757
+ def announce_network_failure_recovery
758
+ if recovery_attempts_limited?
759
+ @logger.warn "Will recover from a network failure (#{@recovery_attempts} out of #{@max_recovery_attempts} left)..."
760
+ else
761
+ @logger.warn "Will recover from a network failure (no retry limit)..."
762
+ end
763
+ end
764
+
765
+ # @private
766
+ def recover_from_network_failure
767
+ sleep @network_recovery_interval
768
+ @logger.debug "Will attempt connection recovery..."
769
+ notify_of_recovery_attempt_start
770
+
771
+ self.initialize_transport
772
+
773
+ @logger.warn "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
774
+ self.start
775
+
776
+ if open?
777
+
778
+ @recovering_from_network_failure = false
779
+ @logger.debug "Connection is now open"
780
+ if @reset_recovery_attempt_counter_after_reconnection
781
+ @logger.debug "Resetting recovery attempt counter after successful reconnection"
782
+ reset_recovery_attempt_counter!
783
+ else
784
+ @logger.debug "Not resetting recovery attempt counter after successful reconnection, as configured"
785
+ end
786
+
787
+ recover_channels
788
+ notify_of_recovery_completion
789
+ end
790
+ rescue HostListDepleted
791
+ reset_address_index
792
+ retry
793
+ rescue TCPConnectionFailedForAllHosts, TCPConnectionFailed, AMQ::Protocol::EmptyResponseError, SystemCallError, Timeout::Error => e
794
+ @logger.warn "TCP connection failed, reconnecting in #{@network_recovery_interval} seconds"
795
+ if should_retry_recovery?
796
+ decrement_recovery_attemp_counter!
797
+ if recoverable_network_failure?(e)
798
+ announce_network_failure_recovery
799
+ retry
800
+ end
801
+ else
802
+ @logger.error "Ran out of recovery attempts (limit set to #{@max_recovery_attempts}), giving up"
803
+ @transport.close
804
+ self.close(false)
805
+ @manually_closed = false
806
+ end
807
+ end
808
+
809
+ # @private
810
+ def recovery_attempts_limited?
811
+ !!@max_recovery_attempts
812
+ end
813
+
814
+ # @private
815
+ def should_retry_recovery?
816
+ !recovery_attempts_limited? || @recovery_attempts > 1
817
+ end
818
+
819
+ # @private
820
+ def decrement_recovery_attemp_counter!
821
+ if @recovery_attempts
822
+ @recovery_attempts -= 1
823
+ @logger.debug "#{@recovery_attempts} recovery attempts left"
824
+ end
825
+ @recovery_attempts
826
+ end
827
+
828
+ # @private
829
+ def reset_recovery_attempt_counter!
830
+ @recovery_attempts = @max_recovery_attempts
831
+ end
832
+
833
+ # @private
834
+ def recover_channels
835
+ @channel_mutex.synchronize do
836
+ @channels.each do |n, ch|
837
+ ch.open
838
+ ch.recover_from_network_failure
839
+ end
840
+ end
841
+ end
842
+
843
+ # @private
844
+ def notify_of_recovery_attempt_start
845
+ @recovery_attempt_started.call if @recovery_attempt_started
846
+ end
847
+
848
+ # @private
849
+ def notify_of_recovery_completion
850
+ @recovery_completed.call if @recovery_completed
851
+ end
852
+
853
+ # @private
854
+ def instantiate_connection_level_exception(frame)
855
+ case frame
856
+ when AMQ::Protocol::Connection::Close then
857
+ klass = case frame.reply_code
858
+ when 320 then
859
+ ConnectionForced
860
+ when 501 then
861
+ FrameError
862
+ when 503 then
863
+ CommandInvalid
864
+ when 504 then
865
+ ChannelError
866
+ when 505 then
867
+ UnexpectedFrame
868
+ when 506 then
869
+ ResourceError
870
+ when 530 then
871
+ NotAllowedError
872
+ when 541 then
873
+ InternalError
874
+ else
875
+ raise "Unknown reply code: #{frame.reply_code}, text: #{frame.reply_text}"
876
+ end
877
+
878
+ klass.new("Connection-level error: #{frame.reply_text}", self, frame)
879
+ end
880
+ end
881
+
882
+ def clean_up_and_fail_on_connection_close!(method)
883
+ @last_connection_error = instantiate_connection_level_exception(method)
884
+ @continuations.push(method)
885
+
886
+ clean_up_on_shutdown
887
+ if threaded?
888
+ @session_error_handler.raise(@last_connection_error)
889
+ else
890
+ raise @last_connection_error
891
+ end
892
+ end
893
+
894
+ def clean_up_on_shutdown
895
+ begin
896
+ shut_down_all_consumer_work_pools!
897
+ maybe_shutdown_reader_loop
898
+ maybe_shutdown_heartbeat_sender
899
+ rescue ShutdownSignal => _sse
900
+ # no-op
901
+ rescue Exception => e
902
+ @logger.warn "Caught an exception when cleaning up after receiving connection.close: #{e.message}"
903
+ ensure
904
+ close_transport
905
+ end
906
+ end
907
+
908
+ # @private
909
+ def addresses_from(options)
910
+ shuffle_strategy = options.fetch(:hosts_shuffle_strategy, @default_hosts_shuffle_strategy)
911
+
912
+ addresses = options[:host] || options[:hostname] || options[:addresses] ||
913
+ options[:hosts] || ["#{DEFAULT_HOST}:#{port_from(options)}"]
914
+ addresses = [addresses] unless addresses.is_a? Array
915
+
916
+ addrs = addresses.map do |address|
917
+ host_with_port?(address) ? address : "#{address}:#{port_from(@opts)}"
918
+ end
919
+
920
+ shuffle_strategy.call(addrs)
921
+ end
922
+
923
+ # @private
924
+ def port_from(options)
925
+ fallback = if options[:tls] || options[:ssl]
926
+ AMQ::Protocol::TLS_PORT
927
+ else
928
+ AMQ::Protocol::DEFAULT_PORT
929
+ end
930
+
931
+ options.fetch(:port, fallback)
932
+ end
933
+
934
+ # @private
935
+ def host_with_port?(address)
936
+ # we need to handle cases such as [2001:db8:85a3:8d3:1319:8a2e:370:7348]:5671
937
+ last_colon = address.rindex(":")
938
+ last_closing_square_bracket = address.rindex("]")
939
+
940
+ if last_closing_square_bracket.nil?
941
+ address.include?(":")
942
+ else
943
+ last_closing_square_bracket < last_colon
944
+ end
945
+ end
946
+
947
+ # @private
948
+ def host_from_address(address)
949
+ # we need to handle cases such as [2001:db8:85a3:8d3:1319:8a2e:370:7348]:5671
950
+ last_colon = address.rindex(":")
951
+ last_closing_square_bracket = address.rindex("]")
952
+
953
+ if last_closing_square_bracket.nil?
954
+ parts = address.split(":")
955
+ # this looks like an unquoted IPv6 address, so emit a warning
956
+ if parts.size > 2
957
+ @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]"
958
+ end
959
+ return parts[0]
960
+ end
961
+
962
+ if last_closing_square_bracket < last_colon
963
+ # there is a port
964
+ address[0, last_colon]
965
+ elsif last_closing_square_bracket > last_colon
966
+ address
967
+ end
968
+ end
969
+
970
+ # @private
971
+ def port_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[1].to_i
983
+ end
984
+
985
+ if last_closing_square_bracket < last_colon
986
+ # there is a port
987
+ address[(last_colon + 1)..-1].to_i
988
+ end
989
+ end
990
+
991
+ # @private
992
+ def vhost_from(options)
993
+ options[:virtual_host] || options[:vhost] || DEFAULT_VHOST
994
+ end
995
+
996
+ # @private
997
+ def username_from(options)
998
+ options[:username] || options[:user] || DEFAULT_USER
999
+ end
1000
+
1001
+ # @private
1002
+ def password_from(options)
1003
+ options[:password] || options[:pass] || options[:pwd] || DEFAULT_PASSWORD
1004
+ end
1005
+
1006
+ # @private
1007
+ def heartbeat_from(options)
1008
+ options[:heartbeat] || options[:heartbeat_timeout] || options[:requested_heartbeat] || options[:heartbeat_interval] || DEFAULT_HEARTBEAT
1009
+ end
1010
+
1011
+ # @private
1012
+ def next_channel_id
1013
+ @channel_id_allocator.next_channel_id
1014
+ end
1015
+
1016
+ # @private
1017
+ def release_channel_id(i)
1018
+ @channel_id_allocator.release_channel_id(i)
1019
+ end
1020
+
1021
+ # @private
1022
+ def register_channel(ch)
1023
+ @channel_mutex.synchronize do
1024
+ @channels[ch.number] = ch
1025
+ end
1026
+ end
1027
+
1028
+ # @private
1029
+ def unregister_channel(ch)
1030
+ @channel_mutex.synchronize do
1031
+ n = ch.number
1032
+
1033
+ self.release_channel_id(n)
1034
+ @channels.delete(ch.number)
1035
+ end
1036
+ end
1037
+
1038
+ # @private
1039
+ def start_reader_loop
1040
+ reader_loop.start
1041
+ end
1042
+
1043
+ # @private
1044
+ def reader_loop
1045
+ @reader_loop ||= ReaderLoop.new(@transport, self, @session_error_handler)
1046
+ end
1047
+
1048
+ # @private
1049
+ def maybe_shutdown_reader_loop
1050
+ if @reader_loop
1051
+ @reader_loop.stop
1052
+ if threaded?
1053
+ # this is the easiest way to wait until the loop
1054
+ # is guaranteed to have terminated
1055
+ @reader_loop.terminate_with(ShutdownSignal)
1056
+ # joining the thread here may take forever
1057
+ # on JRuby because sun.nio.ch.KQueueArrayWrapper#kevent0 is
1058
+ # a native method that cannot be (easily) interrupted.
1059
+ # So we use this ugly hack or else our test suite takes forever
1060
+ # to run on JRuby (a new connection is opened/closed per example). MK.
1061
+ if defined?(JRUBY_VERSION)
1062
+ sleep 0.075
1063
+ else
1064
+ @reader_loop.join
1065
+ end
1066
+ else
1067
+ # single threaded mode, nothing to do. MK.
1068
+ end
1069
+ end
1070
+
1071
+ @reader_loop = nil
1072
+ end
1073
+
1074
+ # @private
1075
+ def close_transport
1076
+ begin
1077
+ @transport.close
1078
+ rescue StandardError => e
1079
+ @logger.error "Exception when closing transport:"
1080
+ @logger.error e.class.name
1081
+ @logger.error e.message
1082
+ @logger.error e.backtrace
1083
+ end
1084
+ end
1085
+
1086
+ # @private
1087
+ def signal_activity!
1088
+ @heartbeat_sender.signal_activity! if @heartbeat_sender
1089
+ end
1090
+
1091
+
1092
+ # Sends frame to the peer, checking that connection is open.
1093
+ # Exposed primarily for Bunny::Channel
1094
+ #
1095
+ # @raise [ConnectionClosedError]
1096
+ # @private
1097
+ def send_frame(frame, signal_activity = true)
1098
+ if open?
1099
+ # @transport_mutex.synchronize do
1100
+ # @transport.write(frame.encode)
1101
+ # end
1102
+ @transport.write(frame.encode)
1103
+ signal_activity! if signal_activity
1104
+ else
1105
+ raise ConnectionClosedError.new(frame)
1106
+ end
1107
+ end
1108
+
1109
+ # Sends frame to the peer, checking that connection is open.
1110
+ # Uses transport implementation that does not perform
1111
+ # timeout control. Exposed primarily for Bunny::Channel.
1112
+ #
1113
+ # @raise [ConnectionClosedError]
1114
+ # @private
1115
+ def send_frame_without_timeout(frame, signal_activity = true)
1116
+ if open?
1117
+ @transport.write_without_timeout(frame.encode)
1118
+ signal_activity! if signal_activity
1119
+ else
1120
+ raise ConnectionClosedError.new(frame)
1121
+ end
1122
+ end
1123
+
1124
+ # Sends multiple frames, in one go. For thread safety this method takes a channel
1125
+ # object and synchronizes on it.
1126
+ #
1127
+ # @private
1128
+ def send_frameset(frames, channel)
1129
+ # some developers end up sharing channels between threads and when multiple
1130
+ # threads publish on the same channel aggressively, at some point frames will be
1131
+ # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
1132
+ # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
1133
+ # locking. Note that "single frame" methods technically do not need this kind of synchronization
1134
+ # (no incorrect frame interleaving of the same kind as with basic.publish isn't possible) but we
1135
+ # still recommend not sharing channels between threads except for consumer-only cases in the docs. MK.
1136
+ channel.synchronize do
1137
+ # see rabbitmq/rabbitmq-server#156
1138
+ if open?
1139
+ data = frames.reduce("") { |acc, frame| acc << frame.encode }
1140
+ @transport.write(data)
1141
+ signal_activity!
1142
+ else
1143
+ raise ConnectionClosedError.new(frames)
1144
+ end
1145
+ end
1146
+ end # send_frameset(frames)
1147
+
1148
+ # Sends multiple frames, one by one. For thread safety this method takes a channel
1149
+ # object and synchronizes on it. Uses transport implementation that does not perform
1150
+ # timeout control.
1151
+ #
1152
+ # @private
1153
+ def send_frameset_without_timeout(frames, channel)
1154
+ # some developers end up sharing channels between threads and when multiple
1155
+ # threads publish on the same channel aggressively, at some point frames will be
1156
+ # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
1157
+ # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
1158
+ # locking. See a note about "single frame" methods in a comment in `send_frameset`. MK.
1159
+ channel.synchronize do
1160
+ if open?
1161
+ frames.each { |frame| self.send_frame_without_timeout(frame, false) }
1162
+ signal_activity!
1163
+ else
1164
+ raise ConnectionClosedError.new(frames)
1165
+ end
1166
+ end
1167
+ end # send_frameset_without_timeout(frames)
1168
+
1169
+ # @private
1170
+ def send_raw_without_timeout(data, channel)
1171
+ # some developers end up sharing channels between threads and when multiple
1172
+ # threads publish on the same channel aggressively, at some point frames will be
1173
+ # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
1174
+ # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
1175
+ # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
1176
+ channel.synchronize do
1177
+ @transport.write(data)
1178
+ signal_activity!
1179
+ end
1180
+ end # send_frameset_without_timeout(frames)
1181
+
1182
+ # @return [String]
1183
+ # @api public
1184
+ def to_s
1185
+ oid = ("0x%x" % (self.object_id << 1))
1186
+ "#<#{self.class.name}:#{oid} #{@user}@#{host}:#{port}, vhost=#{@vhost}, addresses=[#{@addresses.join(',')}]>"
1187
+ end
1188
+
1189
+ def inspect
1190
+ to_s
1191
+ end
1192
+
1193
+ protected
1194
+
1195
+ # @private
1196
+ def init_connection
1197
+ self.send_preamble
1198
+
1199
+ connection_start = @transport.read_next_frame.decode_payload
1200
+
1201
+ @server_properties = connection_start.server_properties
1202
+ @server_capabilities = @server_properties["capabilities"]
1203
+
1204
+ @server_authentication_mechanisms = (connection_start.mechanisms || "").split(" ")
1205
+ @server_locales = Array(connection_start.locales)
1206
+
1207
+ @status_mutex.synchronize { @status = :connected }
1208
+ end
1209
+
1210
+ # @private
1211
+ def open_connection
1212
+ @transport.send_frame(AMQ::Protocol::Connection::StartOk.encode(@client_properties, @mechanism, self.encode_credentials(username, password), @locale))
1213
+ @logger.debug "Sent connection.start-ok"
1214
+
1215
+ frame = begin
1216
+ fr = @transport.read_next_frame
1217
+ while fr.is_a?(AMQ::Protocol::HeartbeatFrame)
1218
+ fr = @transport.read_next_frame
1219
+ end
1220
+ fr
1221
+ # frame timeout means the broker has closed the TCP connection, which it
1222
+ # does per 0.9.1 spec.
1223
+ rescue
1224
+ nil
1225
+ end
1226
+ if frame.nil?
1227
+ raise TCPConnectionFailed.new('An empty frame was received while opening the connection. In RabbitMQ <= 3.1 this could mean an authentication issue.')
1228
+ end
1229
+
1230
+ response = frame.decode_payload
1231
+ if response.is_a?(AMQ::Protocol::Connection::Close)
1232
+ @state = :closed
1233
+ @logger.error "Authentication with RabbitMQ failed: #{response.reply_code} #{response.reply_text}"
1234
+ raise Bunny::AuthenticationFailureError.new(self.user, self.vhost, self.password.size)
1235
+ end
1236
+
1237
+
1238
+
1239
+ connection_tune = response
1240
+
1241
+ @frame_max = negotiate_value(@client_frame_max, connection_tune.frame_max)
1242
+ @channel_max = negotiate_value(@client_channel_max, connection_tune.channel_max)
1243
+ # this allows for disabled heartbeats. MK.
1244
+ @heartbeat = if heartbeat_disabled?(@client_heartbeat)
1245
+ 0
1246
+ else
1247
+ negotiate_value(@client_heartbeat, connection_tune.heartbeat)
1248
+ end
1249
+ @logger.debug { "Heartbeat interval negotiation: client = #{@client_heartbeat}, server = #{connection_tune.heartbeat}, result = #{@heartbeat}" }
1250
+ @logger.info "Heartbeat interval used (in seconds): #{@heartbeat}"
1251
+
1252
+ # We set the read_write_timeout to twice the heartbeat value,
1253
+ # and then some padding for edge cases.
1254
+ # This allows us to miss a single heartbeat before we time out the socket.
1255
+ # If heartbeats are disabled, assume that TCP keepalives or a similar mechanism will be used
1256
+ # and disable socket read timeouts. See ruby-amqp/bunny#551.
1257
+ @transport.read_timeout = @heartbeat * 2.2
1258
+ @logger.debug { "Will use socket read timeout of #{@transport.read_timeout.to_i} seconds" }
1259
+
1260
+ # if there are existing channels we've just recovered from
1261
+ # a network failure and need to fix the allocated set. See issue 205. MK.
1262
+ if @channels.empty?
1263
+ @logger.debug { "Initializing channel ID allocator with channel_max = #{@channel_max}" }
1264
+ @channel_id_allocator = ChannelIdAllocator.new(@channel_max)
1265
+ end
1266
+
1267
+ @transport.send_frame(AMQ::Protocol::Connection::TuneOk.encode(@channel_max, @frame_max, @heartbeat))
1268
+ @logger.debug { "Sent connection.tune-ok with heartbeat interval = #{@heartbeat}, frame_max = #{@frame_max}, channel_max = #{@channel_max}" }
1269
+ @transport.send_frame(AMQ::Protocol::Connection::Open.encode(self.vhost))
1270
+ @logger.debug { "Sent connection.open with vhost = #{self.vhost}" }
1271
+
1272
+ frame2 = begin
1273
+ fr = @transport.read_next_frame
1274
+ while fr.is_a?(AMQ::Protocol::HeartbeatFrame)
1275
+ fr = @transport.read_next_frame
1276
+ end
1277
+ fr
1278
+ # frame timeout means the broker has closed the TCP connection, which it
1279
+ # does per 0.9.1 spec.
1280
+ rescue
1281
+ nil
1282
+ end
1283
+ if frame2.nil?
1284
+ raise TCPConnectionFailed.new('An empty frame was received while opening the connection. In RabbitMQ <= 3.1 this could mean an authentication issue.')
1285
+ end
1286
+ connection_open_ok = frame2.decode_payload
1287
+
1288
+ @status_mutex.synchronize { @status = :open }
1289
+ if @heartbeat && @heartbeat > 0
1290
+ initialize_heartbeat_sender
1291
+ end
1292
+
1293
+ unless connection_open_ok.is_a?(AMQ::Protocol::Connection::OpenOk)
1294
+ if connection_open_ok.is_a?(AMQ::Protocol::Connection::Close)
1295
+ e = instantiate_connection_level_exception(connection_open_ok)
1296
+ begin
1297
+ shut_down_all_consumer_work_pools!
1298
+ maybe_shutdown_reader_loop
1299
+ rescue ShutdownSignal => _sse
1300
+ # no-op
1301
+ rescue Exception => e
1302
+ @logger.warn "Caught an exception when cleaning up after receiving connection.close: #{e.message}"
1303
+ ensure
1304
+ close_transport
1305
+ end
1306
+
1307
+ if threaded?
1308
+ @session_error_handler.raise(e)
1309
+ else
1310
+ raise e
1311
+ end
1312
+ else
1313
+ raise "could not open connection: server did not respond with connection.open-ok but #{connection_open_ok.inspect} instead"
1314
+ end
1315
+ end
1316
+ end
1317
+
1318
+ def heartbeat_disabled?(val)
1319
+ 0 == val || val.nil?
1320
+ end
1321
+
1322
+ # @private
1323
+ def negotiate_value(client_value, server_value)
1324
+ return server_value if [:server, "server"].include?(client_value)
1325
+
1326
+ if client_value == 0 || server_value == 0
1327
+ [client_value, server_value].max
1328
+ else
1329
+ [client_value, server_value].min
1330
+ end
1331
+ end
1332
+
1333
+ # @private
1334
+ def initialize_heartbeat_sender
1335
+ maybe_shutdown_heartbeat_sender
1336
+ @logger.debug "Initializing heartbeat sender..."
1337
+ @heartbeat_sender = HeartbeatSender.new(@transport, @logger)
1338
+ @heartbeat_sender.start(@heartbeat)
1339
+ end
1340
+
1341
+ # @private
1342
+ def maybe_shutdown_heartbeat_sender
1343
+ @heartbeat_sender.stop if @heartbeat_sender
1344
+ end
1345
+
1346
+ # @private
1347
+ def initialize_transport
1348
+ if address = @addresses[ @address_index ]
1349
+ @address_index_mutex.synchronize { @address_index += 1 }
1350
+ @transport.close rescue nil # Let's make sure the previous transport socket is closed
1351
+ @transport = Transport.new(self,
1352
+ host_from_address(address),
1353
+ port_from_address(address),
1354
+ @opts.merge(:session_error_handler => @session_error_handler)
1355
+ )
1356
+
1357
+ # Reset the cached progname for the logger only when no logger was provided
1358
+ @default_logger.progname = self.to_s
1359
+
1360
+ @transport
1361
+ else
1362
+ raise HostListDepleted
1363
+ end
1364
+ end
1365
+
1366
+ # @private
1367
+ def maybe_close_transport
1368
+ @transport.close if @transport
1369
+ end
1370
+
1371
+ # Sends AMQ protocol header (also known as preamble).
1372
+ # @private
1373
+ def send_preamble
1374
+ @transport.write(AMQ::Protocol::PREAMBLE)
1375
+ @logger.debug "Sent protocol preamble"
1376
+ end
1377
+
1378
+
1379
+ # @private
1380
+ def encode_credentials(username, password)
1381
+ @credentials_encoder.encode_credentials(username, password)
1382
+ end # encode_credentials(username, password)
1383
+
1384
+ # @private
1385
+ def credentials_encoder_for(mechanism)
1386
+ Authentication::CredentialsEncoder.for_session(self)
1387
+ end
1388
+
1389
+ if defined?(JRUBY_VERSION)
1390
+ # @private
1391
+ def reset_continuations
1392
+ @continuations = Concurrent::LinkedContinuationQueue.new
1393
+ end
1394
+ else
1395
+ # @private
1396
+ def reset_continuations
1397
+ @continuations = Concurrent::ContinuationQueue.new
1398
+ end
1399
+ end
1400
+
1401
+ # @private
1402
+ def wait_on_continuations
1403
+ unless @threaded
1404
+ reader_loop.run_once until @continuations.length > 0
1405
+ end
1406
+
1407
+ @continuations.poll(@continuation_timeout)
1408
+ end
1409
+
1410
+ # @private
1411
+ def init_default_logger(logfile, level)
1412
+ @default_logger = begin
1413
+ lgr = ::Logger.new(logfile)
1414
+ lgr.level = normalize_log_level(level)
1415
+ lgr.progname = self.to_s
1416
+ lgr
1417
+ end
1418
+ end
1419
+
1420
+ # @private
1421
+ def init_default_logger_without_progname(logfile, level)
1422
+ @default_logger = begin
1423
+ lgr = ::Logger.new(logfile)
1424
+ lgr.level = normalize_log_level(level)
1425
+ lgr
1426
+ end
1427
+ end
1428
+
1429
+ # @private
1430
+ def normalize_log_level(level)
1431
+ case level
1432
+ when :debug, Logger::DEBUG, "debug" then Logger::DEBUG
1433
+ when :info, Logger::INFO, "info" then Logger::INFO
1434
+ when :warn, Logger::WARN, "warn" then Logger::WARN
1435
+ when :error, Logger::ERROR, "error" then Logger::ERROR
1436
+ when :fatal, Logger::FATAL, "fatal" then Logger::FATAL
1437
+ else
1438
+ Logger::WARN
1439
+ end
1440
+ end
1441
+
1442
+ # @private
1443
+ def shut_down_all_consumer_work_pools!
1444
+ @channels.each do |_, ch|
1445
+ ch.maybe_kill_consumer_work_pool!
1446
+ end
1447
+ end
1448
+
1449
+ def normalize_client_channel_max(n)
1450
+ return CHANNEL_MAX_LIMIT if n.nil?
1451
+ return CHANNEL_MAX_LIMIT if n > CHANNEL_MAX_LIMIT
1452
+
1453
+ case n
1454
+ when 0 then
1455
+ CHANNEL_MAX_LIMIT
1456
+ else
1457
+ n
1458
+ end
1459
+ end
1460
+
1461
+ def normalize_auth_mechanism(value)
1462
+ case value
1463
+ when [] then
1464
+ "PLAIN"
1465
+ when nil then
1466
+ "PLAIN"
1467
+ else
1468
+ value
1469
+ end
1470
+ end
1471
+
1472
+ def ignoring_io_errors(&block)
1473
+ begin
1474
+ block.call
1475
+ rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError, Bunny::NetworkFailure => _
1476
+ # ignore
1477
+ end
1478
+ end
1479
+ end # Session
1480
+
1481
+ # backwards compatibility
1482
+ Client = Session
1483
+ end