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