bunny 0.9.0.pre7 → 0.9.0.pre8
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.
- data/ChangeLog.md +22 -0
- data/Gemfile +2 -2
- data/README.md +2 -2
- data/examples/connection/disabled_automatic_recovery.rb +34 -0
- data/lib/bunny.rb +8 -0
- data/lib/bunny/channel.rb +16 -13
- data/lib/bunny/channel_id_allocator.rb +16 -13
- data/lib/bunny/exceptions.rb +45 -33
- data/lib/bunny/main_loop.rb +27 -12
- data/lib/bunny/queue.rb +1 -1
- data/lib/bunny/session.rb +54 -10
- data/lib/bunny/socket.rb +2 -2
- data/lib/bunny/ssl_socket.rb +33 -0
- data/lib/bunny/transport.rb +124 -40
- data/lib/bunny/version.rb +1 -1
- data/spec/higher_level_api/integration/basic_consume_spec.rb +39 -0
- data/spec/higher_level_api/integration/consistent_hash_exchange_spec.rb +51 -0
- data/spec/higher_level_api/integration/publishing_edge_cases_spec.rb +3 -3
- data/spec/issues/issue100_spec.rb +40 -0
- data/spec/issues/issue97_spec.rb +13 -11
- data/spec/stress/channel_open_stress_spec.rb +49 -0
- data/spec/stress/concurrent_consumers_stress_spec.rb +66 -0
- data/spec/stress/concurrent_publishers_stress_spec.rb +58 -0
- data/spec/unit/concurrent/condition_spec.rb +5 -0
- metadata +14 -4
- data/spec/higher_level_api/integration/channel_open_stress_spec.rb +0 -22
data/lib/bunny/socket.rb
CHANGED
@@ -4,7 +4,7 @@ module Bunny
|
|
4
4
|
# TCP socket extension that uses TCP_NODELAY and supports reading
|
5
5
|
# fully.
|
6
6
|
#
|
7
|
-
# Heavily inspired by Dalli
|
7
|
+
# Heavily inspired by Dalli by Mike Perham.
|
8
8
|
class Socket < TCPSocket
|
9
9
|
attr_accessor :options
|
10
10
|
|
@@ -32,7 +32,7 @@ module Bunny
|
|
32
32
|
rescue EOFError
|
33
33
|
@eof = true
|
34
34
|
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
35
|
-
if IO.select([self], nil, nil,
|
35
|
+
if IO.select([self], nil, nil, timeout)
|
36
36
|
retry
|
37
37
|
else
|
38
38
|
raise Timeout::Error, "IO timeout when reading #{count} bytes"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "socket"
|
2
|
+
|
3
|
+
module Bunny
|
4
|
+
begin
|
5
|
+
require "openssl"
|
6
|
+
|
7
|
+
class SSLSocket < OpenSSL::SSL::SSLSocket
|
8
|
+
def read_fully(count, timeout = nil)
|
9
|
+
return nil if @eof
|
10
|
+
|
11
|
+
value = ''
|
12
|
+
begin
|
13
|
+
loop do
|
14
|
+
value << read_nonblock(count - value.bytesize)
|
15
|
+
break if value.bytesize >= count
|
16
|
+
end
|
17
|
+
rescue EOFError
|
18
|
+
@eof = true
|
19
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, OpenSSL::SSL::SSLError => e
|
20
|
+
puts e.inspect
|
21
|
+
if IO.select([self], nil, nil, timeout)
|
22
|
+
retry
|
23
|
+
else
|
24
|
+
raise Timeout::Error, "IO timeout when reading #{count} bytes"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
rescue LoadError => le
|
31
|
+
puts "Could not load OpenSSL"
|
32
|
+
end
|
33
|
+
end
|
data/lib/bunny/transport.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
require "socket"
|
2
2
|
require "thread"
|
3
3
|
|
4
|
+
begin
|
5
|
+
require "openssl"
|
6
|
+
rescue LoadError => le
|
7
|
+
puts "Could not load OpenSSL"
|
8
|
+
end
|
9
|
+
|
4
10
|
require "bunny/exceptions"
|
5
11
|
require "bunny/socket"
|
6
12
|
|
@@ -12,32 +18,35 @@ module Bunny
|
|
12
18
|
#
|
13
19
|
|
14
20
|
DEFAULT_CONNECTION_TIMEOUT = 5.0
|
21
|
+
# same as in RabbitMQ Java client
|
22
|
+
DEFAULT_TLS_PROTOCOL = "SSLv3"
|
15
23
|
|
16
24
|
|
17
25
|
attr_reader :session, :host, :port, :socket, :connect_timeout, :read_write_timeout, :disconnect_timeout
|
18
26
|
|
19
27
|
def initialize(session, host, port, opts)
|
20
|
-
@session
|
28
|
+
@session = session
|
29
|
+
@session_thread = opts[:session_thread]
|
21
30
|
@host = host
|
22
31
|
@port = port
|
23
32
|
@opts = opts
|
24
33
|
|
25
|
-
@
|
26
|
-
@
|
27
|
-
@
|
28
|
-
@
|
29
|
-
@
|
30
|
-
@
|
34
|
+
@tls_enabled = tls_enabled?(opts)
|
35
|
+
@tls_certificate_path = tls_certificate_path_from(opts)
|
36
|
+
@tls_key_path = tls_key_path_from(opts)
|
37
|
+
@tls_certificate = opts[:tls_certificate] || opts[:ssl_cert_string]
|
38
|
+
@tls_key = opts[:tls_key] || opts[:ssl_key_string]
|
39
|
+
@tls_certificate_store = opts[:tls_certificate_store]
|
40
|
+
@verify_peer = opts[:verify_ssl] || opts[:verify_peer]
|
31
41
|
|
32
|
-
@read_write_timeout = opts[:socket_timeout] ||
|
42
|
+
@read_write_timeout = opts[:socket_timeout] || 3
|
33
43
|
@read_write_timeout = nil if @read_write_timeout == 0
|
34
44
|
@connect_timeout = self.timeout_from(opts)
|
35
45
|
@connect_timeout = nil if @connect_timeout == 0
|
36
46
|
@disconnect_timeout = @read_write_timeout || @connect_timeout
|
37
47
|
|
38
|
-
@frames = Hash.new { Array.new }
|
39
|
-
|
40
48
|
initialize_socket
|
49
|
+
connect
|
41
50
|
end
|
42
51
|
|
43
52
|
|
@@ -46,46 +55,78 @@ module Bunny
|
|
46
55
|
end
|
47
56
|
|
48
57
|
def uses_tls?
|
49
|
-
@
|
58
|
+
@tls_enabled
|
50
59
|
end
|
51
60
|
alias tls? uses_tls?
|
52
61
|
|
53
62
|
def uses_ssl?
|
54
|
-
@
|
63
|
+
@tls_enabled
|
55
64
|
end
|
56
65
|
alias ssl? uses_ssl?
|
57
66
|
|
58
67
|
|
68
|
+
def connect
|
69
|
+
if uses_ssl?
|
70
|
+
@socket.connect
|
71
|
+
@socket.post_connection_check(host) if uses_tls? && @verify_peer
|
72
|
+
else
|
73
|
+
# no-op
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
59
77
|
|
60
78
|
# Writes data to the socket. If read/write timeout was specified, Bunny::ClientTimeout will be raised
|
61
79
|
# if the operation times out.
|
62
80
|
#
|
63
81
|
# @raise [ClientTimeout]
|
64
|
-
def write(
|
82
|
+
def write(data)
|
65
83
|
begin
|
66
|
-
raise Bunny::ConnectionError.new("No connection: socket is nil. ", @host, @port) if !@socket
|
67
84
|
if @read_write_timeout
|
68
85
|
Bunny::Timer.timeout(@read_write_timeout, Bunny::ClientTimeout) do
|
69
|
-
|
86
|
+
if open?
|
87
|
+
@socket.write(data)
|
88
|
+
@socket.flush
|
89
|
+
end
|
70
90
|
end
|
71
91
|
else
|
72
|
-
|
92
|
+
if open?
|
93
|
+
@socket.write(data)
|
94
|
+
@socket.flush
|
95
|
+
end
|
73
96
|
end
|
74
|
-
rescue
|
97
|
+
rescue SystemCallError, Bunny::ClientTimeout, Bunny::ConnectionError, IOError => e
|
75
98
|
close
|
76
99
|
|
77
|
-
@session.
|
100
|
+
if @session.automatically_recover?
|
101
|
+
@session.handle_network_failure(e)
|
102
|
+
else
|
103
|
+
@session_thread.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
|
104
|
+
end
|
78
105
|
end
|
79
106
|
end
|
80
107
|
alias send_raw write
|
81
108
|
|
109
|
+
# Writes data to the socket without timeout checks
|
110
|
+
def write_without_timeout(data)
|
111
|
+
begin
|
112
|
+
@socket.write(data)
|
113
|
+
rescue SystemCallError, Bunny::ConnectionError, IOError => e
|
114
|
+
close
|
115
|
+
|
116
|
+
if @session.automatically_recover?
|
117
|
+
@session.handle_network_failure(e)
|
118
|
+
else
|
119
|
+
@session_thread.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
82
124
|
def close(reason = nil)
|
83
|
-
@socket.close
|
84
|
-
@socket = nil
|
125
|
+
@socket.close unless @socket.closed?
|
85
126
|
end
|
86
127
|
|
87
128
|
def open?
|
88
|
-
!@socket.
|
129
|
+
!@socket.closed?
|
89
130
|
end
|
90
131
|
|
91
132
|
def closed?
|
@@ -139,6 +180,20 @@ module Bunny
|
|
139
180
|
end
|
140
181
|
end
|
141
182
|
|
183
|
+
# Sends frame to the peer without timeout control.
|
184
|
+
#
|
185
|
+
# @raise [ConnectionClosedError]
|
186
|
+
# @private
|
187
|
+
def send_frame_without_timeout(frame)
|
188
|
+
if closed?
|
189
|
+
@session.handle_network_failure(ConnectionClosedError.new(frame))
|
190
|
+
else
|
191
|
+
frame.encode_to_array.each do |component|
|
192
|
+
write_without_timeout(component)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
142
197
|
|
143
198
|
def self.reacheable?(host, port, timeout)
|
144
199
|
begin
|
@@ -160,24 +215,31 @@ module Bunny
|
|
160
215
|
|
161
216
|
protected
|
162
217
|
|
218
|
+
def tls_enabled?(opts)
|
219
|
+
opts[:tls] || opts[:ssl] || (opts[:port] == AMQ::Protocol::TLS_PORT) || false
|
220
|
+
end
|
221
|
+
|
222
|
+
def tls_certificate_path_from(opts)
|
223
|
+
opts[:tls_cert] || opts[:ssl_cert] || opts[:tls_cert_path] || opts[:ssl_cert_path] || opts[:tls_certificate_path] || opts[:ssl_certificate_path]
|
224
|
+
end
|
225
|
+
|
226
|
+
def tls_key_path_from(opts)
|
227
|
+
opts[:tls_key] || opts[:ssl_key] || opts[:tls_key_path] || opts[:ssl_key_path]
|
228
|
+
end
|
229
|
+
|
163
230
|
def initialize_socket
|
164
231
|
begin
|
165
|
-
|
232
|
+
s = Bunny::Timer.timeout(@connect_timeout, ConnectionTimeout) do
|
166
233
|
Bunny::Socket.open(@host, @port,
|
167
234
|
:keepalive => @opts[:keepalive],
|
168
235
|
:socket_timeout => @connect_timeout)
|
169
236
|
end
|
170
237
|
|
171
|
-
if
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
@socket.sync_close = true
|
177
|
-
@socket.connect
|
178
|
-
@socket.post_connection_check(host) if @verify_ssl
|
179
|
-
@socket
|
180
|
-
end
|
238
|
+
@socket = if uses_tls?
|
239
|
+
wrap_in_tls_socket(s)
|
240
|
+
else
|
241
|
+
s
|
242
|
+
end
|
181
243
|
rescue StandardError, ConnectionTimeout => e
|
182
244
|
@status = :not_connected
|
183
245
|
raise Bunny::TCPConnectionFailed.new(e, self.hostname, self.port)
|
@@ -186,17 +248,39 @@ module Bunny
|
|
186
248
|
@socket
|
187
249
|
end
|
188
250
|
|
189
|
-
def
|
190
|
-
|
191
|
-
|
251
|
+
def wrap_in_tls_socket(socket)
|
252
|
+
read_tls_keys!
|
253
|
+
|
254
|
+
ctx = initialize_tls_context(OpenSSL::SSL::SSLContext.new(@opts.fetch(:tls_protocol, DEFAULT_TLS_PROTOCOL)))
|
255
|
+
|
256
|
+
s = Bunny::SSLSocket.new(socket, ctx)
|
257
|
+
s.sync_close = true
|
258
|
+
s
|
259
|
+
end
|
260
|
+
|
261
|
+
def check_local_path!(s)
|
262
|
+
raise ArgumentError, "cannot read TLS certificate or key from #{s}" unless File.file?(s) && File.readable?(s)
|
263
|
+
end
|
264
|
+
|
265
|
+
def read_tls_keys!
|
266
|
+
if @tls_certificate_path
|
267
|
+
check_local_path!(@tls_certificate_path)
|
268
|
+
@tls_certificate = File.read(@tls_certificate_path)
|
192
269
|
end
|
193
|
-
if @
|
194
|
-
|
270
|
+
if @tls_key_path
|
271
|
+
check_local_path!(@tls_key_path)
|
272
|
+
@tls_key = File.read(@tls_key_path)
|
195
273
|
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def initialize_tls_context(ctx)
|
277
|
+
ctx.cert = OpenSSL::X509::Certificate.new(@tls_certificate) if @tls_certificate
|
278
|
+
ctx.key = OpenSSL::PKey::RSA.new(@tls_key) if @tls_key
|
279
|
+
ctx.cert_store = @tls_certificate_store if @tls_certificate_store
|
280
|
+
|
281
|
+
ctx.set_params(:verify_mode => OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT) if @verify_peer
|
196
282
|
|
197
|
-
|
198
|
-
sslctx.key = OpenSSL::PKey::RSA.new(@ssl_key_string) if @ssl_key_string
|
199
|
-
sslctx
|
283
|
+
ctx
|
200
284
|
end
|
201
285
|
|
202
286
|
def timeout_from(options)
|
data/lib/bunny/version.rb
CHANGED
@@ -76,4 +76,43 @@ describe Bunny::Queue, "#subscribe" do
|
|
76
76
|
ch.close
|
77
77
|
end
|
78
78
|
end
|
79
|
+
|
80
|
+
20.times do |i|
|
81
|
+
context "with a queue that already has messages (take #{i})" do
|
82
|
+
let(:queue_name) { "bunny.basic_consume#{rand}" }
|
83
|
+
|
84
|
+
it "registers the consumer" do
|
85
|
+
delivered_keys = []
|
86
|
+
delivered_data = []
|
87
|
+
|
88
|
+
ch = connection.create_channel
|
89
|
+
q = ch.queue(queue_name, :auto_delete => true, :durable => false)
|
90
|
+
x = ch.default_exchange
|
91
|
+
100.times do
|
92
|
+
x.publish("hello", :routing_key => queue_name)
|
93
|
+
end
|
94
|
+
|
95
|
+
sleep 0.7
|
96
|
+
q.message_count.should be > 50
|
97
|
+
|
98
|
+
t = Thread.new do
|
99
|
+
ch = connection.create_channel
|
100
|
+
q = ch.queue(queue_name, :auto_delete => true, :durable => false)
|
101
|
+
q.subscribe(:exclusive => false, :manual_ack => false) do |delivery_info, properties, payload|
|
102
|
+
delivered_keys << delivery_info.routing_key
|
103
|
+
delivered_data << payload
|
104
|
+
end
|
105
|
+
end
|
106
|
+
t.abort_on_exception = true
|
107
|
+
sleep 0.5
|
108
|
+
|
109
|
+
delivered_keys.should include(queue_name)
|
110
|
+
delivered_data.should include("hello")
|
111
|
+
|
112
|
+
ch.queue(queue_name, :auto_delete => true, :durable => false).message_count.should == 0
|
113
|
+
|
114
|
+
ch.close
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
79
118
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
unless ENV["CI"]
|
5
|
+
describe "x-consistent-hash exchanges" do
|
6
|
+
let(:connection) do
|
7
|
+
c = Bunny.new(:user => "bunny_gem", :password => "bunny_password", :vhost => "bunny_testbed")
|
8
|
+
c.start
|
9
|
+
c
|
10
|
+
end
|
11
|
+
|
12
|
+
after :all do
|
13
|
+
connection.close
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:list) { Range.new(0, 30).to_a.map(&:to_s) }
|
17
|
+
|
18
|
+
let(:n) { 20 }
|
19
|
+
let(:m) { 10_000 }
|
20
|
+
|
21
|
+
it "can be used" do
|
22
|
+
ch = connection.create_channel
|
23
|
+
body = "сообщение"
|
24
|
+
# requires the consistent hash exchange plugin,
|
25
|
+
# enable it with
|
26
|
+
#
|
27
|
+
# $ [sudo] rabbitmq-plugins enable rabbitmq_consistent_hash_exchange
|
28
|
+
x = ch.exchange("bunny.stress.concurrent.consumers", :type => "x-consistent-hash", :durable => true)
|
29
|
+
|
30
|
+
qs = []
|
31
|
+
|
32
|
+
q1 = ch.queue("", :exclusive => true).bind(x, :routing_key => "15")
|
33
|
+
q2 = ch.queue("", :exclusive => true).bind(x, :routing_key => "15")
|
34
|
+
|
35
|
+
sleep 1.0
|
36
|
+
|
37
|
+
5.times do |i|
|
38
|
+
m.times do
|
39
|
+
x.publish(body, :routing_key => list.sample)
|
40
|
+
end
|
41
|
+
puts "Published #{(i + 1) * m} messages..."
|
42
|
+
end
|
43
|
+
|
44
|
+
sleep 4.0
|
45
|
+
q1.message_count.should be > 1000
|
46
|
+
q2.message_count.should be > 1000
|
47
|
+
|
48
|
+
ch.close
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -44,9 +44,9 @@ describe "Message framing implementation" do
|
|
44
44
|
q = ch.queue("", :exclusive => true)
|
45
45
|
x = ch.default_exchange
|
46
46
|
|
47
|
-
x.publish("", :routing_key => q.name, :persistent => true)
|
47
|
+
x.publish("", :routing_key => q.name, :persistent => false, :mandatory => true)
|
48
48
|
|
49
|
-
sleep(
|
49
|
+
sleep(0.5)
|
50
50
|
q.message_count.should == 1
|
51
51
|
|
52
52
|
envelope, headers, payload = q.pop
|
@@ -54,7 +54,7 @@ describe "Message framing implementation" do
|
|
54
54
|
payload.should == ""
|
55
55
|
|
56
56
|
headers[:content_type].should == "application/octet-stream"
|
57
|
-
headers[:delivery_mode].should ==
|
57
|
+
headers[:delivery_mode].should == 1
|
58
58
|
headers[:priority].should == 0
|
59
59
|
|
60
60
|
ch.close
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Bunny::Channel, "#basic_publish" do
|
4
|
+
let(:connection) do
|
5
|
+
c = Bunny.new(:user => "bunny_gem",
|
6
|
+
:password => "bunny_password",
|
7
|
+
:vhost => "bunny_testbed",
|
8
|
+
:socket_timeout => 0)
|
9
|
+
c.start
|
10
|
+
c
|
11
|
+
end
|
12
|
+
|
13
|
+
after :all do
|
14
|
+
connection.close if connection.open?
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
context "when publishing thousands of messages" do
|
19
|
+
let(:n) { 10_000 }
|
20
|
+
let(:m) { 10 }
|
21
|
+
|
22
|
+
it "successfully publishers them all" do
|
23
|
+
ch = connection.create_channel
|
24
|
+
|
25
|
+
q = ch.queue("", :exclusive => true)
|
26
|
+
x = ch.default_exchange
|
27
|
+
|
28
|
+
body = "x" * 1024
|
29
|
+
m.times do |i|
|
30
|
+
n.times do
|
31
|
+
x.publish(body, :routing_key => q.name)
|
32
|
+
end
|
33
|
+
puts "Published #{i * n} messages so far..."
|
34
|
+
end
|
35
|
+
|
36
|
+
q.purge
|
37
|
+
ch.close
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|