bunny 0.4.3 → 0.4.4

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/README CHANGED
@@ -17,7 +17,7 @@ You can use Bunny to -
17
17
  * Create and delete queues
18
18
  * Publish and consume messages
19
19
 
20
- Bunny is known to work with RabbitMQ versions 1.5.4, 1.5.5 and version 0-8 of the AMQP specification.
20
+ Bunny is known to work with RabbitMQ versions 1.5.4, 1.5.5, 1.6.0 and version 0-8 of the AMQP specification.
21
21
 
22
22
  === INSTALL:
23
23
 
@@ -48,5 +48,12 @@ Bunny is known to work with RabbitMQ versions 1.5.4, 1.5.5 and version 0-8 of th
48
48
  # close the connection
49
49
  b.stop
50
50
 
51
+ === EVEN QUICKER START
52
+
53
+ require 'bunny'
54
+
55
+ # Create a direct queue named 'my_testq'
56
+ Bunny.run { |c| c.queue('my_testq') }
57
+
51
58
  === OTHER:
52
59
  Please see the _examples_ directory for additional usage information.
@@ -0,0 +1,44 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{bunny}
3
+ s.version = "0.4.4"
4
+ s.authors = ["Chris Duncan"]
5
+ s.date = %q{2009-06-19}
6
+ s.description = %q{Another synchronous Ruby AMQP client}
7
+ s.email = %q{celldee@gmail.com}
8
+ s.rubyforge_project = %q{bunny-amqp}
9
+ s.has_rdoc = true
10
+ s.extra_rdoc_files = [ "README" ]
11
+ s.rdoc_options = [ "--main", "README" ]
12
+ s.homepage = %q{http://github.com/celldee/bunny/tree/master}
13
+ s.summary = %q{A synchronous Ruby AMQP client that enables interaction with AMQP-compliant brokers/servers.}
14
+ s.files = ["LICENSE",
15
+ "README",
16
+ "Rakefile",
17
+ "bunny.gemspec",
18
+ "examples/simple.rb",
19
+ "examples/simple_ack.rb",
20
+ "examples/simple_consumer.rb",
21
+ "examples/simple_fanout.rb",
22
+ "examples/simple_publisher.rb",
23
+ "examples/simple_topic.rb",
24
+ "examples/simple_headers.rb",
25
+ "lib/bunny.rb",
26
+ "lib/bunny/client08.rb",
27
+ "lib/bunny/client091.rb",
28
+ "lib/bunny/exchange08.rb",
29
+ "lib/bunny/exchange091.rb",
30
+ "lib/bunny/queue08.rb",
31
+ "lib/bunny/queue091.rb",
32
+ "lib/qrack/client.rb",
33
+ "lib/qrack/protocol/protocol.rb",
34
+ "lib/qrack/protocol/spec08.rb",
35
+ "lib/qrack/protocol/spec091.rb",
36
+ "lib/qrack/qrack08.rb",
37
+ "lib/qrack/qrack091.rb",
38
+ "lib/qrack/transport/buffer.rb",
39
+ "lib/qrack/transport/frame08.rb",
40
+ "lib/qrack/transport/frame091.rb",
41
+ "spec/bunny_spec.rb",
42
+ "spec/exchange_spec.rb",
43
+ "spec/queue_spec.rb"]
44
+ end
@@ -0,0 +1,40 @@
1
+ # simple_headers.rb
2
+
3
+ # Assumes that target message broker/server has a user called 'guest' with a password 'guest'
4
+ # and that it is running on 'localhost'.
5
+
6
+ # If this is not the case, please change the 'Bunny.new' call below to include
7
+ # the relevant arguments e.g. b = Bunny.new(:user => 'john', :pass => 'doe', :host => 'foobar')
8
+
9
+ $:.unshift File.dirname(__FILE__) + '/../lib'
10
+
11
+ require 'bunny'
12
+
13
+ b = Bunny.new
14
+
15
+ # start a communication session with the amqp server
16
+ b.start
17
+
18
+ # declare queues
19
+ q = b.queue('header_q1')
20
+
21
+ # create a headers exchange
22
+ header_exch = b.exchange('header_exch', :type => :headers)
23
+
24
+ # bind the queue to the exchange
25
+ q.bind(header_exch, :arguments => {'h1'=>'a','x-match'=>'all'})
26
+
27
+ # publish messages to the exchange
28
+ header_exch.publish('Headers test msg 1', :headers => {'h1'=>'a'})
29
+ header_exch.publish('Headers test msg 2', :headers => {'h1'=>'z'})
30
+
31
+
32
+ # get messages from the queue - should only be msg 1 that got through
33
+ msg = ""
34
+ until msg == :queue_empty do
35
+ msg = q.pop
36
+ puts 'This is a message from the header_q1 queue: ' + msg + "\n" unless msg == :queue_empty
37
+ end
38
+
39
+ # close the client connection
40
+ b.stop
@@ -1,26 +1,18 @@
1
1
  $:.unshift File.expand_path(File.dirname(__FILE__))
2
2
 
3
3
  # Ruby standard libraries
4
- %w[socket thread timeout].each do |file|
4
+ %w[socket thread timeout logger].each do |file|
5
5
  require file
6
6
  end
7
7
 
8
- require 'qrack/qrack'
9
-
10
- require 'bunny/client'
11
- require 'bunny/exchange'
12
- require 'bunny/queue'
13
-
14
8
  module Bunny
15
-
16
- include Qrack
17
9
 
18
10
  class ProtocolError < StandardError; end
19
11
  class ServerDownError < StandardError; end
20
12
  class ConnectionError < StandardError; end
21
13
  class MessageError < StandardError; end
22
14
 
23
- VERSION = '0.4.3'
15
+ VERSION = '0.4.4'
24
16
 
25
17
  # Returns the Bunny version number
26
18
 
@@ -29,14 +21,24 @@ module Bunny
29
21
  end
30
22
 
31
23
  # Instantiates new Bunny::Client
32
-
24
+
33
25
  def self.new(opts = {})
26
+ # Set up Bunny according to AMQP spec version required
27
+ spec_version = opts[:spec] || '08'
28
+ setup(spec_version)
29
+
34
30
  Bunny::Client.new(opts)
35
31
  end
32
+
33
+ # Runs a code block using a short-lived connection
36
34
 
37
35
  def self.run(opts = {}, &block)
38
36
  raise ArgumentError, 'Bunny#run requires a block' unless block
39
37
 
38
+ # Set up Bunny according to AMQP spec version required
39
+ spec_version = opts[:spec] || '08'
40
+ setup(spec_version)
41
+
40
42
  client = Bunny::Client.new(opts)
41
43
  client.start
42
44
 
@@ -48,4 +50,25 @@ module Bunny
48
50
  :run_ok
49
51
  end
50
52
 
53
+ private
54
+
55
+ def self.setup(version)
56
+
57
+ if version == '08'
58
+ # AMQP 0-8 specification
59
+ require 'qrack/qrack08'
60
+ require 'bunny/client08'
61
+ require 'bunny/exchange08'
62
+ require 'bunny/queue08'
63
+ else
64
+ # AMQP 0-9-1 specification
65
+ require 'qrack/qrack091'
66
+ require 'bunny/client091'
67
+ require 'bunny/exchange091'
68
+ require 'bunny/queue091'
69
+ end
70
+
71
+ include Qrack
72
+ end
73
+
51
74
  end
@@ -0,0 +1,421 @@
1
+ module Bunny
2
+
3
+ =begin rdoc
4
+
5
+ === DESCRIPTION:
6
+
7
+ The Client class provides the major Bunny API methods.
8
+
9
+ =end
10
+
11
+ class Client < Qrack::Client
12
+ CONNECT_TIMEOUT = 1.0
13
+ RETRY_DELAY = 10.0
14
+
15
+ attr_reader :status, :host, :vhost, :port, :logging, :spec
16
+ attr_accessor :channel, :logfile, :exchanges, :queues, :ticket
17
+
18
+ =begin rdoc
19
+
20
+ === DESCRIPTION:
21
+
22
+ Sets up a Bunny::Client object ready for connection to a broker/server. _Client_._status_ is set to
23
+ <tt>:not_connected</tt>.
24
+
25
+ ==== OPTIONS:
26
+
27
+ * <tt>:host => '_hostname_' (default = 'localhost')</tt>
28
+ * <tt>:port => _portno_ (default = 5672)</tt>
29
+ * <tt>:vhost => '_vhostname_' (default = '/')</tt>
30
+ * <tt>:user => '_username_' (default = 'guest')</tt>
31
+ * <tt>:pass => '_password_' (default = 'guest')</tt>
32
+ * <tt>:logfile => '_logfilepath_' (default = nil)</tt>
33
+ * <tt>:logging => true or false (_default_)</tt> - If set to _true_, session information is sent
34
+ to STDOUT if <tt>:logfile</tt> has not been specified. Otherwise, session information is written to
35
+ <tt>:logfile</tt>.
36
+ * <tt>:insist => true or false (_default_)</tt> - In a configuration with multiple load-sharing
37
+ servers, the server may respond to a Connection::Open method with a Connection::Redirect. The insist
38
+ option, if set to _true_, tells the server that the client is insisting on a connection to the
39
+ specified server.
40
+
41
+ =end
42
+
43
+ def initialize(opts = {})
44
+ @spec = opts[:spec] || '08'
45
+ @host = opts[:host] || 'localhost'
46
+ @port = opts[:port] || Qrack::Protocol::PORT
47
+ @user = opts[:user] || 'guest'
48
+ @pass = opts[:pass] || 'guest'
49
+ @vhost = opts[:vhost] || '/'
50
+ @logfile = opts[:logfile] || nil
51
+ @logging = opts[:logging] || false
52
+ @insist = opts[:insist]
53
+ @status = :not_connected
54
+ @logger = nil
55
+ create_logger if @logging
56
+ end
57
+
58
+ =begin rdoc
59
+
60
+ === DESCRIPTION:
61
+
62
+ Declares an exchange to the broker/server. If the exchange does not exist, a new one is created
63
+ using the arguments passed in. If the exchange already exists, a reference to it is created, provided
64
+ that the arguments passed in do not conflict with the existing attributes of the exchange. If an error
65
+ occurs a _Bunny_::_ProtocolError_ is raised.
66
+
67
+ ==== OPTIONS:
68
+
69
+ * <tt>:type => one of :direct (_default_), :fanout, :topic, :headers</tt>
70
+ * <tt>:passive => true or false</tt> - If set to _true_, the server will not create the exchange.
71
+ The client can use this to check whether an exchange exists without modifying the server state.
72
+ * <tt>:durable => true or false (_default_)</tt> - If set to _true_ when creating a new exchange, the exchange
73
+ will be marked as durable. Durable exchanges remain active when a server restarts. Non-durable
74
+ exchanges (transient exchanges) are purged if/when a server restarts.
75
+ * <tt>:auto_delete => true or false (_default_)</tt> - If set to _true_, the exchange is deleted
76
+ when all queues have finished using it.
77
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
78
+
79
+ ==== RETURNS:
80
+
81
+ Exchange
82
+
83
+ =end
84
+
85
+ def exchange(name, opts = {})
86
+ exchanges[name] ||= Bunny::Exchange.new(self, name, opts)
87
+ end
88
+
89
+ =begin rdoc
90
+
91
+ === DESCRIPTION:
92
+
93
+ Returns hash of exchanges declared by Bunny.
94
+
95
+ =end
96
+
97
+ def exchanges
98
+ @exchanges ||= {}
99
+ end
100
+
101
+ =begin rdoc
102
+
103
+ === DESCRIPTION:
104
+
105
+ Declares a queue to the broker/server. If the queue does not exist, a new one is created
106
+ using the arguments passed in. If the queue already exists, a reference to it is created, provided
107
+ that the arguments passed in do not conflict with the existing attributes of the queue. If an error
108
+ occurs a _Bunny_::_ProtocolError_ is raised.
109
+
110
+ ==== OPTIONS:
111
+
112
+ * <tt>:passive => true or false (_default_)</tt> - If set to _true_, the server will not create
113
+ the queue. The client can use this to check whether a queue exists without modifying the server
114
+ state.
115
+ * <tt>:durable => true or false (_default_)</tt> - If set to _true_ when creating a new queue, the
116
+ queue will be marked as durable. Durable queues remain active when a server restarts. Non-durable
117
+ queues (transient queues) are purged if/when a server restarts. Note that durable queues do not
118
+ necessarily hold persistent messages, although it does not make sense to send persistent messages
119
+ to a transient queue.
120
+ * <tt>:exclusive => true or false (_default_)</tt> - If set to _true_, requests an exclusive queue.
121
+ Exclusive queues may only be consumed from by the current connection. Setting the 'exclusive'
122
+ flag always implies 'auto-delete'.
123
+ * <tt>:auto_delete => true or false (_default_)</tt> - If set to _true_, the queue is deleted
124
+ when all consumers have finished using it. Last consumer can be cancelled either explicitly
125
+ or because its channel is closed. If there has never been a consumer on the queue, it is not
126
+ deleted.
127
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
128
+
129
+ ==== RETURNS:
130
+
131
+ Queue
132
+
133
+ =end
134
+
135
+ def queue(name = nil, opts = {})
136
+ if name.is_a?(Hash)
137
+ opts = name
138
+ name = nil
139
+ end
140
+
141
+ return queues[name] if queues.has_key?(name)
142
+
143
+ queue = Bunny::Queue.new(self, name, opts)
144
+ queues[queue.name] = queue
145
+ end
146
+
147
+ =begin rdoc
148
+
149
+ === DESCRIPTION:
150
+
151
+ Returns hash of queues declared by Bunny.
152
+
153
+ =end
154
+
155
+ def queues
156
+ @queues ||= {}
157
+ end
158
+
159
+ def send_frame(*args)
160
+ args.each do |data|
161
+ data.ticket = ticket if ticket and data.respond_to?(:ticket=)
162
+ data = data.to_frame(channel) unless data.is_a?(Qrack::Transport::Frame)
163
+ data.channel = channel
164
+
165
+ @logger.info("send") { data } if @logging
166
+ write(data.to_s)
167
+ end
168
+ nil
169
+ end
170
+
171
+ def next_frame
172
+ frame = Qrack::Transport::Frame.parse(buffer)
173
+ @logger.info("received") { frame } if @logging
174
+ frame
175
+ end
176
+
177
+ def next_method
178
+ next_payload
179
+ end
180
+
181
+ def next_payload
182
+ frame = next_frame
183
+ frame and frame.payload
184
+ end
185
+
186
+ =begin rdoc
187
+
188
+ === DESCRIPTION:
189
+
190
+ Closes the current communication channel and connection. If an error occurs a
191
+ _Bunny_::_ProtocolError_ is raised. If successful, _Client_._status_ is set to <tt>:not_connected</tt>.
192
+
193
+ ==== RETURNS:
194
+
195
+ <tt>:not_connected</tt> if successful.
196
+
197
+ =end
198
+
199
+ def close
200
+ send_frame(
201
+ Qrack::Protocol::Channel::Close.new(:reply_code => 200, :reply_text => 'bye', :method_id => 0, :class_id => 0)
202
+ )
203
+ raise Bunny::ProtocolError, "Error closing channel #{channel}" unless next_method.is_a?(Qrack::Protocol::Channel::CloseOk)
204
+
205
+ self.channel = 0
206
+ send_frame(
207
+ Qrack::Protocol::Connection::Close.new(:reply_code => 200, :reply_text => 'Goodbye', :class_id => 0, :method_id => 0)
208
+ )
209
+ raise Bunny::ProtocolError, "Error closing connection" unless next_method.is_a?(Qrack::Protocol::Connection::CloseOk)
210
+
211
+ close_socket
212
+ end
213
+
214
+ alias stop close
215
+
216
+ def read(*args)
217
+ send_command(:read, *args)
218
+ end
219
+
220
+ def write(*args)
221
+ send_command(:write, *args)
222
+ end
223
+
224
+ =begin rdoc
225
+
226
+ === DESCRIPTION:
227
+
228
+ Opens a communication channel and starts a connection. If an error occurs, a
229
+ _Bunny_::_ProtocolError_ is raised. If successful, _Client_._status_ is set to <tt>:connected</tt>.
230
+
231
+ ==== RETURNS:
232
+
233
+ <tt>:connected</tt> if successful.
234
+
235
+ =end
236
+
237
+ def start_session
238
+ loop do
239
+ # Create/get socket
240
+ socket
241
+
242
+ @channel = 0
243
+ write(Qrack::Protocol::HEADER)
244
+ write([1, 1, Qrack::Protocol::VERSION_MAJOR, Qrack::Protocol::VERSION_MINOR].pack('C4'))
245
+ raise Bunny::ProtocolError, 'Connection initiation failed' unless next_method.is_a?(Qrack::Protocol::Connection::Start)
246
+
247
+ send_frame(
248
+ Qrack::Protocol::Connection::StartOk.new(
249
+ {:platform => 'Ruby', :product => 'Bunny', :information => 'http://github.com/celldee/bunny', :version => VERSION},
250
+ 'AMQPLAIN',
251
+ {:LOGIN => @user, :PASSWORD => @pass},
252
+ 'en_US'
253
+ )
254
+ )
255
+
256
+ method = next_method
257
+ raise Bunny::ProtocolError, "Connection failed - user: #{@user}, pass: #{@pass}" if method.nil?
258
+
259
+ if method.is_a?(Qrack::Protocol::Connection::Tune)
260
+ send_frame(
261
+ Qrack::Protocol::Connection::TuneOk.new( :channel_max => 0, :frame_max => 131072, :heartbeat => 0)
262
+ )
263
+ end
264
+
265
+ send_frame(
266
+ Qrack::Protocol::Connection::Open.new(:virtual_host => @vhost, :capabilities => '', :insist => @insist)
267
+ )
268
+
269
+ case method = next_method
270
+ when Qrack::Protocol::Connection::OpenOk
271
+ break
272
+ when Qrack::Protocol::Connection::Redirect
273
+ raise Bunny::ConnectionError, "Cannot connect to the specified server - host: #{@host}, port: #{@port}" if @insist
274
+
275
+ @host, @port = method.host.split(':')
276
+ close_socket
277
+ else
278
+ raise Bunny::ProtocolError, 'Cannot open connection'
279
+ end
280
+ end
281
+
282
+ @channel = 1
283
+ send_frame(Qrack::Protocol::Channel::Open.new)
284
+ raise Bunny::ProtocolError, "Cannot open channel #{channel}" unless next_method.is_a?(Qrack::Protocol::Channel::OpenOk)
285
+
286
+ send_frame(
287
+ Qrack::Protocol::Access::Request.new(:realm => '/data', :read => true, :write => true, :active => true, :passive => true)
288
+ )
289
+ method = next_method
290
+ raise Bunny::ProtocolError, 'Access denied' unless method.is_a?(Qrack::Protocol::Access::RequestOk)
291
+ self.ticket = method.ticket
292
+
293
+ # return status
294
+ status
295
+ end
296
+
297
+ alias start start_session
298
+
299
+ =begin rdoc
300
+
301
+ === DESCRIPTION:
302
+
303
+ Asks the broker to redeliver all unacknowledged messages on a specifieid channel. Zero or
304
+ more messages may be redelivered.
305
+
306
+ ==== Options:
307
+
308
+ * <tt>:requeue => true or false (_default_)</tt> - If set to _false_, the message will be
309
+ redelivered to the original recipient. If set to _true_, the server will attempt to requeue
310
+ the message, potentially then delivering it to an alternative subscriber.
311
+
312
+ =end
313
+
314
+ def recover(opts = {})
315
+
316
+ send_frame(
317
+ Qrack::Protocol::Basic::Recover.new({ :requeue => false }.merge(opts))
318
+ )
319
+
320
+ end
321
+
322
+ =begin rdoc
323
+
324
+ === DESCRIPTION:
325
+
326
+ Requests a specific quality of service. The QoS can be specified for the current channel
327
+ or for all channels on the connection. The particular properties and semantics of a QoS
328
+ method always depend on the content class semantics. Though the QoS method could in principle
329
+ apply to both peers, it is currently meaningful only for the server.
330
+
331
+ ==== Options:
332
+
333
+ * <tt>:prefetch_size => size in no. of octets (default = 0)</tt> - The client can request that
334
+ messages be sent in advance so that when the client finishes processing a message, the following
335
+ message is already held locally, rather than needing to be sent down the channel. Prefetching gives
336
+ a performance improvement. This field specifies the prefetch window size in octets. The server
337
+ will send a message in advance if it is equal to or smaller in size than the available prefetch
338
+ size (and also falls into other prefetch limits). May be set to zero, meaning "no specific limit",
339
+ although other prefetch limits may still apply. The prefetch-size is ignored if the no-ack option
340
+ is set.
341
+ * <tt>:prefetch_count => no. messages (default = 1)</tt> - Specifies a prefetch window in terms
342
+ of whole messages. This field may be used in combination with the prefetch-size field; a message
343
+ will only be sent in advance if both prefetch windows (and those at the channel and connection level)
344
+ allow it. The prefetch-count is ignored if the no-ack option is set.
345
+ * <tt>:global => true or false (_default_)</tt> - By default the QoS settings apply to the current channel only. If set to
346
+ true, they are applied to the entire connection.
347
+
348
+ =end
349
+
350
+ def qos(opts = {})
351
+
352
+ send_frame(
353
+ Qrack::Protocol::Basic::Qos.new({ :prefetch_size => 0, :prefetch_count => 1, :global => false }.merge(opts))
354
+ )
355
+
356
+ raise Bunny::ProtocolError,
357
+ "Error specifying Quality of Service" unless
358
+ next_method.is_a?(Qrack::Protocol::Basic::QosOk)
359
+
360
+ # return confirmation
361
+ :qos_ok
362
+ end
363
+
364
+ def logging=(bool)
365
+ @logging = bool
366
+ create_logger if @logging
367
+ end
368
+
369
+ private
370
+
371
+ def buffer
372
+ @buffer ||= Qrack::Transport::Buffer.new(self)
373
+ end
374
+
375
+ def send_command(cmd, *args)
376
+ begin
377
+ raise Bunny::ConnectionError, 'No connection - socket has not been created' if !@socket
378
+ @socket.__send__(cmd, *args)
379
+ rescue Errno::EPIPE, IOError => e
380
+ raise Bunny::ServerDownError, e.message
381
+ end
382
+ end
383
+
384
+ def socket
385
+ return @socket if @socket and (@status == :connected) and not @socket.closed?
386
+
387
+ begin
388
+ @status = :not_connected
389
+
390
+ # Attempt to connect.
391
+ @socket = timeout(CONNECT_TIMEOUT) do
392
+ TCPSocket.new(host, port)
393
+ end
394
+
395
+ if Socket.constants.include? 'TCP_NODELAY'
396
+ @socket.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
397
+ end
398
+ @status = :connected
399
+ rescue => e
400
+ @status = :not_connected
401
+ raise Bunny::ServerDownError, e.message
402
+ end
403
+
404
+ @socket
405
+ end
406
+
407
+ def close_socket(reason=nil)
408
+ # Close the socket. The server is not considered dead.
409
+ @socket.close if @socket and not @socket.closed?
410
+ @socket = nil
411
+ @status = :not_connected
412
+ end
413
+
414
+ def create_logger
415
+ @logfile ? @logger = Logger.new("#{logfile}") : @logger = Logger.new(STDOUT)
416
+ @logger.level = Logger::INFO
417
+ @logger.datetime_format = "%Y-%m-%d %H:%M:%S"
418
+ end
419
+
420
+ end
421
+ end