bunny 2.7.4 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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