bunny 2.7.4 → 2.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +61 -35
  3. data/lib/bunny/channel.rb +186 -50
  4. data/lib/bunny/channel_id_allocator.rb +3 -1
  5. data/lib/bunny/consumer.rb +2 -2
  6. data/lib/bunny/consumer_work_pool.rb +2 -1
  7. data/lib/bunny/cruby/socket.rb +3 -0
  8. data/lib/bunny/cruby/ssl_socket.rb +6 -1
  9. data/lib/bunny/delivery_info.rb +1 -1
  10. data/lib/bunny/heartbeat_sender.rb +2 -1
  11. data/lib/bunny/jruby/ssl_socket.rb +5 -0
  12. data/lib/bunny/queue.rb +36 -8
  13. data/lib/bunny/reader_loop.rb +22 -10
  14. data/lib/bunny/session.rb +152 -65
  15. data/lib/bunny/test_kit.rb +14 -0
  16. data/lib/bunny/transport.rb +132 -49
  17. data/lib/bunny/version.rb +1 -1
  18. data/lib/bunny.rb +45 -4
  19. metadata +37 -225
  20. data/.github/ISSUE_TEMPLATE.md +0 -18
  21. data/.gitignore +0 -28
  22. data/.rspec +0 -1
  23. data/.travis.yml +0 -20
  24. data/.yardopts +0 -8
  25. data/CONTRIBUTING.md +0 -111
  26. data/ChangeLog.md +0 -1831
  27. data/Gemfile +0 -53
  28. data/LICENSE +0 -21
  29. data/Rakefile +0 -46
  30. data/benchmarks/basic_publish/with_128K_messages.rb +0 -35
  31. data/benchmarks/basic_publish/with_1k_messages.rb +0 -35
  32. data/benchmarks/basic_publish/with_4K_messages.rb +0 -35
  33. data/benchmarks/basic_publish/with_64K_messages.rb +0 -35
  34. data/benchmarks/channel_open.rb +0 -28
  35. data/benchmarks/mutex_and_monitor.rb +0 -42
  36. data/benchmarks/queue_declare.rb +0 -29
  37. data/benchmarks/queue_declare_and_bind.rb +0 -29
  38. data/benchmarks/queue_declare_bind_and_delete.rb +0 -29
  39. data/benchmarks/synchronized_sorted_set.rb +0 -53
  40. data/benchmarks/write_vs_write_nonblock.rb +0 -49
  41. data/bin/ci/before_build +0 -46
  42. data/bunny.gemspec +0 -35
  43. data/docker/Dockerfile +0 -16
  44. data/docker/docker-entrypoint.sh +0 -37
  45. data/docker-compose.yml +0 -18
  46. data/examples/connection/authentication_failure.rb +0 -16
  47. data/examples/connection/automatic_recovery_with_basic_get.rb +0 -40
  48. data/examples/connection/automatic_recovery_with_client_named_queues.rb +0 -36
  49. data/examples/connection/automatic_recovery_with_multiple_consumers.rb +0 -46
  50. data/examples/connection/automatic_recovery_with_republishing.rb +0 -109
  51. data/examples/connection/automatic_recovery_with_server_named_queues.rb +0 -35
  52. data/examples/connection/channel_level_exception.rb +0 -27
  53. data/examples/connection/disabled_automatic_recovery.rb +0 -34
  54. data/examples/connection/heartbeat.rb +0 -17
  55. data/examples/connection/manually_reconnecting_consumer.rb +0 -23
  56. data/examples/connection/manually_reconnecting_publisher.rb +0 -28
  57. data/examples/connection/unknown_host.rb +0 -16
  58. data/examples/consumers/high_and_low_priority.rb +0 -50
  59. data/examples/guides/exchanges/direct_exchange_routing.rb +0 -36
  60. data/examples/guides/exchanges/fanout_exchange_routing.rb +0 -28
  61. data/examples/guides/exchanges/headers_exchange_routing.rb +0 -31
  62. data/examples/guides/exchanges/mandatory_messages.rb +0 -30
  63. data/examples/guides/extensions/alternate_exchange.rb +0 -30
  64. data/examples/guides/extensions/basic_nack.rb +0 -33
  65. data/examples/guides/extensions/connection_blocked.rb +0 -35
  66. data/examples/guides/extensions/consumer_cancellation_notification.rb +0 -39
  67. data/examples/guides/extensions/dead_letter_exchange.rb +0 -32
  68. data/examples/guides/extensions/exchange_to_exchange_bindings.rb +0 -29
  69. data/examples/guides/extensions/per_message_ttl.rb +0 -36
  70. data/examples/guides/extensions/per_queue_message_ttl.rb +0 -36
  71. data/examples/guides/extensions/publisher_confirms.rb +0 -28
  72. data/examples/guides/extensions/queue_lease.rb +0 -26
  73. data/examples/guides/extensions/sender_selected_distribution.rb +0 -32
  74. data/examples/guides/getting_started/blabbr.rb +0 -27
  75. data/examples/guides/getting_started/hello_world.rb +0 -22
  76. data/examples/guides/getting_started/weathr.rb +0 -49
  77. data/examples/guides/queues/one_off_consumer.rb +0 -25
  78. data/examples/guides/queues/redeliveries.rb +0 -81
  79. data/profiling/basic_publish/with_4K_messages.rb +0 -33
  80. data/repl +0 -3
  81. data/spec/config/enabled_plugins +0 -1
  82. data/spec/config/rabbitmq.config +0 -19
  83. data/spec/higher_level_api/integration/basic_ack_spec.rb +0 -230
  84. data/spec/higher_level_api/integration/basic_cancel_spec.rb +0 -142
  85. data/spec/higher_level_api/integration/basic_consume_spec.rb +0 -349
  86. data/spec/higher_level_api/integration/basic_consume_with_objects_spec.rb +0 -54
  87. data/spec/higher_level_api/integration/basic_get_spec.rb +0 -80
  88. data/spec/higher_level_api/integration/basic_nack_spec.rb +0 -82
  89. data/spec/higher_level_api/integration/basic_publish_spec.rb +0 -74
  90. data/spec/higher_level_api/integration/basic_qos_spec.rb +0 -57
  91. data/spec/higher_level_api/integration/basic_reject_spec.rb +0 -152
  92. data/spec/higher_level_api/integration/basic_return_spec.rb +0 -33
  93. data/spec/higher_level_api/integration/channel_close_spec.rb +0 -25
  94. data/spec/higher_level_api/integration/channel_open_spec.rb +0 -57
  95. data/spec/higher_level_api/integration/connection_recovery_spec.rb +0 -471
  96. data/spec/higher_level_api/integration/connection_spec.rb +0 -559
  97. data/spec/higher_level_api/integration/connection_stop_spec.rb +0 -83
  98. data/spec/higher_level_api/integration/consumer_cancellation_notification_spec.rb +0 -128
  99. data/spec/higher_level_api/integration/dead_lettering_spec.rb +0 -75
  100. data/spec/higher_level_api/integration/exchange_bind_spec.rb +0 -31
  101. data/spec/higher_level_api/integration/exchange_declare_spec.rb +0 -237
  102. data/spec/higher_level_api/integration/exchange_delete_spec.rb +0 -105
  103. data/spec/higher_level_api/integration/exchange_unbind_spec.rb +0 -40
  104. data/spec/higher_level_api/integration/exclusive_queue_spec.rb +0 -28
  105. data/spec/higher_level_api/integration/heartbeat_spec.rb +0 -49
  106. data/spec/higher_level_api/integration/merry_go_round_spec.rb +0 -85
  107. data/spec/higher_level_api/integration/message_properties_access_spec.rb +0 -95
  108. data/spec/higher_level_api/integration/predeclared_exchanges_spec.rb +0 -24
  109. data/spec/higher_level_api/integration/publisher_confirms_spec.rb +0 -191
  110. data/spec/higher_level_api/integration/publishing_edge_cases_spec.rb +0 -87
  111. data/spec/higher_level_api/integration/queue_bind_spec.rb +0 -109
  112. data/spec/higher_level_api/integration/queue_declare_spec.rb +0 -221
  113. data/spec/higher_level_api/integration/queue_delete_spec.rb +0 -41
  114. data/spec/higher_level_api/integration/queue_purge_spec.rb +0 -30
  115. data/spec/higher_level_api/integration/queue_unbind_spec.rb +0 -54
  116. data/spec/higher_level_api/integration/read_only_consumer_spec.rb +0 -60
  117. data/spec/higher_level_api/integration/sender_selected_distribution_spec.rb +0 -36
  118. data/spec/higher_level_api/integration/tls_connection_spec.rb +0 -222
  119. data/spec/higher_level_api/integration/tx_commit_spec.rb +0 -21
  120. data/spec/higher_level_api/integration/tx_rollback_spec.rb +0 -21
  121. data/spec/higher_level_api/integration/with_channel_spec.rb +0 -25
  122. data/spec/issues/issue100_spec.rb +0 -42
  123. data/spec/issues/issue141_spec.rb +0 -43
  124. data/spec/issues/issue202_spec.rb +0 -15
  125. data/spec/issues/issue224_spec.rb +0 -40
  126. data/spec/issues/issue465_spec.rb +0 -32
  127. data/spec/issues/issue78_spec.rb +0 -72
  128. data/spec/issues/issue83_spec.rb +0 -30
  129. data/spec/issues/issue97_attachment.json +0 -1
  130. data/spec/issues/issue97_spec.rb +0 -175
  131. data/spec/lower_level_api/integration/basic_cancel_spec.rb +0 -83
  132. data/spec/lower_level_api/integration/basic_consume_spec.rb +0 -99
  133. data/spec/spec_helper.rb +0 -51
  134. data/spec/stress/channel_close_stress_spec.rb +0 -64
  135. data/spec/stress/channel_open_stress_spec.rb +0 -84
  136. data/spec/stress/channel_open_stress_with_single_threaded_connection_spec.rb +0 -28
  137. data/spec/stress/concurrent_consumers_stress_spec.rb +0 -71
  138. data/spec/stress/concurrent_publishers_stress_spec.rb +0 -54
  139. data/spec/stress/connection_open_close_spec.rb +0 -52
  140. data/spec/stress/long_running_consumer_spec.rb +0 -84
  141. data/spec/tls/ca_certificate.pem +0 -29
  142. data/spec/tls/ca_key.pem +0 -52
  143. data/spec/tls/client_certificate.pem +0 -29
  144. data/spec/tls/client_key.pem +0 -51
  145. data/spec/tls/generate-server-cert.sh +0 -8
  146. data/spec/tls/server-openssl.cnf +0 -10
  147. data/spec/tls/server.csr +0 -16
  148. data/spec/tls/server_certificate.pem +0 -29
  149. data/spec/tls/server_key.pem +0 -51
  150. data/spec/unit/bunny_spec.rb +0 -15
  151. data/spec/unit/concurrent/atomic_fixnum_spec.rb +0 -35
  152. data/spec/unit/concurrent/condition_spec.rb +0 -82
  153. data/spec/unit/concurrent/linked_continuation_queue_spec.rb +0 -35
  154. data/spec/unit/concurrent/synchronized_sorted_set_spec.rb +0 -73
  155. data/spec/unit/exchange_recovery_spec.rb +0 -39
  156. data/spec/unit/version_delivery_tag_spec.rb +0 -28
data/lib/bunny/queue.rb CHANGED
@@ -11,6 +11,23 @@ module Bunny
11
11
  # API
12
12
  #
13
13
 
14
+ module Types
15
+ QUORUM = "quorum"
16
+ CLASSIC = "classic"
17
+ STREAM = "stream"
18
+
19
+ KNOWN = [CLASSIC, QUORUM, STREAM]
20
+
21
+ def self.known?(q_type)
22
+ KNOWN.include?(q_type)
23
+ end
24
+ end
25
+
26
+ module XArgs
27
+ MAX_LENGTH = "x-max-length",
28
+ QUEUE_TYPE = "x-queue-type"
29
+ end
30
+
14
31
  # @return [Bunny::Channel] Channel this queue uses
15
32
  attr_reader :channel
16
33
  # @return [String] Queue name
@@ -25,7 +42,8 @@ module Bunny
25
42
  # @option opts [Boolean] :durable (false) Should this queue be durable?
26
43
  # @option opts [Boolean] :auto_delete (false) Should this queue be automatically deleted when the last consumer disconnects?
27
44
  # @option opts [Boolean] :exclusive (false) Should this queue be exclusive (only can be used by this connection, removed when the connection is closed)?
28
- # @option opts [Boolean] :arguments ({}) Additional optional arguments (typically used by RabbitMQ extensions and plugins)
45
+ # @option opts [String] :type (nil) Type of the declared queue (classic, quorum or stream)
46
+ # @option opts [Hash] :arguments (nil) Additional optional arguments (typically used by RabbitMQ extensions and plugins)
29
47
  #
30
48
  # @see Bunny::Channel#queue
31
49
  # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
@@ -42,7 +60,17 @@ module Bunny
42
60
  @exclusive = @options[:exclusive]
43
61
  @server_named = @name.empty?
44
62
  @auto_delete = @options[:auto_delete]
45
- @arguments = @options[:arguments]
63
+ @type = @options[:type]
64
+
65
+ @arguments = if @type and !@type.empty? then
66
+ (@options[:arguments] || {}).merge({XArgs::QUEUE_TYPE => @type})
67
+ else
68
+ @options[:arguments]
69
+ end
70
+ verify_type!(@arguments)
71
+ # reassigns updated and verified arguments because Bunny::Channel#declare_queue
72
+ # accepts a map of options
73
+ @options[:arguments] = @arguments
46
74
 
47
75
  @bindings = Array.new
48
76
 
@@ -158,7 +186,6 @@ module Bunny
158
186
  # @option opts [Boolean] :ack (false) [DEPRECATED] Use :manual_ack instead
159
187
  # @option opts [Boolean] :manual_ack (false) Will this consumer use manual acknowledgements?
160
188
  # @option opts [Boolean] :exclusive (false) Should this consumer be exclusive for this queue?
161
- # @option opts [Boolean] :block (false) Should the call block calling thread?
162
189
  # @option opts [#call] :on_cancellation Block to execute when this consumer is cancelled remotely (e.g. via the RabbitMQ Management plugin)
163
190
  # @option opts [String] :consumer_tag Unique consumer identifier. It is usually recommended to let Bunny generate it for you.
164
191
  # @option opts [Hash] :arguments ({}) Additional (optional) arguments, typically used by RabbitMQ extensions
@@ -373,11 +400,6 @@ module Bunny
373
400
 
374
401
  protected
375
402
 
376
- # @private
377
- def self.add_default_options(name, opts, block)
378
- { :queue => name, :nowait => (block.nil? && !name.empty?) }.merge(opts)
379
- end
380
-
381
403
  # @private
382
404
  def self.add_default_options(name, opts)
383
405
  # :nowait is always false for Bunny
@@ -395,5 +417,11 @@ module Bunny
395
417
  h
396
418
  end
397
419
  end
420
+
421
+ def verify_type!(args)
422
+ q_type = (args || {})["x-queue-type"]
423
+ throw ArgumentError.new(
424
+ "unsupported queue type #{q_type.inspect}, supported ones: #{Types::KNOWN.join(', ')}") if (q_type and !Types.known?(q_type))
425
+ end
398
426
  end
399
427
  end
@@ -9,13 +9,17 @@ module Bunny
9
9
  # @private
10
10
  class ReaderLoop
11
11
 
12
- def initialize(transport, session, session_thread)
13
- @transport = transport
14
- @session = session
15
- @session_thread = session_thread
16
- @logger = @session.logger
12
+ def initialize(transport, session, session_error_handler)
13
+ @transport = transport
14
+ @session = session
15
+ @session_error_handler = session_error_handler
16
+ @logger = @session.logger
17
17
 
18
- @mutex = Mutex.new
18
+ @mutex = Mutex.new
19
+
20
+ @stopping = false
21
+ @stopped = false
22
+ @network_is_down = false
19
23
  end
20
24
 
21
25
 
@@ -33,7 +37,8 @@ module Bunny
33
37
  begin
34
38
  break if @mutex.synchronize { @stopping || @stopped || @network_is_down }
35
39
  run_once
36
- rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError, Timeout::Error => e
40
+ rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError, Timeout::Error,
41
+ OpenSSL::OpenSSLError => e
37
42
  break if terminate? || @session.closing? || @session.closed?
38
43
 
39
44
  @network_is_down = true
@@ -42,7 +47,7 @@ module Bunny
42
47
  @session.handle_network_failure(e)
43
48
  else
44
49
  log_exception(e)
45
- @session_thread.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
50
+ @session_error_handler.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
46
51
  end
47
52
  rescue ShutdownSignal => _
48
53
  @mutex.synchronize { @stopping = true }
@@ -53,7 +58,7 @@ module Bunny
53
58
  log_exception(e)
54
59
 
55
60
  @network_is_down = true
56
- @session_thread.raise(Bunny::NetworkFailure.new("caught an unexpected exception in the network loop: #{e.message}", e))
61
+ @session_error_handler.raise(Bunny::NetworkFailure.new("caught an unexpected exception in the network loop: #{e.message}", e))
57
62
  end
58
63
  rescue Errno::EBADF => _ebadf
59
64
  break if terminate?
@@ -111,7 +116,14 @@ module Bunny
111
116
  end
112
117
 
113
118
  def join
114
- @thread.join if @thread
119
+ # Thread#join can/would trigger a re-raise of an unhandled exception in this thread.
120
+ # In addition, Thread.handle_interrupt can be used by other libraries or application code
121
+ # that would make this join operation fail with an obscure exception.
122
+ # So we try to save everyone some really unpleasant debugging time by introducing
123
+ # this condition which typically would not evaluate to true anyway.
124
+ #
125
+ # See ruby-amqp/bunny#589 and ruby-amqp/bunny#590 for background.
126
+ @thread.join if @thread && @thread != Thread.current
115
127
  end
116
128
 
117
129
  def kill
data/lib/bunny/session.rb CHANGED
@@ -36,10 +36,10 @@ module Bunny
36
36
  DEFAULT_HEARTBEAT = :server
37
37
  # @private
38
38
  DEFAULT_FRAME_MAX = 131072
39
- # 2^16 - 1, maximum representable signed 16 bit integer.
39
+ # Hard limit the user cannot go over regardless of server configuration.
40
40
  # @private
41
41
  CHANNEL_MAX_LIMIT = 65535
42
- DEFAULT_CHANNEL_MAX = CHANNEL_MAX_LIMIT
42
+ DEFAULT_CHANNEL_MAX = 2047
43
43
 
44
44
  # backwards compatibility
45
45
  # @private
@@ -71,6 +71,7 @@ module Bunny
71
71
  # Default reconnection interval for TCP connection failures
72
72
  DEFAULT_NETWORK_RECOVERY_INTERVAL = 5.0
73
73
 
74
+ DEFAULT_RECOVERABLE_EXCEPTIONS = [StandardError, TCPConnectionFailedForAllHosts, TCPConnectionFailed, AMQ::Protocol::EmptyResponseError, SystemCallError, Timeout::Error, Bunny::ConnectionLevelException, Bunny::ConnectionClosedError]
74
75
 
75
76
  #
76
77
  # API
@@ -78,7 +79,7 @@ module Bunny
78
79
 
79
80
  # @return [Bunny::Transport]
80
81
  attr_reader :transport
81
- attr_reader :status, :port, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
82
+ attr_reader :status, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
82
83
  attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
83
84
  attr_reader :channel_id_allocator
84
85
  # Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
@@ -89,7 +90,9 @@ module Bunny
89
90
  # @return [Integer] Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 15000.
90
91
  attr_reader :continuation_timeout
91
92
  attr_reader :network_recovery_interval
93
+ attr_reader :connection_name
92
94
  attr_accessor :socket_configurator
95
+ attr_accessor :recoverable_exceptions
93
96
 
94
97
  # @param [String, Hash] connection_string_or_opts Connection string or a hash of connection options
95
98
  # @param [Hash] optz Extra options not related to connection
@@ -101,19 +104,21 @@ module Bunny
101
104
  # @option connection_string_or_opts [String] :username ("guest") Username
102
105
  # @option connection_string_or_opts [String] :password ("guest") Password
103
106
  # @option connection_string_or_opts [String] :vhost ("/") Virtual host to use
104
- # @option connection_string_or_opts [Integer, Symbol] :heartbeat (:server) Heartbeat interval. :server means use the default suggested by RabbitMQ. 0 means no heartbeat (not recommended).
107
+ # @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).
105
108
  # @option connection_string_or_opts [Integer] :network_recovery_interval (4) Recovery interval periodic network recovery will use. This includes initial pause after network failure.
106
109
  # @option connection_string_or_opts [Boolean] :tls (false) Should TLS/SSL be used?
107
110
  # @option connection_string_or_opts [String] :tls_cert (nil) Path to client TLS/SSL certificate file (.pem)
108
111
  # @option connection_string_or_opts [String] :tls_key (nil) Path to client TLS/SSL private key file (.pem)
109
112
  # @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
110
113
  # @option connection_string_or_opts [String] :verify_peer (true) Whether TLS peer verification should be performed
111
- # @option connection_string_or_opts [Symbol] :tls_version (negotiated) What TLS version should be used (:TLSv1, :TLSv1_1, or :TLSv1_2)
114
+ # @option connection_string_or_opts [Symbol] :tls_protocol (negotiated) What TLS version should be used (:TLSv1, :TLSv1_1, or :TLSv1_2)
115
+ # @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.
112
116
  # @option connection_string_or_opts [Integer] :continuation_timeout (15000) Timeout for client operations that expect a response (e.g. {Bunny::Queue#get}), in milliseconds.
113
117
  # @option connection_string_or_opts [Integer] :connection_timeout (30) Timeout in seconds for connecting to the server.
114
- # @option connection_string_or_opts [Integer] :read_timeout (30) TCP socket read timeout in seconds.
118
+ # @option connection_string_or_opts [Integer] :read_timeout (30) TCP socket read timeout in seconds. If heartbeats are disabled this will be ignored.
115
119
  # @option connection_string_or_opts [Integer] :write_timeout (30) TCP socket write timeout in seconds.
116
- # @option connection_string_or_opts [Proc] :hosts_shuffle_strategy A Proc that reorders a list of host strings, defaults to Array#shuffle
120
+ # @option connection_string_or_opts [Proc] :hosts_shuffle_strategy a callable that reorders a list of host strings, defaults to Array#shuffle
121
+ # @option connection_string_or_opts [Proc] :recovery_completed a callable that will be called when a network recovery is performed
117
122
  # @option connection_string_or_opts [Logger] :logger The logger. If missing, one is created using :log_file and :log_level.
118
123
  # @option connection_string_or_opts [IO, String] :log_file The file or path to use when creating a logger. Defaults to STDOUT.
119
124
  # @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.
@@ -121,10 +126,15 @@ module Bunny
121
126
  # @option connection_string_or_opts [Boolean] :automatically_recover (true) Should automatically recover from network failures?
122
127
  # @option connection_string_or_opts [Integer] :recovery_attempts (nil) Max number of recovery attempts, nil means forever
123
128
  # @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.
129
+ # @option connection_string_or_opts [Proc] :recovery_attempt_started (nil) Will be called before every connection recovery attempt
130
+ # @option connection_string_or_opts [Proc] :recovery_completed (nil) Will be called after successful connection recovery
131
+ # @option connection_string_or_opts [Proc] :recovery_attempts_exhausted (nil) Will be called when the connection recovery failed after the specified amount of recovery attempts
124
132
  # @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)?
133
+ # @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.
125
134
  #
126
135
  # @option optz [String] :auth_mechanism ("PLAIN") Authentication mechanism, PLAIN or EXTERNAL
127
136
  # @option optz [String] :locale ("PLAIN") Locale RabbitMQ should use
137
+ # @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.
128
138
  #
129
139
  # @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
130
140
  # @see http://rubybunny.info/articles/tls.html TLS/SSL guide
@@ -151,22 +161,25 @@ module Bunny
151
161
  @addresses = self.addresses_from(opts)
152
162
  @address_index = 0
153
163
 
154
- # re-init, see above
155
- @logger = opts.fetch(:logger, init_default_logger(log_file, log_level))
156
-
164
+ @transport = nil
157
165
  @user = self.username_from(opts)
158
166
  @pass = self.password_from(opts)
159
167
  @vhost = self.vhost_from(opts)
160
168
  @threaded = opts.fetch(:threaded, true)
161
169
 
170
+ # re-init, see above
171
+ @logger = opts.fetch(:logger, init_default_logger(log_file, log_level))
172
+
162
173
  validate_connection_options(opts)
174
+ @last_connection_error = nil
163
175
 
164
176
  # should automatic recovery from network failures be used?
165
177
  @automatically_recover = if opts[:automatically_recover].nil? && opts[:automatic_recovery].nil?
166
178
  true
167
179
  else
168
- opts[:automatically_recover] || opts[:automatic_recovery]
180
+ opts[:automatically_recover] | opts[:automatic_recovery]
169
181
  end
182
+ @recovering_from_network_failure = false
170
183
  @max_recovery_attempts = opts[:recovery_attempts]
171
184
  @recovery_attempts = @max_recovery_attempts
172
185
  # When this is set, connection attempts won't be reset after
@@ -180,6 +193,7 @@ module Bunny
180
193
  @continuation_timeout = opts.fetch(:continuation_timeout, DEFAULT_CONTINUATION_TIMEOUT)
181
194
 
182
195
  @status = :not_connected
196
+ @manually_closed = false
183
197
  @blocked = false
184
198
 
185
199
  # these are negotiated with the broker during the connection tuning phase
@@ -187,10 +201,13 @@ module Bunny
187
201
  @client_channel_max = normalize_client_channel_max(opts.fetch(:channel_max, DEFAULT_CHANNEL_MAX))
188
202
  # will be-renegotiated during connection tuning steps. MK.
189
203
  @channel_max = @client_channel_max
204
+ @heartbeat_sender = nil
190
205
  @client_heartbeat = self.heartbeat_from(opts)
191
206
 
192
207
  client_props = opts[:properties] || opts[:client_properties] || {}
208
+ @connection_name = client_props[:connection_name] || opts[:connection_name]
193
209
  @client_properties = DEFAULT_CLIENT_PROPERTIES.merge(client_props)
210
+ .merge(connection_name: connection_name)
194
211
  @mechanism = normalize_auth_mechanism(opts.fetch(:auth_mechanism, "PLAIN"))
195
212
  @credentials_encoder = credentials_encoder_for(@mechanism)
196
213
  @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
@@ -207,10 +224,17 @@ module Bunny
207
224
 
208
225
  @channels = Hash.new
209
226
 
210
- @origin_thread = Thread.current
227
+ @recovery_attempt_started = opts[:recovery_attempt_started]
228
+ @recovery_completed = opts[:recovery_completed]
229
+ @recovery_attempts_exhausted = opts[:recovery_attempts_exhausted]
230
+
231
+ @session_error_handler = opts.fetch(:session_error_handler, Thread.current)
232
+
233
+ @recoverable_exceptions = DEFAULT_RECOVERABLE_EXCEPTIONS.dup
211
234
 
212
235
  self.reset_continuations
213
236
  self.initialize_transport
237
+
214
238
  end
215
239
 
216
240
  def validate_connection_options(options)
@@ -232,9 +256,13 @@ module Bunny
232
256
  # @return [String] Virtual host used
233
257
  def virtual_host; self.vhost; end
234
258
 
235
- # @return [Integer] Heartbeat interval used
259
+ # @deprecated
260
+ # @return [Integer] Heartbeat timeout (not interval) used
236
261
  def heartbeat_interval; self.heartbeat; end
237
262
 
263
+ # @return [Integer] Heartbeat timeout used
264
+ def heartbeat_timeout; self.heartbeat; end
265
+
238
266
  # @return [Boolean] true if this connection uses TLS (SSL)
239
267
  def uses_tls?
240
268
  @transport.uses_tls?
@@ -303,10 +331,6 @@ module Bunny
303
331
  @transport.post_initialize_socket
304
332
  @transport.connect
305
333
 
306
- if @socket_configurator
307
- @transport.configure_socket(&@socket_configurator)
308
- end
309
-
310
334
  self.init_connection
311
335
  self.open_connection
312
336
 
@@ -333,6 +357,14 @@ module Bunny
333
357
  self
334
358
  end
335
359
 
360
+ def update_secret(value, reason)
361
+ @transport.send_frame(AMQ::Protocol::Connection::UpdateSecret.encode(value, reason))
362
+ @last_update_secret_ok = wait_on_continuations
363
+ raise_if_continuation_resulted_in_a_connection_error!
364
+
365
+ @last_update_secret_ok
366
+ end
367
+
336
368
  # Socket operation write timeout used by this connection
337
369
  # @return [Integer]
338
370
  # @private
@@ -363,14 +395,16 @@ module Bunny
363
395
  alias channel create_channel
364
396
 
365
397
  # Closes the connection. This involves closing all of its channels.
366
- def close
398
+ def close(await_response = true)
367
399
  @status_mutex.synchronize { @status = :closing }
368
400
 
369
401
  ignoring_io_errors do
370
402
  if @transport.open?
403
+ @logger.debug "Transport is still open..."
371
404
  close_all_channels
372
405
 
373
- self.close_connection(true)
406
+ @logger.debug "Will close all channels...."
407
+ self.close_connection(await_response)
374
408
  end
375
409
 
376
410
  clean_up_on_shutdown
@@ -379,6 +413,8 @@ module Bunny
379
413
  @status = :closed
380
414
  @manually_closed = true
381
415
  end
416
+ @logger.debug "Connection is closed"
417
+ true
382
418
  end
383
419
  alias stop close
384
420
 
@@ -413,7 +449,7 @@ module Bunny
413
449
  @status_mutex.synchronize { @status == :closed }
414
450
  end
415
451
 
416
- # @return [Boolean] true if this AMQP 0.9.1 connection has been programmatically closed
452
+ # @return [Boolean] true if this AMQP 0.9.1 connection has been closed by the user (as opposed to the server)
417
453
  def manually_closed?
418
454
  @status_mutex.synchronize { @manually_closed == true }
419
455
  end
@@ -464,7 +500,7 @@ module Bunny
464
500
  # @param [String] uri amqp or amqps URI to parse
465
501
  # @return [Hash] Parsed URI as a hash
466
502
  def self.parse_uri(uri)
467
- AMQ::Settings.parse_amqp_url(uri)
503
+ AMQ::Settings.configure(uri)
468
504
  end
469
505
 
470
506
  # Checks if a queue with given name exists.
@@ -507,6 +543,24 @@ module Bunny
507
543
  end
508
544
  end
509
545
 
546
+ # Defines a callable (e.g. a block) that will be called
547
+ # before every connection recovery attempt.
548
+ def before_recovery_attempt_starts(&block)
549
+ @recovery_attempt_started = block
550
+ end
551
+
552
+ # Defines a callable (e.g. a block) that will be called
553
+ # after successful connection recovery.
554
+ def after_recovery_completed(&block)
555
+ @recovery_completed = block
556
+ end
557
+
558
+ # Defines a callable (e.g. a block) that will be called
559
+ # when the connection recovery failed after the specified
560
+ # numbers of recovery attempts.
561
+ def after_recovery_attempts_exhausted(&block)
562
+ @recovery_attempts_exhausted = block
563
+ end
510
564
 
511
565
  #
512
566
  # Implementation
@@ -563,11 +617,13 @@ module Bunny
563
617
  end
564
618
 
565
619
  # @private
566
- def close_connection(sync = true)
620
+ def close_connection(await_response = true)
567
621
  if @transport.open?
622
+ @logger.debug "Transport is still open"
568
623
  @transport.send_frame(AMQ::Protocol::Connection::Close.encode(200, "Goodbye", 0, 0))
569
624
 
570
- if sync
625
+ if await_response
626
+ @logger.debug "Waiting for a connection.close-ok..."
571
627
  @last_connection_close_ok = wait_on_continuations
572
628
  end
573
629
  end
@@ -617,6 +673,8 @@ module Bunny
617
673
  when AMQ::Protocol::Connection::Unblocked then
618
674
  @blocked = false
619
675
  @unblock_callback.call(method) if @unblock_callback
676
+ when AMQ::Protocol::Connection::UpdateSecretOk then
677
+ @continuations.push(method)
620
678
  when AMQ::Protocol::Channel::Close then
621
679
  begin
622
680
  ch = synchronised_find_channel(ch_number)
@@ -625,8 +683,12 @@ module Bunny
625
683
  # avoid doing that while holding a mutex lock. MK.
626
684
  ch.handle_method(method)
627
685
  ensure
628
- # synchronises on @channel_mutex under the hood
629
- self.unregister_channel(ch)
686
+ if ch.nil?
687
+ @logger.warn "Received a server-sent channel.close but the channel was not found locally. Ignoring the frame."
688
+ else
689
+ # synchronises on @channel_mutex under the hood
690
+ self.unregister_channel(ch)
691
+ end
630
692
  end
631
693
  when AMQ::Protocol::Basic::GetEmpty then
632
694
  ch = find_channel(ch_number)
@@ -697,9 +759,7 @@ module Bunny
697
759
 
698
760
  # @private
699
761
  def recoverable_network_failure?(exception)
700
- # No reasonably smart strategy was suggested in a few years.
701
- # So just recover unconditionally. MK.
702
- true
762
+ @recoverable_exceptions.any? {|x| exception.kind_of? x}
703
763
  end
704
764
 
705
765
  # @private
@@ -720,6 +780,7 @@ module Bunny
720
780
  def recover_from_network_failure
721
781
  sleep @network_recovery_interval
722
782
  @logger.debug "Will attempt connection recovery..."
783
+ notify_of_recovery_attempt_start
723
784
 
724
785
  self.initialize_transport
725
786
 
@@ -738,20 +799,28 @@ module Bunny
738
799
  end
739
800
 
740
801
  recover_channels
802
+ notify_of_recovery_completion
741
803
  end
742
804
  rescue HostListDepleted
743
805
  reset_address_index
744
806
  retry
745
- rescue TCPConnectionFailedForAllHosts, TCPConnectionFailed, AMQ::Protocol::EmptyResponseError, SystemCallError => e
746
- @logger.warn "TCP connection failed, reconnecting in #{@network_recovery_interval} seconds"
747
- if should_retry_recovery?
748
- decrement_recovery_attemp_counter!
749
- if recoverable_network_failure?(e)
807
+ rescue => e
808
+ if recoverable_network_failure?(e)
809
+ @logger.warn "TCP connection failed"
810
+ if should_retry_recovery?
811
+ @logger.warn "Reconnecting in #{@network_recovery_interval} seconds"
812
+ decrement_recovery_attemp_counter!
750
813
  announce_network_failure_recovery
751
814
  retry
815
+ else
816
+ @logger.error "Ran out of recovery attempts (limit set to #{@max_recovery_attempts}), giving up"
817
+ @transport.close
818
+ self.close(false)
819
+ @manually_closed = false
820
+ notify_of_recovery_attempts_exhausted
752
821
  end
753
822
  else
754
- @logger.error "Ran out of recovery attempts (limit set to #{@max_recovery_attempts})"
823
+ raise e
755
824
  end
756
825
  end
757
826
 
@@ -767,8 +836,10 @@ module Bunny
767
836
 
768
837
  # @private
769
838
  def decrement_recovery_attemp_counter!
770
- @recovery_attempts -= 1 if @recovery_attempts
771
- @logger.debug "#{@recovery_attempts} recovery attempts left"
839
+ if @recovery_attempts
840
+ @recovery_attempts -= 1
841
+ @logger.debug "#{@recovery_attempts} recovery attempts left"
842
+ end
772
843
  @recovery_attempts
773
844
  end
774
845
 
@@ -782,12 +853,26 @@ module Bunny
782
853
  @channel_mutex.synchronize do
783
854
  @channels.each do |n, ch|
784
855
  ch.open
785
-
786
856
  ch.recover_from_network_failure
787
857
  end
788
858
  end
789
859
  end
790
860
 
861
+ # @private
862
+ def notify_of_recovery_attempt_start
863
+ @recovery_attempt_started.call if @recovery_attempt_started
864
+ end
865
+
866
+ # @private
867
+ def notify_of_recovery_completion
868
+ @recovery_completed.call if @recovery_completed
869
+ end
870
+
871
+ # @private
872
+ def notify_of_recovery_attempts_exhausted
873
+ @recovery_attempts_exhausted.call if @recovery_attempts_exhausted
874
+ end
875
+
791
876
  # @private
792
877
  def instantiate_connection_level_exception(frame)
793
878
  case frame
@@ -823,7 +908,7 @@ module Bunny
823
908
 
824
909
  clean_up_on_shutdown
825
910
  if threaded?
826
- @origin_thread.raise(@last_connection_error)
911
+ @session_error_handler.raise(@last_connection_error)
827
912
  else
828
913
  raise @last_connection_error
829
914
  end
@@ -943,7 +1028,7 @@ module Bunny
943
1028
 
944
1029
  # @private
945
1030
  def heartbeat_from(options)
946
- options[:heartbeat] || options[:heartbeat_interval] || options[:requested_heartbeat] || DEFAULT_HEARTBEAT
1031
+ options[:heartbeat] || options[:heartbeat_timeout] || options[:requested_heartbeat] || options[:heartbeat_interval] || DEFAULT_HEARTBEAT
947
1032
  end
948
1033
 
949
1034
  # @private
@@ -980,7 +1065,7 @@ module Bunny
980
1065
 
981
1066
  # @private
982
1067
  def reader_loop
983
- @reader_loop ||= ReaderLoop.new(@transport, self, Thread.current)
1068
+ @reader_loop ||= ReaderLoop.new(@transport, self, @session_error_handler)
984
1069
  end
985
1070
 
986
1071
  # @private
@@ -1068,12 +1153,18 @@ module Bunny
1068
1153
  # threads publish on the same channel aggressively, at some point frames will be
1069
1154
  # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
1070
1155
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
1071
- # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
1156
+ # locking. Note that "single frame" methods technically do not need this kind of synchronization
1157
+ # (no incorrect frame interleaving of the same kind as with basic.publish isn't possible) but we
1158
+ # still recommend not sharing channels between threads except for consumer-only cases in the docs. MK.
1072
1159
  channel.synchronize do
1073
1160
  # see rabbitmq/rabbitmq-server#156
1074
- data = frames.reduce("") { |acc, frame| acc << frame.encode }
1075
- @transport.write(data)
1076
- signal_activity!
1161
+ if open?
1162
+ data = frames.reduce("") { |acc, frame| acc << frame.encode }
1163
+ @transport.write(data)
1164
+ signal_activity!
1165
+ else
1166
+ raise ConnectionClosedError.new(frames)
1167
+ end
1077
1168
  end
1078
1169
  end # send_frameset(frames)
1079
1170
 
@@ -1087,10 +1178,14 @@ module Bunny
1087
1178
  # threads publish on the same channel aggressively, at some point frames will be
1088
1179
  # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
1089
1180
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
1090
- # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
1181
+ # locking. See a note about "single frame" methods in a comment in `send_frameset`. MK.
1091
1182
  channel.synchronize do
1092
- frames.each { |frame| self.send_frame_without_timeout(frame, false) }
1093
- signal_activity!
1183
+ if open?
1184
+ frames.each { |frame| self.send_frame_without_timeout(frame, false) }
1185
+ signal_activity!
1186
+ else
1187
+ raise ConnectionClosedError.new(frames)
1188
+ end
1094
1189
  end
1095
1190
  end # send_frameset_without_timeout(frames)
1096
1191
 
@@ -1177,26 +1272,18 @@ module Bunny
1177
1272
  @logger.debug { "Heartbeat interval negotiation: client = #{@client_heartbeat}, server = #{connection_tune.heartbeat}, result = #{@heartbeat}" }
1178
1273
  @logger.info "Heartbeat interval used (in seconds): #{@heartbeat}"
1179
1274
 
1180
- # We set the read_write_timeout to twice the heartbeat value
1275
+ # We set the read_write_timeout to twice the heartbeat value,
1276
+ # and then some padding for edge cases.
1181
1277
  # This allows us to miss a single heartbeat before we time out the socket.
1182
- #
1183
- # Since RabbitMQ can be configured to disable heartbeats (bad idea but technically
1184
- # possible nonetheless), we need to take both client and server values into
1185
- # consideration when deciding about using the heartbeat value for read timeouts.
1186
- @transport.read_timeout = if heartbeat_disabled?(@client_heartbeat) || heartbeat_disabled?(@heartbeat)
1187
- @logger.debug { "Will use default socket read timeout of #{Transport::DEFAULT_READ_TIMEOUT}" }
1188
- Transport::DEFAULT_READ_TIMEOUT
1189
- else
1190
- # pad to account for edge cases. MK.
1191
- n = @heartbeat * 2.2
1192
- @logger.debug { "Will use socket read timeout of #{n}" }
1193
- n
1194
- end
1195
-
1278
+ # If heartbeats are disabled, assume that TCP keepalives or a similar mechanism will be used
1279
+ # and disable socket read timeouts. See ruby-amqp/bunny#551.
1280
+ @transport.read_timeout = @heartbeat * 2.2
1281
+ @logger.debug { "Will use socket read timeout of #{@transport.read_timeout.to_i} seconds" }
1196
1282
 
1197
1283
  # if there are existing channels we've just recovered from
1198
1284
  # a network failure and need to fix the allocated set. See issue 205. MK.
1199
1285
  if @channels.empty?
1286
+ @logger.debug { "Initializing channel ID allocator with channel_max = #{@channel_max}" }
1200
1287
  @channel_id_allocator = ChannelIdAllocator.new(@channel_max)
1201
1288
  end
1202
1289
 
@@ -1241,7 +1328,7 @@ module Bunny
1241
1328
  end
1242
1329
 
1243
1330
  if threaded?
1244
- @origin_thread.raise(e)
1331
+ @session_error_handler.raise(e)
1245
1332
  else
1246
1333
  raise e
1247
1334
  end
@@ -1287,8 +1374,8 @@ module Bunny
1287
1374
  @transport = Transport.new(self,
1288
1375
  host_from_address(address),
1289
1376
  port_from_address(address),
1290
- @opts.merge(:session_thread => @origin_thread)
1291
- )
1377
+ @opts.merge(:session_error_handler => @session_error_handler)
1378
+ )
1292
1379
 
1293
1380
  # Reset the cached progname for the logger only when no logger was provided
1294
1381
  @default_logger.progname = self.to_s