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.
Files changed (91) hide show
  1. data/.gitignore +7 -1
  2. data/.travis.yml +14 -4
  3. data/ChangeLog.md +72 -0
  4. data/Gemfile +17 -11
  5. data/README.md +82 -0
  6. data/bunny.gemspec +6 -13
  7. data/examples/connection/heartbeat.rb +17 -0
  8. data/lib/bunny.rb +40 -56
  9. data/lib/bunny/channel.rb +615 -19
  10. data/lib/bunny/channel_id_allocator.rb +59 -0
  11. data/lib/bunny/compatibility.rb +24 -0
  12. data/lib/bunny/concurrent/condition.rb +63 -0
  13. data/lib/bunny/consumer.rb +42 -26
  14. data/lib/bunny/consumer_tag_generator.rb +22 -0
  15. data/lib/bunny/consumer_work_pool.rb +67 -0
  16. data/lib/bunny/exceptions.rb +128 -0
  17. data/lib/bunny/exchange.rb +131 -136
  18. data/lib/bunny/framing.rb +53 -0
  19. data/lib/bunny/heartbeat_sender.rb +59 -0
  20. data/lib/bunny/main_loop.rb +70 -0
  21. data/lib/bunny/message_metadata.rb +126 -0
  22. data/lib/bunny/queue.rb +102 -275
  23. data/lib/bunny/session.rb +478 -0
  24. data/lib/bunny/socket.rb +44 -0
  25. data/lib/bunny/system_timer.rb +9 -9
  26. data/lib/bunny/transport.rb +179 -0
  27. data/lib/bunny/version.rb +1 -1
  28. data/spec/compatibility/queue_declare_spec.rb +40 -0
  29. data/spec/higher_level_api/integration/basic_ack_spec.rb +54 -0
  30. data/spec/higher_level_api/integration/basic_consume_spec.rb +51 -0
  31. data/spec/higher_level_api/integration/basic_get_spec.rb +47 -0
  32. data/spec/higher_level_api/integration/basic_nack_spec.rb +39 -0
  33. data/spec/higher_level_api/integration/basic_publish_spec.rb +105 -0
  34. data/spec/higher_level_api/integration/basic_qos_spec.rb +32 -0
  35. data/spec/higher_level_api/integration/basic_recover_spec.rb +18 -0
  36. data/spec/higher_level_api/integration/basic_reject_spec.rb +53 -0
  37. data/spec/higher_level_api/integration/basic_return_spec.rb +33 -0
  38. data/spec/higher_level_api/integration/channel_close_spec.rb +29 -0
  39. data/spec/higher_level_api/integration/channel_flow_spec.rb +24 -0
  40. data/spec/higher_level_api/integration/channel_open_spec.rb +57 -0
  41. data/spec/higher_level_api/integration/channel_open_stress_spec.rb +22 -0
  42. data/spec/higher_level_api/integration/confirm_select_spec.rb +19 -0
  43. data/spec/higher_level_api/integration/connection_spec.rb +340 -0
  44. data/spec/higher_level_api/integration/exchange_bind_spec.rb +31 -0
  45. data/spec/higher_level_api/integration/exchange_declare_spec.rb +183 -0
  46. data/spec/higher_level_api/integration/exchange_delete_spec.rb +37 -0
  47. data/spec/higher_level_api/integration/exchange_unbind_spec.rb +40 -0
  48. data/spec/higher_level_api/integration/queue_bind_spec.rb +109 -0
  49. data/spec/higher_level_api/integration/queue_declare_spec.rb +129 -0
  50. data/spec/higher_level_api/integration/queue_delete_spec.rb +38 -0
  51. data/spec/higher_level_api/integration/queue_purge_spec.rb +30 -0
  52. data/spec/higher_level_api/integration/queue_unbind_spec.rb +33 -0
  53. data/spec/higher_level_api/integration/tx_commit_spec.rb +21 -0
  54. data/spec/higher_level_api/integration/tx_rollback_spec.rb +21 -0
  55. data/spec/lower_level_api/integration/basic_cancel_spec.rb +57 -0
  56. data/spec/lower_level_api/integration/basic_consume_spec.rb +100 -0
  57. data/spec/spec_helper.rb +64 -0
  58. data/spec/unit/bunny_spec.rb +15 -0
  59. data/spec/unit/concurrent/condition_spec.rb +66 -0
  60. metadata +135 -93
  61. data/CHANGELOG +0 -21
  62. data/README.textile +0 -76
  63. data/Rakefile +0 -14
  64. data/examples/simple.rb +0 -32
  65. data/examples/simple_ack.rb +0 -35
  66. data/examples/simple_consumer.rb +0 -55
  67. data/examples/simple_fanout.rb +0 -41
  68. data/examples/simple_headers.rb +0 -42
  69. data/examples/simple_publisher.rb +0 -29
  70. data/examples/simple_topic.rb +0 -61
  71. data/ext/amqp-0.9.1.json +0 -389
  72. data/ext/config.yml +0 -4
  73. data/ext/qparser.rb +0 -426
  74. data/lib/bunny/client.rb +0 -370
  75. data/lib/bunny/subscription.rb +0 -92
  76. data/lib/qrack/amq-client-url.rb +0 -165
  77. data/lib/qrack/channel.rb +0 -20
  78. data/lib/qrack/client.rb +0 -247
  79. data/lib/qrack/errors.rb +0 -5
  80. data/lib/qrack/protocol/protocol.rb +0 -135
  81. data/lib/qrack/protocol/spec.rb +0 -525
  82. data/lib/qrack/qrack.rb +0 -20
  83. data/lib/qrack/queue.rb +0 -40
  84. data/lib/qrack/subscription.rb +0 -152
  85. data/lib/qrack/transport/buffer.rb +0 -305
  86. data/lib/qrack/transport/frame.rb +0 -102
  87. data/spec/spec_09/amqp_url_spec.rb +0 -19
  88. data/spec/spec_09/bunny_spec.rb +0 -76
  89. data/spec/spec_09/connection_spec.rb +0 -34
  90. data/spec/spec_09/exchange_spec.rb +0 -173
  91. data/spec/spec_09/queue_spec.rb +0 -240
@@ -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
@@ -1,14 +1,14 @@
1
- # encoding: utf-8
1
+ # -*- encoding: utf-8; mode: ruby -*-
2
2
 
3
- module Bunny
4
- # Used for ruby < 1.9.x
5
- class SystemTimer < ::SystemTimer
3
+ require "system_timer"
6
4
 
7
- def timeout(seconds, exception)
8
- timeout_after(seconds) do
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
- end
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
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Bunny
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0.pre1"
5
5
  end
@@ -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