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.
@@ -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