bunny 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,19 +1,20 @@
1
1
  # Bunny: A synchronous Ruby AMQP client
2
2
 
3
- Google Group: [bunny-amqp](http://groups.google.com/group/bunny-amqp)
3
+ Google Group: [http://groups.google.com/group/bunny-amqp](http://groups.google.com/group/bunny-amqp)
4
4
 
5
- Mailing List: [bunny-amqp-devel](http://rubyforge.org/mailman/listinfo/bunny-amqp-devel)
5
+ Mailing List: [http://rubyforge.org/mailman/listinfo/bunny-amqp-devel](http://rubyforge.org/mailman/listinfo/bunny-amqp-devel)
6
6
 
7
- Rubyforge: [bunny-amqp](http://rubyforge.org/projects/bunny-amqp)
7
+ Rubyforge: [http://rubyforge.org/projects/bunny-amqp](http://rubyforge.org/projects/bunny-amqp)
8
+
9
+ Twitter: [http://twitter.com/bunny_amqp](https://twitter.com/bunny_amqp)
8
10
 
9
11
  ## Announcements
10
12
 
11
- Bunny v0.1.1 has been released. It contains the following changes -
13
+ Bunny v0.2.0 is now available. The highlights are -
12
14
 
13
- * Queue#delete method returns ‘QUEUE DELETED’, Exchange#delete method returns ‘EXCHANGE DELETED’, Queue#bind returns ‘BIND SUCCEEDED’, Queue#unbind returns ‘UNBIND SUCCEEDED’
14
- * Queue#subscribe method available (see example bunny/examples/simple_consumer.rb)
15
- * Queue#status now returns a hash {:message_count, :consumer_count}
16
- * Queue#ack works after a Queue#subscribe or Queue#pop if :ack => true was specified
15
+ * Code has been re-organised enabling Bunny to play nicely with [amqp](http://github.com/tmm1/amqp) (thanks [Dan](http://github.com/danielsdeleo))
16
+ * When instantiating a default exchange (one beginning with ‘amq.’) the type will be inferred from the name.
17
+ * Fixed Queue#subscribe and included Queue#unsubscribe method. See examples/simple_consumer.rb for details
17
18
 
18
19
  ## About
19
20
 
@@ -92,6 +93,12 @@ Queue#publish(_**data**_, {_options_})
92
93
  ### Pop a message off of a queue
93
94
  Queue#pop({_options_})
94
95
 
96
+ ### Subscribe to a queue
97
+ Queue#subscribe({_options_}, &blk)
98
+
99
+ ### Unsubscribe from a queue
100
+ Queue#unsubscribe({_options_})
101
+
95
102
  ### Return queue message count
96
103
  Queue#message_count
97
104
 
@@ -1,6 +1,6 @@
1
1
  # consumer.rb
2
2
 
3
- # N.B. To be used in conjunction with publisher.rb - RUN THIS BEFORE simple_publisher.rb
3
+ # N.B. To be used in conjunction with simple_publisher.rb
4
4
 
5
5
  # Assumes that target message broker/server has a user called 'guest' with a password 'guest'
6
6
  # and that it is running on 'localhost'.
@@ -8,6 +8,21 @@
8
8
  # If this is not the case, please change the 'Bunny.new' call below to include
9
9
  # the relevant arguments e.g. b = Bunny.new(:user => 'john', :pass => 'doe', :host => 'foobar')
10
10
 
11
+ # How this example works
12
+ #=======================
13
+ #
14
+ # Open up two console windows start this program in one of them by typing -
15
+ #
16
+ # ruby simple_consumer.rb
17
+ #
18
+ # Then switch to the other console window and type -
19
+ #
20
+ # ruby simple_publisher.rb
21
+ #
22
+ # A message will be printed out by the simple_consumer and it will wait for the next message
23
+ #
24
+ # Run simple_publisher 3 more times. After the last run simple_consumer will stop.
25
+
11
26
  $:.unshift File.dirname(__FILE__) + '/../lib'
12
27
 
13
28
  require 'bunny'
@@ -26,11 +41,15 @@ exch = b.exchange('sorting_room')
26
41
  # bind queue to exchange
27
42
  q.bind(exch, :key => 'fred')
28
43
 
29
- # subscribe to queue
30
- msg = q.subscribe(:consumer_tag => 'testtag1')
44
+ # initialize counter
45
+ i = 1
31
46
 
32
- # output received message
33
- puts msg
47
+ # subscribe to queue
48
+ q.subscribe(:consumer_tag => 'testtag1') do |msg|
49
+ puts i.to_s + ': ' + msg
50
+ i+=1
51
+ q.unsubscribe(:consumer_tag => 'testtag1') if i == 5
52
+ end
34
53
 
35
54
  # close the connection
36
55
  b.stop
@@ -13,12 +13,7 @@ require 'bunny'
13
13
  b = Bunny.new(:logging => true)
14
14
 
15
15
  # start a communication session with the amqp server
16
- begin
17
- b.start
18
- rescue Exception => e
19
- puts 'ERROR - Could not start a session: ' + e
20
- exit
21
- end
16
+ b.start
22
17
 
23
18
  # declare queues
24
19
  q1 = b.queue('test_fan1')
@@ -1,6 +1,6 @@
1
1
  # simple_publisher.rb
2
2
 
3
- # N.B. To be used in conjunction with simple_consumer.rb
3
+ # N.B. To be used in conjunction with simple_consumer.rb. See simple_consumer.rb for explanation.
4
4
 
5
5
  # Assumes that target message broker/server has a user called 'guest' with a password 'guest'
6
6
  # and that it is running on 'localhost'.
@@ -17,15 +17,9 @@ b = Bunny.new(:logging => true)
17
17
  # start a communication session with the amqp server
18
18
  b.start
19
19
 
20
- # create/get queue
21
- q = b.queue('po_box')
22
-
23
20
  # create/get exchange
24
21
  exch = b.exchange('sorting_room')
25
22
 
26
- # bind queue to exchange
27
- q.bind(exch, :key => 'fred')
28
-
29
23
  # publish message to exchange
30
24
  exch.publish('This is a message from the publisher', :key => 'fred')
31
25
 
@@ -0,0 +1,59 @@
1
+ # simple_topic.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
+ soccer = b.queue('topic_soccer')
20
+ cricket = b.queue('topic_cricket')
21
+ rugby = b.queue('topic_rugby')
22
+ allsport = b.queue('topic_allsport')
23
+
24
+ # create a topic exchange
25
+ sports_results = b.exchange('sports_results', :type => :topic)
26
+
27
+ # bind the queues to the exchange
28
+ soccer.bind(sports_results, :key => 'soccer.*')
29
+ cricket.bind(sports_results, :key => 'cricket.*')
30
+ rugby.bind(sports_results, :key => 'rugby.*')
31
+ allsport.bind(sports_results, :key => '*.result')
32
+
33
+ # publish messages to the exchange
34
+ sports_results.publish('Manchester United 1 : Hull City 4', :key => 'soccer.result')
35
+ sports_results.publish('England beat Australia by 5 wickets in first test', :key => 'cricket.result')
36
+ sports_results.publish('British Lions 15 : South Africa 12', :key => 'rugby.result')
37
+
38
+ # get message from the queues
39
+
40
+ # soccer queue got the soccer message
41
+ msg = soccer.pop
42
+ puts 'This is a message from the soccer q: ' + msg + "\n\n"
43
+
44
+ # cricket queue got the cricket message
45
+ msg = cricket.pop
46
+ puts 'This is a message from the cricket q: ' + msg + "\n\n"
47
+
48
+ # rugby queue got the rugby message
49
+ msg = rugby.pop
50
+ puts 'This is a message from the rugby q: ' + msg + "\n\n"
51
+
52
+ # allsport queue got all of the messages
53
+ until msg == 'QUEUE EMPTY' do
54
+ msg = allsport.pop
55
+ puts 'This is a message from the allsport q: ' + msg + "\n\n" unless msg == 'QUEUE EMPTY'
56
+ end
57
+
58
+ # close the client connection
59
+ b.stop
@@ -1,8 +1,4 @@
1
- module AMQP
2
- %w[ spec buffer protocol frame client ].each do |file|
3
- require "amqp/#{file}"
4
- end
5
-
1
+ module API
6
2
  # return messages
7
3
  CONNECTED = 'CONNECTED'
8
4
  NOT_CONNECTED = 'NOT CONNECTED'
@@ -1,19 +1,32 @@
1
1
  $:.unshift File.expand_path(File.dirname(__FILE__))
2
2
 
3
- %w[socket thread timeout amqp].each do |file|
3
+ # Ruby standard libraries
4
+ %w[socket thread timeout].each do |file|
4
5
  require file
5
6
  end
6
7
 
7
- %w[ exchange queue header ].each do |file|
8
- require "bunny/#{file}"
8
+ # AMQP protocol and transport
9
+ %w[spec protocol buffer frame].each do |file|
10
+ require 'engineroom/' + file
9
11
  end
10
12
 
13
+ # Bunny API
14
+ %w[client exchange header queue].each do |file|
15
+ require 'bunny/' + file
16
+ end
17
+
18
+ # Error and return message definitions
19
+ require 'api_messages'
20
+
11
21
  class Bunny
12
-
22
+ include Protocol
23
+ include Transport
24
+ include API
25
+
13
26
  attr_reader :client
14
27
 
15
28
  def initialize(opts = {})
16
- @client = AMQP::Client.new(opts)
29
+ @client = API::Client.new(opts)
17
30
  end
18
31
 
19
32
  def logging=(bool)
@@ -33,11 +46,11 @@ class Bunny
33
46
  end
34
47
 
35
48
  def exchange(name, opts = {})
36
- client.exchanges[name] ||= Exchange.new(client, name, opts)
49
+ client.exchanges[name] ||= API::Exchange.new(client, name, opts)
37
50
  end
38
51
 
39
52
  def queue(name, opts = {})
40
- client.queues[name] ||= Queue.new(client, name, opts)
53
+ client.queues[name] ||= API::Queue.new(client, name, opts)
41
54
  end
42
55
 
43
56
  def stop
@@ -63,5 +76,5 @@ class Bunny
63
76
  def port
64
77
  client.port
65
78
  end
66
-
79
+
67
80
  end
@@ -1,4 +1,4 @@
1
- module AMQP
1
+ module API
2
2
  class Client
3
3
  CONNECT_TIMEOUT = 1.0
4
4
  RETRY_DELAY = 10.0
@@ -8,7 +8,7 @@ module AMQP
8
8
 
9
9
  def initialize(opts = {})
10
10
  @host = opts[:host] || 'localhost'
11
- @port = opts[:port] || AMQP::PORT
11
+ @port = opts[:port] || Protocol::PORT
12
12
  @user = opts[:user] || 'guest'
13
13
  @pass = opts[:pass] || 'guest'
14
14
  @vhost = opts[:vhost] || '/'
@@ -28,7 +28,7 @@ module AMQP
28
28
  def send_frame(*args)
29
29
  args.each do |data|
30
30
  data.ticket = ticket if ticket and data.respond_to?(:ticket=)
31
- data = data.to_frame(channel) unless data.is_a?(Frame)
31
+ data = data.to_frame(channel) unless data.is_a?(Transport::Frame)
32
32
  data.channel = channel
33
33
 
34
34
  log :send, data
@@ -38,7 +38,7 @@ module AMQP
38
38
  end
39
39
 
40
40
  def next_frame
41
- frame = Frame.parse(buffer)
41
+ frame = Transport::Frame.parse(buffer)
42
42
  log :received, frame
43
43
  frame
44
44
  end
@@ -56,13 +56,13 @@ module AMQP
56
56
  send_frame(
57
57
  Protocol::Channel::Close.new(:reply_code => 200, :reply_text => 'bye', :method_id => 0, :class_id => 0)
58
58
  )
59
- raise ProtocolError, "Error closing channel #{channel}" unless next_method.is_a?(Protocol::Channel::CloseOk)
59
+ raise API::ProtocolError, "Error closing channel #{channel}" unless next_method.is_a?(Protocol::Channel::CloseOk)
60
60
 
61
61
  self.channel = 0
62
62
  send_frame(
63
63
  Protocol::Connection::Close.new(:reply_code => 200, :reply_text => 'Goodbye', :class_id => 0, :method_id => 0)
64
64
  )
65
- raise ProtocolError, "Error closing connection" unless next_method.is_a?(Protocol::Connection::CloseOk)
65
+ raise API::ProtocolError, "Error closing connection" unless next_method.is_a?(Protocol::Connection::CloseOk)
66
66
 
67
67
  close_socket
68
68
  end
@@ -77,9 +77,9 @@ module AMQP
77
77
 
78
78
  def start_session
79
79
  @channel = 0
80
- write(HEADER)
81
- write([1, 1, VERSION_MAJOR, VERSION_MINOR].pack('C4'))
82
- raise ProtocolError, 'Connection initiation failed' unless next_method.is_a?(Protocol::Connection::Start)
80
+ write(Protocol::HEADER)
81
+ write([1, 1, Protocol::VERSION_MAJOR, Protocol::VERSION_MINOR].pack('C4'))
82
+ raise API::ProtocolError, 'Connection initiation failed' unless next_method.is_a?(Protocol::Connection::Start)
83
83
 
84
84
  send_frame(
85
85
  Protocol::Connection::StartOk.new(
@@ -91,7 +91,7 @@ module AMQP
91
91
  )
92
92
 
93
93
  method = next_method
94
- raise ProtocolError, "Connection failed - user: #{@user}, pass: #{@pass}" if method.nil?
94
+ raise API::ProtocolError, "Connection failed - user: #{@user}, pass: #{@pass}" if method.nil?
95
95
 
96
96
  if method.is_a?(Protocol::Connection::Tune)
97
97
  send_frame(
@@ -102,17 +102,17 @@ module AMQP
102
102
  send_frame(
103
103
  Protocol::Connection::Open.new(:virtual_host => @vhost, :capabilities => '', :insist => @insist)
104
104
  )
105
- raise ProtocolError, 'Cannot open connection' unless next_method.is_a?(Protocol::Connection::OpenOk)
105
+ raise API::ProtocolError, 'Cannot open connection' unless next_method.is_a?(Protocol::Connection::OpenOk)
106
106
 
107
107
  @channel = 1
108
108
  send_frame(Protocol::Channel::Open.new)
109
- raise ProtocolError, "Cannot open channel #{channel}" unless next_method.is_a?(Protocol::Channel::OpenOk)
109
+ raise API::ProtocolError, "Cannot open channel #{channel}" unless next_method.is_a?(Protocol::Channel::OpenOk)
110
110
 
111
111
  send_frame(
112
112
  Protocol::Access::Request.new(:realm => '/data', :read => true, :write => true, :active => true, :passive => true)
113
113
  )
114
114
  method = next_method
115
- raise ProtocolError, 'Access denied' unless method.is_a?(Protocol::Access::RequestOk)
115
+ raise API::ProtocolError, 'Access denied' unless method.is_a?(Protocol::Access::RequestOk)
116
116
  self.ticket = method.ticket
117
117
 
118
118
  # return status
@@ -122,14 +122,14 @@ module AMQP
122
122
  private
123
123
 
124
124
  def buffer
125
- @buffer ||= Buffer.new(self)
125
+ @buffer ||= Transport::Buffer.new(self)
126
126
  end
127
127
 
128
128
  def send_command(cmd, *args)
129
129
  begin
130
130
  socket.__send__(cmd, *args)
131
131
  rescue Errno::EPIPE, IOError => e
132
- raise ServerDownError, e.message
132
+ raise API::ServerDownError, e.message
133
133
  end
134
134
  end
135
135
 
@@ -147,7 +147,7 @@ module AMQP
147
147
  end
148
148
  @status = CONNECTED
149
149
  rescue SocketError, SystemCallError, IOError, Timeout::Error => e
150
- raise ServerDownError, e.message
150
+ raise API::ServerDownError, e.message
151
151
  end
152
152
 
153
153
  @socket
@@ -1,16 +1,24 @@
1
- class Bunny
1
+ module API
2
2
  class Exchange
3
3
 
4
- include AMQP
5
-
6
4
  attr_reader :client, :type, :name, :opts, :key
7
5
 
8
6
  def initialize(client, name, opts = {})
9
7
  # check connection to server
10
- raise ConnectionError, 'Not connected to server' if client.status == NOT_CONNECTED
8
+ raise API::ConnectionError, 'Not connected to server' if client.status == NOT_CONNECTED
11
9
 
12
10
  @client, @name, @opts = client, name, opts
13
- @type = opts[:type] || :direct
11
+
12
+ # set up the exchange type catering for default names
13
+ if name.match(/^amq\./)
14
+ new_type = name.sub(/amq\./, '')
15
+ # handle 'amq.match' default
16
+ new_type = 'headers' if new_type == 'match'
17
+ @type = new_type.to_sym
18
+ else
19
+ @type = opts[:type] || :direct
20
+ end
21
+
14
22
  @key = opts[:key]
15
23
  @client.exchanges[@name] ||= self
16
24
 
@@ -25,7 +33,7 @@ class Bunny
25
33
  )
26
34
  )
27
35
 
28
- raise ProtocolError,
36
+ raise API::ProtocolError,
29
37
  "Error declaring exchange #{name}: type = #{type}" unless
30
38
  client.next_method.is_a?(Protocol::Exchange::DeclareOk)
31
39
  end
@@ -46,7 +54,7 @@ class Bunny
46
54
  :priority => 0
47
55
  }.merge(opts)
48
56
  )
49
- out << Frame::Body.new(data)
57
+ out << Transport::Frame::Body.new(data)
50
58
 
51
59
  client.send_frame(*out)
52
60
  end
@@ -60,7 +68,7 @@ class Bunny
60
68
  Protocol::Exchange::Delete.new({ :exchange => name, :nowait => false }.merge(opts))
61
69
  )
62
70
 
63
- raise ProtocolError,
71
+ raise API::ProtocolError,
64
72
  "Error deleting exchange #{name}" unless
65
73
  client.next_method.is_a?(Protocol::Exchange::DeleteOk)
66
74
 
@@ -1,8 +1,6 @@
1
- class Bunny
1
+ module API
2
2
  class Header
3
3
 
4
- include AMQP
5
-
6
4
  attr_reader :client
7
5
 
8
6
  def initialize(client, header_obj)
@@ -1,18 +1,17 @@
1
- class Bunny
1
+ module API
2
2
  class Queue
3
3
 
4
- include AMQP
5
-
6
4
  attr_reader :name, :client
7
5
  attr_accessor :delivery_tag
8
6
 
9
7
  def initialize(client, name, opts = {})
10
8
  # check connection to server
11
- raise ConnectionError, 'Not connected to server' if client.status == NOT_CONNECTED
9
+ raise API::ConnectionError, 'Not connected to server' if client.status == NOT_CONNECTED
12
10
 
13
11
  @client = client
14
12
  @opts = opts
15
13
  @name = name
14
+ @delivery_tag = nil
16
15
 
17
16
  # ignore the :nowait option if passed, otherwise program will hang waiting for a
18
17
  # response that will not be sent by the server
@@ -22,7 +21,7 @@ class Bunny
22
21
  Protocol::Queue::Declare.new({ :queue => name, :nowait => false }.merge(opts))
23
22
  )
24
23
 
25
- raise ProtocolError, "Error declaring queue #{name}" unless client.next_method.is_a?(Protocol::Queue::DeclareOk)
24
+ raise API::ProtocolError, "Error declaring queue #{name}" unless client.next_method.is_a?(Protocol::Queue::DeclareOk)
26
25
  end
27
26
 
28
27
  def ack
@@ -54,7 +53,7 @@ class Bunny
54
53
  if method.is_a?(Protocol::Basic::GetEmpty) then
55
54
  return QUEUE_EMPTY
56
55
  elsif !method.is_a?(Protocol::Basic::GetOk)
57
- raise ProtocolError, "Error getting message from queue #{name}"
56
+ raise API::ProtocolError, "Error getting message from queue #{name}"
58
57
  end
59
58
 
60
59
  # get delivery tag to use for acknowledge
@@ -62,7 +61,7 @@ class Bunny
62
61
 
63
62
  header = client.next_payload
64
63
  msg = client.next_payload
65
- raise MessageError, 'unexpected length' if msg.length < header.size
64
+ raise API::MessageError, 'unexpected length' if msg.length < header.size
66
65
 
67
66
  hdr ? {:header => header, :payload => msg} : msg
68
67
 
@@ -90,7 +89,7 @@ class Bunny
90
89
  {:message_count => method.message_count, :consumer_count => method.consumer_count}
91
90
  end
92
91
 
93
- def subscribe(opts = {})
92
+ def subscribe(opts = {}, &blk)
94
93
  consumer_tag = opts[:consumer_tag] || name
95
94
 
96
95
  # ignore the :nowait option if passed, otherwise program will not wait for a
@@ -110,21 +109,31 @@ class Bunny
110
109
  :nowait => false }.merge(opts))
111
110
  )
112
111
 
113
- raise ProtocolError,
112
+ raise API::ProtocolError,
114
113
  "Error subscribing to queue #{name}" unless
115
114
  client.next_method.is_a?(Protocol::Basic::ConsumeOk)
116
115
 
117
- method = client.next_method
116
+ while true
117
+ method = client.next_method
118
+ break if method.is_a?(Protocol::Basic::CancelOk)
118
119
 
119
- # get delivery tag to use for acknowledge
120
- self.delivery_tag = method.delivery_tag if ack
120
+ # get delivery tag to use for acknowledge
121
+ self.delivery_tag = method.delivery_tag if ack
122
+
123
+ header = client.next_payload
124
+ msg = client.next_payload
125
+ raise API::MessageError, 'unexpected length' if msg.length < header.size
126
+
127
+ # pass the message to the block for processing
128
+ blk.call(hdr ? {:header => header, :payload => msg} : msg)
129
+ end
121
130
 
122
- header = client.next_payload
123
- msg = client.next_payload
124
- raise MessageError, 'unexpected length' if msg.length < header.size
125
-
126
- hdr ? {:header => header, :payload => msg} : msg
127
131
  end
132
+
133
+ def unsubscribe(opts = {})
134
+ consumer_tag = opts[:consumer_tag] || name
135
+ client.send_frame( Protocol::Basic::Cancel.new({ :consumer_tag => consumer_tag }.merge(opts)))
136
+ end
128
137
 
129
138
  def bind(exchange, opts = {})
130
139
  exchange = exchange.respond_to?(:name) ? exchange.name : exchange
@@ -141,7 +150,7 @@ class Bunny
141
150
  :nowait => false }.merge(opts))
142
151
  )
143
152
 
144
- raise ProtocolError,
153
+ raise API::ProtocolError,
145
154
  "Error binding queue #{name}" unless
146
155
  client.next_method.is_a?(Protocol::Queue::BindOk)
147
156
 
@@ -161,7 +170,7 @@ class Bunny
161
170
  )
162
171
  )
163
172
 
164
- raise ProtocolError,
173
+ raise API::ProtocolError,
165
174
  "Error unbinding queue #{name}" unless
166
175
  client.next_method.is_a?(Protocol::Queue::UnbindOk)
167
176
 
@@ -178,7 +187,7 @@ class Bunny
178
187
  Protocol::Queue::Delete.new({ :queue => name, :nowait => false }.merge(opts))
179
188
  )
180
189
 
181
- raise ProtocolError,
190
+ raise API::ProtocolError,
182
191
  "Error deleting queue #{name}" unless
183
192
  client.next_method.is_a?(Protocol::Queue::DeleteOk)
184
193