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.
- checksums.yaml +4 -4
- data/ChangeLog.md +23 -0
- data/README.md +2 -2
- data/lib/bunny.rb +15 -2
- data/lib/bunny/authentication/credentials_encoder.rb +18 -0
- data/lib/bunny/authentication/external_mechanism_encoder.rb +1 -0
- data/lib/bunny/authentication/plain_mechanism_encoder.rb +2 -0
- data/lib/bunny/channel.rb +9 -3
- data/lib/bunny/channel_id_allocator.rb +8 -0
- data/lib/bunny/concurrent/condition.rb +2 -0
- data/lib/bunny/concurrent/continuation_queue.rb +3 -0
- data/lib/bunny/concurrent/linked_continuation_queue.rb +3 -0
- data/lib/bunny/consumer.rb +2 -0
- data/lib/bunny/consumer_tag_generator.rb +1 -0
- data/lib/bunny/consumer_work_pool.rb +2 -0
- data/lib/bunny/delivery_info.rb +18 -2
- data/lib/bunny/exceptions.rb +49 -10
- data/lib/bunny/framing.rb +3 -0
- data/lib/bunny/heartbeat_sender.rb +4 -1
- data/lib/bunny/message_properties.rb +22 -0
- data/lib/bunny/queue.rb +49 -0
- data/lib/bunny/reader_loop.rb +1 -0
- data/lib/bunny/return_info.rb +11 -0
- data/lib/bunny/session.rb +53 -19
- data/lib/bunny/socket.rb +1 -0
- data/lib/bunny/ssl_socket.rb +29 -5
- data/lib/bunny/system_timer.rb +2 -0
- data/lib/bunny/test_kit.rb +4 -3
- data/lib/bunny/transport.rb +41 -25
- data/lib/bunny/version.rb +2 -1
- data/spec/compatibility/queue_declare_spec.rb +4 -0
- data/spec/compatibility/queue_declare_with_default_channel_spec.rb +33 -0
- data/spec/higher_level_api/integration/basic_get_spec.rb +35 -0
- data/spec/higher_level_api/integration/basic_nack_spec.rb +19 -1
- data/spec/higher_level_api/integration/connection_spec.rb +5 -0
- data/spec/higher_level_api/integration/queue_unbind_spec.rb +23 -2
- data/spec/higher_level_api/integration/tls_connection_spec.rb +47 -0
- data/spec/lower_level_api/integration/basic_cancel_spec.rb +8 -1
- data/spec/tls/cacert.pem +18 -0
- data/spec/tls/client_cert.pem +18 -0
- data/spec/tls/client_key.pem +27 -0
- data/spec/tls/server_cert.pem +18 -0
- data/spec/tls/server_key.pem +27 -0
- metadata +16 -2
data/lib/bunny/socket.rb
CHANGED
data/lib/bunny/ssl_socket.rb
CHANGED
@@ -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 @
|
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
|
-
@
|
19
|
-
rescue
|
20
|
-
|
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
|
data/lib/bunny/system_timer.rb
CHANGED
@@ -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
|
data/lib/bunny/test_kit.rb
CHANGED
@@ -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
|
data/lib/bunny/transport.rb
CHANGED
@@ -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
|
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
|
-
|
263
|
+
@tls_context = initialize_tls_context(OpenSSL::SSL::SSLContext.new)
|
261
264
|
|
262
|
-
s = Bunny::SSLSocket.new(socket,
|
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)
|
284
|
-
ctx.key = OpenSSL::PKey::RSA.new(@tls_key)
|
285
|
-
ctx.cert_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
|
data/lib/bunny/version.rb
CHANGED
@@ -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 "
|
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
|