bunny 0.2.0 → 0.3.0

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