bunny 0.9.0.pre13 → 0.9.0.rc1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog.md +23 -0
  3. data/README.md +2 -2
  4. data/lib/bunny.rb +15 -2
  5. data/lib/bunny/authentication/credentials_encoder.rb +18 -0
  6. data/lib/bunny/authentication/external_mechanism_encoder.rb +1 -0
  7. data/lib/bunny/authentication/plain_mechanism_encoder.rb +2 -0
  8. data/lib/bunny/channel.rb +9 -3
  9. data/lib/bunny/channel_id_allocator.rb +8 -0
  10. data/lib/bunny/concurrent/condition.rb +2 -0
  11. data/lib/bunny/concurrent/continuation_queue.rb +3 -0
  12. data/lib/bunny/concurrent/linked_continuation_queue.rb +3 -0
  13. data/lib/bunny/consumer.rb +2 -0
  14. data/lib/bunny/consumer_tag_generator.rb +1 -0
  15. data/lib/bunny/consumer_work_pool.rb +2 -0
  16. data/lib/bunny/delivery_info.rb +18 -2
  17. data/lib/bunny/exceptions.rb +49 -10
  18. data/lib/bunny/framing.rb +3 -0
  19. data/lib/bunny/heartbeat_sender.rb +4 -1
  20. data/lib/bunny/message_properties.rb +22 -0
  21. data/lib/bunny/queue.rb +49 -0
  22. data/lib/bunny/reader_loop.rb +1 -0
  23. data/lib/bunny/return_info.rb +11 -0
  24. data/lib/bunny/session.rb +53 -19
  25. data/lib/bunny/socket.rb +1 -0
  26. data/lib/bunny/ssl_socket.rb +29 -5
  27. data/lib/bunny/system_timer.rb +2 -0
  28. data/lib/bunny/test_kit.rb +4 -3
  29. data/lib/bunny/transport.rb +41 -25
  30. data/lib/bunny/version.rb +2 -1
  31. data/spec/compatibility/queue_declare_spec.rb +4 -0
  32. data/spec/compatibility/queue_declare_with_default_channel_spec.rb +33 -0
  33. data/spec/higher_level_api/integration/basic_get_spec.rb +35 -0
  34. data/spec/higher_level_api/integration/basic_nack_spec.rb +19 -1
  35. data/spec/higher_level_api/integration/connection_spec.rb +5 -0
  36. data/spec/higher_level_api/integration/queue_unbind_spec.rb +23 -2
  37. data/spec/higher_level_api/integration/tls_connection_spec.rb +47 -0
  38. data/spec/lower_level_api/integration/basic_cancel_spec.rb +8 -1
  39. data/spec/tls/cacert.pem +18 -0
  40. data/spec/tls/client_cert.pem +18 -0
  41. data/spec/tls/client_key.pem +27 -0
  42. data/spec/tls/server_cert.pem +18 -0
  43. data/spec/tls/server_key.pem +27 -0
  44. metadata +16 -2
@@ -5,6 +5,7 @@ module Bunny
5
5
  # fully.
6
6
  #
7
7
  # Heavily inspired by Dalli by Mike Perham.
8
+ # @private
8
9
  class Socket < TCPSocket
9
10
  attr_accessor :options
10
11
 
@@ -4,9 +4,24 @@ module Bunny
4
4
  begin
5
5
  require "openssl"
6
6
 
7
+ # TLS-enabled TCP socket that implements convenience
8
+ # methods found in Bunny::Socket.
7
9
  class SSLSocket < OpenSSL::SSL::SSLSocket
10
+
11
+ # IO::WaitReadable is 1.9+ only
12
+ READ_RETRY_EXCEPTION_CLASSES = [Errno::EAGAIN, Errno::EWOULDBLOCK]
13
+ READ_RETRY_EXCEPTION_CLASSES << IO::WaitReadable if IO.const_defined?(:WaitReadable)
14
+
15
+
16
+ # Reads given number of bytes with an optional timeout
17
+ #
18
+ # @param [Integer] count How many bytes to read
19
+ # @param [Integer] timeout Timeout
20
+ #
21
+ # @return [String] Data read from the socket
22
+ # @api public
8
23
  def read_fully(count, timeout = nil)
9
- return nil if @eof
24
+ return nil if @__bunny_socket_eof_flag__
10
25
 
11
26
  value = ''
12
27
  begin
@@ -14,10 +29,19 @@ module Bunny
14
29
  value << read_nonblock(count - value.bytesize)
15
30
  break if value.bytesize >= count
16
31
  end
17
- rescue EOFError
18
- @eof = true
19
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK, OpenSSL::SSL::SSLError => e
20
- puts e.inspect
32
+ rescue EOFError => e
33
+ @__bunny_socket_eof_flag__ = true
34
+ rescue OpenSSL::SSL::SSLError => e
35
+ if e.message == "read would block"
36
+ if IO.select([self], nil, nil, timeout)
37
+ retry
38
+ else
39
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
40
+ end
41
+ else
42
+ raise e
43
+ end
44
+ rescue *READ_RETRY_EXCEPTION_CLASSES => e
21
45
  if IO.select([self], nil, nil, timeout)
22
46
  retry
23
47
  else
@@ -5,6 +5,8 @@ require "system_timer"
5
5
  module Bunny
6
6
  # Used for Ruby before 1.9
7
7
  class SystemTimer
8
+ # Executes a block of code, raising if the execution does not finish
9
+ # in the alloted period of time, in seconds.
8
10
  def self.timeout(seconds, exception)
9
11
  if seconds
10
12
  ::SystemTimer.timeout_after(seconds, exception) do
@@ -10,10 +10,11 @@ module Bunny
10
10
  Range.new(a, b).to_a.sample
11
11
  end
12
12
 
13
- # @param [Integer] Lower bound of message size, in KB
14
- # @param [Integer] Upper bound of message size, in KB
15
- # @param [Integer] Random number to use in message generation
13
+ # @param [Integer] a Lower bound of message size, in KB
14
+ # @param [Integer] b Upper bound of message size, in KB
15
+ # @param [Integer] i Random number to use in message generation
16
16
  # @return [String] Message payload of length in the given range, with non-ASCII characters
17
+ # @api public
17
18
  def message_in_kb(a, b, i)
18
19
  s = "Ю#{i}"
19
20
  n = random_in_range(a, b) / s.bytesize
@@ -11,6 +11,7 @@ require "bunny/exceptions"
11
11
  require "bunny/socket"
12
12
 
13
13
  module Bunny
14
+ # @private
14
15
  class Transport
15
16
 
16
17
  #
@@ -23,6 +24,7 @@ module Bunny
23
24
 
24
25
 
25
26
  attr_reader :session, :host, :port, :socket, :connect_timeout, :read_write_timeout, :disconnect_timeout
27
+ attr_reader :tls_context
26
28
 
27
29
  def initialize(session, host, port, opts)
28
30
  @session = session
@@ -37,6 +39,7 @@ module Bunny
37
39
  @tls_certificate = opts[:tls_certificate] || opts[:ssl_cert_string]
38
40
  @tls_key = opts[:tls_key] || opts[:ssl_key_string]
39
41
  @tls_certificate_store = opts[:tls_certificate_store]
42
+ @tls_ca_certificates = opts.fetch(:tls_ca_certificates, [])
40
43
  @verify_peer = opts[:verify_ssl] || opts[:verify_peer]
41
44
 
42
45
  @read_write_timeout = opts[:socket_timeout] || 3
@@ -46,9 +49,6 @@ module Bunny
46
49
  @disconnect_timeout = @read_write_timeout || @connect_timeout
47
50
 
48
51
  @writes_mutex = Mutex.new
49
-
50
- initialize_socket
51
- connect
52
52
  end
53
53
 
54
54
 
@@ -76,6 +76,10 @@ module Bunny
76
76
  end
77
77
  end
78
78
 
79
+ def configure_socket(&block)
80
+ block.call(@socket)
81
+ end
82
+
79
83
 
80
84
  # Writes data to the socket. If read/write timeout was specified, Bunny::ClientTimeout will be raised
81
85
  # if the operation times out.
@@ -149,7 +153,7 @@ module Bunny
149
153
 
150
154
 
151
155
  def close(reason = nil)
152
- @socket.close unless @socket.closed?
156
+ @socket.close if @socket && !@socket.closed?
153
157
  end
154
158
 
155
159
  def open?
@@ -218,21 +222,6 @@ module Bunny
218
222
  raise ConnectionTimeout.new("#{host}:#{port} is unreachable") if !reacheable?(host, port, timeout)
219
223
  end
220
224
 
221
-
222
- protected
223
-
224
- def tls_enabled?(opts)
225
- opts[:tls] || opts[:ssl] || (opts[:port] == AMQ::Protocol::TLS_PORT) || false
226
- end
227
-
228
- def tls_certificate_path_from(opts)
229
- opts[:tls_cert] || opts[:ssl_cert] || opts[:tls_cert_path] || opts[:ssl_cert_path] || opts[:tls_certificate_path] || opts[:ssl_certificate_path]
230
- end
231
-
232
- def tls_key_path_from(opts)
233
- opts[:tls_key] || opts[:ssl_key] || opts[:tls_key_path] || opts[:ssl_key_path]
234
- end
235
-
236
225
  def initialize_socket
237
226
  begin
238
227
  s = Bunny::Timer.timeout(@connect_timeout, ConnectionTimeout) do
@@ -254,12 +243,26 @@ module Bunny
254
243
  @socket
255
244
  end
256
245
 
246
+ protected
247
+
248
+ def tls_enabled?(opts)
249
+ opts[:tls] || opts[:ssl] || (opts[:port] == AMQ::Protocol::TLS_PORT) || false
250
+ end
251
+
252
+ def tls_certificate_path_from(opts)
253
+ opts[:tls_cert] || opts[:ssl_cert] || opts[:tls_cert_path] || opts[:ssl_cert_path] || opts[:tls_certificate_path] || opts[:ssl_certificate_path]
254
+ end
255
+
256
+ def tls_key_path_from(opts)
257
+ opts[:tls_key] || opts[:ssl_key] || opts[:tls_key_path] || opts[:ssl_key_path]
258
+ end
259
+
257
260
  def wrap_in_tls_socket(socket)
258
261
  read_tls_keys!
259
262
 
260
- ctx = initialize_tls_context(OpenSSL::SSL::SSLContext.new(@opts.fetch(:tls_protocol, DEFAULT_TLS_PROTOCOL)))
263
+ @tls_context = initialize_tls_context(OpenSSL::SSL::SSLContext.new)
261
264
 
262
- s = Bunny::SSLSocket.new(socket, ctx)
265
+ s = Bunny::SSLSocket.new(socket, @tls_context)
263
266
  s.sync_close = true
264
267
  s
265
268
  end
@@ -280,15 +283,28 @@ module Bunny
280
283
  end
281
284
 
282
285
  def initialize_tls_context(ctx)
283
- ctx.cert = OpenSSL::X509::Certificate.new(@tls_certificate) if @tls_certificate
284
- ctx.key = OpenSSL::PKey::RSA.new(@tls_key) if @tls_key
285
- ctx.cert_store = @tls_certificate_store if @tls_certificate_store
286
-
286
+ ctx.cert = OpenSSL::X509::Certificate.new(@tls_certificate)
287
+ ctx.key = OpenSSL::PKey::RSA.new(@tls_key)
288
+ ctx.cert_store = if @tls_certificate_store
289
+ @tls_certificate_store
290
+ else
291
+ initialize_tls_certificate_store(@tls_ca_certificates)
292
+ end
293
+
294
+ # setting TLS/SSL version only works correctly when done
295
+ # vis set_params. MK.
296
+ ctx.set_params(:ssl_version => @opts.fetch(:tls_protocol, DEFAULT_TLS_PROTOCOL))
287
297
  ctx.set_params(:verify_mode => OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT) if @verify_peer
288
298
 
289
299
  ctx
290
300
  end
291
301
 
302
+ def initialize_tls_certificate_store(certs)
303
+ OpenSSL::X509::Store.new.tap do |store|
304
+ certs.each { |path| store.add_file(path) }
305
+ end
306
+ end
307
+
292
308
  def timeout_from(options)
293
309
  options[:connect_timeout] || options[:connection_timeout] || options[:timeout] || DEFAULT_CONNECTION_TIMEOUT
294
310
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Bunny
4
- VERSION = "0.9.0.pre13"
4
+ # @return [String] Version of the library
5
+ VERSION = "0.9.0.rc1"
5
6
  end
@@ -7,6 +7,10 @@ describe Bunny::Queue, "backwards compatibility API" do
7
7
  c
8
8
  end
9
9
 
10
+ after :all do
11
+ connection.close if connection.open?
12
+ end
13
+
10
14
  context "when queue name is specified" do
11
15
  let(:name) { "a queue declared at #{Time.now.to_i}" }
12
16
 
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+
3
+ describe Bunny::Session do
4
+ let(:connection) do
5
+ c = Bunny.new
6
+ c.start
7
+ c
8
+ end
9
+
10
+ after :all do
11
+ connection.close if connection.open?
12
+ end
13
+
14
+ it "proxies #queue to the pre-opened channel for backwards compatibility" do
15
+ q = connection.queue("", :exclusive => true)
16
+ q.name.should =~ /^amq.gen/
17
+ end
18
+
19
+ it "proxies #fanout to the pre-opened channel for backwards compatibility" do
20
+ x = connection.fanout("amq.fanout")
21
+ x.name.should == "amq.fanout"
22
+ end
23
+
24
+ it "proxies #topic to the pre-opened channel for backwards compatibility" do
25
+ x = connection.topic("amq.topic")
26
+ x.name.should == "amq.topic"
27
+ end
28
+
29
+ it "proxies #direct to the pre-opened channel for backwards compatibility" do
30
+ x = connection.topic("amq.direct")
31
+ x.name.should == "amq.direct"
32
+ end
33
+ end
@@ -30,6 +30,25 @@ describe Bunny::Queue, "#pop" do
30
30
  end
31
31
 
32
32
 
33
+ context "with all defaults and a timeout that is never hit" do
34
+ it "fetches a messages which is automatically acknowledged" do
35
+ ch = connection.create_channel
36
+
37
+ q = ch.queue("", :exclusive => true)
38
+ x = ch.default_exchange
39
+
40
+ x.publish("xyzzy", :routing_key => q.name)
41
+
42
+ sleep(0.5)
43
+ delivery_info, properties, content = q.pop_waiting(:timeout => 1.0)
44
+ content.should == "xyzzy"
45
+ q.message_count.should == 0
46
+
47
+ ch.close
48
+ end
49
+ end
50
+
51
+
33
52
  context "with an empty queue" do
34
53
  it "returns an empty response" do
35
54
  ch = connection.create_channel
@@ -44,4 +63,20 @@ describe Bunny::Queue, "#pop" do
44
63
  ch.close
45
64
  end
46
65
  end
66
+
67
+
68
+ context "with an empty queue and a timeout" do
69
+ it "raises an exception" do
70
+ ch = connection.create_channel
71
+
72
+ q = ch.queue("", :exclusive => true)
73
+ q.purge
74
+
75
+ lambda {
76
+ _, _, content = q.pop_waiting(:timeout => 0.5)
77
+ }.should raise_error(Timeout::Error)
78
+
79
+ ch.close
80
+ end
81
+ end
47
82
  end
@@ -34,7 +34,25 @@ describe Bunny::Channel, "#nack" do
34
34
  end
35
35
 
36
36
  context "with multiple = true" do
37
- it "rejects multiple messages"
37
+ it "rejects multiple messages" do
38
+ q = subject.queue("bunny.basic.nack.with-requeue-true-multi-true", :exclusive => true)
39
+ x = subject.default_exchange
40
+
41
+ 3.times do
42
+ x.publish("bunneth", :routing_key => q.name)
43
+ end
44
+ sleep(0.5)
45
+ q.message_count.should == 3
46
+ _, _, _ = q.pop(:ack => true)
47
+ _, _, _ = q.pop(:ack => true)
48
+ delivery_info, _, content = q.pop(:ack => true)
49
+
50
+ subject.nack(delivery_info.delivery_tag, true, true)
51
+ sleep(0.5)
52
+ q.message_count.should == 3
53
+
54
+ subject.close
55
+ end
38
56
  end
39
57
 
40
58
 
@@ -62,6 +62,11 @@ describe Bunny::Session do
62
62
  Bunny.new
63
63
  end
64
64
 
65
+ it "provides a way to fine tune socket options" do
66
+ subject.start
67
+ subject.transport.socket.should respond_to(:setsockopt)
68
+ end
69
+
65
70
  it "successfully negotiates the connection" do
66
71
  subject.start
67
72
  subject.should be_connected
@@ -12,7 +12,20 @@ describe Bunny::Queue, "bound to an exchange" do
12
12
  end
13
13
 
14
14
 
15
- it "can be unbound from an exchange it was bound to"
15
+ it "can be unbound from an exchange it was bound to" do
16
+ ch = connection.create_channel
17
+ x = ch.fanout("amq.fanout")
18
+ q = ch.queue("", :exclusive => true).bind(x)
19
+
20
+ x.publish("")
21
+ sleep 0.3
22
+ q.message_count.should == 1
23
+
24
+ q.unbind(x)
25
+
26
+ x.publish("")
27
+ q.message_count.should == 1
28
+ end
16
29
  end
17
30
 
18
31
 
@@ -29,5 +42,13 @@ describe Bunny::Queue, "NOT bound to an exchange" do
29
42
  end
30
43
 
31
44
 
32
- it "cannot be unbound (raises a channel error)"
45
+ it "cannot be unbound (raises a channel error)" do
46
+ ch = connection.create_channel
47
+ x = ch.fanout("amq.fanout")
48
+ q = ch.queue("", :exclusive => true)
49
+
50
+ lambda {
51
+ q.unbind(x)
52
+ }.should raise_error(Bunny::NotFound, /no binding/)
53
+ end
33
54
  end
@@ -0,0 +1,47 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "spec_helper"
3
+
4
+ unless ENV["CI"]
5
+ describe "TLS connection to RabbitMQ" do
6
+ let(:connection) do
7
+ c = Bunny.new(:user => "bunny_gem",
8
+ :password => "bunny_password",
9
+ :vhost => "bunny_testbed",
10
+ :tls => true,
11
+ :tls_cert => "spec/tls/client_cert.pem",
12
+ :tls_key => "spec/tls/client_key.pem",
13
+ :tls_ca_certificates => ["./spec/tls/cacert.pem"])
14
+ c.start
15
+ c
16
+ end
17
+
18
+ after :all do
19
+ connection.close
20
+ end
21
+
22
+ it "provides the same API as a regular connection" do
23
+ ch = connection.create_channel
24
+
25
+ q = ch.queue("", :exclusive => true)
26
+ x = ch.default_exchange
27
+
28
+ x.publish("xyzzy", :routing_key => q.name).
29
+ publish("xyzzy", :routing_key => q.name).
30
+ publish("xyzzy", :routing_key => q.name).
31
+ publish("xyzzy", :routing_key => q.name)
32
+
33
+ sleep 0.5
34
+ q.message_count.should == 4
35
+
36
+ i = 0
37
+ q.subscribe do |delivery_info, _, payload|
38
+ i += 1
39
+ end
40
+ sleep 1.0
41
+ i.should == 4
42
+ q.message_count.should == 0
43
+
44
+ ch.close
45
+ end
46
+ end
47
+ end
@@ -57,6 +57,13 @@ describe Bunny::Channel, "#basic_cancel" do
57
57
  end
58
58
 
59
59
  context "when the given consumer tag is invalid (was never registered)" do
60
- it "causes a channel error"
60
+ it "DOES NOT cause a channel error" do
61
+ ch = connection.create_channel
62
+
63
+ # RabbitMQ 3.1 does not raise an exception w/ unknown consumer tag. MK.
64
+ ch.basic_cancel("878798s7df89#{rand}#{Time.now.to_i}")
65
+
66
+ ch.close
67
+ end
61
68
  end
62
69
  end