arvicco-amqp 0.6.8 → 0.6.9

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/HISTORY CHANGED
@@ -4,4 +4,8 @@
4
4
 
5
5
  == 0.6.8 / 2010-11-03
6
6
 
7
- * Additional improvements from latest forks added
7
+ * Additional improvements from latest forks added
8
+
9
+ == 0.6.9 / 2010-11-04
10
+
11
+ * Exchange#publish raises error if no connection to broker
data/README.md CHANGED
@@ -9,26 +9,24 @@ This library was tested primarily with RabbitMQ, although it should be compatibl
9
9
  server implementing the [AMQP 0-8 spec](http://www.amqp.org/confluence/download/attachments/720900/amqp0-8.pdf).
10
10
 
11
11
 
12
- This fork of AMQP
13
- =================
12
+ This fork of AMQP contains following improvements:
13
+ --------------------------------------------------
14
14
 
15
- Contains following improvements:
16
15
  * Support for setting extended headers in MQ::Exchange#publish. Particularly useful for :reply_to for RPC.
17
16
  * Multibyte character support (Ruby 1.9) in MQ::Exchange#publish.
18
- * MQ::Queue only wraps headers with new MQ::Headers if they are not nil. This allows pops to tell more easily when they've requested a message from an empty queue.
17
+ * MQ::Exchange#publish raises error if no connection to broker.
18
+ * MQ::Queue only wraps headers with new MQ::Headers if they are not nil. This allows pops to tell more easily when they've requested a message from an empty queue. See (https://github.com/tmm1/amqp/issues#issue/22)
19
19
  * Support for receiving Headers with zero-size data packets. Such contents with no body frames are totally legit if indicated header size is zero.
20
-
20
+ # * Support for AMQP::Protocol::Basic::Return method. See (https://github.com/tmm1/amqp/issues#issue/1).
21
21
 
22
22
  Getting started
23
23
  ===============
24
24
 
25
- To use examples with RabbitMQ, first [install the broker](http://www.rabbitmq.com/install.html).
26
- If you have Mercurial and Erlang/OTP installed, here is how to do it in 4 lines:
25
+ First things first, start with:
27
26
 
28
- hg clone http://hg.rabbitmq.com/rabbitmq-codegen
29
- hg clone http://hg.rabbitmq.com/rabbitmq-server
30
- cd rabbitmq-server
31
- make run
27
+ $ gem install arvicco-amqp
28
+
29
+ To use examples with RabbitMQ, first [install the broker](http://www.rabbitmq.com/install.html).
32
30
 
33
31
  Then have a look at the various bundled examples:
34
32
 
@@ -76,12 +74,14 @@ code.
76
74
  Same separate thread technique can be used to make EventMachine play nicely with other
77
75
  libraries that would block current thread (like [File::Tail](http://rubygems.org/gems/file-tail)).
78
76
 
79
- AMQP gem mailing list
80
- ==============================
77
+ AMQP gem resources
78
+ ==================
81
79
 
82
80
  * [AMQP gem mailing list](http://groups.google.com/group/ruby-amqp)
83
81
  * [AMQP gem at GitHub](http://github.com/tmm1/amqp)
84
- * [AMQP gem at Gemcutter](http://rubygems.org/gems/amqp)
82
+ * [AMQP gem at Rubygems](http://rubygems.org/gems/amqp)
83
+ * [This fork at GitHub](http://github.com/arvicco/amqp)
84
+ * [This fork as a gem at Rubygems](http://rubygems.org/gems/arvicco-amqp)
85
85
 
86
86
  Running specifications suite
87
87
  ============================
@@ -94,6 +94,13 @@ The lib/amqp/spec.rb file is generated automatically based on the [AMQP specific
94
94
 
95
95
  rake codegen
96
96
 
97
+ Writing your own AMQP/evented specs
98
+ ===================================
99
+
100
+ Writing evented specs properly is a bit difficult due to gory details of event loop management. Libraries such as
101
+ [AMQP-Spec](http://github.com/arvicco/amqp-spec) and [Moqueue](https://github.com/danielsdeleo/moqueue) may be helpful.
102
+
103
+
97
104
  Credits and more information
98
105
  ============================
99
106
 
data/Rakefile CHANGED
@@ -23,7 +23,7 @@ task :codegen do
23
23
  sh 'ruby lib/amqp/spec.rb'
24
24
  end
25
25
 
26
- desc "Run spec suite (uses bacon gem)"
26
+ desc "Run test suite (uses bacon gem)"
27
27
  task :test do
28
28
  sh 'bacon lib/amqp.rb'
29
29
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.8
1
+ 0.6.9
data/lib/amqp.rb ADDED
@@ -0,0 +1,110 @@
1
+ require File.expand_path('../ext/em', __FILE__)
2
+ require File.expand_path('../ext/blankslate', __FILE__)
3
+
4
+ %w[ version buffer spec protocol frame client ].each do |file|
5
+ require File.expand_path("../amqp/#{file}", __FILE__)
6
+ end
7
+
8
+ module AMQP
9
+ class << self
10
+ @logging = false
11
+ attr_accessor :logging
12
+ attr_reader :conn, :closing
13
+ alias :closing? :closing
14
+ alias :connection :conn
15
+ end
16
+
17
+ def self.connect *args
18
+ Client.connect *args
19
+ end
20
+
21
+ def self.settings
22
+ @settings ||= {
23
+ # server address
24
+ :host => '127.0.0.1',
25
+ :port => PORT,
26
+
27
+ # login details
28
+ :user => 'guest',
29
+ :pass => 'guest',
30
+ :vhost => '/',
31
+
32
+ # connection timeout
33
+ :timeout => nil,
34
+
35
+ # logging
36
+ :logging => false,
37
+
38
+ # ssl
39
+ :ssl => false
40
+ }
41
+ end
42
+
43
+ # Must be called to startup the connection to the AMQP server.
44
+ #
45
+ # The method takes several arguments and an optional block.
46
+ #
47
+ # This takes any option that is also accepted by EventMachine::connect.
48
+ # Additionally, there are several AMQP-specific options.
49
+ #
50
+ # * :user => String (default 'guest')
51
+ # The username as defined by the AMQP server.
52
+ # * :pass => String (default 'guest')
53
+ # The password for the associated :user as defined by the AMQP server.
54
+ # * :vhost => String (default '/')
55
+ # The virtual host as defined by the AMQP server.
56
+ # * :timeout => Numeric (default nil)
57
+ # Measured in seconds.
58
+ # * :logging => true | false (default false)
59
+ # Toggle the extremely verbose logging of all protocol communications
60
+ # between the client and the server. Extremely useful for debugging.
61
+ #
62
+ # AMQP.start do
63
+ # # default is to connect to localhost:5672
64
+ #
65
+ # # define queues, exchanges and bindings here.
66
+ # # also define all subscriptions and/or publishers
67
+ # # here.
68
+ #
69
+ # # this block never exits unless EM.stop_event_loop
70
+ # # is called.
71
+ # end
72
+ #
73
+ # Most code will use the MQ api. Any calls to MQ.direct / MQ.fanout /
74
+ # MQ.topic / MQ.queue will implicitly call #start. In those cases,
75
+ # it is sufficient to put your code inside of an EventMachine.run
76
+ # block. See the code examples in MQ for details.
77
+ #
78
+ def self.start *args, &blk
79
+ EM.run{
80
+ @conn ||= connect *args
81
+ @conn.callback(&blk) if blk
82
+ @conn
83
+ }
84
+ end
85
+
86
+ class << self
87
+ alias :run :start
88
+ end
89
+
90
+ def self.stop
91
+ if @conn and not @closing
92
+ @closing = true
93
+ @conn.close{
94
+ yield if block_given?
95
+ @conn = nil
96
+ @closing = false
97
+ }
98
+ end
99
+ end
100
+
101
+ def self.fork workers
102
+ EM.fork(workers) do
103
+ # clean up globals in the fork
104
+ Thread.current[:mq] = nil
105
+ AMQP.instance_variable_set('@conn', nil)
106
+
107
+ yield
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,395 @@
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 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.bytesize
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.bytesize, 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.bytesize, 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.bytesize
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 @pos + size > length
252
+ raise Overflow
253
+ else
254
+ data = @data[@pos,size]
255
+ @data[@pos,size] = ''
256
+ if pack
257
+ data = data.unpack(pack)
258
+ data = data.pop if data.size == 1
259
+ end
260
+ data
261
+ end
262
+ end
263
+
264
+ def _write data, pack = nil
265
+ data = [*data].pack(pack) if pack
266
+ @data[@pos,0] = data
267
+ @pos += data.bytesize
268
+ end
269
+ end
270
+ end
271
+
272
+ if $0 =~ /bacon/ or $0 == __FILE__
273
+ require 'bacon'
274
+ include AMQP
275
+
276
+ describe Buffer do
277
+ before do
278
+ @buf = Buffer.new
279
+ end
280
+
281
+ should 'have contents' do
282
+ @buf.contents.should == ''
283
+ end
284
+
285
+ should 'initialize with data' do
286
+ @buf = Buffer.new('abc')
287
+ @buf.contents.should == 'abc'
288
+ end
289
+
290
+ should 'append raw data' do
291
+ @buf << 'abc'
292
+ @buf << 'def'
293
+ @buf.contents.should == 'abcdef'
294
+ end
295
+
296
+ should 'append other buffers' do
297
+ @buf << Buffer.new('abc')
298
+ @buf.data.should == 'abc'
299
+ end
300
+
301
+ should 'have a position' do
302
+ @buf.pos.should == 0
303
+ end
304
+
305
+ should 'have a length' do
306
+ @buf.length.should == 0
307
+ @buf << 'abc'
308
+ @buf.length.should == 3
309
+ end
310
+
311
+ should 'know the end' do
312
+ @buf.empty?.should == true
313
+ end
314
+
315
+ should 'read and write data' do
316
+ @buf._write('abc')
317
+ @buf.rewind
318
+ @buf._read(2).should == 'ab'
319
+ @buf._read(1).should == 'c'
320
+ end
321
+
322
+ should 'raise on overflow' do
323
+ lambda{ @buf._read(1) }.should.raise Buffer::Overflow
324
+ end
325
+
326
+ should 'raise on invalid types' do
327
+ lambda{ @buf.read(:junk) }.should.raise Buffer::InvalidType
328
+ lambda{ @buf.write(:junk, 1) }.should.raise Buffer::InvalidType
329
+ end
330
+
331
+ { :octet => 0b10101010,
332
+ :short => 100,
333
+ :long => 100_000_000,
334
+ :longlong => 666_555_444_333_222_111,
335
+ :shortstr => 'hello',
336
+ :longstr => 'bye'*500,
337
+ :timestamp => time = Time.at(Time.now.to_i),
338
+ :table => { :this => 'is', :a => 'hash', :with => {:nested => 123, :and => time, :also => 123.456} },
339
+ :bit => true
340
+ }.each do |type, value|
341
+
342
+ should "read and write a #{type}" do
343
+ @buf.write(type, value)
344
+ @buf.rewind
345
+ @buf.read(type).should == value
346
+ @buf.should.be.empty
347
+ end
348
+
349
+ end
350
+
351
+ should 'read and write multiple bits' do
352
+ bits = [true, false, false, true, true, false, false, true, true, false]
353
+ @buf.write(:bit, bits)
354
+ @buf.write(:octet, 100)
355
+
356
+ @buf.rewind
357
+
358
+ bits.map do
359
+ @buf.read(:bit)
360
+ end.should == bits
361
+ @buf.read(:octet).should == 100
362
+ end
363
+
364
+ should 'read and write properties' do
365
+ properties = ([
366
+ [:octet, 1],
367
+ [:shortstr, 'abc'],
368
+ [:bit, true],
369
+ [:bit, false],
370
+ [:shortstr, nil],
371
+ [:timestamp, nil],
372
+ [:table, { :a => 'hash' }],
373
+ ]*5).sort_by{rand}
374
+
375
+ @buf.write(:properties, properties)
376
+ @buf.rewind
377
+ @buf.read(:properties, *properties.map{|type,_| type }).should == properties.map{|_,value| value }
378
+ @buf.should.be.empty
379
+ end
380
+
381
+ should 'do transactional reads with #extract' do
382
+ @buf.write :octet, 8
383
+ orig = @buf.to_s
384
+
385
+ @buf.rewind
386
+ @buf.extract do |b|
387
+ b.read :octet
388
+ b.read :short
389
+ end
390
+
391
+ @buf.pos.should == 0
392
+ @buf.data.should == orig
393
+ end
394
+ end
395
+ end