right_amqp 0.2.0

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