right_amqp 0.2.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.
@@ -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
@@ -0,0 +1,282 @@
1
+ require 'right_support'
2
+ require File.expand_path('../frame', __FILE__)
3
+
4
+ module AMQP
5
+ class Error < StandardError; end
6
+
7
+ module BasicClient
8
+ def process_frame frame
9
+ if mq = channels[frame.channel]
10
+ mq.process_frame(frame)
11
+ return
12
+ end
13
+
14
+ case frame
15
+ when Frame::Method
16
+ case method = frame.payload
17
+ when Protocol::Connection::Start
18
+ send Protocol::Connection::StartOk.new({:platform => 'Ruby/EventMachine',
19
+ :product => 'AMQP',
20
+ :information => 'http://github.com/tmm1/amqp',
21
+ :version => VERSION},
22
+ 'AMQPLAIN',
23
+ {:LOGIN => @settings[:user],
24
+ :PASSWORD => @settings[:pass]},
25
+ 'en_US')
26
+
27
+ when Protocol::Connection::Tune
28
+ send Protocol::Connection::TuneOk.new(:channel_max => 0,
29
+ :frame_max => 131072,
30
+ :heartbeat => @settings[:heartbeat] || 0)
31
+
32
+ send Protocol::Connection::Open.new(:virtual_host => @settings[:vhost],
33
+ :capabilities => '',
34
+ :insist => @settings[:insist])
35
+
36
+ when Protocol::Connection::OpenOk
37
+ logger.debug("[amqp] Received open completion from broker #{@settings[:identity]}")
38
+ succeed(self)
39
+
40
+ when Protocol::Connection::Close
41
+ # raise Error, "#{method.reply_text} in #{Protocol.classes[method.class_id].methods[method.method_id]}"
42
+ STDERR.puts "#{method.reply_text} in #{Protocol.classes[method.class_id].methods[method.method_id]}"
43
+
44
+ when Protocol::Connection::CloseOk
45
+ logger.debug("[amqp] Received close completion from broker #{@settings[:identity]}")
46
+ @on_disconnect.call if @on_disconnect
47
+ end
48
+
49
+ when Frame::Heartbeat
50
+ logger.debug("[amqp] Received heartbeat from broker #{@settings[:identity]}")
51
+ @last_server_heartbeat = Time.now
52
+
53
+ end
54
+
55
+ # Make callback now that handshake with the broker has completed
56
+ # The 'connected' status callback happens before the handshake is done and if it results in
57
+ # a lot of activity it might prevent EM from being able to call the code handling the
58
+ # incoming handshake packet in a timely fashion causing the broker to close the connection
59
+ @connection_status.call(:ready) if @connection_status && frame.payload.is_a?(AMQP::Protocol::Connection::Start)
60
+ end
61
+ end
62
+
63
+ def self.client
64
+ @client ||= BasicClient
65
+ end
66
+
67
+ def self.client= mod
68
+ mod.__send__ :include, AMQP
69
+ @client = mod
70
+ end
71
+
72
+ module Client
73
+ include EM::Deferrable
74
+ include RightSupport::Log::Mixin
75
+
76
+ def self.included(base)
77
+ base.extend(RightSupport::Log::Mixin::ClassMethods)
78
+ end
79
+
80
+ def initialize opts = {}
81
+ @settings = opts
82
+ extend AMQP.client
83
+
84
+ @on_disconnect ||= proc{ @connection_status.call(:failed) if @connection_status }
85
+
86
+ timeout @settings[:timeout] if @settings[:timeout]
87
+ errback{ @on_disconnect.call } unless @reconnecting
88
+
89
+ @connected = false
90
+ end
91
+
92
+ def connection_completed
93
+ start_tls if @settings[:ssl]
94
+ log 'connected'
95
+ # @on_disconnect = proc{ raise Error, 'Disconnected from server' }
96
+ unless @closing
97
+ @on_disconnect = method(:disconnected)
98
+ @reconnecting = false
99
+ end
100
+
101
+ @connected = true
102
+ @connection_status.call(:connected) if @connection_status
103
+
104
+ @buf = Buffer.new
105
+ send_data HEADER
106
+ send_data [1, 1, VERSION_MAJOR, VERSION_MINOR].pack('C4')
107
+
108
+ if heartbeat = @settings[:heartbeat]
109
+ init_heartbeat if (@settings[:heartbeat] = heartbeat.to_i) > 0
110
+ end
111
+ end
112
+
113
+ def init_heartbeat
114
+ logger.debug("[amqp] Initializing heartbeat for broker #{@settings[:identity]} to #{@settings[:heartbeat]}")
115
+ @last_server_heartbeat = Time.now
116
+
117
+ @timer.cancel if @timer
118
+ @timer = EM::PeriodicTimer.new(@settings[:heartbeat]) do
119
+ if connected?
120
+ if @last_server_heartbeat < (Time.now - (@settings[:heartbeat] * 2))
121
+ log "Reconnecting due to missing server heartbeats"
122
+ logger.info("[amqp] Reconnecting to broker #{@settings[:identity]} due to missing server heartbeats")
123
+ reconnect(true)
124
+ else
125
+ logger.debug("[amqp] Sending heartbeat to broker #{@settings[:identity]}")
126
+ @last_server_heartbeat = Time.now
127
+ send AMQP::Frame::Heartbeat.new, :channel => 0
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ def connected?
134
+ @connected
135
+ end
136
+
137
+ def unbind
138
+ log 'disconnected'
139
+ @connected = false
140
+ EM.next_tick{ @on_disconnect.call }
141
+ end
142
+
143
+ def add_channel mq
144
+ (@_channel_mutex ||= Mutex.new).synchronize do
145
+ channels[ key = (channels.keys.max || 0) + 1 ] = mq
146
+ key
147
+ end
148
+ end
149
+
150
+ def channels
151
+ @channels ||= {}
152
+ end
153
+
154
+ # Catch exceptions that would otherwise cause EM to stop or be in a bad
155
+ # state if a top level EM error handler was setup. Instead close the connection and leave EM
156
+ # alone.
157
+ # Don't log an error if the environment variable IGNORE_AMQP_FAILURES is set (used in the
158
+ # enroll script)
159
+ def receive_data data
160
+ begin
161
+ # log 'receive_data', data
162
+ @buf << data
163
+
164
+ while frame = Frame.parse(@buf)
165
+ log 'receive', frame
166
+ process_frame frame
167
+ end
168
+ rescue Exception => e
169
+ logger.exception("[amqp] Failed processing frame, closing connection", e, :trace) unless ENV['IGNORE_AMQP_FAILURES']
170
+ failed
171
+ end
172
+ end
173
+
174
+ def process_frame frame
175
+ # this is a stub meant to be
176
+ # replaced by the module passed into initialize
177
+ end
178
+
179
+ def send data, opts = {}
180
+ channel = opts[:channel] ||= 0
181
+ data = data.to_frame(channel) unless data.is_a? Frame
182
+ data.channel = channel
183
+
184
+ log 'send', data
185
+ send_data data.to_s
186
+ end
187
+
188
+ #:stopdoc:
189
+ # def send_data data
190
+ # log 'send_data', data
191
+ # super
192
+ # end
193
+ #:startdoc:
194
+
195
+ def close &on_disconnect
196
+ if on_disconnect
197
+ @closing = true
198
+ @on_disconnect = proc{
199
+ on_disconnect.call
200
+ @closing = false
201
+ }
202
+ end
203
+
204
+ callback{ |c|
205
+ if c.channels.any?
206
+ c.channels.each do |ch, mq|
207
+ mq.close
208
+ end
209
+ else
210
+ send Protocol::Connection::Close.new(:reply_code => 200,
211
+ :reply_text => 'Goodbye',
212
+ :class_id => 0,
213
+ :method_id => 0)
214
+ end
215
+ }
216
+ end
217
+
218
+ def reconnect force = false
219
+ if @reconnecting and not force
220
+ # Wait after first reconnect attempt and in between each subsequent attempt
221
+ EM.add_timer(@settings[:reconnect_interval] || 5) { reconnect(true) }
222
+ return
223
+ end
224
+
225
+ unless @reconnecting
226
+ @deferred_status = nil
227
+ initialize(@settings)
228
+
229
+ mqs = @channels
230
+ @channels = {}
231
+ mqs.each{ |_,mq| mq.reset } if mqs
232
+
233
+ @reconnecting = true
234
+
235
+ again = @settings[:reconnect_delay]
236
+ again = again.call if again.is_a?(Proc)
237
+ if again.is_a?(Numeric)
238
+ # Wait before making initial reconnect attempt
239
+ EM.add_timer(again) { reconnect(true) }
240
+ return
241
+ elsif ![nil, true].include?(again)
242
+ raise ::AMQP::Error, "Could not interpret :reconnect_delay => #{again.inspect}; expected nil, true, or Numeric"
243
+ end
244
+ end
245
+
246
+ log 'reconnecting'
247
+ logger.info("[amqp] Attempting to reconnect to #{@settings[:identity]}")
248
+ EM.reconnect(@settings[:host], @settings[:port], self)
249
+ end
250
+
251
+ def self.connect opts = {}
252
+ opts = AMQP.settings.merge(opts)
253
+ EM.connect opts[:host], opts[:port], self, opts
254
+ end
255
+
256
+ def connection_status &blk
257
+ @connection_status = blk
258
+ end
259
+
260
+ def failed
261
+ @connection_status.call(:failed) if @connection_status
262
+ @failed = true
263
+ close_connection
264
+ end
265
+
266
+ private
267
+
268
+ def disconnected
269
+ unless @failed
270
+ @connection_status.call(:disconnected) if @connection_status
271
+ reconnect
272
+ end
273
+ end
274
+
275
+ def log *args
276
+ return unless @settings[:logging] or AMQP.logging
277
+ require 'pp'
278
+ pp args
279
+ puts
280
+ end
281
+ end
282
+ end