garaio_bunny 2.19.1

Sign up to get free protection for your applications and to get access to all the features.
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