bunny 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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