bunny 0.5.3 → 0.6.0

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 (41) hide show
  1. data/{README → README.rdoc} +9 -2
  2. data/bunny.gemspec +12 -7
  3. data/examples/simple_08.rb +1 -1
  4. data/examples/simple_09.rb +1 -1
  5. data/examples/simple_ack_08.rb +1 -1
  6. data/examples/simple_ack_09.rb +1 -1
  7. data/examples/simple_consumer_08.rb +6 -14
  8. data/examples/simple_consumer_09.rb +6 -14
  9. data/examples/simple_fanout_08.rb +2 -2
  10. data/examples/simple_fanout_09.rb +2 -2
  11. data/examples/simple_headers_08.rb +1 -1
  12. data/examples/simple_headers_09.rb +1 -1
  13. data/examples/simple_topic_08.rb +4 -4
  14. data/examples/simple_topic_09.rb +4 -4
  15. data/lib/bunny.rb +23 -15
  16. data/lib/bunny/channel08.rb +8 -2
  17. data/lib/bunny/channel09.rb +8 -2
  18. data/lib/bunny/client08.rb +67 -24
  19. data/lib/bunny/client09.rb +88 -48
  20. data/lib/bunny/exchange08.rb +55 -43
  21. data/lib/bunny/exchange09.rb +67 -54
  22. data/lib/bunny/queue08.rb +79 -137
  23. data/lib/bunny/queue09.rb +79 -141
  24. data/lib/bunny/subscription08.rb +85 -0
  25. data/lib/bunny/subscription09.rb +85 -0
  26. data/lib/qrack/client.rb +29 -10
  27. data/lib/qrack/protocol/spec08.rb +1 -0
  28. data/lib/qrack/protocol/spec09.rb +1 -0
  29. data/lib/qrack/qrack08.rb +1 -0
  30. data/lib/qrack/qrack09.rb +1 -0
  31. data/lib/qrack/queue.rb +1 -1
  32. data/lib/qrack/subscription.rb +102 -0
  33. data/spec/spec_08/bunny_spec.rb +6 -0
  34. data/spec/spec_08/connection_spec.rb +12 -0
  35. data/spec/spec_08/exchange_spec.rb +19 -3
  36. data/spec/spec_08/queue_spec.rb +87 -13
  37. data/spec/spec_09/bunny_spec.rb +7 -1
  38. data/spec/spec_09/connection_spec.rb +12 -0
  39. data/spec/spec_09/exchange_spec.rb +19 -3
  40. data/spec/spec_09/queue_spec.rb +94 -21
  41. metadata +11 -6
@@ -0,0 +1,85 @@
1
+ module Bunny
2
+
3
+ =begin rdoc
4
+
5
+ === DESCRIPTION:
6
+
7
+ Asks the server to start a "consumer", which is a transient request for messages from a specific
8
+ queue. Consumers last as long as the channel they were created on, or until the client cancels them
9
+ with an _unsubscribe_. Every time a message reaches the queue it is passed to the _blk_ for
10
+ processing. If error occurs, _Bunny_::_ProtocolError_ is raised.
11
+
12
+ ==== OPTIONS:
13
+ * <tt>:consumer_tag => '_tag_'</tt> - Specifies the identifier for the consumer. The consumer tag is
14
+ local to a connection, so two clients can use the same consumer tags. If this option is not
15
+ specified a server generated name is used.
16
+ * <tt>:ack => false (_default_) or true</tt> - If set to _false_, the server does not expect an
17
+ acknowledgement message from the client. If set to _true_, the server expects an acknowledgement
18
+ message from the client and will re-queue the message if it does not receive one within a time specified
19
+ by the server.
20
+ * <tt>:exclusive => true or false (_default_)</tt> - Request exclusive consumer access, meaning
21
+ only this consumer can access the queue.
22
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
23
+ * <tt>:timeout => number of seconds - The subscribe loop will continue to wait for
24
+ messages until terminated (Ctrl-C or kill command) or this timeout interval is reached.
25
+ * <tt>:message_max => max number messages to process</tt> - When the required number of messages
26
+ is processed subscribe loop is exited.
27
+
28
+ ==== OPERATION:
29
+
30
+ Passes a hash of message information to the block, if one has been supplied. The hash contains
31
+ :header, :payload and :delivery_details. The structure of the data is as follows -
32
+
33
+ :header has instance variables -
34
+ @klass
35
+ @size
36
+ @weight
37
+ @properties is a hash containing -
38
+ :content_type
39
+ :delivery_mode
40
+ :priority
41
+
42
+ :payload contains the message contents
43
+
44
+ :delivery details is a hash containing -
45
+ :consumer_tag
46
+ :delivery_tag
47
+ :redelivered
48
+ :exchange
49
+ :routing_key
50
+
51
+ If the :timeout option is specified then Qrack::ClientTimeout is raised if method times out
52
+ waiting to receive the next message from the queue.
53
+
54
+ ==== EXAMPLES
55
+
56
+ my_queue.subscribe(:timeout => 5) {|msg| puts msg[:payload]}
57
+
58
+ my_queue.subscribe(:message_max => 10, :ack => true) {|msg| puts msg[:payload]}
59
+
60
+ =end
61
+
62
+ class Subscription < Qrack::Subscription
63
+
64
+
65
+ def setup_consumer
66
+ client.send_frame(
67
+ Qrack::Protocol::Basic::Consume.new({ :queue => queue.name,
68
+ :consumer_tag => consumer_tag,
69
+ :no_ack => !ack,
70
+ :exclusive => exclusive,
71
+ :nowait => false }.merge(@opts))
72
+ )
73
+
74
+ method = client.next_method
75
+
76
+ client.check_response(method, Qrack::Protocol::Basic::ConsumeOk,
77
+ "Error subscribing to queue #{queue.name}")
78
+
79
+ @consumer_tag = method.consumer_tag
80
+
81
+ end
82
+
83
+ end
84
+
85
+ end
@@ -0,0 +1,85 @@
1
+ module Bunny
2
+
3
+ =begin rdoc
4
+
5
+ === DESCRIPTION:
6
+
7
+ Asks the server to start a "consumer", which is a transient request for messages from a specific
8
+ queue. Consumers last as long as the channel they were created on, or until the client cancels them
9
+ with an _unsubscribe_. Every time a message reaches the queue it is passed to the _blk_ for
10
+ processing. If error occurs, _Bunny_::_ProtocolError_ is raised.
11
+
12
+ ==== OPTIONS:
13
+ * <tt>:consumer_tag => '_tag_'</tt> - Specifies the identifier for the consumer. The consumer tag is
14
+ local to a connection, so two clients can use the same consumer tags. If this option is not
15
+ specified a server generated name is used.
16
+ * <tt>:ack => false (_default_) or true</tt> - If set to _false_, the server does not expect an
17
+ acknowledgement message from the client. If set to _true_, the server expects an acknowledgement
18
+ message from the client and will re-queue the message if it does not receive one within a time specified
19
+ by the server.
20
+ * <tt>:exclusive => true or false (_default_)</tt> - Request exclusive consumer access, meaning
21
+ only this consumer can access the queue.
22
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
23
+ * <tt>:timeout => number of seconds - The subscribe loop will continue to wait for
24
+ messages until terminated (Ctrl-C or kill command) or this timeout interval is reached.
25
+ * <tt>:message_max => max number messages to process</tt> - When the required number of messages
26
+ is processed subscribe loop is exited.
27
+
28
+ ==== OPERATION:
29
+
30
+ Passes a hash of message information to the block, if one has been supplied. The hash contains
31
+ :header, :payload and :delivery_details. The structure of the data is as follows -
32
+
33
+ :header has instance variables -
34
+ @klass
35
+ @size
36
+ @weight
37
+ @properties is a hash containing -
38
+ :content_type
39
+ :delivery_mode
40
+ :priority
41
+
42
+ :payload contains the message contents
43
+
44
+ :delivery details is a hash containing -
45
+ :consumer_tag
46
+ :delivery_tag
47
+ :redelivered
48
+ :exchange
49
+ :routing_key
50
+
51
+ If the :timeout option is specified then Qrack::ClientTimeout is raised if method times out
52
+ waiting to receive the next message from the queue.
53
+
54
+ ==== EXAMPLES
55
+
56
+ my_queue.subscribe(:timeout => 5) {|msg| puts msg[:payload]}
57
+
58
+ my_queue.subscribe(:message_max => 10, :ack => true) {|msg| puts msg[:payload]}
59
+
60
+ =end
61
+
62
+ class Subscription09 < Qrack::Subscription
63
+
64
+ def setup_consumer
65
+ client.send_frame(
66
+ Qrack::Protocol09::Basic::Consume.new({ :reserved_1 => 0,
67
+ :queue => queue.name,
68
+ :consumer_tag => consumer_tag,
69
+ :no_ack => !ack,
70
+ :exclusive => exclusive,
71
+ :nowait => false}.merge(@opts))
72
+ )
73
+
74
+ method = client.next_method
75
+
76
+ client.check_response(method, Qrack::Protocol09::Basic::ConsumeOk,
77
+ "Error subscribing to queue #{queue.name}")
78
+
79
+ @consumer_tag = method.consumer_tag
80
+
81
+ end
82
+
83
+ end
84
+
85
+ end
@@ -1,11 +1,12 @@
1
1
  module Qrack
2
2
 
3
3
  class ClientTimeout < Timeout::Error; end
4
+ class ConnectionTimeout < Timeout::Error; end
4
5
 
5
6
  # Client ancestor class
6
7
  class Client
7
8
 
8
- CONNECT_TIMEOUT = 1.0
9
+ CONNECT_TIMEOUT = 5.0
9
10
  RETRY_DELAY = 10.0
10
11
 
11
12
  attr_reader :status, :host, :vhost, :port, :logging, :spec, :heartbeat
@@ -19,10 +20,13 @@ module Qrack
19
20
  @vhost = opts[:vhost] || '/'
20
21
  @logfile = opts[:logfile] || nil
21
22
  @logging = opts[:logging] || false
23
+ @ssl = opts[:ssl] || false
24
+ @verify_ssl = opts[:verify_ssl].nil? || opts[:verify_ssl]
22
25
  @status = :not_connected
23
26
  @frame_max = opts[:frame_max] || 131072
24
27
  @channel_max = opts[:channel_max] || 0
25
28
  @heartbeat = opts[:heartbeat] || 0
29
+ @connect_timeout = opts[:connect_timeout] || CONNECT_TIMEOUT
26
30
  @logger = nil
27
31
  create_logger if @logging
28
32
  @message_in = false
@@ -100,24 +104,30 @@ with the <tt>:immediate</tt> or <tt>:mandatory</tt> options.
100
104
 
101
105
  ==== RETURNS:
102
106
 
103
- <tt>:no_return</tt> if message was not returned before timeout .
107
+ <tt>{:header => nil, :payload => :no_return, :return_details => nil}</tt> if message is
108
+ not returned before timeout.
104
109
  <tt>{:header, :return_details, :payload}</tt> if message is returned. <tt>:return_details</tt> is
105
110
  a hash <tt>{:reply_code, :reply_text, :exchange, :routing_key}</tt>.
106
111
 
107
112
  =end
108
113
 
109
114
  def returned_message(opts = {})
110
- secs = opts[:timeout] || 0.1
111
- frame = next_frame(:timeout => secs)
112
-
113
- if frame.is_a?(Symbol)
114
- return :no_return if frame == :timed_out
115
+
116
+ begin
117
+ frame = next_frame(:timeout => opts[:timeout] || 0.1)
118
+ rescue Qrack::ClientTimeout
119
+ return {:header => nil, :payload => :no_return, :return_details => nil}
115
120
  end
116
121
 
117
122
  method = frame.payload
118
123
  header = next_payload
119
- msg = next_payload
120
- raise Bunny::MessageError, 'unexpected length' if msg.length < header.size
124
+
125
+ # If maximum frame size is smaller than message payload body then message
126
+ # will have a message header and several message bodies
127
+ msg = ''
128
+ while msg.length < header.size
129
+ msg += next_payload
130
+ end
121
131
 
122
132
  # Return the message and related info
123
133
  {:header => header, :payload => msg, :return_details => method.arguments}
@@ -165,13 +175,22 @@ a hash <tt>{:reply_code, :reply_text, :exchange, :routing_key}</tt>.
165
175
 
166
176
  begin
167
177
  # Attempt to connect.
168
- @socket = timeout(CONNECT_TIMEOUT) do
178
+ @socket = timeout(@connect_timeout, ConnectionTimeout) do
169
179
  TCPSocket.new(host, port)
170
180
  end
171
181
 
172
182
  if Socket.constants.include? 'TCP_NODELAY'
173
183
  @socket.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
174
184
  end
185
+
186
+ if @ssl
187
+ require 'openssl' unless defined? OpenSSL::SSL
188
+ @socket = OpenSSL::SSL::SSLSocket.new(@socket)
189
+ @socket.sync_close = true
190
+ @socket.connect
191
+ @socket.post_connection_check(host) if @verify_ssl
192
+ @socket
193
+ end
175
194
  rescue => e
176
195
  @status = :not_connected
177
196
  raise Bunny::ServerDownError, e.message
@@ -12,6 +12,7 @@ module Qrack
12
12
  VERSION_MINOR = 0
13
13
  REVISION = 0
14
14
  PORT = 5672
15
+ SSL_PORT = 5671
15
16
 
16
17
  RESPONSES = {
17
18
  200 => :REPLY_SUCCESS,
@@ -12,6 +12,7 @@ module Qrack
12
12
  VERSION_MINOR = 9
13
13
  REVISION = 1
14
14
  PORT = 5672
15
+ SSL_PORT = 5671
15
16
 
16
17
  RESPONSES = {
17
18
  200 => :REPLY_SUCCESS,
@@ -9,6 +9,7 @@ require 'transport/frame08'
9
9
  require 'qrack/client'
10
10
  require 'qrack/channel'
11
11
  require 'qrack/queue'
12
+ require 'qrack/subscription'
12
13
 
13
14
  module Qrack
14
15
 
@@ -9,6 +9,7 @@ require 'transport/frame09'
9
9
  require 'qrack/client'
10
10
  require 'qrack/channel'
11
11
  require 'qrack/queue'
12
+ require 'qrack/subscription'
12
13
 
13
14
  module Qrack
14
15
 
@@ -4,7 +4,7 @@ module Qrack
4
4
  class Queue
5
5
 
6
6
  attr_reader :name, :client
7
- attr_accessor :delivery_tag
7
+ attr_accessor :delivery_tag, :subscription
8
8
 
9
9
  =begin rdoc
10
10
 
@@ -0,0 +1,102 @@
1
+ module Qrack
2
+ # Subscription ancestor class
3
+ class Subscription
4
+
5
+ attr_accessor :consumer_tag, :delivery_tag, :message_max, :timeout, :ack, :exclusive
6
+ attr_reader :client, :queue, :message_count
7
+
8
+ def initialize(client, queue, opts = {})
9
+ @client = client
10
+ @queue = queue
11
+
12
+ # Get timeout value
13
+ @timeout = opts[:timeout] || nil
14
+
15
+ # Get maximum amount of messages to process
16
+ @message_max = opts[:message_max] || nil
17
+
18
+ # If a consumer tag is not passed in the server will generate one
19
+ @consumer_tag = opts[:consumer_tag] || nil
20
+
21
+ # Ignore the :nowait option if passed, otherwise program will hang waiting for a
22
+ # response from the server causing an error.
23
+ opts.delete(:nowait)
24
+
25
+ # Do we want to have to provide an acknowledgement?
26
+ @ack = opts[:ack] || nil
27
+
28
+ # Does this consumer want exclusive use of the queue?
29
+ @exclusive = opts[:exclusive] || false
30
+
31
+ # Initialize message counter
32
+ @message_count = 0
33
+
34
+ # Give queue reference to this subscription
35
+ @queue.subscription = self
36
+
37
+ # Store options
38
+ @opts = opts
39
+
40
+ end
41
+
42
+ def start(&blk)
43
+
44
+ # Do not process any messages if zero message_max
45
+ if message_max == 0
46
+ return
47
+ end
48
+
49
+ # Notify server about new consumer
50
+ setup_consumer
51
+
52
+ # Start subscription loop
53
+ loop do
54
+
55
+ begin
56
+ method = client.next_method(:timeout => timeout)
57
+ rescue Qrack::ClientTimeout
58
+ queue.unsubscribe()
59
+ break
60
+ end
61
+
62
+ # Increment message counter
63
+ @message_count += 1
64
+
65
+ # get delivery tag to use for acknowledge
66
+ queue.delivery_tag = method.delivery_tag if @ack
67
+
68
+ header = client.next_payload
69
+
70
+ # If maximum frame size is smaller than message payload body then message
71
+ # will have a message header and several message bodies
72
+ msg = ''
73
+ while msg.length < header.size
74
+ msg += client.next_payload
75
+ end
76
+
77
+ # If block present, pass the message info to the block for processing
78
+ blk.call({:header => header, :payload => msg, :delivery_details => method.arguments}) if !blk.nil?
79
+
80
+ # Exit loop if message_max condition met
81
+ if (!message_max.nil? and message_count == message_max)
82
+ # Stop consuming messages
83
+ queue.unsubscribe()
84
+ # Acknowledge receipt of the final message
85
+ queue.ack() if @ack
86
+ # Quit the loop
87
+ break
88
+ end
89
+
90
+ # Have to do the ack here because the ack triggers the release of messages from the server
91
+ # if you are using Client#qos prefetch and you will get extra messages sent through before
92
+ # the unsubscribe takes effect to stop messages being sent to this consumer unless the ack is
93
+ # deferred.
94
+ queue.ack() if @ack
95
+
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -52,6 +52,12 @@ describe Bunny do
52
52
  @b.queues.has_key?('test1').should be(true)
53
53
  end
54
54
 
55
+ # Current RabbitMQ has not implemented some functionality
56
+ it "should raise an error if setting of QoS fails" do
57
+ lambda { @b.qos(:global => true) }.should raise_error(Bunny::ForcedConnectionCloseError)
58
+ @b.status.should == :not_connected
59
+ end
60
+
55
61
  it "should be able to set QoS" do
56
62
  @b.qos.should == :qos_ok
57
63
  end
@@ -0,0 +1,12 @@
1
+ # connection_spec.rb
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. lib bunny]))
4
+
5
+ describe Bunny do
6
+
7
+ it "should raise an error if the wrong user name or password is used" do
8
+ b = Bunny.new(:user => 'wrong')
9
+ lambda { b.start}.should raise_error(Bunny::ProtocolError)
10
+ end
11
+
12
+ end
@@ -8,7 +8,7 @@
8
8
 
9
9
  require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. lib bunny]))
10
10
 
11
- describe Bunny do
11
+ describe 'Exchange' do
12
12
 
13
13
  before(:each) do
14
14
  @b = Bunny.new
@@ -16,7 +16,8 @@ describe Bunny do
16
16
  end
17
17
 
18
18
  it "should raise an error if instantiated as non-existent type" do
19
- lambda { @b.exchange('bogus_ex', :type => :bogus) }.should raise_error(Bunny::ProtocolError)
19
+ lambda { @b.exchange('bogus_ex', :type => :bogus) }.should raise_error(Bunny::ForcedConnectionCloseError)
20
+ @b.status.should == :not_connected
20
21
  end
21
22
 
22
23
  it "should allow a default direct exchange to be instantiated by specifying :type" do
@@ -124,13 +125,28 @@ describe Bunny do
124
125
  end
125
126
 
126
127
  it "should be able to return an undeliverable message" do
127
- exch = @b.exchange('')
128
+ exch = @b.exchange('return_exch')
128
129
  exch.publish('This message should be undeliverable', :immediate => true)
129
130
  ret_msg = @b.returned_message
130
131
  ret_msg.should be_an_instance_of(Hash)
131
132
  ret_msg[:payload].should == 'This message should be undeliverable'
132
133
  end
133
134
 
135
+ it "should be able to return a message that exceeds maximum frame size" do
136
+ exch = @b.exchange('return_exch')
137
+ lg_msg = 'z' * 142000
138
+ exch.publish(lg_msg, :immediate => true)
139
+ ret_msg = @b.returned_message
140
+ ret_msg.should be_an_instance_of(Hash)
141
+ ret_msg[:payload].should == lg_msg
142
+ end
143
+
144
+ it "should report an error if delete fails" do
145
+ exch = @b.exchange('direct_exchange')
146
+ lambda { exch.delete(:exchange => 'bogus_ex') }.should raise_error(Bunny::ForcedChannelCloseError)
147
+ @b.channel.active.should == false
148
+ end
149
+
134
150
  it "should be able to be deleted" do
135
151
  exch = @b.exchange('direct_exchange')
136
152
  res = exch.delete