maxlapshin-carrot 0.6.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.
- data/README.markdown +34 -0
- data/VERSION.yml +4 -0
- data/lib/amqp/buffer.rb +401 -0
- data/lib/amqp/exchange.rb +51 -0
- data/lib/amqp/frame.rb +121 -0
- data/lib/amqp/header.rb +27 -0
- data/lib/amqp/protocol.rb +209 -0
- data/lib/amqp/queue.rb +94 -0
- data/lib/amqp/server.rb +184 -0
- data/lib/amqp/spec.rb +820 -0
- data/lib/carrot.rb +91 -0
- data/lib/examples/simple_pop.rb +13 -0
- data/test/carrot_test.rb +8 -0
- data/test/test_helper.rb +18 -0
- metadata +69 -0
data/README.markdown
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Carrot
|
2
|
+
|
3
|
+
A synchronous amqp client. Based on Aman's amqp client:
|
4
|
+
|
5
|
+
[http://github.com/tmm1/amqp/tree/master] (http://github.com/tmm1/amqp/tree/master)
|
6
|
+
|
7
|
+
## Motivation
|
8
|
+
|
9
|
+
This client does not use eventmachine so no background thread necessary. As a result, it is much easier to use from script/console and Passenger. It also solves the problem of buffering messages and ack responses. For more details see [this thread] (http://groups.google.com/group/ruby-amqp/browse_thread/thread/fdae324a0ebb1961/fa185fdce1841b68).
|
10
|
+
|
11
|
+
There is currently no way to prevent buffering using eventmachine. Support for prefetch is still unreliable.
|
12
|
+
|
13
|
+
|
14
|
+
## Example
|
15
|
+
|
16
|
+
require 'carrot'
|
17
|
+
|
18
|
+
q = Carrot.queue('name')
|
19
|
+
10.times do |num|
|
20
|
+
q.publish(num.to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
puts "Queued #{q.message_count} messages"
|
24
|
+
puts
|
25
|
+
|
26
|
+
while msg = q.pop(:ack => true)
|
27
|
+
puts "Popping: #{msg}"
|
28
|
+
q.ack
|
29
|
+
end
|
30
|
+
Carrot.stop
|
31
|
+
|
32
|
+
# LICENSE
|
33
|
+
|
34
|
+
Copyright (c) 2009 Amos Elliston, Geni.com; Published under The MIT License, see License
|
data/VERSION.yml
ADDED
data/lib/amqp/buffer.rb
ADDED
@@ -0,0 +1,401 @@
|
|
1
|
+
if [].map.respond_to? :with_index
|
2
|
+
class Array #:nodoc:
|
3
|
+
def enum_with_index
|
4
|
+
each.with_index
|
5
|
+
end
|
6
|
+
end
|
7
|
+
else
|
8
|
+
require 'enumerator'
|
9
|
+
end
|
10
|
+
|
11
|
+
module Carrot::AMQP
|
12
|
+
class Buffer #:nodoc: all
|
13
|
+
class Overflow < StandardError; end
|
14
|
+
class InvalidType < StandardError; end
|
15
|
+
|
16
|
+
def initialize data = ''
|
17
|
+
@data = data
|
18
|
+
@pos = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :pos
|
22
|
+
|
23
|
+
def data
|
24
|
+
@data.clone
|
25
|
+
end
|
26
|
+
alias :contents :data
|
27
|
+
alias :to_s :data
|
28
|
+
|
29
|
+
def << data
|
30
|
+
@data << data.to_s
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def length
|
35
|
+
@data.length
|
36
|
+
end
|
37
|
+
|
38
|
+
def empty?
|
39
|
+
pos == length
|
40
|
+
end
|
41
|
+
|
42
|
+
def rewind
|
43
|
+
@pos = 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def read_properties *types
|
47
|
+
types.shift if types.first == :properties
|
48
|
+
|
49
|
+
i = 0
|
50
|
+
values = []
|
51
|
+
|
52
|
+
while props = read(:short)
|
53
|
+
(0..14).each do |n|
|
54
|
+
# no more property types
|
55
|
+
break unless types[i]
|
56
|
+
|
57
|
+
# if flag is set
|
58
|
+
if props & (1<<(15-n)) != 0
|
59
|
+
if types[i] == :bit
|
60
|
+
# bit values exist in flags only
|
61
|
+
values << true
|
62
|
+
else
|
63
|
+
# save type name for later reading
|
64
|
+
values << types[i]
|
65
|
+
end
|
66
|
+
else
|
67
|
+
# property not set or is false bit
|
68
|
+
values << (types[i] == :bit ? false : nil)
|
69
|
+
end
|
70
|
+
|
71
|
+
i+=1
|
72
|
+
end
|
73
|
+
|
74
|
+
# bit(0) == 0 means no more property flags
|
75
|
+
break unless props & 1 == 1
|
76
|
+
end
|
77
|
+
|
78
|
+
values.map do |value|
|
79
|
+
value.is_a?(Symbol) ? read(value) : value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def read *types
|
84
|
+
if types.first == :properties
|
85
|
+
return read_properties(*types)
|
86
|
+
end
|
87
|
+
|
88
|
+
values = types.map do |type|
|
89
|
+
case type
|
90
|
+
when :octet
|
91
|
+
_read(1, 'C')
|
92
|
+
when :short
|
93
|
+
_read(2, 'n')
|
94
|
+
when :long
|
95
|
+
_read(4, 'N')
|
96
|
+
when :longlong
|
97
|
+
upper, lower = _read(8, 'NN')
|
98
|
+
upper << 32 | lower
|
99
|
+
when :shortstr
|
100
|
+
_read read(:octet)
|
101
|
+
when :longstr
|
102
|
+
_read read(:long)
|
103
|
+
when :timestamp
|
104
|
+
Time.at read(:longlong)
|
105
|
+
when :table
|
106
|
+
t = Hash.new
|
107
|
+
|
108
|
+
table = Buffer.new(read(:longstr))
|
109
|
+
until table.empty?
|
110
|
+
key, type = table.read(:shortstr, :octet)
|
111
|
+
key = key.intern
|
112
|
+
t[key] ||= case type
|
113
|
+
when 83 # 'S'
|
114
|
+
table.read(:longstr)
|
115
|
+
when 73 # 'I'
|
116
|
+
table.read(:long)
|
117
|
+
when 68 # 'D'
|
118
|
+
exp = table.read(:octet)
|
119
|
+
num = table.read(:long)
|
120
|
+
num / 10.0**exp
|
121
|
+
when 84 # 'T'
|
122
|
+
table.read(:timestamp)
|
123
|
+
when 70 # 'F'
|
124
|
+
table.read(:table)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
t
|
129
|
+
when :bit
|
130
|
+
if (@bits ||= []).empty?
|
131
|
+
val = read(:octet)
|
132
|
+
@bits = (0..7).map{|i| (val & 1<<i) != 0 }
|
133
|
+
end
|
134
|
+
|
135
|
+
@bits.shift
|
136
|
+
else
|
137
|
+
raise InvalidType, "Cannot read data of type #{type}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
types.size == 1 ? values.first : values
|
142
|
+
end
|
143
|
+
|
144
|
+
def write type, data
|
145
|
+
case type
|
146
|
+
when :octet
|
147
|
+
_write(data, 'C')
|
148
|
+
when :short
|
149
|
+
_write(data, 'n')
|
150
|
+
when :long
|
151
|
+
_write(data, 'N')
|
152
|
+
when :longlong
|
153
|
+
lower = data & 0xffffffff
|
154
|
+
upper = (data & ~0xffffffff) >> 32
|
155
|
+
_write([upper, lower], 'NN')
|
156
|
+
when :shortstr
|
157
|
+
data = (data || '').to_s
|
158
|
+
_write([data.length, data], 'Ca*')
|
159
|
+
when :longstr
|
160
|
+
if data.is_a? Hash
|
161
|
+
write(:table, data)
|
162
|
+
else
|
163
|
+
data = (data || '').to_s
|
164
|
+
_write([data.length, data], 'Na*')
|
165
|
+
end
|
166
|
+
when :timestamp
|
167
|
+
write(:longlong, data.to_i)
|
168
|
+
when :table
|
169
|
+
data ||= {}
|
170
|
+
write :longstr, (data.inject(Buffer.new) do |table, (key, value)|
|
171
|
+
table.write(:shortstr, key.to_s)
|
172
|
+
|
173
|
+
case value
|
174
|
+
when String
|
175
|
+
table.write(:octet, 83) # 'S'
|
176
|
+
table.write(:longstr, value.to_s)
|
177
|
+
when Fixnum
|
178
|
+
table.write(:octet, 73) # 'I'
|
179
|
+
table.write(:long, value)
|
180
|
+
when Float
|
181
|
+
table.write(:octet, 68) # 'D'
|
182
|
+
# XXX there's gotta be a better way to do this..
|
183
|
+
exp = value.to_s.split('.').last.length
|
184
|
+
num = value * 10**exp
|
185
|
+
table.write(:octet, exp)
|
186
|
+
table.write(:long, num)
|
187
|
+
when Time
|
188
|
+
table.write(:octet, 84) # 'T'
|
189
|
+
table.write(:timestamp, value)
|
190
|
+
when Hash
|
191
|
+
table.write(:octet, 70) # 'F'
|
192
|
+
table.write(:table, value)
|
193
|
+
end
|
194
|
+
|
195
|
+
table
|
196
|
+
end)
|
197
|
+
when :bit
|
198
|
+
[*data].to_enum(:each_slice, 8).each{|bits|
|
199
|
+
write(:octet, bits.enum_with_index.inject(0){ |byte, (bit, i)|
|
200
|
+
byte |= 1<<i if bit
|
201
|
+
byte
|
202
|
+
})
|
203
|
+
}
|
204
|
+
when :properties
|
205
|
+
values = []
|
206
|
+
data.enum_with_index.inject(0) do |short, ((type, value), i)|
|
207
|
+
n = i % 15
|
208
|
+
last = i+1 == data.size
|
209
|
+
|
210
|
+
if (n == 0 and i != 0) or last
|
211
|
+
if data.size > i+1
|
212
|
+
short |= 1<<0
|
213
|
+
elsif last and value
|
214
|
+
values << [type,value]
|
215
|
+
short |= 1<<(15-n)
|
216
|
+
end
|
217
|
+
|
218
|
+
write(:short, short)
|
219
|
+
short = 0
|
220
|
+
end
|
221
|
+
|
222
|
+
if value and !last
|
223
|
+
values << [type,value]
|
224
|
+
short |= 1<<(15-n)
|
225
|
+
end
|
226
|
+
|
227
|
+
short
|
228
|
+
end
|
229
|
+
|
230
|
+
values.each do |type, value|
|
231
|
+
write(type, value) unless type == :bit
|
232
|
+
end
|
233
|
+
else
|
234
|
+
raise InvalidType, "Cannot write data of type #{type}"
|
235
|
+
end
|
236
|
+
|
237
|
+
self
|
238
|
+
end
|
239
|
+
|
240
|
+
def extract
|
241
|
+
begin
|
242
|
+
cur_data, cur_pos = @data.clone, @pos
|
243
|
+
yield self
|
244
|
+
rescue Overflow
|
245
|
+
@data, @pos = cur_data, cur_pos
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def _read(size, pack = nil)
|
251
|
+
if @data.is_a?(Server)
|
252
|
+
raw = @data.read(size)
|
253
|
+
return raw if raw.nil? or pack.nil?
|
254
|
+
return raw.unpack(pack).first
|
255
|
+
end
|
256
|
+
|
257
|
+
if @pos + size > length
|
258
|
+
raise Overflow
|
259
|
+
else
|
260
|
+
data = @data[@pos,size]
|
261
|
+
@data[@pos,size] = ''
|
262
|
+
if pack
|
263
|
+
data = data.unpack(pack)
|
264
|
+
data = data.pop if data.size == 1
|
265
|
+
end
|
266
|
+
data
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def _write data, pack = nil
|
271
|
+
data = [*data].pack(pack) if pack
|
272
|
+
@data[@pos,0] = data
|
273
|
+
@pos += data.length
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
if $0 =~ /bacon/ or $0 == __FILE__
|
279
|
+
require 'bacon'
|
280
|
+
include AMQP
|
281
|
+
|
282
|
+
describe Buffer do
|
283
|
+
before do
|
284
|
+
@buf = Buffer.new
|
285
|
+
end
|
286
|
+
|
287
|
+
should 'have contents' do
|
288
|
+
@buf.contents.should == ''
|
289
|
+
end
|
290
|
+
|
291
|
+
should 'initialize with data' do
|
292
|
+
@buf = Buffer.new('abc')
|
293
|
+
@buf.contents.should == 'abc'
|
294
|
+
end
|
295
|
+
|
296
|
+
should 'append raw data' do
|
297
|
+
@buf << 'abc'
|
298
|
+
@buf << 'def'
|
299
|
+
@buf.contents.should == 'abcdef'
|
300
|
+
end
|
301
|
+
|
302
|
+
should 'append other buffers' do
|
303
|
+
@buf << Buffer.new('abc')
|
304
|
+
@buf.data.should == 'abc'
|
305
|
+
end
|
306
|
+
|
307
|
+
should 'have a position' do
|
308
|
+
@buf.pos.should == 0
|
309
|
+
end
|
310
|
+
|
311
|
+
should 'have a length' do
|
312
|
+
@buf.length.should == 0
|
313
|
+
@buf << 'abc'
|
314
|
+
@buf.length.should == 3
|
315
|
+
end
|
316
|
+
|
317
|
+
should 'know the end' do
|
318
|
+
@buf.empty?.should == true
|
319
|
+
end
|
320
|
+
|
321
|
+
should 'read and write data' do
|
322
|
+
@buf._write('abc')
|
323
|
+
@buf.rewind
|
324
|
+
@buf._read(2).should == 'ab'
|
325
|
+
@buf._read(1).should == 'c'
|
326
|
+
end
|
327
|
+
|
328
|
+
should 'raise on overflow' do
|
329
|
+
lambda{ @buf._read(1) }.should.raise Buffer::Overflow
|
330
|
+
end
|
331
|
+
|
332
|
+
should 'raise on invalid types' do
|
333
|
+
lambda{ @buf.read(:junk) }.should.raise Buffer::InvalidType
|
334
|
+
lambda{ @buf.write(:junk, 1) }.should.raise Buffer::InvalidType
|
335
|
+
end
|
336
|
+
|
337
|
+
{ :octet => 0b10101010,
|
338
|
+
:short => 100,
|
339
|
+
:long => 100_000_000,
|
340
|
+
:longlong => 666_555_444_333_222_111,
|
341
|
+
:shortstr => 'hello',
|
342
|
+
:longstr => 'bye'*500,
|
343
|
+
:timestamp => time = Time.at(Time.now.to_i),
|
344
|
+
:table => { :this => 'is', :a => 'hash', :with => {:nested => 123, :and => time, :also => 123.456} },
|
345
|
+
:bit => true
|
346
|
+
}.each do |type, value|
|
347
|
+
|
348
|
+
should "read and write a #{type}" do
|
349
|
+
@buf.write(type, value)
|
350
|
+
@buf.rewind
|
351
|
+
@buf.read(type).should == value
|
352
|
+
@buf.should.be.empty
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
|
357
|
+
should 'read and write multiple bits' do
|
358
|
+
bits = [true, false, false, true, true, false, false, true, true, false]
|
359
|
+
@buf.write(:bit, bits)
|
360
|
+
@buf.write(:octet, 100)
|
361
|
+
|
362
|
+
@buf.rewind
|
363
|
+
|
364
|
+
bits.map do
|
365
|
+
@buf.read(:bit)
|
366
|
+
end.should == bits
|
367
|
+
@buf.read(:octet).should == 100
|
368
|
+
end
|
369
|
+
|
370
|
+
should 'read and write properties' do
|
371
|
+
properties = ([
|
372
|
+
[:octet, 1],
|
373
|
+
[:shortstr, 'abc'],
|
374
|
+
[:bit, true],
|
375
|
+
[:bit, false],
|
376
|
+
[:shortstr, nil],
|
377
|
+
[:timestamp, nil],
|
378
|
+
[:table, { :a => 'hash' }],
|
379
|
+
]*5).sort_by{rand}
|
380
|
+
|
381
|
+
@buf.write(:properties, properties)
|
382
|
+
@buf.rewind
|
383
|
+
@buf.read(:properties, *properties.map{|type,_| type }).should == properties.map{|_,value| value }
|
384
|
+
@buf.should.be.empty
|
385
|
+
end
|
386
|
+
|
387
|
+
should 'do transactional reads with #extract' do
|
388
|
+
@buf.write :octet, 8
|
389
|
+
orig = @buf.to_s
|
390
|
+
|
391
|
+
@buf.rewind
|
392
|
+
@buf.extract do |b|
|
393
|
+
b.read :octet
|
394
|
+
b.read :short
|
395
|
+
end
|
396
|
+
|
397
|
+
@buf.pos.should == 0
|
398
|
+
@buf.data.should == orig
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Carrot::AMQP
|
2
|
+
class Exchange
|
3
|
+
attr_reader :server, :type, :name, :opts, :key, :carrot
|
4
|
+
|
5
|
+
def initialize(carrot, type, name, opts = {})
|
6
|
+
@server, @type, @name, @opts = carrot.server, type, name, opts
|
7
|
+
@key = opts[:key]
|
8
|
+
@carrot = carrot
|
9
|
+
|
10
|
+
unless name == "amq.#{type}" or name == ''
|
11
|
+
server.send_frame(
|
12
|
+
Protocol::Exchange::Declare.new(
|
13
|
+
{ :exchange => name, :type => type, :nowait => true }.merge(opts)
|
14
|
+
)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
attr_reader :name, :type, :key
|
19
|
+
|
20
|
+
def publish(data, opts = {})
|
21
|
+
out = []
|
22
|
+
|
23
|
+
out << Protocol::Basic::Publish.new(
|
24
|
+
{ :exchange => name, :routing_key => opts.delete(:key) || key }.merge(opts)
|
25
|
+
)
|
26
|
+
data = data.to_s
|
27
|
+
out << Protocol::Header.new(
|
28
|
+
Protocol::Basic,
|
29
|
+
data.length, {
|
30
|
+
:content_type => 'application/octet-stream',
|
31
|
+
:delivery_mode => (opts.delete(:persistent) ? 2 : 1),
|
32
|
+
:priority => 0
|
33
|
+
}.merge(opts)
|
34
|
+
)
|
35
|
+
out << Frame::Body.new(data)
|
36
|
+
|
37
|
+
server.send_frame(*out)
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete(opts = {})
|
41
|
+
server.send_frame(
|
42
|
+
Protocol::Exchange::Delete.new({ :exchange => name, :nowait => true }.merge(opts))
|
43
|
+
)
|
44
|
+
carrot.exchanges.delete(name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset
|
48
|
+
initialize(server, type, name, opts)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|