bunny 0.8.0 → 0.9.0.pre1
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/.gitignore +7 -1
- data/.travis.yml +14 -4
- data/ChangeLog.md +72 -0
- data/Gemfile +17 -11
- data/README.md +82 -0
- data/bunny.gemspec +6 -13
- data/examples/connection/heartbeat.rb +17 -0
- data/lib/bunny.rb +40 -56
- data/lib/bunny/channel.rb +615 -19
- data/lib/bunny/channel_id_allocator.rb +59 -0
- data/lib/bunny/compatibility.rb +24 -0
- data/lib/bunny/concurrent/condition.rb +63 -0
- data/lib/bunny/consumer.rb +42 -26
- data/lib/bunny/consumer_tag_generator.rb +22 -0
- data/lib/bunny/consumer_work_pool.rb +67 -0
- data/lib/bunny/exceptions.rb +128 -0
- data/lib/bunny/exchange.rb +131 -136
- data/lib/bunny/framing.rb +53 -0
- data/lib/bunny/heartbeat_sender.rb +59 -0
- data/lib/bunny/main_loop.rb +70 -0
- data/lib/bunny/message_metadata.rb +126 -0
- data/lib/bunny/queue.rb +102 -275
- data/lib/bunny/session.rb +478 -0
- data/lib/bunny/socket.rb +44 -0
- data/lib/bunny/system_timer.rb +9 -9
- data/lib/bunny/transport.rb +179 -0
- data/lib/bunny/version.rb +1 -1
- data/spec/compatibility/queue_declare_spec.rb +40 -0
- data/spec/higher_level_api/integration/basic_ack_spec.rb +54 -0
- data/spec/higher_level_api/integration/basic_consume_spec.rb +51 -0
- data/spec/higher_level_api/integration/basic_get_spec.rb +47 -0
- data/spec/higher_level_api/integration/basic_nack_spec.rb +39 -0
- data/spec/higher_level_api/integration/basic_publish_spec.rb +105 -0
- data/spec/higher_level_api/integration/basic_qos_spec.rb +32 -0
- data/spec/higher_level_api/integration/basic_recover_spec.rb +18 -0
- data/spec/higher_level_api/integration/basic_reject_spec.rb +53 -0
- data/spec/higher_level_api/integration/basic_return_spec.rb +33 -0
- data/spec/higher_level_api/integration/channel_close_spec.rb +29 -0
- data/spec/higher_level_api/integration/channel_flow_spec.rb +24 -0
- data/spec/higher_level_api/integration/channel_open_spec.rb +57 -0
- data/spec/higher_level_api/integration/channel_open_stress_spec.rb +22 -0
- data/spec/higher_level_api/integration/confirm_select_spec.rb +19 -0
- data/spec/higher_level_api/integration/connection_spec.rb +340 -0
- data/spec/higher_level_api/integration/exchange_bind_spec.rb +31 -0
- data/spec/higher_level_api/integration/exchange_declare_spec.rb +183 -0
- data/spec/higher_level_api/integration/exchange_delete_spec.rb +37 -0
- data/spec/higher_level_api/integration/exchange_unbind_spec.rb +40 -0
- data/spec/higher_level_api/integration/queue_bind_spec.rb +109 -0
- data/spec/higher_level_api/integration/queue_declare_spec.rb +129 -0
- data/spec/higher_level_api/integration/queue_delete_spec.rb +38 -0
- data/spec/higher_level_api/integration/queue_purge_spec.rb +30 -0
- data/spec/higher_level_api/integration/queue_unbind_spec.rb +33 -0
- data/spec/higher_level_api/integration/tx_commit_spec.rb +21 -0
- data/spec/higher_level_api/integration/tx_rollback_spec.rb +21 -0
- data/spec/lower_level_api/integration/basic_cancel_spec.rb +57 -0
- data/spec/lower_level_api/integration/basic_consume_spec.rb +100 -0
- data/spec/spec_helper.rb +64 -0
- data/spec/unit/bunny_spec.rb +15 -0
- data/spec/unit/concurrent/condition_spec.rb +66 -0
- metadata +135 -93
- data/CHANGELOG +0 -21
- data/README.textile +0 -76
- data/Rakefile +0 -14
- data/examples/simple.rb +0 -32
- data/examples/simple_ack.rb +0 -35
- data/examples/simple_consumer.rb +0 -55
- data/examples/simple_fanout.rb +0 -41
- data/examples/simple_headers.rb +0 -42
- data/examples/simple_publisher.rb +0 -29
- data/examples/simple_topic.rb +0 -61
- data/ext/amqp-0.9.1.json +0 -389
- data/ext/config.yml +0 -4
- data/ext/qparser.rb +0 -426
- data/lib/bunny/client.rb +0 -370
- data/lib/bunny/subscription.rb +0 -92
- data/lib/qrack/amq-client-url.rb +0 -165
- data/lib/qrack/channel.rb +0 -20
- data/lib/qrack/client.rb +0 -247
- data/lib/qrack/errors.rb +0 -5
- data/lib/qrack/protocol/protocol.rb +0 -135
- data/lib/qrack/protocol/spec.rb +0 -525
- data/lib/qrack/qrack.rb +0 -20
- data/lib/qrack/queue.rb +0 -40
- data/lib/qrack/subscription.rb +0 -152
- data/lib/qrack/transport/buffer.rb +0 -305
- data/lib/qrack/transport/frame.rb +0 -102
- data/spec/spec_09/amqp_url_spec.rb +0 -19
- data/spec/spec_09/bunny_spec.rb +0 -76
- data/spec/spec_09/connection_spec.rb +0 -34
- data/spec/spec_09/exchange_spec.rb +0 -173
- data/spec/spec_09/queue_spec.rb +0 -240
data/lib/bunny/socket.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require "socket"
|
2
|
+
|
3
|
+
module Bunny
|
4
|
+
# TCP socket extension that uses TCP_NODELAY and supports reading
|
5
|
+
# fully.
|
6
|
+
#
|
7
|
+
# Heavily inspired by Dalli::Server::KSocket from Dalli by Mike Perham.
|
8
|
+
class Socket < TCPSocket
|
9
|
+
attr_accessor :options
|
10
|
+
|
11
|
+
def self.open(host, port, options = {})
|
12
|
+
Timeout.timeout(options[:socket_timeout]) do
|
13
|
+
sock = new(host, port)
|
14
|
+
if Socket.constants.include?('TCP_NODELAY') || Socket.constants.include?(:TCP_NODELAY)
|
15
|
+
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
|
16
|
+
end
|
17
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
|
18
|
+
sock.options = {:host => host, :port => port}.merge(options)
|
19
|
+
sock
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def read_fully(count, timeout = nil)
|
24
|
+
return nil if @eof
|
25
|
+
|
26
|
+
value = ''
|
27
|
+
begin
|
28
|
+
loop do
|
29
|
+
value << read_nonblock(count - value.bytesize)
|
30
|
+
break if value.bytesize >= count
|
31
|
+
end
|
32
|
+
rescue EOFError
|
33
|
+
@eof = true
|
34
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
35
|
+
if IO.select([self], nil, nil, options.fetch(:socket_timeout, timeout))
|
36
|
+
retry
|
37
|
+
else
|
38
|
+
raise Timeout::Error, "IO timeout when reading #{count} bytes"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/bunny/system_timer.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
# encoding: utf-8
|
1
|
+
# -*- encoding: utf-8; mode: ruby -*-
|
2
2
|
|
3
|
-
|
4
|
-
# Used for ruby < 1.9.x
|
5
|
-
class SystemTimer < ::SystemTimer
|
3
|
+
require "system_timer"
|
6
4
|
|
7
|
-
|
8
|
-
|
5
|
+
module Bunny
|
6
|
+
# Used for Ruby before 1.9
|
7
|
+
class SystemTimer
|
8
|
+
def self.timeout(seconds, exception)
|
9
|
+
::SystemTimer.timeout_after(seconds) do
|
9
10
|
yield
|
10
11
|
end
|
11
12
|
end
|
12
|
-
|
13
|
-
|
14
|
-
end
|
13
|
+
end # SystemTimer
|
14
|
+
end # Bunny
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require "socket"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
require "bunny/exceptions"
|
5
|
+
require "bunny/socket"
|
6
|
+
|
7
|
+
module Bunny
|
8
|
+
class Transport
|
9
|
+
|
10
|
+
#
|
11
|
+
# API
|
12
|
+
#
|
13
|
+
|
14
|
+
DEFAULT_CONNECTION_TIMEOUT = 5.0
|
15
|
+
|
16
|
+
|
17
|
+
attr_reader :host, :port, :socket, :connect_timeout
|
18
|
+
|
19
|
+
def initialize(host, port, opts)
|
20
|
+
@host = host
|
21
|
+
@port = port
|
22
|
+
@opts = opts
|
23
|
+
|
24
|
+
@ssl = opts[:ssl] || false
|
25
|
+
@ssl_cert = opts[:ssl_cert]
|
26
|
+
@ssl_key = opts[:ssl_key]
|
27
|
+
@ssl_cert_string = opts[:ssl_cert_string]
|
28
|
+
@ssl_key_string = opts[:ssl_key_string]
|
29
|
+
@verify_ssl = opts[:verify_ssl].nil? || opts[:verify_ssl]
|
30
|
+
|
31
|
+
@read_write_timeout = opts[:socket_timeout] || 1
|
32
|
+
@read_write_timeout = nil if @read_write_timeout == 0
|
33
|
+
@disconnect_timeout = @read_write_timeout || @connect_timeout
|
34
|
+
@connect_timeout = self.timeout_from(opts)
|
35
|
+
|
36
|
+
@frames = Hash.new { Array.new }
|
37
|
+
|
38
|
+
initialize_socket
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def uses_tls?
|
43
|
+
@ssl
|
44
|
+
end
|
45
|
+
alias tls? uses_tls?
|
46
|
+
|
47
|
+
def uses_ssl?
|
48
|
+
@ssl
|
49
|
+
end
|
50
|
+
alias ssl? uses_ssl?
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
# Writes data to the socket. If read/write timeout was specified, Bunny::ClientTimeout will be raised
|
55
|
+
# if the operation times out.
|
56
|
+
#
|
57
|
+
# @raise [ClientTimeout]
|
58
|
+
def write(*args)
|
59
|
+
begin
|
60
|
+
raise Bunny::ConnectionError.new('No connection - socket has not been created', @host, @port) if !@socket
|
61
|
+
if @read_write_timeout
|
62
|
+
Bunny::Timer.timeout(@read_write_timeout, Bunny::ClientTimeout) do
|
63
|
+
@socket.write(*args)
|
64
|
+
end
|
65
|
+
else
|
66
|
+
@socket.write(*args)
|
67
|
+
end
|
68
|
+
rescue Errno::EPIPE, Errno::EAGAIN, Bunny::ClientTimeout, IOError => e
|
69
|
+
close!
|
70
|
+
raise Bunny::ConnectionError, e.message
|
71
|
+
end
|
72
|
+
end
|
73
|
+
alias send_raw write
|
74
|
+
|
75
|
+
def close(reason = nil)
|
76
|
+
@socket.close if @socket and not @socket.closed?
|
77
|
+
@socket = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def open?
|
81
|
+
!@socket.nil? && !@socket.closed?
|
82
|
+
end
|
83
|
+
|
84
|
+
def closed?
|
85
|
+
!open?
|
86
|
+
end
|
87
|
+
|
88
|
+
def flush
|
89
|
+
@socket.flush
|
90
|
+
end
|
91
|
+
|
92
|
+
def read_fully(*args)
|
93
|
+
@socket.read_fully(*args)
|
94
|
+
end
|
95
|
+
|
96
|
+
def read_ready?(timeout = nil)
|
97
|
+
io = IO.select([@socket].compact, nil, nil, timeout)
|
98
|
+
io && io[0].include?(@socket)
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# Exposed primarily for Bunny::Channel
|
103
|
+
# @private
|
104
|
+
def read_next_frame(opts = {})
|
105
|
+
header = @socket.read_fully(7)
|
106
|
+
type, channel, size = AMQ::Protocol::Frame.decode_header(header)
|
107
|
+
payload = @socket.read_fully(size)
|
108
|
+
frame_end = @socket.read_fully(1)
|
109
|
+
|
110
|
+
# 1) the size is miscalculated
|
111
|
+
if payload.bytesize != size
|
112
|
+
raise BadLengthError.new(size, payload.bytesize)
|
113
|
+
end
|
114
|
+
|
115
|
+
# 2) the size is OK, but the string doesn't end with FINAL_OCTET
|
116
|
+
raise NoFinalOctetError.new if frame_end != AMQ::Protocol::Frame::FINAL_OCTET
|
117
|
+
AMQ::Protocol::Frame.new(type, payload, channel)
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
# Sends frame to the peer.
|
122
|
+
#
|
123
|
+
# @raise [ConnectionClosedError]
|
124
|
+
# @private
|
125
|
+
def send_frame(frame)
|
126
|
+
if closed?
|
127
|
+
raise ConnectionClosedError.new(frame)
|
128
|
+
else
|
129
|
+
send_raw(frame.encode)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
protected
|
135
|
+
|
136
|
+
def initialize_socket
|
137
|
+
begin
|
138
|
+
@socket = Bunny::Timer.timeout(@connect_timeout, ConnectionTimeout) do
|
139
|
+
Bunny::Socket.open(@host, @port,
|
140
|
+
:keepalive => @opts[:keepalive],
|
141
|
+
:socket_timeout => @connect_timeout)
|
142
|
+
end
|
143
|
+
|
144
|
+
if @ssl
|
145
|
+
require 'openssl' unless defined? OpenSSL::SSL
|
146
|
+
sslctx = OpenSSL::SSL::SSLContext.new
|
147
|
+
initialize_client_pair(sslctx)
|
148
|
+
@socket = OpenSSL::SSL::SSLSocket.new(@socket, sslctx)
|
149
|
+
@socket.sync_close = true
|
150
|
+
@socket.connect
|
151
|
+
@socket.post_connection_check(host) if @verify_ssl
|
152
|
+
@socket
|
153
|
+
end
|
154
|
+
rescue Exception => e
|
155
|
+
@status = :not_connected
|
156
|
+
raise Bunny::TCPConnectionFailed.new(e, self.hostname, self.port)
|
157
|
+
end
|
158
|
+
|
159
|
+
@socket
|
160
|
+
end
|
161
|
+
|
162
|
+
def initialize_client_pair(sslctx)
|
163
|
+
if @ssl_cert
|
164
|
+
@ssl_cert_string = File.read(@ssl_cert)
|
165
|
+
end
|
166
|
+
if @ssl_key
|
167
|
+
@ssl_key_string = File.read(@ssl_key)
|
168
|
+
end
|
169
|
+
|
170
|
+
sslctx.cert = OpenSSL::X509::Certificate.new(@ssl_cert_string) if @ssl_cert_string
|
171
|
+
sslctx.key = OpenSSL::PKey::RSA.new(@ssl_key_string) if @ssl_key_string
|
172
|
+
sslctx
|
173
|
+
end
|
174
|
+
|
175
|
+
def timeout_from(options)
|
176
|
+
options[:connect_timeout] || options[:connection_timeout] || options[:timeout] || DEFAULT_CONNECTION_TIMEOUT
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/lib/bunny/version.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Bunny::Queue, "backwards compatibility API" do
|
4
|
+
let(:connection) do
|
5
|
+
c = Bunny.new
|
6
|
+
c.start
|
7
|
+
c
|
8
|
+
end
|
9
|
+
|
10
|
+
context "when queue name is specified" do
|
11
|
+
let(:name) { "a queue declared at #{Time.now.to_i}" }
|
12
|
+
|
13
|
+
it "declares a new queue with that name" do
|
14
|
+
q = Bunny::Queue.new(connection, name)
|
15
|
+
q.name.should == name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
context "when queue name is passed on as an empty string" do
|
21
|
+
it "uses server-assigned queue name" do
|
22
|
+
q = Bunny::Queue.new(connection, "")
|
23
|
+
q.name.should_not be_empty
|
24
|
+
q.name.should =~ /^amq.gen.+/
|
25
|
+
q.should be_server_named
|
26
|
+
q.delete
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
context "when queue name is completely omitted" do
|
32
|
+
it "uses server-assigned queue name" do
|
33
|
+
q = Bunny::Queue.new(connection)
|
34
|
+
q.name.should_not be_empty
|
35
|
+
q.name.should =~ /^amq.gen.+/
|
36
|
+
q.should be_server_named
|
37
|
+
q.delete
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Bunny::Channel, "#ack" do
|
4
|
+
let(:connection) do
|
5
|
+
c = Bunny.new(:user => "bunny_gem", :password => "bunny_password", :vhost => "bunny_testbed")
|
6
|
+
c.start
|
7
|
+
c
|
8
|
+
end
|
9
|
+
|
10
|
+
after :all do
|
11
|
+
connection.close if connection.open?
|
12
|
+
end
|
13
|
+
|
14
|
+
subject do
|
15
|
+
connection.create_channel
|
16
|
+
end
|
17
|
+
|
18
|
+
context "with a valid (known) delivery tag" do
|
19
|
+
it "acknowleges a message" do
|
20
|
+
q = subject.queue("bunny.basic.ack.manual-acks", :exclusive => true)
|
21
|
+
x = subject.default_exchange
|
22
|
+
|
23
|
+
x.publish("bunneth", :routing_key => q.name)
|
24
|
+
sleep(0.25)
|
25
|
+
q.message_count.should == 1
|
26
|
+
delivery_details, properties, content = q.pop(:ack => true)
|
27
|
+
|
28
|
+
subject.ack(delivery_details.delivery_tag, true)
|
29
|
+
sleep(0.25)
|
30
|
+
q.message_count.should == 0
|
31
|
+
|
32
|
+
subject.close
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "with an invalid (random) delivery tag" do
|
37
|
+
it "causes a channel-level error" do
|
38
|
+
pending "We need to design async channel error handling for cases when there is no reply methods (e.g. basic.ack)"
|
39
|
+
|
40
|
+
q = subject.queue("bunny.basic.ack.unknown-delivery-tag", :exclusive => true)
|
41
|
+
x = subject.default_exchange
|
42
|
+
|
43
|
+
x.publish("bunneth", :routing_key => q.name)
|
44
|
+
sleep(0.25)
|
45
|
+
q.message_count.should == 1
|
46
|
+
_, _, content = q.pop(:ack => true)
|
47
|
+
|
48
|
+
subject.on_error do |ch, channel_close|
|
49
|
+
@channel_close = channel_close
|
50
|
+
end
|
51
|
+
# subject.ack(82, true)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Bunny::Queue, "#subscribe" do
|
4
|
+
let(:connection) do
|
5
|
+
c = Bunny.new(:user => "bunny_gem", :password => "bunny_password", :vhost => "bunny_testbed")
|
6
|
+
c.start
|
7
|
+
c
|
8
|
+
end
|
9
|
+
|
10
|
+
after :all do
|
11
|
+
connection.close if connection.open?
|
12
|
+
end
|
13
|
+
|
14
|
+
context "with automatic acknowledgement mode" do
|
15
|
+
let(:queue_name) { "bunny.basic_consume#{rand}" }
|
16
|
+
|
17
|
+
it "registers the consumer" do
|
18
|
+
delivered_keys = []
|
19
|
+
delivered_data = []
|
20
|
+
|
21
|
+
t = Thread.new do
|
22
|
+
ch = connection.create_channel
|
23
|
+
q = ch.queue(queue_name, :auto_delete => true, :durable => false)
|
24
|
+
q.subscribe(:exclusive => false, :ack => false) do |metadata, payload|
|
25
|
+
delivered_keys << metadata.routing_key
|
26
|
+
delivered_data << payload
|
27
|
+
end
|
28
|
+
end
|
29
|
+
t.abort_on_exception = true
|
30
|
+
sleep 0.5
|
31
|
+
|
32
|
+
ch = connection.create_channel
|
33
|
+
x = ch.default_exchange
|
34
|
+
x.publish("hello", :routing_key => queue_name)
|
35
|
+
|
36
|
+
sleep 0.7
|
37
|
+
delivered_keys.should include(queue_name)
|
38
|
+
delivered_data.should include("hello")
|
39
|
+
|
40
|
+
ch.queue(queue_name, :auto_delete => true, :durable => false).message_count.should == 0
|
41
|
+
|
42
|
+
ch.close
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "with manual acknowledgement mode" do
|
47
|
+
let(:queue_name) { "bunny.basic_consume#{rand}" }
|
48
|
+
|
49
|
+
it "register a consumer with manual acknowledgements mode"
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Bunny::Queue, "#pop" do
|
4
|
+
let(:connection) do
|
5
|
+
c = Bunny.new(:user => "bunny_gem", :password => "bunny_password", :vhost => "bunny_testbed")
|
6
|
+
c.start
|
7
|
+
c
|
8
|
+
end
|
9
|
+
|
10
|
+
after :all do
|
11
|
+
connection.close if connection.open?
|
12
|
+
end
|
13
|
+
|
14
|
+
context "with all defaults" do
|
15
|
+
it "fetches a messages which is automatically acknowledged" do
|
16
|
+
ch = connection.create_channel
|
17
|
+
|
18
|
+
q = ch.queue("", :exclusive => true)
|
19
|
+
x = ch.default_exchange
|
20
|
+
|
21
|
+
x.publish("xyzzy", :routing_key => q.name)
|
22
|
+
|
23
|
+
sleep(0.5)
|
24
|
+
delivery_info, properties, content = q.pop
|
25
|
+
content.should == "xyzzy"
|
26
|
+
q.message_count.should == 0
|
27
|
+
|
28
|
+
ch.close
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
context "with an empty queue" do
|
34
|
+
it "returns an empty response" do
|
35
|
+
ch = connection.create_channel
|
36
|
+
|
37
|
+
q = ch.queue("", :exclusive => true)
|
38
|
+
q.purge
|
39
|
+
|
40
|
+
_, _, content = q.pop
|
41
|
+
content.should be_nil
|
42
|
+
q.message_count.should == 0
|
43
|
+
|
44
|
+
ch.close
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|