bunny 1.4.1 → 1.5.0.pre1

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