bunny 1.4.1 → 1.5.0.pre1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +1 -1
  4. data/ChangeLog.md +19 -5
  5. data/README.md +23 -3
  6. data/examples/guides/extensions/basic_nack.rb +1 -1
  7. data/examples/guides/extensions/dead_letter_exchange.rb +1 -1
  8. data/examples/guides/queues/redeliveries.rb +2 -2
  9. data/lib/bunny.rb +1 -1
  10. data/lib/bunny/channel.rb +34 -21
  11. data/lib/bunny/exceptions.rb +17 -2
  12. data/lib/bunny/exchange.rb +14 -22
  13. data/lib/bunny/queue.rb +19 -12
  14. data/lib/bunny/session.rb +67 -57
  15. data/lib/bunny/version.rb +1 -1
  16. data/spec/config/enabled_plugins +1 -0
  17. data/spec/config/rabbitmq.config +18 -0
  18. data/spec/higher_level_api/integration/basic_ack_spec.rb +29 -2
  19. data/spec/higher_level_api/integration/basic_consume_with_objects_spec.rb +1 -1
  20. data/spec/higher_level_api/integration/basic_nack_spec.rb +5 -5
  21. data/spec/higher_level_api/integration/basic_reject_spec.rb +3 -3
  22. data/spec/higher_level_api/integration/connection_recovery_spec.rb +79 -2
  23. data/spec/higher_level_api/integration/connection_spec.rb +24 -0
  24. data/spec/higher_level_api/integration/dead_lettering_spec.rb +1 -1
  25. data/spec/higher_level_api/integration/exchange_unbind_spec.rb +1 -1
  26. data/spec/higher_level_api/integration/message_properties_access_spec.rb +1 -1
  27. data/spec/issues/issue78_spec.rb +2 -2
  28. data/spec/spec_helper.rb +6 -7
  29. data/spec/tls/ca_certificate.pem +18 -0
  30. data/spec/tls/ca_key.pem +27 -0
  31. data/spec/tls/client_cert.pem +16 -16
  32. data/spec/tls/client_key.pem +25 -25
  33. data/spec/tls/server_cert.pem +16 -16
  34. data/spec/tls/server_key.pem +25 -25
  35. metadata +12 -10
  36. data/.ruby-version +0 -1
  37. data/lib/bunny/compatibility.rb +0 -24
  38. data/spec/compatibility/queue_declare_spec.rb +0 -44
  39. data/spec/compatibility/queue_declare_with_default_channel_spec.rb +0 -33
data/lib/bunny/session.rb CHANGED
@@ -82,9 +82,8 @@ module Bunny
82
82
 
83
83
  # @return [Bunny::Transport]
84
84
  attr_reader :transport
85
- attr_reader :status, :host, :port, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
85
+ attr_reader :status, :port, :heartbeat, :user, :pass, :vhost, :frame_max, :channel_max, :threaded
86
86
  attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
87
- attr_reader :default_channel
88
87
  attr_reader :channel_id_allocator
89
88
  # Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
90
89
  # @return [String]
@@ -99,6 +98,7 @@ module Bunny
99
98
  # @param [Hash] optz Extra options not related to connection
100
99
  #
101
100
  # @option connection_string_or_opts [String] :host ("127.0.0.1") Hostname or IP address to connect to
101
+ # @option connection_string_or_opts [Array<String>] :hosts (["127.0.0.1"]) list of hostname or IP addresses to select hostname from when connecting
102
102
  # @option connection_string_or_opts [Integer] :port (5672) Port RabbitMQ listens on
103
103
  # @option connection_string_or_opts [String] :username ("guest") Username
104
104
  # @option connection_string_or_opts [String] :password ("guest") Password
@@ -109,8 +109,10 @@ module Bunny
109
109
  # @option connection_string_or_opts [String] :tls_cert (nil) Path to client TLS/SSL certificate file (.pem)
110
110
  # @option connection_string_or_opts [String] :tls_key (nil) Path to client TLS/SSL private key file (.pem)
111
111
  # @option connection_string_or_opts [Array<String>] :tls_ca_certificates Array of paths to TLS/SSL CA files (.pem), by default detected from OpenSSL configuration
112
+ # @option connection_string_or_opts [String] :verify_peer (true) Whether TLS peer verification should be performed
112
113
  # @option connection_string_or_opts [Integer] :continuation_timeout (4000) Timeout for client operations that expect a response (e.g. {Bunny::Queue#get}), in milliseconds.
113
114
  # @option connection_string_or_opts [Integer] :connection_timeout (5) Timeout in seconds for connecting to the server.
115
+ # @option connection_string_or_opts [Proc] :hosts_shuffle_strategy A Proc that reorders a list of host strings, defaults to Array#shuffle
114
116
  #
115
117
  # @option optz [String] :auth_mechanism ("PLAIN") Authentication mechanism, PLAIN or EXTERNAL
116
118
  # @option optz [String] :locale ("PLAIN") Locale RabbitMQ should use
@@ -128,8 +130,12 @@ module Bunny
128
130
  connection_string_or_opts
129
131
  end.merge(optz)
130
132
 
133
+ @default_hosts_shuffle_strategy = Proc.new { |hosts| hosts.shuffle }
134
+
131
135
  @opts = opts
132
- @host = self.hostname_from(opts)
136
+ @hosts = self.hostnames_from(opts)
137
+ @host_index = 0
138
+
133
139
  @port = self.port_from(opts)
134
140
  @user = self.username_from(opts)
135
141
  @pass = self.password_from(opts)
@@ -173,6 +179,8 @@ module Bunny
173
179
  # the non-reentrant Ruby mutexes. MK.
174
180
  @transport_mutex = @mutex_impl.new
175
181
  @status_mutex = @mutex_impl.new
182
+ @host_index_mutex = @mutex_impl.new
183
+
176
184
  @channels = Hash.new
177
185
 
178
186
  @origin_thread = Thread.current
@@ -210,6 +218,14 @@ module Bunny
210
218
  @threaded
211
219
  end
212
220
 
221
+ def host
222
+ @transport ? @transport.host : @hosts[@host_index]
223
+ end
224
+
225
+ def reset_host_index
226
+ @host_index_mutex.synchronize { @host_index = 0 }
227
+ end
228
+
213
229
  # @private
214
230
  attr_reader :mutex_impl
215
231
 
@@ -232,6 +248,7 @@ module Bunny
232
248
  # @see http://rubybunny.info/articles/connecting.html
233
249
  # @api public
234
250
  def start
251
+
235
252
  return self if connected?
236
253
 
237
254
  @status_mutex.synchronize { @status = :connecting }
@@ -241,27 +258,42 @@ module Bunny
241
258
  self.reset_continuations
242
259
 
243
260
  begin
244
- # close existing transport if we have one,
245
- # to not leak sockets
246
- @transport.maybe_initialize_socket
247
261
 
248
- @transport.post_initialize_socket
249
- @transport.connect
262
+ begin
250
263
 
251
- if @socket_configurator
252
- @transport.configure_socket(&@socket_configurator)
253
- end
264
+ # close existing transport if we have one,
265
+ # to not leak sockets
266
+ @transport.maybe_initialize_socket
254
267
 
255
- self.init_connection
256
- self.open_connection
268
+ @transport.post_initialize_socket
269
+ @transport.connect
257
270
 
258
- @reader_loop = nil
259
- self.start_reader_loop if threaded?
271
+ if @socket_configurator
272
+ @transport.configure_socket(&@socket_configurator)
273
+ end
260
274
 
261
- @default_channel = self.create_channel unless @default_channel
262
- rescue Exception => e
275
+ self.init_connection
276
+ self.open_connection
277
+
278
+ @reader_loop = nil
279
+ self.start_reader_loop if threaded?
280
+
281
+ rescue TCPConnectionFailed => e
282
+ self.initialize_transport
283
+
284
+ @logger.warn e.message
285
+ @logger.warn "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
286
+
287
+ return self.start
288
+ rescue Exception
289
+ @status_mutex.synchronize { @status = :not_connected }
290
+ raise
291
+ end
292
+
293
+ rescue HostListDepleted
294
+ self.reset_host_index
263
295
  @status_mutex.synchronize { @status = :not_connected }
264
- raise e
296
+ raise TCPConnectionFailedForAllHosts
265
297
  end
266
298
 
267
299
  self
@@ -355,40 +387,6 @@ module Bunny
355
387
  @automatically_recover
356
388
  end
357
389
 
358
- #
359
- # Backwards compatibility
360
- #
361
-
362
- # @private
363
- def queue(*args)
364
- @default_channel.queue(*args)
365
- end
366
-
367
- # @private
368
- def direct(*args)
369
- @default_channel.direct(*args)
370
- end
371
-
372
- # @private
373
- def fanout(*args)
374
- @default_channel.fanout(*args)
375
- end
376
-
377
- # @private
378
- def topic(*args)
379
- @default_channel.topic(*args)
380
- end
381
-
382
- # @private
383
- def headers(*args)
384
- @default_channel.headers(*args)
385
- end
386
-
387
- # @private
388
- def exchange(*args)
389
- @default_channel.exchange(*args)
390
- end
391
-
392
390
  # Defines a callback that will be executed when RabbitMQ blocks the connection
393
391
  # because it is running low on memory or disk space (as configured via config file
394
392
  # and/or rabbitmqctl).
@@ -646,6 +644,8 @@ module Bunny
646
644
  begin
647
645
  sleep @network_recovery_interval
648
646
  @logger.debug "About to start connection recovery..."
647
+
648
+ self.reset_host_index # since we are starting a fresh try.
649
649
  self.initialize_transport
650
650
  self.start
651
651
 
@@ -654,7 +654,7 @@ module Bunny
654
654
 
655
655
  recover_channels
656
656
  end
657
- rescue TCPConnectionFailed, AMQ::Protocol::EmptyResponseError => e
657
+ rescue TCPConnectionFailedForAllHosts, TCPConnectionFailed, AMQ::Protocol::EmptyResponseError => e
658
658
  @logger.warn "TCP connection failed, reconnecting in #{@network_recovery_interval} seconds"
659
659
  sleep @network_recovery_interval
660
660
  retry if recoverable_network_failure?(e)
@@ -663,6 +663,9 @@ module Bunny
663
663
 
664
664
  # @private
665
665
  def recover_channels
666
+ # default channel is reopened right after connection
667
+ # negotiation is completed, so make sure we do not try to open
668
+ # it twice. MK.
666
669
  @channels.each do |n, ch|
667
670
  ch.open
668
671
 
@@ -726,8 +729,10 @@ module Bunny
726
729
  end
727
730
 
728
731
  # @private
729
- def hostname_from(options)
730
- options[:host] || options[:hostname] || DEFAULT_HOST
732
+ def hostnames_from(options)
733
+ options.fetch(:hosts_shuffle_strategy, @default_hosts_shuffle_strategy).call(
734
+ [ options[:hosts] || options[:host] || options[:hostname] || DEFAULT_HOST ].flatten
735
+ )
731
736
  end
732
737
 
733
738
  # @private
@@ -923,7 +928,7 @@ module Bunny
923
928
  # @return [String]
924
929
  # @api public
925
930
  def to_s
926
- "#<#{self.class.name}:#{object_id} #{@user}@#{@host}:#{@port}, vhost=#{@vhost}>"
931
+ "#<#{self.class.name}:#{object_id} #{@user}@#{host}:#{@port}, vhost=#{@vhost}, hosts=[#{@hosts.join(',')}]>"
927
932
  end
928
933
 
929
934
  protected
@@ -1076,7 +1081,12 @@ module Bunny
1076
1081
 
1077
1082
  # @private
1078
1083
  def initialize_transport
1079
- @transport = Transport.new(self, @host, @port, @opts.merge(:session_thread => @origin_thread))
1084
+ if host = @hosts[ @host_index ]
1085
+ @host_index_mutex.synchronize { @host_index += 1 }
1086
+ @transport = Transport.new(self, host, @port, @opts.merge(:session_thread => @origin_thread))
1087
+ else
1088
+ raise HostListDepleted
1089
+ end
1080
1090
  end
1081
1091
 
1082
1092
  # @private
data/lib/bunny/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Bunny
4
4
  # @return [String] Version of the library
5
- VERSION = "1.4.1"
5
+ VERSION = "1.5.0.pre1"
6
6
  end
@@ -0,0 +1 @@
1
+ [rabbitmq_management, rabbitmq_consistent_hash_exchange].
@@ -0,0 +1,18 @@
1
+ [
2
+
3
+ {rabbit, [
4
+ {ssl_listeners, [5671]},
5
+ {ssl_options, [{cacertfile,"spec/tls/cacert.pem"},
6
+ {certfile,"spec/tls/server_cert.pem"},
7
+ {keyfile,"spec/tls/server_key.pem"},
8
+ {verify,verify_none},
9
+ {fail_if_no_peer_cert,false}]} ]
10
+ },
11
+
12
+ {rabbitmq_management,
13
+ [{listener,
14
+ [{port, 15672}]
15
+ }]
16
+ }
17
+
18
+ ].
@@ -20,7 +20,7 @@ describe Bunny::Channel, "#ack" do
20
20
  x.publish("bunneth", :routing_key => q.name)
21
21
  sleep 0.5
22
22
  q.message_count.should == 1
23
- delivery_details, properties, content = q.pop(:ack => true)
23
+ delivery_details, properties, content = q.pop(:manual_ack => true)
24
24
 
25
25
  ch.ack(delivery_details.delivery_tag, true)
26
26
  q.message_count.should == 0
@@ -57,7 +57,7 @@ describe Bunny::Channel, "#ack" do
57
57
  x.publish("bunneth", :routing_key => q.name)
58
58
  sleep 0.5
59
59
  q.message_count.should == 1
60
- _, _, content = q.pop(:ack => true)
60
+ _, _, content = q.pop(:manual_ack => true)
61
61
 
62
62
  ch.on_error do |ch, channel_close|
63
63
  @channel_close = channel_close
@@ -68,4 +68,31 @@ describe Bunny::Channel, "#ack" do
68
68
  @channel_close.reply_code.should == AMQ::Protocol::PreconditionFailed::VALUE
69
69
  end
70
70
  end
71
+
72
+ context "with a valid (known) delivery tag" do
73
+ it "gets a depricated message warning for using :ack" do
74
+ ch = connection.create_channel
75
+ q = ch.queue("bunny.basic.ack.manual-acks", :exclusive => true)
76
+ x = ch.default_exchange
77
+
78
+ x.publish("bunneth", :routing_key => q.name)
79
+ sleep 0.5
80
+ q.message_count.should == 1
81
+
82
+ orig_stderr = $stderr
83
+ $stderr = StringIO.new
84
+
85
+ delivery_details, properties, content = q.pop(:ack => true)
86
+
87
+ $stderr.rewind
88
+ $stderr.string.chomp.should eq("[DEPRECATION] `:ack` is deprecated. Please use `:manual_ack` instead.\n[DEPRECATION] `:ack` is deprecated. Please use `:manual_ack` instead.")
89
+
90
+ $stderr = orig_stderr
91
+
92
+ ch.ack(delivery_details.delivery_tag, true)
93
+ q.message_count.should == 0
94
+
95
+ ch.close
96
+ end
97
+ end
71
98
  end
@@ -44,7 +44,7 @@ describe Bunny::Queue, "#subscribe_with" do
44
44
  end
45
45
  t.abort_on_exception = true
46
46
 
47
- q1.subscribe_with(ec, :ack => true)
47
+ q1.subscribe_with(ec, :manual_ack => true)
48
48
  sleep 2
49
49
  ch1.close
50
50
 
@@ -23,7 +23,7 @@ describe Bunny::Channel, "#nack" do
23
23
  x.publish("bunneth", :routing_key => q.name)
24
24
  sleep(0.5)
25
25
  q.message_count.should == 1
26
- delivery_info, _, content = q.pop(:ack => true)
26
+ delivery_info, _, content = q.pop(:manual_ack => true)
27
27
 
28
28
  subject.nack(delivery_info.delivery_tag, false, false)
29
29
  sleep(0.5)
@@ -43,9 +43,9 @@ q = subject.queue("bunny.basic.nack.with-requeue-true-multi-true", :exclusive =>
43
43
  end
44
44
  sleep(0.5)
45
45
  q.message_count.should == 3
46
- _, _, _ = q.pop(:ack => true)
47
- _, _, _ = q.pop(:ack => true)
48
- delivery_info, _, content = q.pop(:ack => true)
46
+ _, _, _ = q.pop(:manual_ack => true)
47
+ _, _, _ = q.pop(:manual_ack => true)
48
+ delivery_info, _, content = q.pop(:manual_ack => true)
49
49
 
50
50
  subject.nack(delivery_info.delivery_tag, true, true)
51
51
  sleep(0.5)
@@ -64,7 +64,7 @@ q = subject.queue("bunny.basic.nack.with-requeue-true-multi-true", :exclusive =>
64
64
  x.publish("bunneth", :routing_key => q.name)
65
65
  sleep(0.25)
66
66
  q.message_count.should == 1
67
- _, _, content = q.pop(:ack => true)
67
+ _, _, content = q.pop(:manual_ack => true)
68
68
 
69
69
  subject.on_error do |ch, channel_close|
70
70
  @channel_close = channel_close
@@ -20,7 +20,7 @@ describe Bunny::Channel, "#reject" do
20
20
  x.publish("bunneth", :routing_key => q.name)
21
21
  sleep(0.5)
22
22
  q.message_count.should == 1
23
- delivery_info, _, _ = q.pop(:ack => true)
23
+ delivery_info, _, _ = q.pop(:manual_ack => true)
24
24
 
25
25
  ch.reject(delivery_info.delivery_tag, true)
26
26
  sleep(0.5)
@@ -39,7 +39,7 @@ describe Bunny::Channel, "#reject" do
39
39
  x.publish("bunneth", :routing_key => q.name)
40
40
  sleep(0.5)
41
41
  q.message_count.should == 1
42
- delivery_info, _, _ = q.pop(:ack => true)
42
+ delivery_info, _, _ = q.pop(:manual_ack => true)
43
43
 
44
44
  ch.reject(delivery_info.delivery_tag, false)
45
45
  sleep(0.5)
@@ -59,7 +59,7 @@ describe Bunny::Channel, "#reject" do
59
59
  x.publish("bunneth", :routing_key => q.name)
60
60
  sleep(0.25)
61
61
  q.message_count.should == 1
62
- _, _, content = q.pop(:ack => true)
62
+ _, _, content = q.pop(:manual_ack => true)
63
63
 
64
64
  ch.on_error do |ch, channel_close|
65
65
  @channel_close = channel_close
@@ -8,7 +8,11 @@ unless ENV["CI"]
8
8
 
9
9
  def close_all_connections!
10
10
  http_client.list_connections.each do |conn_info|
11
- http_client.close_connection(conn_info.name)
11
+ begin
12
+ http_client.close_connection(conn_info.name)
13
+ rescue Bunny::ConnectionForced
14
+ # This is not a problem, but the specs intermittently believe it is.
15
+ end
12
16
  end
13
17
  end
14
18
 
@@ -25,6 +29,29 @@ unless ENV["CI"]
25
29
  end
26
30
  end
27
31
 
32
+ def with_open_multi_host( c = Bunny.new( :hosts => ["127.0.0.1", "localhost"],
33
+ :network_recovery_interval => 0.2,
34
+ :recover_from_connection_close => true), &block)
35
+ begin
36
+ c.start
37
+ block.call(c)
38
+ ensure
39
+ c.close
40
+ end
41
+ end
42
+
43
+ def with_open_multi_broken_host( c = Bunny.new( :hosts => ["broken", "127.0.0.1", "localhost"],
44
+ :hosts_shuffle_strategy => Proc.new { |hosts| hosts }, # We do not shuffle for these tests so we always hit the broken host
45
+ :network_recovery_interval => 0.2,
46
+ :recover_from_connection_close => true), &block)
47
+ begin
48
+ c.start
49
+ block.call(c)
50
+ ensure
51
+ c.close
52
+ end
53
+ end
54
+
28
55
  def ensure_queue_recovery(ch, q)
29
56
  q.purge
30
57
  x = ch.default_exchange
@@ -66,7 +93,29 @@ unless ENV["CI"]
66
93
  end
67
94
  end
68
95
 
69
- it "recovers channel" do
96
+ it "reconnects after grace period (with multiple hosts)" do
97
+ with_open_multi_host do |c|
98
+ close_all_connections!
99
+ sleep 0.1
100
+ c.should_not be_open
101
+
102
+ wait_for_recovery
103
+ c.should be_open
104
+ end
105
+ end
106
+
107
+ it "reconnects after grace period (with multiple hosts, including a broken one)" do
108
+ with_open_multi_broken_host do |c|
109
+ close_all_connections!
110
+ sleep 0.1
111
+ c.should_not be_open
112
+
113
+ wait_for_recovery
114
+ c.should be_open
115
+ end
116
+ end
117
+
118
+ it "recovers channels" do
70
119
  with_open do |c|
71
120
  ch1 = c.create_channel
72
121
  ch2 = c.create_channel
@@ -80,6 +129,34 @@ unless ENV["CI"]
80
129
  end
81
130
  end
82
131
 
132
+ it "recovers channels (with multiple hosts)" do
133
+ with_open_multi_host do |c|
134
+ ch1 = c.create_channel
135
+ ch2 = c.create_channel
136
+ close_all_connections!
137
+ sleep 0.1
138
+ c.should_not be_open
139
+
140
+ wait_for_recovery
141
+ ch1.should be_open
142
+ ch2.should be_open
143
+ end
144
+ end
145
+
146
+ it "recovers channels (with multiple hosts, including a broken one)" do
147
+ with_open_multi_broken_host do |c|
148
+ ch1 = c.create_channel
149
+ ch2 = c.create_channel
150
+ close_all_connections!
151
+ sleep 0.1
152
+ c.should_not be_open
153
+
154
+ wait_for_recovery
155
+ ch1.should be_open
156
+ ch2.should be_open
157
+ end
158
+ end
159
+
83
160
  it "recovers basic.qos prefetch setting" do
84
161
  with_open do |c|
85
162
  ch = c.create_channel