bunny 0.9.0.pre10 → 0.9.0.pre11
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 +7 -0
- data/.ruby-version +1 -1
- data/.travis.yml +4 -7
- data/ChangeLog.md +79 -0
- data/Gemfile +3 -1
- data/README.md +1 -1
- data/benchmarks/basic_publish/with_128K_messages.rb +35 -0
- data/benchmarks/basic_publish/with_1k_messages.rb +35 -0
- data/benchmarks/basic_publish/with_4K_messages.rb +35 -0
- data/benchmarks/basic_publish/with_64K_messages.rb +35 -0
- data/benchmarks/channel_open.rb +28 -0
- data/benchmarks/queue_declare.rb +29 -0
- data/benchmarks/queue_declare_and_bind.rb +29 -0
- data/benchmarks/queue_declare_bind_and_delete.rb +29 -0
- data/benchmarks/write_vs_write_nonblock.rb +49 -0
- data/bunny.gemspec +3 -3
- data/lib/bunny.rb +2 -0
- data/lib/bunny/channel.rb +31 -35
- data/lib/bunny/concurrent/continuation_queue.rb +10 -0
- data/lib/bunny/concurrent/linked_continuation_queue.rb +4 -2
- data/lib/bunny/exceptions.rb +5 -2
- data/lib/bunny/heartbeat_sender.rb +6 -4
- data/lib/bunny/queue.rb +3 -0
- data/lib/bunny/{main_loop.rb → reader_loop.rb} +5 -8
- data/lib/bunny/session.rb +93 -48
- data/lib/bunny/socket.rb +37 -3
- data/lib/bunny/test_kit.rb +26 -0
- data/lib/bunny/transport.rb +39 -33
- data/lib/bunny/version.rb +1 -1
- data/profiling/basic_publish/with_4K_messages.rb +33 -0
- data/spec/higher_level_api/integration/consistent_hash_exchange_spec.rb +10 -11
- data/spec/higher_level_api/integration/queue_declare_spec.rb +55 -13
- data/spec/issues/issue100_spec.rb +29 -27
- data/spec/issues/issue78_spec.rb +54 -52
- data/spec/stress/channel_open_stress_with_single_threaded_connection_spec.rb +0 -22
- data/spec/stress/concurrent_consumers_stress_spec.rb +2 -1
- data/spec/stress/concurrent_publishers_stress_spec.rb +7 -10
- data/spec/stress/long_running_consumer_spec.rb +83 -0
- data/spec/unit/concurrent/condition_spec.rb +7 -5
- data/spec/unit/concurrent/linked_continuation_queue_spec.rb +35 -0
- metadata +48 -44
data/lib/bunny/socket.rb
CHANGED
@@ -20,8 +20,15 @@ module Bunny
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
# Reads given number of bytes with an optional timeout
|
24
|
+
#
|
25
|
+
# @param [Integer] count How many bytes to read
|
26
|
+
# @param [Integer] timeout Timeout
|
27
|
+
#
|
28
|
+
# @return [String] Data read from the socket
|
29
|
+
# @api public
|
23
30
|
def read_fully(count, timeout = nil)
|
24
|
-
return nil if @
|
31
|
+
return nil if @__bunny_socket_eof_flag__
|
25
32
|
|
26
33
|
value = ''
|
27
34
|
begin
|
@@ -30,8 +37,9 @@ module Bunny
|
|
30
37
|
break if value.bytesize >= count
|
31
38
|
end
|
32
39
|
rescue EOFError
|
33
|
-
@eof
|
34
|
-
|
40
|
+
# @eof will break Rubinius' TCPSocket implementation. MK.
|
41
|
+
@__bunny_socket_eof_flag__ = true
|
42
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
|
35
43
|
if IO.select([self], nil, nil, timeout)
|
36
44
|
retry
|
37
45
|
else
|
@@ -39,6 +47,32 @@ module Bunny
|
|
39
47
|
end
|
40
48
|
end
|
41
49
|
value
|
50
|
+
end # read_fully
|
51
|
+
|
52
|
+
# Writes provided data using IO#write_nonblock, taking care of handling
|
53
|
+
# of exceptions it raises when writing would fail (e.g. due to socket buffer
|
54
|
+
# being full).
|
55
|
+
#
|
56
|
+
# IMPORTANT: this method will mutate (slice) the argument. Pass in duplicates
|
57
|
+
# if this is not appropriate in your case.
|
58
|
+
#
|
59
|
+
# @param [String] data Data to write
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
def write_nonblock_fully(data)
|
63
|
+
return nil if @__bunny_socket_eof_flag__
|
64
|
+
|
65
|
+
begin
|
66
|
+
while !data.empty?
|
67
|
+
written = self.write_nonblock(data)
|
68
|
+
data.slice!(0, written)
|
69
|
+
end
|
70
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN
|
71
|
+
IO.select([], [self])
|
72
|
+
retry
|
73
|
+
end
|
74
|
+
|
75
|
+
data.bytesize
|
42
76
|
end
|
43
77
|
end
|
44
78
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
module Bunny
|
3
|
+
# Unit, integration and stress testing toolkit
|
4
|
+
class TestKit
|
5
|
+
class << self
|
6
|
+
|
7
|
+
# @return [Integer] Random integer in the range of [a, b]
|
8
|
+
# @api private
|
9
|
+
def random_in_range(a, b)
|
10
|
+
Range.new(a, b).to_a.sample
|
11
|
+
end
|
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
|
16
|
+
# @return [String] Message payload of length in the given range, with non-ASCII characters
|
17
|
+
def message_in_kb(a, b, i)
|
18
|
+
s = "Ю#{i}"
|
19
|
+
n = random_in_range(a, b) / s.bytesize
|
20
|
+
|
21
|
+
s * n * 1024
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/bunny/transport.rb
CHANGED
@@ -45,6 +45,8 @@ module Bunny
|
|
45
45
|
@connect_timeout = nil if @connect_timeout == 0
|
46
46
|
@disconnect_timeout = @read_write_timeout || @connect_timeout
|
47
47
|
|
48
|
+
@writes_mutex = Mutex.new
|
49
|
+
|
48
50
|
initialize_socket
|
49
51
|
connect
|
50
52
|
end
|
@@ -84,13 +86,13 @@ module Bunny
|
|
84
86
|
if @read_write_timeout
|
85
87
|
Bunny::Timer.timeout(@read_write_timeout, Bunny::ClientTimeout) do
|
86
88
|
if open?
|
87
|
-
@socket.write(data)
|
89
|
+
@writes_mutex.synchronize { @socket.write(data) }
|
88
90
|
@socket.flush
|
89
91
|
end
|
90
92
|
end
|
91
93
|
else
|
92
94
|
if open?
|
93
|
-
@socket.write(data)
|
95
|
+
@writes_mutex.synchronize { @socket.write(data) }
|
94
96
|
@socket.flush
|
95
97
|
end
|
96
98
|
end
|
@@ -104,12 +106,12 @@ module Bunny
|
|
104
106
|
end
|
105
107
|
end
|
106
108
|
end
|
107
|
-
alias send_raw write
|
108
109
|
|
109
110
|
# Writes data to the socket without timeout checks
|
110
111
|
def write_without_timeout(data)
|
111
112
|
begin
|
112
|
-
@socket.write(data)
|
113
|
+
@writes_mutex.synchronize { @socket.write(data) }
|
114
|
+
@socket.flush
|
113
115
|
rescue SystemCallError, Bunny::ConnectionError, IOError => e
|
114
116
|
close
|
115
117
|
|
@@ -121,6 +123,31 @@ module Bunny
|
|
121
123
|
end
|
122
124
|
end
|
123
125
|
|
126
|
+
# Sends frame to the peer.
|
127
|
+
#
|
128
|
+
# @raise [ConnectionClosedError]
|
129
|
+
# @private
|
130
|
+
def send_frame(frame)
|
131
|
+
if closed?
|
132
|
+
@session.handle_network_failure(ConnectionClosedError.new(frame))
|
133
|
+
else
|
134
|
+
write(frame.encode)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Sends frame to the peer without timeout control.
|
139
|
+
#
|
140
|
+
# @raise [ConnectionClosedError]
|
141
|
+
# @private
|
142
|
+
def send_frame_without_timeout(frame)
|
143
|
+
if closed?
|
144
|
+
@session.handle_network_failure(ConnectionClosedError.new(frame))
|
145
|
+
else
|
146
|
+
write_without_timeout(frame.encode)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
|
124
151
|
def close(reason = nil)
|
125
152
|
@socket.close unless @socket.closed?
|
126
153
|
end
|
@@ -151,6 +178,14 @@ module Bunny
|
|
151
178
|
# @private
|
152
179
|
def read_next_frame(opts = {})
|
153
180
|
header = @socket.read_fully(7)
|
181
|
+
# TODO: network issues here will sometimes cause
|
182
|
+
# the socket method return an empty string. We need to log
|
183
|
+
# and handle this better.
|
184
|
+
# type, channel, size = begin
|
185
|
+
# AMQ::Protocol::Frame.decode_header(header)
|
186
|
+
# rescue AMQ::Protocol::EmptyResponseError => e
|
187
|
+
# puts "Got AMQ::Protocol::EmptyResponseError, header is #{header.inspect}"
|
188
|
+
# end
|
154
189
|
type, channel, size = AMQ::Protocol::Frame.decode_header(header)
|
155
190
|
payload = @socket.read_fully(size)
|
156
191
|
frame_end = @socket.read_fully(1)
|
@@ -166,35 +201,6 @@ module Bunny
|
|
166
201
|
end
|
167
202
|
|
168
203
|
|
169
|
-
# Sends frame to the peer.
|
170
|
-
#
|
171
|
-
# @raise [ConnectionClosedError]
|
172
|
-
# @private
|
173
|
-
def send_frame(frame)
|
174
|
-
if closed?
|
175
|
-
@session.handle_network_failure(ConnectionClosedError.new(frame))
|
176
|
-
else
|
177
|
-
frame.encode_to_array.each do |component|
|
178
|
-
send_raw(component)
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
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
|
-
|
197
|
-
|
198
204
|
def self.reacheable?(host, port, timeout)
|
199
205
|
begin
|
200
206
|
s = Bunny::Socket.open(host, port,
|
data/lib/bunny/version.rb
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
require "rubygems"
|
5
|
+
require "bunny"
|
6
|
+
require "ruby-prof"
|
7
|
+
|
8
|
+
conn = Bunny.new
|
9
|
+
conn.start
|
10
|
+
|
11
|
+
puts
|
12
|
+
puts "-" * 80
|
13
|
+
puts "Benchmarking on #{RUBY_DESCRIPTION}"
|
14
|
+
|
15
|
+
n = 50_000
|
16
|
+
ch = conn.create_channel
|
17
|
+
x = ch.default_exchange
|
18
|
+
s = "z" * 4096
|
19
|
+
|
20
|
+
# warm up the JIT, etc
|
21
|
+
puts "Doing a warmup run..."
|
22
|
+
16000.times { x.publish(s, :routing_key => "anything") }
|
23
|
+
|
24
|
+
# give OS, the server and so on some time to catch
|
25
|
+
# up
|
26
|
+
sleep 2.0
|
27
|
+
|
28
|
+
result = RubyProf.profile do
|
29
|
+
n.times { x.publish(s, :routing_key => "anything") }
|
30
|
+
end
|
31
|
+
|
32
|
+
printer = RubyProf::FlatPrinter.new(result)
|
33
|
+
printer.print(STDOUT, {})
|
@@ -2,7 +2,7 @@
|
|
2
2
|
require "spec_helper"
|
3
3
|
|
4
4
|
unless ENV["CI"]
|
5
|
-
describe "x-consistent-hash
|
5
|
+
describe "x-consistent-hash exchange" do
|
6
6
|
let(:connection) do
|
7
7
|
c = Bunny.new(:user => "bunny_gem", :password => "bunny_password", :vhost => "bunny_testbed")
|
8
8
|
c.start
|
@@ -13,12 +13,11 @@ unless ENV["CI"]
|
|
13
13
|
connection.close
|
14
14
|
end
|
15
15
|
|
16
|
-
let(:list) { Range.new(0,
|
16
|
+
let(:list) { Range.new(0, 6).to_a.map(&:to_s) }
|
17
17
|
|
18
|
-
let(:
|
19
|
-
let(:m) { 10_000 }
|
18
|
+
let(:m) { 1500 }
|
20
19
|
|
21
|
-
it "
|
20
|
+
it "distributes messages between queues bound with the same routing key" do
|
22
21
|
ch = connection.create_channel
|
23
22
|
body = "сообщение"
|
24
23
|
# requires the consistent hash exchange plugin,
|
@@ -29,8 +28,8 @@ unless ENV["CI"]
|
|
29
28
|
|
30
29
|
qs = []
|
31
30
|
|
32
|
-
q1 = ch.queue("", :exclusive => true).bind(x, :routing_key => "
|
33
|
-
q2 = ch.queue("", :exclusive => true).bind(x, :routing_key => "
|
31
|
+
q1 = ch.queue("", :exclusive => true).bind(x, :routing_key => "5")
|
32
|
+
q2 = ch.queue("", :exclusive => true).bind(x, :routing_key => "5")
|
34
33
|
|
35
34
|
sleep 1.0
|
36
35
|
|
@@ -38,12 +37,12 @@ unless ENV["CI"]
|
|
38
37
|
m.times do
|
39
38
|
x.publish(body, :routing_key => list.sample)
|
40
39
|
end
|
41
|
-
puts "Published #{(i + 1) * m} messages..."
|
40
|
+
puts "Published #{(i + 1) * m} tiny messages..."
|
42
41
|
end
|
43
42
|
|
44
|
-
sleep
|
45
|
-
q1.message_count.should be >
|
46
|
-
q2.message_count.should be >
|
43
|
+
sleep 2.0
|
44
|
+
q1.message_count.should be > 100
|
45
|
+
q2.message_count.should be > 100
|
47
46
|
|
48
47
|
ch.close
|
49
48
|
end
|
@@ -96,34 +96,76 @@ describe Bunny::Queue do
|
|
96
96
|
end
|
97
97
|
|
98
98
|
|
99
|
-
|
99
|
+
|
100
|
+
context "when queue is declared with a different set of attributes" do
|
101
|
+
it "raises an exception" do
|
102
|
+
ch = connection.create_channel
|
103
|
+
|
104
|
+
q = ch.queue("bunny.tests.queues.auto-delete", :auto_delete => true, :durable => false)
|
105
|
+
expect {
|
106
|
+
# force re-declaration
|
107
|
+
ch.queue_declare("bunny.tests.queues.auto-delete", :auto_delete => false, :durable => true)
|
108
|
+
}.to raise_error(Bunny::PreconditionFailed)
|
109
|
+
|
110
|
+
ch.should be_closed
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
context "when queue is declared with message TTL" do
|
100
116
|
let(:args) do
|
117
|
+
# in ms
|
101
118
|
{"x-message-ttl" => 1000}
|
102
119
|
end
|
103
120
|
|
104
|
-
it "
|
121
|
+
it "causes all messages in it to have a TTL" do
|
105
122
|
ch = connection.create_channel
|
106
123
|
|
107
|
-
q = ch.queue("bunny.tests.queues.with-arguments", :arguments => args)
|
124
|
+
q = ch.queue("bunny.tests.queues.with-arguments.ttl", :arguments => args, :exclusive => true)
|
108
125
|
q.arguments.should == args
|
109
|
-
|
126
|
+
|
127
|
+
q.publish("xyzzy")
|
128
|
+
sleep 0.1
|
129
|
+
|
130
|
+
q.message_count.should == 1
|
131
|
+
sleep 1.5
|
132
|
+
q.message_count.should == 0
|
110
133
|
|
111
134
|
ch.close
|
112
135
|
end
|
113
136
|
end
|
114
137
|
|
115
138
|
|
116
|
-
|
117
|
-
|
118
|
-
|
139
|
+
unless ENV["CI"]
|
140
|
+
# requires RabbitMQ 3.1+
|
141
|
+
context "when queue is declared with bounded length" do
|
142
|
+
let(:n) { 10 }
|
143
|
+
let(:args) do
|
144
|
+
# in ms
|
145
|
+
{"x-max-length" => n}
|
146
|
+
end
|
119
147
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
ch.queue_declare("bunny.tests.queues.auto-delete", :auto_delete => false, :durable => true)
|
124
|
-
}.to raise_error(Bunny::PreconditionFailed)
|
148
|
+
# see http://www.rabbitmq.com/maxlength.html for more info
|
149
|
+
it "causes the queue to be bounded" do
|
150
|
+
ch = connection.create_channel
|
125
151
|
|
126
|
-
|
152
|
+
q = ch.queue("bunny.tests.queues.with-arguments.max-length", :arguments => args, :exclusive => true)
|
153
|
+
q.arguments.should == args
|
154
|
+
|
155
|
+
(n * 10).times do
|
156
|
+
q.publish("xyzzy")
|
157
|
+
end
|
158
|
+
|
159
|
+
q.message_count.should == n
|
160
|
+
(n * 5).times do
|
161
|
+
q.publish("xyzzy")
|
162
|
+
end
|
163
|
+
|
164
|
+
q.message_count.should == n
|
165
|
+
q.delete
|
166
|
+
|
167
|
+
ch.close
|
168
|
+
end
|
127
169
|
end
|
128
170
|
end
|
129
171
|
end
|
@@ -1,40 +1,42 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
unless ENV["CI"]
|
4
|
+
describe Bunny::Channel, "#basic_publish" do
|
5
|
+
let(:connection) do
|
6
|
+
c = Bunny.new(:user => "bunny_gem",
|
7
|
+
:password => "bunny_password",
|
8
|
+
:vhost => "bunny_testbed",
|
9
|
+
:socket_timeout => 0)
|
10
|
+
c.start
|
11
|
+
c
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
after :all do
|
15
|
+
connection.close if connection.open?
|
16
|
+
end
|
16
17
|
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
context "when publishing thousands of messages" do
|
20
|
+
let(:n) { 2_000 }
|
21
|
+
let(:m) { 10 }
|
21
22
|
|
22
|
-
|
23
|
-
|
23
|
+
it "successfully publishers them all" do
|
24
|
+
ch = connection.create_channel
|
24
25
|
|
25
|
-
|
26
|
-
|
26
|
+
q = ch.queue("", :exclusive => true)
|
27
|
+
x = ch.default_exchange
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
body = "x" * 1024
|
30
|
+
m.times do |i|
|
31
|
+
n.times do
|
32
|
+
x.publish(body, :routing_key => q.name)
|
33
|
+
end
|
34
|
+
puts "Published #{i * n} 1K messages..."
|
32
35
|
end
|
33
|
-
puts "Published #{i * n} messages so far..."
|
34
|
-
end
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
q.purge
|
38
|
+
ch.close
|
39
|
+
end
|
38
40
|
end
|
39
41
|
end
|
40
42
|
end
|