bunny 0.2.0 → 0.3.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.
@@ -1,12 +1,21 @@
1
- module API
1
+ module Bunny
2
+
3
+ =begin rdoc
4
+
5
+ === DESCRIPTION:
6
+
7
+ Queues store and forward messages. Queues can be configured in the server or created at runtime.
8
+ Queues must be attached to at least one exchange in order to receive messages from publishers.
9
+
10
+ =end
11
+
2
12
  class Queue
3
-
4
13
  attr_reader :name, :client
5
14
  attr_accessor :delivery_tag
6
15
 
7
16
  def initialize(client, name, opts = {})
8
17
  # check connection to server
9
- raise API::ConnectionError, 'Not connected to server' if client.status == NOT_CONNECTED
18
+ raise Bunny::ConnectionError, 'Not connected to server' if client.status == :not_connected
10
19
 
11
20
  @client = client
12
21
  @opts = opts
@@ -21,8 +30,25 @@ module API
21
30
  Protocol::Queue::Declare.new({ :queue => name, :nowait => false }.merge(opts))
22
31
  )
23
32
 
24
- raise API::ProtocolError, "Error declaring queue #{name}" unless client.next_method.is_a?(Protocol::Queue::DeclareOk)
33
+ raise Bunny::ProtocolError, "Error declaring queue #{name}" unless client.next_method.is_a?(Protocol::Queue::DeclareOk)
25
34
  end
35
+
36
+ =begin rdoc
37
+
38
+ === DESCRIPTION:
39
+
40
+ Acknowledges one or more messages delivered via the _Deliver_ or _Get_-_Ok_ methods. The client can
41
+ ask to confirm a single message or a set of messages up to and including a specific message.
42
+
43
+ ==== OPTIONS:
44
+
45
+ * <tt>:delivery_tag</tt>
46
+ * <tt>:multiple => true or false (_default_)</tt> - If set to _true_, the delivery tag is treated
47
+ as "up to and including", so that the client can acknowledge multiple messages with a single
48
+ method. If set to _false_, the delivery tag refers to a single message. If the multiple field
49
+ is _true_, and the delivery tag is zero, tells the server to acknowledge all outstanding messages.
50
+
51
+ =end
26
52
 
27
53
  def ack
28
54
  client.send_frame(
@@ -33,6 +59,29 @@ module API
33
59
  self.delivery_tag = nil
34
60
  end
35
61
 
62
+ =begin rdoc
63
+
64
+ === DESCRIPTION:
65
+
66
+ Gets a message from a queue in a synchronous way. If error occurs, raises _Bunny_::_ProtocolError_.
67
+
68
+ ==== OPTIONS:
69
+
70
+ * <tt>:header => true or false (_default_)</tt> - If set to _true_,
71
+ hash <tt>{:header, :delivery_details, :payload}</tt> is returned.
72
+ * <tt>:no_ack => true (_default_) or false</tt> - If set to _true_, the server does not expect an
73
+ acknowledgement message from the client. If set to _false_, the server expects an acknowledgement
74
+ message from the client and will re-queue the message if it does not receive one within a time specified
75
+ by the server.
76
+
77
+ ==== RETURNS:
78
+
79
+ If <tt>:header => true</tt> returns hash <tt>{:header, :delivery_details, :payload}</tt>. <tt>:delivery_details</tt> is
80
+ a hash <tt>{:delivery_tag, :redelivered, :exchange, :routing_key, :message_count}</tt>. If
81
+ <tt>:header => false</tt> only the message payload is returned.
82
+
83
+ =end
84
+
36
85
  def pop(opts = {})
37
86
 
38
87
  # do we want the message header?
@@ -51,9 +100,9 @@ module API
51
100
  method = client.next_method
52
101
 
53
102
  if method.is_a?(Protocol::Basic::GetEmpty) then
54
- return QUEUE_EMPTY
103
+ return :queue_empty
55
104
  elsif !method.is_a?(Protocol::Basic::GetOk)
56
- raise API::ProtocolError, "Error getting message from queue #{name}"
105
+ raise Bunny::ProtocolError, "Error getting message from queue #{name}"
57
106
  end
58
107
 
59
108
  # get delivery tag to use for acknowledge
@@ -61,39 +110,107 @@ module API
61
110
 
62
111
  header = client.next_payload
63
112
  msg = client.next_payload
64
- raise API::MessageError, 'unexpected length' if msg.length < header.size
113
+ raise Bunny::MessageError, 'unexpected length' if msg.length < header.size
65
114
 
66
- hdr ? {:header => header, :payload => msg} : msg
115
+ # Return message with additional info if requested
116
+ hdr ? {:header => header, :payload => msg, :delivery_details => method.arguments} : msg
67
117
 
68
118
  end
69
119
 
120
+ =begin rdoc
121
+
122
+ === DESCRIPTION:
123
+
124
+ Publishes a message to the queue via the default nameless '' direct exchange.
125
+
126
+ ==== RETURNS:
127
+
128
+ nil
129
+
130
+ =end
131
+
70
132
  def publish(data, opts = {})
71
133
  exchange.publish(data, opts)
72
134
  end
73
135
 
136
+ =begin rdoc
137
+
138
+ === DESCRIPTION:
139
+
140
+ Returns message count from Queue#status.
141
+
142
+ =end
143
+
74
144
  def message_count
75
145
  s = status
76
146
  s[:message_count]
77
147
  end
78
148
 
149
+ =begin rdoc
150
+
151
+ === DESCRIPTION:
152
+
153
+ Returns consumer count from Queue#status.
154
+
155
+ =end
156
+
79
157
  def consumer_count
80
158
  s = status
81
159
  s[:consumer_count]
82
160
  end
83
-
84
- def status(opts = {})
161
+
162
+ =begin rdoc
163
+
164
+ === DESCRIPTION:
165
+
166
+ Returns hash {:message_count, :consumer_count}.
167
+
168
+ =end
169
+
170
+ def status
85
171
  client.send_frame(
86
- Protocol::Queue::Declare.new({ :queue => name, :passive => true }.merge(opts))
172
+ Protocol::Queue::Declare.new({ :queue => name, :passive => true })
87
173
  )
88
174
  method = client.next_method
89
175
  {:message_count => method.message_count, :consumer_count => method.consumer_count}
90
176
  end
177
+
178
+ =begin rdoc
179
+
180
+ === DESCRIPTION:
181
+
182
+ Asks the server to start a "consumer", which is a transient request for messages from a specific
183
+ queue. Consumers last as long as the channel they were created on, or until the client cancels them
184
+ with an _unsubscribe_. Every time a message reaches the queue it is passed to the _blk_ for
185
+ processing. If error occurs, _Bunny_::_ProtocolError_ is raised.
186
+
187
+ ==== OPTIONS:
188
+ * <tt>:header => true or false (_default_)</tt> - If set to _true_, hash is delivered for each message
189
+ <tt>{:header, :delivery_details, :payload}</tt>.
190
+ * <tt>:consumer_tag => '_tag_'</tt> - Specifies the identifier for the consumer. The consumer tag is
191
+ local to a connection, so two clients can use the same consumer tags. If this field is empty the
192
+ queue name is used.
193
+ * <tt>:no_ack=> true (_default_) or false</tt> - If set to _true_, the server does not expect an
194
+ acknowledgement message from the client. If set to _false_, the server expects an acknowledgement
195
+ message from the client and will re-queue the message if it does not receive one within a time specified
196
+ by the server.
197
+ * <tt>:exclusive => true or false (_default_)</tt> - Request exclusive consumer access, meaning
198
+ only this consumer can access the queue.
199
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
200
+
201
+ ==== RETURNS:
202
+
203
+ If <tt>:header => true</tt> returns hash <tt>{:header, :delivery_details, :payload}</tt> for each message.
204
+ <tt>:delivery_details</tt> is a hash <tt>{:consumer_tag, :delivery_tag, :redelivered, :exchange, :routing_key}</tt>.
205
+ If <tt>:header => false</tt> only message payload is returned.
206
+
207
+ =end
91
208
 
92
209
  def subscribe(opts = {}, &blk)
93
210
  consumer_tag = opts[:consumer_tag] || name
94
211
 
95
- # ignore the :nowait option if passed, otherwise program will not wait for a
96
- # message to get to the server causing an error
212
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
213
+ # response from the server causing an error.
97
214
  opts.delete(:nowait)
98
215
 
99
216
  # do we want the message header?
@@ -109,12 +226,13 @@ module API
109
226
  :nowait => false }.merge(opts))
110
227
  )
111
228
 
112
- raise API::ProtocolError,
229
+ raise Bunny::ProtocolError,
113
230
  "Error subscribing to queue #{name}" unless
114
231
  client.next_method.is_a?(Protocol::Basic::ConsumeOk)
115
232
 
116
233
  while true
117
234
  method = client.next_method
235
+
118
236
  break if method.is_a?(Protocol::Basic::CancelOk)
119
237
 
120
238
  # get delivery tag to use for acknowledge
@@ -122,19 +240,56 @@ module API
122
240
 
123
241
  header = client.next_payload
124
242
  msg = client.next_payload
125
- raise API::MessageError, 'unexpected length' if msg.length < header.size
243
+ raise Bunny::MessageError, 'unexpected length' if msg.length < header.size
126
244
 
127
- # pass the message to the block for processing
128
- blk.call(hdr ? {:header => header, :payload => msg} : msg)
245
+ # pass the message and related info, if requested, to the block for processing
246
+ blk.call(hdr ? {:header => header, :payload => msg, :delivery_details => method.arguments} : msg)
129
247
  end
130
248
 
131
249
  end
132
250
 
251
+ =begin rdoc
252
+
253
+ === DESCRIPTION:
254
+
255
+ Cancels a consumer. This does not affect already delivered messages, but it does mean
256
+ the server will not send any more messages for that consumer.
257
+
258
+ ==== OPTIONS:
259
+
260
+ * <tt>:consumer_tag => '_tag_'</tt> - Specifies the identifier for the consumer.
261
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
262
+
263
+ =end
264
+
133
265
  def unsubscribe(opts = {})
134
266
  consumer_tag = opts[:consumer_tag] || name
267
+
268
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
269
+ # response from the server causing an error
270
+ opts.delete(:nowait)
271
+
135
272
  client.send_frame( Protocol::Basic::Cancel.new({ :consumer_tag => consumer_tag }.merge(opts)))
273
+
136
274
  end
137
275
 
276
+ =begin rdoc
277
+
278
+ === DESCRIPTION:
279
+
280
+ Binds a queue to an exchange. Until a queue is bound it will not receive any messages. Queues are
281
+ bound to the direct exchange '' by default. If error occurs, a _Bunny_::_ProtocolError_ is raised.
282
+
283
+ * <tt>:key => 'routing key'* <tt>:key => 'routing_key'</tt> - Specifies the routing key for
284
+ the binding. The routing key is used for routing messages depending on the exchange configuration.
285
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
286
+
287
+ ==== RETURNS:
288
+
289
+ <tt>:bind_ok</tt> if successful.
290
+
291
+ =end
292
+
138
293
  def bind(exchange, opts = {})
139
294
  exchange = exchange.respond_to?(:name) ? exchange.name : exchange
140
295
 
@@ -150,16 +305,38 @@ module API
150
305
  :nowait => false }.merge(opts))
151
306
  )
152
307
 
153
- raise API::ProtocolError,
308
+ raise Bunny::ProtocolError,
154
309
  "Error binding queue #{name}" unless
155
310
  client.next_method.is_a?(Protocol::Queue::BindOk)
156
311
 
157
312
  # return message
158
- BIND_SUCCEEDED
313
+ :bind_ok
159
314
  end
160
315
 
316
+ =begin rdoc
317
+
318
+ === DESCRIPTION:
319
+
320
+ Removes a queue binding from an exchange. If error occurs, a _Bunny_::_ProtocolError_ is raised.
321
+
322
+ ==== OPTIONS:
323
+ * <tt>:key => 'routing key'* <tt>:key => 'routing_key'</tt> - Specifies the routing key for
324
+ the binding.
325
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
326
+
327
+ ==== RETURNS:
328
+
329
+ <tt>:unbind_ok</tt> if successful.
330
+
331
+ =end
332
+
161
333
  def unbind(exchange, opts = {})
162
334
  exchange = exchange.respond_to?(:name) ? exchange.name : exchange
335
+
336
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
337
+ # response that will not be sent by the server
338
+ opts.delete(:nowait)
339
+
163
340
  bindings.delete(exchange)
164
341
 
165
342
  client.send_frame(
@@ -170,13 +347,36 @@ module API
170
347
  )
171
348
  )
172
349
 
173
- raise API::ProtocolError,
350
+ raise Bunny::ProtocolError,
174
351
  "Error unbinding queue #{name}" unless
175
352
  client.next_method.is_a?(Protocol::Queue::UnbindOk)
176
353
 
177
354
  # return message
178
- UNBIND_SUCCEEDED
355
+ :unbind_ok
179
356
  end
357
+
358
+ =begin rdoc
359
+
360
+ === DESCRIPTION:
361
+
362
+ Requests that a queue is deleted from broker/server. When a queue is deleted any pending messages
363
+ are sent to a dead-letter queue if this is defined in the server configuration. Removes reference
364
+ from queues if successful. If an error occurs raises _Bunny_::_ProtocolError_.
365
+
366
+ ==== Options:
367
+
368
+ * <tt>:if_unused => true or false (_default_)</tt> - If set to _true_, the server will only
369
+ delete the queue if it has no consumers. If the queue has consumers the server does not
370
+ delete it but raises a channel exception instead.
371
+ * <tt>:if_empty => true or false (_default_)</tt> - If set to _true_, the server will only
372
+ delete the queue if it has no messages. If the queue is not empty the server raises a channel
373
+ exception.
374
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
375
+
376
+ ==== Returns:
377
+
378
+ <tt>:delete_ok</tt> if successful
379
+ =end
180
380
 
181
381
  def delete(opts = {})
182
382
  # ignore the :nowait option if passed, otherwise program will hang waiting for a
@@ -187,14 +387,14 @@ module API
187
387
  Protocol::Queue::Delete.new({ :queue => name, :nowait => false }.merge(opts))
188
388
  )
189
389
 
190
- raise API::ProtocolError,
390
+ raise Bunny::ProtocolError,
191
391
  "Error deleting queue #{name}" unless
192
392
  client.next_method.is_a?(Protocol::Queue::DeleteOk)
193
393
 
194
394
  client.queues.delete(name)
195
395
 
196
396
  # return confirmation
197
- QUEUE_DELETED
397
+ :delete_ok
198
398
  end
199
399
 
200
400
  private
@@ -0,0 +1,266 @@
1
+ module Bunny
2
+ module Transport #:nodoc: all
3
+ class Buffer
4
+
5
+ def initialize data = ''
6
+ @data = data
7
+ @pos = 0
8
+ end
9
+
10
+ attr_reader :pos
11
+
12
+ def data
13
+ @data.clone
14
+ end
15
+ alias :contents :data
16
+ alias :to_s :data
17
+
18
+ def << data
19
+ @data << data.to_s
20
+ self
21
+ end
22
+
23
+ def length
24
+ @data.length
25
+ end
26
+
27
+ def empty?
28
+ pos == length
29
+ end
30
+
31
+ def rewind
32
+ @pos = 0
33
+ end
34
+
35
+ def read_properties *types
36
+ types.shift if types.first == :properties
37
+
38
+ i = 0
39
+ values = []
40
+
41
+ while props = read(:short)
42
+ (0..14).each do |n|
43
+ # no more property types
44
+ break unless types[i]
45
+
46
+ # if flag is set
47
+ if props & (1<<(15-n)) != 0
48
+ if types[i] == :bit
49
+ # bit values exist in flags only
50
+ values << true
51
+ else
52
+ # save type name for later reading
53
+ values << types[i]
54
+ end
55
+ else
56
+ # property not set or is false bit
57
+ values << (types[i] == :bit ? false : nil)
58
+ end
59
+
60
+ i+=1
61
+ end
62
+
63
+ # bit(0) == 0 means no more property flags
64
+ break unless props & 1 == 1
65
+ end
66
+
67
+ values.map do |value|
68
+ value.is_a?(Symbol) ? read(value) : value
69
+ end
70
+ end
71
+
72
+ def read *types
73
+ if types.first == :properties
74
+ return read_properties(*types)
75
+ end
76
+
77
+ values = types.map do |type|
78
+ case type
79
+ when :octet
80
+ _read(1, 'C')
81
+ when :short
82
+ _read(2, 'n')
83
+ when :long
84
+ _read(4, 'N')
85
+ when :longlong
86
+ upper, lower = _read(8, 'NN')
87
+ upper << 32 | lower
88
+ when :shortstr
89
+ _read read(:octet)
90
+ when :longstr
91
+ _read read(:long)
92
+ when :timestamp
93
+ Time.at read(:longlong)
94
+ when :table
95
+ t = Hash.new
96
+
97
+ table = Buffer.new(read(:longstr))
98
+ until table.empty?
99
+ key, type = table.read(:shortstr, :octet)
100
+ key = key.intern
101
+ t[key] ||= case type
102
+ when 83 # 'S'
103
+ table.read(:longstr)
104
+ when 73 # 'I'
105
+ table.read(:long)
106
+ when 68 # 'D'
107
+ exp = table.read(:octet)
108
+ num = table.read(:long)
109
+ num / 10.0**exp
110
+ when 84 # 'T'
111
+ table.read(:timestamp)
112
+ when 70 # 'F'
113
+ table.read(:table)
114
+ end
115
+ end
116
+
117
+ t
118
+ when :bit
119
+ if (@bits ||= []).empty?
120
+ val = read(:octet)
121
+ @bits = (0..7).map{|i| (val & 1<<i) != 0 }
122
+ end
123
+
124
+ @bits.shift
125
+ else
126
+ raise Bunny::InvalidTypeError, "Cannot read data of type #{type}"
127
+ end
128
+ end
129
+
130
+ types.size == 1 ? values.first : values
131
+ end
132
+
133
+ def write type, data
134
+ case type
135
+ when :octet
136
+ _write(data, 'C')
137
+ when :short
138
+ _write(data, 'n')
139
+ when :long
140
+ _write(data, 'N')
141
+ when :longlong
142
+ lower = data & 0xffffffff
143
+ upper = (data & ~0xffffffff) >> 32
144
+ _write([upper, lower], 'NN')
145
+ when :shortstr
146
+ data = (data || '').to_s
147
+ _write([data.length, data], 'Ca*')
148
+ when :longstr
149
+ if data.is_a? Hash
150
+ write(:table, data)
151
+ else
152
+ data = (data || '').to_s
153
+ _write([data.length, data], 'Na*')
154
+ end
155
+ when :timestamp
156
+ write(:longlong, data.to_i)
157
+ when :table
158
+ data ||= {}
159
+ write :longstr, (data.inject(Buffer.new) do |table, (key, value)|
160
+ table.write(:shortstr, key.to_s)
161
+
162
+ case value
163
+ when String
164
+ table.write(:octet, 83) # 'S'
165
+ table.write(:longstr, value.to_s)
166
+ when Fixnum
167
+ table.write(:octet, 73) # 'I'
168
+ table.write(:long, value)
169
+ when Float
170
+ table.write(:octet, 68) # 'D'
171
+ # XXX there's gotta be a better way to do this..
172
+ exp = value.to_s.split('.').last.length
173
+ num = value * 10**exp
174
+ table.write(:octet, exp)
175
+ table.write(:long, num)
176
+ when Time
177
+ table.write(:octet, 84) # 'T'
178
+ table.write(:timestamp, value)
179
+ when Hash
180
+ table.write(:octet, 70) # 'F'
181
+ table.write(:table, value)
182
+ end
183
+
184
+ table
185
+ end)
186
+ when :bit
187
+ [*data].to_enum(:each_slice, 8).each{|bits|
188
+ write(:octet, bits.enum_with_index.inject(0){ |byte, (bit, i)|
189
+ byte |= 1<<i if bit
190
+ byte
191
+ })
192
+ }
193
+ when :properties
194
+ values = []
195
+ data.enum_with_index.inject(0) do |short, ((type, value), i)|
196
+ n = i % 15
197
+ last = i+1 == data.size
198
+
199
+ if (n == 0 and i != 0) or last
200
+ if data.size > i+1
201
+ short |= 1<<0
202
+ elsif last and value
203
+ values << [type,value]
204
+ short |= 1<<(15-n)
205
+ end
206
+
207
+ write(:short, short)
208
+ short = 0
209
+ end
210
+
211
+ if value and !last
212
+ values << [type,value]
213
+ short |= 1<<(15-n)
214
+ end
215
+
216
+ short
217
+ end
218
+
219
+ values.each do |type, value|
220
+ write(type, value) unless type == :bit
221
+ end
222
+ else
223
+ raise Bunny::InvalidTypeError, "Cannot write data of type #{type}"
224
+ end
225
+
226
+ self
227
+ end
228
+
229
+ def extract
230
+ begin
231
+ cur_data, cur_pos = @data.clone, @pos
232
+ yield self
233
+ rescue Bunny::BufferOverflowError
234
+ @data, @pos = cur_data, cur_pos
235
+ nil
236
+ end
237
+ end
238
+
239
+ def _read(size, pack = nil)
240
+ if @data.is_a?(Bunny::Client)
241
+ raw = @data.read(size)
242
+ return raw if raw.nil? or pack.nil?
243
+ return raw.unpack(pack).first
244
+ end
245
+
246
+ if @pos + size > length
247
+ raise Bunny::BufferOverflowError
248
+ else
249
+ data = @data[@pos,size]
250
+ @data[@pos,size] = ''
251
+ if pack
252
+ data = data.unpack(pack)
253
+ data = data.pop if data.size == 1
254
+ end
255
+ data
256
+ end
257
+ end
258
+
259
+ def _write data, pack = nil
260
+ data = [*data].pack(pack) if pack
261
+ @data[@pos,0] = data
262
+ @pos += data.length
263
+ end
264
+ end
265
+ end
266
+ end