bunny 0.9.0.pre13 → 0.9.0.rc1

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