bunny 0.4.3 → 0.4.4

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