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.
@@ -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::Server::KSocket from Dalli by Mike Perham.
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, options.fetch(:socket_timeout, timeout))
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
@@ -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 = 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
- @ssl = opts[:ssl] || false
26
- @ssl_cert = opts[:ssl_cert]
27
- @ssl_key = opts[:ssl_key]
28
- @ssl_cert_string = opts[:ssl_cert_string]
29
- @ssl_key_string = opts[:ssl_key_string]
30
- @verify_ssl = opts[:verify_ssl].nil? || opts[:verify_ssl]
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] || 1
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
- @ssl
58
+ @tls_enabled
50
59
  end
51
60
  alias tls? uses_tls?
52
61
 
53
62
  def uses_ssl?
54
- @ssl
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(*args)
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
- @socket.write(*args) if open?
86
+ if open?
87
+ @socket.write(data)
88
+ @socket.flush
89
+ end
70
90
  end
71
91
  else
72
- @socket.write(*args) if open?
92
+ if open?
93
+ @socket.write(data)
94
+ @socket.flush
95
+ end
73
96
  end
74
- rescue Errno::EPIPE, Errno::EAGAIN, Bunny::ClientTimeout, Bunny::ConnectionError, IOError => e
97
+ rescue SystemCallError, Bunny::ClientTimeout, Bunny::ConnectionError, IOError => e
75
98
  close
76
99
 
77
- @session.handle_network_failure(e)
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 if @socket and not @socket.closed?
84
- @socket = nil
125
+ @socket.close unless @socket.closed?
85
126
  end
86
127
 
87
128
  def open?
88
- !@socket.nil? && !@socket.closed?
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
- @socket = Bunny::Timer.timeout(@connect_timeout, ConnectionTimeout) do
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 @ssl
172
- require 'openssl' unless defined? OpenSSL::SSL
173
- sslctx = OpenSSL::SSL::SSLContext.new
174
- initialize_client_pair(sslctx)
175
- @socket = OpenSSL::SSL::SSLSocket.new(@socket, sslctx)
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 initialize_client_pair(sslctx)
190
- if @ssl_cert
191
- @ssl_cert_string = File.read(@ssl_cert)
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 @ssl_key
194
- @ssl_key_string = File.read(@ssl_key)
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
- sslctx.cert = OpenSSL::X509::Certificate.new(@ssl_cert_string) if @ssl_cert_string
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)
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Bunny
4
- VERSION = "0.9.0.pre7"
4
+ VERSION = "0.9.0.pre8"
5
5
  end
@@ -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(1)
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 == 2
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